<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>lujun</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://lujun.pages.dev/</id>
  <link href="https://lujun.pages.dev/" rel="alternate"/>
  <link href="https://lujun.pages.dev/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, lujun</rights>
  <title>卢俊的Blog</title>
  <updated>2026-06-22T14:03:58.804Z</updated>
  <entry>
    <author>
      <name>lujun</name>
    </author>
    <category term="Unity" scheme="https://lujun.pages.dev/tags/Unity/"/>
    <content>
      <![CDATA[<script src="/assets/js/APlayer.min.js"> </script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p><code>UnityEngine.Object</code> 类作为 Unity 中所有内建对象的基类，可以在 Unity 中被任意引用。继承自 <code>System.Object</code>，并且重载了 <code>==</code>、<code>!=</code> 和 <code>bool</code> 几个特殊的操作符。</p><span id="more"></span><h1 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h1><p>问题从一行错误日志开始。在最近的一个线上项目中收到这样一条错误上报:</p><blockquote><p>MissingReferenceException: The object of type ‘GameObject’ has been destroyed but you are still trying to access it.<br>Your script should either check if it is null or you should not destroy the object.</p></blockquote><p>定位到具体代码，大致如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Transform transform = gameObject?.transform;</span><br></pre></td></tr></table></figure><p>当前 GameObject 已经被销毁仍然尝试读取其 transform，但是这里明明已经使用 <code>?.</code> 操作符判定了，为什么还会出现这个问题？下面尝试模拟这种情况。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">GameObject gameObject = <span class="keyword">new</span> <span class="built_in">GameObject</span>(<span class="string">&quot;go&quot;</span>);</span><br><span class="line"><span class="built_in">DestroyImmediate</span>(gameObject);</span><br><span class="line"></span><br><span class="line">Transform transform = gameObject?.transform;</span><br></pre></td></tr></table></figure><p>运行上面的代码，确实抛出了一样的错误异常。难道是 <code>?.</code> 操作符的问题？下面我们换一种方式来 check null，如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">GameObject gameObject = <span class="keyword">new</span> <span class="built_in">GameObject</span>(<span class="string">&quot;go&quot;</span>);</span><br><span class="line"><span class="built_in">DestroyImmediate</span>(gameObject);</span><br><span class="line"></span><br><span class="line">Transform transform;</span><br><span class="line"><span class="keyword">if</span> (gameObject != null)</span><br><span class="line">&#123;</span><br><span class="line">    transform = gameObject.transform;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>运行这一段代码，没有错误异常抛出，说明 check null 成功。为什么 <code>?.</code> 操作符会检验失败？带着疑问来深入看一下编译器为我们生成的部分 CIL 代码。</p><h1 id="探究"><a href="#探究" class="headerlink" title="探究"></a>探究</h1><p>在探究之前，首先补充一段关于 UnityEngine.Object 知识。在 Unity 中，所有实际的 UnityEngine.Object 的数据均存储在一段本地原生的对象空间中，与 CLR 无关；在 CLR 层中的 UnityEngine.Object 对象实际上是一个指向本地原生对象的指针，这类对象又被称为 “wrapper objects”。</p><p>原生对象的生命周期由 Unity 管理，在加载新场景、显式调用 <code>Destroy</code> 方法(当前帧的某个时刻执行)或调用 <code>DestroyImmediate</code> 方法(立即执行)时原生对象会被销毁。C# 中的 UnityEngine.Object 对象的销毁回收由 CLR 决定。因此就可能出现一种情况: 本地原生对象已经被销毁，但是由于 CLR GC 未发生，导致 C# 中的 UnityEngine.Object 对象未被回收。</p><p>了解了这个知识点，下面开始探究问题所在。</p><p>看看第一段使用了 <code>?.</code> 操作符生成的的 CIL 代码指令:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">.<span class="function">locals <span class="title">init</span> <span class="params">(</span></span></span><br><span class="line"><span class="params"><span class="function">    [<span class="number">0</span>] <span class="keyword">class</span> [UnityEngine.CoreModule]UnityEngine.GameObject gameObject,</span></span></span><br><span class="line"><span class="params"><span class="function">    [<span class="number">1</span>] <span class="keyword">class</span> [UnityEngine.CoreModule]UnityEngine.Transform transform</span></span></span><br><span class="line"><span class="params"><span class="function">)</span></span></span><br><span class="line"><span class="function">IL_0001: ldstr <span class="string">&quot;go&quot;</span></span></span><br><span class="line"><span class="function">IL_0006: newobj instance void [UnityEngine.CoreModule]UnityEngine.GameObject::.ctor(string)</span></span><br><span class="line"><span class="function">IL_000b: stloc<span class="number">.0</span></span></span><br><span class="line"><span class="function">IL_000c: ldloc<span class="number">.0</span></span></span><br><span class="line"><span class="function">IL_000d: call void [UnityEngine.CoreModule]UnityEngine.Object::DestroyImmediate(class [UnityEngine.CoreModule]UnityEngine.Object)</span></span><br><span class="line"><span class="function"></span></span><br><span class="line"><span class="function">// check null 部分指令</span></span><br><span class="line"><span class="function">IL_0013: ldloc<span class="number">.0</span></span></span><br><span class="line"><span class="function">IL_0014: brtrue.s IL_0019</span></span><br><span class="line"><span class="function"></span></span><br><span class="line"><span class="function">IL_0016: ldnull</span></span><br><span class="line"><span class="function">IL_0017: br.s IL_001f</span></span><br><span class="line"><span class="function"></span></span><br><span class="line"><span class="function">IL_0019: ldloc<span class="number">.0</span></span></span><br><span class="line"><span class="function">IL_001a: call instance class [UnityEngine.CoreModule]UnityEngine.Transform [UnityEngine.CoreModule]UnityEngine.GameObject::get_transform()</span></span><br><span class="line"><span class="function"></span></span><br><span class="line"><span class="function">IL_001f: stloc<span class="number">.1</span></span></span><br><span class="line"><span class="function">IL_0020: ret</span></span><br></pre></td></tr></table></figure><p>上面生成的 CIL 指令，从 <code>IL_0001</code> 到 <code>IL_000d</code> 主要是生成新的 GameObject(存在于 Managed Heap 中) 并将其引用存入 Record frame 的第 0 个变量处，然后调用 <code>DestroyImmediate</code> 方法销毁这个 GameObject。</p><p>接着就是 <code>?.</code> 操作符部分的 CIL 指令(从 <code>IL_0013</code> 开始)。首先加载当前 Record frame 中的第一个变量值(这里是 GameObject 的引用)到 Evaluation stack 中，使用 <code>brtrue.s</code> 判断这个引用的对象是否为空，不为空则跳转到 <code>IL_0019</code> 处开始调用 <code>get_transform</code> 方法，为空则返回 null。这样看下来 <code>?.</code> 操作符就是判断了当前 GameObject 引用对象是否为空，和普通 System.Object 对象 <code>!= null</code> 处理类似。</p><p>那么绕过空检测出现错误异常的原因也就知道了。当我们调用 <code>DestroyImmediate</code> 方法销毁一个 GameObject 对象的时候，此时在底层对应的原生对象是被销毁了，但是其存在于 CLR 层的引用对象(方法结束后对象临时引用变量被释放销毁，存放于 Managed heap 中的 GameObject 对象等待 GC 释放)并未被销毁，若使用 <code>?.</code> 操作符在 CLR 层面判断不为空，跳转到 <code>get_transform</code> 方法所在指令附近获取 transform，由于底层的 GameObject 已经被销毁所以底层抛出 ‘MissingReferenceException’ 异常。</p><p>那么为什么使用常规 <code>!= null</code> 形式判空没出现这个问题了，再来看看这种方式生成的 CIL 代码:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">.<span class="function">locals <span class="title">init</span> <span class="params">(</span></span></span><br><span class="line"><span class="params"><span class="function">    [<span class="number">0</span>] <span class="keyword">class</span> [UnityEngine.CoreModule]UnityEngine.GameObject gameObject,</span></span></span><br><span class="line"><span class="params"><span class="function">    [<span class="number">1</span>] <span class="keyword">class</span> [UnityEngine.CoreModule]UnityEngine.Transform transform,</span></span></span><br><span class="line"><span class="params"><span class="function">    [<span class="number">2</span>] <span class="type">bool</span></span></span></span><br><span class="line"><span class="params"><span class="function">)</span></span></span><br><span class="line"><span class="function"><span class="comment">// 生成 GameObject 以及销毁 GameObject ...</span></span></span><br><span class="line"><span class="function"></span></span><br><span class="line"><span class="function"><span class="comment">// check null 部分指令</span></span></span><br><span class="line"><span class="function">IL_0013: ldloc<span class="number">.0</span></span></span><br><span class="line"><span class="function">IL_0014: ldnull</span></span><br><span class="line"><span class="function">IL_0015: call bool [UnityEngine.CoreModule]UnityEngine.Object::op_Inequality(class [UnityEngine.CoreModule]UnityEngine.Object, class [UnityEngine.CoreModule]UnityEngine.Object)</span></span><br><span class="line"><span class="function">IL_001a: stloc<span class="number">.2</span></span></span><br><span class="line"><span class="function">IL_001b: ldloc<span class="number">.2</span></span></span><br><span class="line"><span class="function">IL_001c: brfalse.s IL_0027</span></span><br><span class="line"><span class="function"></span></span><br><span class="line"><span class="function">IL_001f: ldloc<span class="number">.0</span></span></span><br><span class="line"><span class="function">IL_0020: callvirt instance class [UnityEngine.CoreModule]UnityEngine.Transform [UnityEngine.CoreModule]UnityEngine.GameObject::get_transform()</span></span><br><span class="line"><span class="function">IL_0025: stloc<span class="number">.1</span></span></span><br><span class="line"><span class="function"></span></span><br><span class="line"><span class="function">IL_0027: ret</span></span><br></pre></td></tr></table></figure><p>前面的指令基本都相同，我们主要看看判空部分。首先加载了生成的 GameObject 以及 null，然后在 <code>IL_0015</code> 处<strong>调用了 UnityEngine.Object 类重载的 <code>op_Inequality</code> 操作符(operator !&#x3D;)判断 GameObject 是否不为空</strong>，如果判定为空则跳转到 <code>IL_0027</code> 指令结束方法，否则从 <code>IL_001a</code> 处继续执行指令获取 transform。下面就来重点解读 UnityEngine.Object 重载的 <code>!=</code> 操作符的实现部分。</p><p>首先看看源码:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="type">static</span> <span class="type">bool</span> <span class="keyword">operator</span> !=(Object x, Object y)</span><br><span class="line">&#123;    </span><br><span class="line">    <span class="keyword">return</span> !Object.<span class="built_in">CompareBaseObjects</span>(x, y);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>具体的实现在 <code>CompareBaseObjects</code> 方法:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="type">static</span> <span class="type">bool</span> <span class="title">CompareBaseObjects</span><span class="params">(Object lhs, Object rhs)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="type">bool</span> flag1 = (object) lhs == null;</span><br><span class="line">    <span class="type">bool</span> flag2 = (object) rhs == null;</span><br><span class="line">    <span class="keyword">if</span> (flag2 &amp;&amp; flag1)</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">    <span class="keyword">if</span> (flag2)</span><br><span class="line">        <span class="keyword">return</span> !Object.<span class="built_in">IsNativeObjectAlive</span>(lhs);</span><br><span class="line">    <span class="keyword">if</span> (flag1)</span><br><span class="line">        <span class="keyword">return</span> !Object.<span class="built_in">IsNativeObjectAlive</span>(rhs);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> lhs.m_InstanceID == rhs.m_InstanceID;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面的方法用来比较两个 UnityEngine.Object 对象是否相等，lhs 和 rhs 分别代表操作符两边的参数:</p><ul><li><p>若两边参数在 CLR 层均为 null，判定相等；</p></li><li><p>若右边参数在 CLR 层为 null，左边不为 null，根据左边参数 CLR 层所在对象对应的底层原生对象是否 Alive 返回比较结果；</p></li><li><p>若左边参数在 CLR 层为 null，右边不为 null，根据右边参数 CLR 层所在对象对应的底层原生对象是否 Alive 返回比较结果；</p></li><li><p>否则左右两边对象在 CLR 层对象均不为 null，比较它们的 <code>m_InstanceID</code> 是否相等。</p></li></ul><p>在我们的测试代码中分别是 GameObject 对象和 null。通过前面的分析指导，调用 <code>DestroyImmediate</code> 方法销毁一个 GameObject 对象时仅底层对应的原生对象是被销毁了，但是其存在于 CLR 层的引用对象未被销毁；所以上面方法中 <code>flag1</code> 为 false，<code>flag2</code> 为 true，最终返回结果由 UnityEngine.Object 类的 <code>IsNativeObjectAlive</code> 方法决定，这个方法正是判断 CLR 层所在 GameObject 对应的原生对象是否处于 Alive 状态。</p><p>在测试代码中，调用了 <code>DestroyImmediate</code> 方法销毁了原生的对象，最终 <code>CompareBaseObjects</code> 方法返回 true，重载的 <code>!=</code> 操作符返回 false。因此 <code>get_transform</code> 方法所在的指令不会被调用，从而不会出现错误异常。</p><h1 id="和-bool-操作符"><a href="#和-bool-操作符" class="headerlink" title="!&#x3D; 和 bool 操作符"></a>!&#x3D; 和 bool 操作符</h1><p><code>!=</code> 操作符同重载的 <code>==</code> 操作符逻辑刚好相反；<code>bool</code> 操作符具体实现也是调用 <code>CompareBaseObjects</code> 方法:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="type">static</span> implicit <span class="keyword">operator</span> <span class="title">bool</span><span class="params">(Object exists)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">return</span> !Object.<span class="built_in">CompareBaseObjects</span>(exists, (Object) null);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>从上面源码可以看出 <code>bool</code> 操作符内部同样使用了 <code>CompareBaseObjects</code> 方法将判定对象与 null 比较，如果判定对象为 null 返回 false，否则返回 true。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>到这里应该清楚某些情况下出现错误异常的原因了，在 <a href="https://docs.unity3d.com/ScriptReference/Object.html">Unity 文档</a>中关于 Object 有下面这样一句提及:</p><blockquote><p>This class doesn’t support the null-conditional operator (?.) and the null-coalescing operator (??).</p></blockquote><p>UnityEngine.Object 类重载了 <code>==</code>、<code>!=</code> 以及 <code>bool</code> 操作符，对于这几类操作 Unity 会结合 CLR 层的对象以及其底层对应原生对象来得到比较结果。</p><p>所以对于 UnityEngine.Object 类(子孙类)类型相关变量使用 <code>?.</code> 和 <code>??</code> 操作符一定要谨慎，有时显式的使用 <code>null</code> 判断或 <code>bool</code> 重载符对 UnityEngine.Object 往往更“安全”。</p><h1 id="关于-MonoBehaviour-可序列化的域"><a href="#关于-MonoBehaviour-可序列化的域" class="headerlink" title="关于 MonoBehaviour 可序列化的域"></a>关于 MonoBehaviour 可序列化的域</h1><p>对于 MonoBehaviour 可序列化的域，在 Editor 模式下就算这些域没有真正的被“赋值”，Unity 默认也会为其 CLR 层所在的 GameObject 对象默认设置上 “fake null” object (底层原生对象不会被赋值)，通过这样的小 track Unity 能够为开发者调试提供更多的有用信息。</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ul><li><p><a href="https://github.com/JetBrains/resharper-unity/wiki/Possible-unintended-bypass-of-lifetime-check-of-underlying-Unity-engine-object">Possible unintended bypass of lifetime check of underlying Unity engine object</a></p></li><li><p><a href="https://blogs.unity3d.com/2014/05/16/custom-operator-should-we-keep-it/">Custom &#x3D;&#x3D; operator, should we keep it?</a></p></li></ul>]]>
    </content>
    <id>https://lujun.pages.dev/2020/05/06/unity_object_operator/</id>
    <link href="https://lujun.pages.dev/2020/05/06/unity_object_operator/"/>
    <published>2020-05-06T00:02:29.000Z</published>
    <summary>
      <![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p><code>UnityEngine.Object</code> 类作为 Unity 中所有内建对象的基类，可以在 Unity 中被任意引用。继承自 <code>System.Object</code>，并且重载了 <code>==</code>、<code>!=</code> 和 <code>bool</code> 几个特殊的操作符。</p>]]>
    </summary>
    <title>深入 UnityEngine.Object 中重载的几个运算符</title>
    <updated>2026-06-22T14:03:58.804Z</updated>
  </entry>
  <entry>
    <author>
      <name>lujun</name>
    </author>
    <category term="Unity" scheme="https://lujun.pages.dev/tags/Unity/"/>
    <category term="CIL" scheme="https://lujun.pages.dev/tags/CIL/"/>
    <content>
      <![CDATA[<script src="/assets/js/APlayer.min.js"> </script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>CIL(Common Intermediate Language) 作为 C# 语言跨平台上重要的一环，通过对其进行“改造”往往实现一类自定的需求。</p><span id="more"></span><h1 id="C-泛型代码的-CIL-样貌"><a href="#C-泛型代码的-CIL-样貌" class="headerlink" title="C# 泛型代码的 CIL 样貌"></a>C# 泛型代码的 CIL 样貌</h1><p>定义泛型方法，编译后查看生成的 CIL 代码:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="type">void</span> <span class="built_in">GenericMethod_T</span>&lt;T&gt;(T input) &#123; &#125;</span><br></pre></td></tr></table></figure><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">.method public hidebysig </span><br><span class="line">instance void GenericMethod_T&lt;T&gt; (!!T input) cil managed </span><br><span class="line">&#123;</span><br><span class="line">    .maxstack 8</span><br><span class="line">    // CIL code...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对于方法的定义 CIL 代码和 C# 有很多相似的地方，CIL 代码中包含对于这个方法更完整的信息(比如这是一个实例方法在 CIL 中就使用了 <code>instance</code> 标记)。在 CIL 代码中泛型方法的泛型参数依旧是泛型的。</p><p>对于泛型类型约束，来看看 <code>new()</code> 和 <code>class</code> 的区别:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="type">void</span> <span class="built_in">GenericMethod_New</span>&lt;T&gt;(T input) where T : <span class="keyword">new</span>() &#123; &#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="type">void</span> <span class="built_in">GenericMethod_Class</span>&lt;T&gt;(T input) where T : <span class="keyword">class</span> &#123; &#125;</span><br></pre></td></tr></table></figure><p>限定了泛型类型的方法，其 CIL 代码如下:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">.method public hidebysig </span><br><span class="line">instance void GenericMethod_New&lt;.ctor T&gt; (!!T input) cil managed </span><br><span class="line">&#123;</span><br><span class="line">    .maxstack 8</span><br><span class="line">    // CIL code...</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">.method public hidebysig </span><br><span class="line">instance void GenericMethod_Class&lt;class T&gt; (!!T input) cil managed </span><br><span class="line">&#123;</span><br><span class="line">    .maxstack 8</span><br><span class="line">    // CIL code...</span><br><span class="line">&#125; </span><br></pre></td></tr></table></figure><p>泛型类型的约束在 CIL 中也体现了，即 <code>where T :</code> 被编译成 <code>&lt;constraint T&gt;</code>。</p><p>接下来看看对 <code>GenericMethod_T</code> 泛型方法调用的部分:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">GenericMethod_T</span>&lt;<span class="type">int</span>&gt;(<span class="number">1</span>);</span><br></pre></td></tr></table></figure><p>对应 CIL 调用指令:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">IL_0009: ldarg.0</span><br><span class="line">IL_000a: ldc.i4.1</span><br><span class="line">IL_000b: call instance void CILGeneric::GenericMethod_T&lt;int32&gt;(!!0)</span><br></pre></td></tr></table></figure><p><code>GenericMethod_T</code> 方法的调用被编译为 3 条指令，具体为:</p><ul><li><p><code>IL_0009: ldarg.0</code> - 加载方法第一个参数，这里是当前实例 <code>this</code>；</p></li><li><p><code>IL_000a: ldc.i4.1</code> - 加载 <code>int32</code> 类型数值 <code>1</code>；</p></li><li><p><code>IL_000b: call instance void CILGeneric::GenericMethod_T&lt;int32&gt;(!!0)</code> - 调用实例的 <code>GenericMethod_T</code> 方法，输入参数为 <code>1</code>。</p></li></ul><p>从 CIL 代码中可以看出，调用泛型方法时泛型类型已经被具体类型替换(这里被替换为 <code>int32</code>)，方法参数列表中第一个泛型类型参数类型对应为方法中的第一个泛型类型。</p><p>简单看了 CIL 中泛型的样貌，下面具体来看看如何来编织泛型 CIL 代码。</p><h1 id="编织-插桩"><a href="#编织-插桩" class="headerlink" title="编织 &amp; 插桩"></a>编织 &amp; 插桩</h1><p>在 Unity 中常使用 <a href="https://www.mono-project.com/docs/tools+libraries/libraries/Mono.Cecil/">Mono.Cecil</a> 来编织自定的 CIL 代码，实现一些“插桩”。</p><h2 id="编织「泛型方法」"><a href="#编织「泛型方法」" class="headerlink" title="编织「泛型方法」"></a>编织「泛型方法」</h2><p>编织泛型方法和编织普通方法类似，但是对于泛型需要做一些特殊的处理。</p><ol><li>首先使用 Mono.Cecil 编织一个普通方法，如下所示:</li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">MethodDefinition method = <span class="keyword">new</span> <span class="built_in">MethodDefinition</span>(<span class="string">&quot;GenericMethod_T&quot;</span>, </span><br><span class="line">    MethodAttributes.Public | MethodAttributes.HideBySig, </span><br><span class="line">    moduleDefinition.TypeSystem.Void);</span><br></pre></td></tr></table></figure><ol start="2"><li>定义一个泛型类型并给上面编织的方法添加这个泛型类型:</li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Define generic type param</span></span><br><span class="line">GenericParameter genericParameter = <span class="keyword">new</span> <span class="built_in">GenericParameter</span>(<span class="string">&quot;T&quot;</span>, method);</span><br><span class="line"><span class="comment">// Add generic param for method</span></span><br><span class="line">method.GenericParameters.<span class="built_in">Add</span>(genericParameter);</span><br></pre></td></tr></table></figure><ol start="3"><li>最后编织泛型输入参数:</li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Define param</span></span><br><span class="line">ParameterDefinition param = <span class="keyword">new</span> <span class="built_in">ParameterDefinition</span>(<span class="string">&quot;t&quot;</span>, </span><br><span class="line">    ParameterAttributes.None, genericParameter);</span><br><span class="line"></span><br><span class="line"><span class="comment">// Add param for method</span></span><br><span class="line">method.Parameters.<span class="built_in">Add</span>(param);</span><br></pre></td></tr></table></figure><p>通过上面几步就定义了泛型方法 <code>public void GenericMethod_T&lt;T&gt;(T input)</code> 的 CIL 实现。</p><h2 id="「泛型方法调用」编织"><a href="#「泛型方法调用」编织" class="headerlink" title="「泛型方法调用」编织"></a>「泛型方法调用」编织</h2><ol><li>再来看看如何编织泛型方法的调用过程。调用实例方法需要实例 <code>this</code> 的指针地址，因此需要加载当前实例:</li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ILProcessor.<span class="built_in">Emit</span>(OpCodes.Ldarg, <span class="number">0</span>);</span><br></pre></td></tr></table></figure><p>上面的编织就对应 <code>IL_0009: ldarg.0</code> 指令。</p><ol start="2"><li>上一步中编织的泛型方法需要传入一个参数，但是在传入这个参数之前我们必须指定泛型的具体类型:</li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// New &#x27;GenericInstanceMethod&#x27; for call, the &#x27;method&#x27; param is the </span></span><br><span class="line"><span class="comment">// method definition defined above</span></span><br><span class="line">GenericInstanceMethod genericMethod = <span class="keyword">new</span> <span class="built_in">GenericInstanceMethod</span>(method);</span><br><span class="line"><span class="comment">// Specify type for generic param</span></span><br><span class="line">genericMethod.GenericArguments.<span class="built_in">Add</span>(ModuleDefinition.<span class="built_in">ImportReference</span>(<span class="built_in">typeof</span>(<span class="type">int</span>)));</span><br></pre></td></tr></table></figure><p>上面的编织代码对应 <code>instance void CILGeneric::GenericMethod_T&lt;int32&gt;(!!0)</code>，这里指定了泛型的具体类型为 <code>int</code>。</p><ol start="3"><li>压入传递具体参数</li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ILProcessor.<span class="built_in">Emit</span>(OpCodes.Ldc_I4, <span class="number">1</span>);</span><br></pre></td></tr></table></figure><p>上面编织对应 <code>IL_000a: ldc.i4.1</code> 指令，将 <code>1</code> 传入 <code>GenericMethod_T</code> 方法。</p><ol start="4"><li>调用泛型方法</li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ILProcessor.<span class="built_in">Emit</span>(OpCodes.Call, genericMethod);</span><br></pre></td></tr></table></figure><p>上面编织对应 <code>IL_000b: call instance void CILGeneric::GenericMethod_T&lt;int32&gt;(!!0)</code> 指令。</p><p>通过上面的编织，就能成功的调用泛型方法 <code>void GenericMethod_T&lt;T&gt;(T input)</code>。在编织泛型方法的具体 CIL 调用指令时，一定要注意指定正确泛型的具体类型，这里泛型类型约束没有编译器负责检查。</p><h2 id="「初始化泛型的类成员变量」"><a href="#「初始化泛型的类成员变量」" class="headerlink" title="「初始化泛型的类成员变量」"></a>「初始化泛型的类成员变量」</h2><p>通过 Mono.Cecil 可以方便的获取到类的成员变量(以 <code>FieldDefinition</code> 存在)。现在尝试通过 CIL 自动初始化编织实现初始化成员变量。这里以常用的 <code>List&lt;T&gt;</code> 为例，假设有成员变量 <code>List&lt;string&gt; _list</code>，初始化代码应当如何编织了？</p><ol><li>首先成员变量属于当前实例，因此需要加载实例:</li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ILProcessor.<span class="built_in">Emit</span>(OpCodes.Ldarg, <span class="number">0</span>);</span><br></pre></td></tr></table></figure><ol start="2"><li>初始化一个类型对应调用其某个构造方法执行，因此可以获取 <code>List&lt;T&gt;</code> 的一个构造方法然后执行来达到目的。</li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">MethodReference listCtor = ModuleDefinition.<span class="built_in">ImportReference</span>(</span><br><span class="line">    <span class="built_in">typeof</span>(List&lt;string&gt;).<span class="built_in">GetConstructor</span>(<span class="keyword">new</span> Type[<span class="number">0</span>]));</span><br></pre></td></tr></table></figure><p>获取到构造方法后，使用 Mono.Cecil 的 <code>Newobj</code> 初始化:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ILProcessor.<span class="built_in">Emit</span>(OpCodes.Newobj, listCtor);</span><br></pre></td></tr></table></figure><p>上面的编织过程就可以初始化 <code>List&lt;string&gt; _list</code>。</p><h2 id="更通用的「初始化泛型的类成员变量」编织过程"><a href="#更通用的「初始化泛型的类成员变量」编织过程" class="headerlink" title="更通用的「初始化泛型的类成员变量」编织过程"></a>更通用的「初始化泛型的类成员变量」编织过程</h2><p>以上情况指明了 <code>List</code> 的具体泛型类型为 <code>string</code>，但是在 CIL 编织中这个“具体泛型类型”往往是未知的，比如现在想对当前类中的所有 <code>List&lt;T&gt;</code> 类型的成员变量初始化，可能包含 <code>List&lt;string&gt;、List&lt;int&gt;、List&lt;float&gt;</code> 等等，这种情况如何来编织通用的 CIL 了？</p><p>既然“具体泛型类型”未知，那么在获取 <code>List&lt;T&gt;</code> 构造方法时也就不用具体指定泛型类型了，如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">MethodReference listCtor = ModuleDefinition.<span class="built_in">ImportReference</span>(</span><br><span class="line">    <span class="built_in">typeof</span>(List&lt;&gt;).<span class="built_in">GetConstructor</span>(<span class="keyword">new</span> Type[<span class="number">0</span>]));</span><br></pre></td></tr></table></figure><p>上面获取到 <code>List&lt;T&gt;</code> 的一个泛型构造方法，要是能为其指定泛型类型就完整了。那这个具体泛型类型怎么获取了？答案就是从源头获取，因为不管是泛型成员变量定义还是泛型方法调用都会指定具体泛型类型。</p><p>下面看看成员变量 <code>List&lt;string&gt; _list</code>，如何获取其泛型类型 <code>string</code>。假设已经通过 Mono.Cecil 获取到了 <code>_list</code> 的 FieldDefinition 为 <code>listFieldDefinition</code>:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Get &#x27;List&lt;string&gt;&#x27; generic type</span></span><br><span class="line">GenericInstanceType fieldGeneric = </span><br><span class="line">    listFieldDefinition.FieldType as GenericInstanceType;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Get &#x27;List&lt;string&gt;&#x27; the first generic argument</span></span><br><span class="line">TypeReference listGenericType = fieldGeneric.GenericArguments[<span class="number">0</span>];</span><br></pre></td></tr></table></figure><p>通过上面的代码就能获取到 <code>_list</code> 成员变量泛型类型 <code>string</code> 的 TypeReference。然后将这个具体泛型类型指定给前面获取到的泛型构造方法的泛型参数:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">GenericInstanceType listCtorDeclaringGeneric = </span><br><span class="line">    listCtor.DeclaringType as GenericInstanceType;</span><br><span class="line">listCtorDeclaringGeneric.GenericArguments[<span class="number">0</span>] = listGenericType;</span><br></pre></td></tr></table></figure><p>执行初始化:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ILProcessor.<span class="built_in">Emit</span>(OpCodes.Newobj, listCtor);</span><br></pre></td></tr></table></figure><p>通过上面的编织，<code>List&lt;T&gt;</code> 任意的泛型类型都能满足初始化要求。</p>]]>
    </content>
    <id>https://lujun.pages.dev/2020/02/08/cil_emit_meet_generic/</id>
    <link href="https://lujun.pages.dev/2020/02/08/cil_emit_meet_generic/"/>
    <published>2020-02-08T00:02:29.000Z</published>
    <summary>
      <![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>CIL(Common Intermediate Language) 作为 C# 语言跨平台上重要的一环，通过对其进行“改造”往往实现一类自定的需求。</p>]]>
    </summary>
    <title>从 CIL 的编织看 C# 泛型</title>
    <updated>2026-06-22T14:03:58.804Z</updated>
  </entry>
  <entry>
    <author>
      <name>lujun</name>
    </author>
    <category term="Unity" scheme="https://lujun.pages.dev/tags/Unity/"/>
    <content>
      <![CDATA[<script src="/assets/js/APlayer.min.js"> </script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>作为研发的你工作中可能碰到过这样的场景：</p><blockquote><p>某天，策划同学 J 想到一个能给玩家带来愉悦感的「送钱特效」，这是一个三维场景中很多金币模型带有曲线的飞跃效果；渲染在屏幕上时，金币模型还被要求能从屏幕的指定位置飞到屏幕的另一指定位置。第二天，美术同学 S 在三维场景中调出了最好看的金币模型渲染效果，剩下的…</p></blockquote><p>当然剩下的就由作为研发的你来接手实现了。</p><span id="more"></span><h1 id="简单实现这个需求"><a href="#简单实现这个需求" class="headerlink" title="简单实现这个需求"></a>简单实现这个需求</h1><p>3D 模型渲染到已有的 Unity UI 上面，使用 Camera、Render Texture 和 Raw Image 来实现；脚本控制模型运动效果，Camera 来拍摄这些效果并渲染到 Render Texture 上，再利用 Raw Image 将 Render Texture 在 Unity UI 层展示出来。</p><p>通过上面操作，已经能将「送钱特效」渲染到屏幕上了。但是好像有一点被忽略了，如何保证「送钱特效」中金币飞跃的起始位置了？金币模型是在三维场景(世界空间)中，而屏幕上的位置属于二维场景(屏幕空间)，必须得将它们联系起来。</p><h1 id="Screen-Space-屏幕空间-到-World-Space-世界空间"><a href="#Screen-Space-屏幕空间-到-World-Space-世界空间" class="headerlink" title="Screen Space (屏幕空间)到 World Space (世界空间)"></a>Screen Space (屏幕空间)到 World Space (世界空间)</h1><p>在谈转换之前，首先需要了解几个知识点:</p><ol><li><p>当没有相机渲染 Canvas (Canvas 的 Render Mode 设置为 <code>Screen Space - Overlay</code> 或 <code>Screen Space - Camera 且不设置相机</code>)时:</p><ul><li><p>Canvas 左下角位于世界空间下的原点处(Canvas 此模式下世界空间下 Z 坐标为 0)；</p></li><li><p>位于 Canvas 中的 RectTransform 世界坐标就是其所在的屏幕坐标(X 和 Y 方向)，Z 方向世界坐标是其相对于父元素世界坐标偏移 localPosition.z (或 Pos Z)的值；</p></li></ul></li><li><p>Canvas 的 Render Mode 设置为 <code>Screen Space - Camera 并设置相机</code>)时:</p><ul><li><p>Unity 中一个 UI 单位<strong>不再是对应</strong>一个 Unity 单位；</p></li><li><p>Canvas 的 Pivot 处 X 和 Y 方向世界坐标和渲染 Camera X 和 Y 方向世界坐标相等；</p></li><li><p>Canvas 所在平面 Z 方向世界坐标由其设置的 <code>Plane Distance</code> 值和渲染 Camera 的 Z 方向世界坐标共同决定；</p></li></ul></li><li><p>Canvas 的 Render Mode 设置为 <code>World Space</code> 时:</p><ul><li><p>此时会将 Unity UI 当做 3D 物体来渲染，Canvas 可以位于空间中任何位置；</p></li><li><p>位于 Canvas 中的 RectTransform 世界坐标即可根据 Canvas 位置以及其 localPosition 得出；</p></li></ul></li></ol><p>上面三种情况下，Unity 中 UI 单位与 Unity 单位对应关系受 RectTransform 祖先元素缩放影响，具体表达式为:</p><div class="math-display">\[PerUIUnitUnityUnit &#x3D; \sum_{n&#x3D;1}^N Parent(n).Scale\]</div><p>有了上面的知识点，再来看看 Screen Space 到 World Space 的转换。</p><h2 id="对于-Canvas-的-Render-Mode-为-Screen-Space-Overlay-或-Screen-Space-Camera-且不设置相机-这种模式，Canvas-下-RectTransform-的世界坐标表达式为"><a href="#对于-Canvas-的-Render-Mode-为-Screen-Space-Overlay-或-Screen-Space-Camera-且不设置相机-这种模式，Canvas-下-RectTransform-的世界坐标表达式为" class="headerlink" title="对于 Canvas 的 Render Mode 为 Screen Space - Overlay 或 Screen Space - Camera 且不设置相机 这种模式，Canvas 下 RectTransform 的世界坐标表达式为:"></a>对于 Canvas 的 Render Mode 为 <code>Screen Space - Overlay</code> 或 <code>Screen Space - Camera 且不设置相机</code> 这种模式，Canvas 下 RectTransform 的世界坐标表达式为:</h2><div class="math-display">\[\begin{eqnarray*}World.Position.x &amp; &#x3D; &amp; Screen.Position.x \\World.Position.y &amp; &#x3D; &amp; Screen.Position.y \\World.Position.z &amp; &#x3D; &amp; \sum_{n&#x3D;1}^N (Parent(n).LocalPosition.z \times PerUIUnitUnityUnit(n)) \\&amp;&amp; + Self.LocalPosition.z \times PerUIUnitUnityUnit(Self)\end{eqnarray*}\]</div><p>其中 <code>World.Position</code> 为 RectTransform 的世界坐标；<code>Screen.Position</code> 为 RectTransform 在屏幕上的坐标；<code>Parent(n).LocalPosition</code> 为 RectTransform 某一层祖先元素相对其父节点的坐标，<code>PerUIUnitUnityUnit(n)</code> 为这一层祖先元素一个 UI 单位对应的 Unity 单位的数目；<code>Self.LocalPosition</code> 是 RectTransform 自身相对父节点的坐标，<code>PerUIUnitUnityUnit(Self)</code> 为自身 RectTransform 一个 UI 单位对应的 Unity 单位的数目。</p><h2 id="对于-Canvas-的-Render-Mode-为-World-Space，Canvas-下-RectTransform-的世界坐标表达式为"><a href="#对于-Canvas-的-Render-Mode-为-World-Space，Canvas-下-RectTransform-的世界坐标表达式为" class="headerlink" title="对于 Canvas 的 Render Mode 为 World Space，Canvas 下 RectTransform 的世界坐标表达式为:"></a>对于 Canvas 的 Render Mode 为 <code>World Space</code>，Canvas 下 RectTransform 的世界坐标表达式为:</h2><div class="math-display">\[\begin{eqnarray*}World.Position.x &amp; &#x3D; &amp; \sum_{n&#x3D;1}^N (Parent(n).LocalPosition.x \times PerUIUnitUnityUnit(n)) \\&amp;&amp; + Self.LocalPosition.x \times PerUIUnitUnityUnit(Self) \\&amp;&amp; + Canvas.Position.x\end{eqnarray*}\]</div><div class="math-display">\[\begin{eqnarray}World.Position.y &amp; &#x3D; &amp; \sum_{n&#x3D;1}^N (Parent(n).LocalPosition.y \times PerUIUnitUnityUnit(n)) \\&amp;&amp; + Self.LocalPosition.y \times PerUIUnitUnityUnit(Self) \\&amp;&amp; + Canvas.Position.y\end{eqnarray}\]</div><div class="math-display">\[\begin{eqnarray*}World.Position.z &amp; &#x3D; &amp; \sum_{n&#x3D;1}^N (Parent(n).LocalPosition.z \times PerUIUnitUnityUnit(n)) \\&amp;&amp; + Self.LocalPosition.z \times PerUIUnitUnityUnit(Self) \\&amp;&amp; + Canvas.Position.z\end{eqnarray*}\]</div><p>其中 <code>World.Position</code> 为 RectTransform 的世界坐标；<code>Parent(n).LocalPosition</code> 为 RectTransform 某一层祖先元素相对其父节点的坐标，<code>PerUIUnitUnityUnit(n)</code> 为这一层祖先元素一个 UI 单位对应的 Unity 单位的数目；<code>Self.LocalPosition</code> 是 RectTransform 自身相对父节点的坐标，<code>PerUIUnitUnityUnit(Self)</code> 为自身 RectTransform 一个 UI 单位对应的 Unity 单位的数目；<code>Canvas.Position</code> 是 Canvas 在世界空间下的坐标。</p><h2 id="对于-Canvas-的-Render-Mode-为-Screen-Space-Camera-并设置相机-，这种情况相对复杂一点。"><a href="#对于-Canvas-的-Render-Mode-为-Screen-Space-Camera-并设置相机-，这种情况相对复杂一点。" class="headerlink" title="对于 Canvas 的 Render Mode 为 Screen Space - Camera 并设置相机)，这种情况相对复杂一点。"></a>对于 Canvas 的 Render Mode 为 <code>Screen Space - Camera 并设置相机</code>)，这种情况相对复杂一点。</h2><p>首先需要明白在这种情况下，为什么一个 Unity 单位不再固定对应一个 UI 单位。当 Canvas 的 Render Mode 设置为这种模式时，Camera 需要保证在其正前方 <code>Plane Distance</code> 距离处的视锥体切面大小能够完全吻合 Canvas 平面大小；有了这点限制，Camera 的 <code>Field Of View</code> 以及 Canvas 的 <code>Plane Distance</code> 都可以影响 Unity 单位和 UI 单位的对应关系。</p><p>下面就来找出 Canvas 在这种渲染模式下 Unity 单位和 UI 单位的对应关系，如下图:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_pixel_to_world_0.jpg" width="60%" height="60%" /></center><p>从上图可以看出，屏幕高度(UI 单位)和 <code>Plane Distance</code> 以及 <code>Field Of View</code> 之间存在三角函数关系，如下:</p><div class="math-display">\[tan(\frac{FOV}{2}) &#x3D; \frac{Screen Height \div 2}{Plane Distance}\]</div><p>一个 Unity 单位和 UI 单位关系表达式为:</p><div class="math-display">\[PerUnitPixels &#x3D; \frac{Screen Height \div 2}{Plane Distance \times tan(\frac{FOV}{2})}\]</div><p>从上面的关系表达式中可以知道: 当屏幕分辨率(Canvas 尺寸大小)和 Camera 的 <code>Field Of View</code> 都固定时，Canvas 的 <code>Plane Distance</code> 越大，一个 Unity 单位对应的 UI 单位数越少，反之越多；当屏幕分辨率(Canvas 尺寸大小)和 Canvas 的 <code>Plane Distance</code> 都固定时，Camera 的 <code>Field Of View</code> 越大，一个 Unity 单位对应的 UI 单位数越少，反之越多。</p><p>知道了 Unity 单位和 UI 单位关系表达式，就能够得到 Canvas 中 RectTransform 在世界空间下的坐标表达式了，如下图是一个 Button Camera 拍摄切面图:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_pixel_to_world_1.jpg" width="60%" height="60%" /></center><p>首先来计算 Canvas 的 Pivot 处的世界坐标，前面提到过这个坐标和 Camera 的世界坐标以及 <code>Plane Distance</code> 相关，具体表达式如下:</p><div class="math-display">\[CanvasPivot.Position.x &#x3D; Camera.Position.x\]</div><div class="math-display">\[CanvasPivot.Position.y &#x3D; Camera.Position.y\]</div><div class="math-display">\[CanvasPivot.Position.z &#x3D; Camera.Position.z + Plane Distance\]</div><p>Canvas 的 Pivot 世界坐标最终表达式:</p><div class="math-display">\[\begin{eqnarray*}CanvasPivot.Position &amp; &#x3D; &amp; (Camera.Position.x, Camera.Position.y, \\&amp;&amp; Camera.Position.z + Plane Distance)\end{eqnarray*}\]</div><p>有了 Canvas 的世界坐标计算表达式和 Unity 单位和 UI 单位的对应关系，Canvas 下 RectTransform 的世界坐标计算表达式也就可以方便得到:</p><div class="math-display">\[\begin{eqnarray*}RectTransform.Position.z &amp; &#x3D; &amp; \frac{\sum_{n&#x3D;1}^N Parent(n).LocalPosition.z + RectTransform.LocalPosition.z}{PerUnitPixels} \\&amp;&amp; + CanvasPivot.Position.z\end{eqnarray*}\]</div><p>其中 <code>CanvasPivot.Position</code> 为上面计算得到的 Canvas Pivot 的世界坐标，<code>Parent(n).LocalPosition</code> 为 RectTransform 第 n 个祖先节点相对其父元素的坐标，<code>RectTransform.LocalPosition</code> 是 RectTransform 自身相对父节点的坐标，<code>PerUnitPixels</code> 是前面得到的 Unity 单位和 UI 单位的对应关系表达式。</p><p>最后将 RectTransform 祖先元素的缩放影响添加到上面公式中得到最终 Z 方向的世界坐标表达式:</p><div class="math-display">\[\begin{eqnarray*}RectTransform.Position.z &amp; &#x3D; &amp;  (\sum_{n&#x3D;1}^N (Parent(n).LocalPosition.z \times PerUIUnitUnityUnit(n)) \\&amp;&amp; + RectTransform.LocalPosition.z \times PerUIUnitUnityUnit(RectTransform)) \\&amp;&amp; \div PerUnitPixels \\&amp;&amp; + CanvasPivot.Position.z\end{eqnarray*}\]</div><p>上面的表达式是 Z 方向上的世界坐标，同理也可以求得 X 和 Y 方向的表达式。</p><p>RectTransform 世界坐标最终表达式如下:</p><div class="math-display">\[\begin{eqnarray*}RectTransform.Position &amp; &#x3D; &amp; (\sum_{n&#x3D;1}^N (Parent(n).LocalPosition \times PerUIUnitUnityUnit(n)) \\&amp;&amp; + RectTransform.LocalPosition \times PerUIUnitUnityUnit(RectTransform)) \\&amp;&amp; \div PerUnitPixels \\&amp;&amp; + CanvasPivot.Position\end{eqnarray*}\]</div><p>下面使用上面的公式来计算 Canvas 下 Button 的世界坐标。Camera 世界坐标为 (0, 0, 0)、FOV 为 90，Canvas 设置 Plane Distance 100，屏幕分辨率为 (750 x 1334) 像素，Button 的 anchoredPosition(localPosition) 为 (0, 0, 100)，根据公式计算得到 Button 的世界坐标为 (0.0, 0.0, 115.0)。</p><p>为了验证这个结果的准确性，使用 RectTransformUtility 类的 <code>ScreenPointToWorldPointInRectangle</code> 方法根据同样的参数再来计算，代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">RectTransformUtility.<span class="built_in">ScreenPointToWorldPointInRectangle</span>(</span><br><span class="line">    button.<span class="built_in">GetComponent</span>&lt;RectTransform&gt;(),</span><br><span class="line">    <span class="keyword">new</span> <span class="built_in">Vector2</span>(Screen.width / <span class="number">2</span>, Screen.height / <span class="number">2</span>),</span><br><span class="line">    Camera.main,</span><br><span class="line">    out Vector3 worldPoint</span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>上面输出结果同样为 (0.0, 0.0, 115.0)。</p><p>在 Unity 的底层实现中，对于上面的从屏幕空间到世界空间的坐标转换，计算原理也是类似。</p>]]>
    </content>
    <id>https://lujun.pages.dev/2019/12/15/unity_pixel_to_world/</id>
    <link href="https://lujun.pages.dev/2019/12/15/unity_pixel_to_world/"/>
    <published>2019-12-15T00:02:29.000Z</published>
    <summary>
      <![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>作为研发的你工作中可能碰到过这样的场景：</p>
<blockquote>
<p>某天，策划同学 J 想到一个能给玩家带来愉悦感的「送钱特效」，这是一个三维场景中很多金币模型带有曲线的飞跃效果；渲染在屏幕上时，金币模型还被要求能从屏幕的指定位置飞到屏幕的另一指定位置。第二天，美术同学 S 在三维场景中调出了最好看的金币模型渲染效果，剩下的…</p>
</blockquote>
<p>当然剩下的就由作为研发的你来接手实现了。</p>]]>
    </summary>
    <title>RectTransform 如何从 Screen Space 到 World Space</title>
    <updated>2026-06-22T14:03:58.804Z</updated>
  </entry>
  <entry>
    <author>
      <name>lujun</name>
    </author>
    <category term="Unity" scheme="https://lujun.pages.dev/tags/Unity/"/>
    <content>
      <![CDATA[<script src="/assets/js/APlayer.min.js"> </script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>在 Projects Settings - Time 设置中，有一个和 <code>Fixed Timestep</code> 并列的设置项 <code>Maximum Allowed Timestep</code>；当游戏中大量运用物理计算时候，这个设置项可能就会引起你的注意。</p><span id="more"></span><h1 id="探讨"><a href="#探讨" class="headerlink" title="探讨"></a>探讨</h1><h2 id="Maximum-Allowed-Timestep-作用"><a href="#Maximum-Allowed-Timestep-作用" class="headerlink" title="Maximum Allowed Timestep 作用"></a>Maximum Allowed Timestep 作用</h2><p>在游戏帧率很低的情况下这个值用来限制<strong>最坏</strong>的情况发生，同时它将物理模块的执行时间<strong>限定</strong>在设定值之中。</p><p>上面描述中有两个关键点，在弄清楚这两个关键点之前需要先深入了解一下关于物理模块(以 <code>FixedUpdate</code> 方法为例)在 MonoBehaviour 生命流水线中「占据」的阶段。</p><h2 id="Physics-阶段"><a href="#Physics-阶段" class="headerlink" title="Physics 阶段"></a>Physics 阶段</h2><p>物理模块在整个 MonoBehaviour 生命周期中位于 <code>Start</code> 之后、输入事件检测之前；模块由 <code>FixedUpdate</code>、Animation update 和物理引擎计算组成。在每一帧中物理模块执行次数由设置的 <code>Maximum Allowed Timestep</code> 以及实际帧率决定。</p><p>下图是物理模块在整个 MonoBehaviour 生命周期中的位置:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_fixed_update_6.jpeg" width="50%" height="50%" /></center><p>从上图中可以看出，在帧主循环基础上 Physics 阶段还拥有一个自循环。</p><p>Unity 被设计为单线程，因此在每一帧中 Physics 阶段自循环一定有跳出该自循环的条件，这样才能够继续当前帧中的其他操作(如检测输入事件、渲染等)。那么这个条件是什么呢？下面从 <code>FixedUpdate</code>、<code>Update</code> 和 <code>LateUpdate</code> 三个方法来看看分析。</p><p>首先使用代码设置游戏帧率为 50FPS，其余设置默认。观察各个「Update」方法打印的日志，如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">QualitySettings.vSyncCount = <span class="number">0</span>;</span><br><span class="line">Application.targetFrameRate = <span class="number">50</span>;</span><br></pre></td></tr></table></figure><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_fixed_update_7.jpeg" width="40%" height="40%" /></center><p>从上图日志可以看出运行稳定时每帧间隔为 20ms 左右，<code>FixedUpdate</code> 方法基本都会在 <code>LateUpdate</code> 和 <code>Update</code> 方法之间执行一次。</p><p>接着讲游戏帧率修改为 10FPS，继续查看日志，如下图:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_fixed_update_8.jpeg" width="40%" height="40%" /></center><p>可以看出运行稳定时每帧间隔变成了 100ms 左右，<code>FixedUpdate</code> 方法此时会在 <code>LateUpdate</code> 和 <code>Update</code> 方法之间执行 5 次左右。后面再修改帧率为其它值，游戏运行稳定时 <code>FixedUpdate</code> 方法调用顺序和次数都呈现一个规律: </p><p>总是在 <code>LateUpdate</code> 和 <code>Update</code> 方法之间执行大约 $\frac{Time.deltaTime}{Fixed Timestep}$ 次。</p><p>上面提到了 <code>Fixed Timestep</code>，它是一个独立于帧率的用来指示物理模块执行的一个常量。通过这个值可以独立于帧率来驱动物理模拟，从而保证了不同帧率下物理模块计算的一致性。现在我们大概知道了 Physics 阶段的自循环大概是怎么实现的了，简单的模拟伪代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">Stopwatch _stopwatch = <span class="keyword">new</span> <span class="built_in">Stopwatch</span>();</span><br><span class="line"></span><br><span class="line"><span class="type">float</span> _physicsTime = <span class="number">.0f</span>;</span><br><span class="line"><span class="type">float</span> _fixedTimeStep = <span class="number">0.02f</span>;</span><br><span class="line"></span><br><span class="line">_stopwatch.<span class="built_in">Start</span>();</span><br><span class="line"><span class="keyword">while</span>(<span class="built_in">Running</span>())</span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">float</span> elapsedSeconds = _stopwatch.ElapsedMilliseconds / <span class="number">1000.0f</span>;</span><br><span class="line">    <span class="keyword">while</span>(_physicsTime &lt; elapsedSeconds)</span><br><span class="line">    &#123;</span><br><span class="line">        _physicsTime += _fixedTimeStep;</span><br><span class="line">        <span class="built_in">DoPhysics</span>();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="built_in">DoInput</span>();</span><br><span class="line">    <span class="built_in">DoUpdate</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>就像 MonoBehaviour 生命周期图中描述的那样，物理模块通过一个自循环来进行物理香相关计算，通过 <code>Fixed Timestep</code> 和当前帧间隔的时间来控制自循环次数。<code>FixedUpdate</code> 方法中的 <code>Time.fixedDeltaTime</code> 和 <code>Time.deltaTime</code> 的值也是 Fixed Timestep 设置的值。</p><p>下面再来看看最文章开始提到的两个关键点。</p><h2 id="“最坏”情况是如何发生的？"><a href="#“最坏”情况是如何发生的？" class="headerlink" title="“最坏”情况是如何发生的？"></a>“最坏”情况是如何发生的？</h2><p>通过上面对 Physics 阶段介绍知道，Unity 物理模块在主线程中也是和当前帧率有关联的。当帧率很低时，物理模块循环次数会变多，若此时每次物理模块计算量也很繁重，就会导致整个物理模块循环非常耗时，进而再次降低帧率。</p><h2 id="Maximum-Allowed-Timestep-如何将物理模块的执行时间限定在设定值之中"><a href="#Maximum-Allowed-Timestep-如何将物理模块的执行时间限定在设定值之中" class="headerlink" title="Maximum Allowed Timestep 如何将物理模块的执行时间限定在设定值之中?"></a>Maximum Allowed Timestep 如何将物理模块的执行时间限定在设定值之中?</h2><p>继续保持游戏帧率为 10FPS，对 Time 进行如下设置:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_fixed_update_0.jpeg" width="40%" height="40%" /></center><p>运行观察各个「Update」打印的日志情况，如下:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_fixed_update_0.jpeg" width="40%" height="40%" /></center><p>此时真实帧间隔 100ms 左右，<code>Time.deltaTime</code> 也和真实帧间隔几乎保持相同；<code>FixedUpdate</code> 方法执行顺序以及次数依旧满足之前伪代码的实现。接着将 Maximum Allowed Timestep 设置为 0.06s，再次运行观察日志:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_fixed_update_3.jpeg" width="40%" height="40%" /></center><p>可以看出设置了 Maximum Allowed Timestep 为 0.06s 之后，真实帧间隔依旧是 100ms 左右，但是 <code>Time.deltaTime</code> 固定在了 60ms，并且 <code>FixedUpdate</code> 方法执行次数变成了三次。这和上面伪代码的模拟好像有点出入。</p><p>再次将 Maximum Allowed Timestep 设置为 0.04s，运行后 <code>Time.deltaTime</code> 固定在 40ms，<code>FixedUpdate</code> 方法执行次数变成了两次。出现这些结果并不意外，因为此时正是 Maximum Allowed Timestep 设置的值在起作用。</p><p>在测试中帧率设置为 10FPS(每帧的时间间隔为 100ms)时，帧间隔时间大于设置的 Maximum Allowed Timestep 值。此时系统认为游戏帧率过低，如果继续使用<strong>当前帧间隔时间</strong>来自循环物理模块，假如此时每循环一次物理模块计算也很耗时，这必然会导致帧率进一步降低，“最坏”情况就发生了；因此通过 <code>Maximum Allowed Timestep</code> 来限制物理模块自循环次数，当真实帧间隔时间大于这个值时，将自循环次数降到大约 $\frac{Maximum Allowed Timestep}{Fixed Timestep}$ 次。这也就是 <code>FixedUpdate</code> 方法执行次数和上面伪代码的模拟有出入的原因。</p><p>所以，将 <code>Maximum Allowed Timestep</code> 控制也加入到上面模拟伪代码中:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line">Stopwatch _stopwatch = <span class="keyword">new</span> <span class="built_in">Stopwatch</span>();</span><br><span class="line"></span><br><span class="line"><span class="type">float</span> _physicsTime = <span class="number">.0f</span>;</span><br><span class="line"><span class="type">float</span> _fixedTimeStep = <span class="number">0.02f</span>;</span><br><span class="line"><span class="type">float</span> _maxAllowedTimestep = <span class="number">0.06f</span>;</span><br><span class="line"><span class="type">int</span> _maxAllowedPhysicsLoopCount = _maxAllowedTimestep / _fixedTimeStep;</span><br><span class="line"></span><br><span class="line">_stopwatch.<span class="built_in">Start</span>();</span><br><span class="line"><span class="keyword">while</span>(<span class="built_in">Running</span>())</span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">int</span> physicsLoopCount = <span class="number">0</span>;</span><br><span class="line">    <span class="type">float</span> elapsedSeconds = _stopwatch.ElapsedMilliseconds / <span class="number">1000.0f</span>;</span><br><span class="line">    <span class="keyword">while</span>(_physicsTime &lt; elapsedSeconds &amp;&amp; physicsLoopCount &lt; _maxAllowedPhysicsLoopCount)</span><br><span class="line">    &#123;</span><br><span class="line">        _physicsTime += _fixedTimeStep;</span><br><span class="line">    physicsLoopCount++;</span><br><span class="line">        <span class="built_in">DoPhysics</span>();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// update frame delta time</span></span><br><span class="line">    Time.deltaTime = Mathf.<span class="built_in">Min</span>(Time.deltaTime, _maxAllowedTimestep);</span><br><span class="line"></span><br><span class="line">    <span class="built_in">DoInput</span>();</span><br><span class="line">    <span class="built_in">DoUpdate</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面的代码加入了 Maximum Allowed Timestep 控制，从而能够在帧率较低时使用它设置的值来控制物理模块的自循环次数，这样物理模块计算就会慢下来(等待渲染进度赶上)。当物理模块计算也比较耗时的时候，“最坏情况”也被削弱。</p><p>上面代码中间还有一行用来更新 <code>Time.deltaTime</code>，当帧间隔时间大于 Maximum Allowed Timestep 设置的值时，<code>Time.deltaTime</code> 被设置为 Maximum Allowed Timestep 的值，因此就出现了 <code>Update</code> 方法中的 <code>Time.deltaTime</code> 固定值的情况。</p>]]>
    </content>
    <id>https://lujun.pages.dev/2019/11/07/unity_fixed_update/</id>
    <link href="https://lujun.pages.dev/2019/11/07/unity_fixed_update/"/>
    <published>2019-11-07T00:02:29.000Z</published>
    <summary>
      <![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>在 Projects Settings - Time 设置中，有一个和 <code>Fixed Timestep</code> 并列的设置项 <code>Maximum Allowed Timestep</code>；当游戏中大量运用物理计算时候，这个设置项可能就会引起你的注意。</p>]]>
    </summary>
    <title>从'Maximum Allowed Timestep'看 Unity 中的 Physics 阶段</title>
    <updated>2026-06-22T14:03:58.804Z</updated>
  </entry>
  <entry>
    <author>
      <name>lujun</name>
    </author>
    <category term="Unity" scheme="https://lujun.pages.dev/tags/Unity/"/>
    <content>
      <![CDATA[<script src="/assets/js/APlayer.min.js"> </script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>Scroll Rect 也是常用 Unity UI 之一。当需要滚动显示大量内容时 Scroll Rect 组件就能实现滚动效果；若需要有滚动条类似的拖动滚动，Scroll Rect 配合 Scrollbar 就可以简单实现拖动滚动。</p><span id="more"></span><h1 id="从一个问题说起"><a href="#从一个问题说起" class="headerlink" title="从一个问题说起"></a>从一个问题说起</h1><p>使用 Scroll Rect 时最典型的一个问题就是滚动嵌套，当 Scrll Rect 需要滚动的内容也是一个可滚动的 UI 元素，那么就有可能发生一些不愿看到的结果。</p><p>结合这个问题，下面将深入实现 Scroll Rect 内容滚动相关组件的源码，来剖析具体工作原理。</p><p>首先来看看 ScrollRect 组件。</p><h1 id="回到-ScrollRect-类"><a href="#回到-ScrollRect-类" class="headerlink" title="回到 ScrollRect 类"></a>回到 ScrollRect 类</h1><p>ScrollRect 类继承自 UIBehaviour，因此它具有 Unity 完整的生命周期；同时还实现了很多接口，例如 ILayoutGroup 等，后面会依次分析每个实现接口的意义。下面首先来看看 ScrollRect 类中一些比较重要的成员变量。</p><h2 id="ScrollRect-类的成员属性"><a href="#ScrollRect-类的成员属性" class="headerlink" title="ScrollRect 类的成员属性"></a>ScrollRect 类的成员属性</h2><table><thead><tr><th align="left">属性</th><th align="left">描述</th></tr></thead><tbody><tr><td align="left"><code>m_Content</code></td><td align="left">需要被滚动的 Rect Transform 元素。</td></tr><tr><td align="left"><code>m_Horizontal</code></td><td align="left">开启水平方向滚动。</td></tr><tr><td align="left"><code>m_Vertical</code></td><td align="left">开启垂直方向滚动。</td></tr><tr><td align="left"><code>m_MovementType</code></td><td align="left">内容滚动方式，有 Unrestricted, Elastic 和 Clamped 三种方式。设置为 Unrestricted 模式表示滚动无限制，Elastic 和 Clamped 模式会将滚动限制在 Scroll Rect 内，设置为 Elastic 还会带有弹性效果，通过弹性系数 <code>m_Elasticity</code> 可以调整弹性。</td></tr><tr><td align="left"><code>m_Inertia</code></td><td align="left">是否开启拖拽释放后滚动的惯性效果，开启时可以设置 <code>m_DecelerationRate</code> 值来控制减速速率。</td></tr><tr><td align="left"><code>m_ScrollSensitivity</code></td><td align="left">外设拖动事件监测灵敏度。</td></tr><tr><td align="left"><code>m_Viewport</code></td><td align="left">滚动内容 Rect Transform 的父节点，通常带有 Mask 组件，Scroll Rect 的重要组成部分之一。</td></tr><tr><td align="left"><code>m_HorizontalScrollbar</code></td><td align="left">水平方向上的 Scrollbar(可选)。</td></tr><tr><td align="left"><code>m_VerticalScrollbar</code></td><td align="left">垂直方向上的 Scrollbar(可选)。</td></tr></tbody></table><p>对于 Scrollbar 后面会详细介绍，这里当 Scroll Rect 中使用时，有两个属性可以在 ScrollRect 组件设置面板调整: </p><ul><li><p><code>m_XXScrollbarVisibility</code> 代表 Scrollbar 的可见性，它有三种模式:</p><ul><li><p>Permanent - 永远显示 Scrollbar；</p></li><li><p>AutoHide - 当 Scroll Rect 不需要滚动(比如 Viewport 尺寸比 Content 的要大)时自动隐藏 Scrollbar，这种模式不会更新 Viewport 尺寸；</p></li><li><p>AutoHideAndExpandViewport - 当 Scroll Rect 不需要滚动(比如 Viewport 尺寸比 Content 的要大)时自动隐藏 Scrollbar，这种模式会更新 Viewport 尺寸，同时这种模式下 Scrollbar 以及 Viewport 的 RectTransform 的参数由 ScrollRect 自动计算。</p></li></ul></li><li><p><code>m_XXScrollbarSpacing</code> 表示 Scrollbar 和 Viewport 之间的空间大小。</p></li></ul><p>另外 ScrollRect 类中还有一个 ScrollRectEvent(UnityEvent) 类型的成员变量 <code>m_OnValueChanged</code>，通过它可以监听 Scroll Rect 滚动带来的位置更新。</p><p>ScrollRect 类实现了 IInitializePotentialDragHandler、IBeginDragHandler、IEndDragHandler、IDragHandler、IScrollHandler、ICanvasElement、ILayoutElement 和 ILayoutGroup 这些接口，下面就从这些接口着手分析其重要的方法。</p><h1 id="Scroll-Rect-的布局"><a href="#Scroll-Rect-的布局" class="headerlink" title="Scroll Rect 的布局"></a>Scroll Rect 的布局</h1><h2 id="ICanvasElement-接口和-Rebuild-方法"><a href="#ICanvasElement-接口和-Rebuild-方法" class="headerlink" title="ICanvasElement 接口和 Rebuild 方法"></a>ICanvasElement 接口和 Rebuild 方法</h2><p>实现这个接口 Scroll Rect 能够接收到 Canvas 渲染更新时 Layout 重建的通知(具体是通过 CanvasUpdateRegistry 类的 <code>PerformUpdate</code> 方法来执行)以实现重新构建 Layout，ICanvasElement 接口中最重要的方法是 <code>Rebuild</code>，下面看看 ScrollRect 中对于这个方法的具体实现:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">virtual</span> <span class="type">void</span> <span class="title">Rebuild</span><span class="params">(CanvasUpdate executing)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (executing == CanvasUpdate.Prelayout)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="built_in">UpdateCachedData</span>();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (executing == CanvasUpdate.PostLayout)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="built_in">UpdateBounds</span>();</span><br><span class="line">        <span class="built_in">UpdateScrollbars</span>(Vector<span class="number">2.</span>zero);</span><br><span class="line">        <span class="built_in">UpdatePrevData</span>();</span><br><span class="line">        m_HasRebuiltLayout = <span class="literal">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看出在 ScrollRect 类自身实现的 <code>Rebuild</code> 方法中仅仅会对 Layout 前(<code>CanvasUpdate.Prelayout</code>)以及 Layout 后(<code>CanvasUpdate.PostLayout</code>)的重建回调做处理。</p><p>对于 Layout 之前的通知仅仅是调用了自身类的 <code>UpdateCachedData</code> 方法，这个方法缓存了以后计算会用到的相关数据，比如水平滚动条所在的 RectTransform 成员变量 <code>m_HorizontalScrollbarRect</code>、在隐藏垂直方向 Scrollbar 后是否可以将 Viewport 在水平方向上展开控制变量 <code>m_HSliderExpand</code> 等，代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">UpdateCachedData</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    m_HorizontalScrollbarRect = m_HorizontalScrollbar == null ? null : m_HorizontalScrollbar.transform as RectTransform;</span><br><span class="line"></span><br><span class="line">    <span class="type">bool</span> viewIsChild = (viewRect.parent == transform);</span><br><span class="line">    <span class="type">bool</span> hScrollbarIsChild = (!m_HorizontalScrollbarRect || m_HorizontalScrollbarRect.parent == transform);</span><br><span class="line">    <span class="type">bool</span> allAreChildren = (viewIsChild &amp;&amp; hScrollbarIsChild &amp;&amp; vScrollbarIsChild);</span><br><span class="line"></span><br><span class="line">    m_HSliderExpand = allAreChildren &amp;&amp; m_HorizontalScrollbarRect &amp;&amp; horizontalScrollbarVisibility == ScrollbarVisibility.AutoHideAndExpandViewport;</span><br><span class="line">    m_HSliderHeight = (m_HorizontalScrollbarRect == null ? <span class="number">0</span> : m_HorizontalScrollbarRect.rect.height);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其中对于 <code>m_HSliderExpand</code> 和 <code>mVSliderExpand</code> 的计算比较重要，这两个变量只有在为 Scroll Rect 设置了对应的 Scrollbar 并且 Scrollbar 的可见模式为 <code>AutoHideAndExpandViewport</code> 以及 Viewport 和 Scrollbar 都是 Scroll Rect 子元素时才被设置为 <code>true</code>，后面的 Viewport 展开计算中会经常用到这两个变量。</p><p>对于收到 Layout 完成之后的通知，会依次调用下面几个方法来更新对应的数据:</p><ol><li>首先调用 <code>UpdateBounds</code> 方法更新当前 Viewport 以及 Content 的 Bounds。方法中比较重要的有两部分，第一部分是调用 <code>AdjustBounds</code> 方法保证 Content 的 Bounds 不小于 Viewport 的 Bounds，只有在这个前提下 Scroll Rect 才能正确的滚动；另一点就是在内容滚动方式是 <code>MovementType.Clamped</code> 时，调整 Content 的 Bounds 的中心点位置在合适的位置(比如保证其 bottom 不高于 Viewport 的 bottom)，这样就能达到在 <code>MovementType.Clamped</code> 模式时 Content 滚动到 Viewport 边界就无法在滚动的要求。部分代码如下:</li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">protected</span> <span class="type">void</span> <span class="title">UpdateBounds</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    m_ViewBounds = <span class="keyword">new</span> <span class="built_in">Bounds</span>(viewRect.rect.center, viewRect.rect.size);</span><br><span class="line">    m_ContentBounds = <span class="built_in">GetBounds</span>();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Make sure content bounds are at least as large as view by adding padding if not.</span></span><br><span class="line">    <span class="built_in">AdjustBounds</span>(ref m_ViewBounds, ref contentPivot, ref contentSize, ref contentPos);</span><br><span class="line">    m_ContentBounds.size = contentSize;</span><br><span class="line">    m_ContentBounds.center = contentPos;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (movementType == MovementType.Clamped)</span><br><span class="line">    &#123;</span><br><span class="line">        Vector2 delta = Vector<span class="number">2.</span>zero;</span><br><span class="line">        <span class="keyword">if</span> (m_ViewBounds.max.x &gt; m_ContentBounds.max.x)</span><br><span class="line">        &#123;</span><br><span class="line">            delta.x = Math.<span class="built_in">Min</span>(m_ViewBounds.min.x - m_ContentBounds.min.x, m_ViewBounds.max.x - m_ContentBounds.max.x);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// other code ...</span></span><br><span class="line">        <span class="comment">// Calculate delta y ...</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (delta.sqrMagnitude &gt; <span class="type">float</span>.Epsilon)</span><br><span class="line">        &#123;</span><br><span class="line">            contentPos = m_Content.anchoredPosition + delta;</span><br><span class="line">            <span class="comment">// other code ...</span></span><br><span class="line">            <span class="built_in">AdjustBounds</span>(ref m_ViewBounds, ref contentPivot, ref contentSize, ref contentPos);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="2"><li>在收到 Layout 完成的通知调用 <code>UpdateBounds</code> 方法之后，紧接着就是执行 <code>UpdateScrollbars</code> 方法初始化 Scrollbar，包括其 handle bar 的长度(<code>Viewport size / Content size</code>)，以及 Scrollbar 的初始值(通过 <code>horizontalNormalizedPosition</code> 或 <code>verticalNormalizedPosition</code> 方法得到)。具体来看看 <code>horizontalNormalizedPosition</code> 方法代码:</li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">get</span><br><span class="line">&#123;</span><br><span class="line">    <span class="built_in">UpdateBounds</span>();</span><br><span class="line">    <span class="keyword">if</span> ((m_ContentBounds.size.x &lt;= m_ViewBounds.size.x) || Mathf.<span class="built_in">Approximately</span>(m_ContentBounds.size.x, m_ViewBounds.size.x))</span><br><span class="line">        <span class="keyword">return</span> (m_ViewBounds.min.x &gt; m_ContentBounds.min.x) ? <span class="number">1</span> : <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Calculate the scrollbar value ...</span></span><br><span class="line">    <span class="keyword">return</span> (m_ViewBounds.min.x - m_ContentBounds.min.x) / (m_ContentBounds.size.x - m_ViewBounds.size.x);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看出当 Content Bounds 尺寸小于 Viewport Bounds 尺寸时，Scrollbar 的值为 1；否则相等时 Scrollbar 的值为 0；否则大于时会根据两个 Bounds 的 <code>min</code> 和 <code>size</code> 两个参数进行计算得到。</p><ol start="3"><li>最后执行的是 <code>UpdatePrevData</code> 方法，这个方法主要是在接下来要更新 ScrollRect 时缓存 ScrollRect 的一些数据，后面计算会用到。</li></ol><p>看完了 ScrollRect 自身实现的 <code>Rebuild</code> 方法，是不是觉得有点懵? 收到 Layout 重新构建通知为什么只对 Layout 前(<code>CanvasUpdate.Prelayout</code>)以及 Layout 后(<code>CanvasUpdate.PostLayout</code>)的重建回调做处理，最重要的 Layout 过程(<code>CanvasUpdate.Layout</code>)回调不处理怎么进行布局了? 没错，ScrollRect 类自身实现的 <code>Rebuild</code> 方法确实并未直接处理 Layout 过程的回调，真正的布局是在 LayoutRebuilder 辅助类中完成的，下面看看详细分析。</p><p>我们知道通过 LayoutRebuilder 类来进行布局，首先需要将自身 RectTransdorm 注册到 LayoutRebuilder 中，然后 LayoutRebuilder 通过调用自身的 <code>Initialize</code> 方法内将目标 RectTransform 与自身实例绑定在一起，最后 LayoutRebuilder 会调用 CanvasUpdateRegistry 类的 <code>TryRegisterCanvasElementForLayoutRebuild</code> 方法将自身实例注册到 CanvasUpdateRegistry 类中(<strong>LayoutRebuilder 实现了 ICanvasElement 接口</strong>)，这样 Canvas 渲染更新时发出 Layout 重建的通知时，LayoutRebuilder 类的 <code>Rebuild</code> 方法会被回调，这里面才会对其绑定的 RectTransform 做真正的布局操作。</p><p>所以 ScrollRect 通过 LayoutRebuilder 辅助类进行布局，首先要做的就是将自身注册到 LayoutRebuilder 中，代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">LayoutRebuilder.<span class="built_in">MarkLayoutForRebuild</span>(rectTransform);</span><br></pre></td></tr></table></figure><p>这行代码直接或间接的在 ScrollRect 类中被多次调用，比如改变 Viewport、Scrollbar 等操作以及执行 <code>OnEnable</code> 等方法。</p><h2 id="OnEnable-和-OnDisable-方法"><a href="#OnEnable-和-OnDisable-方法" class="headerlink" title="OnEnable 和 OnDisable 方法"></a>OnEnable 和 OnDisable 方法</h2><p>ScrollRect 类继承自 UIBehaviour，所以这两个方法都是 Unity 生命周期回调方法。首先来看看 <code>OnEnable</code>，代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">override</span> <span class="type">void</span> <span class="title">OnEnable</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (m_HorizontalScrollbar)</span><br><span class="line">        m_HorizontalScrollbar.onValueChanged.<span class="built_in">AddListener</span>(SetHorizontalNormalizedPosition);</span><br><span class="line">    <span class="keyword">if</span> (m_VerticalScrollbar)</span><br><span class="line">        m_VerticalScrollbar.onValueChanged.<span class="built_in">AddListener</span>(SetVerticalNormalizedPosition);</span><br><span class="line"></span><br><span class="line">    CanvasUpdateRegistry.<span class="built_in">RegisterCanvasElementForLayoutRebuild</span>(<span class="keyword">this</span>);</span><br><span class="line">    <span class="built_in">SetDirty</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>代码很简单，首先添加了 Scrollbar 位置改变时的回调，以便能够更新 Content 位置；然后向 CanvasUpdateRegistry 中注册了自身以能够收到 Layout 重建前和重建完成后的回调；最后调用了 <code>SetDirty</code> 方法注册自身或者父级 LayoutGroup 到 LayoutRebuilder 中，以便能够真正的布局当前 ScrollRect。</p><p>再来看看 <code>OnDisable</code> 方法，它所做的事情基本上和 <code>OnEnable</code> 相反，比如从 CanvasUpdateRegistry 中反注册自己，从 Scrollbar 位置改变时的执行事件中移除对自身的回调等等。</p><h2 id="ILayoutElement-接口"><a href="#ILayoutElement-接口" class="headerlink" title="ILayoutElement 接口"></a>ILayoutElement 接口</h2><p>ScrollRect 类实现 ILayoutElement 能够保证其所在的 UI 元素能够在 Auto Layout 系统下自动适配和布局，详见<a href="https://blog.lujun.co/2018/04/10/how_unity_ui_layout_1/">《Unity UI - 布局(一)》</a>，因此 Scroll Rect 能够被 ILayoutController 控制布局。值得注意的一点是在实现的 ILayoutElement 接口方法中，对于 <code>minWidth</code>、<code>preferredWidth</code> 这些数值方法返回值都是 -1，因此在某些 ILayoutController 控制布局时若是单纯使用这些数值方法作为其对应的尺寸大小，特别需要注意可能会出现的一些问题。</p><h2 id="ILayoutGroup-接口和-ScrollRect-类对其中的方法具体实现"><a href="#ILayoutGroup-接口和-ScrollRect-类对其中的方法具体实现" class="headerlink" title="ILayoutGroup 接口和 ScrollRect 类对其中的方法具体实现"></a>ILayoutGroup 接口和 ScrollRect 类对其中的方法具体实现</h2><p>ScrollRect 类同时还实现了 ILayoutGroup 接口，因此除了自身能够被 ILayoutController 控制布局，它还能控制子元素的布局；这里主要涉及到 <code>SetLayoutHorizontal</code> 和 <code>SetLayoutVertical</code> 方法。</p><ol><li><code>SetLayoutHorizontal</code> 方法主要计算 Viewport 的尺寸大小以及位置、Viewport 和 Content 的 Bounds 数据等。</li></ol><p>这个方法中的代码比较长，首先第一步判断 <code>m_HSliderExpand</code> 或 <code>m_VSliderExpand</code> 的计算值(由 <code>UpdateCachedData</code> 方法中计算，表示是否支持在水平或垂直方向上允许 Viewport 扩展增加 Scrollbar 部分的尺寸大小)，如果为 <code>true</code> 那么首先就让 Viewport 尝试撑满父元素，然后来检测 Content 是否能够充满整个 Viewport，代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (m_HSliderExpand || m_VSliderExpand)</span><br><span class="line">&#123;</span><br><span class="line">    viewRect.anchorMin = Vector<span class="number">2.</span>zero;</span><br><span class="line">    viewRect.anchorMax = Vector<span class="number">2.</span>one;</span><br><span class="line">    viewRect.sizeDelta = Vector<span class="number">2.</span>zero;</span><br><span class="line">    viewRect.anchoredPosition = Vector<span class="number">2.</span>zero;</span><br><span class="line"></span><br><span class="line">    LayoutRebuilder.<span class="built_in">ForceRebuildLayoutImmediate</span>(content);</span><br><span class="line">    m_ViewBounds = <span class="keyword">new</span> <span class="built_in">Bounds</span>(viewRect.rect.center, viewRect.rect.size);</span><br><span class="line">    m_ContentBounds = <span class="built_in">GetBounds</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这种情况下 Viewport 相关在 Unity 编辑器面板会出现如下提示:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_scroll_rect_1.jpeg" width="40%" height="40%" /></center><p>从上图可以看出这时候 Viewport 的位置以及尺寸大小是由父节点的 ScrollRect 计算的，不可以手动修改。</p><p>计算完 Viewport 的布局相关信息，就会调用 LayoutRebuilder 类的 <code>ForceRebuildLayoutImmediate</code> 方法重新计算 Content 的布局信息看它是否能在没有 Scrollbar 的情况下充满 Viewport。</p><p>第二步就根据上一步得到的信息以及操作，判断 Content 充满 Viewport 的情况，首先检测的是垂直方向，代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (m_VSliderExpand &amp;&amp; vScrollingNeeded)</span><br><span class="line">&#123;</span><br><span class="line">    viewRect.sizeDelta = <span class="keyword">new</span> <span class="built_in">Vector2</span>(-(m_VSliderWidth + m_VerticalScrollbarSpacing), viewRect.sizeDelta.y);</span><br><span class="line"></span><br><span class="line">    LayoutRebuilder.<span class="built_in">ForceRebuildLayoutImmediate</span>(content);</span><br><span class="line">    m_ViewBounds = <span class="keyword">new</span> <span class="built_in">Bounds</span>(viewRect.rect.center, viewRect.rect.size);</span><br><span class="line">    m_ContentBounds = <span class="built_in">GetBounds</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>代码中，若垂直方向 Content 尺寸要比 Viewport 大(说明垂直方向需要滚动)，那么会给垂直方向的 Scrollbar 预留空间并缩小 Viewport 水平方向的尺寸大小，紧接着再次调用 LayoutRebuilder 类的 <code>ForceRebuildLayoutImmediate</code> 方法重新计算 Content 的布局信息。</p><p>第三步和第二步类似，计算水平方向的 Scrollbar 是否需要预留空间以及 Viewport 在垂直方向上是否需要缩小。</p><p>最后一步再次检测垂直方向 Scrollbar 没有生效的情况，若未生效就给垂直方向的 Scrollbar 预留空间并缩小 Viewport 水平方向的尺寸大小，代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (m_VSliderExpand &amp;&amp; vScrollingNeeded &amp;&amp; viewRect.sizeDelta.x == <span class="number">0</span> &amp;&amp; viewRect.sizeDelta.y &lt; <span class="number">0</span>)</span><br><span class="line">&#123;</span><br><span class="line">    viewRect.sizeDelta = <span class="keyword">new</span> <span class="built_in">Vector2</span>(-(m_VSliderWidth + m_VerticalScrollbarSpacing), viewRect.sizeDelta.y);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="2"><li>继续来分析 <code>SetLayoutVertical</code> 方法，这个方法是紧接着 <code>SetLayoutHorizontal</code> 方法被调用的。通过上面的分析我们知道 <code>SetLayoutHorizontal</code> 方法已经计算好了 Viewport 的布局信息、Viewport 和 Content 的 Bounds 数据等，也确定了是否需要启用 Scrollbar；在 <code>SetLayoutVertical</code> 方法中，主要的工作就是更新 Scrollbar 布局，所以它里面的代码也很简单，最重要的就是调用 ScrollRect 类自身的 <code>UpdateScrollbarLayout</code> 方法，这里我们直接分析 <code>UpdateScrollbarLayout</code> 方法，其代码如下:</li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">UpdateScrollbarLayout</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (m_VSliderExpand &amp;&amp; m_HorizontalScrollbar)</span><br><span class="line">    &#123;</span><br><span class="line">        m_HorizontalScrollbarRect.anchorMin = <span class="keyword">new</span> <span class="built_in">Vector2</span>(<span class="number">0</span>, m_HorizontalScrollbarRect.anchorMin.y);</span><br><span class="line">        m_HorizontalScrollbarRect.anchorMax = <span class="keyword">new</span> <span class="built_in">Vector2</span>(<span class="number">1</span>, m_HorizontalScrollbarRect.anchorMax.y);</span><br><span class="line">        m_HorizontalScrollbarRect.anchoredPosition = <span class="keyword">new</span> <span class="built_in">Vector2</span>(<span class="number">0</span>, m_HorizontalScrollbarRect.anchoredPosition.y);</span><br><span class="line">        <span class="keyword">if</span> (vScrollingNeeded)</span><br><span class="line">            m_HorizontalScrollbarRect.sizeDelta = <span class="keyword">new</span> <span class="built_in">Vector2</span>(-(m_VSliderWidth + m_VerticalScrollbarSpacing), m_HorizontalScrollbarRect.sizeDelta.y);</span><br><span class="line">        <span class="keyword">else</span></span><br><span class="line">            m_HorizontalScrollbarRect.sizeDelta = <span class="keyword">new</span> <span class="built_in">Vector2</span>(<span class="number">0</span>, m_HorizontalScrollbarRect.sizeDelta.y);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Vertical scroll bar update code ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上部分的代码中，若垂直方向上 Viewport 是可以展开的且设置了水平方向的 Scrollbar，则布局水平方向 Scrollbar。将其 <code>anchorMin.x</code> 设置为 0(位于 Scroll Rect 最左边)，<code>anchorMax.x</code> 设置为 1(位于 Scroll Rect 最右边)，<code>anchoredPosition.x</code> 设置为 0(Scrollbar 支点的水平坐标位于 Scroll Rect 中间)；若垂直方向需要 Scrollbar，则将水平方向的 Scrollbar 宽度设置为 Scroll Rect 宽度减去垂直方向滚动条需要的空间大小，否则直接将水平方向的 Scrollbar 宽度设置为 Scroll Rect 宽度。</p><p>计算设置垂直方向 Scrollbar 布局过程和上面类似，这两个过程 Scrollbar 的计算也是由 ScrollRect 控制，因此部分属性也不能在 Unity 编辑器中手动修改，如下图:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_scroll_rect_2.jpeg" width="40%" height="40%" /></center><p>在 <code>SetLayoutVertical</code> 方法中计算设置完成 Scrollbar 布局后，还会更新一下 Viewport 和 Content 的 Bounds 信息。</p><h1 id="Scroll-Rect-如何滚动内容-Event-System-事件接口"><a href="#Scroll-Rect-如何滚动内容-Event-System-事件接口" class="headerlink" title="Scroll Rect 如何滚动内容 - Event System 事件接口"></a>Scroll Rect 如何滚动内容 - Event System 事件接口</h1><p>前面看完了布局相关的接口方法，再来看看 ScrollRect 的滚动相关的分析，ScrollRect 类实现了一些列 Event System 中预定义的事件接口，下面就来一一来分析每个接口。</p><h2 id="IInitializePotentialDragHandler-接口"><a href="#IInitializePotentialDragHandler-接口" class="headerlink" title="IInitializePotentialDragHandler 接口"></a>IInitializePotentialDragHandler 接口</h2><p>实现这个接口，能够让 Event System 在分发「找到拖拽初始化对象」事件时，将自身所在 UI 元素作为事件待分发对象之一，如果确定该事件分发对象是自身，那么 ScrollRect 类中所实现的 <code>OnInitializePotentialDrag</code> 方法将会被回调。</p><p>ScrollRect 类对这个方法具体实现也很简单，仅简单地将拖拽速度变量 <code>m_Velocity</code> 初始化为 0。</p><h2 id="IBeginDragHandler-接口"><a href="#IBeginDragHandler-接口" class="headerlink" title="IBeginDragHandler 接口"></a>IBeginDragHandler 接口</h2><p>当拖拽对象开始发生拖拽时，实现的接口中的 <code>OnBeginDrag</code> 方法会被调用。ScrollRect 中 <code>OnBeginDrag</code> 方法部分代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">virtual</span> <span class="type">void</span> <span class="title">OnBeginDrag</span><span class="params">(PointerEventData eventData)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="built_in">UpdateBounds</span>();</span><br><span class="line"></span><br><span class="line">    m_PointerStartLocalCursor = Vector<span class="number">2.</span>zero;</span><br><span class="line">    RectTransformUtility.<span class="built_in">ScreenPointToLocalPointInRectangle</span>(viewRect, eventData.position, eventData.pressEventCamera, out m_PointerStartLocalCursor);</span><br><span class="line">    m_ContentStartPosition = m_Content.anchoredPosition;</span><br><span class="line">    m_Dragging = <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>从上面的代码中可以看出，它首先调用了 ScrollRect 类的 <code>UpdateBounds</code> 方法更新 Viewport 以及 Content 的 Bounds；紧接着初始化了 <code>m_PointerStartLocalCursor</code> 和 <code>m_ContentStartPosition</code> 相关变量，并将 <code>m_Dragging</code> 设置为 <code>true</code>。  </p><h2 id="IEndDragHandler-接口"><a href="#IEndDragHandler-接口" class="headerlink" title="IEndDragHandler 接口"></a>IEndDragHandler 接口</h2><p>当拖拽对象开始拖拽结束时，实现的这个接口中的 <code>OnEndDrag</code> 方法会被调用，对于 ScrollRect 中的这个方法仅仅将 <code>m_Dragging</code> 设置为了 <code>false</code>。</p><h2 id="IDragHandler-接口"><a href="#IDragHandler-接口" class="headerlink" title="IDragHandler 接口"></a>IDragHandler 接口</h2><p>当拖拽对象发生拖拽时，实现的这个接口中的 <code>OnDrag</code> 方法会被调用，ScrollRect 中具体的 <code>OnDrag</code> 方法代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">virtual</span> <span class="type">void</span> <span class="title">OnDrag</span><span class="params">(PointerEventData eventData)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    Vector2 localCursor;</span><br><span class="line">    <span class="keyword">if</span> (!RectTransformUtility.<span class="built_in">ScreenPointToLocalPointInRectangle</span>(viewRect, eventData.position, eventData.pressEventCamera, out localCursor))</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line">    <span class="built_in">UpdateBounds</span>();</span><br><span class="line"></span><br><span class="line">    var pointerDelta = localCursor - m_PointerStartLocalCursor;</span><br><span class="line">    Vector2 position = m_ContentStartPosition + pointerDelta;</span><br><span class="line">    Vector2 offset = <span class="built_in">CalculateOffset</span>(position - m_Content.anchoredPosition);</span><br><span class="line">    position += offset;</span><br><span class="line">    <span class="keyword">if</span> (m_MovementType == MovementType.Elastic)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">if</span> (offset.x != <span class="number">0</span>)</span><br><span class="line">            position.x = position.x - <span class="built_in">RubberDelta</span>(offset.x, m_ViewBounds.size.x);</span><br><span class="line">        <span class="keyword">if</span> (offset.y != <span class="number">0</span>)</span><br><span class="line">            position.y = position.y - <span class="built_in">RubberDelta</span>(offset.y, m_ViewBounds.size.y);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="built_in">SetContentAnchoredPosition</span>(position);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>拖拽过程中，通过改变 Content 的位置从而实现了 Content 的滚动。下面来看看整个过程:</p><ol><li><p>首先调用 RectTransformUtility 类的静态方法 <code>ScreenPointToLocalPointInRectangle</code> 将当前屏幕上的坐标转换到 UI 元素的坐标系统下，并缓存至 <code>localCursor</code> 临时变量中；</p></li><li><p>计算开始拖拽和当前拖拽回调发生的偏移量 pointerDelta，同时将这个偏移量累加到 Content 起始位置 <code>m_ContentStartPosition</code> 中得到新的 position；</p></li><li><p>将上一步得到的新的 position 减去此时 Content 的位置(此次更新前的位置) <code>anchoredPosition</code> 得到的值作为参数，调用 ScrollRect 类本身的 <code>CalculateOffset</code> 方法来计算这次拖拽回调 Content 的「补偿偏移」，其中计算「补偿偏移」大致代码如下:</p></li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">internal <span class="type">static</span> Vector2 <span class="title">InternalCalculateOffset</span><span class="params">(ref Bounds viewBounds, ref Bounds contentBounds, <span class="type">bool</span> horizontal, <span class="type">bool</span> vertical, MovementType movementType, ref Vector2 delta)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    Vector2 offset = Vector<span class="number">2.</span>zero;</span><br><span class="line">    <span class="keyword">if</span> (movementType == MovementType.Unrestricted)</span><br><span class="line">        <span class="keyword">return</span> offset;</span><br><span class="line"></span><br><span class="line">    Vector2 min = contentBounds.min;</span><br><span class="line">    Vector2 max = contentBounds.max;</span><br><span class="line">    <span class="keyword">if</span> (horizontal)</span><br><span class="line">    &#123;</span><br><span class="line">        min.x += delta.x;</span><br><span class="line">        max.x += delta.x;</span><br><span class="line"></span><br><span class="line">        <span class="type">float</span> maxOffset = viewBounds.max.x - max.x;</span><br><span class="line">        <span class="type">float</span> minOffset = viewBounds.min.x - min.x;</span><br><span class="line">        <span class="keyword">if</span> (minOffset &lt; <span class="number">-0.001f</span>)</span><br><span class="line">            offset.x = minOffset;</span><br><span class="line">        <span class="keyword">else</span> <span class="keyword">if</span> (maxOffset &gt; <span class="number">0.001f</span>)</span><br><span class="line">            offset.x = maxOffset;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// Calculate vertical offset code ...</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> offset;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>若当前 Scroll Rect 滚动模式为 <code>Unrestricted</code> 「补偿偏移」为 0，也就是说对于无限滚动模式 Content 的位置根据滚动的距离决定不需要任何补偿调整。若滚动模式是另外两种有限滚动模式(限制在 Viewport 内)，则会为此次滚动距离计算「补偿偏移」，水平方向「补偿偏移」计算过程如下: 首先将 Content Bounds 水平方向上的最小值和最大值分别加上此次应该累加的偏移量得到临时的 min 和 max；然后用 Viewport Bounds 水平方向上的最小值和最大值分别与 min 和 max 相减，得到“假设”移动 Content 后 Viewport Bounds 和 Content Bounds 水平方向上的两个差值 minOffset 和 maxOffset；若最左边得到 min 差值小于 0，说明 Content 水平方向上左边界已经超过了 Viewport 的左边界，此时对于 Content 在水平方向上的移动就应该在上一步得到的位置 position 基础上加上这个 minOffset(这个值为负数，即减去这段 minOffset 的距离)，来让 Content 水平方向上左边界不超过 Viewport 的左边界，对于又边界也是同样的计算过程。具体看下图会更清楚:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_scroll_rect_3.jpeg" width="60%" height="60%" /></center><p>这就是计算水平方向上「补偿偏移」的过程，对于垂直方向也是类似的计算方式，得到「补偿偏移」后将这个偏移累加到第二步计算的位置上。</p><ol start="4"><li>接下来就是对于滚动模式为 <code>Elastic</code> 时的特殊处理，来实现<strong>拖拽过程中的弹性效果</strong>，代码如下:</li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (m_MovementType == MovementType.Elastic)</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">if</span> (offset.x != <span class="number">0</span>)</span><br><span class="line">        position.x = position.x - <span class="built_in">RubberDelta</span>(offset.x, m_ViewBounds.size.x);</span><br><span class="line">    <span class="keyword">if</span> (offset.y != <span class="number">0</span>)</span><br><span class="line">        position.y = position.y - <span class="built_in">RubberDelta</span>(offset.y, m_ViewBounds.size.y);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>主要就是调用了 ScrollRect 类方法 <code>RubberDelta</code> 对即将设置给 Content 的位置 position 进行了处理；<code>RubberDelta</code> 方法模拟了一条弹性曲线，使用「补偿偏移」offset 以及 Viewport Bounds 大小进行弹性位移的计算。</p><ol start="5"><li>最后一步调用 <code>SetContentAnchoredPosition</code> 方法为 Content 设置新位置 position。</li></ol><h2 id="IScrollHandler-接口"><a href="#IScrollHandler-接口" class="headerlink" title="IScrollHandler 接口"></a>IScrollHandler 接口</h2><p>当鼠标滚轮滚动时，实现了这个接口中的 <code>OnScroll</code> 方法会被回调，ScrollRect 中具体的 <code>OnScroll</code> 方法代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">virtual</span> <span class="type">void</span> <span class="title">OnScroll</span><span class="params">(PointerEventData data)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="comment">// other code ...</span></span><br><span class="line">    Vector2 delta = data.scrollDelta;</span><br><span class="line">    delta.y *= <span class="number">-1</span>;</span><br><span class="line">    <span class="keyword">if</span> (vertical &amp;&amp; !horizontal)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">if</span> (Mathf.<span class="built_in">Abs</span>(delta.x) &gt; Mathf.<span class="built_in">Abs</span>(delta.y))</span><br><span class="line">            delta.y = delta.x;</span><br><span class="line">        delta.x = <span class="number">0</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// Calculate horizontal delta code ...</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (data.<span class="built_in">IsScrolling</span>())</span><br><span class="line">        m_Scrolling = <span class="literal">true</span>;</span><br><span class="line">    Vector2 position = m_Content.anchoredPosition;</span><br><span class="line">    position += delta * m_ScrollSensitivity;</span><br><span class="line">    <span class="keyword">if</span> (m_MovementType == MovementType.Clamped)</span><br><span class="line">        position += <span class="built_in">CalculateOffset</span>(position - m_Content.anchoredPosition);</span><br><span class="line"></span><br><span class="line">    <span class="built_in">SetContentAnchoredPosition</span>(position);</span><br><span class="line">    <span class="built_in">UpdateBounds</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面计算过程也很简单，根据鼠标滚轮滚动的值来滚动 Content。</p><ol><li><p>如果开启了垂直方向滚动且关闭了水平方向的滚动，那么使用滚轮较大方向上的那个值作为最终垂直方向滚动的差值，水平方向上的滚动差值设置为 0，然后使用差值乘以滚动灵敏度 <code>m_ScrollSensitivity</code> 得到最终的滚动距离差值；</p></li><li><p>如果滚动模式为 <code>Clamped</code>，也是首先计算「补偿偏移」使得 Content 边界不越界；这里和 <code>OnDrag</code> 方法中处理有点不同，仅仅在滚动模式为 <code>Clamped</code> 下加了这个保护而 <code>Elastic</code> 模式没有，原因在于在 <code>OnDrag</code> 方法中，虽然在 <code>Elastic</code> 模式也保证了 Content 边界不越界，但是后面会紧接着调用 <code>RubberDelta</code> 方法来模拟弹性效果，使得 Content 依旧会“假”越界，而在 <code>OnScroll</code> 方法中边界未做保护，就会导致滚轮滚动的距离实际就会设置为 Content 的距离，但是真正的弹性效果会在 <code>LateUpdate</code> 方法中实现(<code>OnEndDrag</code> 以及 <code>OnScroll</code> 的回弹效果都会在 <code>LateUpdate</code> 方法中处理，后面会分析到)。</p></li><li><p>最后调用 <code>SetContentAnchoredPosition</code> 设置 Content 的新位置，并更新 Viewport 以及 Content 的 Bounds。</p></li></ol><h2 id="LateUpdate-方法"><a href="#LateUpdate-方法" class="headerlink" title="LateUpdate 方法"></a>LateUpdate 方法</h2><p>这个方法是 Unity 生命周期中的回调方法之一，在所有的 Update 方法执行完成之后该方法会被回调执行。ScrollRect 类重写了这个方法，主要用来做以下几件事情:</p><ol><li>实现在滚轮滚动时 Content 边界越界时可能会有的弹性拉伸效果、Content 的弹性回弹效果以及拖拽结束后的惯性滚动效果，代码如下:</li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">float</span> deltaTime = Time.unscaledDeltaTime;</span><br><span class="line">Vector2 offset = <span class="built_in">CalculateOffset</span>(Vector<span class="number">2.</span>zero);</span><br><span class="line"><span class="keyword">if</span> (!m_Dragging &amp;&amp; (offset != Vector<span class="number">2.</span>zero || m_Velocity != Vector<span class="number">2.</span>zero))</span><br><span class="line">&#123;</span><br><span class="line">    Vector2 position = m_Content.anchoredPosition;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> axis = <span class="number">0</span>; axis &lt; <span class="number">2</span>; axis++)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">if</span> (m_MovementType == MovementType.Elastic &amp;&amp; offset[axis] != <span class="number">0</span>)</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="type">float</span> speed = m_Velocity[axis];</span><br><span class="line">            <span class="type">float</span> smoothTime = m_Elasticity;</span><br><span class="line">            <span class="keyword">if</span> (m_Scrolling)</span><br><span class="line">                smoothTime *= <span class="number">3.0f</span>;</span><br><span class="line">            position[axis] = Mathf.<span class="built_in">SmoothDamp</span>(m_Content.anchoredPosition[axis], m_Content.anchoredPosition[axis] + offset[axis], ref speed, smoothTime, Mathf.Infinity, deltaTime);</span><br><span class="line">            <span class="keyword">if</span> (Mathf.<span class="built_in">Abs</span>(speed) &lt; <span class="number">1</span>)</span><br><span class="line">                speed = <span class="number">0</span>;</span><br><span class="line">            m_Velocity[axis] = speed;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">else</span> <span class="keyword">if</span> (m_Inertia)</span><br><span class="line">        &#123;</span><br><span class="line">            m_Velocity[axis] *= Mathf.<span class="built_in">Pow</span>(m_DecelerationRate, deltaTime);</span><br><span class="line">            <span class="keyword">if</span> (Mathf.<span class="built_in">Abs</span>(m_Velocity[axis]) &lt; <span class="number">1</span>)</span><br><span class="line">                m_Velocity[axis] = <span class="number">0</span>;</span><br><span class="line">            position[axis] += m_Velocity[axis] * deltaTime;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">else</span></span><br><span class="line">            m_Velocity[axis] = <span class="number">0</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (m_MovementType == MovementType.Clamped)</span><br><span class="line">    &#123;</span><br><span class="line">        offset = <span class="built_in">CalculateOffset</span>(position - m_Content.anchoredPosition);</span><br><span class="line">        position += offset;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="built_in">SetContentAnchoredPosition</span>(position);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>从上面的代码中可以看出，只有在 <code>m_Dragging</code> 为 <code>false</code> 时，这段代码才有可能被执行，也就是说只有当滚轮滚动操作时或者是拖拽结束后才有可能有滚轮滚动的弹性拉伸效果或回弹效果以及惯性结束停止滚动效果。</p><p>最开始调用 <code>CalculateOffset</code> 方法计算「补偿偏移」，注意这里传入的参数为 0，也就是仅用当前 Viewport 和 Content 的 Bounds 数据来计算而不需要累加偏移量。</p><p>如果滚动模式是 <code>Elastic</code> 且当前求得「补偿偏移」大于 0 时，那么计算弹性效果。首先来看看滚轮滚动使得 Content 到达边界处时，此时若滚轮继续处于滚动状态，那么会有弹性拉伸效果产生。通过之前的分析我们知道，滚轮滚动回调的 <code>OnScroll</code> 方法中，如果滚动模式为 <code>Elastic</code> 那么 Content 的位置是根据滚动计算得到的位移确定的(并未像 <code>Clamped</code> 模式一样做边界保护，有可能“越界”)，当发生“越界”之后来到当前的代码中 Content 的位置有会被以弹性拉伸的方式来纠正，具体就是调用 Mathf 类的 <code>SmoothDamp</code> 方法传入当前速度 <code>m_Velocity</code>、动画时间 <code>smoothTime</code> 等参数尝试让 Content 以阻尼效果的方式回到边界处(若是正处于 Scrolling 状态，这里的时间动画 <code>smoothTime</code> 会在弹性系数 <code>m_Elasticity</code> 基础上扩大三倍)，连续这样的过程就让滚轮滚动也能使 Content 产生了弹性拉伸的效果。再来看看拖拽结束或者是滚轮滚动结束让 Content 产生的回弹的情况，和刚刚说的滚轮拉伸效果一样，只不过这里的回弹动画时间 <code>smoothTime</code> 就是弹性系数 <code>m_Elasticity</code>。</p><p>滚动模式不是 <code>Elastic</code> 或者当前求得「补偿偏移」为 0 时，表示不需要弹性滚动，此时根据减速速率来计算停止滚动(带有惯性)。</p><p>否则如果既不需要弹性滚动也未设置惯性停止滚动，则将滚动速度 <code>m_Velocity</code> 设置为 0。</p><p>计算得到 Content 的新位置之后，接着对滚动模式为 <code>Clamped</code> 时限制了滚动边界，最后调用 <code>SetContentAnchoredPosition</code> 改变 Content 的位置。</p><ol start="2"><li>拖拽过程中，如果设置了需要停止滚动后以惯性停止，则会更新滚动速度，代码如下:</li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (m_Dragging &amp;&amp; m_Inertia)</span><br><span class="line">&#123;</span><br><span class="line">    Vector3 newVelocity = (m_Content.anchoredPosition - m_PrevPosition) / deltaTime;</span><br><span class="line">    m_Velocity = Vector<span class="number">3.L</span>erp(m_Velocity, newVelocity, deltaTime * <span class="number">10</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="3"><li>最后在 <code>LateUpdate</code> 方法中所做的就是更新 Scrollbar 相关信息，代码如下:</li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (m_ViewBounds != m_PrevViewBounds || m_ContentBounds != m_PrevContentBounds || m_Content.anchoredPosition != m_PrevPosition)</span><br><span class="line">&#123;</span><br><span class="line">    <span class="built_in">UpdateScrollbars</span>(offset);</span><br><span class="line">    UISystemProfilerApi.<span class="built_in">AddMarker</span>(<span class="string">&quot;ScrollRect.value&quot;</span>, <span class="keyword">this</span>);</span><br><span class="line">    m_OnValueChanged.<span class="built_in">Invoke</span>(normalizedPosition);</span><br><span class="line">    <span class="built_in">UpdatePrevData</span>();</span><br><span class="line">&#125;</span><br><span class="line"><span class="built_in">UpdateScrollbarVisibility</span>();</span><br></pre></td></tr></table></figure><p>上面的代码中首先调用了 ScrollRect 类自身的 <code>UpdateScrollbars</code> 方法计算 Scrollbar  相关信息，看看这个方法的水平方向上计算代码(垂直方向也是同样的方式计算):</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="type">void</span> <span class="title">UpdateScrollbars</span><span class="params">(Vector2 offset)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (m_HorizontalScrollbar)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">if</span> (m_ContentBounds.size.x &gt; <span class="number">0</span>)</span><br><span class="line">            m_HorizontalScrollbar.size = Mathf.<span class="built_in">Clamp01</span>((m_ViewBounds.size.x - Mathf.<span class="built_in">Abs</span>(offset.x)) / m_ContentBounds.size.x);</span><br><span class="line">        <span class="keyword">else</span></span><br><span class="line">            m_HorizontalScrollbar.size = <span class="number">1</span>;</span><br><span class="line"></span><br><span class="line">        m_HorizontalScrollbar.value = horizontalNormalizedPosition;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// Calculate vertical scroll bar ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Scrollbar 的大小值由 Viewport Bounds 尺寸减去当前滚动的「补偿偏移」量除以 Content Bounds 尺寸，计算结果为在 0-1 之间，结果值越大 Scrollbar 尺寸越大(为 1 时会充满整个 Scrollbar 区域)，因此在 <code>Elastic</code> 这种弹性“越界”模式下，当越界后若继续在某个方向上滚动随着「补偿偏移」越来越大，会看到 Scrollbar 在这个方向上的尺寸越来越小。</p><p>计算完 Scrollbar 的大小，紧接着调用 <code>horizontalNormalizedPosition</code> 方法计算 Scrollbar 的位置值，代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="type">float</span> horizontalNormalizedPosition</span><br><span class="line">&#123;</span><br><span class="line">    get</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="built_in">UpdateBounds</span>();</span><br><span class="line">        <span class="keyword">if</span> ((m_ContentBounds.size.x &lt;= m_ViewBounds.size.x) || Mathf.<span class="built_in">Approximately</span>(m_ContentBounds.size.x, m_ViewBounds.size.x))</span><br><span class="line">            <span class="keyword">return</span> (m_ViewBounds.min.x &gt; m_ContentBounds.min.x) ? <span class="number">1</span> : <span class="number">0</span>;</span><br><span class="line">        <span class="keyword">return</span> (m_ViewBounds.min.x - m_ContentBounds.min.x) / (m_ContentBounds.size.x - m_ViewBounds.size.x);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>主要就是用 Viewport Bounds 的最小值和 Content Bounds 的最小值相减，除以 Content Bounds 尺寸和 Viewport Bounds 尺寸的差值得到最终 Scrollbar 的位置值。</p><p>执行完 <code>UpdateScrollbars</code> 方法，<code>m_OnValueChanged</code> 被执行来通知所有的观察者位置更新，接着调用 <code>UpdatePrevData</code> 更新缓存数据；最后调用 <code>UpdateScrollbarVisibility</code> 更新 Scrollbar 的显示。</p><p>到这里，整个 Scroll Rect 的滚动过程就分析完了，其滚动主要监听就是靠 Event System 中一些预制事件实时计算位置来实现。使用事件监听就会遇到令人头疼的问题，比如事件被拦截导致滚动失效，这也是本文最开始提到的一个问题，下面我们将来分析这个问题的发生过程以及解决方案。</p><h1 id="回到最开始的问题-解决-Scroll-Rect-中滚动嵌套出现的问题"><a href="#回到最开始的问题-解决-Scroll-Rect-中滚动嵌套出现的问题" class="headerlink" title="回到最开始的问题 - 解决 Scroll Rect 中滚动嵌套出现的问题"></a>回到最开始的问题 - 解决 Scroll Rect 中滚动嵌套出现的问题</h1><p>在探讨这个问题之前，先了解 Event System 中事件分发的一些重要内容:</p><ul><li><p>Event System 中，某个事件一旦被一个对象拦截消费将不会向祖先节点上冒泡传递；若这个事件对象无法消费成功，那么事件会依旧像上寻找能够消费它的对象。因此若 Scroll Rect 某个子元素消费了其滚动所需的事件(如 IDragHandler 等)，那么将导致 Scroll Rect 无法滚动；但是若这个子元素消费了非 Scroll Rect 滚动所需事件(如 IPointerDownHandler 等)，那么 Scroll Rect 依旧能够拦截滚动所需事件来实现滚动效果。</p></li><li><p>Unity UI 中检测事件接收对象主要靠 GraphicRaycaster 类完成，但它只识别 Graphic 作为检测对象，因此一个 ScrollRect 所在对象要想接收到滚动所需的预制事件，这个对象也必须绑定 Graphic 组件，或者其子节点有绑定 Graphic 组件但是子节点并未消费这些预制事件。</p></li></ul><p>如果某个需求导致 Scroll Rect 滚动所需事件被子元素拦截并消费，Scroll Rect 如何才能继续保持接收这些事件来滚动了？有以下两中解决方案:</p><ol><li><p>在子元素消费的特定事件中强制调用当前 ScrollRect 对应的响应事件方法，比如在子元素处理的 <code>OnDrag</code> 方法中调用 ScrollRect 类的 <code>OnDrag</code> 方法，这样能够依旧保证 Scroll Rect 的滚动效果；</p></li><li><p>第二种方式依然是在子元素消费的特定事件方法中处理，只不过这里不再强制调用 ScrollRect 对应的响应事件方法，因为这样显得代码太过于耦合，这种方法中在处理完子元素的逻辑后，手动进行一次 Event System 的事件分发对象检测操作，然后将事件再次分发下去。这种方法的代码大致如下:</p></li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="type">void</span> <span class="built_in">PassEvent</span>&lt;T&gt;(PointerEventData eventData, ExecuteEvents.EventFunction&lt;T&gt; function) where T : IEventSystemHandler</span><br><span class="line">&#123;</span><br><span class="line">    List&lt;RaycastResult&gt; results = <span class="keyword">new</span> <span class="built_in">List</span>&lt;RaycastResult&gt;();</span><br><span class="line">    EventSystem.current.<span class="built_in">RaycastAll</span>(eventData, results);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; results.Count; i++)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">if</span> (gameObject != results[i].gameObject)</span><br><span class="line">        &#123;</span><br><span class="line">            GameObject go = ExecuteEvents.<span class="built_in">ExecuteHierarchy</span>(results[i].gameObject, eventData, function);</span><br><span class="line">            <span class="keyword">if</span> (go != null)</span><br><span class="line">                <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面的代码能够解决嵌套滚动问题，但是有个局限性就是鼠标位置移出其 Viewport，那么 <code>RaycastAll</code> 将检测不到任何可接收事件的对象，因此事件也将不能再继续传递。</p><p>如果 Content 很大很大，此时的鼠标明明还在其 Rect 内，为什么 Content 也无法再次被检测为事件接收对象？回答这个问题要从 <a href="https://blog.lujun.co/2019/07/20/unity_event_system_raycasters/#graphic.raycast">Graphic.Raycast</a> 说起，判断一个 Graphic 是否能够作为事件接收对象除了满足最基础的条件(Active 等属性设置)外，还有另一个重要条件就是调用 Graphic 自身的 <code>Raycast</code> 检测要合法，这个方法检测过程大致如下: 循环遍历这个 Graphic 所在对象以及其祖先节点对象，每一次遍历过程都需要满足其对象上绑定的所有的 ICanvasRaycastFilter 接口的 <code>IsRaycastLocationValid</code> 方法返回 <code>true</code>，这有所有的遍历成功返回 <code>true</code>，这个 Graphic 才能作为事件检测对象之一，详细分析见<a href="https://blog.lujun.co/2019/07/20/unity_event_system_raycasters/#graphic.raycast">《Unity Raycasters 剖析》</a>。 </p><p>所以当鼠标移出 Scroll Rect 的 Viewport 时，就算鼠标还位于其子元素的范围内也无法在检测到这几个元素作为事件接收对象的原因也就水落石出了: </p><ul><li><p>当判断 Content 元素时，对于其自身所有组件检测通过，但是当开始检测其父节点 Viewport 时，由于绑定了 Mask 组件，Maks 组件也是一个 ICanvasRaycastFilter，并且其具体 <code>IsRaycastLocationValid</code> 方法直接将鼠标点是否位于自身 Rect 范围内作为返回值；上面的情况中鼠标已经移出了 Viewport，所以 Content 检测不能作为事件接收对象。</p></li><li><p>Content 元素检测失败，对于 Viewport 元素自然直接返回失败，它甚至还没到达 Graphic Raycst 这一步就已经被淘汰(GraphicRaycaster 会提前过滤)。</p></li><li><p>对于 ScrollRect 所在对象，由于根本没有绑定 Graphic 组件，直接淘汰。</p></li></ul><p><strong>注意: 上面的检测中 Mask 组件具体 <code>IsRaycastLocationValid</code> 方法实现最终调用了 RectTransformUtility 类的 <code>RectangleContainsScreenPoint</code> 方法直接判断点是否在 Rect 内；而 Image 组件的具体 <code>IsRaycastLocationValid</code> 方法主要调用 RectTransformUtility 类的 <code>ScreenPointToLocalPointInRectangle</code> 来判断(<a href="https://docs.unity3d.com/ScriptReference/RectTransformUtility.ScreenPointToLocalPointInRectangle.html">文档</a>)，这个方法会判断当前 Image 的 RectTransform 所在整个平面能否被射线检测到碰撞，因此大部分情况是返回 <code>true</code>。</strong></p><h1 id="Scroll-Rect-内容遮罩-Mask-组件登场"><a href="#Scroll-Rect-内容遮罩-Mask-组件登场" class="headerlink" title="Scroll Rect 内容遮罩 - Mask 组件登场"></a>Scroll Rect 内容遮罩 - Mask 组件登场</h1><p>Mask 组件除了能够限制拖动事件响应范围，另一个更大的作用就是遮罩显示 Content 的内容，更多关于这部分的内容读者可参考<a href="https://blog.lujun.co/2019/09/05/unity_mask/">《Unity 遮罩“绘制”图》</a></p><h1 id="Scrollbar-简单介绍"><a href="#Scrollbar-简单介绍" class="headerlink" title="Scrollbar 简单介绍"></a>Scrollbar 简单介绍</h1><p>作为 Scroll Rect 可配置的一部分，Scrollbar 可以直接响应拖动从而来控制 Scroll Rect 内容的滚动。上面分析的时候也讲到了 Scrollbar 的 <code>value</code> 和 <code>size</code> 属性，除了这两个之外它还有很多其它属性，如 <code>direction</code> 控制 Scrollbar 的 <code>value</code> 增长方向等，更多请参考<a href="https://docs.unity3d.com/Manual/script-Scrollbar.html">Unity Manual - Scrollbar</a>。</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ul><li><p><a href="https://docs.unity3d.com/Manual/script-ScrollRect.html">Unity Manual - Scroll Rect</a></p></li><li><p><a href="https://docs.unity3d.com/ScriptReference/Bounds.html">Unity Scripting API - Bounds</a></p></li><li><p><a href="https://blog.lujun.co/2018/04/10/how_unity_ui_layout_1/">Unity UI - 布局(一)</a></p></li></ul>]]>
    </content>
    <id>https://lujun.pages.dev/2019/10/21/unity_scroll_rect/</id>
    <link href="https://lujun.pages.dev/2019/10/21/unity_scroll_rect/"/>
    <published>2019-10-21T00:02:29.000Z</published>
    <summary>
      <![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>Scroll Rect 也是常用 Unity UI 之一。当需要滚动显示大量内容时 Scroll Rect 组件就能实现滚动效果；若需要有滚动条类似的拖动滚动，Scroll Rect 配合 Scrollbar 就可以简单实现拖动滚动。</p>]]>
    </summary>
    <title>ScrollRect 探究</title>
    <updated>2026-06-22T14:03:58.807Z</updated>
  </entry>
  <entry>
    <author>
      <name>lujun</name>
    </author>
    <category term="Unity" scheme="https://lujun.pages.dev/tags/Unity/"/>
    <content>
      <![CDATA[<script src="/assets/js/APlayer.min.js"> </script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><blockquote><p>A Mask is not a visible UI control but rather a way to modify the appearance of a control’s child elements. the parent will be visible.</p></blockquote><p>Mask 是一个不可见的用于控制子元素遮罩显示的一个组件，在 Unity UI 中分别有 Mask 和 RectMask2D 两种类型的 Mask 组件实现遮罩效果；这两个组件都有各自的使用场景，下面就来分析 Unity 中是如何应用这两个组件来为一副图像“绘制”遮罩效果。</p><span id="more"></span><h1 id="IMaskable、IMaterialModifier、IClippable-和-IClipper"><a href="#IMaskable、IMaterialModifier、IClippable-和-IClipper" class="headerlink" title="IMaskable、IMaterialModifier、IClippable 和 IClipper"></a>IMaskable、IMaterialModifier、IClippable 和 IClipper</h1><p>在分析这两个 Mask 组件之前，先来分别认识一下 IMaskable、IMaterialModifier、IClippable 以及 IClipper 接口。</p><h2 id="IMaskable"><a href="#IMaskable" class="headerlink" title="IMaskable"></a>IMaskable</h2><p>实现了 IMaskable 接口的组件都可以被用来实现遮罩效果，它有一个方法 <code>RecalculateMasking</code> 用来计算当前元素及其子元素的 Mask。</p><h2 id="IMaterialModifier"><a href="#IMaterialModifier" class="headerlink" title="IMaterialModifier"></a>IMaterialModifier</h2><p>IMaterialModifier 接口通常用于一个 Graphic 在渲染之前修改其 Material，它也有一个方法需要实现 <code>GetModifiedMaterial</code>，在这个方法中可以自定义修改渲染所用 Material 的实现方式。一个典型的使用就是 Graphic 在渲染时，会获取其对象上的所有的 IMaterialModifier，然后依次调用 <code>GetModifiedMaterial</code> 方法修改当前渲染所用的 Material。部分代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">virtual</span> Material materialForRendering</span><br><span class="line">&#123;</span><br><span class="line">    get</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="built_in">GetComponents</span>(<span class="built_in">typeof</span>(IMaterialModifier), components);</span><br><span class="line">        var currentMat = material;</span><br><span class="line">        <span class="keyword">for</span> (var i = <span class="number">0</span>; i &lt; components.Count; i++)</span><br><span class="line">            currentMat = (components[i] as IMaterialModifier).<span class="built_in">GetModifiedMaterial</span>(currentMat);</span><br><span class="line">        <span class="keyword">return</span> currentMat;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="IClipper"><a href="#IClipper" class="headerlink" title="IClipper"></a>IClipper</h2><p>实现了这个接口的组件能够接收到 Canvas 更新时裁剪的回调，它有一个需要实现的方法 <code>PerformClipping</code> 用于执行裁剪，这个方法在 Graphic Layout 重建之后以及在 Graphic 渲染重建之前执行。</p><h2 id="IClippable"><a href="#IClippable" class="headerlink" title="IClippable"></a>IClippable</h2><p>实现了这个接口的组件其所在的 UI 元素能够被父元素裁剪，这个父元素需绑定实现了 IClipper 接口的组件。</p><h1 id="Mask"><a href="#Mask" class="headerlink" title="Mask"></a>Mask</h1><p>Mask 类继承自 UIBehaviour 类，因此它具有 Unity 完整生命周期回调，同时它实现了 ICanvasRaycastFilter 接口，所以也可作为 <a href="https://blog.lujun.co/2019/07/20/unity_event_system_raycasters/">Unity Event System 中事件分发</a>射线检测对象，还实现了 IMaterialModifier 接口，这样在当前对象上的 Graphic 被渲染的时候，就会通过 Mask 类中的 <code>GetModifiedMaterial</code> 方法来修改渲染所用的材质。</p><p>为对象绑定 Mask 组件，必须在同一个对象上绑定一个 Graphic，这是因为 Mask 组件依赖 Graphic 组件来实现遮罩。将 Mask 组件绑定到对象上，如下:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_mask_8.jpeg" width="70%" height="70%" /></center><p>可以看到在 Unity 编辑器中有一个可编辑属性 Show Mask Graphic，这个属性对应了 Mask 类里面的 <code>m_ShowMaskGraphic</code> 变量，用来标记在遮罩渲染中是否输出遮罩所在的 Graphic。</p><p>下面从 Mask 类源码出发，来分析整个 Mask 组件实现遮罩效果的流程。首先看看类中的主要的成员变量:</p><table><thead><tr><th>属性</th><th>描述</th></tr></thead><tbody><tr><td><code>m_ShowMaskGraphic</code></td><td>标记在遮罩渲染中是否输出遮罩所在的 Graphic</td></tr><tr><td><code>m_Graphic</code></td><td>Mask 关联的 Graphic</td></tr><tr><td><code>m_MaskMaterial</code></td><td>用于实现遮罩加入 Stencil Buffer 过后的 Material</td></tr><tr><td><code>m_UnmaskMaterial</code></td><td>用于取消遮罩恢复 Stencil Buffer 默认值过后的 Material</td></tr><tr><td><code>m_RectTransform</code></td><td>组件所在对象的 RectTransform</td></tr></tbody></table><p>再来看看类中的相关方法:</p><h2 id="showMaskGraphic-get-set-方法"><a href="#showMaskGraphic-get-set-方法" class="headerlink" title="showMaskGraphic get&#x2F;set 方法"></a>showMaskGraphic get&#x2F;set 方法</h2><p>用来获取或设置 <code>m_ShowMaskGraphic</code> 变量，当设置这个变量时，如果设置值改变会触发当前组件所在的 Graphic 重新构建操作，如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">set</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">if</span> (m_ShowMaskGraphic == value)</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line">    m_ShowMaskGraphic = value;</span><br><span class="line">    <span class="keyword">if</span> (graphic != null)</span><br><span class="line">        <span class="comment">// set material dirty &amp; wait for rebuild ...</span></span><br><span class="line">        graphic.<span class="built_in">SetMaterialDirty</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="OnEnable-方法"><a href="#OnEnable-方法" class="headerlink" title="OnEnable 方法"></a>OnEnable 方法</h2><p>在组件 OnEnable 的时候，回调用如下代码:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (graphic != null)</span><br><span class="line">&#123;</span><br><span class="line">    graphic.canvasRenderer.hasPopInstruction = <span class="literal">true</span>;</span><br><span class="line">    graphic.<span class="built_in">SetMaterialDirty</span>();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">MaskUtilities.<span class="built_in">NotifyStencilStateChanged</span>(<span class="keyword">this</span>);</span><br></pre></td></tr></table></figure><p>从代码中可以看出，首先设置了 Graphic 所在 CanvasRender 的 <code>hasPopInstruction</code> 值为 <code>true</code>，并标记 Graphic 可以被重新构建；紧接着调用了 MaskUtilities 类的 <code>NotifyStencilStateChanged</code> 方法通知当前对象的所有绑定了实现了 IMaskable 接口组件的子元素计算它们的遮罩，MaskUtilities 类是实现遮罩效果常用的一个工具类，里面的方法在用到的时候会具体分析，先来看看 <code>NotifyStencilStateChanged</code> 方法如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="type">static</span> <span class="type">void</span> <span class="title">NotifyStencilStateChanged</span><span class="params">(Component mask)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="comment">// other code ...</span></span><br><span class="line">    mask.<span class="built_in">GetComponentsInChildren</span>(components);</span><br><span class="line">    <span class="keyword">for</span> (var i = <span class="number">0</span>; i &lt; components.Count; i++)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="comment">// is IMaskable</span></span><br><span class="line">        var toNotify = components[i] as IMaskable;</span><br><span class="line">        <span class="keyword">if</span> (toNotify != null)</span><br><span class="line">            toNotify.<span class="built_in">RecalculateMasking</span>();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// other code ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>代码很简单，通过遍历子元素上绑定的实现了 IMaskable 接口的组件，依次调用 <code>RecalculateMasking</code> 方法来计算遮罩相关数据。</p><h2 id="OnDisable-方法"><a href="#OnDisable-方法" class="headerlink" title="OnDisable 方法"></a>OnDisable 方法</h2><ol><li><p>在 OnDisable 方法中，首先所在 Graphic 调用了 <code>SetMaterialDirty</code> 方法标记 Graphic 可以被重新构建；</p></li><li><p>StencilMaterial 类是一个辅助类，管理用于实现遮罩效果的 Stencil Materil，其中具体的方法在用到时具体分析；第二步就是调用 StencilMaterial 类的 <code>Remove</code> 方法依次移除 <code>m_MaskMaterial</code> 和 <code>m_UnmaskMaterial</code>；</p></li><li><p>最后又一次调用 MaskUtilities 类的 <code>NotifyStencilStateChanged</code> 方法通知当前对象的所有绑定了实现了 IMaskable 接口组件的子元素再次计算它们的遮罩数据。</p></li></ol><h2 id="IsRaycastLocationValid-方法"><a href="#IsRaycastLocationValid-方法" class="headerlink" title="IsRaycastLocationValid 方法"></a>IsRaycastLocationValid 方法</h2><p>这是 ICanvasRaycastFilter 接口实现的一个方法，用于 Unity Event System 中事件分发射线检测过程，在这个方法内部实现最终调用了 RectTransformUtility 类的静态方法 <code>RectangleContainsScreenPoint</code> 来确定当前所在的 RectTransform 是否包含了所指的屏幕上的点。</p><h2 id="GetModifiedMaterial-方法"><a href="#GetModifiedMaterial-方法" class="headerlink" title="GetModifiedMaterial 方法"></a>GetModifiedMaterial 方法</h2><p>最后就是核心方法 <code>GetModifiedMaterial</code>，它是实现遮罩的关键方法之一。</p><p>首先回顾一下一个 Graphic <a href="https://blog.lujun.co/2018/09/01/unity_graphic_class/">重新构建</a>的过程: 当可被重新构建标记之后，在 Canvas 的渲染过程中调用了 Graphic 的 <code>Rebuild</code> 方法，然后调用 <code>UpdateMaterial</code> 方法设置渲染 Graphic 用的 Material，此时获取 Material 如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">get</span><br><span class="line">&#123;</span><br><span class="line">    <span class="built_in">GetComponents</span>(<span class="built_in">typeof</span>(IMaterialModifier), components);</span><br><span class="line">    var currentMat = material;</span><br><span class="line">    <span class="keyword">for</span> (var i = <span class="number">0</span>; i &lt; components.Count; i++)</span><br><span class="line">        currentMat = (components[i] as IMaterialModifier).<span class="built_in">GetModifiedMaterial</span>(currentMat);</span><br><span class="line">    <span class="keyword">return</span> currentMat;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Mask 需要和 Graphic 组件(具体来说是 MaskableGraphic 组件)一起绑定在同一个对象上才能发挥作用。当这个 MaskableGraphic 重新构建时，上面的代码会被调用用来获取渲染所用的材质，代码中遍历了当前对象上绑定的实现了 IMaterialModifier 接口的组件，然后调用其 <code>GetModifiedMaterial</code> 方法修改渲染用的 Material；这样的话 MaskableGraphic 和 Mask 组件的 <code>GetModifiedMaterial</code> 方法都会依次被调用。</p><p>下面就来看看这两个组件中对 <code>GetModifiedMaterial</code> 方法的实现吧！</p><ol><li>MaskableGraphic 类中的 <code>GetModifiedMaterial</code> 方法</li></ol><p>一个 MaskableGraphic 可以同 Mask 组件一起绑定(如 Image 组件所在对象上绑定一个 Mask 组件)来实现遮罩效果，也可以作为一个单独的组件绑定在某个对象上，比如创建 Unity UI 中的 Image(Image 类继承自 MaskableGraphic)元素。在这两种不同情况下，<code>GetModifiedMaterial</code> 方法发挥的作用也不一样，具体来看看代码:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">var toUse = baseMaterial;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (m_ShouldRecalculateStencil)</span><br><span class="line">&#123;</span><br><span class="line">    var rootCanvas = MaskUtilities.<span class="built_in">FindRootSortOverrideCanvas</span>(transform);</span><br><span class="line">    m_StencilValue = maskable ? MaskUtilities.<span class="built_in">GetStencilDepth</span>(transform, rootCanvas) : <span class="number">0</span>;</span><br><span class="line">    m_ShouldRecalculateStencil = <span class="literal">false</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>首先判断当前 MaskableGraphic 是否需要计算模板数据，需要就调用 MaskUtilities 类的 <code>GetStencilDepth</code> 计算模板深度 <code>m_StencilValue</code>；<code>GetStencilDepth</code> 方法代码很简单，就是遍历计算对象的父元素看是否有绑定 Mask 组件，如果则 <code>m_StencilValue</code> 模板深度值加一。</p><p>若当前 MaskableGraphic 上绑定有 Mask 组件，那么当前方法也就执行完成了，返回的修改后的材质就是传入的材质(这种情况是作为 Mask 发挥作用的 Graphic 对象)；</p><p>如果没有绑定 Mask 组件，那么会执行以下代码计算修改材质(这种情况常见场景是一个需要被遮罩的 Image，且位于 Mask 组件所在的对象之下):</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">Mask maskComponent = <span class="built_in">GetComponent</span>&lt;Mask&gt;();</span><br><span class="line"><span class="keyword">if</span> (m_StencilValue &gt; <span class="number">0</span> &amp;&amp; (maskComponent == null || !maskComponent.<span class="built_in">IsActive</span>()))</span><br><span class="line">&#123;</span><br><span class="line">    var maskMat = StencilMaterial.<span class="built_in">Add</span>(toUse, (<span class="number">1</span> &lt;&lt; m_StencilValue) - <span class="number">1</span>, StencilOp.Keep, CompareFunction.Equal, ColorWriteMask.All, (<span class="number">1</span> &lt;&lt; m_StencilValue) - <span class="number">1</span>, <span class="number">0</span>);</span><br><span class="line">    StencilMaterial.<span class="built_in">Remove</span>(m_MaskMaterial);</span><br><span class="line">    m_MaskMaterial = maskMat;</span><br><span class="line">    toUse = m_MaskMaterial;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>从代码中可以看出，如果当前 MaskableGraphic 所在对象没有绑定 Mask 组件(或处于未激活状态)且 <code>m_StencilValue</code> 大于 0(其父元素中有 Mask 组件)，就调用 StencilMaterial 类的 <code>Add</code> 方法修改传入的材质，这个方法流程如下:</p><ul><li><p>首先检测当前渲染所用的 Shader 是否定义了用于模板测试的相关变量，如未定义则返回当前 Shder 不作任何修改；</p></li><li><p>如果模板测试所用变量都定义了，遍历 MaskableGraphic 类中对于当前需求材质缓存，若从缓存中找到对应材质直接返回这个材质；</p></li><li><p>否则就根据当前渲染的材质生成一个新的材质，这个新的材质包含模板测试相关的属性，并缓存。</p></li></ul><p>第三步主要是设置了材质的 Shader 中<strong>模板测试</strong>相关数据，对应关系如下:</p><table><thead><tr><th>Shader variable</th><th>Define parameter</th><th>value</th><th>description</th></tr></thead><tbody><tr><td>Ref(Stencil)</td><td>_Stencil</td><td><code>(1 &lt;&lt; m_StencilValue) - 1</code></td><td>模板测试参考值</td></tr><tr><td>Pass(Stencil)</td><td>_StencilOp</td><td><code>StencilOp.Keep</code></td><td>模板测试(和深度测试)通过后，根据此值对模板缓冲值进行处理</td></tr><tr><td>Comp(Stencil)</td><td>_StencilComp</td><td><code>CompareFunction.Equal</code></td><td>模板测试参考值和模板缓冲值比较函数</td></tr><tr><td>ReadMask(Stencil)</td><td>_StencilReadMask</td><td><code>(1 &lt;&lt; m_StencilValue) - 1</code></td><td>对模板测试参考值和模板缓冲值进行按位与操作后再使用</td></tr><tr><td>WriteMask(Stencil)</td><td>_StencilWriteMask</td><td><code>0</code></td><td>写入模板缓冲值时对这个值按位与之后再写入</td></tr><tr><td>ColorMask(Pass)</td><td>_ColorMask</td><td><code>ColorWriteMask.All</code></td><td>输出通道</td></tr><tr><td><code>UNITY_UI_ALPHACLIP</code></td><td>_UseUIAlphaClip</td><td><code>(_StencilOp != StencilOp.Keep &amp;&amp; writeMask &gt; 0) ? 1 : 0</code></td><td>Shader 中是否使用透明度裁剪</td></tr></tbody></table><p>这样就得到了修改之后包含模板测试的材质，如果设置成功了模板测试的相关数据，Unity 视图中会呈现出来，如下图:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_mask_1.jpeg" width="70%" height="70%" /></center><p>上面模板测试条件为: </p><div class="math-display">\[StencilBufferValue \&amp; ReadMask &#x3D; StencilRef \&amp; ReadMask\]</div><p>满足上述条件的像素就会通过测试，否则该像素不会被渲染。模板测试(和深度测试)通过后，模板缓冲值不会作任何处理(因为此处设置的 Pass 为 <code>StencilOp.Keep</code>)。通过这段分析我们知道，位于 Mask 组件所在对象下的 MaskableGraphic，如果所用 Shader 满足条件则会进行模板测试(默认渲染 UI 的 Shader 就是满足这个条件的，稍后会分析这个 Shader)。</p><ol start="2"><li>Mask 类中的 <code>GetModifiedMaterial</code> 方法</li></ol><p>另一个<code>GetModifiedMaterial</code> 方法就在 Mask 类中。上面讲到渲染前 MaskableGraphic 和 Mask 类中的这个方法依次被调用来修改材质， 但是与 Mask 相呼应的 MaskableGraphic 的<code>GetModifiedMaterial</code> 方法并未对当前渲染材质做任何修改，所以主要修改任务就来到了 Mask 中。下面就来看看其实现:</p><p>首先计算模板深度值，并限制了最大模板深度不能超过 8 层，如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">var rootSortCanvas = MaskUtilities.<span class="built_in">FindRootSortOverrideCanvas</span>(transform);</span><br><span class="line">var stencilDepth = MaskUtilities.<span class="built_in">GetStencilDepth</span>(transform, rootSortCanvas);</span><br><span class="line"><span class="keyword">if</span> (stencilDepth &gt;= <span class="number">8</span>)</span><br><span class="line">&#123;</span><br><span class="line">    Debug.<span class="built_in">LogWarning</span>(<span class="string">&quot;Attempting to use a stencil mask with depth &gt; 8&quot;</span>, gameObject);</span><br><span class="line">    <span class="keyword">return</span> baseMaterial;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>紧接着如果当前 Mask 所在的模板深度为 0(父元素中没有任何 Mask 组件)，使用如下代码修改材质的 Shader:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">var maskMaterial = StencilMaterial.<span class="built_in">Add</span>(baseMaterial, <span class="number">1</span>, StencilOp.Replace, CompareFunction.Always, m_ShowMaskGraphic ? ColorWriteMask.All : <span class="number">0</span>);</span><br><span class="line">StencilMaterial.<span class="built_in">Remove</span>(m_MaskMaterial);</span><br><span class="line">m_MaskMaterial = maskMaterial;</span><br><span class="line"></span><br><span class="line">var unmaskMaterial = StencilMaterial.<span class="built_in">Add</span>(baseMaterial, <span class="number">1</span>, StencilOp.Zero, CompareFunction.Always, <span class="number">0</span>);</span><br><span class="line">StencilMaterial.<span class="built_in">Remove</span>(m_UnmaskMaterial);</span><br><span class="line">m_UnmaskMaterial = unmaskMaterial;</span><br><span class="line">graphic.canvasRenderer.popMaterialCount = <span class="number">1</span>;</span><br><span class="line">graphic.canvasRenderer.<span class="built_in">SetPopMaterial</span>(m_UnmaskMaterial, <span class="number">0</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> m_MaskMaterial;</span><br></pre></td></tr></table></figure><p>上面的代码中，首先依旧是调用 StencilMaterial 类的 <code>Add</code> 方法获取设置模板测试相关数据后的材质；其中模板测试条件是所有来到模板测试的像素全部通过，且对应的模板缓冲值都被设置为 1；</p><p>在修改完渲染所用材质之后，还更新了一个名为 <code>m_UnmaskMaterial</code> 的材质，其模板测试条件是所有来到模板测试的像素全部通过，且对应的模板缓冲值都被设置为 0，这个材质被 CanvasRender 通过调用 <code>SetPopMaterial</code> 方法设置到 CanvasRender 中，用于当前 Mask 所在对象的全部子元素渲染完成之后，重置模板缓冲区(<strong>这里会增加一个 drawCall</strong>)。</p><p>这两个材质的模板测试参数 ReadMask 和 WriteMask 都被设置为了 255，默认不影响模板测试参考值的读取以及模板缓冲值的读取和写入。如下图:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_mask_0.jpeg" width="70%" height="70%" /></center><p>回顾一下前面讲到的内容，当 MaskableGraphic 作为 Mask 组件所在对象的子元素时，若此时这个 Mask 组件对象的模板深度值为 0，那么 MaskableGraphic 的模板深度值就是 1(渲染 Shader 中模板测试的参考值也是 1)；由于父元素在子元素之前渲染(Unity UI 渲染顺序决定)，在 MaskableGraphic 渲染之时模板缓冲区某些像素对应的模板缓冲值已经被置为 1，而另一些可能是默认值 0，此时 MaskableGraphic 的像素就只有在模板缓冲值是 1 的地方才能通过测试进而有可能被渲染出来。</p><p>看完了 Mask 所在的模板深度为 0的情况，再来看看 Mask 嵌套的问题。此时更新材质代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> desiredStencilBit = <span class="number">1</span> &lt;&lt; stencilDepth;</span><br><span class="line"></span><br><span class="line">var maskMaterial2 = StencilMaterial.<span class="built_in">Add</span>(baseMaterial, desiredStencilBit | (desiredStencilBit - <span class="number">1</span>), StencilOp.Replace, CompareFunction.Equal, m_ShowMaskGraphic ? ColorWriteMask.All : <span class="number">0</span>, desiredStencilBit - <span class="number">1</span>, desiredStencilBit | (desiredStencilBit - <span class="number">1</span>));</span><br><span class="line"></span><br><span class="line"><span class="comment">// other code ...</span></span><br><span class="line">m_MaskMaterial = maskMaterial2;</span><br><span class="line"></span><br><span class="line">var unmaskMaterial2 = StencilMaterial.<span class="built_in">Add</span>(baseMaterial, desiredStencilBit - <span class="number">1</span>, StencilOp.Replace, CompareFunction.Equal, <span class="number">0</span>, desiredStencilBit - <span class="number">1</span>, desiredStencilBit | (desiredStencilBit - <span class="number">1</span>));</span><br><span class="line"></span><br><span class="line">m_UnmaskMaterial = unmaskMaterial2;</span><br><span class="line"><span class="comment">// other code ...</span></span><br></pre></td></tr></table></figure><p>同样的这里计算了 <code>m_MaskMaterial</code> 和 <code>m_UnmaskMaterial</code> 两个材质，与前面计算过程类似，只是对应模板测试的参数有点不同。</p><p>对于 Stencil 参数的设置，来看看这个对比表格:</p><table><thead><tr><th>Build Stencil\Params</th><th>Stencil Ref</th><th>Stencil Op</th><th>Stencil Comp</th><th>ReadMask</th><th>WriteMask</th></tr></thead><tbody><tr><td>Normal MaskableGraphic</td><td>(1 &lt;&lt; m_StencilValue) - 1</td><td>Keep</td><td>Equal</td><td>(1 &lt;&lt; m_StencilValue) - 1</td><td>0</td></tr><tr><td>Top Level Mask</td><td>1</td><td>Replace</td><td>Always</td><td>255</td><td>255</td></tr><tr><td>Top Level UnMask</td><td>1</td><td>Zero</td><td>Always</td><td>255</td><td>255</td></tr><tr><td>Other Level Mask</td><td>(1 &lt;&lt; stencilDepth) | (1 &lt;&lt; stencilDepth)- 1</td><td>Replace</td><td>Equal</td><td>(1 &lt;&lt; stencilDepth)- 1</td><td>(1 &lt;&lt; stencilDepth) | (1 &lt;&lt; stencilDepth)- 1</td></tr><tr><td>Other Level UnMask</td><td>(1 &lt;&lt; stencilDepth)- 1</td><td>Replace</td><td>Equal</td><td>(1 &lt;&lt; stencilDepth)- 1</td><td>(1 &lt;&lt; stencilDepth) | (1 &lt;&lt; stencilDepth)- 1</td></tr></tbody></table><p><em>其中 <code>m_StencilValue</code> 为其组件所在的模板深度值</em>。</p><p>从表格中可以看出，当 Mask 所在组件的模板深度值大于 0 时其材质 Shader 模板测试参数的计算方式以及同其他情况下参数的对比。下面通过一个 Mask 嵌套的示例来分析整个流程，如下图:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_mask_10.jpeg" width="50%" height="50%" /></center><p>其中 MaskPanel (0) 和 MaskPanel (1) 都是绑定了 Mask 组件的对象，MaskableGraphic 是一个普通的 Image，根据上面分析的过程可以知道这三个对象所在的模板深度 <code>m_StencilValue</code> 从上到下分别为 0、1 和 2。通过上面的表格能计算出渲染时模板测试的各个参数，得到如下表格:</p><table><thead><tr><th>Build Stencil\Params</th><th align="center">Stencil value</th><th align="center">Stencil Ref</th><th>Stencil Op</th><th>Stencil Comp</th><th align="center">ReadMask</th><th align="center">WriteMask</th></tr></thead><tbody><tr><td>MaskPanel (0) - Mask</td><td align="center">0</td><td align="center">1</td><td>Replace</td><td>Always</td><td align="center">255</td><td align="center">255</td></tr><tr><td>MaskPanel (0) - UnMask</td><td align="center">0</td><td align="center">1</td><td>Zero</td><td>Always</td><td align="center">255</td><td align="center">255</td></tr><tr><td>MaskPanel (1) - Mask</td><td align="center">1</td><td align="center">3</td><td>Replace</td><td>Equal</td><td align="center">1</td><td align="center">3</td></tr><tr><td>MaskPanel (1) - UnMask</td><td align="center">1</td><td align="center">1</td><td>Replace</td><td>Equal</td><td align="center">1</td><td align="center">3</td></tr><tr><td>MaskableGraphic</td><td align="center">2</td><td align="center">3</td><td>Keep</td><td>Equal</td><td align="center">3</td><td align="center">0</td></tr></tbody></table><p>从表格中可以总结以下几点:</p><ul><li><p>从第二层(Stencil value 为 1)开始往下，每层渲染模板测试的 ReadMask 都是上一层的模板测试参考值，这是为了模板测试时能够完整的读取到之前 Stencil buffer 的值；</p></li><li><p>从第二层(Stencil value 为 1)开始往下(不包括非 Mask 的 UI 元素)，其模板测试参数 WriteMask 都和模板测试参考值相同，这是为了能在模板测试通过时能够将当前模板测试参考值完整写入到 Stencil buffer 中；</p></li><li><p>从第二层(Stencil value 为 1)开始往下(不包括非 Mask 的 UI 元素)，用于 UnMask 的模板测试参数 ReadMask 都和模板测试参考值相同。<strong>这是因为在重置 Stencil buffer 的时候是从下往上一个接一个 Mask 所在的 Shader 来渲染进行</strong>，此时模板测试参数 ReadMask 和模板测试参考值相同能够保证模板测试通过且让  Stencil buffer 的值恢复到模板测试参考值；</p></li><li><p>从第二层(Stencil value 为 1)开始往下(不包括非 Mask 的 UI 元素)，用于 UnMask 的模板测试参数 WriteMask 与当前层 Mask 时模板测试参考值相同，用于 UnMask 的模板测试参考值与上一层 Mask 时模板测试参考值相同，综合这两个条件，在重置当前层的 Stencil buffer 的时候就能够将模板测试缓冲区的值重置到上一层 Mask 时模板测试参考值。</p></li></ul><p>接着从上往下依次开始渲染，假设现在渲染的一块像素区域如下:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_mask_11.jpeg" width="20%" height="20%" /></center><p>上图中每一个格子对应一个像素，初始状态每个像素上的 Stencil Buffer 值为 0，现在第一个 Mask 所在的 UI 元素 MaskPanel (0) 开始渲染(使用 <code>UI-Default.shader</code>)，渲染完成之后各个像素的 Stencil Buffer 值如下图:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_mask_12.jpeg" width="20%" height="20%" /></center><p>从上图中可以看出，有一部分 Stencil Buffer 值如之前分析的那样被设置成了 1，但是不应该是根据模板测试参数所有的 Stencil Buffer 值都被设置成 1 吗？这里是因为在使用 <code>UI-Default.shader</code> 的时候，在模板测试之前会进行 Alpha 测试，部分像素在那一步就没有通过，因此还没来得及到模板测试就被 discard 了，所以会有部分像素的 Stencil Buffer 值为 0。</p><p>注意看上图中那些 Stencil Buffer 值被置为 1 的像素其颜色也输出了，这里 Mask 所在 Graphic 颜色能否输出就是之前所说的 Mask 类的 <code>m_ShowMaskGraphic</code> 决定的，当这个值被设置为 <code>true</code>，Shader 中的 ColorMask 会被设置为 <code>ColorWriteMask.All</code>，从而能输出所有颜色通道值，否则<code>m_ShowMaskGraphic</code> 被设置为 <code>false</code> 将不会有任何颜色通道值输出。</p><p>下面再来看看第二层 Mask 所在 MaskPanel (1) 的渲染，结果如下图:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_mask_13.jpeg" width="20%" height="20%" /></center><p>计算过程大致如下: 将当前模板测试参考值和 Stencil Buffer 值都与 ReadMask 按位与操作后，如果这两个结果值相等则通过模板测试，紧接着若是深度测试也通过则将当前模板测试参考值与 WriteMask 进行按位与操作，得到的值写入 Stencil Buffer。</p><p>通过上述的过程，原来 Stencil Buffer 值为 1 的像素被设置成了 3；像素的颜色值能否输出也和自身 Mask 组件上的 <code>m_ShowMaskGraphic</code> 相关，若输出颜色通道值则最后像素颜色值根据 Shader 中 Blend 的设置(<code>UI-Default.shader</code> 默认 <code>Blend SrcAlpha OneMinusSrcAlpha</code>)进行混合。</p><p>最后来看看 MaskableGraphic 的渲染，结果如下图:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_mask_14.jpeg" width="20%" height="20%" /></center><p>到这一步，就很简单了。如上图所示，Stencil Buffer 值都没有发生改变，但是像素颜色发生了变化，这一渲染中模板测试的过程如下: 当前模板测试参考值是 3，而上一步仅部分 Stencil Buffer 值计算设置为 3，所以在这部分像素模板测试通过，像素颜色通道值通过混合后输出为新的值；而上一步 Stencil Buffer 值为 0 的像素这里模板测试失败，因此被 discard 无法渲染，至此一个 MaskableGraphic 遮罩效果就实现了。</p><p>前面分析 Mask 类的 <code>GetModifiedMaterial</code> 方法时讲到过，这个方法除了修改用于 Mask 的材质，还会修改用于 UnMask 的材质，下面就来看看 UnMask 的过程。</p><p>首先是 UI 元素 MaskPanel (1) UnMask 时渲染情况，如下图:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_mask_17.jpeg" width="20%" height="20%" /></center><p>从上图中可以看出，根据 MaskPanel (1) 元素 UnMask 所用 Shader 的模板测试相关参数和条件方式，将 Stencil Buffer 值为 3 的像素的 Stencil Buffer 值更新为了 1，这个过程如下:</p><p>UnMask 所在的 Shader 的模板参考值以及 Stencil Buffer 值与当前计算所用的 ReadMask 进行按位与操作，若结果相等则模板测试通过(假设深度测试也通过)，那么将当前使用的模板测试参考值和 WriteMask 进行按位与操作，得到的值写入 Stencil Buffer。</p><p>需要注意的是，UnMask 渲染流程中像素的颜色值没有发生任何变化，这是因为<strong>所有的 UnMask 使用材质的 Shader 中的 ColorMask 参数被设置为 <code>0</code></strong>，因此不会输出任何颜色通道值。</p><p>然后是 UI 元素 MaskPanel (0) UnMask 时渲染情况，如下图:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_mask_18.jpeg" width="20%" height="20%" /></center><p>这个过程和上一步类似，只是使用的模板测试参数和计算方式不一样。经过 UnMask 流程最终所有像素对应的 Stencil buffer 值全部重置为 0。这里再强调一下，这里<strong>每一次 UnMask 都会增加一个 drawCall</strong>，因此项目中尽可能注意少用或者不用 Mask 实现遮罩。如下图是 Frame Debug 中 MaskPanel (1) 和 MaskPanel (0) 两个元素 UnMask 渲染时参数:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_mask_19.jpg" width="100%" height="100%" /></center><h1 id="Unity-UI-默认-Shader"><a href="#Unity-UI-默认-Shader" class="headerlink" title="Unity UI 默认 Shader"></a>Unity UI 默认 Shader</h1><p>上面基本上分析完了遮罩实现的流程，部分 Shader 的知识点也有分析，下面再来看看 Unity UI 默认 Shader 代码，首先是属性定义</p><figure class="highlight glsl"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">Properties</span><br><span class="line">&#123;</span><br><span class="line">    [PerRendererData] _MainTex (&quot;Sprite Texture&quot;, <span class="number">2</span>D) = &quot;white&quot; &#123;&#125;</span><br><span class="line">    _Color (&quot;Tint&quot;, Color) = (<span class="number">1</span>,<span class="number">1</span>,<span class="number">1</span>,<span class="number">1</span>)</span><br><span class="line"></span><br><span class="line">    _StencilComp (&quot;Stencil Comparison&quot;, Float) = <span class="number">8</span></span><br><span class="line">    _Stencil (&quot;Stencil ID&quot;, Float) = <span class="number">0</span></span><br><span class="line">    _StencilOp (&quot;Stencil Operation&quot;, Float) = <span class="number">0</span></span><br><span class="line">    _StencilWriteMask (&quot;Stencil Write Mask&quot;, Float) = <span class="number">255</span></span><br><span class="line">    _StencilReadMask (&quot;Stencil Read Mask&quot;, Float) = <span class="number">255</span></span><br><span class="line"></span><br><span class="line">    _ColorMask (&quot;Color Mask&quot;, Float) = <span class="number">15</span></span><br><span class="line"></span><br><span class="line">    [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip (&quot;Use Alpha Clip&quot;, Float) = <span class="number">0</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面的代码中除了定义常规的纹理、颜色等属性之外，还有用于模板测试的一些变量。<br>接下来看看模板测试的代码，如下:</p><figure class="highlight glsl"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">Stencil</span><br><span class="line">&#123;</span><br><span class="line">    Ref [_Stencil]</span><br><span class="line">    Comp [_StencilComp]</span><br><span class="line">    Pass [_StencilOp]</span><br><span class="line">    ReadMask [_StencilReadMask]</span><br><span class="line">    WriteMask [_StencilWriteMask]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>代码中主要定义了模板测试参考值 <code>Ref</code>、参考值与 Stencil buffer 值比较函数 <code>Comp</code>、模板测试(和深度测试)通过对 Stencil buffer 值的处理方式 <code>Pass</code>、写 Stencil buffer 值时的写入(按位与)掩码、模板测试参考值和 Stencil buffer 值读取(按位与)掩码 <code>ReadMask</code>。</p><p>接着就是渲染管线部分配置，如下:</p><figure class="highlight glsl"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Cull Off</span><br><span class="line">Lighting Off</span><br><span class="line">ZWrite Off</span><br><span class="line">ZTest [unity_GUIZTestMode]</span><br><span class="line">Blend SrcAlpha OneMinusSrcAlpha</span><br><span class="line">ColorMask [_ColorMask]</span><br></pre></td></tr></table></figure><p>这里设置了 ZTest 方式、混合模式、ColorMask 等配置。</p><p>然后是顶点着色器和片元着色器的代码:</p><figure class="highlight glsl"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">v2f vert(appdata_t v)</span><br><span class="line">&#123;</span><br><span class="line">    v2f OUT;</span><br><span class="line">    <span class="comment">// other code ...</span></span><br><span class="line">    OUT.worldPosition = v.vertex;</span><br><span class="line">    OUT.vertex = UnityObjectToClipPos(OUT.worldPosition);</span><br><span class="line">    OUT.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);</span><br><span class="line">    OUT.color = v.color * _Color;</span><br><span class="line">    <span class="keyword">return</span> OUT;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>顶点着色器代码很简单，主要就是顶点变换、UV 坐标校正以及顶点颜色计算。下面主要来看看片元着色器部分代码:</p><figure class="highlight glsl"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">fixed4 frag(v2f IN) : SV_Target</span><br><span class="line">&#123;</span><br><span class="line">    half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;</span><br><span class="line"></span><br><span class="line">    <span class="meta">#ifdef UNITY_UI_CLIP_RECT</span></span><br><span class="line">    color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);</span><br><span class="line">    <span class="meta">#endif</span></span><br><span class="line"></span><br><span class="line">    <span class="meta">#ifdef UNITY_UI_ALPHACLIP</span></span><br><span class="line">    clip (color.a - <span class="number">0.001</span>);</span><br><span class="line">    <span class="meta">#endif</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> color;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>代码中首先对纹理进行采样，然后有分两种情况对像素进行处理:</p><ul><li><p>如果是 <code>UNITY_UI_CLIP_RECT</code> 就使用 <code>UnityGet2DClipping</code> 函数判断当前点是否在遮罩区域 <code>_ClipRect</code> 内，如果在这个方法就返回 1，否则返回 0；所以最终输出颜色的 Alpha 也就跟当前像素是否在遮罩区域类有关，在遮罩区域内 Alpha 值保持原来的值输出，否则 Alpha 值为 0。这种方式用于 RectMask2D 遮罩使用，下面会分析到。</p></li><li><p><code>UNITY_UI_ALPHACLIP</code> 这种方式就是我们分析的 Mask 遮罩实现的关键之一。<strong>Mask 组件所在的 MaskableGraphic 渲染时，如果某个像素纹理采样计算之后得到的 Alpha 值小于 0.001，那么在这里会使用 <code>clip</code> 函数 discard 这个像素，这也就是为什么之前分析的时候讲到可能会有部分像素所在的 Stencil buffer 值为初始值 0 的原因</strong>。这样也就实现了用纹理 Alpha 值来控制遮罩。</p></li></ul><h1 id="RectMask2D"><a href="#RectMask2D" class="headerlink" title="RectMask2D"></a>RectMask2D</h1><p>RectMask2D 和 Mask 相似，其主要区别在于 RectMask2D 直接实现矩形遮罩效果(将子元素显示区域控制在一个矩形框内)。</p><p>RectMask2D 类继承自 UIBehaviour 类，同样它具有 Unity 完整生命周期回调，同时它实现了 ICanvasRaycastFilter 接口，所以也可作为 <a href="https://blog.lujun.co/2019/07/20/unity_event_system_raycasters/">Unity Event System 中事件分发</a>射线检测对象，还实现了 IClipper 接口用于更新裁剪相关信息。下面就来分析一下 RectMask2D 类的代码。</p><h2 id="OnEnable-方法-1"><a href="#OnEnable-方法-1" class="headerlink" title="OnEnable 方法"></a>OnEnable 方法</h2><p>还是从 <code>OnEnable</code> 方法开始，首先调用 ClipperRegistry 类 <code>Register</code> 方法将自己注册，这样在 Canvas 执行更新时，自身就能收到回调从而调用 <code>PerformClipping</code> 方法；紧接着调用 MaskUtilities 类的 <code>Notify2DMaskStateChanged</code> 方法通知子元素重新计算裁剪相关的参数信息。</p><p>比如一个 MaskableGeaphic 元素位于 RectMask2D 下(MaskableGeaphic 实现了 IClippable 接口)，当调用 <code>Notify2DMaskStateChanged</code> 方法时 MaskableGeaphic 的 <code>RecalculateClipping</code> 方法会被回调，从而 MaskableGeaphic 的父 RectMask2D 元素会被更新(具体是 MaskableGeaphic 的 <code>RecalculateClipping</code> 方法又调用其自身的 <code>UpdateClipParent</code> 方法来寻找最合适的父 RectMask2D 元素，找到后调用之前父 RectMask2D 的 <code>RemoveClippable</code> 方法将自身移除，然后调用新父 RectMask2D 的 <code>AddClippable</code> 将自身添加到 RectMask2D 中)。</p><h2 id="OnDisable-方法-1"><a href="#OnDisable-方法-1" class="headerlink" title="OnDisable 方法"></a>OnDisable 方法</h2><p>这个方法首先清除相关数据，然后从 ClipperRegistry 中反注册自己，最后再次调用 MaskUtilities 类的 <code>Notify2DMaskStateChanged</code> 方法通知子元素重新计算裁剪相关的参数信息。</p><h2 id="IsRaycastLocationValid-方法-1"><a href="#IsRaycastLocationValid-方法-1" class="headerlink" title="IsRaycastLocationValid 方法"></a>IsRaycastLocationValid 方法</h2><p>这个方法和 Mask 中的相同，这里不再分析。</p><h2 id="AddClippable-方法"><a href="#AddClippable-方法" class="headerlink" title="AddClippable 方法"></a>AddClippable 方法</h2><p>将一个 IClippable 添加到自身的跟踪列表中，用于后续的裁剪计算等。</p><h2 id="RemoveClippable-方法"><a href="#RemoveClippable-方法" class="headerlink" title="RemoveClippable 方法"></a>RemoveClippable 方法</h2><p>从自身的 IClippable 跟踪列表中移除一个 IClippable，代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="type">void</span> <span class="title">RemoveClippable</span><span class="params">(IClippable clippable)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="comment">// other code ...</span></span><br><span class="line">    clippable.<span class="built_in">SetClipRect</span>(<span class="keyword">new</span> <span class="built_in">Rect</span>(), <span class="literal">false</span>);</span><br><span class="line"></span><br><span class="line">    MaskableGraphic maskable = clippable as MaskableGraphic;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (maskable == null)</span><br><span class="line">        m_ClipTargets.<span class="built_in">Remove</span>(clippable);</span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">        m_MaskableTargets.<span class="built_in">Remove</span>(maskable);</span><br><span class="line">    <span class="comment">// other code ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>从代码中可以看出在移除之前，首先调用了 IClippable 的 <code>SetClipRect</code> 方法使得这个 IClippable 不再计算 Rect 裁剪。比如 MaskableGeaphic 的 <code>SetClipRect</code> 就调用了 <code>canvasRenderer.DisableRectClipping()</code> 来禁用裁剪。</p><h2 id="PerformClipping-方法"><a href="#PerformClipping-方法" class="headerlink" title="PerformClipping 方法"></a>PerformClipping 方法</h2><p>当 Canvas 执行更新 ClipperRegistry 类的 <code>Cull</code> 方法在 Layout Rebuild 之后、在 Graphic Rebuild 之前被调用，从而 RectMask2D 的 <code>PerformClipping</code> 被回调。下面就来分析这个方法的执行过程:</p><p>首先如果 <code>m_ShouldRecalculateClipRects</code> 为 <code>true</code>，那么执行如下代码计算当前 RectMask2D 所在元素的所有父 RectMask2D 元素(包括自身):</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">MaskUtilities.<span class="built_in">GetRectMasksForClip</span>(<span class="keyword">this</span>, m_Clippers);</span><br></pre></td></tr></table></figure><p>接着调用 Clipping 类的 <code>FindCullAndClipWorldRect</code> 方法计算裁剪的矩形区域 <code>clipRect</code>。代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="type">static</span> Rect <span class="title">FindCullAndClipWorldRect</span><span class="params">(List&lt;RectMask2D&gt; rectMaskParents, out <span class="type">bool</span> validRect)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="comment">// no RectMask2D return empty Rect, validRect is &#x27;false&#x27;</span></span><br><span class="line"></span><br><span class="line">    Rect current = rectMaskParents[<span class="number">0</span>].canvasRect;</span><br><span class="line">    <span class="type">float</span> xMin = current.xMin;</span><br><span class="line">    <span class="type">float</span> xMax = current.xMax;</span><br><span class="line">    <span class="type">float</span> yMin = current.yMin;</span><br><span class="line">    <span class="type">float</span> yMax = current.yMax;</span><br><span class="line">    <span class="keyword">for</span> (var i = <span class="number">1</span>; i &lt; rectMaskParents.Count; ++i)</span><br><span class="line">    &#123;</span><br><span class="line">        current = rectMaskParents[i].canvasRect;</span><br><span class="line">        <span class="keyword">if</span> (xMin &lt; current.xMin) xMin = current.xMin;</span><br><span class="line">        <span class="keyword">if</span> (yMin &lt; current.yMin) yMin = current.yMin;</span><br><span class="line">        <span class="keyword">if</span> (xMax &gt; current.xMax) xMax = current.xMax;</span><br><span class="line">        <span class="keyword">if</span> (yMax &gt; current.yMax) yMax = current.yMax;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    validRect = xMax &gt; xMin &amp;&amp; yMax &gt; yMin;</span><br><span class="line">    <span class="keyword">if</span> (validRect)</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="built_in">Rect</span>(xMin, yMin, xMax - xMin, yMax - yMin);</span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="built_in">Rect</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面的代码很简单，就是遍历上一步寻找到的所有的 RectMask2D 元素，然后通过调用 RectangularVertexClipper 的 <code>GetCanvasRect</code> 方法获取这个 RectMask2D 元素在 Canvas 空间下的 Rect，最终保留所有 RectMask2D 的重叠区域来作为矩形裁剪区域(如果是合法的) <code>clipRect</code>。</p><p>接着判断上一步计算得到的重叠矩形裁剪区域 <code>clipRect</code> 是否和根 Canvas 所在矩形区域重叠，如果 Canvas Render Mode 是 <code>RenderMode.ScreenSpaceXX</code> 且 <code>clipRect</code> 和根 Canvas 所在矩形区域不重叠，那么将不会渲染其内容。代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">RenderMode renderMode = Canvas.rootCanvas.renderMode;</span><br><span class="line"><span class="type">bool</span> maskIsCulled = (renderMode == RenderMode.ScreenSpaceCamera || renderMode == RenderMode.ScreenSpaceOverlay) &amp;&amp; !clipRect.<span class="built_in">Overlaps</span>(rootCanvasRect, <span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (maskIsCulled)</span><br><span class="line">&#123;</span><br><span class="line">    <span class="comment">// set an invalid rect to allow calls to avoid some processing ...</span></span><br><span class="line">    clipRect = Rect.zero;</span><br><span class="line">    validRect = <span class="literal">false</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>最后就设置 IClippable 跟踪列表中子元素的裁剪区域。</p><ul><li>如果当前计算的 <code>clipRect</code> 和上次的裁剪矩形区域不相等，那么执行以下代码:</li></ul><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">foreach (IClippable clipTarget in m_ClipTargets)</span><br><span class="line">&#123;</span><br><span class="line">    clipTarget.<span class="built_in">SetClipRect</span>(clipRect, validRect);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">foreach (MaskableGraphic maskableTarget in m_MaskableTargets)</span><br><span class="line">&#123;</span><br><span class="line">    maskableTarget.<span class="built_in">SetClipRect</span>(clipRect, validRect);</span><br><span class="line">    maskableTarget.<span class="built_in">Cull</span>(clipRect, validRect);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面的代码中调用 IClippable 的 <code>SetClipRect</code> 方法设置 ClipRect。这里以 MaskableGraphic 为例，最终调用的代码是 <code>canvasRenderer.EnableRectClipping(clipRect)</code>，也就是渲染这个 MaskableGraphic 的 CanvasRender 会启用 Rect Clip 并设置渲染 Shader 中的 <code>_ClipRect</code> 变量的值，这样当渲染执行的时候就能够实现矩形遮罩效果。</p><p>对于 MaskableGraphic 还有一步，就是调用其 <code>Cull</code> 方法更新 CanvasRender 的 cull 值。</p><ul><li><p>如果当前计算的 <code>clipRect</code> 和上次的裁剪矩形区域相等但是 <code>m_ForceClip</code> 为 <code>true</code>，那么强制调用 <code>SetClipRect</code> 方法设置 ClipRect。这里有一点小不同是对于最后 MaskableGraphic 的 <code>Cull</code> 方法的调用，仅在当前 UI 元素有任何影响几何更新的变动时(CanvasRender 的 <code>hasMoved</code> 被标记为 <code>true</code>) <code>Cull</code> 方法才会被调用。</p></li><li><p>最后如果上面两种情况都不满足，那么就只会对绑定了 MaskableGraphic 组件的子元素执行 <code>Cull</code> 方法更新 CanvasRender 的 cull 值(这个元素所在 CanvasRender 的 <code>hasMoved</code> 需标记为 <code>true</code>)。</p></li></ul><p>至此 RectMask2D 实现遮罩的流程代码也就分析完了。</p><p>分析完 RectMask2D 的重要代码部分，可以看出其和 Mask 的不同之处在于 RectMask2D 并未使用模板测试来计算遮罩，因此相比之下 RectMask2D 会少一个 drawCall，因此相比使用 Mask 性能会好一些。但是 RectMask2D 实现遮罩效果被限制在一个矩形区域内，相比 Mask 遮罩就显得没有那么灵活，所以具体使用何种方式实现遮罩视项目需求而定。</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ul><li><p><a href="https://docs.unity3d.com/Manual/script-Mask.html">Unity Manual - Mask</a></p></li><li><p><a href="https://github.com/TwoTailsGames/Unity-Built-in-Shaders/blob/master/DefaultResourcesExtra/UI/UI-Default.shader">UI-Default.shader</a></p></li><li><p><a href="https://docs.unity3d.com/Manual/script-RectMask2D.html">Unity Manual - RectMask2D</a></p></li></ul>]]>
    </content>
    <id>https://lujun.pages.dev/2019/09/05/unity_mask/</id>
    <link href="https://lujun.pages.dev/2019/09/05/unity_mask/"/>
    <published>2019-09-05T00:02:29.000Z</published>
    <summary>
      <![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><blockquote>
<p>A Mask is not a visible UI control but rather a way to modify the appearance of a control’s child elements. the parent will be visible.</p>
</blockquote>
<p>Mask 是一个不可见的用于控制子元素遮罩显示的一个组件，在 Unity UI 中分别有 Mask 和 RectMask2D 两种类型的 Mask 组件实现遮罩效果；这两个组件都有各自的使用场景，下面就来分析 Unity 中是如何应用这两个组件来为一副图像“绘制”遮罩效果。</p>]]>
    </summary>
    <title>Unity 遮罩“绘制”图</title>
    <updated>2026-06-22T14:03:58.804Z</updated>
  </entry>
  <entry>
    <author>
      <name>lujun</name>
    </author>
    <category term="Unity" scheme="https://lujun.pages.dev/tags/Unity/"/>
    <content>
      <![CDATA[<script src="/assets/js/APlayer.min.js"> </script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>CanvasGroup 组件用来调整一组 UI 元素的部分属性，这个组件的出现就是为了方便开发者实现一些批量更新操作，而不用单独修改多个 UI 元素的属性。</p><span id="more"></span><h2 id="Alpha"><a href="#Alpha" class="headerlink" title="Alpha"></a>Alpha</h2><p>调整当前 group 下面所有 UI 元素的透明度，UI 元素自身的透明度值不会改变，但是最终的透明度是当前设置的 Alpha 和 UI 元素透明度乘积。</p><h2 id="Interactable"><a href="#Interactable" class="headerlink" title="Interactable"></a>Interactable</h2><p>设定当前 group 下的 UI 元是否响应输入事件(可以交互)。</p><p>对于这个配置，我们以 Button 为例来分析这个属性是如何生效的。对于 Button 组件来说，当它响应自定义绑定的函数 OnClick，是在接收到 EventSystem 发送的 Press 事件，代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="type">void</span> <span class="title">Press</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (!<span class="built_in">IsActive</span>() || !<span class="built_in">IsInteractable</span>())</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line">    UISystemProfilerApi.<span class="built_in">AddMarker</span>(<span class="string">&quot;Button.onClick&quot;</span>, <span class="keyword">this</span>);</span><br><span class="line">    m_OnClick.<span class="built_in">Invoke</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>从代码中可以看出，当 Button 组件不是激活状态或者是不响应交互状态，那么就不会执行回调函数，这里我们主要看 <code>IsInteractable()</code> 方法。</p><p>Button 类继承自 Selectable 类，它没有覆写<code>IsInteractable()</code> 方法，所以看Selectable 类中该方法的实现:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">virtual</span> <span class="type">bool</span> <span class="title">IsInteractable</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">return</span> m_GroupsAllowInteraction &amp;&amp; m_Interactable;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>该方法实现很简单，当 CanvasGroup 组件的 Interactable 为 <code>true</code> 且自身的 Interactable 设置也为 <code>true</code>，那么当前的 Button 就能响应交互事件。</p><p>那么 CanvasGroup 组件的 Interactable 配置是如何赋值到 Button 组件的 <code>m_GroupsAllowInteraction</code> 成员变量中的了？Selectable 类继承自 UIBehaviour 类，因此当有 CanvasGroup 组件配置更新时，<code>OnCanvasGroupChanged（)</code> 方法会被回调(<strong>仅能够影响这个组件的 Canvas Group 更新才会收到回调，比如父节点或自身节点上的 Canvas Group 更新</strong>)，正是在这个方法里面，CanvasGroup 组件的  Interactable 配置被更新到了 Button 组件的 <code>m_GroupsAllowInteraction</code> 变量中。代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">override</span> <span class="type">void</span> <span class="title">OnCanvasGroupChanged</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    var groupAllowInteraction = <span class="literal">true</span>;</span><br><span class="line">    Transform t = transform;</span><br><span class="line">    <span class="keyword">while</span> (t != null)</span><br><span class="line">    &#123;</span><br><span class="line">        t.<span class="built_in">GetComponents</span>(m_CanvasGroupCache);</span><br><span class="line">        <span class="type">bool</span> shouldBreak = <span class="literal">false</span>;</span><br><span class="line">        <span class="keyword">for</span> (var i = <span class="number">0</span>; i &lt; m_CanvasGroupCache.Count; i++)</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="keyword">if</span> (!m_CanvasGroupCache[i].interactable)</span><br><span class="line">            &#123;</span><br><span class="line">                groupAllowInteraction = <span class="literal">false</span>;</span><br><span class="line">                shouldBreak = <span class="literal">true</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="comment">// do &#x27;ignoreParentGroups&#x27; code...</span></span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (shouldBreak)</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">        <span class="comment">// other code...</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 更新 CanvasGroup 的 Interactable 到 m_GroupsAllowInteraction</span></span><br><span class="line">    <span class="keyword">if</span> (groupAllowInteraction != m_GroupsAllowInteraction)</span><br><span class="line">    &#123;</span><br><span class="line">        m_GroupsAllowInteraction = groupAllowInteraction;</span><br><span class="line">        <span class="built_in">OnSetProperty</span>();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Block-Raycasts"><a href="#Block-Raycasts" class="headerlink" title="Block Raycasts"></a>Block Raycasts</h2><p>UI 元素是否接收射线检测，在 Event System 中检测事件拦截对象时使用。</p><p>这里还是以 Button 组件为例分析。在 Event System 射线检测拦截事件对象这一步时，对于 UI 元素的检测会使用到 Graphic 的 <code>Raycast(Vector2 sp, Camera eventCamera)</code> 方法，其中就根据这个值判断了射线检测是否成功，如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">var filter = components[i] as ICanvasRaycastFilter;</span><br><span class="line"><span class="keyword">if</span> (filter == null)</span><br><span class="line">    <span class="keyword">continue</span>;</span><br><span class="line"></span><br><span class="line">var raycastValid = <span class="literal">true</span>;</span><br><span class="line">var group = components[i] as CanvasGroup;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (group != null)</span><br><span class="line">&#123;</span><br><span class="line">    <span class="comment">// other code... </span></span><br><span class="line">    raycastValid = filter.<span class="built_in">IsRaycastLocationValid</span>(sp, eventCamera);</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// other code...</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (!raycastValid)</span><br><span class="line">&#123;</span><br><span class="line">    ListPool&lt;Component&gt;.<span class="built_in">Release</span>(components);</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>由于 Button 类及其祖先类都没有实现 ICanvasRaycastFilter 接口，而 CanvasGroup 类实现了；当在 Button 对象上添加 CanvasGroup 组件，raycastValid 结果就由 CanvasGroup 类的 <code>IsRaycastLocationValid()</code> 方法决定，CanvasGroup 类中的 <code>IsRaycastLocationValid()</code> 方法返回值就是设置的 Block Raycasts，所以当 Block Raycasts 设置为 <code>true</code>，则会通过射线检测。当然在上面只是 Graphic 类中射线检测代码的不部分，要通过完整的检测还包括其它条件判断以及整个节点链的测试通过等，具体可阅读 <a href="https://blog.lujun.co/2019/07/20/unity_event_system_raycasters/">Unity Raycasters 剖析</a> 一文。</p><h2 id="Ignore-Parent-Groups"><a href="#Ignore-Parent-Groups" class="headerlink" title="Ignore Parent Groups"></a>Ignore Parent Groups</h2><p>忽略祖先节点对象中 CanvasGroup 组件的设置。</p><p>如果当前 CanvasGroup 所在对象的父节点或是祖先节点也绑定了 CanvasGroup 组件，那么它们的 CanvasGroup 组件设置也会影响当前对象的行为。</p><p>这里我们接着以 Button 射线检测为例，看看 Graphic 类中的射线检测另一部分代码:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line">var t = transform;</span><br><span class="line">var components = ListPool&lt;Component&gt;.<span class="built_in">Get</span>();</span><br><span class="line"><span class="type">bool</span> ignoreParentGroups = <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span> (t != null)</span><br><span class="line">&#123;</span><br><span class="line">    t.<span class="built_in">GetComponents</span>(components);</span><br><span class="line">    <span class="keyword">for</span> (var i = <span class="number">0</span>; i &lt; components.Count; i++)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="comment">// other code...</span></span><br><span class="line">        var filter = components[i] as ICanvasRaycastFilter;</span><br><span class="line">        <span class="keyword">if</span> (filter == null) <span class="keyword">continue</span>;</span><br><span class="line"></span><br><span class="line">        var raycastValid = <span class="literal">true</span>;</span><br><span class="line">        var group = components[i] as CanvasGroup;</span><br><span class="line">        <span class="keyword">if</span> (group != null)</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="keyword">if</span> (ignoreParentGroups == <span class="literal">false</span> &amp;&amp; group.ignoreParentGroups)</span><br><span class="line">            &#123;</span><br><span class="line">                ignoreParentGroups = <span class="literal">true</span>;</span><br><span class="line">                raycastValid = filter.<span class="built_in">IsRaycastLocationValid</span>(sp, eventCamera);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">else</span> <span class="keyword">if</span> (!ignoreParentGroups)</span><br><span class="line">                raycastValid = filter.<span class="built_in">IsRaycastLocationValid</span>(sp, eventCamera);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (!raycastValid) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    t = continueTraversal ? t.parent : null;</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// other code...</span></span><br><span class="line"><span class="keyword">return</span> <span class="literal">true</span>;</span><br></pre></td></tr></table></figure><p>当 Button 对象绑定了 CanvasGroup 组件并且设置其 Ignore Parent Groups 为 <code>true</code>，那么上面代码中变量 <code>raycastValid</code> 的值就只由当前 CanvasGroup 组件设置的 Block Raycasts 值决定；否则如果设置的 Ignore Parent Groups 为 <code>false</code>，那么变量 <code>raycastValid</code> 的值会一层层向上冒泡去求得。</p><p>所以 Ignore Parent Groups 这个配置会忽略祖先节点的 CanvasGroup 组件配置。</p>]]>
    </content>
    <id>https://lujun.pages.dev/2019/08/31/unity_canvas_group/</id>
    <link href="https://lujun.pages.dev/2019/08/31/unity_canvas_group/"/>
    <published>2019-08-31T10:02:29.000Z</published>
    <summary>
      <![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>CanvasGroup 组件用来调整一组 UI 元素的部分属性，这个组件的出现就是为了方便开发者实现一些批量更新操作，而不用单独修改多个 UI 元素的属性。</p>]]>
    </summary>
    <title>CanvasGroup 组件中的属性是如何被应用的?</title>
    <updated>2026-06-22T14:03:58.804Z</updated>
  </entry>
  <entry>
    <author>
      <name>lujun</name>
    </author>
    <category term="Unity" scheme="https://lujun.pages.dev/tags/Unity/"/>
    <content>
      <![CDATA[<script src="/assets/js/APlayer.min.js"> </script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>Canvas 是所有 UI 组件被布局和渲染的空间，这些 UI 组件都必须位于 Canvas 子节点。在 Unity 编辑器中，我们可以方便的进行 Canvas UI 编辑。</p><p>一个 Canvas 系统通常包含以下几个组件:</p><ul><li><p>Canvas 组件(Canvas 对象的基础组件)</p></li><li><p>CanvasScaler 组件(控制所有 UI 元素的缩放)</p></li><li><p><a href="https://blog.lujun.co/2019/07/20/unity_event_system_raycasters/">GraphicRaycaster</a> 组件(用于 Event System 中的射线检测)</p></li></ul><p>接下来，我们就一起来看看每个组件的分工与作用。</p><span id="more"></span><h1 id="先来简单介绍-Canvas-组件"><a href="#先来简单介绍-Canvas-组件" class="headerlink" title="先来简单介绍 Canvas 组件"></a>先来简单介绍 Canvas 组件</h1><p>Canvas 组件(类)继承自 <a href="https://docs.unity3d.com/ScriptReference/Behaviour.html">Behaviour</a> 类，因此它可以被 enable 或 disable，同时它继承了祖先类的属性和方法。在其内部包含一些属性，可以在 Unity 编辑器中直接设置。这些属性配置用来描述 Canvas 系统是如何运作的，如下:</p><table><thead><tr><th align="left">属性</th><th align="left">描述</th></tr></thead><tbody><tr><td align="left">Render Mode</td><td align="left">UI 的渲染方式，包含 <code>Screen Space-Overlay</code>、<code>Screen Space-Camera</code>和 <code>World Space</code> 三个值</td></tr><tr><td align="left">Pixel Perfect (仅 <code>Screen Space-xx</code> 模式)</td><td align="left">是否在渲染 UI 元素时进行像素对齐，启用后元素看起来会更清晰</td></tr><tr><td align="left">Render Camera(仅 <code>Screen Space-Camera</code> 模式)</td><td align="left"><code>Screen Space-Camera</code> 模式下渲染 UI 元素的 Camera</td></tr><tr><td align="left">Event Camera(仅 <code>World Space</code> 模式)</td><td align="left">处理 UI 事件的 Camera</td></tr><tr><td align="left">Plane Distance(<code>Screen Space-Camera</code> 模式可设置)</td><td align="left">UI Plane 距离 Camera 的距离</td></tr><tr><td align="left">Target Display(仅 <code>Screen Space-Overlay</code> 模式)</td><td align="left">Canvas 渲染目标展示的窗口展示器</td></tr><tr><td align="left">Sort Order(仅 <code>Screen Space-Overlay</code> 模式)</td><td align="left">在渲染或 Event System 射线检测时用到，值越大表示越靠前展示</td></tr><tr><td align="left">Sorting Layer(<code>Screen Space-Camera</code>和 <code>World Space</code> 模式)</td><td align="left">Canvas 所在的排序层，值为 Tags and Layers 中的一个，设置为越靠前的值越先渲染(越靠后展示)</td></tr><tr><td align="left">Order in Layer(<code>Screen Space-Camera</code>和 <code>World Space</code> 模式)</td><td align="left">如果 Sorting Layer 相同，则可以使用这个值来细分渲染顺序，该值越小越先渲染</td></tr><tr><td align="left">Additional Shader Channels</td><td align="left">配置为 Canvas 生成 Mesh 时携带额外的数据</td></tr></tbody></table><p>这里主要说明一下 Render Mode 这个属性，用来设置 UI 的渲染方式，有 <code>Screen Space-Overlay</code>、<code>Screen Space-Camera</code>和 <code>World Space</code> 三个值可以设置。</p><ul><li>Screen Space-Overlay</li></ul><p>这种模式设置让所有 UI 直接显示在屏幕的最上方(包括 Camera 渲染的场景图像)。Canvas 通过缩放来适配不同的屏幕分辨率，当屏幕尺寸改变或分辨率改变，会自动重新计算并布局。</p><ul><li>Screen Space-Camera</li></ul><p>这种模式和 Screen Space - Overlay 有点相似，不同的是在这种模式下需要设定一个 Camera，所有的 UI 元素由该 Camera 渲染(如果未指定 Render Camera，就会以 Screen Space - Overlay 模式去渲染)。这种模式下 Canvas 朝向 Camera，通过 Plane Distance 可以调整 Canvas 和 Camera 的距离。当屏幕尺寸、分辨率或 Camera 的视锥体大小发生变化，Canvas 都会通过改变自身的 scale 去自动适配。如果场景中有 3D 物体并且离 Camera 近，那么 3D 物体会<strong>渲染在</strong> Canvas 前面，反之 Canvas 会<strong>渲染在</strong> 3D 物体的前面。</p><ul><li>World Space</li></ul><p>这种模式下，Canvas 被当成世界空间下一个普通的 3D 对象去渲染，所以它既可以渲染在某些对象的前面，也可以渲染在某些对象的后面。和 Screen Space-Camera 模式的区别是 Canvas 不需要再朝向 Camera，Canvas 可以通过设置自身的不同属性调整位置、大小、角度等。</p><p>Canvas 组件(类)中，还有一些其他属性，但是它们不可以在编辑器中配置，例如 <code>scaleFactor</code> 属性就是 Canvas 自适应会用到的一个变量，使用它来缩放整个 Canvas 以使其适配屏幕。还有更多属性的作用与介绍请参考 <a href="https://docs.unity3d.com/ScriptReference/Canvas.html">Unity Scripting API - Canvas</a>，这里不再一一介绍。</p><h1 id="Canvas-如何自动适配了？"><a href="#Canvas-如何自动适配了？" class="headerlink" title="Canvas 如何自动适配了？"></a>Canvas 如何自动适配了？</h1><p>上面讲到，在 Render Mode 设置为 <code>Screen Space-Overlay</code> 和 <code>Screen Space-Camera</code> 时，当屏幕尺寸、分辨率变换 Canvas 都会自动重新计算来进行自动适配，那么适配工作是如何进行的了？</p><ul><li><strong>Screen Space-Overlay</strong></li></ul><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_canvas_3.jpeg" width="40%" height="40%" /></center><p>在这种模式下，Canvas 所在的 Rect Transform 的宽高分别为 400 和 400，Scale 均为 1，当前游戏屏幕设定的分辨率也是 400*400，因此 Canvas 的宽高在这种模式下就会自动跟随物理分辨率自动设置。</p><ul><li><strong>Screen Space-Camera</strong></li></ul><p>在这种模式下，首先来看看 Canvas 的配置以及计算得到的适配结果:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_canvas_2.jpeg" width="40%" height="40%" /></center><p>图中 Rect Transform 的 Width 和 Height 的值分别为 400 和 400，Scale 的三个值都被计算 0.05；Canvas 的 Plane Distance 被设置为 10，并添加了一个 Render Camera。</p><p>当前游戏屏幕设定的分辨率为 400*400，所以 Canvas 宽高都被设置为 400 很好理解(布满屏幕)，但是这里的 Scale 为什么都被计算成了 0.05 了？</p><p>带着疑问，再来看看 Render Camera 的一些配置:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_canvas_1.jpeg" width="40%" height="40%" /></center><p>其中 Projection 为 <code>Perspective</code> (透视投影)，Field of View 设置为 90 度，Target Display 中的值就是设置的 400*400 的屏幕。</p><p>根据上面的配置以及视锥体和三角函数，其实可以计算得到 Canvas 的真实高度为:</p><div class="math-display">\[height &#x3D; tan(\frac{FOV}{2}) \times planeDistance \times 2\]</div><p>由于 Target Display 的宽高比例为 1:1，因此 Canvas 的宽就等于高:</p><div class="math-display">\[width &#x3D; height\]</div><p>对 Canvas 进行缩放就得到了真实的宽高，所以缩放系数:</p><div class="math-display">\[scaleX &#x3D; \frac{width}{canvasOriginWidth}\]</div><div class="math-display">\[scaleY &#x3D; \frac{height}{canvasOriginHeight}\]</div><p>带入数字计算得到 Canvas 的 Scale 就是 0.05(当 Camera 投影方式设置为 <code>Orthographic</code>，也可使用对应的参数去计算)。</p><h1 id="CanvasScaler-起到了什么作用？"><a href="#CanvasScaler-起到了什么作用？" class="headerlink" title="CanvasScaler 起到了什么作用？"></a>CanvasScaler 起到了什么作用？</h1><p>对于多分辨率的适配问题，究竟什么样的适配才算得上完美，也许依游戏不同适配的要求也有所区别。在 Unity 中，适配的时候我们常常离不开 CanvasScaler 这个组件，它用来帮助我们的游戏“适应”不同尺寸的屏幕。</p><blockquote><p>The <strong>Canvas Scaler</strong> component is used for controlling the overall scale and <strong>pixel</strong> density of <strong>UI</strong><br>elements in the Canvas. This scaling affects everything under the Canvas, including font sizes and image borders.</p></blockquote><p>如<a href="https://docs.unity3d.com/Manual/script-CanvasScaler.html">官方文档</a>所说，Canvas Scaler 用来控制 Canvas 下所有 UI 元素的缩放以及像素密度，它的缩放比例会影响 Canvas 下所有的元素。</p><p>CanvasScaler 组件在编辑器中也有很多可以配置的属性，首先最重要的就是 UI Scale Mode，有三种模式可以设置，用来控制 UI 元素在 Canvas 下是如何缩放的(当 Canvas 的 Render Mode 设置为 <code>Screen Space-xx</code> 时这三种模式能够生效)。下面就结合源码分别来分析一下究竟是如何进行缩放的了？</p><h2 id="Constant-Pixel-Size"><a href="#Constant-Pixel-Size" class="headerlink" title="Constant Pixel Size"></a>Constant Pixel Size</h2><p>设置为这种模式时，所有 UI 元素的位置和大小都保持为原来的像素大小，但是会根据设置的 Scale Factor 缩放。若一个 Canvas 没有绑定 CanvasScaler 组件(上一部分讲到的情况)，其适配过程就是使用这种模式。</p><p>在这种模式下，有两个参数可以手动配置: Scale Factor 和 Reference Pixels Per Unit。在 CanvasScaler 类的 <code>Handle()</code> 方法中，使用 <code>HandleConstantPixelSize()</code> 对这两个配置进行使用，代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">virtual</span> <span class="type">void</span> <span class="title">HandleConstantPixelSize</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="built_in">SetScaleFactor</span>(m_ScaleFactor);</span><br><span class="line">    <span class="built_in">SetReferencePixelsPerUnit</span>(m_ReferencePixelsPerUnit);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其中 <code>m_ScaleFactor</code> 就是配置项 Scale Factor，<code>m_ReferencePixelsPerUnit</code> 就是配置项 Reference Pixels Per Unit。SetScaleFactor 方法和 SetReferencePixelsPerUnit 方法就是设置当前 Canvas 的这两个值。</p><ol><li>配置 Scale Factor 后，Canvas 组件中的属性 <code>scaleFactor</code> 也被更新为设置的这个值，然后用这个缩放系数来缩放 Canvas 下所有的 UI 元素。</li></ol><p>例如屏幕大小为 400*400，Scale Factor 设置为 2，Canvas 为了适配屏幕的宽高就会通过公式 $ScreenSize \div Scale Factor$ 计算得到宽高值为都 200，同时 Canvas 下所有的 UI 元素也都被缩放 2 倍。</p><ol start="2"><li>Reference Pixels Per Unit 配置就是 Canvas 组件中的成员变量 <code>referencePixelsPerUnit</code>，<strong>表示每个 Unity 单位包含的屏幕(UI)像素</strong>。这个参数需要与 Sprite 设置下的 Pixel Per Unit(<strong>每 Unity 单位中包含多少个 Sprite 像素</strong>) 一起使用。</li></ol><p>在 Image 组件的源码中我们就可以看到这样的使用，代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="type">float</span> pixelsPerUnit</span><br><span class="line">&#123;</span><br><span class="line">    get</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="type">float</span> spritePixelsPerUnit = <span class="number">100</span>;</span><br><span class="line">        <span class="keyword">if</span> (activeSprite)</span><br><span class="line">            spritePixelsPerUnit = activeSprite.pixelsPerUnit;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (canvas)</span><br><span class="line">            m_CachedReferencePixelsPerUnit = canvas.referencePixelsPerUnit;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> spritePixelsPerUnit / m_CachedReferencePixelsPerUnit;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">override</span> <span class="type">void</span> <span class="title">SetNativeSize</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (activeSprite != null)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="type">float</span> w = activeSprite.rect.width / pixelsPerUnit;</span><br><span class="line">        <span class="type">float</span> h = activeSprite.rect.height / pixelsPerUnit;</span><br><span class="line">        rectTransform.anchorMax = rectTransform.anchorMin;</span><br><span class="line">        rectTransform.sizeDelta = <span class="keyword">new</span> <span class="built_in">Vector2</span>(w, h);</span><br><span class="line">        <span class="built_in">SetAllDirty</span>();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在上面的代码 pixelsPerUnit 方法中，<code>spritePixelsPerUnit</code> 被设置为了 Sprite 的 pixelsPerUnit，<code>m_CachedReferencePixelsPerUnit</code> 被设置成了 Canvas 的 referencePixelsPerUnit(这个值就是我们再 Canvas Scaler 中配置的值)。最后这里的计算返回的 pixelsPerUnit 为 <code>spritePixelsPerUnit / m_CachedReferencePixelsPerUnit</code>。</p><p>再看看 SetNativeSize，这个方法用来设置 Image 使其更加 pixel-perfect (更加清晰)，设置后 Image 的 <code>rectransform.sizedelta</code> 就有可能和 Sprite 的尺寸相等。将 Sprite 转换到 Unity 单位下:</p><div class="math-display">\[spriteUnitySize &#x3D; \frac{sprite.size}{pixelPerUnit}\]</div><p>然后将 Sprite 由 Unity 单位尺寸转换为屏幕 UI 尺寸:</p><div class="math-display">\[spriteDimensions &#x3D; spriteUnitySize \times referencePixelsPerUnit\]</div><p>即 </p><div class="math-display">\[spriteDimensions &#x3D; sprite.size \div pixelPerUnit \times referencePixelsPerUnit\]</div><p>比如: Sprite 的原始尺寸为 400<em>400，Pixel Per Unit 为 100，那么 Sprite 在 Unity 中的大小就是 4</em>4 个单位；Canvas Scaler 的 Reference Pixels Per Unit 为 100(一个 Unity 单位对应屏幕 UI 100 个像素)，因此最后 Sprite pixel-perfect 的尺寸就是 400*400。</p><p>在 Constant Pixel Size 模式下，所有的 UI 元素的大小和位置都使用屏幕像素来指定计算。通过 Scale Factor 指定缩放系数，Reference Pixels Per Unit 用来指定每 Unity 单位又会以多少像素渲染在屏幕上。</p><h2 id="Scale-With-Screen-Size"><a href="#Scale-With-Screen-Size" class="headerlink" title="Scale With Screen Size"></a>Scale With Screen Size</h2><p>使用这种模式，UI 元素能够根据参考分辨率以及配置的参数达到自适应(计算最合适的 Scale Factor)不同分辨率屏幕的目的。首先还是先看一下这种模式下可以配置的一些参数:</p><ol><li><p>Reference Resolution - 参考分辨率，通常为设计所用的分辨率。</p></li><li><p>Reference Pixels Per Unit - 这个值在 Constant Pixel Size 模式下讲过，计算方式都相同，这里不再讲解。</p></li><li><p>Screen Match Mode - 屏幕适配模式，当屏幕分辨率的比例和参考分辨率的比例不同时，就会根据这个参数来计算 Scale Factor(缩放系数，其实在比例相同的时候也会使用以下配置计算，只是对于下面不同的设置值计算结果都相同)。它有以下几个值:</p><ul><li><p>Match Width or Height - 在这种屏幕适配模式下，Canvas 会根据某个方向(Width、 Height 或他们中间的某个值)单独(混合)的计算缩放系数来适配；</p><ul><li>Match - 通过改变这个值(0-1 之间。0 是代表 Width、1 是代表 Height)计算缩放系数。</li></ul></li><li><p>Expand - 设置为这个值的时候 Canvas 会水平或垂直展开以缩放，Canvas 的大小永远不会比参考分辨率小；</p></li><li><p>Shrink - 设置为这个值的时候 Canvas 会水平或垂直裁剪以缩放，Canvas 的大小永远不会比参考分辨率大。</p></li></ul></li></ol><p>下面看看具体是如何计算 Scale Factor 来达到以上的适配要求。在 <code>Handle()</code> 方法中，如果 Scale Mode 是 Scale With Screen Size，会使用 <code>HandleScaleWithScreenSize()</code> 计算 Scale Factor，代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">virtual</span> <span class="type">void</span> <span class="title">HandleScaleWithScreenSize</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="comment">// get screen size...</span></span><br><span class="line"></span><br><span class="line">    <span class="type">float</span> scaleFactor = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">switch</span> (m_ScreenMatchMode)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">case</span> ScreenMatchMode.MatchWidthOrHeight:</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="type">float</span> logWidth = Mathf.<span class="built_in">Log</span>(screenSize.x / m_ReferenceResolution.x, kLogBase);</span><br><span class="line">            <span class="type">float</span> logHeight = Mathf.<span class="built_in">Log</span>(screenSize.y / m_ReferenceResolution.y, kLogBase);</span><br><span class="line">            <span class="type">float</span> logWeightedAverage = Mathf.<span class="built_in">Lerp</span>(logWidth, logHeight, m_MatchWidthOrHeight);</span><br><span class="line">            scaleFactor = Mathf.<span class="built_in">Pow</span>(kLogBase, logWeightedAverage);</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">case</span> ScreenMatchMode.Expand:</span><br><span class="line">        &#123;</span><br><span class="line">            scaleFactor = Mathf.<span class="built_in">Min</span>(screenSize.x / m_ReferenceResolution.x, screenSize.y / m_ReferenceResolution.y);</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">case</span> ScreenMatchMode.Shrink:</span><br><span class="line">        &#123;</span><br><span class="line">            scaleFactor = Mathf.<span class="built_in">Max</span>(screenSize.x / m_ReferenceResolution.x, screenSize.y / m_ReferenceResolution.y);</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// set scale factor &amp; reference pixels per unit</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><strong>ScreenMatchMode.MatchWidthOrHeight</strong></li></ul><p>在这个设置下，在对数空间计算 scaleFactor 值。通过代码可以看出，首先将所在的屏幕宽度与参考分辨率宽度比例转换到对数空间，高度比例也转换到对数空间，然后在对数空间下通过 Match 的配置对宽度比例和高度比例进行插值得到对数空间下的 scaleFactor，最后在将对数空间下的 scaleFactor 转换回原始空间下(对数空间下计算有更好的表现)。</p><p>根据上面的代码，可以得到 scaleFactor 计算公式。</p><p>对数空间下屏幕宽度和参考分辨率的宽度比例:</p><div class="math-display">\[logWidthRatio &#x3D; \log_{2}(\frac{screenSize.x}{m_ReferenceResolution.x})\]</div><p>对数空间下屏幕高度和参考分辨率的宽度比例:</p><div class="math-display">\[logHeightRatio &#x3D; \log_{2}(\frac{screenSize.y}{m_ReferenceResolution.y})\]</div><p>最终 scaleFactor 等于:</p><div class="math-display">\[scaleFactor &#x3D; 2^{(1 - Match) \times logWidthRatio + Match \times logHeightRatio}\]</div><p>由此可以看出当 Match 设置为 0 (在最左边)时，带入公式计算得</p><div class="math-display">\[scaleFactor &#x3D; 2^{\log_{2}(\frac{screenSize.x}{m_ReferenceResolution.x})} &#x3D; \frac{screenSize.x}{m_ReferenceResolution.x}\]</div><p>scaleFactor 就是屏幕宽度与参考分辨率宽度比例，这种情况下高度对 Canvas 缩放没有任何影响。同理如果 Match 设置为 1 (拖到最右边)，那么 scaleFactor 就是屏幕高度与参考分辨率高度比例。</p><p>那么在什么情况下设置为这个值比较好了？当你的游戏发布后可能面对的分辨率千奇百怪(比各种如 Android 手机)，这个时候 Screen Match Mode 设置为 <code>ScreenMatchMode.MatchWidthOrHeight</code>，通过按宽度、高度或是混合宽度高度来计算缩放系数，以尽可能达到各种分辨率屏幕适配。朝着一个方向缩放，能够保证这个方向上位置布局不会出现问题，但是另一个方向会根据适配屏幕适配这个要求去拉伸；这时也许设置一个混合值 Match 可以更好的达到适配缩放显示效果。</p><ul><li><strong>ScreenMatchMode.Expand</strong></li></ul><p>通过上面代码可以看出，当 Screen Match Mode 设置为 <code>ScreenMatchMode.Expand</code> 时，最终得到的 scaleFactor 是屏幕分辨率与参考分辨率宽高比中较小的一个，然后根据 scaleFactor 计算 Canvas 的宽高的公式为:</p><div class="math-display">\[canvasSize &#x3D; \frac{screenSize}{scaleFactor}\]</div><p>假如上面计算的到的 scaleFactor 是 <code>screenSize.x / m_ReferenceResolution.x</code> (宽的分辨率比例小)，则有:</p><div class="math-display">\[\frac{screenSize.x}{m_ReferenceResolution.x} \leqslant \frac{screenSize.y }{m_ReferenceResolution.y}\]</div><p>带入计算 Canvas 的公式得到 Canvas 宽高分别为:</p><div class="math-display">\[canvasWidth &#x3D; \frac{screenSize.x}{(\frac{screenSize.x}{m_ReferenceResolution.x})} &#x3D; m_ReferenceResolution.x\]</div><div class="math-display">\[canvasHeight &#x3D; \frac{screenSize.y}{(\frac{screenSize.x}{ m_ReferenceResolution.x})}\]</div><p>可以看到 Canvas 的宽就是参考分辨率的宽，高度是一个计算值。根据计算 scaleFactor 的不等式以及不等式的基本性质可以得到:</p><div class="math-display">\[canvasHeight &#x3D; \frac{screenSize.y}{(\frac{screenSize.x}{ m_ReferenceResolution.x})} \geqslant m_ReferenceResolution.y\]</div><p>因此最终得到的 Canvas 的高度不小于参考分辨率的高度；又因为缩放后 Canvas 的宽是参考分辨率的宽，因此当 Screen Match Mode 设置为 <code>ScreenMatchMode.Expand</code> 时，缩放后的 Canvas 的大小永远不会比参考分辨率小。</p><p><strong>Q: 放大流程到底是怎么样的？</strong></p><p><strong>A:</strong> 首先此时的参考分辨率的长宽都使用缩放系数放大，但是这只能满足较小的那个方向的适配，对于较大比例方向，Canvas 还需要进行拉伸扩大以适配屏幕。</p><p><strong>Q: 那么什么情况下使用这个设置比较好了？</strong></p><p><strong>A:</strong> 由于此种情况下得到的 Canvas 的尺寸肯定是大于或等于参考分辨率的，因此适合屏幕分辨率两个方向都大于参考分辨率的情形；当屏幕的分辨率比参考分辨率要大，说明这种情况下需要的 UI 元素进行放大处理最好，因此 Screen Match Mode 设置为 <code>ScreenMatchMode.Expand</code>。</p><p><strong>Q: 为什么此时要使用分辨率宽度比和高度比中较小的一个(这个比例就是 UI 元素的放大比例)？</strong></p><p><strong>A:</strong> 取小的比例合适是因为对布局来说更安全(元素放大导致的遮挡问题)。比如如果取了比较大的那个比例值作为 Canvas 的缩放系数，那么 UI 元素在水平和垂直两个方向上都会使用该比例进行放大处理，此时只是满足了较大比例的那个方向的适配，对于较小比例方向，Canvas 还需要通过缩小以适配屏幕，此时被放大的 UI 元素由于 Canvas 被缩小而位置发送变化就极有可能彼此遮挡；但是取较小比例时这种情况就不会发生，因为对于参考分辨率来说首先 Canvas 两个方向都以这个小的比例放大，此时不会遮挡，然后对于比例较大的方向通过拉伸适配屏幕，拉伸让这个方向上的元素彼此离得更远了，更不会遮挡。</p><ul><li><strong>ScreenMatchMode.Shrink</strong></li></ul><p>用同样的计算方式也可得到在设置 <code>ScreenMatchMode.Shrink</code> 时，计算得到的 scaleFactor 是屏幕分辨率与参考分辨率宽高比中较大的一个，因此缩放后的 Canvas 的大小永远不会比参考分辨率大(证明过程和上面类似)。</p><p><strong>Q: 缩小流程到底是怎么样的？</strong></p><p><strong>A:</strong> 首先此时的参考分辨率的长宽都使用缩放系数缩小，但是这只能满足较大比例的那个方向的缩小适配(比例越大这里缩小的像素就越小)，对于较小比例方向就缩小还不能达到适配屏幕的目的，因此 Canvas 还需要缩小。</p><p><strong>Q: 那么什么情况下使用这个设置比较好了？</strong></p><p><strong>A:</strong> 由于此种情况下得到的 Canvas 的尺寸肯定是小于或等于参考分辨率的，因此适合屏幕分辨率两个方向都小于参考分辨率的情况；当屏幕的分辨率比参考分辨率要小，说明这种情况下需要的 UI 元素进行缩小处理最好，因此 Screen Match Mode 设置为 <code>ScreenMatchMode.Shrink</code>。</p><p>总结一下，当 Canvas Scaler 的 UI Scale Mode 设置为 Scale With Screen Size 时自适应屏幕的几种情况:</p><ol><li><p>屏幕分辨率比例和参考分辨率相同，但是比参考分辨率大。这种情况下，Canvas  尺寸保持参考分辨率的大小，但会缩放(大)来适配屏幕，UI 也会被放大。</p></li><li><p>屏幕分辨率比例和参考分辨率相同，但是比参考分辨率小。在这种情况下，Canvas 尺寸同样也会保持参考分辨率的大小，但会缩(小)放来适配屏幕，UI 也会被缩小。</p></li><li><p>屏幕分辨率比例和参考分辨率不同。在这种情况下，如果屏幕分辨率都两个方向都大于参考分辨率，使用 Expand 较好；如果屏幕分辨率两个方向都小于参考分辨率，使用 Shrink 更能达到自适应目的；当设置为 Match Width Or Height 时，可以通过调节 Match 参数来计算最佳缩放系数以自适应，这种方式最为灵活。</p></li></ol><h2 id="Constant-Physical-Size"><a href="#Constant-Physical-Size" class="headerlink" title="Constant Physical Size"></a>Constant Physical Size</h2><p>使用这种模式，UI 元素的位置和大小信息都是用物理单位来表示。同样它也可以配置一些参数:</p><ol><li><p>Physical Unit - 指定的武力单位。</p></li><li><p>Fallback Screen DPI - 设置的默认 DPI。</p></li><li><p>Default Sprite DPI - 设置的默认 Sprite DPI。</p></li><li><p>Reference Pixels Per Unit，同 Constant Pixel Size 中这个参数的设置。</p></li></ol><p>在回到代码中，看到 HandleConstantPhysicalSize 方法，这个方法同样会计算 Scale Factor，代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">virtual</span> <span class="type">void</span> <span class="title">HandleConstantPhysicalSize</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="type">float</span> currentDpi = Screen.dpi;</span><br><span class="line">    <span class="type">float</span> dpi = (currentDpi == <span class="number">0</span> ? m_FallbackScreenDPI : currentDpi);</span><br><span class="line">    <span class="type">float</span> targetDPI = <span class="number">1</span>;</span><br><span class="line">    <span class="keyword">switch</span> (m_PhysicalUnit)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">case</span> Unit.Centimeters: targetDPI = <span class="number">2.54f</span>; <span class="keyword">break</span>;</span><br><span class="line">        <span class="keyword">case</span> Unit.Millimeters: targetDPI = <span class="number">25.4f</span>; <span class="keyword">break</span>;</span><br><span class="line">        <span class="keyword">case</span> Unit.Inches:      targetDPI =     <span class="number">1</span>; <span class="keyword">break</span>;</span><br><span class="line">        <span class="keyword">case</span> Unit.Points:      targetDPI =    <span class="number">72</span>; <span class="keyword">break</span>;</span><br><span class="line">        <span class="keyword">case</span> Unit.Picas:       targetDPI =     <span class="number">6</span>; <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="built_in">SetScaleFactor</span>(dpi / targetDPI);</span><br><span class="line">    <span class="built_in">SetReferencePixelsPerUnit</span>(m_ReferencePixelsPerUnit * targetDPI / m_DefaultSpriteDPI);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>从代码中可以看到计算 Scale Factor 和 Reference Pixels Per Unit 都是使用 DPI 去计算的，targetDPI 根据使用的 Physical Unit 不同而不同。</p><h2 id="World-Space"><a href="#World-Space" class="headerlink" title="World Space"></a>World Space</h2><p>当 Canvas 的 Render Mode 设置为 World Space，Canvas Scaler 用来控制 UI 元素的像素密度。</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ul><li><a href="https://docs.unity3d.com/Manual/class-Canvas.html">Unity Canvas</a></li><li><a href="https://docs.unity3d.com/Manual/script-CanvasScaler.html">Unity CanvasScaler</a></li></ul>]]>
    </content>
    <id>https://lujun.pages.dev/2019/08/27/unity_canvas_components/</id>
    <link href="https://lujun.pages.dev/2019/08/27/unity_canvas_components/"/>
    <published>2019-08-27T00:02:29.000Z</published>
    <summary>
      <![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>Canvas 是所有 UI 组件被布局和渲染的空间，这些 UI 组件都必须位于 Canvas 子节点。在 Unity 编辑器中，我们可以方便的进行 Canvas UI 编辑。</p>
<p>一个 Canvas 系统通常包含以下几个组件:</p>
<ul>
<li><p>Canvas 组件(Canvas 对象的基础组件)</p>
</li>
<li><p>CanvasScaler 组件(控制所有 UI 元素的缩放)</p>
</li>
<li><p><a href="https://blog.lujun.co/2019/07/20/unity_event_system_raycasters/">GraphicRaycaster</a> 组件(用于 Event System 中的射线检测)</p>
</li>
</ul>
<p>接下来，我们就一起来看看每个组件的分工与作用。</p>]]>
    </summary>
    <title>Unity Canvas 自适应探究</title>
    <updated>2026-06-22T14:03:58.804Z</updated>
  </entry>
  <entry>
    <author>
      <name>lujun</name>
    </author>
    <category term="Unity" scheme="https://lujun.pages.dev/tags/Unity/"/>
    <content>
      <![CDATA[<script src="/assets/js/APlayer.min.js"> </script><p>Event System 是 Unity 提供的内部 GameObject 响应输入(如键盘输入、鼠标输入、触摸输入等)事件的一种实现，它由多个组件组成，协同工作来实现整个事件通信。</p><span id="more"></span><p>组成 Event System 的组件包括:</p><ul><li><p>EventSystem 组件(本文内容)</p></li><li><p><a href="https://blog.lujun.co/2019/08/18/unity_input_modules/">Input Modules</a></p></li><li><p><a href="https://blog.lujun.co/2019/07/20/unity_event_system_raycasters/">Raycasters</a></p></li></ul><h1 id="EventSystem-组件"><a href="#EventSystem-组件" class="headerlink" title="EventSystem 组件"></a>EventSystem 组件</h1><p>EventSystem 组件主要实现以下功能:</p><ul><li><p>控制哪个 GameObject 被选中响应事件</p></li><li><p>控制选择使用哪一种 Input Module(同一时刻只有一个 Input Module 能处于激活状态)</p></li><li><p>管理 Raycasting</p></li><li><p>管理更新 Input Module</p></li></ul><p>在 Unity 编辑器中，可以直接为场景添加 EventSystem 组件(一个场景通常只包含一个 EventSystem 组件)。可以直接设置一些属性以达到自定义需求，如下:</p><table><thead><tr><th align="left">属性</th><th align="left">成员变量</th><th align="left">描述</th></tr></thead><tbody><tr><td align="left">First Selected</td><td align="left">m_FirstSelected</td><td align="left">EventSystem 中第一个被选中的对象</td></tr><tr><td align="left">Send Navigation Event</td><td align="left">m_sendNavigationEvents</td><td align="left">EventSystem 是否接收处理 Navigation 事件(move &#x2F; submit &#x2F; cancel)</td></tr><tr><td align="left">Drag Threshold</td><td align="left">m_DragThreshold</td><td align="left">可以判定为拖拽(drag)的阈值(单位: 像素)</td></tr></tbody></table><p>除了上面三个可以在编辑器中编辑的属性之外，还有很多其他特性，如: <code>m_HasFocus</code> 可以用来表示当前 EventSystem 是否处于拥有焦点状态，如果当前没有获得焦点，诸如 Input Module 相关的更新和激活方法不会被调用，事件处理方法 <code>Process()</code> 也不会进行。</p><p>EventSystem 组件也是一个 MonoBehaviour，因此它具有完整的 Unity 生命周期。除了有一些生命周期相关的回调，它也有一些其它比较重要的方法。</p><h2 id="void-UpdateModules-方法"><a href="#void-UpdateModules-方法" class="headerlink" title="void UpdateModules() 方法"></a>void UpdateModules() 方法</h2><p>这个方法作用就是重新计算当前 <code>m_SystemInputModules</code> 中的 Input Modules，保证 <code>m_SystemInputModules</code> 中的 Input Module 都是出于激活状态的。这个方法会在 BaseInputModule 类的 <code>OnEnable()</code> 和 <code>OnDisable()</code> 方法中被调用，以注册或反注册对应的 Input Module。</p><h2 id="void-SetSelectedGameObject-GameObject-selected-BaseEventData-pointer-方法"><a href="#void-SetSelectedGameObject-GameObject-selected-BaseEventData-pointer-方法" class="headerlink" title="void SetSelectedGameObject(GameObject selected, BaseEventData pointer) 方法"></a>void SetSelectedGameObject(GameObject selected, BaseEventData pointer) 方法</h2><p>设置当前被选中的对象，属性 <code>m_CurrentSelected</code> 会被赋值为被选中的 GameObject。设置过程中，会给之前选中的对象发送 <code>ExecuteEvents.deselectHandler</code> 事件，给被设置的对象发送 <code>ExecuteEvents.selectHandler</code> 事件。这个方法会在需要设置 Selected 对象的时候被调用，如 InputField 组件接收到 Pointer Down 事件时，就会将其对应的 GameObject 设置为 Event System 当前选中对象(m_CurrentSelected)。方法代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="type">void</span> <span class="title">SetSelectedGameObject</span><span class="params">(GameObject selected, BaseEventData pointer)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (m_SelectionGuard)</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line">    m_SelectionGuard = <span class="literal">true</span>;</span><br><span class="line">    <span class="keyword">if</span> (selected == m_CurrentSelected)</span><br><span class="line">    &#123;</span><br><span class="line">        m_SelectionGuard = <span class="literal">false</span>;</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    ExecuteEvents.<span class="built_in">Execute</span>(m_CurrentSelected, pointer, ExecuteEvents.deselectHandler);</span><br><span class="line">    m_CurrentSelected = selected;</span><br><span class="line">    ExecuteEvents.<span class="built_in">Execute</span>(m_CurrentSelected, pointer, ExecuteEvents.selectHandler);</span><br><span class="line">    m_SelectionGuard = <span class="literal">false</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="void-RaycastAll-PointerEventData-eventData-List-raycastResults-方法"><a href="#void-RaycastAll-PointerEventData-eventData-List-raycastResults-方法" class="headerlink" title="void RaycastAll(PointerEventData eventData, List raycastResults) 方法"></a>void RaycastAll(PointerEventData eventData, List<RaycastResult> raycastResults) 方法</h2><p>这个方法用来进行射线检测得到可以拦截事件的对象。方法实现很简单，使用 RaycasterManager 中的所有处于激活状态的 BaseRaycaster 进行检测，得到响应事件的对象。这个方法会在 PointerInputModule 类的 <code>GetTouchPointerEventData</code> 和 <code>GetMousePointerEventData</code> 方法中被调用，用于创建对应的可被分发拦截处理的具体事件。</p><h2 id="bool-IsPointerOverGameObject-int-pointerId-方法"><a href="#bool-IsPointerOverGameObject-int-pointerId-方法" class="headerlink" title="bool IsPointerOverGameObject(int pointerId) 方法"></a>bool IsPointerOverGameObject(int pointerId) 方法</h2><p>用来判断指定 ID 的 Pointer 是否还在 EventSystem 的那个对象上。方法内部最终调用 PointerInputModule 类的 <code>IsPointerOverGameObject</code> 方法，在 PointerInputModule 类的方法实现也很简单，就是根据 pointerId 获取对应的 PointerEventData，然后判断如果 PointerEventData 中 <code>pointerEnter</code> 对应的 GameObject 不为空，这说明这个 ID 对应的 Pointer 还在 EventSystem 的对象上。</p><h2 id="void-Update-方法"><a href="#void-Update-方法" class="headerlink" title="void Update() 方法"></a>void Update() 方法</h2><ol><li><p>首先调用了 <code>TickModules()</code> 去更新当前 Event System 中的所有的 Input Modules （调用了对应 Input Module 的 <code>UpdateModule()</code> 方法），这样 Input Module 中的 <code>m_LastMousePosition</code> 和 <code>m_MousePosition</code> 就得到了更新；</p></li><li><p>如果当前 <code>m_CurrentInputModule</code> 不为空，就将当前 Event System 中挂载所有的 Input Modules 中的第一个可用且处于激活状态的 Input Module 设置给 <code>m_CurrentInputModule</code>；</p></li><li><p>如果从来没有设置过 <code>m_CurrentInputModule</code>，就将挂载所有的 Input Modules 中的第一个可用 Input Module 设置给 <code>m_CurrentInputModule</code>；</p></li><li><p>最后就会调用 Input Module 组件的 <code>Process()</code> 方法处理输入并产生事件、拦截处理事件。</p></li></ol><p>上面的第二步和第三步中设置 <code>m_CurrentInputModule</code> 会调用 <code>ChangeEventModule</code> 方法。如果是切换 Input Module，则会反激活上一个 Input Module；还会激活当前要设置的 Input Module。代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="type">void</span> <span class="title">ChangeEventModule</span><span class="params">(BaseInputModule <span class="keyword">module</span>)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (m_CurrentInputModule == <span class="keyword">module</span>)</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (m_CurrentInputModule != null)</span><br><span class="line">        m_CurrentInputModule.<span class="built_in">DeactivateModule</span>();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (<span class="keyword">module</span> != null)</span><br><span class="line">        <span class="keyword">module</span>.<span class="built_in">ActivateModule</span>();</span><br><span class="line">    m_CurrentInputModule = <span class="keyword">module</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="void-OnDisable-方法"><a href="#void-OnDisable-方法" class="headerlink" title="void OnDisable() 方法"></a>void OnDisable() 方法</h2><p>在这个方法中，主要就是将当前的 Input Module (m_CurrentInputModule) 反激活，调用了对应 InputModule 的 <code>DeactivateModule()</code> 方法。</p>]]>
    </content>
    <id>https://lujun.pages.dev/2019/08/26/unity_event_system/</id>
    <link href="https://lujun.pages.dev/2019/08/26/unity_event_system/"/>
    <published>2019-08-26T09:02:29.000Z</published>
    <summary>
      <![CDATA[<p>Event System 是 Unity 提供的内部 GameObject 响应输入(如键盘输入、鼠标输入、触摸输入等)事件的一种实现，它由多个组件组成，协同工作来实现整个事件通信。</p>]]>
    </summary>
    <title>Unity 内部的 Event System(组件)</title>
    <updated>2026-06-22T14:03:58.804Z</updated>
  </entry>
  <entry>
    <author>
      <name>lujun</name>
    </author>
    <category term="Unity" scheme="https://lujun.pages.dev/tags/Unity/"/>
    <content>
      <![CDATA[<script src="/assets/js/APlayer.min.js"> </script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>Input Modules 是 Event System 组成的一部分，负责产生抽象事件并分发事件到 GameObject 处理；在代码架构上它也是 Event System 中处理大部分业务逻辑的地方，可以高度配置化。不同的 Input Module 可以映射外部不同的硬件输入至 Unity Event System 中转化成能处理的事件，并管理这些事件的状态。Unity 中内置了 Standalone Input Module 和 Touch Input Module(已经被标记为 <code>obsolete</code>，现在由 Standalone Input Module 一并处理)。</p><p>本篇文章就来剖析 Input Modules 在 Unity 中的实现。</p><span id="more"></span><h1 id="BaseInputModule"><a href="#BaseInputModule" class="headerlink" title="BaseInputModule"></a>BaseInputModule</h1><p>BaseInputModule 是所有关于 Input Module 类的最上层抽象类，它包含一些通用的成员变量(如 <code>List&lt;RaycastResult&gt; m_RaycastResultCache 用于存储射线检测时的结果)和方法(如 </code>abstract void Process()&#96; 定义了 Input Module 的处理事件行为)。</p><p>BaseInputModule 是一个 MonoBehaviour 组件，所以当挂载在某个 GameObject 上运行时也会拥有 Unity MonoBehaviour 的生命周期。BaseInputModule 覆写了 <code>OnEnable</code> 和 <code>OnDisable</code> 方法，如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">override</span> <span class="type">void</span> <span class="title">OnEnable</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    base.<span class="built_in">OnEnable</span>();</span><br><span class="line">    m_EventSystem = <span class="built_in">GetComponent</span>&lt;EventSystem&gt;();</span><br><span class="line">    m_EventSystem.<span class="built_in">UpdateModules</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">override</span> <span class="type">void</span> <span class="title">OnDisable</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    m_EventSystem.<span class="built_in">UpdateModules</span>();</span><br><span class="line">    base.<span class="built_in">OnDisable</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 enable 和 disable 的时候，都调用了 EventSystem 的 <code>UpdateModules</code> 方法去更新当前 EventSystem 中处于可用状态的 BaseInputModules。</p><p>BaseInputModule 是一个抽象类，因为它其中定义了唯一的一个抽象方法 <code>Process</code>，如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> abstract <span class="type">void</span> <span class="title">Process</span><span class="params">()</span></span>;</span><br></pre></td></tr></table></figure><p>这个方法就是不同 Input Module 子类实现具体事件抽象产生、分发的地方。</p><p>在 BaseInputModule 类还有一个比较重要的方法 <code>HandlePointerExitAndEnter</code>，用来处理当 pointer 的移出或进入某个 GameObject 时候，发送 enter 或 exit 事件，方法定义如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">HandlePointerExitAndEnter</span><span class="params">(PointerEventData currentPointerData, GameObject newEnterTarget)</span></span>;</span><br></pre></td></tr></table></figure><p>方法具体实现中，首先检测是否需要处理退出。当没有 newEnterTarget 或者当前的 currentPointerData 的 pointerEnter 被清除，则给当前 currentPointerData 中所有的 hovered 对象发送 exit 事件并清除所有 hovered 对象；此时如果 newEnterTarget 不为空，说明是进入了新的 GameObject 对象，否则则将当前的 currentPointerData 的 pointerEnter 也置空并退出。代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (newEnterTarget == null || currentPointerData.pointerEnter == null)</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">for</span> (var i = <span class="number">0</span>; i &lt; currentPointerData.hovered.Count; ++i)</span><br><span class="line">        ExecuteEvents.<span class="built_in">Execute</span>(currentPointerData.hovered[i], currentPointerData, ExecuteEvents.pointerExitHandler);</span><br><span class="line"></span><br><span class="line">    currentPointerData.hovered.<span class="built_in">Clear</span>();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (newEnterTarget == null)</span><br><span class="line">    &#123;</span><br><span class="line">        currentPointerData.pointerEnter = null;</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>紧接着判断是否事件是否是在同一个 target 上面，如果是则返回。否则需要处理事件对象切换过程中事件的产生和分发，如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (currentPointerData.pointerEnter == newEnterTarget &amp;&amp; newEnterTarget)</span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line">GameObject commonRoot = <span class="built_in">FindCommonRoot</span>(currentPointerData.pointerEnter, newEnterTarget);</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (currentPointerData.pointerEnter != null)</span><br><span class="line">&#123;</span><br><span class="line">    Transform t = currentPointerData.pointerEnter.transform;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">while</span> (t != null)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">if</span> (commonRoot != null &amp;&amp; commonRoot.transform == t)</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line">        ExecuteEvents.<span class="built_in">Execute</span>(t.gameObject, currentPointerData, ExecuteEvents.pointerExitHandler);</span><br><span class="line">        currentPointerData.hovered.<span class="built_in">Remove</span>(t.gameObject);</span><br><span class="line">        t = t.parent;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">currentPointerData.pointerEnter = newEnterTarget;</span><br><span class="line"><span class="keyword">if</span> (newEnterTarget != null)</span><br><span class="line">&#123;</span><br><span class="line">    Transform t = newEnterTarget.transform;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">while</span> (t != null &amp;&amp; t.gameObject != commonRoot)</span><br><span class="line">    &#123;</span><br><span class="line">        ExecuteEvents.<span class="built_in">Execute</span>(t.gameObject, currentPointerData, ExecuteEvents.pointerEnterHandler);</span><br><span class="line">        currentPointerData.hovered.<span class="built_in">Add</span>(t.gameObject);</span><br><span class="line">        t = t.parent;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面的代码中，当当前进入的对象(newEnterTarget)和上次进入的对象(currentPointerData.pointerEnter)不同的时候，首先调用 <code>FindCommonRoot</code> 寻找它俩的共同父节点(commonRoot)，对从上次进入的对象到 commonRoot 之间所有的对象节点(不包括 commonRoot)依次发送 exit 事件；对从 newEnterTarget 到 commonRoot 之间所有的对象节点(不包括 commonRoot)依次发送 enter 事件。</p><p>除了上面介绍的几个(抽象)方法之外，BaseInputModule 类还有一些静态辅助方法和一些虚方法。静态辅助方法如: <code>FindFirstRaycast</code> 获取射线检测的第一个结果；方法 <code>DetermineMoveDirection</code> 可以根据输入值确定当期那的移动方向；<code>FindCommonRoot</code> 方法返回两个对象第一个共同拥有的节点。虚方法如: <code>GetAxisEventData</code> 根据输入数据产生一个 AxisEventData(可复用)；<code>GetBaseEventData</code> 方法用于复用当前实例的 m_BaseEventData。</p><h1 id="PointerInputModule"><a href="#PointerInputModule" class="headerlink" title="PointerInputModule"></a>PointerInputModule</h1><p>PointerInputModule 继承自 BaseInputModule 类，它里面定义了一些常用的事件相关的成员变量和方法。类 StandaloneInputModule 和 TouchInputModule 继承自该类。</p><p>在成员变量中，有一个 m_PointerData 成员变量如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">protected</span> Dictionary&lt;<span class="type">int</span>, PointerEventData&gt; m_PointerData = <span class="keyword">new</span> <span class="built_in">Dictionary</span>&lt;<span class="type">int</span>, PointerEventData&gt;();</span><br></pre></td></tr></table></figure><p>这个成员变量是当前 Input Module 中的事件的生产者的 ID 以及事件数据对象 <a href="https://docs.unity3d.com/2017.3/Documentation/ScriptReference/EventSystems.PointerEventData.html">PointerEventData</a> 之间的一个映射。</p><p>在 PointerInputModule 类中，有几个比较重要的方法:</p><h2 id="bool-GetPointerData-int-id-out-PointerEventData-data-bool-create-方法"><a href="#bool-GetPointerData-int-id-out-PointerEventData-data-bool-create-方法" class="headerlink" title="bool GetPointerData(int id, out PointerEventData data, bool create) 方法"></a>bool GetPointerData(int id, out PointerEventData data, bool create) 方法</h2><p>用来根据某个事件生产者的 ID 获取 m_PointerData 映射的事件对象，返回 <code>true</code> 表示为该 ID 创建了一个新的事件，否则返回 <code>false</code>。</p><h2 id="PointerEventData-GetTouchPointerEventData-Touch-input-out-bool-pressed-out-bool-released-方法"><a href="#PointerEventData-GetTouchPointerEventData-Touch-input-out-bool-pressed-out-bool-released-方法" class="headerlink" title="PointerEventData GetTouchPointerEventData(Touch input, out bool pressed, out bool released) 方法"></a>PointerEventData GetTouchPointerEventData(Touch input, out bool pressed, out bool released) 方法</h2><p>根据一个 Touch 生成一个点击事件 PointerEventData，同时指定当前帧是否有按下(pressed)、释放(released)操作。如何根据一个 Touch 事件就确定当前是否有按下和释放操作了？下面就来看看该方法具体的实现。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line">PointerEventData pointerData;</span><br><span class="line">var created = <span class="built_in">GetPointerData</span>(input.fingerId, out pointerData, <span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line">pointerData.<span class="built_in">Reset</span>();</span><br><span class="line"></span><br><span class="line">pressed = created || (input.phase == TouchPhase.Began);</span><br><span class="line">released = (input.phase == TouchPhase.Canceled) || (input.phase == TouchPhase.Ended);</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (created)</span><br><span class="line">    pointerData.position = input.position;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (pressed)</span><br><span class="line">    pointerData.delta = Vector<span class="number">2.</span>zero;</span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">    pointerData.delta = input.position - pointerData.position;</span><br><span class="line"></span><br><span class="line">pointerData.position = input.position;</span><br><span class="line"></span><br><span class="line">pointerData.button = PointerEventData.InputButton.Left;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (input.phase == TouchPhase.Canceled)</span><br><span class="line">&#123;</span><br><span class="line">    pointerData.pointerCurrentRaycast = <span class="keyword">new</span> <span class="built_in">RaycastResult</span>();</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">&#123;</span><br><span class="line">    eventSystem.<span class="built_in">RaycastAll</span>(pointerData, m_RaycastResultCache);</span><br><span class="line"></span><br><span class="line">    var raycast = <span class="built_in">FindFirstRaycast</span>(m_RaycastResultCache);</span><br><span class="line">    pointerData.pointerCurrentRaycast = raycast;</span><br><span class="line">    m_RaycastResultCache.<span class="built_in">Clear</span>();</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> pointerData;</span><br></pre></td></tr></table></figure><p>在该方法中，首先使用上面介绍过的 <code>GetPointerData</code> 方法去获取一个 PointerEventData，如果该 PointerEventData 是新创建的或者当前处理的 Touch 的 <a href="https://docs.unity3d.com/ScriptReference/TouchPhase.html">TouchPhase</a> 等于 <code>TouchPhase.Began</code> (触摸开始阶段)，则 pressed 置为 <code>true</code> 表示按下；如果 TouchPhase 等于 <code>TouchPhase.Canceled</code> (触摸被动结束)或 <code>TouchPhase.Ended</code> (触摸主动结束)，则 released 被置为 <code>true</code> 表示释放。</p><p>紧接着，设置 PointerEventData 的相关信息。</p><ul><li><p>如果是新创建的事件实例，则 PointerEventData 的 position 就是 Touch 的 position；</p></li><li><p>如果当前帧被判定为有按下操作，则与上一帧的事件点击位置的变化量 delta 为 0，否则事件点击位置变化量就是 <code>input.position - pointerData.position</code>；</p></li><li><p>接着更新 position 为 Touch 的 position；鼠标按键 <a href="https://docs.unity3d.com/2017.3/Documentation/ScriptReference/EventSystems.PointerEventData-button.html">button</a> 被置为 <code>PointerEventData.InputButton.Left</code>;</p></li><li><p>最后是为当前事件关联的射线检测结果 pointerCurrentRaycast 设置值，如果当前处理的 Touch 的被动中断(<code>input.phase == TouchPhase.Canceled</code>)，pointerCurrentRaycast 被设置为一个新的 RaycastResult，如没有被中断则调用 EventSystem 的 <code>RaycastAll</code> 方法进行射线检测，并将射线检测的第一个结果作为 pointerCurrentRaycast 的值。</p></li></ul><p>设置结束，返回该 PointerEventData。</p><h2 id="MouseState-GetMousePointerEventData-int-id-方法"><a href="#MouseState-GetMousePointerEventData-int-id-方法" class="headerlink" title="MouseState GetMousePointerEventData(int id) 方法"></a>MouseState GetMousePointerEventData(int id) 方法</h2><p>用于获取当前的 MouseState。在分析这个方法之前，先来看看再 PointerInputModule 类中定义的三个内部类。</p><ul><li><p>MouseButtonEventData 类</p><p>这个类中包含一个 <code>PointerEventData.FramePressState</code> 类型的 buttonState 变量，表示当前帧 button press 的状态。它是一个枚举类型，有 <code>Pressed</code>(按下)、<code>Released</code>(释放)、<code>PressedAndReleased</code>(按下且释放)和 <code>NotChanged</code>(状态同上一帧没有变化)四个值；类中还包含另一个 <code>PointerEventData</code> 类型的变量 buttonData，代表着当前关联事件数据对象。该类中也定义了 <code>bool PressedThisFrame()</code> 和 <code>bool ReleasedThisFrame()</code> 两个方法判断当前帧中 button 是否被按下和是否被释放。</p></li><li><p>ButtonState 类</p><p>ButtonState 类更加单，仅包含两个成员变量: <code>PointerEventData.InputButton</code> 类型的成员变量 m_Button 以及 <code>MouseButtonEventData</code> 类型的 m_EventData。</p></li><li><p>MouseState 类</p><p>MouseState 包含一个 <code>ButtonState</code> 列表，用于同时管理多个 ButtonState。可以使用 <code>bool AnyPressesThisFrame()</code> 和 <code> bool AnyReleasesThisFrame()</code> 方法来判断当前帧是否有 button 被按下或被释放。</p></li></ul><p>看完这几个简单的内部类，再回到 <code>GetMousePointerEventData</code> 方法。这个方法生成 PointerEventData 的过程同 <code>GetTouchPointerEventData</code> 方法相似，不同的是其 position 赋值是当前类(继承自 BaseInputModule)的 <code>input</code> 所在的 mousePosition 值，而 <code>GetTouchPointerEventData</code> 方法中使用的是 Touch 的 position；另一处不同是在当前方法中，会生成三个 PointerEventData 分别给 InputButton 中的 Left、Right 和 Middle 对应的三个 ButtonState 使用，生成好的数据会赋值给当前类的 <code>m_MouseState</code> 变量。</p><h2 id="virtual-void-ProcessMove-PointerEventData-pointerEvent-方法"><a href="#virtual-void-ProcessMove-PointerEventData-pointerEvent-方法" class="headerlink" title="virtual void ProcessMove(PointerEventData pointerEvent) 方法"></a>virtual void ProcessMove(PointerEventData pointerEvent) 方法</h2><p>虚方法 <code>ProcessMove</code> 用于处理“移动”事件，当在一个 GameObject 上产生按下事件并且没有释放则会产生“移动”事件。该方法代码很简单，如果此时 Cursor.lockState 处于 <code>CursorLockMode.Locked</code> 状态，那么目标移动对象会被置为空，否则被设置为当前射线检测得到的对象；移动过程中需要 BaseInputModule 类的 <code>HandlePointerExitAndEnter</code> 方法检测是否需要改变事件 hover 的对象。</p><h2 id="virtual-void-ProcessDrag-PointerEventData-pointerEvent-方法"><a href="#virtual-void-ProcessDrag-PointerEventData-pointerEvent-方法" class="headerlink" title="virtual void ProcessDrag(PointerEventData pointerEvent) 方法"></a>virtual void ProcessDrag(PointerEventData pointerEvent) 方法</h2><p>“移动”事件处理完之后，紧接着虚方法 <code>ProcessDrag</code> 处理“拖拽”事件。代码如下:</p><p>如果当前 PointerEventData 还不是处于 dragging 并且可以开始拖拽，则为 PointerEventData 的 pointerDrag 发送 <code>ExecuteEvents.beginDragHandler</code> 事件。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (!pointerEvent.dragging</span><br><span class="line">    &amp;&amp; <span class="built_in">ShouldStartDrag</span>(pointerEvent.pressPosition, pointerEvent.position, eventSystem.pixelDragThreshold, pointerEvent.useDragThreshold))</span><br><span class="line">&#123;</span><br><span class="line">    ExecuteEvents.<span class="built_in">Execute</span>(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.beginDragHandler);</span><br><span class="line">    pointerEvent.dragging = <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在开始拖拽之前，将所有有关 press 的状态删除，同时清除 selection。如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (pointerEvent.pointerPress != pointerEvent.pointerDrag)</span><br><span class="line">&#123;</span><br><span class="line">    ExecuteEvents.<span class="built_in">Execute</span>(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);</span><br><span class="line"></span><br><span class="line">    pointerEvent.eligibleForClick = <span class="literal">false</span>;</span><br><span class="line">    pointerEvent.pointerPress = null;</span><br><span class="line">    pointerEvent.rawPointerPress = null;</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// 发送拖拽事件</span></span><br><span class="line">ExecuteEvents.<span class="built_in">Execute</span>(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.dragHandler);</span><br></pre></td></tr></table></figure><p>在 PointerInputModule 类中，除了上面的几个比较重要的方法，还有一些简单的辅助方法或者其他方法，如 <code>DeselectIfSelectionChanged</code> 用来在 Selection 改变的时候反注册 Event System 的 Selected GameObject (最终调用的也是 EventSystem 的 <code>SetSelectedGameObject</code> 方法)；再比如 <code>ClearSelection</code> 用来清除当前所有缓存的 PointerEventData 中的 Selection(发送 <code>ExecuteEvents.pointerExitHandler</code> 事件，清空 hovered)。</p><p>到这里可能会感觉全是干涩的代码分析，没有应用到真正的 Event System 的流程中。那么接下来，就从 StandaloneInputModule 类开始，看看一个具体的 Input Module 是如何工作的以及再来看看上面讲到过的方法具体的应用。</p><h1 id="Standalone-Input-Module"><a href="#Standalone-Input-Module" class="headerlink" title="Standalone Input Module"></a>Standalone Input Module</h1><p>这种模式是为类似鼠标、控制器等硬件(也包括触摸设备)的输入抽象设计的。根据不同的输入，将会转换成为 Unity 内不同的抽象事件被分发出去，然后被拦截处理。</p><p>当产生事件时，自定义配置的 Raycaster 将会计算拦截事件的元素对象；同时，对于 Navigation events，也可以设置要检测的名称。StandaloneInputModule 类有以下属性可以自定义配置:</p><table><thead><tr><th align="left">属性</th><th align="left">描述</th></tr></thead><tbody><tr><td align="left">Horizontal Axis</td><td align="left">Input Module 中水平轴所在的名字(Input Manager 中预设的值)</td></tr><tr><td align="left">Vertical Axis</td><td align="left">垂直轴所在的名字(Input Manager 中预设的值)</td></tr><tr><td align="left">Submit Button</td><td align="left">Submit button 所在的名字(Input Manager 中预设的值)</td></tr><tr><td align="left">Cancel Button</td><td align="left">Cancel button 所在的名字(Input Manager 中预设的值)</td></tr><tr><td align="left">Input Actions Per Second</td><td align="left">每秒允许最大输入事件次数</td></tr><tr><td align="left">Repeat Delay</td><td align="left">重复输入延迟(单位: 秒)</td></tr><tr><td align="left">Force Module Active</td><td align="left">开启将强制 <strong>Standalone Input Module</strong> 处于激活状态</td></tr></tbody></table><ul><li><p>Vertical &#x2F; Horizontal 轴用于键盘&#x2F;控制器的导航性事件</p></li><li><p>Submit &#x2F; Cancel button 用来发送 submit 和 cancel 事件</p></li><li><p>事件和事件之间有超时机制，每秒中最多有 Input Actions Per Second 个事件</p></li></ul><p>究竟 Standalone Input Module 是如何应用到 EventSystem 流程中的了？前面在解析 BaseInputModule 类时提到过，在 <code>OnEnable</code> 方法中会将当前绑定的 Input Module 组件和 EventSystem 关联起来(通过 EventSystem 类的 <code>UpdateModules</code> 方法)；然后在 EventSystem 每一帧更新中，都会调用 Input Module 的 <code>Process()</code> 方法，这样 Input Module 就开始运作起来。</p><h2 id="void-Process-方法"><a href="#void-Process-方法" class="headerlink" title="void Process() 方法"></a>void Process() 方法</h2><p>StandaloneInputModule 类覆写了 <code>Process()</code> 方法，并在其中实现了各类事件的处理。所以接下来就从 StandaloneInputModule 类的 <code>Process()</code> 入手开始分析。</p><h3 id="首先是处理当前被-Selected-对象的-Update-事件，代码如下"><a href="#首先是处理当前被-Selected-对象的-Update-事件，代码如下" class="headerlink" title="首先是处理当前被 Selected 对象的 Update 事件，代码如下:"></a>首先是处理当前被 Selected 对象的 Update 事件，代码如下:</h3><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">bool</span> usedEvent = <span class="built_in">SendUpdateEventToSelectedObject</span>();</span><br></pre></td></tr></table></figure><p><code>SendUpdateEventToSelectedObject()</code> 方法首先判断当前 EventSystem Selected 的对象是否为空，不为空则获取当前事件数据 m_BaseEventData，然后发送 <code>ExecuteEvents.updateSelectedHandler</code> 事件供拦截处理，返回结果表示当前的事件数据 m_BaseEventData 是否被使用。</p><h3 id="然后使用-ProcessTouchEvents-方法处理-Touch-事件，方法返回-true-表示处理了-Touch-事件；"><a href="#然后使用-ProcessTouchEvents-方法处理-Touch-事件，方法返回-true-表示处理了-Touch-事件；" class="headerlink" title="然后使用 ProcessTouchEvents() 方法处理 Touch 事件，方法返回 true 表示处理了 Touch 事件；"></a>然后使用 <code>ProcessTouchEvents()</code> 方法处理 Touch 事件，方法返回 <code>true</code> 表示处理了 Touch 事件；</h3><p>该方法主要是处理 Touch 事件。对于当前 Input 中的每个 Touch 事件，都会执行以下操作: 首先调用 PointerInputModule 中的 <code>GetTouchPointerEventData</code> 方法根据 Touch 事件获取一个 PointerEventData，并判断当前 Touch 的状态是 pressed 还是 released 状态。接着调用 <code>ProcessTouchPress</code> 方法处理 Touch 按下，下面就来看看处理 Touch press 的过程: 具体下来就有 press 和 release 两大块的实现。</p><ul><li><strong>press 处理</strong></li></ul><p>如果方法传入参数 <code>pressed</code> 为 <code>true</code>，表示当前事件是新创建或 Touch  phase 处于 <code>TouchPhase.Began</code> 阶段，所以需要处理 press 相关的事件。在处理 press 中，首先初始化了当前 PointerEventData 的相关值，接着调用了 PointerInputModule 中的 <code>DeselectIfSelectionChanged</code> 方法检测是否需要删除当前 EventSystem 的 Selected 对象(比如点击了新的对象，就会删除旧的 Selected 对象，然后由当前的 press 决定新的 Selected 对象)。处理 press 包含如下几个阶段:</p><ol><li><p>如果当前 PointerEventData 的 <code>pointerEnter</code> 对象和射线检测的对象不是同一个，那么就会调用 BaseInputModule 类的 <code>HandlePointerExitAndEnter</code> 方法给射线检测到的对象及其祖先发送 <code>ExecuteEvents.pointerEnterHandler</code> 事件。</p></li><li><p>处理完 pointer enter，紧接着处理 pointer down 事件；从当前射线检测到的对象开始搜寻能处理 <code>ExecuteEvents.pointerDownHandler</code> 事件的对象。代码如下:</p></li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">var newPressed = ExecuteEvents.<span class="built_in">ExecuteHierarchy</span>(currentOverGo, pointerEvent, ExecuteEvents.pointerDownHandler);</span><br></pre></td></tr></table></figure><ol start="3"><li>若未能处理 press，接着寻找能处理 click 事件的对象。代码如下:</li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (newPressed == null)</span><br><span class="line">    newPressed = ExecuteEvents.<span class="built_in">GetEventHandler</span>&lt;IPointerClickHandler&gt;(currentOverGo);</span><br></pre></td></tr></table></figure><ol start="4"><li>得到 newPressed 对象之后，接着对 PointerEventData 相关属性赋值。然后开始处理 <code>ExecuteEvents.initializePotentialDrag</code> 事件，代码如下:</li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">pointerEvent.pointerDrag = ExecuteEvents.<span class="built_in">GetEventHandler</span>&lt;IDragHandler&gt;(currentOverGo);</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (pointerEvent.pointerDrag != null)</span><br><span class="line">    ExecuteEvents.<span class="built_in">Execute</span>(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.initializePotentialDrag);</span><br></pre></td></tr></table></figure><p>到这里，press 阶段相关的事件就处理完毕。接下来就是 release 相关部分的实现。</p><ul><li><strong>release 处理</strong></li></ul><p>如果方法传入参数 <code>released</code> 为 <code>true</code>，表示当前 Touch 已经被系统中断或已经结束，所以需要处理 release 相关会触发的事件。</p><ol><li>对 press 的对象进行 <code>ExecuteEvents.pointerUpHandler</code> 事件处理。代码如下:</li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ExecuteEvents.<span class="built_in">Execute</span>(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);</span><br></pre></td></tr></table></figure><ol start="2"><li>紧接着寻找是否有处理 pointer click 事件的对象，如果有且该对象是当前 PointerEventData 的 pointerPress，那么就处理 <code>ExecuteEvents.pointerClickHandler</code> 事件；否则如果当前 PointerEventData 的 pointerDrag 不为空且正在拖拽中，就处理 <code>ExecuteEvents.dropHandler</code> 事件。</li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">var pointerUpHandler = ExecuteEvents.<span class="built_in">GetEventHandler</span>&lt;IPointerClickHandler&gt;(currentOverGo);</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (pointerEvent.pointerPress == pointerUpHandler &amp;&amp; pointerEvent.eligibleForClick)</span><br><span class="line">&#123;</span><br><span class="line">    ExecuteEvents.<span class="built_in">Execute</span>(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerClickHandler);</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">else</span> <span class="keyword">if</span> (pointerEvent.pointerDrag != null &amp;&amp; pointerEvent.dragging)</span><br><span class="line">&#123;</span><br><span class="line">    ExecuteEvents.<span class="built_in">ExecuteHierarchy</span>(currentOverGo, pointerEvent, ExecuteEvents.dropHandler);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="3"><li>最后根据相应条件处理结束拖拽(<code>ExecuteEvents.endDragHandler</code>)和退出(<code>ExecuteEvents.pointerExitHandler</code>)事件。</li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (pointerEvent.pointerDrag != null &amp;&amp; pointerEvent.dragging)</span><br><span class="line">    ExecuteEvents.<span class="built_in">Execute</span>(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.endDragHandler);</span><br><span class="line"></span><br><span class="line">ExecuteEvents.<span class="built_in">ExecuteHierarchy</span>(pointerEvent.pointerEnter, pointerEvent, ExecuteEvents.pointerExitHandler);</span><br></pre></td></tr></table></figure><p>到这里，release 相关部分也完成了。</p><p>处理完 Touch press 相关事件后，然后根据 released 状态，如果为 <code>false</code> (未释放)则继续调用 <code>ProcessMove</code> 和 <code>ProcessDrag</code> 方法分别处理移动和拖拽事件(这两个事件的处理是直接使用 PointerInputModule 类中的方法，上面已经讲解过)，如果已经 released 就将得到的 PointerEventData 从当前的 Input Module 中的 m_PointerData 移除。最后返回是否处理了至少一个 Touch 事件。</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_input_module_1.jpeg" width="70%" height="70%" /></center><p><em>上图示 Touch 事件处理流程</em></p><h3 id="如果-Touch-事件处理失败并且检测到了鼠标事件，那么就使用-ProcessMouseEvent-方法处理鼠标事件；"><a href="#如果-Touch-事件处理失败并且检测到了鼠标事件，那么就使用-ProcessMouseEvent-方法处理鼠标事件；" class="headerlink" title="如果 Touch 事件处理失败并且检测到了鼠标事件，那么就使用 ProcessMouseEvent() 方法处理鼠标事件；"></a>如果 Touch 事件处理失败并且检测到了鼠标事件，那么就使用 <code>ProcessMouseEvent()</code> 方法处理鼠标事件；</h3><p><code>ProcessMouseEvent()</code> 方法用来处理所有的鼠标事件，它内部调用了重载的 <code>ProcessMouseEvent(int id)</code> 方法处理。鼠标事件的处理过程中，首先调用 PointerInputModule 类的 <code>GetMousePointerEventData</code> 方法(上面分析过该方法)获得 MouseState 对象；然后根据得到的 MouseState 对象调用 <code>ProcessMousePress</code> 方法处理鼠标左键的按下，同样下面就来看看处理鼠标 press 的过程: 具体下来也是 press 和 release 两大块的实现。</p><ul><li><p>鼠标 press 处理和 Touch press 处理时使用 <code>ProcessTouchPress</code> 方法内部实现基本一致，但有一点不同就是: 在进行检测是否需要删除当前 EventSystem 的 Selected 对象之后，紧接着不会调用 BaseInputModule 类的 <code>HandlePointerExitAndEnter</code> 方法给射线检测到的对象及其祖先发送 <code>ExecuteEvents.pointerEnterHandler</code> 事件(这一步被移至 release 阶段处理)。</p></li><li><p>release 处理也和 Touch 处理时 <code>ProcessTouchPress</code> 方法中 release 阶段一致，只不过在该阶段最后一步，鼠标事件 enter 和 exit 会被重新刷新。因此在鼠标事件的 press 处理中，在 release 阶段最后一步，当前射线检测到的对象及其祖先对象才会收到 <code>ExecuteEvents.pointerEnterHandler</code> 事件，上一个触发鼠标事件的对象(如不为空)及其相关祖先对象才会接收到 <code>ExecuteEvents.pointerExitHandler</code> 事件。</p></li></ul><p>处理完鼠标左键 press，接下来就调用 PointerInputModule 类的 <code>ProcessMove</code> 方法处理移动事件，前面讲到过 <code>ProcessMove</code> 方法调用 BaseInputModule 类的 <code>HandlePointerExitAndEnter</code> 方法，从而当前射线检测的对象会接收到 <code>ExecuteEvents.pointerExitHandler</code> 事件。</p><p>处理完移动事件，最后就是处理鼠标左键的拖拽事件，调用 PointerInputModule 类的 <code>ProcessDrag</code> 方法处理。这个方法之前分析过，这里不再讲解。</p><p>到这里鼠标左键处理完毕，接着使用同样的方法对鼠标右键和中间键进行 press 和 drag 处理(不需要进行 move 事件处理)。最后在 <code>ProcessMouseEvent</code> 方法中处理的就是滚轮事件，代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (!Mathf.<span class="built_in">Approximately</span>(leftButtonData.buttonData.scrollDelta.sqrMagnitude, <span class="number">0.0f</span>))</span><br><span class="line">&#123;</span><br><span class="line">    var scrollHandler = ExecuteEvents.<span class="built_in">GetEventHandler</span>&lt;IScrollHandler&gt;(leftButtonData.buttonData.pointerCurrentRaycast.gameObject);</span><br><span class="line">    ExecuteEvents.<span class="built_in">ExecuteHierarchy</span>(scrollHandler, leftButtonData.buttonData, ExecuteEvents.scrollHandler);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>当 scrollDelta 不近似为 0 时，就发送 <code>ExecuteEvents.scrollHandler</code> 事件。</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_input_module_2.jpeg" width="70%" height="70%" /></center><p><em>上图示 Mouse 事件处理流程</em></p><h3 id="Touch-鼠标事件处理完毕后，紧接着如果当前需要处理-Navigation-事件。"><a href="#Touch-鼠标事件处理完毕后，紧接着如果当前需要处理-Navigation-事件。" class="headerlink" title="Touch &#x2F; 鼠标事件处理完毕后，紧接着如果当前需要处理 Navigation 事件。"></a>Touch &#x2F; 鼠标事件处理完毕后，紧接着如果当前需要处理 Navigation 事件。</h3><p>因为在处理 Touch &#x2F; 鼠标事件时会改变当前 Selected 对象，所以要先处理 Touch &#x2F; 鼠标事件，然后按需处理Navigation 事件。</p><p>首先处理 Navgation 中当前 Selected 对象的 Move 事件，代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (!usedEvent)</span><br><span class="line">    usedEvent |= <span class="built_in">SendMoveEventToSelectedObject</span>();</span><br></pre></td></tr></table></figure><p>在上面处理 <code>ExecuteEvents.updateSelectedHandler</code> 事件时，如果收到事件的对象没有使用 BaseInputModule 中的事件 <code>m_BaseEventData</code>，那么就开始对当前 EventSystem Selected 的对象处理 move 事件，调用 <code>SendMoveEventToSelectedObject()</code> 方法，这个方法代码主要就是处理 move 事件。</p><ol><li>首先就是判断是否有按下轴对应的按键，如果没有则返回，如下:</li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Vector2 movement = <span class="built_in">GetRawMoveVector</span>();</span><br><span class="line"><span class="keyword">if</span> (Mathf.<span class="built_in">Approximately</span>(movement.x, <span class="number">0</span>f) &amp;&amp; Mathf.<span class="built_in">Approximately</span>(movement.y, <span class="number">0</span>f))</span><br><span class="line">&#123;</span><br><span class="line">    m_ConsecutiveMoveCount = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="2"><li>接着检测是否再次按下了轴按键。如果没有再次按下(上次按下后没有松开)且当前按键得到的方向与上一次同向，则等待延时 m_RepeatDelay(这个就是上面提到过可以配置的重复按键的延时时间)；若方向改变了 90 度以上或者已经超过了延时时间，则根据当前输入速率是否超过每秒允许输入的最多次数(m_InputActionsPerSecond，也是可以配置的)来判定是否需要处理当前 move，需要处理则进入下一步。这一步的最开始判定如果是再次按下轴按键，那么也是自动进入下一步。</li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">bool</span> allow = input.<span class="built_in">GetButtonDown</span>(m_HorizontalAxis) || input.<span class="built_in">GetButtonDown</span>(m_VerticalAxis);</span><br><span class="line"><span class="type">bool</span> similarDir = (Vector<span class="number">2.</span><span class="built_in">Dot</span>(movement, m_LastMoveVector) &gt; <span class="number">0</span>);</span><br><span class="line"><span class="keyword">if</span> (!allow)</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">if</span> (similarDir &amp;&amp; m_ConsecutiveMoveCount == <span class="number">1</span>)</span><br><span class="line">        allow = (time &gt; m_PrevActionTime + m_RepeatDelay);</span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">        allow = (time &gt; m_PrevActionTime + <span class="number">1</span>f / m_InputActionsPerSecond);</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span> (!allow)</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">false</span>;</span><br></pre></td></tr></table></figure><ol start="3"><li>到了这一步，就开始处理 move 事件了。首先获取轴按键按下方向得到事件数据 axisEventData，如果当前按下事件的方向 moveDir 不等于 <code>MoveDirection.None</code>，就对当前被 Selected 的对象发送 <code>ExecuteEvents.moveHandler</code> 事件；否则设置 <code>m_ConsecutiveMoveCount</code> (连续移动计数)为 0。代码如下:</li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">var axisEventData = <span class="built_in">GetAxisEventData</span>(movement.x, movement.y, <span class="number">0.6f</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (axisEventData.moveDir != MoveDirection.None)</span><br><span class="line">&#123;</span><br><span class="line">    ExecuteEvents.<span class="built_in">Execute</span>(eventSystem.currentSelectedGameObject, axisEventData, ExecuteEvents.moveHandler);</span><br><span class="line">    <span class="keyword">if</span> (!similarDir)</span><br><span class="line">        m_ConsecutiveMoveCount = <span class="number">0</span>;</span><br><span class="line">    m_ConsecutiveMoveCount++;</span><br><span class="line">    m_PrevActionTime = time;</span><br><span class="line">    m_LastMoveVector = movement;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">&#123;</span><br><span class="line">    m_ConsecutiveMoveCount = <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>SendMoveEventToSelectedObject()</code> 方法返回结果为 axisEventData 事件数据是否被使用。</p><p>如果上一步处理 move 事件，axisEventData 未被使用，那么最后就会调用 <code>SendSubmitEventToSelectedObject()</code> 方法处理当前 Selected 对象的 Submit 事件，代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">var data = <span class="built_in">GetBaseEventData</span>();</span><br><span class="line"><span class="keyword">if</span> (input.<span class="built_in">GetButtonDown</span>(m_SubmitButton))</span><br><span class="line">    ExecuteEvents.<span class="built_in">Execute</span>(eventSystem.currentSelectedGameObject, data, ExecuteEvents.submitHandler);</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (input.<span class="built_in">GetButtonDown</span>(m_CancelButton))</span><br><span class="line">    ExecuteEvents.<span class="built_in">Execute</span>(eventSystem.currentSelectedGameObject, data, ExecuteEvents.cancelHandler);</span><br></pre></td></tr></table></figure><p>代码很简单，如果当前按下了 m_SubmitButton 对应的按键，就发送 <code>ExecuteEvents.submitHandler</code> 事件；如果按下 m_CancelButton 按键，就发送 <code>ExecuteEvents.cancelHandler</code> 事件。</p><p>到这里 <code>Process()</code> 就分析完了，在 Event System 中，每一帧都会调用该方法。因此事件得以在整个系统中不断产生和分发，然后被拦截处理。</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_input_module_0.jpeg" width="50%" height="50%" /></center><p><em>上图示 Standalone Input Module 中事件流处理</em></p><p>StandaloneInputModule 类的主要方法在这里就分析完了。将这些方法串联起来，我们知道了 Standalone Input Module 设计的目的以及其在 Event System 中所扮演的角色。整个 Event System 中主要的逻辑大部分是在 Standalone Input Module 中实现的，包括: 外部事件的输入抽象为 Unity 组件可处理的事件、事件流动的过程、射线检测响应事件的对象、分发事件等等。</p><h1 id="Touch-Input-Module"><a href="#Touch-Input-Module" class="headerlink" title="Touch Input Module"></a>Touch Input Module</h1><p>Touch Input Module 主要是为可触摸设备设计的，对于用户的输入它可以转化为 touching 和 dragging 等事件供 Unity 组件处理。TouchInputModule 类已经被标记为废弃(obsolete)，在 StandaloneInputModule 类中已经有了所有关于 Touch Input Module 的实现。</p>]]>
    </content>
    <id>https://lujun.pages.dev/2019/08/18/unity_input_modules/</id>
    <link href="https://lujun.pages.dev/2019/08/18/unity_input_modules/"/>
    <published>2019-08-18T10:02:29.000Z</published>
    <summary>
      <![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>Input Modules 是 Event System 组成的一部分，负责产生抽象事件并分发事件到 GameObject 处理；在代码架构上它也是 Event System 中处理大部分业务逻辑的地方，可以高度配置化。不同的 Input Module 可以映射外部不同的硬件输入至 Unity Event System 中转化成能处理的事件，并管理这些事件的状态。Unity 中内置了 Standalone Input Module 和 Touch Input Module(已经被标记为 <code>obsolete</code>，现在由 Standalone Input Module 一并处理)。</p>
<p>本篇文章就来剖析 Input Modules 在 Unity 中的实现。</p>]]>
    </summary>
    <title>Input Modules 在 Unity 中扮演了什么样的角色?</title>
    <updated>2026-06-22T14:03:58.804Z</updated>
  </entry>
  <entry>
    <author>
      <name>lujun</name>
    </author>
    <category term="Unity" scheme="https://lujun.pages.dev/tags/Unity/"/>
    <content>
      <![CDATA[<script src="/assets/js/APlayer.min.js"> </script><p>在 Unity 射线检测中，常常会用到 <code>Camera.ScreenPointToRay</code> 方法。这个方法很简单，传入一个屏幕上的像素坐标，返回一条在世界空间下从 Camera 的近裁剪面出发穿过屏幕上的像素坐标点的射线。</p><span id="more"></span><h2 id="ScreenPointToRay-方法"><a href="#ScreenPointToRay-方法" class="headerlink" title="ScreenPointToRay 方法"></a>ScreenPointToRay 方法</h2><blockquote><p>Resulting ray is in world space, starting on the near plane of the camera and going through position’s (x,y) pixel coordinates on the screen (position.z is ignored).</p></blockquote><p><code>ScreenPointToRay</code> 方法生成一条从近裁剪面出发，穿过屏幕像素坐标点的一条射线。该方法除了传入一个屏幕像素坐标作为参数以外，还有一个重载方法，需要多传入一个 <code>Camera.MonoOrStereoscopicEye</code> 类型的参数，用于指定使用哪一种 Camera eye。通常在立方体渲染会用到，这里不做过多解析。</p><h2 id="Camera-的一些属性"><a href="#Camera-的一些属性" class="headerlink" title="Camera 的一些属性"></a>Camera 的一些属性</h2><p>接下来主要看看 <code>Ray ScreenPointToRay(Vector3 pos)</code> 如何得到一条射线。由于计算会涉及到 Camera 的一些属性，所以先来简单了解一下下面这几个 Camera 属性的定义。</p><ul><li>Field Of View(当 Projection 设置为 <code>Perspective</code> 生效)</li></ul><p>摄像机视野角度的宽度，沿 Y 轴方向扩张。</p><ul><li>Viewport Rect，包含 X、Y、W 和 H(范围均在 0 - 1)</li></ul><p>用于指定 Camera 渲染的图像绘制在屏幕的位置和区域大小。其中 X 和 Y 是摄像机视图在屏幕上绘制的左下角坐标；W 和 H 是视图所在的宽和高。</p><ul><li>Clipping Planes，包含 Near 和 Far。</li></ul><p>裁剪面距离摄像机的距离。Near 是近裁剪面距离摄像机对象的距离；Far 是远裁剪面距离摄像机对象的距离。</p><ul><li>Target Display</li></ul><p>目标展示的窗口展示器，比如手机屏幕、平板屏幕等，和物理分辨率有关系。</p><h2 id="如何计算得到屏幕像素点的射线"><a href="#如何计算得到屏幕像素点的射线" class="headerlink" title="如何计算得到屏幕像素点的射线"></a>如何计算得到屏幕像素点的射线</h2><p>了解了 Camera 上面的这几个属性后，接着就来计算一下近裁剪面在世界空间下的大小和位置。在接下来的计算中，Camera 的投影模式如未提及，则默认为 <code>Perspective</code>。</p><p>首先配置 Camera 的相关参数: 将 Field Of View 设置为 60；Clipping Planes 的 Near 和 Far 分别设置为 1 和 20；Viewport Rect 中的 X、Y、W、H 分别使用默认参数 0、0、1、1；Target Display 设置为 Display1(此时真实分辨率为 200*200)；Camera Transform 的 Position 为 (0,1,-10)。</p><p>上面的配置完成后，取屏幕像素坐标 (0, 0) 处作为目标点作为参数传入 <code>ScreenPointToRay</code> 方法(需要的参数是 Vector3 类型，z 轴会被忽略)，运行 Unity 看看返回结果 Ray 的信息。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Ray ray = Camera.main.<span class="built_in">ScreenPointToRay</span>(<span class="keyword">new</span> <span class="built_in">Vector3</span>(<span class="number">0</span>, <span class="number">0</span>, <span class="number">0</span>));</span><br><span class="line">Debug.<span class="built_in">Log</span>(<span class="string">&quot;Ray origin: &quot;</span> + ray.origin + <span class="string">&quot;, Ray direction: &quot;</span> + ray.direction);</span><br></pre></td></tr></table></figure><p>运行后 Console 打印如下所示:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_camera_screen_point_to_ray_1.png" alt="unity_camera_screen_point_to_ray_1.png" title="unity_camera_screen_point_to_ray_1.png" width="60%" height="60%" /></center><p>屏幕像素坐标 (0, 0) 处使用 <code>ScreenPointToRay</code> 方法得到的 Ray origin 值是 <code>(-0.6,0.4,-9.0)</code>，direction 值为 <code>(-0.4,-0.4,0.8)</code>，这些值是如何计算得到的了？</p><p>此时 Camera 的视锥体截面图大致如下:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_camera_screen_point_to_ray_2.png" alt="unity_camera_screen_point_to_ray_3.png" title="unity_camera_screen_point_to_ray_3.png" width="70%" height="70%" /></center><p>对于近裁剪面的高度，已知 FOV 和 Near，简单的三角函数就能求得。如下:</p><div class="math-display">\[nearClipPanelHeight &#x3D; tan(\frac{FOV}{2}) \times Near \times 2\]</div><p>远裁剪面的高度也可以使用类似的方式求得，也就是:</p><div class="math-display">\[farClipPanelHeight &#x3D; tan(\frac{FOV}{2}) \times Far \times 2\]</div><p>那近裁剪面的宽度如何求得了? 我在 Unity 中 Camera 的横纵比由其参数 <code>Target Display</code> 所在的真实比例和 <code>Viewport Rect</code> 中的 W 和 H 属性共同决定，因为在上面我们将 <code>Viewport Rect</code> 设置为默认值，所以 Camera 的横纵比就是其参数 <code>Target Display</code> 所在的显示视图的比例(如果设置了 Viewport Rect 中的值不是默认值，那么计算横纵比也要将 Viewport Rect 考虑进去)。此时 <code>Target Display</code> 的 Display1 分辨率为 200*200，所以 Camera 的横纵比就是 1:1。因此得到近裁剪面的宽度就是其高度，远裁剪面也类似得到。</p><div class="math-display">\[nearClipPanelWidth &#x3D; nearClipPanelHeight\]</div><div class="math-display">\[farClipPanelWidth &#x3D; farClipPanelHeight\]</div><p><strong>Camera 的横纵比在渲染管线中进行至投影变换(观察空间 - 裁剪空间)一步时也要用到。同样是是通过横纵比计算得到变换中需要用到的投影矩阵，将顶点从观察空间变换到裁剪空间，然后进行裁剪剔除。</strong></p><p>在计算得到近裁剪面的宽高之后，根据 Camera Transform 在世界空间下的位置和距离 Camera 的距离(Near)，就可以求得近裁剪面上个点在世界空间下的坐标。如下图:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_camera_screen_point_to_ray_3.png" alt="unity_camera_screen_point_to_ray_3.png" title="unity_camera_screen_point_to_ray_3.png" width="70%" height="70%" /></center><p>此时计算近裁剪面在世界空间下左下角的坐标:</p><div class="math-display">\[pos &#x3D; (\frac{-nearClipPanelWidth}{2}, 1 - \frac{nearClipPanelHeight}{2}, Camera.z + Near)\]</div><p>在渲染管线的屏幕映射这一步中，将裁剪空间中的三维顶点坐标投影到屏幕上(将视锥体中的三维坐标映射到屏幕上的二维像素坐标)。首先使用齐次除法将裁剪空间变换到一个立方体中，坐标范围是[-1,1]；然后将这些经过齐次除法变换过之后的坐标映射为屏幕像素坐标(缩放)。</p><p>屏幕左下角像素坐标为(0,0)，右上角为(pixelWidth,piexelHeight)。这里以上面计算所得近裁剪面中左下角的点为例，通过一些列变换来到屏幕映射齐次除法过后的的立方体中，此时它对应着立方体中(-1,-1,-1)位置处的点，将这个坐标映射到屏幕像素坐标就是(0,0)，z 分量被用作深度缓冲。</p><p>所以当使用 <code>Camera.ScreenPointToRay</code> 方法计算屏幕像素坐标(0,0)处的射线时，实际上就是计算的从 Camera 出发，穿过近裁剪面左下角处的一条射线。上面计算的世界坐标下近裁剪面左下角坐标也就是 <code>ScreenPointToRay</code> 方法在屏幕像素坐标(0,0)处得到的射线的 ray.origin 值，同时该方法计算得到的 ray.direction 也就是从 Camera 朝近裁剪面左下角点的方向值(归一化后的结果)。</p><h2 id="ViewportPointToRay-方法"><a href="#ViewportPointToRay-方法" class="headerlink" title="ViewportPointToRay 方法"></a>ViewportPointToRay 方法</h2><p>Camera 类还有一个 <code>ViewportPointToRay</code> 方法，这个方法同 <code>ScreenPointToRay</code> 类似也是计算某点的一条射线，不同的是该方法使用 Viewport space 中的坐标来计算的(Viewport space 中的坐标: 屏幕映射中齐次除法之后，将坐标从[-1,1]缩放为[0,1]就是 Viewport space 下的坐标，所以该坐标是相对于 Camera 的，并且已经归一化)。</p>]]>
    </content>
    <id>https://lujun.pages.dev/2019/08/14/unity_camera_screen_point_to_ray/</id>
    <link href="https://lujun.pages.dev/2019/08/14/unity_camera_screen_point_to_ray/"/>
    <published>2019-08-14T01:02:29.000Z</published>
    <summary>
      <![CDATA[<p>在 Unity 射线检测中，常常会用到 <code>Camera.ScreenPointToRay</code> 方法。这个方法很简单，传入一个屏幕上的像素坐标，返回一条在世界空间下从 Camera 的近裁剪面出发穿过屏幕上的像素坐标点的射线。</p>]]>
    </summary>
    <title>Unity 中的 ScreenPointToRay 方法</title>
    <updated>2026-06-22T14:03:58.804Z</updated>
  </entry>
  <entry>
    <author>
      <name>lujun</name>
    </author>
    <category term="Unity" scheme="https://lujun.pages.dev/tags/Unity/"/>
    <content>
      <![CDATA[<script src="/assets/js/APlayer.min.js"> </script><p>Raycasters 用来检测当前事件发送给哪个对象，检测原理就是 Raycast。当给定一个屏幕坐标系中的位置，Raycasters 就会利用射线检测寻找潜在的对象，并返回一个离当前屏幕最近的对象。</p><p>在 Unity Raycasters 中有三种类型的 Raycasters:</p><ul><li><p>Graphic Raycaster - 存在于 Canvas 下，用于检测 Canvas 中所有的物体</p></li><li><p>Physics 2D Raycaster - 用于检测 2D 物体</p></li><li><p>Physics Raycaster - 用于检测 3D 物体</p></li></ul><p>接下来，就来分析一下各个类型 Raycaster 的源码来看看其的工作流程。</p><span id="more"></span><p>Raycast 在 Event System 流程中所处的位置大致如下图:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_event_system_raycaster.png" alt="unity_event_system_raycaster.png" title="unity_event_system_raycaster.png" width="50%" height="50%" /></center><h1 id="BaseRaycaster-类"><a href="#BaseRaycaster-类" class="headerlink" title="BaseRaycaster 类"></a>BaseRaycaster 类</h1><p>Unity Raycasters 中的三个 Raycaster 类都继承自 BaseRaycaster。首先就来看看 BaseRaycaster 类。</p><p>BaseRaycaster 类很简单，它包含一个抽象方法 <code>Raycast</code>，定义如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">abstract <span class="type">void</span> <span class="title">Raycast</span><span class="params">(PointerEventData eventData, List&lt;RaycastResult&gt; resultAppendList)</span></span>;</span><br></pre></td></tr></table></figure><p>这个方法供子类覆写以实现对不同类别的物体进行射线检测。BaseRaycaster 类还继承自 UIBehaviour 类，因此它还覆写了 <code>OnEnable</code> 和 <code>OnDisable</code> 方法，在 <code>OnEnable</code> 方法中向 RaycasterManager 类注册了自己，在 <code>OnDisable</code> 方法中从 RaycasterManager 类移除了自己的注册。</p><p>另外该类中还包含了 eventCamera、sortOrderPriority、renderOrderPriority 等属性，在射线检测物体时会用到。</p><h1 id="Physics-Raycaster"><a href="#Physics-Raycaster" class="headerlink" title="Physics Raycaster"></a>Physics Raycaster</h1><p>Physics Raycaster 用于检测场景中的 3D 物体对象。</p><p>PhysicsRaycaster 类继承自 BaseRaycaster，既然是射线检测那么最重要的方法莫过于 <code>Raycast</code>，接下来就一起看看这个方法。</p><p>在 <code>Raycast</code> 方法中，首先使用传入的 PointerEventData 参数调用 <code>ComputeRayAndDistance</code> 方法，计算得到从当前射线检测使用的 Camera 的近裁剪面处出发，穿过屏幕事件发生处位置的一条射线；这个方法还会计算一个射线检测使用的最大距离 <code>distanceToClipPlane</code>。</p><p><code>ComputeRayAndDistance</code> 内部使用了 Camera 类的 <code>ScreenPointToRay</code> 方法将某点转换成一条射线，根据得到的射线的方向以及 Camera 的 farClipPlane 和 nearClipPlane 求得检测最大距离 <code>distanceToClipPlane</code>。具体代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">ComputeRayAndDistance</span><span class="params">(PointerEventData eventData, out Ray ray, out <span class="type">float</span> distanceToClipPlane)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    ray = eventCamera.<span class="built_in">ScreenPointToRay</span>(eventData.position);</span><br><span class="line"></span><br><span class="line">    <span class="type">float</span> projectionDirection = ray.direction.z;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 这里加了个保护，因为 projectionDirection 近似为 0 的时候不能被除，因此 distanceToClipPlane 取 Mathf.Infinity 无限大</span></span><br><span class="line">    distanceToClipPlane = Mathf.<span class="built_in">Approximately</span>(<span class="number">0.0f</span>, projectionDirection) ? Mathf.Infinity : Mathf.<span class="built_in">Abs</span>((eventCamera.farClipPlane - eventCamera.nearClipPlane) / projectionDirection);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>接下来就是进行射线检测了，代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">var hits = ReflectionMethodsCache.Singleton.<span class="built_in">raycast3DAll</span>(ray, distanceToClipPlane, finalEventMask);</span><br></pre></td></tr></table></figure><p>这里的 <code>ReflectionMethodsCache</code> 类里面缓存了一些通过反射得到的射线检测相关的类方法。在上面的代码中使用了 <code>raycast3DAll</code> 这个代理，最终执行的是 Physics 类的 <code>RaycastAll</code> 方法。传入的三个参数就是射线 ray，最大检测距离 distanceToClipPlane 以及需要检测的层 finalEventMask，返回结果就是检测成功得到的 RaycastHit 数组。第三个参数 finalEventMask 定义如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="type">int</span> finalEventMask</span><br><span class="line">&#123;</span><br><span class="line">    get &#123; <span class="keyword">return</span> (eventCamera != null) ? eventCamera.cullingMask &amp; m_EventMask : kNoEventMaskSet; &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们知道，射线检测的时候可以设置哪些 layer 可以接收检测碰撞。上面定义的 finalEventMask 就是需要检测的 layer，如果当前 raycaster 所在的对象有 Camera 组件，那么 finalEventMask 就是摄像机设置的渲染的所有层(<code>eventCamera.cullingMask &amp; m_EventMask</code>)，否则就是默认所有的层(<code>int kNoEventMaskSet = -1</code>)都可以接收射线碰撞检测。</p><p>然后对检测得到的 RaycastHit 数组按照 distance 由小到大排序。最后将这些射线检测结果依次拼装成 RaycastResult 并返回给 Event System，这里的 RaycastResult 中的 distance 就是 RaycastHit 的 distance(射线起点到射线碰撞点的距离)。</p><h1 id="Physics2D-Raycaster"><a href="#Physics2D-Raycaster" class="headerlink" title="Physics2D Raycaster"></a>Physics2D Raycaster</h1><p>Physics2DRaycaster 类继承自 PhysicsRaycaster，主要就是 <code>Raycast</code> 方法中的一点点细小的区别。</p><p>第一，在进行射线检测的时候，Physics2DRaycaster 中最后调用的是 Physics2D 的 <code>GetRayIntersectionAll</code> 方法。</p><p>第二处同 PhysicsRaycaster 的不同之处是在返回构造 RaycastResult 时，填充的部分值不一样，包括以下几个: </p><ul><li><p>distance，这个值是摄像机到射线检测碰撞点的距离，而在 PhysicsRaycaster 中是 RaycastHit 的 <code>distance</code> 值(射线起点在近裁剪面发出到碰撞点的距离)。</p></li><li><p>sortingLayer，这个值是当前对象 SpriteRenderer 组件中的 <code>sortingLayerID</code> 值，在 PhysicsRaycaster 为 0。</p></li><li><p>sortingOrder，这个同样为当前对象 SpriteRenderer 组件中的 <code>sortingOrder</code> 值，在 PhysicsRaycaster 为 0。</p></li></ul><h1 id="Graphic-Raycaster"><a href="#Graphic-Raycaster" class="headerlink" title="Graphic Raycaster"></a>Graphic Raycaster</h1><p>Graphic Raycaster 用于射线检测 Canvas 中的 Graphic 对象物体，通常绑定在 Canvas 所在的对象身上。</p><h2 id="属性或方法"><a href="#属性或方法" class="headerlink" title="属性或方法"></a>属性或方法</h2><p>GraphicRaycaster 类的成员属性很少，除了继承 BaseRaycaster 类的一些属性和方法外，它还拥有以下一些常用的属性或方法:</p><table><thead><tr><th>属性</th><th>描述</th></tr></thead><tbody><tr><td><code>Ignore Reversed Graphics</code></td><td>射线检测时是否忽略背向的 Graphics</td></tr><tr><td><code>Blocked Objects</code></td><td>哪些类型的对象会阻挡 Graphic raycasts</td></tr><tr><td><code>Blocking Mask</code></td><td>哪些 Layer 会阻挡 Graphic raycasts(对 <code>Blocked Objects</code> 指定的对象生效)</td></tr></tbody></table><p>不同于 PhysicsRaycaster 和 Physics2DRaycaster 类中直接使用父类的 <code>sortOrderPriority</code> 方法和 <code>renderOrderPriority</code>，GraphicRaycaster 覆写了这两个方法，并且当 Canvas 的 render mode 设置为 <code>RenderMode.ScreenSpaceOverlay</code> 时，上面两个方法分别返回 canvas 的 sortingOrder 以及 rootCanvas 的 renderOrder。</p><p>对于 eventCamera 的 get 方法，如果 Canvas 的 render mode 设置为 <code>RenderMode.ScreenSpaceOverlay</code> 或者 <code>enderMode.ScreenSpaceCamera</code> 并且 Canvas 的 worldCamera 未设置时，返回 null，否则返回 Canvas 的 worldCamera 或者 Main Camera。</p><h2 id="GraphicRaycaster-Raycast"><a href="#GraphicRaycaster-Raycast" class="headerlink" title="GraphicRaycaster.Raycast"></a>GraphicRaycaster.Raycast</h2><p>接下来就来到最重要的覆写的 <code>Raycast</code> 方法。</p><p>首先调用 <code>GraphicRegistry.GetGraphicsForCanvas</code> 方法获取当前 Canvas 下所有的 Graphic(canvasGraphics，这些 Graphics 在进行射线检测的时候会用到)。</p><p>紧接着就是 MultiDisplay 的一些检测，代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> displayIndex;</span><br><span class="line">var currentEventCamera = eventCamera;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (canvas.renderMode == RenderMode.ScreenSpaceOverlay || currentEventCamera == null)</span><br><span class="line">    displayIndex = canvas.targetDisplay;</span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">    displayIndex = currentEventCamera.targetDisplay;</span><br><span class="line"></span><br><span class="line">var eventPosition = Display.<span class="built_in">RelativeMouseAt</span>(eventData.position);</span><br><span class="line"><span class="keyword">if</span> (eventPosition != Vector<span class="number">3.</span>zero)</span><br><span class="line">&#123;</span><br><span class="line">    <span class="comment">// 当前平台支持 MultiDisplay</span></span><br><span class="line">    <span class="type">int</span> eventDisplayIndex = (<span class="type">int</span>)eventPosition.z;</span><br><span class="line">    <span class="keyword">if</span> (eventDisplayIndex != displayIndex)</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="comment">// 当前平台不支持 MultiDiplay</span></span><br><span class="line">    eventPosition = eventData.position;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看出，当平台支持 MultiDisplay 时，如果用户操作的不是当前的 Display，那么所有的其他 Display 上产生的事件都会被舍弃。</p><p>然后将屏幕坐标转换到 Camera 视窗坐标下。如果 eventCamera 不为空，则使用 <code>Camera.ScreenToViewportPoint</code> 方法转换坐标，否则直接使用当前 Display 的宽高除以 eventPosition 转换为视窗坐标([0,1]之间)。转换后的坐标若超出 Cmera 的范围(0 - 1)，则舍弃该事件。</p><h3 id="Blocked-Objects-和-Blocked-Mask-出场"><a href="#Blocked-Objects-和-Blocked-Mask-出场" class="headerlink" title="Blocked Objects 和 Blocked Mask 出场"></a>Blocked Objects 和 Blocked Mask 出场</h3><p>前面讲到 GraphicRaycaster 可以设置 Blocked Objects 和 Blocked Mask 来指定射线检测阻挡，下面一步就到了使用这两个属性来阻断射线检测部分。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (canvas.renderMode != RenderMode.ScreenSpaceOverlay &amp;&amp; blockingObjects != BlockingObjects.None)</span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">float</span> distanceToClipPlane</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 计算 distanceToClipPlane...</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (blockingObjects == BlockingObjects.ThreeD || blockingObjects == BlockingObjects.All)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">if</span> (ReflectionMethodsCache.Singleton.raycast3D != null)</span><br><span class="line">        &#123;</span><br><span class="line">            var hits = ReflectionMethodsCache.Singleton.<span class="built_in">raycast3DAll</span>(ray, distanceToClipPlane, (<span class="type">int</span>)m_BlockingMask);</span><br><span class="line">            <span class="keyword">if</span> (hits.Length &gt; <span class="number">0</span>)</span><br><span class="line">                hitDistance = hits[<span class="number">0</span>].distance;</span><br><span class="line">        &#125;    </span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//如果 blockingObjects 包含 BlockingObjects.TwoD，使用 ReflectionMethodsCache.Singleton.getRayIntersectionAll 方法再次计算 hitDistance</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>当 Canvas renderMode 不为 <code>RenderMode.ScreenSpaceOverlay</code> 并且设置了 blockingObjects，此时就会 Blocked Objects 和 Blocked Mask 就会生效。</p><ul><li><p>如果 blockingObjects 包含了 <code>BlockingObjects.ThreeD</code> 那么则会使用 <code>ReflectionMethodsCache.Singleton.raycast3DAll</code> 方法计算  hitDistance(PhysicsRaycaster 中也使用的该方法进行射线检测)。</p></li><li><p>如果 blockingObjects 也包含了 <code>BlockingObjects.TwoD</code>，那么会使用 <code>ReflectionMethodsCache.Singleton.getRayIntersectionAll</code> 方法(Physics2DRaycaster 射线检测使用)再计算 hitDistance。</p></li></ul><p>具体的计算过程大致是: 这上面的代码中 raycast3DAll 时指定了射线检测层 <code>m_BlockingMask</code>，这个参数就是自定义设定的 <code>Blocking Mask</code>，属于 block mask 的对象在这里就会就行射线检测，并得到最小的一个 hitDistance；<strong>后面对所有的 Graphics 进行射线检测时，如果检测结果 distance 大于 hitDistance，那么那个结果会被舍弃</strong>。如此一来，<code>Blocking Mask</code> 就起到了阻挡的作用，属于这个 layer 的所有对象的一旦被射线检测成功并得到 hitDistance，PhysicsRaycaster 最后的射线检测结果都只会包含这个 hitDistance 距离以内的对象。</p><h3 id="GraphicRaycaster-类重载了-“真”-Raycast-方法"><a href="#GraphicRaycaster-类重载了-“真”-Raycast-方法" class="headerlink" title="GraphicRaycaster 类重载了 “真”  Raycast 方法"></a>GraphicRaycaster 类重载了 “真”  Raycast 方法</h3><p>终于可以进行真真切切的 Graphic Raycast 了。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="type">static</span> <span class="type">void</span> <span class="title">Raycast</span><span class="params">(Canvas canvas, Camera eventCamera, Vector2 pointerPosition, IList&lt;Graphic&gt; foundGraphics, List&lt;Graphic&gt; results)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="type">int</span> totalCount = foundGraphics.Count;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; totalCount; ++i)</span><br><span class="line">    &#123;</span><br><span class="line">        Graphic graphic = foundGraphics[i];</span><br><span class="line"></span><br><span class="line">        <span class="comment">// depth 为 -1 说明没有被 canvas 处理(未被绘制)</span></span><br><span class="line">        <span class="comment">// raycastTarget 为 false 说明当前 graphic 不需要被射线检测</span></span><br><span class="line">        <span class="comment">// graphic.canvasRenderer.cull 为 true，忽略当前 graphic 的 CanvasRender 渲染的物体</span></span><br><span class="line">        <span class="keyword">if</span> (graphic.depth == <span class="number">-1</span> || !graphic.raycastTarget || graphic.canvasRenderer.cull)</span><br><span class="line">            <span class="keyword">continue</span>;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 从指定的 eventCamera 计算 pointerPosition 是否在 graphic 的 Rectangle 区域内 </span></span><br><span class="line">        <span class="keyword">if</span> (!RectTransformUtility.<span class="built_in">RectangleContainsScreenPoint</span>(graphic.rectTransform, pointerPosition, eventCamera))</span><br><span class="line">            <span class="keyword">continue</span>;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (graphic.<span class="built_in">Raycast</span>(pointerPosition, eventCamera))</span><br><span class="line">        &#123;</span><br><span class="line">            s_SortedGraphics.<span class="built_in">Add</span>(graphic);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    s_SortedGraphics.<span class="built_in">Sort</span>((g1, g2) =&gt; g<span class="number">2.</span>depth.<span class="built_in">CompareTo</span>(g<span class="number">1.</span>depth));</span><br><span class="line">    <span class="comment">// return result</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在循环中对每一个 Graphic 首先进行了初步的筛选，满足条件的 Graphic 才会调用其 <code>Raycast</code> 方法，这里的条件筛选包括 deth、raycastTarget 设置、位置信息是否满足等。</p><h3 id="Graphic-Raycast"><a href="#Graphic-Raycast" class="headerlink" title="Graphic.Raycast"></a>Graphic.Raycast</h3><p>对 Canvas 下所有的 graphic 遍历，满足条件则进行射线检测。Graphic 射线检测过程如下:</p><p>整个检测过程是在一个循环中实现的，从当前 Graphic 所在节点开始往祖先节点不断<strong>递归</strong>，直至向上再没有节点或者节点绑定的组件中有被射线检测出不合法而返回。</p><p>对于节点对象，首先获取其绑定的所有组件，依次<strong>遍历</strong>判断组件:</p><ul><li><p>若组件不是 <code>Canvas</code> 或者是 其 <code>Canvas</code> 但是其属性 overrideSorting 为 <code>false</code>，此时的检测过程如下: 判断组件是否是 <code>ICanvasRaycastFilter</code>，如不是则继续下一个组件判断；若是则调用 <code>ICanvasRaycastFilter</code> 的 <code>IsRaycastLocationValid</code> 方法判断事件发生位置相对这个节点对象是否是合法的，如果不合法直接跳出循环和遍历，<code>Raycast</code> 返回 <code>false</code>，表示用于检测的 Graphic 不需要接收此事件；若所有的组件检测都合法且 <code>IsRaycastLocationValid</code> 都则返回 <code>true</code>，则继续遍历下一个父节点对象。</p></li><li><p>上一步的检测过程中，当节点遍历完成还没有返回那么就 <code>Raycast</code> 方法返回 <code>true</code> 表示用于检测的 Graphic 可以作为事件接收对象。</p></li><li><p>另一种遍历完成的条件为当遍历到某个节点的某个组件，这个组件是 <code>Canvas</code> 并且其 overrideSorting 为 <code>true</code>，在这种情况下会将 <code>continueTraversal</code> 局部变量设置为 <code>false</code> 表示到这个节点遍历就可以完成了；当前这个节点的判断计算过程同第一步中相同: 在当前节点绑定的一系列实现了 <code>ICanvasRaycastFilter</code> 接口的组件上调用 <code>IsRaycastLocationValid</code> 方法判断事件发生位置相对这个 Graphic 是否是合法的，如果不合法 <code>Raycast</code> 方法直接返回 <code>false</code>，表示当前 Graphic 不需要接收此事件，否则所有的组件检测都合法返回 <code>true</code>，表示当前 Graphic 需要作为事件接收对象。</p></li><li><p>在上面对实现了 <code>ICanvasRaycastFilter</code> 接口的组件判断计算过程中，还会判断组件是否是 <code>CanvasGroup</code>。若是 <code>CanvasGroup</code> 且设置了 ignoreParentGroups 为 <code>false</code>，那么会调用 <code>IsRaycastLocationValid</code> 计算判断；若是 <code>CanvasGroup</code> 但是设置了 ignoreParentGroups 为 <code>true</code>，那么依旧会调用 <code>IsRaycastLocationValid</code> 计算判断一次，但是对接下来后面所有的 CanvasGroup 组件将不会调用 <code>IsRaycastLocationValid</code> 方法检测(忽略这些父 CanvasGrpup 的判断)；如果不是 <code>CanvasGroup</code>，直接调用 <code>IsRaycastLocationValid</code> 方法判断事件发生位置相对这个 Graphic 是否是合法的。</p></li></ul><p>从整个 Graphic.Raycast 检测过程可以看出，检测是自当前 graphic 所在节点开始，一旦检测到某个节点添加实现了 <code>ICanvasRaycastFilter</code> 接口且 <code>IsRaycastLocationValid</code> 方法返回 <code>false</code> 则此 graphic 检测失败并结束检测；否则还会继续向上递归检测父节点，当所有节点(绑定了 Canvas 组件并设置了 <code>Canvas.overrideSorting</code> 为 <code>true</code>的节点会截止此次检测)都射线检测成功，则此次 Graphic.Raycast 成功。</p><h3 id="Graphic-Raycast-成功的对象深度排序"><a href="#Graphic-Raycast-成功的对象深度排序" class="headerlink" title="Graphic.Raycast 成功的对象深度排序"></a>Graphic.Raycast 成功的对象深度排序</h3><p>对所有射线检测成功的 graphics 按照深度 depth 从大到小排序。</p><h3 id="Reversed-Graphics-过滤"><a href="#Reversed-Graphics-过滤" class="headerlink" title="Reversed Graphics 过滤"></a>Reversed Graphics 过滤</h3><p>最后对检测结果再过滤。如果设置了 <code>Ignore Reversed Graphics</code> 为 true，则将背向 Camera 的对象过滤掉，这里面又分为两种情况:</p><ul><li><p>Camera 为空，直接判断当前 Graphic 方向与正方向 <code>Vector3.forward</code> 是否相交，如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">var dir = go.transform.rotation * Vector<span class="number">3.f</span>orward;</span><br><span class="line">appendGraphic = Vector<span class="number">3.</span><span class="built_in">Dot</span>(Vector<span class="number">3.f</span>orward, dir) &gt; <span class="number">0</span>;</span><br></pre></td></tr></table></figure><p>首先将 <code>Vector3.forward</code> 绕着当前 Graphic 的 rotation 旋转得到 Graphic 的正方向，然后通过点积判断 Graphic 正方向是否与默认正方向(没有 Camera 所以默认正方向为 <code>Vector3.forward</code>)相交。点积大于 0 则相交，说明当前 Graphic 可以加入射线加测结果中。</p></li><li><p>当 Camera 不为空，就使用 Camera 的正方向与 Graphic 的正方向比较是否相交。</p></li></ul><h3 id="distance-检测是最终一道坎"><a href="#distance-检测是最终一道坎" class="headerlink" title="distance 检测是最终一道坎"></a>distance 检测是最终一道坎</h3><p><code>Ignore Reversed Graphics</code> 检测完，对结果进行 distance 计算:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">float</span> distance = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (currentEventCamera == null || canvas.renderMode == RenderMode.ScreenSpaceOverlay)</span><br><span class="line">    distance = <span class="number">0</span>;</span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">&#123;</span><br><span class="line">    Transform trans = go.transform;</span><br><span class="line">    Vector3 transForward = trans.forward;</span><br><span class="line"></span><br><span class="line">    distance = (Vector<span class="number">3.</span><span class="built_in">Dot</span>(transForward, trans.position - currentEventCamera.transform.position) / Vector<span class="number">3.</span><span class="built_in">Dot</span>(transForward, ray.direction));</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (distance &lt; <span class="number">0</span>)</span><br><span class="line">        <span class="keyword">continue</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Render Mode 为 <code>RenderMode.ScreenSpaceOverlay</code> 或者 Camera 为 null，distance 为 0；否则就计算 Graphic 和 Camera 之间的向量在 Graphic 正方向上的投影以及计算射线方向在 Graphic 正方向上的投影，两者相除就得到最终的 distance。</p><p>如果 distance 小于 hitDistance(设置的 Blocked Objects 和 Blocked Mask 产生)，则结果通过最终的测试可被用作事件的接收者之一。</p><h2 id="射线检测前后的一些操作"><a href="#射线检测前后的一些操作" class="headerlink" title="射线检测前后的一些操作"></a>射线检测前后的一些操作</h2><p>首先来看看这些 Raycaster 被唤起的部分，也就是最开始的流程图中的第三步。Input Module 中使用 Raycaster 处理射线检测，真正的 Raycaster 实施代码又回到了 EventSystem 类中的 <code>RaycastAll</code> 方法，具体代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="type">void</span> <span class="title">RaycastAll</span><span class="params">(PointerEventData eventData, List&lt;RaycastResult&gt; raycastResults)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    raycastResults.<span class="built_in">Clear</span>();</span><br><span class="line">    var modules = RaycasterManager.<span class="built_in">GetRaycasters</span>();</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; modules.Count; ++i)</span><br><span class="line">    &#123;</span><br><span class="line">        var <span class="keyword">module</span> = modules[i];</span><br><span class="line">        <span class="keyword">if</span> (<span class="keyword">module</span> == null || !<span class="keyword">module</span>.<span class="built_in">IsActive</span>())</span><br><span class="line">            <span class="keyword">continue</span>;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">module</span>.<span class="built_in">Raycast</span>(eventData, raycastResults);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    raycastResults.<span class="built_in">Sort</span>(s_RaycastComparer);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>场景中可以存在一个或多个 Raycaster。当存在多个时，如果需要发起射线检测，那么每个处于 Active 状态的 Raycaster 都会工作，所有 Raycaster 检测得到的结果都会存放在 <code>raycastResults</code> 中(这些 RaycastResult 都是在各自射线检测器中根据 distance 从小到大排过序的)。方法最后使用自定义 Comparer 对所有的 RaycastResult 排序。<code>s_RaycastComparer</code> 有以下几种比较流程:</p><ul><li>两个 RaycastResult 检测所在的 Raycaster 不同</li></ul><p>首先比较两个对象的 Camera 的 depth。在渲染中，Camera depth 越小会越先渲染，越大越往后渲染，因此对于射线检测来说，Camera 的 depth 越大，它对应的物体应该先于 Camera depth 小的物体进行射线检测，检测得到的结果也应排在前面。代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (lhsEventCamera.depth &lt; rhsEventCamera.depth)</span><br><span class="line">    <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line"><span class="keyword">if</span> (lhsEventCamera.depth == rhsEventCamera.depth)</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> <span class="number">-1</span>;</span><br></pre></td></tr></table></figure><p>当 Camera depth 相等的时候，使用 <code>sortOrderPriority</code> 进行比较。优先级数值越大，越先被射线检测选中，所以这里的 <code>CompareTo</code> 方法使用的是右边的参数去比较左边的参数，最终的结果就是按照从大到小(降序)的顺序排列。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">return</span> rhs.<span class="keyword">module</span>.sortOrderPriority.<span class="built_in">CompareTo</span>(lhs.<span class="keyword">module</span>.sortOrderPriority);</span><br></pre></td></tr></table></figure><p>在 PhysicsRaycaster 和 Physics2DRaycaster 类中没有覆写 <code>sortOrderPriority</code> 方法，因此都返回基类的 <code>int.MinValue</code>；但在 GraphicRaycaster 类中覆写了此方法，当对应的 Canvas 的 renderMode 设置为 <code>RenderMode.ScreenSpaceOverlay</code> 时，此时的 <code>sortOrderPriority</code> 返回 Canvas 的 sortingOrder(Sort Order越大越在上层)，否则同样也是返回基类设置的 <code>int.MinValue</code>，这是因为在 <code>RenderMode.ScreenSpaceOverlay</code> 模式下，所有的 distance 都将是 0。</p><p>当 sortOrderPriority 相同，再使用 renderOrderPriority 比较。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">return</span> rhs.<span class="keyword">module</span>.renderOrderPriority.<span class="built_in">CompareTo</span>(lhs.<span class="keyword">module</span>.renderOrderPriority);</span><br></pre></td></tr></table></figure><p>renderOrderPriority 和 sortOrderPriority 类似，仅在 GraphicRaycaster 类中被覆写，也只有在 Canvas 的 renderMode 设置为 <code>RenderMode.ScreenSpaceOverlay</code> 时才返回 <code>canvas.rootCanvas.renderOrder</code>，这是因为 Canvas 在其他几种 renderMode 下，渲染的先后顺序都和距离摄像机的距离有关。所以 renderOrderPriority 比较也是按照从大到小的顺序得到最终的结果。</p><ul><li>同属于一个 Raycaster 检测得到，但是它们的 sortingLayer 不一样</li></ul><p>对于 PhysicsRaycaster 检测得到的对象，sortingLayer 都为 0。</p><p>对于 Physics2DRaycaster 检测得到的对象，如果对象上挂载有 SpriteRenderer 组件，那么 sortingLayer 对应的 sortingLayerID，否则也为 0。</p><p>对于 GraphicRaycaster 检测所得，sortingLayer 就是所在 Canvas 的 sortingLayerID。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">var rid = SortingLayer.<span class="built_in">GetLayerValueFromID</span>(rhs.sortingLayer);</span><br><span class="line">var lid = SortingLayer.<span class="built_in">GetLayerValueFromID</span>(lhs.sortingLayer);</span><br><span class="line"><span class="keyword">return</span> rid.<span class="built_in">CompareTo</span>(lid);</span><br></pre></td></tr></table></figure><p>通过 <code>SortingLayer.GetLayerValueFromID</code> 方法计算 sortingLayer 最终的 sorting layer 值，同样是按照降序排列，因此计算得到的 sorting layer 值越大越先排在前面。</p><ul><li>sortingLayer 也相同，使用 sortingOrder 比较</li></ul><p>sortingOrder 和 sortingLayer 类似，PhysicsRaycaster 检测得到的对象 sortingOrder 为 0；Physics2DRaycaster 检测得到的对象是 SpriteRenderer 中的 sortingOrder；GraphicRaycaster 检测所得是所在 Canvas 的 sortingOrder。最终 sortingOrder 越大的对象越排前面。代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">return</span> rhs.sortingOrder.<span class="built_in">CompareTo</span>(lhs.sortingOrder);</span><br></pre></td></tr></table></figure><ul><li>sortingOrder 相同，使用 depth 比较</li></ul><p>PhysicsRaycaster 和 Physics2DRaycaster 中 depth 都被设置为了 0；GraphicRaycaster 检测所得的对象的 depth 就是继承自 Graphic 类的对象所在的 Graphic 的 depth，即 Canvas 下所有 Graphic 深度遍历的顺序。比较同样也是按照降序进行的，因此越嵌套在靠近 Canvas 的对象越排在前面。</p><ul><li>depth 相同，使用 distance 比较</li></ul><p>PhysicsRaycaster 中的 distance 就是 RaycastHit 的 distance(射线起点到射线碰撞点的距离)。</p><p>Physics2DRaycaster 类中返回的是 Camera 的位置和射线碰撞点之间的距离。</p><p>GraphicRaycaster 类中 distance <a href="http://geomalgorithms.com/a06-_intersect-2.html">计算</a>如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">var go = m_RaycastResults[index].gameObject;</span><br><span class="line">Transform trans = go.transform;</span><br><span class="line">Vector3 transForward = trans.forward;</span><br><span class="line"></span><br><span class="line"><span class="comment">// TODO why user DOT to caculate distance?</span></span><br><span class="line">distance = Vector<span class="number">3.</span><span class="built_in">Dot</span>(transForward, trans.position - currentEventCamera.transform.position) / Vector<span class="number">3.</span><span class="built_in">Dot</span>(transForward, ray.direction);</span><br></pre></td></tr></table></figure><p>距离 distance 越小越靠前。</p><ul><li>最后如果上述情况都不能满足，使用 index 比较。先被射线检测到的对象排在前面。</li></ul><p>Raycaster 后段部分的流程: 取排过序的 RaycastResult 中第一个结果作为响应事件的输入事件的 pointerCurrentRaycast，根据它来在 Messaging System 中分发事件，大致代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 获取 Raycast 结果中对应的 GameObject</span></span><br><span class="line">var currentOverGo = pointerEvent.pointerCurrentRaycast.gameObject;</span><br><span class="line"><span class="comment">// 分发事件</span></span><br><span class="line">ExecuteEvents.<span class="built_in">ExecuteHierarchy</span>(currentOverGo, pointerEvent, ExecuteEvents.pointerDownHandler);</span><br></pre></td></tr></table></figure><p>Raycaster 在 Event System 中的作用和流程基本就是上述的内容。</p>]]>
    </content>
    <id>https://lujun.pages.dev/2019/07/20/unity_event_system_raycasters/</id>
    <link href="https://lujun.pages.dev/2019/07/20/unity_event_system_raycasters/"/>
    <published>2019-07-20T20:02:29.000Z</published>
    <summary>
      <![CDATA[<p>Raycasters 用来检测当前事件发送给哪个对象，检测原理就是 Raycast。当给定一个屏幕坐标系中的位置，Raycasters 就会利用射线检测寻找潜在的对象，并返回一个离当前屏幕最近的对象。</p>
<p>在 Unity Raycasters 中有三种类型的 Raycasters:</p>
<ul>
<li><p>Graphic Raycaster - 存在于 Canvas 下，用于检测 Canvas 中所有的物体</p>
</li>
<li><p>Physics 2D Raycaster - 用于检测 2D 物体</p>
</li>
<li><p>Physics Raycaster - 用于检测 3D 物体</p>
</li>
</ul>
<p>接下来，就来分析一下各个类型 Raycaster 的源码来看看其的工作流程。</p>]]>
    </summary>
    <title>Unity Raycasters 剖析</title>
    <updated>2026-06-22T14:03:58.804Z</updated>
  </entry>
  <entry>
    <author>
      <name>lujun</name>
    </author>
    <category term="Unity" scheme="https://lujun.pages.dev/tags/Unity/"/>
    <content>
      <![CDATA[<script src="/assets/js/APlayer.min.js"> </script><p>当我们把 Unity 脚本绑定到对象上，游戏运行时对象上的脚本会被执行。</p><p>在项目开发中，我们可能会遇到过这种问题: 如果对象 A 脚本中使用了对象 B 的某个脚本的实例，但是 A 在 B 还没有初始化时就调用了 B 脚本实例中的方法，这样就会出现异常。所以脚本的执行顺序得控制好。</p><span id="more"></span><p>首先，有如下场景。<strong>在场景中由上到下，依次添加对象；随后对象中的脚本依次绑定</strong>(此处的脚本都是 Unity 脚本):</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line">场景 Scene</span><br><span class="line">├── GameObject0</span><br><span class="line">│   └── Script4.cs</span><br><span class="line">│       ├── Awake()</span><br><span class="line">│       ├── OnEnable()</span><br><span class="line">│       ├── Start()</span><br><span class="line">│       ├── Update()</span><br><span class="line">│       ├── OnDisable()</span><br><span class="line">│       └── OnDestroy()</span><br><span class="line">├── GameObject1</span><br><span class="line">│   └── Script3.cs</span><br><span class="line">│       ├── Awake()</span><br><span class="line">│       ├── OnEnable()</span><br><span class="line">│       ├── Start()</span><br><span class="line">│       ├── Update()</span><br><span class="line">│       ├── OnDisable()</span><br><span class="line">│       └── OnDestroy()</span><br><span class="line">└── GameObject2</span><br><span class="line">    ├── Script0.cs</span><br><span class="line">    │   ├── Awake()</span><br><span class="line">    │   ├── OnEnable()</span><br><span class="line">    │   ├── Start()</span><br><span class="line">    │   ├── Update()</span><br><span class="line">    │   ├── OnDisable()</span><br><span class="line">    │   └── OnDestroy()</span><br><span class="line">    └── Script1.cs</span><br><span class="line">        ├── Awake()</span><br><span class="line">        ├── OnEnable()</span><br><span class="line">        ├── Start()</span><br><span class="line">        ├── Update()</span><br><span class="line">        ├── OnDisable()</span><br><span class="line">        └── OnDestroy()</span><br></pre></td></tr></table></figure><p>记住添加脚本的顺序，<strong>也是由上到下为游戏对象依次添加的</strong>。运行后结果是:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">GameObject2.Script1.Awake &gt; GameObject2.Script1.OnEnable &gt; </span><br><span class="line">GameObject2.Script0.Awake &gt; GameObject2.Script0.OnEnable &gt; </span><br><span class="line">GameObject1.Script3.Awake &gt; GameObject1.Script3.OnEnable &gt; </span><br><span class="line">GameObject0.Script4.Awake &gt; GameObject0.Script4.OnEnable &gt; </span><br><span class="line">GameObject2.Script1.Start &gt; GameObject2.Script0.Start &gt; </span><br><span class="line">GameObject1.Script3.Start &gt; GameObject0.Script4.Start &gt;</span><br><span class="line">GameObject2.Script1.Update &gt; GameObject2.Script0.Update &gt;</span><br><span class="line">GameObject1.Script3.Update &gt; GameObject0.Script4.Update &gt;</span><br><span class="line">... &gt;</span><br><span class="line">GameObject1.Script3.OnDisable &gt; GameObject1.Script3.OnDestroy &gt;</span><br><span class="line">GameObject2.Script0.OnDisable &gt; GameObject2.Script1.OnDisable &gt;</span><br><span class="line">GameObject2.Script0.OnDestroy &gt; GameObject2.Script1.OnDestroy &gt;</span><br><span class="line">GameObject0.Script4.OnDisable &gt; GameObject0.Script4.OnDestroy</span><br></pre></td></tr></table></figure><p>多次运行或者切换场景再运行，会发现结果还有些不一样。下面就依次看看导致不同结果出现的原因。</p><h2 id="首先是第一个问题-多次运行或切换场景回来在运行，GameObject2、GameObject1-和-GameObject0-执行顺序可能会有先后。"><a href="#首先是第一个问题-多次运行或切换场景回来在运行，GameObject2、GameObject1-和-GameObject0-执行顺序可能会有先后。" class="headerlink" title="首先是第一个问题 - 多次运行或切换场景回来在运行，GameObject2、GameObject1 和 GameObject0 执行顺序可能会有先后。"></a>首先是第一个问题 - 多次运行或切换场景回来在运行，GameObject2、GameObject1 和 GameObject0 执行顺序可能会有先后。</h2><p>首先看看第一部分的问题，不同脚本的执行先后顺序会出现不同的结果。先来看看 Unity <a href="https://docs.unity3d.com/Manual/class-MonoManager.html">文档</a>对控制脚本执行顺序得解决办法:</p><blockquote><p>Scripts can be added to the <strong>inspector</strong> using the Plus “+” button and dragged to change their relative order. Note that it is possible to drag a script either above or below the <strong>Default Time</strong> bar; those above will execute ahead of the default time while those below will execute after. The ordering of scripts in the dialog from top to bottom determines their execution order. <strong>All scripts not in the dialog execute in the default time slot in arbitrary order</strong>.</p></blockquote><p>Unity 提供了一种脚本执行顺序的解决方案，在 <code>Edit &gt; Project Settings &gt; Script Execution Order</code> 中我们可以自定义脚本的执行顺序。设置框中间的 <code>default time</code> 区域，这是脚本默认(脚本未设置 Execution Order)的执行区间。在设置框中，自上到下脚本依次按顺序执行。文档中最后说到，<strong>未设置 Execution Order 的脚本会在 default time 的时间间隙中随机执行</strong>。</p><p>上面的设置，会修改脚本的 meta 文件中 <code>executionOrder</code> 的值，类似 <code>executionOrder: -50</code>，这个值越小该脚本越先执行。</p><p>上面最后一句就是我们遇到的情况。因为在测试中，我们所有的脚本都未设置 Execution Order，难道这些脚本就一直是随机执行的吗？再次多次进行测试，结果发现不同对象上的脚本执行顺序后面就会以固定的先后顺序去执行了。此刻就想，Unity 总会有东西记住了这个顺序，然后才能以这个固定的顺序去执行吧!</p><p>我们知道，场景文件其实就是一个 YAML(配置文件的一种格式)，其中记录了整个场景的配置信息。那么会不会在这个文件中有记录顺序了？当向场景中添加一个对象的时候，会增加以下配置信息:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_script_execution_order_1.jpeg" alt="unity_script_execution_order_1.jpeg" title="unity_script_execution_order_1.jpeg" width="43%" height="43%" /></center><p>第一行 <code>--- !u!1 &amp;1609942489</code> 后面的数字对应了新增加的对象的 fileID，<code>m_Component</code> 里面就是该对象身上拥有的组件，可以通过 fileID 找到对应的组件信息。</p><p>场景的配置文件中，我们测试的三个对象的配置如下:</p><table><thead><tr><th align="left">m_Name</th><th align="left">fileID</th><th align="center">在配置文件中的行数</th></tr></thead><tbody><tr><td align="left">GameObject0</td><td align="left">927923936</td><td align="center">253</td></tr><tr><td align="left">GameObject1</td><td align="left">1609942489</td><td align="center">296</td></tr><tr><td align="left">GameObject2</td><td align="left">384326570</td><td align="center">116</td></tr></tbody></table><p>向场景添加这三个对象的时候是依次按顺序添加的，但每次添加对象对应的配置信息在场景配置文件中的位置是随机的，且生成的 fileID 也是随机且唯一的。<strong>而处于 default time(未自定义脚本执行顺序)的脚本的随机执行顺序和这个 fileID 就有关系</strong>。</p><ul><li><p>当第一次编辑场景的 Session 一直未被销毁时(切换场景或重启 Unity)，脚本执行的顺序和挂载脚本的顺序有关。对于不同对象上的不同脚本，后挂载脚本对象上的脚本会先执行(且等该场景中所有脚本某个方法均执行完后才会轮到下一个方法)。所以上面表格中那几个对象上挂载的脚本执行顺序是: GameObject2 &gt; GameObject1 &gt; GameObject0。</p></li><li><p><strong>但是当这个编辑场景的 Session 被更新(切换场景或重启 Unity)，脚本的执行顺序开始以一种固定的顺序执行</strong>。fileID 越小的对象，其上挂载的脚本越先执行。所以上面表格中那几个对象上挂载的脚本执行顺序是: GameObject2 &gt; GameObject0 &gt; GameObject1。</p></li></ul><p>为了验证是 fileID 影响的执行结果，我们尝试将 GameObject1 以及其组件的 fileID 都改的比 GameObject0 要小但是比 GameObject2 对象的要大。</p><p>按照上述方法修改之后的执行顺序结果是: GameObject2 &gt; GameObject1 &gt; GameObject0。</p><p>这就证明了 fileID 就是影响不同对象上脚本(这些脚本都处于 default time 执行)执行顺序的一个点，fileID 越小的对象，其上挂载的脚本越先执行。Unity 文档中说这类脚本是随机执行的，是因为当你向场景中添加一个对象时，它的 fileID 是随机生成的(大小也随机)而且有可能被更新，所以就有了 ‘All scripts not in the dialog execute in the default time slot in arbitrary order’。</p><p>看完了不同对象的 fileID 是如何影响不同的脚本执行顺序的，接下来再来看看同一个对象上的不同脚本执行顺序。</p><p>为对象挂载一个脚本的时候，它所在场景的配置信息中也会有一些改变。首先它的 <code>m_Component</code> 这个配置下会增加一个新的 component 信息，类似 <code>- component: {fileID: 384326573}</code>，然后场景配置中也会增加对应的这个 fileID 的 component 配置信息。如果是脚本，component 信息中会有一个 <code>m_Script</code> 属性，类似 <code>m_Script: {fileID: 11500000, guid: a4969ee1e69314f91be663e63e413a51, type: 3}</code>，这里面的 guid 对应的就是脚本的 meta 文件的 guid，最终就能找到需要加载脚本的。</p><p><strong>有一点需要注意的是，对象绑定的脚本的 fileID 是根据对象本身的 fileID 开始往下递增，如果 fileID 存在则跳过继续查找下一个可用的值作为 fileID。新增的 component 放在对象 <code>m_Component</code> 中最后的位置</strong>。</p><p>比如下面在 GameObject2 对象上新增了几个脚本，此时它的 <code>m_Component</code> 有以下这些值:</p><table><thead><tr><th align="left">fileID</th><th align="left">Script Type</th><th align="left">Script</th><th align="center">索引位置</th></tr></thead><tbody><tr><td align="left">384326571</td><td align="left">Transform</td><td align="left"></td><td align="center">0</td></tr><tr><td align="left">384326572</td><td align="left">MonoBehaviour</td><td align="left">Script0.cs</td><td align="center">1</td></tr><tr><td align="left">384326573</td><td align="left">MonoBehaviour</td><td align="left">Script1.cs</td><td align="center">2</td></tr><tr><td align="left">384326574</td><td align="left">MonoBehaviour</td><td align="left">Script2.cs</td><td align="center">3</td></tr></tbody></table><ul><li>对同一对象上的不同脚本，其最终执行顺序也是由那个脚本所在对象配置中的 fileID 大小决定的，fileID 越小的脚本越先被执行。表格里的索引位置反映其在 Unity 编辑器中的位置，索引越大越靠下显示。</li></ul><p>所以上面表格中脚本执行顺序是: Script0.cs &gt; Script1.cs &gt; Script2.cs。</p><p>现在修改 Script0.cs 的 fileID 位于 Script2.cs 的 fileID 后一位，修改后运行发现执行顺序也变成了 Script1.cs &gt; Script2.cs &gt; Script0.cs。</p><h2 id="接下来就是第二个问题-某些杂乱无章的回调。"><a href="#接下来就是第二个问题-某些杂乱无章的回调。" class="headerlink" title="接下来就是第二个问题 - 某些杂乱无章的回调。"></a>接下来就是第二个问题 - 某些杂乱无章的回调。</h2><p>Unity 事件回调方法中，有时候看起来很有规律，而某些方法却又显得杂乱无章。不过这些事件回调都遵循 <a href="https://docs.unity3d.com/Manual/ExecutionOrder.html">Unity Script Lifecycle</a>，大致有以下几点:</p><ul><li><p>场景中所有脚本某个方法均执行完后才会开始执行这些脚本的下一阶段的方法。</p></li><li><p>生命周期中的特定方法都是一起调用的，比如全部 <code>Start</code> 执行完后才会开始执行下一阶段的方法。但是 <code>OnEnable</code> 方法不会等待 <code>Awake</code> 方法全部执行完后再执行，而是执行完一个脚本上的 <code>Awake</code> 方法就会紧接着执行这个脚本上的 <code>OnEnable</code>，它们可以看成是一体的。</p></li><li><p>另外有区别之处的是 <code>OnDisable</code> 和 <code>OnDestroy</code> 方法，它们执行的顺序是: </p><ul><li>不同对象上的不同脚本，执行没有先后规律，而且是成对执行；</li><li>同一个对象中的不同脚本上，这两个方法是按照脚本在 <code>m_Component</code> 中的索引位置按顺序执行的，且不是成对执行。索引自小到大对应的脚本中 <code>OnDisable</code> 方法依次执行，这个对象中所有挂载的脚本的 <code>OnDisable</code> 方法都执行完后，在按照执行 <code>OnDisable</code> 方法的顺序依次执行 <code>OnDestroy</code> 方法。</li></ul></li></ul><p>这两个问题都弄清除怎么回事了，文章开始我们遇到的需求就有了解决思路。</p><ul><li><p>如果一个对象上的脚本使用了未初始化脚本中的某个方法导致出现异常，首先查看这些脚本是否设置了 Execution Order 导致顺序有问题；</p></li><li><p>如果都未设置 Execution Order，查看这些脚本所在的配置文件中的 fileID，通常 fileID 越小会越先被执行。</p></li></ul><p>但是，这样有时还是不能解决问题。比如对象 A 上的脚本在 <code>Awake</code> 方法调用了对象 B 上的脚本中的单例对象，且对象 B 上的脚本后挂载(这样这个脚本就会先初始化)，在这种情况下，如果 B 上的那个脚本单例也是在 <code>Awake</code> 方法初始化，没有任何问题，但是如果延迟到 <code>Start</code> 方法才初始化，同样会出现异常情况，所以最好是使用以下方案解决:</p><ul><li><strong>在 <code>Awake</code> 方法中初始化，但是其他调用方在 <code>Start</code> 方法再调用保证足够安全(由 Unity 事件机制保证)</strong>。</li></ul>]]>
    </content>
    <id>https://lujun.pages.dev/2019/07/09/unity_script_execution_order/</id>
    <link href="https://lujun.pages.dev/2019/07/09/unity_script_execution_order/"/>
    <published>2019-07-09T10:12:29.000Z</published>
    <summary>
      <![CDATA[<p>当我们把 Unity 脚本绑定到对象上，游戏运行时对象上的脚本会被执行。</p>
<p>在项目开发中，我们可能会遇到过这种问题: 如果对象 A 脚本中使用了对象 B 的某个脚本的实例，但是 A 在 B 还没有初始化时就调用了 B 脚本实例中的方法，这样就会出现异常。所以脚本的执行顺序得控制好。</p>]]>
    </summary>
    <title>挖一挖 Unity 中脚本的执行顺序</title>
    <updated>2026-06-22T14:03:58.804Z</updated>
  </entry>
  <entry>
    <author>
      <name>lujun</name>
    </author>
    <category term="Unity" scheme="https://lujun.pages.dev/tags/Unity/"/>
    <category term="Shader" scheme="https://lujun.pages.dev/tags/Shader/"/>
    <content>
      <![CDATA[<script src="/assets/js/APlayer.min.js"> </script><p>在渲染的世界中，为图形增加更多的细节，有很多种方式。比如，可以为每个顶点添加颜色来达到多彩的效果，也可以使用纹理来添加细节。当使用顶点携带额外的颜色属性来增强细节时，为了达到更逼真的效果就必须增加顶点的数量，无疑会带来渲染性能上的开销。所以，大多数时候都会选择使用纹理贴图(纹理映射)的方式来增强细节。</p><p>对于一张纹理，除了普通存储颜色值之外，它还可以用来存储高度值(用于凹凸映射)或者法线值(用于法线映射)。</p><span id="more"></span><h1 id="纹理映射"><a href="#纹理映射" class="headerlink" title="纹理映射"></a>纹理映射</h1><p>使用纹理映射技术可以达到多彩的效果。在 Unity 中，使用纹理坐标对纹理进行采样，纹理坐标范围在 [0,1] 之间，坐标系与 OpenGL 一致(原点在左下角，X 轴朝右，Y 轴朝上)。在前面的<a href="https://blog.lujun.co/2019/06/26/mesh_rendering/">文章</a>中说到过，网格顶点上通常包含了一组或多组可用的纹理坐标，我们可以使用这些纹理坐标对纹理进行采样。</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/texture_in_unity_rendering_1.png" alt="texture_in_unity_rendering_1.png" title="texture_in_unity_rendering_1.png" width="50%" height="50%" /></center><h2 id="纹理设置-Wrap-Mode"><a href="#纹理设置-Wrap-Mode" class="headerlink" title="纹理设置 - Wrap Mode"></a>纹理设置 - Wrap Mode</h2><p>在 Unity 中，Wrap Mode 主要就是规定在纹理坐标超过 [0,1] 纹理该如何采样。</p><blockquote><p>Wrap mode determines how texture is sampled when texture coordinates are outside of the typical 0..1 range.</p></blockquote><p>Texture 有多种 Wrap Mode，不同的模式在不同的参数下采样情况会有些不同，这些参数主要体现在材质中对 Texture 的设置，如 <code>Tiling</code> 和 <code>Offset</code>，在<a href="https://blog.lujun.co/2019/06/26/mesh_rendering/">《一次简单的网格渲染》</a>一文中提到过，这两个设置参数会改变网格的纹理坐标(默认 Shader 中使用了 <code>TRANSFORM_TEX</code>)，主要作用是缩放和位移。</p><p>下面就来简单看看各种 Wrap Mode 在不同 <code>Tiling</code> 和 <code>Offset</code> 出现的差异(Texture Type 默认为 <code>Default</code>，Filter Mode 默认为 <code>Bilinear</code>，默认对 U 方向进行了设置，V 同理)。</p><ul><li>当 Wrap Mode 为 <code>Repeat</code> 或 <code>Clamp</code>，且材质中 Texture 的设置为 <code>Tiling(1,1)</code>、<code>Offset(0,0)</code>，这时纹理会完全贴在模型上，一般情况下这都是我们需要的结果。</li></ul><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/texture_in_unity_rendering_3.png" alt="texture_in_unity_rendering_3.png" title="texture_in_unity_rendering_3.png" width="50%" height="50%" /></center><ul><li>接下来，将 Wrap Mode 设置为 <code>Repeat</code>，且材质中 Texture 的设置修改为 <code>Tiling(2,1)</code>、<code>Offset(0,0)</code>。</li></ul><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/texture_in_unity_rendering_4.png" alt="texture_in_unity_rendering_4.png" title="texture_in_unity_rendering_4.png" width="50%" height="50%" /></center><p>我们发现，纹理在 X 轴上就行了平铺。当 Wrap Mode 设置为 <code>Repeat</code> 模式下，Unity 会根据 <code>Tiling</code> 设置对网格纹理坐标进行缩放(Shader 中使用 <code>TRANSFORM_TEX</code> 宏实现)。</p><p>看一下修改网格顶点纹理坐标的 Shader 代码(<code>TRANSFORM_TEX</code> 宏的实现):</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">o.uv = v.uv * _MainTex_ST.xy + _MainTex_ST.zw;</span><br></pre></td></tr></table></figure><p>其中 <code> _MainTex_ST.xy</code> 就是 <code>Tiling.xy</code>，通过与原始顶点上的纹理坐标相乘就就行了缩放操作，在上面的 <code>Tiling</code> 为 (2,1) 的设置下，最终纹理坐标 U 的范围就是 [0，2]。在片元着色器中纹理采样时，由于设置了 <code>Repeat</code> 模式，所以会重复采样。 <code>Repeat</code> 模式下的纹理本身坐标大概如下图:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/texture_in_unity_rendering_2.png" alt="texture_in_unity_rendering_2.png" title="texture_in_unity_rendering_2.png" width="50%" height="50%" /></center><ul><li>现在，保持上面的 <code>Tiling</code> 和 <code>Offset</code> 的设置，将 Wrap Mode 设置为 <code>Clamp</code>，会得到如下的渲染效果。</li></ul><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/texture_in_unity_rendering_5.png" alt="texture_in_unity_rendering_5.png" title="texture_in_unity_rendering_5.png" width="50%" height="50%" /></center><p><code>Clamp</code> 模式将纹理坐标限定在了 [0,1] 之间，当采样坐标小于 0 会返回 0 坐标处对应的颜色值，超过 1 时会返回 1 坐标处对应的颜色值。我们上面的渲染过程中，采样纹理坐标 U 的值在 [0,2] 之间，所以对 U 值在 [1,2] 内的采样值都是返回坐标 1 处的颜色。</p><ul><li>现在再将 Wrap Mode 设置为 <code>Mirror</code>，<code>Tiling</code> 为 (4,1) 以及 <code>Offset</code> 设置为 (-2,0)。</li></ul><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/texture_in_unity_rendering_6.png" alt="texture_in_unity_rendering_6.png" title="texture_in_unity_rendering_6.png" width="50%" height="50%" /></center><p>看模式名字也能知道，设置为 <code>Mirror</code> 纹理采样时就是镜像处理。为了更好的看镜像之后的结果，我们对 U 轴进行了 4 倍缩放，并向左移动了 2，此时的采样坐标范围就是 [-2,2]。可以看出， <code>Mirror</code> 和 <code>Repeat</code> 有些类似，但是不同的是 <code>Mirror</code> 重复采样时是对纹理进行翻转再采样。</p><ul><li>看完了 <code>Mirror</code>，再看看与之类似的 <code>Mirror Once</code>。将 Wrap Mode 设置为 <code>Mirror Once</code>，其它设置不变，看看渲染效果。</li></ul><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/texture_in_unity_rendering_7.png" alt="texture_in_unity_rendering_7.png" title="texture_in_unity_rendering_7.png" width="50%" height="50%" /></center><p>在 [-1,1] 之间，同样进行了镜像采样操作。但是超过这个区间，采样就和 <code>Clamp</code> 模式一样了，超过 1 返回的是 1 坐标处的颜色值，小于 -1 返回 -1 坐标处的颜色值。<code>Mirror Once</code> 可以看作 <code>Mirror</code> 和 <code>Clamp</code> 相结合。</p><ul><li>最后看看 <code>Per-axis</code> 这种模式。选择这种模式时，会让你设置 <code>U axix</code> 和 <code>V axis</code> 两个轴上的 Wrap Mode，这个模式就是让你能够区分 U 和 V 设置单独的 Wrap Mode。</li></ul><h2 id="纹理设置-Filter-Mode"><a href="#纹理设置-Filter-Mode" class="headerlink" title="纹理设置 - Filter Mode"></a>纹理设置 - Filter Mode</h2><p>讲这个属性设置之前，先说说我遇到的一个问题。</p><p>有一个平面模型，它由多个三角形网格组成。如下图。</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/texture_in_unity_rendering_8.png" alt="texture_in_unity_rendering_8.png" title="texture_in_unity_rendering_8.png" width="50%" height="50%" /></center><p>这个平面模型有点特别，特别的是它每个网格顶点携带的 UV 坐标，可以看到除了最后一列顶点的 U 为 1 其余列的全部为 0。V 也类似。再看看将纹理渲染出的效果。</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/texture_in_unity_rendering_11.png" alt="texture_in_unity_rendering_11.png" title="texture_in_unity_rendering_11.png" width="90%" height="90%" /></center><p>在 Wrap Mode 设置为 <code>Clamp</code>、<code>Mirror</code>、<code>Mirror Once</code> 时，看上去渲染得到的图像很正常，但当设置为 <code>Repeat</code> 模式时，问题来了，那些莫名的颜色是哪来的?带着这个问题，再来看看 Filter Mode。</p><p>在 Unity 中，Filter Mode 用于设置纹理被拉伸变换时，纹理将如何过滤插值。可以设置为 <code>Point(no filter)</code>、<code>Bilinear</code> 和 <code>Trilinear</code> 几种模式。</p><ul><li>Point(no filter) 模式</li></ul><blockquote><p>Point filtering - texture pixels become blocky up close.</p></blockquote><p>使用这种模式时，纹理采样得到的颜色值是纹理坐标所对应的那一个纹理像素对应的颜色值。如同上面文档所说，纹理将会变得像素(颗粒状)。这种模式对应了 OpenGL 中的 <code>GL_NEAREST</code>。</p><p>渲染效果如下图:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/texture_in_unity_rendering_13.png" alt="texture_in_unity_rendering_13.png" title="texture_in_unity_rendering_13.png" width="50%" height="50%" /></center><p>可以看出，在颜色交界处有较多的颗粒状颜色。有像素化的风格。</p><ul><li>Bilinear (双线性插值)模式</li></ul><blockquote><p>Bilinear filtering - texture samples are averaged.</p></blockquote><p>双线性插值，根据相邻的四个像素进行插值得到最终的颜色值。双线性插值的具体思想是在两个方向上进行插值。简单来说就是根据四个已知点，首先在 X 轴方向插值两次，对得到的两个插值结果再在 Y 轴方向上进行一次插值。</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/texture_in_unity_rendering_16.png" alt="texture_in_unity_rendering_16.png" title="texture_in_unity_rendering_16.png" width="50%" height="50%" /></center><p>双线性插值简单流程: 已知 A1、A2、A3、A4 四个点(对应四个纹理像素的中心)的纹理坐标分别为 (x1,y1)、(x2,y1)、(x1,y2) 和 (x2,y2)，四个点颜色值分别为 f(A1)、f(A2)、f(A3) 和 f(A4)，现在要使用双线性插值计算得到点 F 处(x,y)的颜色值 f(F)。</p><p>首先在 X 轴方向上通过 A1、A2 对点 R2 进行插值得到 R2 处的颜色值。R2 处的纹理坐标为 (x,y1)。</p><div class="math-display">\[\frac{f(R2)-f(A1)}{x-x1}&#x3D;\frac{f(A2)-f(A1)}{x2-x1}\]</div><div class="math-display">\[f(R2)&#x3D;\frac{(f(A2)-f(A1))\times(x-x1)}{x2-x1}+f(A1)\]</div><div class="math-display">\[f(R2)&#x3D;\frac{x-x1}{x2-x1}\times f(A2)+\frac{x2-x}{x2-x1}\times f(A1)\]</div><p>根据上面的公式可以看出，x 越靠近 x1，得到的值越接近 f(A1)，也就是颜色值越接近 A1 处的颜色值。</p><p>同理再在 X 轴方向上使用 A3、A4 对点 R1 进行插值得到 R1 处的颜色值。</p><div class="math-display">\[f(R1)&#x3D;\frac{(f(A4)-f(A3))\times(x-x1)}{x2-x1}+f(A3)\]</div><div class="math-display">\[f(R1)&#x3D;\frac{x-x1}{x2-x1} \times f(A4)+\frac{x2-x}{x2-x1} \times f(A3)\]</div><p>然后再使用 R1 和 R2 对点 F 在 Y 轴上插值。</p><div class="math-display">\[\frac{f(F)-f(R1)}{y-y2}&#x3D;\frac{f(R2)-f(R1)}{y1-y2}\]</div><div class="math-display">\[f(F)&#x3D;\frac{f(R2)-f(R1)}{y1-y2}\times(y-y2)+f(R1)\]</div><div class="math-display">\[f(F)&#x3D;\frac{y-y2}{y1-y2} \times f(R2)+\frac{y1-y}{y1-y2} \times f(R1)\]</div><p>根据 Y 轴插值公式可以看出最终 F 点出的颜色值，y 越靠近 y1，值越靠近 f(R2)，即颜色越靠近 R2 处的颜色。所以纹理坐标越靠近某个纹理像素中心点，该纹理像素对最终颜色值的贡献越大。这种模式对应了 OpenGL 中的 <code>GL_LINEAR</code>。</p><p>Bilinear 渲染效果如下图:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/texture_in_unity_rendering_14.png" alt="texture_in_unity_rendering_14.png" title="texture_in_unity_rendering_14.png" width="50%" height="50%" /></center><p>渲染途中对颜色交界处处理的比较平滑。看上去像是被模糊处理了。</p><ul><li>Trilinear (三线性滤波插值)模式</li></ul><blockquote><p>Trilinear filtering - texture samples are averaged and also blended between mipmap levels.</p></blockquote><p>三线性滤波插值，类似 Bilinear 三线性滤波插值。它还会在双线性滤波插值的基础上使用 Mipmap(多级渐远纹理)对颜色值进行混合。</p><p>现在再回到前面我们遇到的那个问题。当 Wrap Mode 设置为 <code>Repeat</code>，Filter Mode 设置为 <code>Bilinear</code>，且纹理渲染在了一组拥有特别纹理坐标的网格上。现在就看看问题是如何产生的吧!</p><p>我们知道，Wrap Mode 设置为 <code>Repeat</code> 时，纹理采样是采样平铺的模式，相当于使用同一张纹理无限的拼接成了一张大纹理，而采样就根据具体的纹理坐标采样。而此时当 Filter Mode 设置为 <code>Bilinear</code>,会根据<strong>纹理坐标所映射的纹理像素以及其周边三个纹理像素进行滤波插值</strong>。正是这个原因: 当在边界处进行纹理采样时，由于需要四个纹理像素进行滤波插值，所以 0 坐标处的颜色值就可能会和其左边纹理像素混合采样插值，而其左边的纹理像素又是当前纹理的纹理坐标为 1 处的颜色，我们使用的纹理图片中最左边和最右边纹理像素对应的颜色值是不一样的，所以经过双线性滤波插值就出现了混合色的效果。同理上下也有可能出现这样的问题。</p><p>那么如何解决了? 我们知道，Filter Mode 如果使用 <code>Point</code> 模式，则只会采样 [0,1] 内的纹理像素颜色值，超过 [0,1] 则返回边界处纹理像素的颜色，因此就不会出现混合色效果。</p><p>所以，将 Filter Mode 设置为 <code>Point</code>。修改后的渲染图如下:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/texture_in_unity_rendering_12.png" alt="texture_in_unity_rendering_12.png" title="texture_in_unity_rendering_12.png" width="50%" height="50%" /></center><p>可以看到，那些因 <code>Bilinear</code> 滤波插值得到的混合色被坐标 0 处的颜色给替代了。</p><h1 id="凹凸映射"><a href="#凹凸映射" class="headerlink" title="凹凸映射"></a>凹凸映射</h1><p>普通纹理映射可以达到增强表面效果的作用。但是对于想要渲染出一种类似凹凸的效果，纹理映射就无法满足需求。我们可以使用高度纹理或法线纹理实现凹凸映射。</p><h2 id="高度纹理"><a href="#高度纹理" class="headerlink" title="高度纹理"></a>高度纹理</h2><p>凹凸映射使用高度纹理(图)(纹理贴图存储的是颜色值，高度图存储的是强度值)，高度图中的强度值表示模型的局部海拔高度。凹凸映射的主要原理是通过计算高度图中相邻像素的高度差值来改变表面法向量的值，从而影响光照计算的结果，就能模拟出凹凸的效果。通常的计算方式如下:</p><p>计算每个纹理像素上 X 和 Y 的倾斜度:</p><div class="math-display">\[xGradient &#x3D; pixel(x-1, y) - pixel(x+1, y)\]</div><div class="math-display">\[yGradient &#x3D; pixel(x, y-1) - pixel(x, y+1)\]</div><p>根据计算得到的倾斜度，在顶点的<a href="https://blog.lujun.co/2019/04/04/unity_shader_tangent_space_rotation/">切线空间</a>中对法线进行干扰偏移(切线方向使用 yGradient 偏移，副切线方向使用 xGradient 偏移)。</p><div class="math-display">\[newNormal &#x3D; originNormal + tangent \times yGradient + bitangent \times xGradient\]</div><p>最后使用新得到的法线向量计算光照。高度图通常也会和法线映射一起使用，用于给出表面凹凸的额外信息。</p><p>使用高度图计算简单过程如下:</p><ul><li>首先，在顶点着色器中计算邻域采样坐标。</li></ul><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">sampler2D _BumpMap;</span><br><span class="line">sampler2D _Bump;</span><br><span class="line">half4 _Bump_TexelSize;</span><br><span class="line"></span><br><span class="line">o.uv[<span class="number">0</span>] = v.texcoord;</span><br><span class="line">o.uv[<span class="number">1</span>] = v.texcoord - <span class="built_in">float2</span>(_Bump_TexelSize.x, <span class="number">0.0</span>);</span><br><span class="line">o.uv[<span class="number">2</span>] = v.texcoord + <span class="built_in">float2</span>(_Bump_TexelSize.x, <span class="number">0.0</span>);</span><br><span class="line">o.uv[<span class="number">3</span>] = v.texcoord - <span class="built_in">float2</span>(<span class="number">0.0</span>, _Bump_TexelSize.y);</span><br><span class="line">o.uv[<span class="number">4</span>] = v.texcoord + <span class="built_in">float2</span>(<span class="number">0.0</span>, _Bump_TexelSize.y);</span><br></pre></td></tr></table></figure><ul><li>其次，在片元着色器中分别计算倾斜度，根据倾斜度计算干扰后的法线。最后根据法线向量计算光照。</li></ul><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">fixed3 xGradient = <span class="built_in">tex2D</span>(_Bump, i.uv[<span class="number">1</span>]).rgb - <span class="built_in">tex2D</span>(_Bump, i.uv[<span class="number">2</span>]).rgb;</span><br><span class="line">fixed3 yGradient = <span class="built_in">tex2D</span>(_Bump, i.uv[<span class="number">3</span>]).rgb - <span class="built_in">tex2D</span>(_Bump, i.uv[<span class="number">4</span>]).rgb;</span><br><span class="line"></span><br><span class="line">fixed3 normal = <span class="built_in">UnpackNormal</span>(<span class="built_in">tex2D</span>(_BumpMap, i.uv[<span class="number">0</span>]));</span><br><span class="line">normal = normal + i.tangent * yGradient + i.bitangent * xGradient;</span><br><span class="line"></span><br><span class="line"><span class="comment">// use normal ...</span></span><br></pre></td></tr></table></figure><h2 id="法线纹理"><a href="#法线纹理" class="headerlink" title="法线纹理"></a>法线纹理</h2><p>法线贴图也是一种特定的凹凸贴图的方法。在法线纹理中，我们将法线信息直接存储到了纹理中。像素值范围通常为 [0,1]，而法线向量分量范围为 [-1,1]，所以通常使用法线纹理的时候需要做一个映射得到像素值对应的真正的法线向量。</p><div class="math-display">\[normal &#x3D; pixel \times 2 - 1\]</div><p>法线到像素的映射为:</p><div class="math-display">\[piexl &#x3D; (normal + 1) \times 2\]</div><p>在 Unity 中使用法线纹理，导入的时候应当将纹理 Texture Type 设置为 <code>Normal map</code>，然后在 Shader 中使用 <code>UnpackNormal</code> 即可得到映射过后的正确的法线向量。当然你也可以自行在 Shader 中进行映射:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">fixed4 normal = <span class="built_in">tex2D</span>(_Bump, i.uv);</span><br><span class="line">normal.xyz = norm.xyz * <span class="number">2</span> - <span class="number">1</span>;</span><br></pre></td></tr></table></figure><p>但是，使用 <code>UnpackNormal</code> 以及设置 Texture Type 为 <code>Normal map</code> 可以免去不同平台所需要进行的处理(比如压缩处理)。</p><p>表面法线在法线纹理中的法线向量可以属于不同的坐标空间中。比如最直观的，将模型空间中每个顶点的表面法线直接存储于一张纹理中，即模型空间中的法线纹理。但是在不同的模型使用模型空间中的法线纹理时，就有可能导致意想不到的效果，所以复用性很差。通常情况下，我们会使用<a href="https://blog.lujun.co/2019/04/04/unity_shader_tangent_space_rotation/">切线空间</a>下的法线纹理。这样把该纹理应用到不同的模型上，也可以得到一个合理的效果。</p><p>Shader 使用切线空间的法线纹理计算光照过程大致如下: </p><ul><li>首先，在顶点着色器中计算切线空间到世界空间的转换矩阵。</li></ul><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">sampler2D _BumpMap;</span><br><span class="line"></span><br><span class="line"><span class="function">v2f <span class="title">vert</span><span class="params">(a2v v)</span> </span>&#123;</span><br><span class="line">    v2f o;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">    float3 worldPos = <span class="built_in">mul</span>(unity_ObjectToWorld, v.vertex).xyz;  </span><br><span class="line">    fixed3 worldNormal = <span class="built_in">UnityObjectToWorldNormal</span>(v.normal);  </span><br><span class="line">    fixed3 worldTangent = <span class="built_in">UnityObjectToWorldDir</span>(v.tangent.xyz);  </span><br><span class="line">    fixed3 worldBinormal = <span class="built_in">cross</span>(worldNormal, worldTangent) * v.tangent.w; </span><br><span class="line"></span><br><span class="line">    o.TtoW0 = <span class="built_in">float4</span>(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);</span><br><span class="line">    o.TtoW1 = <span class="built_in">float4</span>(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);</span><br><span class="line">    o.TtoW2 = <span class="built_in">float4</span>(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);  </span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>其次，在片元着色器中采样法线纹理，映射后得到切线空间下的法线。</li></ul><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">fixed4 <span class="title">frag</span><span class="params">(v2f i)</span> : SV_Target &#123;</span></span><br><span class="line">    fixed3 normal = <span class="built_in">UnpackNormal</span>(<span class="built_in">tex2D</span>(_BumpMap, i.uv.zw));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>最后，将法线向量由切线空间转换到世界空间，再使用法线向量参与光照计算即可。</li></ul><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">normal = <span class="built_in">half3</span>(<span class="built_in">dot</span>(i.TtoW<span class="number">0.</span>xyz, normal), <span class="built_in">dot</span>(i.TtoW<span class="number">1.</span>xyz, normal), <span class="built_in">dot</span>(i.TtoW<span class="number">2.</span>xyz, normal));</span><br><span class="line"><span class="comment">// use normal ...</span></span><br></pre></td></tr></table></figure><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ul><li><a href="https://www.cnblogs.com/xpvincent/archive/2013/03/15/2961448.html">https://www.cnblogs.com/xpvincent/archive/2013/03/15/2961448.html</a></li><li><a href="https://blog.csdn.net/candycat1992/article/details/41605257">https://blog.csdn.net/candycat1992/article/details/41605257</a></li></ul>]]>
    </content>
    <id>https://lujun.pages.dev/2019/06/29/texture_in_unity_rendering/</id>
    <link href="https://lujun.pages.dev/2019/06/29/texture_in_unity_rendering/"/>
    <published>2019-06-29T10:12:29.000Z</published>
    <summary>
      <![CDATA[<p>在渲染的世界中，为图形增加更多的细节，有很多种方式。比如，可以为每个顶点添加颜色来达到多彩的效果，也可以使用纹理来添加细节。当使用顶点携带额外的颜色属性来增强细节时，为了达到更逼真的效果就必须增加顶点的数量，无疑会带来渲染性能上的开销。所以，大多数时候都会选择使用纹理贴图(纹理映射)的方式来增强细节。</p>
<p>对于一张纹理，除了普通存储颜色值之外，它还可以用来存储高度值(用于凹凸映射)或者法线值(用于法线映射)。</p>]]>
    </summary>
    <title>Unity 渲染过程中的纹理</title>
    <updated>2026-06-22T14:03:58.804Z</updated>
  </entry>
  <entry>
    <author>
      <name>lujun</name>
    </author>
    <category term="Unity" scheme="https://lujun.pages.dev/tags/Unity/"/>
    <content>
      <![CDATA[<script src="/assets/js/APlayer.min.js"> </script><blockquote><p>在 3D 计算机图形学中，可采用各种建模技术方案。如考察某一球体对象，可使用球体方程 $(x - Cx)^2 + (y - Cy)^2 + (z - Cz)^2 &#x3D; r^2$ 这种基于隐式函数 $f(x,y,z) &#x3D; 0$ 的方式来表示隐式表面，也可根据拓扑实体(如顶点)采用显式方式表达球体对象，比如常用的多边形网格。</p></blockquote><p><em>《计算机图形学-基于3D图形开发技术》</em></p><p>对于建模人员来说使用图形软件即可创建基于多边形网格的 3D 对象，并且 GPU 对多边形网格进行了优化处理。因此在大多数时候，我们都是用多边形网格表现 3D 对象。</p><p>下面我们就来看看网格是如何从构建成一个 3D 对象，到最终渲染成一副多彩的二维图像的。</p><span id="more"></span><h1 id="从-Unity-中的-Mesh-说起"><a href="#从-Unity-中的-Mesh-说起" class="headerlink" title="从 Unity 中的 Mesh 说起"></a>从 Unity 中的 <code>Mesh</code> 说起</h1><p>网格能表现 3D 对象，那么构成网格又需要哪些信息了? Unity 中，<code>Mesh</code> 类用于创建或者修改网格，它包含了许多的成员变量:</p><ul><li><p><code>vertices</code> 保存了顶点信息</p></li><li><p><code>triangles</code> 所有的三角形顶点的索引(三个数据构成一个三角形)</p></li><li><p><code>normals</code> 顶点的法线信息</p></li><li><p><code>tangents</code> 顶点的切线信息</p></li><li><p><code>uv - uv8</code> 8 组纹理坐标</p></li><li><p>…</p></li></ul><p>这里只列举了常用的一些需要的数据，还有更多的变量，读者可自行查阅<a href="https://docs.unity3d.com/ScriptReference/Mesh.html">文档</a>。</p><p>在这里，我们先创建一个 <code>Mesh</code>。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Mesh _mesh = <span class="keyword">new</span> <span class="built_in">Mesh</span>();</span><br></pre></td></tr></table></figure><h1 id="MeshFilter-和-MeshRender"><a href="#MeshFilter-和-MeshRender" class="headerlink" title="MeshFilter 和 MeshRender"></a>MeshFilter 和 MeshRender</h1><p>在 Unity 中，创建的 <code>Mesh</code> 要能够表现在你面前需要 <code>MeshFilter</code> 类的帮助。 <code>MeshFilter</code> 很简单，主要就是一个中间层来让你在程序上动态使用 <code>Mesh</code>。它有两个成员变量，<code>mesh</code> 和 <code>sharedMesh</code>，<code>mesh</code> 就是这个类实例化自身独有的一个 <code>Mesh</code>，而 <code>sharedMesh</code> 顾名思义就是共享的一个 <code>Mesh</code> 实例，如果对这个共享的实例修改，那么可能会影响到其它也共享使用这个 <code>Mesh</code> 实例的 <code>MeshFilter</code>。<code>MeshFilter</code> 将网格传递给 <code>MeshRender</code> 渲染。</p><p>除了 <code>MeshFilter</code>，渲染网格还需要一个类 <code>MeshRender</code>，它主要是负责渲染相关的设置。包括很多属性：</p><ul><li><p><code>materials</code> 渲染所有的材质(可以多个)</p></li><li><p><code>receiveShadows</code> 是否接受阴影</p></li><li><p><code>shadowCastingMode</code> 阴影投射模式</p></li><li><p>…</p></li></ul><p>诸如上面还有很多其它属性，其中最重要的就是 <code>materials</code>，如果不设置好渲染材质，那么 GPU 将不知道将要如何渲染你的网格。</p><p>接下来我们开始进行这次简单的网格渲染吧!</p><p>创建一个 GameObject，并给它添加上 <code>MeshFilter</code> 和 <code>MeshRender</code>，<code>MeshRender</code> 需要设置好材质，并将我们最初创建的 <code>Mesh</code> 交给 <code>MeshFilter</code>:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">GetComponent</span>&lt;MeshFilter&gt;().mesh = _mesh;</span><br></pre></td></tr></table></figure><p>接下来再回到我们的 <code>Mesh</code>，这里具体说说 <code>vertices</code>、<code>triangles</code> 和 <code>uv</code>。</p><h2 id="vertices"><a href="#vertices" class="headerlink" title="vertices"></a>vertices</h2><p><code>vertices</code> 是顶点数据(通常我们所说模型的顶点们就是它)存放的地方，也称为顶点缓冲区。通常不同的多边形网格连接在一起可能会共享一些顶点数据，如果采用每个多边形单独存储其顶点数据的方式，那么顶点缓冲区就会包含冗余数据。因此通常都会有一个独立索引缓冲区(<code>triangles</code>)来索引每个多边形网格的顶点，从而使得顶点缓冲区更加紧凑(不存在重复顶点数据)。</p><p><strong>这里的顶点缓冲区定义，也可以将 <code>normals</code>、<code>tangents</code> 和 <code>uv - uv8</code> 等包含进来，因为他们也是属于顶点的一部分，索引缓冲区同样对这些数据有效。</strong></p><p><code>vertices</code> 中的顶点数据决定了多边形网格可能会处的位置。接下来给 <code>Mesh</code> 的顶点缓冲区赋予一些顶点数据:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// mesh size</span></span><br><span class="line"><span class="type">int</span> _xSize = <span class="number">6</span>, _ySize = <span class="number">3</span>;</span><br><span class="line"><span class="comment">// mesh vertices size</span></span><br><span class="line">Vector3[] _vector3s = <span class="keyword">new</span> Vector3[_xSize * _ySize];</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> (<span class="type">int</span> j = <span class="number">0</span>; j &lt; _ySize; j++)</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; _xSize; i++)</span><br><span class="line">    &#123;</span><br><span class="line">        _vector3s[j * _xSize + i] = <span class="keyword">new</span> <span class="built_in">Vector3</span>(i, j, <span class="number">0</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// assign to vertex buffer</span></span><br><span class="line">_mesh.vertices = _vector3s;</span><br></pre></td></tr></table></figure><p>通过上面的代码，我们大致得到了空间中这样一些点:</p><div class="math-display">\[\left\{  \begin{matrix}   (0,2,0) &amp; (1,2,0) &amp; (2,2,0) &amp; (3,2,0) &amp; (4,2,0) &amp; (5,2,0) \\   (0,1,0) &amp; (1,1,0) &amp; (2,1,0) &amp; (3,1,0) &amp; (4,1,0) &amp; (5,1,0) \\   (0,0,0) &amp; (1,0,0) &amp; (2,0,0) &amp; (3,0,0) &amp; (4,0,0) &amp; (5,0,0)  \end{matrix} \right\}\]</div><p><code>vertices</code> 中存有上面这些顶点数据，依次从左到右，从下至上，索引如下:</p><div class="math-display">\[\left\{  \begin{matrix}   12 &amp; 13 &amp; 14 &amp; 15 &amp; 16 &amp; 17 \\   6 &amp; 7 &amp; 8 &amp; 9 &amp; 10 &amp; 11 \\   0 &amp; 1 &amp; 2 &amp; 3 &amp; 4 &amp; 5  \end{matrix} \right\}\]</div><p>点可以连接起来组成多边形网格或线段。OpenGL 支持具有任意数量顶点的多边形，Direct3D 仅支持三角形网格，本文中也使用三角形网格。</p><h2 id="triangles"><a href="#triangles" class="headerlink" title="triangles"></a>triangles</h2><p><code>triangles</code> (索引缓冲区)包含了三角形网格的顶点数据的索引(三个数据代表一个三角形)。三个索引组成一个三角形，顺序可能有顺时针(CW)或逆时针(CCW)两种情况。</p><p>首先我们尝试使用逆时针顺序(CCW)索引来组成一个三角形:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span>[] triangles = <span class="keyword">new</span> <span class="type">int</span>[<span class="number">3</span>];</span><br><span class="line">triangles[<span class="number">0</span>] = <span class="number">0</span>;</span><br><span class="line">triangles[<span class="number">1</span>] = <span class="number">1</span>;</span><br><span class="line">triangles[<span class="number">2</span>] = <span class="number">6</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// assign to indices buffer</span></span><br><span class="line">_mesh.triangles = triangles;</span><br></pre></td></tr></table></figure><p>这种情况下运行看不到渲染出来的三角形面。为什么了?</p><p>在 Unity 中使用左手坐标系(LHS，X朝右Y朝上Z朝里)，因此如果索引是逆时针构成三角形网格，那么根据左手定则三角面法线(法线方向指定正面)朝里(Z 轴正方向，背向摄像机)，在 Shader 默认 <code>Cull Back</code> 情况下(背面将被剔除不渲染)，摄像机拍摄的三角面(背面)将不会被渲染，因此看不到三角面。</p><p>在 Scene 编辑器中把场景转到 Z 轴正方向可以看到渲染出来的三角形网格(因为这一面才是正面)。解决上面问题有两个办法:</p><ul><li><p>将索引修改为顺时针构成三角形网格，根据左手定则三角面法线将朝外(Z轴负方向)，正面将朝向摄像机，因此在 Shader 默认 <code>Cull Back</code> 情况下就能看到渲染的三角形。</p></li><li><p>将 Shader <code>Cull Back</code> 改成剔除正面 <code>Cull Front</code> 或者不剔除 <code>Cull Off</code> 即可。</p></li></ul><p>我们使用顺时针索引顺序来作修改:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">triangles[<span class="number">0</span>] = <span class="number">0</span>;</span><br><span class="line">triangles[<span class="number">1</span>] = <span class="number">6</span>;</span><br><span class="line">triangles[<span class="number">2</span>] = <span class="number">1</span>;</span><br><span class="line"></span><br><span class="line">_mesh.triangles = triangles;</span><br></pre></td></tr></table></figure><p>现在能看到渲染出的三角形，如下图:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/mesh_rendering_2.jpeg" alt="mesh_rendering_2.jpeg" title="mesh_rendering_2.jpeg" width="50%" height="50%" /></center><p>把之前输入的顶点的全部按照顺时针顺序索引渲染三角形，用三角形网格组成一个平面，代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">triangles = <span class="keyword">new</span> <span class="type">int</span>[_xSize * _ySize * <span class="number">6</span>];</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>, m = <span class="number">0</span>; i &lt; _ySize - <span class="number">1</span>; i++)</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> j = <span class="number">0</span>; j &lt; _xSize - <span class="number">1</span>; j++, m += <span class="number">6</span>)</span><br><span class="line">    &#123;</span><br><span class="line">        triangles[m] = i * _xSize + j;</span><br><span class="line">        triangles[m + <span class="number">1</span>] = triangles[m] + _xSize;</span><br><span class="line">        triangles[m + <span class="number">2</span>] = i * _xSize + j + <span class="number">1</span>;</span><br><span class="line">        triangles[m + <span class="number">3</span>] = triangles[m + <span class="number">1</span>];</span><br><span class="line">        triangles[m + <span class="number">4</span>] = triangles[m + <span class="number">1</span>] + <span class="number">1</span>;</span><br><span class="line">        triangles[m + <span class="number">5</span>] = triangles[m + <span class="number">2</span>];</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">_mesh.triangles = triangles;</span><br></pre></td></tr></table></figure><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/mesh_rendering_3.jpeg" alt="mesh_rendering_3.jpeg" title="mesh_rendering_3.jpeg" width="50%" height="50%" /></center><p>上面我们定义了 <code>triangles</code> 的大小为 <code>_xSize * _ySize * 6</code>，为什么是这么多了。想象一下三角形组成的平面中，一个顶点最多可能被 6 个三角形共享，因此这个顶点会被索引 6 次，我们的代码中顶点的数量是 <code>_xSize * _ySize</code>，因此索引缓冲区的大小就是 <code>_xSize * _ySize * 6</code>。</p><h2 id="uv"><a href="#uv" class="headerlink" title="uv"></a>uv</h2><p><code>uv</code>(第一组纹理坐标)包含了模型顶点上面对应的纹理采样坐标。它的大小应该是同 <code>vertices</code> 一样大，从另一个角度来看，纹理坐标其实也是属于顶点数据的一部分，所以也可以看做是属于顶点缓冲区的。</p><p>当需要将一个 Texture 贴到我们的模型(网格组成)表面，就应当给模型每个顶点指定纹理采样坐标。纹理的 UV 值通常限定在了 [0,1] 之间，因此顶点的 <code>uv</code> 也应当属于 [0,1]，不然纹理就可能采样错误(纹理不同的设置采样结果也不一样)。</p><p>下面为 <code>Mesh</code> 设置上第一组纹理坐标:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">Vector2[] uvs = <span class="keyword">new</span> Vector2[_vector3s.Length];</span><br><span class="line"><span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; _ySize; i++)</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> j = <span class="number">0</span>; j &lt; _xSize; j++)</span><br><span class="line">    &#123;</span><br><span class="line">        uvs[i * _xSize + j] = <span class="keyword">new</span> <span class="built_in">Vector2</span>((<span class="type">float</span>)j / (_xSize - <span class="number">1</span>), (<span class="type">float</span>)i / (_ySize - <span class="number">1</span>));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// assign to uv</span></span><br><span class="line">_mesh.uv = uvs;</span><br></pre></td></tr></table></figure><p>纹理坐标设置了，最后就需要对理进行采样。为了弄清除纹理坐标是如何被应用到采样过程中的，我们通过 Unity 创建一个无光照 Shader 并将其用到渲染网格的材质上，看看其采样纹理部分代码:</p><figure class="highlight glsl"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ...</span></span><br><span class="line">struct appdata</span><br><span class="line">&#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">    float2 uv : TEXCOORD0;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">struct v2f</span><br><span class="line">&#123;</span><br><span class="line">    float2 uv : TEXCOORD0;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="type">sampler2D</span> _MainTex;</span><br><span class="line">float4 _MainTex_ST;</span><br><span class="line"></span><br><span class="line">v2f vert (appdata v)</span><br><span class="line">&#123;</span><br><span class="line">    v2f o;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">    o.uv = TRANSFORM_TEX(v.uv, _MainTex);</span><br><span class="line">    <span class="keyword">return</span> o;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">fixed4 frag (v2f i) : SV_Target</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">return</span> tex2D(_MainTex, i.uv);</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// ...</span></span><br></pre></td></tr></table></figure><p>顶点着色器 <code>vert</code> 中，需要从顶点数据中读取顶点的模型坐标(语义 <code>POSITION</code>)和第一组纹理坐标(语义 <code>TEXCOORD0</code>)，这里读取的第一组纹理坐标也就是在程序中赋予 <code>Mesh</code> 的 <code>uv</code>。这里有个有意思的事情，如果只给 <code>Mesh</code> 的 <code>uv</code> 赋予了值，而 <code>uv2 - uv8</code> 没有赋值，那么在 Shader 中如果我读取第二组(<code>TEXCOORD1</code>)或其它组纹理坐标(<code>TEXCOORDx</code>)同样能得到正确的采样结果，说明 Unity 给 <code>uv2 - uv8</code> 赋予了 <code>uv</code> 的数据。</p><table><thead><tr><th align="center">索引</th><th align="center">0</th><th align="center">1</th><th align="center">2</th><th align="center">3</th><th align="center">4</th><th align="center">5</th><th align="center">6</th><th align="center">7</th></tr></thead><tbody><tr><td align="center">Mesh</td><td align="center">uv</td><td align="center">uv2</td><td align="center">uv3</td><td align="center">uv4</td><td align="center">uv5</td><td align="center">uv6</td><td align="center">uv7</td><td align="center">uv8</td></tr><tr><td align="center">Set</td><td align="center">x</td><td align="center">x</td><td align="center">x</td><td align="center">4 组</td><td align="center">x</td><td align="center">x</td><td align="center">7 组</td><td align="center">x</td></tr><tr><td align="center">Shader</td><td align="center">TEX0</td><td align="center">TEX1</td><td align="center">TEX2</td><td align="center">TEX3</td><td align="center">TEX4</td><td align="center">TEX5</td><td align="center">TEX6</td><td align="center">TEX7</td></tr><tr><td align="center">Get</td><td align="center">0</td><td align="center">0</td><td align="center">0</td><td align="center">4 组</td><td align="center">4 组</td><td align="center">4 组</td><td align="center">7 组</td><td align="center">7 组</td></tr></tbody></table><p><em>表格中 Set 栏的 <code>x</code> 表示未给设置纹理坐标；<code>4 组</code> 表示为当前 <code>Mesh</code> 的 <code>uv4</code> 设置上了自定义的第四组纹理坐标；Shader 栏中的 <code>TEX0</code> 是语义 <code>TEXCOORD0</code> 的缩写；Get 栏中的 <code>0</code> 表示 Shader 中获取的纹理坐标是 0，<code>4 组</code> 表示 <code>TEXCOORD4</code> 读取的是程序中自定义的第四组纹理坐标</em></p><p>所以当设置某组 <code>uv</code> 时，若其后面的 <code>uv(x)</code> 没有设置，那么后边的将会被设置一份和 <code>uv</code> 一样的纹理坐标值，直至遇到某个 <code>uv(x)</code> 又被设置了值为止，后面没有设置纹理坐标的又会拥有刚刚设置的这份新的值。都不设置为默认值 0。 </p><p>再回到 Shader 中，在顶点着色器中通过 Unity 内置宏 <code>TRANSFORM_TEX</code> 对顶点输入的纹理坐标进行的缩放和平移，<code>TRANSFORM_TEX</code> 实现大致如下:</p><figure class="highlight glsl"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">o.uv = v.uv * _MainTex_ST.xy + _MainTex_ST.zw;</span><br></pre></td></tr></table></figure><p>其中 <code>_MainTex_ST</code> 里面的四个值就对应了材质中对 Texture 设置的 <code>Tiling</code> 和 <code>Offset</code>。</p><p>最后纹理采样是在片元着色器中进行的，使用变换过的纹理坐标对目标纹理进行采样并将颜色返回:</p><figure class="highlight glsl"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">fixed4 color = tex2D(_MainTex, i.uv);</span><br></pre></td></tr></table></figure><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/mesh_rendering_4.jpeg" alt="mesh_rendering_4.jpeg" title="mesh_rendering_4.jpeg" width="50%" height="50%" /></center><p>到这里，从创建网格到渲染呈现基本完成。当然真实的过程复杂的多，比如复杂网格通常是由美术建模得到的，并将顶点、三角形网格、纹理坐标甚至法线等数据事先预制在模型中。GPU 渲染通常也是复杂的工程，Shader 编写可能涉及到顶点变换、光照计算、透明度混合等等许多事项。并且本文主要是对网格渲染做一个大致的讲解。</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ul><li>《计算机图形学-基于3D图形开发技术》</li><li><a href="http://catlikecoding.com/unity/tutorials/procedural-grid/">http://catlikecoding.com/unity/tutorials/procedural-grid/</a></li></ul>]]>
    </content>
    <id>https://lujun.pages.dev/2019/06/26/mesh_rendering/</id>
    <link href="https://lujun.pages.dev/2019/06/26/mesh_rendering/"/>
    <published>2019-06-26T00:12:29.000Z</published>
    <summary>
      <![CDATA[<blockquote>
<p>在 3D 计算机图形学中，可采用各种建模技术方案。如考察某一球体对象，可使用球体方程 $(x - Cx)^2 + (y - Cy)^2 + (z - Cz)^2 &#x3D; r^2$ 这种基于隐式函数 $f(x,y,z) &#x3D; 0$ 的方式来表示隐式表面，也可根据拓扑实体(如顶点)采用显式方式表达球体对象，比如常用的多边形网格。</p>
</blockquote>
<p><em>《计算机图形学-基于3D图形开发技术》</em></p>
<p>对于建模人员来说使用图形软件即可创建基于多边形网格的 3D 对象，并且 GPU 对多边形网格进行了优化处理。因此在大多数时候，我们都是用多边形网格表现 3D 对象。</p>
<p>下面我们就来看看网格是如何从构建成一个 3D 对象，到最终渲染成一副多彩的二维图像的。</p>]]>
    </summary>
    <title>一次简单的网格渲染</title>
    <updated>2026-06-22T14:03:58.804Z</updated>
  </entry>
  <entry>
    <author>
      <name>lujun</name>
    </author>
    <category term="设计模式" scheme="https://lujun.pages.dev/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"/>
    <category term="Unity" scheme="https://lujun.pages.dev/tags/Unity/"/>
    <content>
      <![CDATA[<script src="/assets/js/APlayer.min.js"> </script><p><code>前进</code>、<code>后退</code>、<code>跳跃</code>等诸多的动作让角色在游戏世界中生动起来，用户发出指令，角色响应用户的操作(命令)。在代码世界中，相信我们能快速实现这一些列动作指令:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (Input.<span class="built_in">GetKeyDown</span>(KeyCode.RightArrow)) </span><br><span class="line">&#123; </span><br><span class="line">    actor.<span class="built_in">Forward</span>(); </span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">else</span> <span class="keyword">if</span> (Input.<span class="built_in">GetKeyDown</span>(KeyCode.LeftArrow)) </span><br><span class="line">&#123;</span><br><span class="line">    actor.<span class="built_in">Back</span>();</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// ...</span></span><br></pre></td></tr></table></figure><p>看上面的代码，并没有什么问题，在上面的代码中，玩家输入了事件(发送命令)，名为 <code>actor</code> 的角色响应了用户的操作前进或者后退。这其中，玩家相当于是命令发送者，而角色是命令消费者。没错！玩家发送命令和角色消费命令的代码紧紧耦合在了一起，如果此时按下 <code>RightArrow</code> 的时候，消费者不再是名为 <code>actor</code> 的角色，或者执行的动作不是 <code>Forward</code>，则需要手动的修改代码。</p><span id="more"></span><h1 id="看看设计需求"><a href="#看看设计需求" class="headerlink" title="看看设计需求"></a>看看设计需求</h1><p>上面的设计需求经常出现，比如在一款联网游戏中，服务器模拟的 AI 数据需要实时同步到客户端处理，此时你可能想：那对每个动作加以判断，然后对应的角色去执行不就行了吗?这样确实可行，但是不优雅。想想随着动作的增多而你的判断也要增加，最后的代码可能会是下面的样子:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">ExecuteCommand</span><span class="params">(Action action, Actor actor)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">if</span>(action == RIGHT)</span><br><span class="line">    &#123;</span><br><span class="line">        actor.<span class="built_in">Forward</span>();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span> <span class="keyword">if</span>(action == LEFT)</span><br><span class="line">    &#123;</span><br><span class="line">        actor.<span class="built_in">Back</span>();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span> <span class="keyword">if</span>(action == UP)</span><br><span class="line">    &#123;</span><br><span class="line">        actor.<span class="built_in">Jump</span>();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 更多的 action 处理...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对于上面的这种情况，我们并不需要关注 AI 角色到底该如何去做什么(服务器已经都做好了)，服务器发送了一条指令，让指令执行(其他都不需要关注，有点像 RPC)才是我们该做的事情。此时，使用命令模式就可以满足我们的设计需求。</p><h1 id="定义"><a href="#定义" class="headerlink" title="定义"></a>定义</h1><p>上面的两个例子阐述什么情况下需要使用命令模式，那么到底什么是命令模式了?以下引用摘自*《游戏编程模式》*。</p><blockquote><p>将一个请求封装为一个对象，从而使你可用不同的请求对客户进行参数化；对请求排队或记录请求日志，以及支持可撤销操作。<strong>其本质是将命令的生产者和命令的消费者分开</strong>；命令模式属于一种行为模式。</p></blockquote><h1 id="实现"><a href="#实现" class="headerlink" title="实现"></a>实现</h1><ul><li>普通的命令 Receiver 类，比如角色类，消费命令实现本身特定的操作</li></ul><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Actor</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="type">void</span> <span class="title">Left</span><span class="params">()</span></span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        Debug.<span class="built_in">Log</span>(<span class="string">&quot;Left!&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="type">void</span> <span class="title">Right</span><span class="params">()</span></span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        Debug.<span class="built_in">Log</span>(<span class="string">&quot;Right!&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// other action...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>引入抽象命令接口，生产者(调用者)使用抽象命令接口编程</li></ul><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> abstract <span class="keyword">class</span> <span class="title class_">Command</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> abstract <span class="type">void</span> <span class="title">Execute</span><span class="params">(Actor actor)</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>具体命令类需要实现抽象接口，在实现中执行接收者对应的操作</li></ul><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">LeftCommand</span> : Command</span><br><span class="line">&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">override</span> <span class="type">void</span> <span class="title">Execute</span><span class="params">(Actor actor)</span></span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        actor.<span class="built_in">Left</span>();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>生产者(调用者)依据抽象命令接口编程，通过命令对象来执行请求</li></ul><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">GameController</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="type">void</span> <span class="title">Main</span><span class="params">()</span></span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        Actor _actor = <span class="keyword">new</span> <span class="built_in">Actor</span>();</span><br><span class="line">        Command _leftCommand = <span class="keyword">new</span> <span class="built_in">LeftCommand</span>();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// execute</span></span><br><span class="line">        _leftCommand.<span class="built_in">Execute</span>(_actor);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过实现方式可以看出，命令的生产者和命令消费者之间已经没了直接调用关系，他们之间多了一层 <code>Command</code>。这样当你需要消费者执行不同的操作时，添加一个新的命令类并实现消费者的操作即可；甚至你的命令生产者能产生许多命令，将其 push 到一个命令队列中，消费者只要从队列中读取命令消费，因为命令的消费已经与生产者无关了！</p><p>再回到我们的第二个例子中。通过使用命令模式对 <code>ExecuteCommand</code> 方法加以改造，从而让代码更加优雅易懂。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">ExecuteCommand</span><span class="params">(Command command, Actor actor)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    command.<span class="built_in">Execute</span>(actor);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>现在，<code>ExecuteCommand</code> 接收的是一个 <code>Command</code> 和一个 <code>Actor</code>，这样看起来是不是更加符合我们的设计需求了?服务器模拟的 AI 的每一个操作通过一个个 <code>Command</code> 发送给客户端，客户端只需要执行命令命令就能够被消费。</p><p><code>ExecuteCommand</code> 方法需要两个参数，在多数情况下这样是不错的选择。因为对于某个命令对象，你可以替换其中的操作执行者从而达到不同的执行者共享同一个命令对象的目的。但是如果碰到需要实现类似命令队列的问题，可能将每个操作执行者作为命令类的成员变量是更好的选择，但因此也就牺牲了共享命令类实例的优势。</p><p>如产生一系列命令的同时，将命令 push 到一个队列中，这样实现命令的撤销和恢复，尝试如下改动:</p><ul><li>首先，将操作执行类作为命令类的成员变量</li></ul><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> abstract <span class="keyword">class</span> <span class="title class_">Command</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">protected</span> Actor _actor;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> abstract <span class="type">void</span> <span class="title">Execute</span><span class="params">()</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>具体命令类修改</li></ul><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">LeftCommand</span> : Command</span><br><span class="line">&#123;</span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="title">LeftCommand</span><span class="params">(Actor actor)</span></span></span><br><span class="line"><span class="function">  </span>&#123;</span><br><span class="line">      _actor = actor;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">override</span> <span class="type">void</span> <span class="title">Execute</span><span class="params">()</span></span></span><br><span class="line"><span class="function">  </span>&#123;</span><br><span class="line">      _actor.<span class="built_in">Left</span>();</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>实现命令队列以及撤销、恢复操作</li></ul><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">GameController</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">private</span> List&lt;Command&gt; _commands = <span class="keyword">new</span> <span class="built_in">List</span>&lt;Command&gt;();</span><br><span class="line">    <span class="keyword">private</span> <span class="type">int</span> _curCommandIdx = <span class="number">-1</span>;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="type">void</span> <span class="title">Main</span><span class="params">()</span></span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        <span class="built_in">ExecuteCommand</span>(<span class="keyword">new</span> <span class="built_in">LeftCommand</span>(<span class="keyword">new</span> <span class="built_in">Actor</span>()));</span><br><span class="line">        <span class="built_in">ExecuteCommand</span>(<span class="keyword">new</span> <span class="built_in">RightCommand</span>(<span class="keyword">new</span> <span class="built_in">Actor</span>()));</span><br><span class="line">        <span class="built_in">Undo</span>();</span><br><span class="line">        <span class="built_in">Do</span>();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">ExecuteCommand</span><span class="params">(Command command)</span></span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        _curCommandIdx++;</span><br><span class="line">        _commands.<span class="built_in">Add</span>(command);</span><br><span class="line">        _commands[_curCommandIdx].<span class="built_in">Execute</span>();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">Undo</span><span class="params">()</span></span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        _curCommandIdx--;</span><br><span class="line">        _commands[_curCommandIdx].<span class="built_in">Execute</span>();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">Do</span><span class="params">()</span></span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        _curCommandIdx++;</span><br><span class="line">        _commands[_curCommandIdx].<span class="built_in">Execute</span>();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>以上就是命令队列简单的实现，虽然牺牲了共享命令类实例的优势，但是换回了更多的设计需求。</p><p>所以在有些情况下，当操作非常多的时候，就需要写更多的具体命令类(虽然本来的目的就如此)，当不能共享命令类(如同上面的命令队列实现)就可能会产生大量的命令实例。所以，最终的权衡还是要根据更多的需求实际情况来确定。</p>]]>
    </content>
    <id>https://lujun.pages.dev/2019/06/18/command_pattern_in_game_programing/</id>
    <link href="https://lujun.pages.dev/2019/06/18/command_pattern_in_game_programing/"/>
    <published>2019-06-18T21:42:00.000Z</published>
    <summary>
      <![CDATA[<p><code>前进</code>、<code>后退</code>、<code>跳跃</code>等诸多的动作让角色在游戏世界中生动起来，用户发出指令，角色响应用户的操作(命令)。在代码世界中，相信我们能快速实现这一些列动作指令:</p>
<figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (Input.<span class="built_in">GetKeyDown</span>(KeyCode.RightArrow)) </span><br><span class="line">&#123; </span><br><span class="line">    actor.<span class="built_in">Forward</span>(); </span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">else</span> <span class="keyword">if</span> (Input.<span class="built_in">GetKeyDown</span>(KeyCode.LeftArrow)) </span><br><span class="line">&#123;</span><br><span class="line">    actor.<span class="built_in">Back</span>();</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// ...</span></span><br></pre></td></tr></table></figure>

<p>看上面的代码，并没有什么问题，在上面的代码中，玩家输入了事件(发送命令)，名为 <code>actor</code> 的角色响应了用户的操作前进或者后退。这其中，玩家相当于是命令发送者，而角色是命令消费者。没错！玩家发送命令和角色消费命令的代码紧紧耦合在了一起，如果此时按下 <code>RightArrow</code> 的时候，消费者不再是名为 <code>actor</code> 的角色，或者执行的动作不是 <code>Forward</code>，则需要手动的修改代码。</p>]]>
    </summary>
    <title>游戏编程模式 - 命令模式</title>
    <updated>2026-06-22T14:03:58.804Z</updated>
  </entry>
  <entry>
    <author>
      <name>lujun</name>
    </author>
    <category term="设计模式" scheme="https://lujun.pages.dev/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"/>
    <category term="Unity" scheme="https://lujun.pages.dev/tags/Unity/"/>
    <content>
      <![CDATA[<script src="/assets/js/APlayer.min.js"> </script><p>自然界中 <code>状态</code> 属于物体本身。猫咪正在睡觉、吃、喝水，这些动作都属于猫咪自己，它通过自己的大脑调整改变自己每刻的状态。</p><p>游戏世界中，每个物体也是如此。他们都有一系列状态，通过状态之间的切换，去描述本身的行为。所以，游戏中每个物体都可能都会存在大量的状态，在编程过程中，我们也需要控制物体不同状态间的切换。</p><span id="more"></span><h1 id="看看设计需求"><a href="#看看设计需求" class="headerlink" title="看看设计需求"></a>看看设计需求</h1><p>在某一款游戏中，存在这样一个角色: 它能够走、跑、跳跃和滑行四个状态。通常情况下我们控制这个角色的状态切换，会使用条件语句，如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">SwitchState</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (Input.<span class="built_in">GetKeyDown</span>(KeyCode.RightArrow))</span><br><span class="line">    &#123;</span><br><span class="line">        _player.<span class="built_in">Walk</span>();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span> <span class="keyword">if</span> (Input.<span class="built_in">GetKeyDown</span>(KeyCode.LeftShift))</span><br><span class="line">    &#123;</span><br><span class="line">        _player.<span class="built_in">Run</span>();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span> <span class="keyword">if</span> (Input.<span class="built_in">GetKeyDown</span>(KeyCode.Space))</span><br><span class="line">    &#123;</span><br><span class="line">        _player.<span class="built_in">Jump</span>();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span> <span class="keyword">if</span> (Input.<span class="built_in">GetKeyDown</span>(KeyCode.RightShift))</span><br><span class="line">    &#123;</span><br><span class="line">        _player.<span class="built_in">Slide</span>();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面的代码某些情况下会出现一些问题:</p><ul><li>在上面的代码中，我们将角色的行为和输入控制器绑定在了一起，角色的状态改变全听从用户的输入，但有些时候角色也需要自己改变自己的状态，而不需要外部干预。比如:角色执行完 “滑倒” 动作越过障碍之后，需要自己立马切换回 ‘Run’ 的状态。</li></ul><p>因此一个好的 “角色” 设计应该是它的状态既可以对外被更新，也可以对内被更新，控制权由设计者决定。</p><ul><li>每次输入对应着一个状态的切换，往往一个角色执行一个状态时会需要一定时间，比如说 <code>滑倒</code> 动作，角色可能需要播放一个滑倒的动画。但此时若紧接着按下 <code>Space</code> 按键，那么角色就处于 “滑倒式跳跃” 的动画中，是不是很怪异。有问题就有解决方案，加入一个控制变量控制某个状态执行的时候不能执行其他状态，如下面的代码:</li></ul><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="type">bool</span> _isSliding = <span class="literal">false</span>;</span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">SwitchState</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (Input.<span class="built_in">GetKeyDown</span>(KeyCode.Space))</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">if</span> (!_isSliding)</span><br><span class="line">        &#123;</span><br><span class="line">            _player.<span class="built_in">Jump</span>();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span> <span class="keyword">if</span> (Input.<span class="built_in">GetKeyDown</span>(KeyCode.RightShift))</span><br><span class="line">    &#123;</span><br><span class="line">        _isSliding = <span class="literal">true</span>;</span><br><span class="line">        _player.<span class="built_in">Slide</span>();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过加入一个控制变量，问题确实得到了解决。但是，随着状态的增多，你的控制变量也能会随之增多。条件语句嵌套会更加 “彻底”。这样不好，代码不易维护扩展也不够优雅。</p><p>也许状态模式就是为这些扰人的需求情况而诞生的吧！</p><h1 id="定义"><a href="#定义" class="headerlink" title="定义"></a>定义</h1><p>*《游戏编程模式》*一书中这样定义状态模式:</p><blockquote><p><strong>允许对象在当内部状态改变时改变其行为</strong>，就好像此对象改变了自己的类一样。状态模式属于一种行为模式。</p></blockquote><h1 id="实现"><a href="#实现" class="headerlink" title="实现"></a>实现</h1><ul><li>引入抽象类或接口，声明状态类基本该有的功能</li></ul><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> interface State</span><br><span class="line">&#123;</span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">Handle</span><span class="params">()</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>实现具体状态类，继承或实现状态抽象类和接口，实现当前具体状态类需要实现的行为</li></ul><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">JumpState</span> : State</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">protected</span> Player _player;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">JumpState</span><span class="params">(Player player)</span></span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        _player = player;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="type">void</span> <span class="title">Handle</span><span class="params">()</span></span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        <span class="comment">// Execute state</span></span><br><span class="line">        _player.<span class="built_in">Jump</span>();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Change state</span></span><br><span class="line">        <span class="built_in">TimerInvoke</span>(<span class="string">&quot;JumpFinished&quot;</span>, <span class="number">2000</span>);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Do nothing for input</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="type">void</span> <span class="title">JumpFinished</span><span class="params">(object obj, ElapsedEventArgs args)</span></span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        _player.State = <span class="keyword">new</span> <span class="built_in">IdleState</span>(_player);</span><br><span class="line">        Debug.<span class="built_in">Log</span>(<span class="string">&quot;Jump Done! Switch to Run state...&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>JumpState</code> 类的 <code>Handle</code> 方法中，实现了 “跳跃” 状态所需做的事情(也许是一个动画，或者是播放一段音效)。动作完成之后，由 “跳跃” 态进入到 “Idle” 态。状态的切换隐匿在了这个状态类内部，改变的是 <code>_player</code> 所拥有的 <code>State</code> 变量；当切换成功后又会开始执行下一个状态所处的行为(另一个实现了 <code>State</code> 接口的具体状态类的 <code>Handle</code> 方法被执行)。</p><p>不仅仅是自身主动切换状态，我们还可以在 <code>Handle</code> 方法内部根据玩家的输入，来控制状态切换:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="type">void</span> <span class="title">Handle</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    _player.<span class="built_in">Idle</span>();</span><br><span class="line">    <span class="keyword">if</span> (Input.<span class="built_in">GetKey</span>(KeyCode.RightArrow))</span><br><span class="line">    &#123;</span><br><span class="line">        _player.State = <span class="keyword">new</span> <span class="built_in">WalkState</span>(_player);</span><br><span class="line">        _player.<span class="built_in">SwitchState</span>();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span> <span class="keyword">if</span> (Input.<span class="built_in">GetKey</span>(KeyCode.LeftShift) &amp;&amp; Input.<span class="built_in">GetKey</span>(KeyCode.RightArrow))</span><br><span class="line">    &#123;</span><br><span class="line">        _player.State = <span class="keyword">new</span> <span class="built_in">RunState</span>(_player);</span><br><span class="line">        _player.<span class="built_in">SwitchState</span>();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span> <span class="keyword">if</span> (Input.<span class="built_in">GetKeyDown</span>(KeyCode.Space))</span><br><span class="line">    &#123;</span><br><span class="line">        _player.State = <span class="keyword">new</span> <span class="built_in">JumpState</span>(_player);</span><br><span class="line">        _player.<span class="built_in">SwitchState</span>();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面是 <code>IdleState</code> 类(<a href="https://github.com/whilu/GameProgrammingPatterns/blob/master/Assets/StatePattern/Scripts/IdleState.cs">源码</a>)的 <code>Handle</code> 方法。可以看到，我们将玩家的输入也添加了进来，从而玩家可以主动控制状态切换，也就是状态 “对外” 可被更新。</p><p>玩家的在具体状态类中对状态的更新，主要是通过 “角色” 类，毕竟这才是玩家看的到的。紧接着，就来看看角色类吧。</p><ul><li>状态的拥有者(角色)</li></ul><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Player</span> : MonoBehaviour</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">private</span> State _state;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="type">void</span> <span class="title">Jump</span><span class="params">()</span></span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        Debug.<span class="built_in">Log</span>(<span class="string">&quot;Jump...!&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="type">void</span> <span class="title">SwitchState</span><span class="params">()</span></span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        _state.<span class="built_in">Handle</span>();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="type">void</span> <span class="title">Update</span><span class="params">()</span></span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        <span class="built_in">SwitchState</span>();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看出，角色直接面向玩家。角色拥有状态，并且掌控者玩家的输入 “大权”。<strong>状态被改变，同时角色行为也就发生改变</strong>，角色持有的是 <code>State</code> 接口类变量，它不需要知道自己究竟需要切换到 “哪个状态”，但它知道自己需要切换 “状态”，同时它自己也定义了一些列状态的实现供状态类帮它执行。在不同状态类内部主动切换至其它状态(或是响应输入而切换)，切换后的状态也可以切换去任意其它所定义的状态类。状态属于物体，就像我们的实现中，状态属于 <code>Player</code> 类，所有的状态切换都服务于它。</p><ul><li>带有控制变量的状态类</li></ul><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">JumpState</span> : State</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="type">static</span> <span class="type">bool</span> _sIsJumping = <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="type">void</span> <span class="title">Handle</span><span class="params">()</span></span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (_sIsJumping)</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        _sIsJumping = <span class="literal">true</span>;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Execute state...</span></span><br><span class="line">        <span class="comment">// Change state...</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="type">void</span> <span class="title">JumpFinished</span><span class="params">(object obj, ElapsedEventArgs args)</span></span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        _sIsJumping = <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Do change state</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在原来 <code>JumpState</code> 中我们增加了 <code>_sIsJumping</code> 用来保证 “跳跃” 状态不被重复执行(处于 “跳跃” 状态不能再跳跃)。</p><p>多状态之间切换控制问题，最初我们为了控制某个状态 “不变形”，而加入了很多控制变量。后面随着状态增多而增多了控制变量，导致代码难以维护。现在引入了状态模式后，我们可以为每个具体状态类引入对应的控制变量，这样无论是对于以后的需求扩展还是代码得维护都会变得更加容易。而每个单独的状态类都能够限制此个状态下能否响应哪些输入、能切换哪些状态，这正是我们需要的!</p><p>同时，新增状态类也就意味着你的项目中的具体状态类可能无限增长(属于行为模式的设计模式好像都无法绕过这一点，因为设计初衷就是为此而生)；在我们的实现中，所有的状态切换都是先 <code>new</code> 出来新状态类再切换，这有可能导致对象数量增多(对于不具有垃圾回收的语言，切记做好内存释放工作)，不过也可以对相同的状态实现状态类实例共享来减少 GC。</p><p>看完上面关于状态模式的定义、实现介绍等，感觉似曾相识。多个状态间切换，物体始终处于某一态，等待着某些指令进入下一态，这不就是状态机吗? 是的，这就是一个简单的有限状态机。状态模式将状态切换内部封装，状态自己拥有自己的控制变量从而和外部解耦；由于所有的具体状态类实现了通用状态接口且对象持有接口类，对象不需要关注下一步我是哪个状态，只需要执行状态就行；对于新增状态只需要实现接口并做好对象状态切换工作即可。</p><p><a href="https://github.com/whilu/GameProgrammingPatterns/tree/master/Assets/StatePattern">示例源码</a></p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ul><li>《游戏编程模式》</li><li><a href="https://indienova.com/indie-game-development/game-programming-patterns-3/">Indienova - 状态模式、有限状态机与 Unity版本实现</a></li></ul>]]>
    </content>
    <id>https://lujun.pages.dev/2019/06/11/state_pattern_in_game_programing/</id>
    <link href="https://lujun.pages.dev/2019/06/11/state_pattern_in_game_programing/"/>
    <published>2019-06-11T15:02:00.000Z</published>
    <summary>
      <![CDATA[<p>自然界中 <code>状态</code> 属于物体本身。猫咪正在睡觉、吃、喝水，这些动作都属于猫咪自己，它通过自己的大脑调整改变自己每刻的状态。</p>
<p>游戏世界中，每个物体也是如此。他们都有一系列状态，通过状态之间的切换，去描述本身的行为。所以，游戏中每个物体都可能都会存在大量的状态，在编程过程中，我们也需要控制物体不同状态间的切换。</p>]]>
    </summary>
    <title>游戏编程模式 - 状态模式</title>
    <updated>2026-06-22T14:03:58.804Z</updated>
  </entry>
  <entry>
    <author>
      <name>lujun</name>
    </author>
    <category term="Unity" scheme="https://lujun.pages.dev/tags/Unity/"/>
    <content>
      <![CDATA[<script src="/assets/js/APlayer.min.js"> </script><p>Camera 作为开发者最熟悉的组件之一，有很多常用属性，比如 <code>Clear Flags</code>、<code>Projection</code> 和 <code>Depth</code> 等。接下来主要就来谈谈简单易用而又不寻常的 <code>Clear Flags</code>。</p><p>Unity 文档中对 <code>Clear Flags</code> 的描述也很简单:</p><blockquote><p>Determines which parts of the screen will be cleared. This is handy when using multiple Cameras to draw different game elements.</p></blockquote><p>设置这个标志位会对屏幕(缓存帧)指定区域进行清除，通常在多个 Camera 绘渲染不同物体的时候用到。字面意思很简单，下面来解析一下设置不同的标志位的情况。</p><span id="more"></span><h2 id="Skybox-Solid-Color"><a href="#Skybox-Solid-Color" class="headerlink" title="Skybox &amp; Solid Color"></a>Skybox &amp; Solid Color</h2><p>将这两个标志位值放在一起，因为它们很相似。</p><p>当标志位被设置为这两个值中的某一个时，那么 Camera 在渲染每一帧前，逐像素过程(Shader 部分知识，读者可自行查阅)中会对每个像素进行深度值与颜色值的 Clear，然后在将需要渲染的颜色值与深度值(如有需要)写入。这两个标志位值的差异就是进行写入时前者是用的天空盒子中的颜色值，后者 Solid Color 需要配合 Camera 下另一个属性 <code>Background</code> 设置的颜色值进行写入。</p><p><strong>关于 <code>Background</code> 的说明如下:</strong></p><blockquote><p>The color applied to the remaining screen after all elements in view have been drawn and there is no skybox.</p></blockquote><p>Camera 进行渲染物体时的流程大致是: 逐像素过程中，由于像素的深度值被 Clear 了，根据使用的 Shader 中的设定规则如果 ZTest 通过，把要渲染的颜色值写入，深度值是否写入看使用的 Shader 中的 ZWrite 是否开启。由于 <code>Skybox</code> 或 <code>Solid Color</code> 是最先被渲染的(默认 <code>Queue=Background</code>)，所以默认它们就作为了背景。</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_camera_clear_flags_screen_record_1.gif" alt="unity_camera_clear_flags_screen_record_1.gif" title="unity_camera_clear_flags_screen_record_1.gif" width="269" height="264" /></center><p>上图为当 Clear Flags 设置为 <code>Skybox</code> 时的效果，其中三个物体都使用了内置 <code>Standard.shader</code>，开启了 ZWrite、ZTest，所以渲染每一帧都是清除了之前的所有信息渲染出了新的图像。</p><h2 id="Depth-Only"><a href="#Depth-Only" class="headerlink" title="Depth Only"></a>Depth Only</h2><p>作为 Clear Flags 中最常用的设置值，<code>Depth Only</code> 就是仅清除缓存帧中的深度信息。</p><p>清除深度信息后会发生什么？渲染过的像素如果没有需要渲染的物体，则会保持原来的颜色值；否则会渲染新的物体颜色值，不论这个点之前是什么(因为深度值被 Clear)，所有在这个像素上发生的 ZTest 默认规则下都会通过，ZWrite 也会按照设定的规则生效写入对应的深度值。</p><p>如下图所示:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_camera_clear_flags_screen_record_2.gif" alt="unity_camera_clear_flags_screen_record_2.gif" title="unity_camera_clear_flags_screen_record_2.gif" width="269" height="264" /></center><p>之前说 <code>Depth Only</code> 是最常使用的设定值，官方文档中介绍的:</p><blockquote><p>This is handy when using multiple Cameras to draw different game elements.</p></blockquote><p>意义就在此，当我们需要使用多个 Camera 渲染不同的物体时，当在编辑器 Hierarchy 中为后面 Camera 的 Clear Flags 设定为该值时，该 Camera 渲染时由于不会清除之前 Camera 已渲染的图像(假定后面的 Camera 的 Depth 值大于前面的 Camera，后执行渲染)，并且由于清除了深度值，所以不会因为后面摄像机渲染的物体的深度值大于之前物体的深度值而不太能通过 ZTest，所以图像会覆盖在已有图像上方。</p><p><strong>注意: 上面提到的 Camera 的 Depth 值是 Camera 下的一个属性。Camera 的 Depth 值设定越大，越会渲染在上方。</strong></p><blockquote><p>The camera’s position in the draw order. Cameras with a larger value will be drawn on top of cameras with a smaller value.</p></blockquote><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_camera_clear_flags_screen_record_3.gif" alt="unity_camera_clear_flags_screen_record_3.gif" title="unity_camera_clear_flags_screen_record_3.gif" width="269" height="264" /></center><p>如上图，红蓝黄三个物体由一个 Camera 渲染，绿色物体由第二个 Camera 渲染(第二个 Camera 的 Depth 值大于第一个且第二个的 Clear Flags 设置为了 <code>Depth Only</code>),可以看出第一个 Camera 渲染的图像保留在屏幕上，当改变绿色物体的 Z 坐标时，无论是否大于或小于红蓝黄物体的 Z 坐标都能覆盖在最上面。</p><h2 id="Don’t-Clear"><a href="#Don’t-Clear" class="headerlink" title="Don’t Clear"></a>Don’t Clear</h2><p>设定为这个值不会清除任何信息。</p><p>所以之前的颜色值会一直存在于屏幕上，直至新的颜色值替换掉缓存的颜色值。同理深度值也不会被清除，直至某个通过了 ZTest 并且 ZWrite 为 On 的像素将深度值更新。</p><p>大致流程: 逐像素 ZTest，通过则写入新的颜色值，并根据是否开启 ZWrite 来确定是否更新深度值，否则舍弃。</p><p>演示如下图:</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_camera_clear_flags_screen_record_5.gif" alt="unity_camera_clear_flags_screen_record_5.gif" title="unity_camera_clear_flags_screen_record_5.gif" width="616" height="241" /></center><p>对于使用 ZWrite Off 的 Shader 的物体，在 <code>Don&#39;t Clear</code> 下(<strong>注意: 在设置为该值的同时关闭了 Camrea 的 HDR 和抗锯齿</strong>)，由于深度值未写入，所以就算该物体当前帧的深度值比较小而渲染在了前方，但是当下一帧如果深度值变大之后，比其深度值小的物体渲染时就算没有清除之前的颜色缓存，但依旧能通过 ZTest，从而更新像素点的颜色值和深度值。这和开启 ZWrite 刚好相反。</p><p>如下图所示，紫色片物体是一个 Sprite，使用了 <code>Sprite-Default.shader</code>，该 Shader 默认关闭了 ZWrite，所以当它 Z 坐标变大(深度值变大)之后，比其深度值小的物体依旧能在前面显示出来。</p><center><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/unity_camera_clear_flags_screen_record_4.gif" alt="unity_camera_clear_flags_screen_record_4.gif" title="unity_camera_clear_flags_screen_record_4.gif" width="616" height="241" /></center><h2 id="OpenGL-和-DirectX-中也有-Clear"><a href="#OpenGL-和-DirectX-中也有-Clear" class="headerlink" title="OpenGL 和 DirectX 中也有 Clear?"></a>OpenGL 和 DirectX 中也有 Clear?</h2><p>答案是肯定的!</p><p>在 OpenGL 关于 Clear 对应了几个方法，分别是 <code>glClearColor</code>、<code>glClearDepth</code> 和 <code>glClear</code>。</p><ul><li><code>glClearColor</code> 方法用于指定颜色缓冲区的值，在颜色缓冲区清空时使用:</li></ul><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">glClearColor</span><span class="params">(GLclampf red,GLclampf green,Glclampf blue,GLclampf alpha)</span></span>;</span><br></pre></td></tr></table></figure><ul><li><code>glClearDepth</code> 方法指定清除深度缓存时使用的值，范围在[0,1]之间:</li></ul><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">glClearDepth</span><span class="params">(GLclampd depth)</span></span>;</span><br></pre></td></tr></table></figure><ul><li><code>glClear</code> 方法将缓存清除为预先设置的值:</li></ul><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">glClear</span><span class="params">(GLbitfield mask)</span></span>;</span><br></pre></td></tr></table></figure><p>其中参数对应需要清除的缓存区(有 <code>GL_COLOR_BUFFER_BIT</code> 颜色缓冲区、<code>GL_DEPTH_BUFFER_BIT</code> 深度缓冲区、<code>GL_STENCIL_BUFFER_BIT</code> 模板缓冲区和 <code>GL_ACCUM_BUFFER_BIT</code> 累计缓冲区)，这个方法参数的几种组合就正好对应了 Unity Camera 中的 Clear Flags 几个标志位。</p><p>在 Direct3D 中也有这样的方法，如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">HRESULT <span class="title">Clear</span><span class="params">(</span></span></span><br><span class="line"><span class="params"><span class="function">  DWORD         Count,</span></span></span><br><span class="line"><span class="params"><span class="function">  <span class="type">const</span> D3DRECT *pRects,</span></span></span><br><span class="line"><span class="params"><span class="function">  DWORD         Flags,</span></span></span><br><span class="line"><span class="params"><span class="function">  D3DCOLOR      Color,</span></span></span><br><span class="line"><span class="params"><span class="function">  <span class="type">float</span>         Z,</span></span></span><br><span class="line"><span class="params"><span class="function">  DWORD         Stencil</span></span></span><br><span class="line"><span class="params"><span class="function">)</span></span>;</span><br></pre></td></tr></table></figure><p>其中 <code>Flags</code> 参数也是对应着 Unity Clear Flags 标志位，包含 <code>D3DCLEAR_STENCIL</code>、<code>D3DCLEAR_TARGET</code> 和 <code>D3DCLEAR_ZBUFFER</code>，详见 <a href="https://docs.microsoft.com/en-us/windows/desktop/api/d3d9/nf-d3d9-idirect3ddevice9-clear">IDirect3DDevice9::Clear method</a>。</p>]]>
    </content>
    <id>https://lujun.pages.dev/2019/06/02/unity_camera_clear_flags/</id>
    <link href="https://lujun.pages.dev/2019/06/02/unity_camera_clear_flags/"/>
    <published>2019-06-02T11:15:01.000Z</published>
    <summary>
      <![CDATA[<p>Camera 作为开发者最熟悉的组件之一，有很多常用属性，比如 <code>Clear Flags</code>、<code>Projection</code> 和 <code>Depth</code> 等。接下来主要就来谈谈简单易用而又不寻常的 <code>Clear Flags</code>。</p>
<p>Unity 文档中对 <code>Clear Flags</code> 的描述也很简单:</p>
<blockquote>
<p>Determines which parts of the screen will be cleared. This is handy when using multiple Cameras to draw different game elements.</p>
</blockquote>
<p>设置这个标志位会对屏幕(缓存帧)指定区域进行清除，通常在多个 Camera 绘渲染不同物体的时候用到。字面意思很简单，下面来解析一下设置不同的标志位的情况。</p>]]>
    </summary>
    <title>探寻 Unity Camera 属性之 Clear Flags</title>
    <updated>2026-06-22T14:03:58.804Z</updated>
  </entry>
  <entry>
    <author>
      <name>lujun</name>
    </author>
    <category term="Unity" scheme="https://lujun.pages.dev/tags/Unity/"/>
    <category term="Shader" scheme="https://lujun.pages.dev/tags/Shader/"/>
    <content>
      <![CDATA[<script src="/assets/js/APlayer.min.js"> </script><p>我们知道 <code>TANGENTA_SPACE_ROTATION</code> 是内建在 <code>UnityCG.cginc</code> 中的一个宏，作用是实现从模型空间到切线空间的变换。那么究竟具体是怎么变换的了？</p><span id="more"></span><h2 id="首先，什么是切线空间？"><a href="#首先，什么是切线空间？" class="headerlink" title="首先，什么是切线空间？"></a>首先，什么是切线空间？</h2><p>切线空间，简单来说就是一个坐标系。对于模型的每个顶点，都有一个切线空间，坐标系的原点就是顶点，切线方向(t)即使是 X 轴，顶点的法线方向(n)就是 Z 轴，Y 轴就是副切线方向(法线和切线叉积可得到，有两个方向，可根据 <code>v.tangent.w</code> 决定选取哪一条作为副切线)。</p><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/tangent_space_coord.jpeg" alt="tangent_space_coord.jpeg" title="tangent_space_coord.jpeg" width="300" height="275" /><p>图片来自:《Unity Shader 入门精要》</p><p>在使用法线纹理时我们一般都会使用到切线空间。当使用的是切线空间下的法线纹理时，由于每个法线位于各自的切线空间下(每个切线空间都不一样，下面会讲到什么是切线空间而导致不一样)，这种法线纹理存储的就是每个点在各自切线空间中的法线扰动方向。</p><h2 id="接着，我们来看看-TANGENTA-SPACE-ROTATION-具体的代码，如下"><a href="#接着，我们来看看-TANGENTA-SPACE-ROTATION-具体的代码，如下" class="headerlink" title="接着，我们来看看 TANGENTA_SPACE_ROTATION 具体的代码，如下:"></a>接着，我们来看看 <code>TANGENTA_SPACE_ROTATION</code> 具体的代码，如下:</h2><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Declares 3x3 matrix &#x27;rotation&#x27;, filled with tangent space basis</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> TANGENT_SPACE_ROTATION \</span></span><br><span class="line"><span class="meta">    float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) ) * v.tangent.w; \</span></span><br><span class="line"><span class="meta">    float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal )</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><p>代码首先使用模型空间下的法线方向和切线方向叉积得到了副切线方向 <code>binormal</code>(叉积结果与 <code>v.tangent.w</code> 进行相乘，因为与法线和切线都垂直的方向有两个，与 <code>v.tangent.w</code> 相乘决定选择其中哪一个方向)。</p></li><li><p>然后定义了 3x3 变换矩阵 <code>rotation</code>，分别将切线方向、副切线方向和法线方向按行摆放组成了这个矩阵。</p></li></ul><h2 id="为什么这样得到的矩阵就是从模型空间到切线空间的变换矩阵了？"><a href="#为什么这样得到的矩阵就是从模型空间到切线空间的变换矩阵了？" class="headerlink" title="为什么这样得到的矩阵就是从模型空间到切线空间的变换矩阵了？"></a>为什么这样得到的矩阵就是从模型空间到切线空间的变换矩阵了？</h2><p>我们知道，要得到一个从 C 空间到 P 空间的变换矩阵，只需要将 C 空间在 P 空间下表示的坐标轴矢量以及原点按列摆放构建得到的就是 C -&gt; P 的变换矩阵(坐标空间变换基础)。</p><p>因此我们想要求得模型空间到切线空间的变换矩阵时，只需要将模型空间在切线空间下表示的坐标轴和原点按照列摆放即可。求模型空间到切线空间的变换矩阵比较麻烦，但是得到切线空间到模型空间的变换矩阵却很容易，切线空间到模型空间的变换矩阵的逆矩阵就是我们需要的矩阵。切线空间到模型空间的变换矩阵就是切线坐标轴和原点在模型空间下按列摆放得到的矩阵，即只需要将切线方向(X轴)、副切线方向(Y轴)和法线方向(Z轴)按列摆放即可。我们知道，如果一个变换中仅存在旋转和平移变换(正交矩阵)，那么这个变换矩阵的逆矩阵就等于它的转置矩阵，从切线空间到模型空间的变换正好满足这种情况，因此我们就得到了模型空间到切线空间的变换矩阵：切线坐标轴和原点在模型空间下按行摆放得到的矩阵，即将切线方向(X轴)、副切线方向(Y轴)和法线方向(Z轴)按行摆放得到的矩阵。</p><p>需要注意的是，在 <code>TANGENTA_SPACE_ROTATION</code> 得到的 <code>rotation</code> 是一个 3x3 的矩阵，也就是说这个矩阵是用来对方向矢量进行坐标空间变换的。因为矢量是没有位置的，因此坐标空间的原点可以忽略。</p><p>这就是 <code>TANGENTA_SPACE_ROTATION</code> 得到的 <code>rotation</code> 矩阵为什么是从模型空间到切线空间的变换矩阵的原理。</p><h2 id="再来类推一下世界空间到切线空间的变换矩阵"><a href="#再来类推一下世界空间到切线空间的变换矩阵" class="headerlink" title="再来类推一下世界空间到切线空间的变换矩阵"></a>再来类推一下世界空间到切线空间的变换矩阵</h2><p>首先，我们计算出切线空间各个坐标轴在世界空间下的表示：</p><ul><li><p>世界空间下顶点处的切线方向(X 轴) <code>fixed3 worldTangent = UnityObjectToWorldDir(v.tangent);</code></p></li><li><p>世界空间下顶点处的法线方向(Z 轴) <code>fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);</code></p></li><li><p>世界空间下顶点处的副切线方向(Y 轴) <code>fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;</code></p></li></ul><p>然后构建切线空间到世界空间的变换矩阵，将上面三个计算得到的值按照 X、Y、Z 轴分别按照列摆放得到切线空间到世界空间的变换矩阵。如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">float4x4 tangentToWorldMatrix = <span class="built_in">float4x4</span>(</span><br><span class="line"><span class="built_in">half4</span>(worldTangent.x, worldBinormal.x, worldNormal.x, <span class="number">0.0</span>),</span><br><span class="line"><span class="built_in">half4</span>(worldTangent.y, worldBinormal.y, worldNormal.y, <span class="number">0.0</span>),</span><br><span class="line"><span class="built_in">half4</span>(worldTangent.z, worldBinormal.z, worldNormal.z, <span class="number">0.0</span>),</span><br><span class="line">    <span class="built_in">half4</span>(<span class="number">0.0</span>, <span class="number">0.0</span>, <span class="number">0.0</span>, <span class="number">1.0</span>)</span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>世界空间到切线空间的变换矩阵就是上面矩阵的转置矩阵(将切线、副切线和法线按行摆放)，如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">float3x3 worldToTangentMatrix = <span class="built_in">float3x3</span>(</span><br><span class="line">worldTangent, worldBinormal, worldNormal.z</span><br><span class="line">);</span><br></pre></td></tr></table></figure><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li>《Unity Shader 入门精要》</li></ul>]]>
    </content>
    <id>https://lujun.pages.dev/2019/04/04/unity_shader_tangent_space_rotation/</id>
    <link href="https://lujun.pages.dev/2019/04/04/unity_shader_tangent_space_rotation/"/>
    <published>2019-04-04T21:12:04.000Z</published>
    <summary>
      <![CDATA[<p>我们知道 <code>TANGENTA_SPACE_ROTATION</code> 是内建在 <code>UnityCG.cginc</code> 中的一个宏，作用是实现从模型空间到切线空间的变换。那么究竟具体是怎么变换的了？</p>]]>
    </summary>
    <title>谈谈 TANGENTA_SPACE_ROTATION</title>
    <updated>2026-06-22T14:03:58.807Z</updated>
  </entry>
  <entry>
    <author>
      <name>lujun</name>
    </author>
    <category term="Unity" scheme="https://lujun.pages.dev/tags/Unity/"/>
    <category term="Shader" scheme="https://lujun.pages.dev/tags/Shader/"/>
    <content>
      <![CDATA[<script src="/assets/js/APlayer.min.js"> </script><p>Unity 表面着色器为开发者提供了一层简单快速的编写 Shader 的方式，对开发者来说隐藏了光照模型这些复杂的的概念，但是有时候 Unity 自带的光照模型往往不能满足我们的需求，而需要自己定义光照模型。所以接下来就一起看看 Unity 中常见的光照模型函数。</p><p>光照模型是一个用许多光照属性(反射率、高光等)来计算每个像素点上最终的着色函数。光照模型有很多种类，比如早期游戏引擎中的标准光照模型，以及后来的 <a href="https://en.wikipedia.org/wiki/Bidirectional_reflectance_distribution_function">BRDF 光照模型</a>，还有<a href="https://zhuanlan.zhihu.com/p/21376124">基于物理的 BRDF 模型</a>等等。</p><span id="more"></span><h2 id="Lambert-光照模型"><a href="#Lambert-光照模型" class="headerlink" title="Lambert 光照模型"></a>Lambert 光照模型</h2><p>粗糙的物体表面向各个方向等强度地反射光的现象叫作漫反射，产生这种现象的表面体称为理想漫反射体(Lambert 反射体)。</p><p>当场景中仅存在环境光(给予物体在每个顶点处的光照是一样的，如平行光)时，表面某点处的光强:</p><ul><li><p>$Iad &#x3D; kd \times Ia$</p></li><li><p>Ia 是环境光的强度</p></li><li><p>kd 为材质对环境光的反射系数(0 &lt; kd &lt; 1)</p></li></ul><p>当场景中仅有方向光(入射角度不同光线方向也不同，比如聚光灯)的存在，表面某点处的光强：</p><ul><li><p>$Iad &#x3D; kd \times Il \times cosθ$</p></li><li><p>Il 是方向光的强度</p></li><li><p>kd 为材质对环境光的反射系数(0 &lt; kd &lt; 1)</p></li><li><p>θ 是入射光方向和顶点法线的夹角。当夹角为 0°，说明入射光平行于法线(垂直于表面)，此时反射强度最大；当夹角为 90° 时，说明入射光同表面顶点切线平行，此时物体不会反射任何光线。</p></li></ul><p><strong>cosθ 等价于顶点单位法向量 N 与从顶点指向光源的单位向量 L 的点积，所以有 $Iad &#x3D; kd \times Il \times (N \cdot L)$</strong></p><p>当场景中两种光线都存在时，表面某点处的光强：</p><p>$Idiff &#x3D; kd \times Ia + kd \times Il \times (N·L)$</p><p>接着我们来看看 Unity 中的 Lambert 光照模型的源码:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">inline</span> fixed4 <span class="title">UnityLambertLight</span> <span class="params">(SurfaceOutput s, UnityLight light)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    fixed diff = <span class="built_in">max</span> (<span class="number">0</span>, <span class="built_in">dot</span> (s.Normal, light.dir));</span><br><span class="line"></span><br><span class="line">    fixed4 c;</span><br><span class="line">    c.rgb = s.Albedo * light.color * diff;</span><br><span class="line">    c.a = s.Alpha;</span><br><span class="line">    <span class="keyword">return</span> c;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看出最终的颜色输出的计算就是根据某点本来的颜色值和当前场景中的光强度以及(N·L)的积，其中 N 和 L 的点积只取了大于 0 的部分，因为当值为负时此时光照方向是表面的背面，对于不透明的物体来说不会被渲染。</p><h2 id="Half-Lambert-光照模型"><a href="#Half-Lambert-光照模型" class="headerlink" title="Half Lambert 光照模型"></a>Half Lambert 光照模型</h2><p>Half Lambert 用来给在比较暗的区域显示物体。实现也很简单，代码如下：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">inline</span> half4 <span class="title">LightingCustomLambert</span><span class="params">(SurfaceOutput s, half3 lightDir, half atten)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="comment">// Lambert</span></span><br><span class="line">    half diffLight = <span class="built_in">dot</span>(s.Normal, lightDir);</span><br><span class="line">    <span class="comment">// half Lambert</span></span><br><span class="line">    diffLight = diffLight * <span class="number">0.5</span> + <span class="number">0.5</span>;</span><br><span class="line"></span><br><span class="line">    half4 c;</span><br><span class="line">    c.rgb = s.Albedo * _LightColor<span class="number">0.</span>rgb * (diffLight * atten * <span class="number">1</span>);</span><br><span class="line">    c.a = s.Alpha;</span><br><span class="line">    <span class="keyword">return</span> c;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看出在 Lambert 的基础上，通过 $diffLight &#x3D; diffLight \times 0.5 + 0.5$，使得 diffLight 变大了从而增强了在光线暗的区域的视觉效果。</p><h2 id="Phong-光照模型"><a href="#Phong-光照模型" class="headerlink" title="Phong 光照模型"></a>Phong 光照模型</h2><p>物理世界的某些物体看起来并不会像 Lambert 光照模型描述的那样简单。比如光滑的金属表面被光照射时，某一点处会有很强的反射光(肉眼可见的光斑)。这是因为在接近镜面反射角的某个区域内，反射了入射光的大部分光强，这种现象称作镜面反射。</p><p>在镜面反射中相对于漫反射增加了一个观察角度，Phong 光照模型就是基于光照的方向和用户的视角方向进行计算的。</p><p>Phong 光照模型表面某点处的镜面反射光强：</p><ul><li><p>$Is &#x3D; ks \times Il \times (R \cdot V) ^ p$</p></li><li><p>ks 是材质的镜面反射系数</p></li><li><p>Il 是光强</p></li><li><p>R 为反射光的方向</p></li><li><p>V 表示从顶点到视点的方向</p></li><li><p>p 是高光指数，p 越大反射越集中，当慢慢视线方向偏离反射方向光线开始慢慢衰减，反之 p 越小观察到的光斑区域也就越小，反射光强度也很弱。</p></li></ul><p>在上面的数学模型中，R 应该是我们最陌生的变量。如何求得反射光方向 R?</p><p>$R + L &#x3D; 2 \times N \times (N \cdot L)$ &#x3D;&gt; $R &#x3D; 2 \times N \times (N \cdot L) - L$</p><p>$Is &#x3D; ks \times Il \times ((2 \times N \times (N \cdot L) - L) \cdot V) ^ p$</p><p>因为在 Phong 光照模型中，我们需要视角方向作为一个输入参数来计算最终的输出值，所以在自定义光照模型函数时，如果需要用到镜面反射，那么我们需要在函数的输入参数中增加视角方向 viewDir，如下:</p><p><code>inline fixed4 PhongLight (SurfaceOutput s, half3 viewDir, UnityLight light)</code></p><p>Phong 光照模型时假设反射型表面的最终光照强度取决于两个因素：漫反射颜色和光线的反射值，所以有：</p><ul><li><p>$I &#x3D; D + S$</p></li><li><p>D 是根据 Lambert 计算的漫反射部分</p></li><li><p>S 就是上面我们介绍的镜面反射部分</p></li></ul><h2 id="BlinnPhong-光照模型"><a href="#BlinnPhong-光照模型" class="headerlink" title="BlinnPhong 光照模型"></a>BlinnPhong 光照模型</h2><p>Blinn 是一种高效计算和模拟高光的方式，它是通过视角方向和光线方向构成的半角向量完成的。BlinnPhong 光照模型则是混合和了 Lambert 的漫反射和 Blinn 计算高光的模式，渲染有时比 Phong 高光更柔和、更平滑，此外它的处理速度相当快。</p><p>上面说到，在 BlinnPhong 中我们使用视角方向和光线方向构成的半角向量来计算高光。让我们来看看 BlinnPhong 光照模型表面某点处的光强计算公式：</p><ul><li><p>$Ibp &#x3D; ks \times Il \times (N \cdot H) ^ p$</p></li><li><p>ks 是材质的镜面反射系数</p></li><li><p>Il 是光强</p></li><li><p>N 为入射点的单位法向量</p></li><li><p>H 表示光线方向和视角方向的半角向量</p></li><li><p>p 是高光指数，p 越大反射越集中，当慢慢视线方向偏离反射方向光线开始慢慢衰减，反之 p 越小观察到的光斑区域也就越小，反射光强度也很弱。</p></li></ul><p>这里的 H 该怎么计算? 很简单，这里的半角向量 H 就是视角方向 V 和光线方向 L 叠加之后归一化的值：</p><p><code>H = normalize(V, L)</code></p><img src="https://raw.githubusercontent.com/whilu/lujun.co-storge/master/image/blinnphongpic1.jpeg" alt="blinnphongpic1.jpeg" title="blinnphongpic1.jpeg" width="429" height="302" /><p>与 Phong 光照模型比较，BlinnPhong 光照模型计算大致一样，不过由于引入了光线方向和视角方向的半角向量来计算使得不用计算反射光，使得计算更加简单快速，使用视角方向和光线方向构成的半角向量来模拟反射向量，事实上这中方式比 Phong 光照模型中在物理上更加精准。</p><p>同样，最后我们也来看看 Unity 中的 BlinnPhong 光照模型的代码:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">inline</span> fixed4 <span class="title">UnityBlinnPhongLight</span> <span class="params">(SurfaceOutput s, half3 viewDir, UnityLight light)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    half3 h = <span class="built_in">normalize</span> (light.dir + viewDir);</span><br><span class="line"></span><br><span class="line">    fixed diff = <span class="built_in">max</span> (<span class="number">0</span>, <span class="built_in">dot</span> (s.Normal, light.dir));</span><br><span class="line"></span><br><span class="line">    <span class="type">float</span> nh = <span class="built_in">max</span> (<span class="number">0</span>, <span class="built_in">dot</span> (s.Normal, h));</span><br><span class="line">    <span class="type">float</span> spec = <span class="built_in">pow</span> (nh, s.Specular*<span class="number">128.0</span>) * s.Gloss;</span><br><span class="line"></span><br><span class="line">    fixed4 c;</span><br><span class="line">    c.rgb = s.Albedo * light.color * diff + light.color * _SpecColor.rgb * spec;</span><br><span class="line">    c.a = s.Alpha;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> c;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面的代码中首先计算了光线方向和视角方向的半角向量 h，接着计算了 Lambert 光照模型中计算光强的乘法因子 diff，然后又计算了法向量和 h 的点积 nh，最后通过指数计算得到了高光乘法因子 spec，最终输出就是 Lambert 光照模型得到的漫反射值以及 BlinnPhong 光照模型得到的高光反射值的和。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><p>《Unity 5.x Shaders and Effects Cookbook》</p></li><li><p><a href="https://blog.csdn.net/taoqilin/article/details/52800702">https://blog.csdn.net/taoqilin/article/details/52800702</a></p></li></ul>]]>
    </content>
    <id>https://lujun.pages.dev/2019/03/16/unity_light_model/</id>
    <link href="https://lujun.pages.dev/2019/03/16/unity_light_model/"/>
    <published>2019-03-16T22:25:44.000Z</published>
    <summary>
      <![CDATA[<p>Unity 表面着色器为开发者提供了一层简单快速的编写 Shader 的方式，对开发者来说隐藏了光照模型这些复杂的的概念，但是有时候 Unity 自带的光照模型往往不能满足我们的需求，而需要自己定义光照模型。所以接下来就一起看看 Unity 中常见的光照模型函数。</p>
<p>光照模型是一个用许多光照属性(反射率、高光等)来计算每个像素点上最终的着色函数。光照模型有很多种类，比如早期游戏引擎中的标准光照模型，以及后来的 <a href="https://en.wikipedia.org/wiki/Bidirectional_reflectance_distribution_function">BRDF 光照模型</a>，还有<a href="https://zhuanlan.zhihu.com/p/21376124">基于物理的 BRDF 模型</a>等等。</p>]]>
    </summary>
    <title>Unity Shader 常见光照模型</title>
    <updated>2026-06-22T14:03:58.804Z</updated>
  </entry>
</feed>
