<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[[Unity]时间控制插件Chronos的基本使用与原理分析]]></title><description><![CDATA[<p dir="auto">时间控制在游戏中是一类常见的功能，例如菜单里的暂停、倍速，再如《武士 零》中的慢动作、倒带等时间系能力。最近初步尝试了一款时间控制插件Chronos，网上相关的中文资料比较少，不知道会不会踩坑，总之记录一下使用笔记，并结合源码对其原理做些简单的分析。</p>
<p dir="auto"><img src="https://s2.loli.net/2023/03/01/UB9Pdm1w6Zq4JeA.gif" alt class=" img-responsive img-markdown" /><br />
<img src="https://s2.loli.net/2023/03/01/MbYh1u4HwTiA7eQ.gif" alt class=" img-responsive img-markdown" /><br />
<img src="https://s2.loli.net/2023/03/01/uabTm4dgkvP3INW.gif" alt class=" img-responsive img-markdown" /></p>
<p dir="auto">与《武士 零》中可以预知未来、操控时间的药物“柯罗诺斯”一样，这款插件也以古希腊神话中的时间神命名，通过它可以控制游戏中的时间流速，实现倍速、暂停、时光倒流，同时它还提供针对单个物体、一组物体和指定区域内的时间控制。</p>
<p dir="auto">从插件描述可以看出，虽然不能预知未来，但只要不是太复杂的时间控制功能，它基本都可以实现。</p>
<p dir="auto">插件从2020年6月开始永久免费，在Asset Store下载并在Unity中导入即可：<br />
<a href="https://assetstore.unity.com/packages/tools/particles-effects/chronos-31225" rel="nofollow ugc">https://assetstore.unity.com/packages/tools/particles-effects/chronos-31225</a></p>
<h1>设计理念</h1>
<p dir="auto">在使用之前，先来了解它的设计理念。假设现在要做一个正经的塔防游戏“今夜圆饼”，游戏有如下要求：</p>
<ol>
<li>游戏时间与用户界面时间互不影响，游戏可以暂停、倍速，而用户界面始终保持正常速度。</li>
<li>每类物体（敌人、防御塔、玩家角色）的时间统一受游戏时间影响并且可以单独调整，它们之间互不影响。</li>
<li>每个物体的时间也可以单独调整，比如某种环境效果，让区域内的敌我单位加/减速。</li>
<li>时间流速改变时，动画、粒子效果等的速度要一同变化。</li>
</ol>
<p dir="auto"><img src="https://s2.loli.net/2023/03/03/XumiNExRdJPy9Vl.png" alt class=" img-responsive img-markdown" /><br />
<img src="https://s2.loli.net/2023/03/03/ElZhtU6DYsoXMQ2.png" alt class=" img-responsive img-markdown" /></p>
<p dir="auto">针对上述需求，Chronos给出了这样的结构：</p>
<p dir="auto"><img src="https://s2.loli.net/2023/03/04/MUyJgrCFip9c768.png" alt class=" img-responsive img-markdown" /></p>
<p dir="auto"><strong>Timekeeper</strong></p>
<p dir="auto">作为根节点，管理场景中的所有时钟，每个场景中只需要一个Timekeeper，是单例模式。</p>
<p dir="auto"><strong>Global Clock</strong></p>
<p dir="auto">管理一组物体的时间，根节点下分为了Root与Interface两个时钟，Root用来管理游戏时间，Interface用来管理用户界面时间。Root时钟下又有Enemies、Turrets和Player时钟，Root时钟的流速改变时，这三个时钟都会随之改变。</p>
<p dir="auto"><strong>Local Clock</strong></p>
<p dir="auto">与Global Clock类似，Local Clock用来管理单个物体的时间，对于玩家角色只存在一个的情况，Local Clock更为合适。</p>
<p dir="auto"><strong>Timeline</strong></p>
<p dir="auto">可以理解为时钟的具体实施者，Timeline组件挂在各个游戏物体上，改变它们的时间流速。</p>
<p dir="auto"><strong>Area Clock</strong></p>
<p dir="auto">改变区域内物体的时间流速，比如宣传图里的时间结界。</p>
<p dir="auto">以上就是Chronos的核心组件，使用时添加好对应的组件就行了，比如这里的正经塔防游戏引入Chronos的步骤：</p>
<ol>
<li>创建一个空物体，添加Timekeeper组件。<br />
<img src="https://s2.loli.net/2023/03/03/HOs6fVbFjWzBv1y.png" alt class=" img-responsive img-markdown" /></li>
<li>在同一物体上继续添加Global Clock组件，设置好它们的Key与父子关系。<br />
<img src="https://s2.loli.net/2023/03/03/lXSg3YuTjrvLdE9.png" alt class=" img-responsive img-markdown" /></li>
<li>如果有玩家角色（萝卜之类的），在玩家物体上添加Local Clock组件。</li>
<li>给防御塔、敌人、玩家角色物体添加Timeline组件，设置好对应的时钟。<br />
<img src="https://s2.loli.net/2023/03/03/pbvuzaEAXNZOTQj.png" alt class=" img-responsive img-markdown" /></li>
<li>在脚本中获取对应的时钟，改变它的timeScale来调整时间流速。</li>
</ol>
<pre><code class="language-C#">clock.localTimeScale = value;
</code></pre>
<p dir="auto">运行效果（并不是塔防）：</p>
<p dir="auto"><img src="https://s2.loli.net/2023/03/01/RuzoM6h3DyWqT24.gif" alt class=" img-responsive img-markdown" /><br />
<img src="https://s2.loli.net/2023/03/01/4JDy5AeXQ2N6anh.gif" alt class=" img-responsive img-markdown" /><br />
<img src="https://s2.loli.net/2023/03/01/T5MxguraRD4ZwBv.gif" alt class=" img-responsive img-markdown" /></p>
<p dir="auto">这样就搞定了，是不是很简单，那么本篇笔记就到这里，最后祝您身体健康，再见。</p>
<h1>深入使用</h1>
<p dir="auto">上面是<a href="https://ludiq.io/chronos/tutorial" rel="nofollow ugc">官方教程</a>中介绍的使用步骤，在项目中引入这个插件确实很简单，但个人更关心这些问题：</p>
<ol>
<li>插件的作用范围，它支持哪些组件，如何配合使用，有哪些限制。</li>
<li>对于自己的脚本以及不支持的组件，如何进行扩展。</li>
<li>插件的实现原理，性能如何。</li>
</ol>
<h2>作用范围</h2>
<p dir="auto">从自带的示例可以看出，Chronos支持的Unity组件有Rigidbody、Animator、Nav Mesh Agent、Particle System、Audio Source等，在源码中可以看到它已适配的组件：</p>
<p dir="auto"><img src="https://s2.loli.net/2023/03/03/UYKCuFqzVMLO4ga.png" alt class=" img-responsive img-markdown" /></p>
<h2>扩展</h2>
<h3>Timeline</h3>
<p dir="auto">在自己的脚本中引入时间控制，最基础的方法就是使用Timeline代替Unity的Time，比如原来使用Time.deltaTime，改为Timeline.deltaTime：</p>
<pre><code class="language-C#">private void Start()
{
    timeline = GetComponent&lt;Timeline&gt;();
}

private void Update()
{
    if (targetingCounter &gt; 0)
    {
        // targetingCounter -= Time.deltaTime;
        targetingCounter -= timeline.deltaTime;
        ...
    }
}
</code></pre>
<p dir="auto">如果用到了内置组件，同样用Timeline中的对应组件代替，比如rigidbody改为Timeline.rigidbody：</p>
<pre><code class="language-C#">// rb.velocity = targetDirection * targetSpeed;
timeline.rigidbody.velocity = targetDirection * targetSpeed;
</code></pre>
<p dir="auto">完整的替换表：</p>
<p dir="auto"><a href="https://ludiq.io/chronos/manual/migration" rel="nofollow ugc">https://ludiq.io/chronos/manual/migration</a></p>
<h3>Occurrence</h3>
<p dir="auto">不远处有个怪物生成点（比如刷怪笼）正在不停地刷怪，一大波僵尸正向我袭来，然后我灵机一动将敌方的时间流速改为了负二倍速，刷怪笼和一大波僵尸的时间都被逆转，它们回到了原来的位置——但仅限于此，僵尸并不会随着时光倒流而消失。</p>
<p dir="auto">为什么呢？Chronos会按设置的时间间隔，不断记录物体的各种信息，在时光倒流时进行回溯，但它并不会记录什么时候物体被生成，什么时候需要被销毁，这部分工作需要我们自己完成。</p>
<p dir="auto">为此Chronos提供了一个叫Occurrence的工具，通过Timeline调用，结构长这样：</p>
<pre><code class="language-C#">timeline.Do
(
    true, // 是否可重复执行
    delegate() // 前向操作
    {
        // 生成物体并返回它
    },
    delegate(object transfer) // 逆向操作
    {
        // 销毁对应的物体
    } 
);
</code></pre>
<p dir="auto">有点电影《信条》的感觉，这里前向操作与逆向操作是成对的，时间正常流转时执行前向操作，时间倒流时执行对应的逆向操作。</p>
<p dir="auto">使用示例：</p>
<pre><code class="language-C#">private void Start()
{
    timeline = GetComponent&lt;Timeline&gt;();
    StartCoroutine(Spawn());
}
private IEnumerator Spawn()
{
    while (true)
    {
        timeline.Do(
            true, // 允许重复执行
            () =&gt;
            {
                // 前向操作
                if (num &gt;= maxNum)
                    return null;
                var go = Instantiate(prefabs[Random.Range(0, prefabs.Length)]);
                go.transform.position = spawnPoint.position;
                go.transform.rotation = spawnPoint.rotation;
                num++;
                return go;
            },
            (gameObject) =&gt;
            {
                // 逆向操作
                if (gameObject != null)
                {
                    Destroy(gameObject);
                    num--;
                }
            });
        yield return new WaitForSeconds(spawnInterval);
    }
}
</code></pre>
<p dir="auto">使用Timeline与Occurrence，可以满足一些简单的需求，对于更复杂的情况，比如物体数值与状态的记录与回溯，则可能需要做一套完整的适配。目前个人项目需求比较简单，暂时没到这一步，之后如果有做相关的扩展再来补充。</p>
<h2>一些坑</h2>
<p dir="auto">Chronos并不是万能的，官网列出了它的限制：</p>
<p dir="auto"><a href="https://ludiq.io/chronos/manual/limitations" rel="nofollow ugc">https://ludiq.io/chronos/manual/limitations</a></p>
<p dir="auto">如果粒子系统需要支持时间回溯，其中的限制可能影响较大：</p>
<ul>
<li>低速(小于0.25倍速)时粒子系统可能会卡顿。</li>
<li>粒子系统的模拟空间只能是本地。</li>
<li>不支持粒子的碰撞检测。</li>
</ul>
<p dir="auto">这些问题主要是由粒子系统的Simulate方法引起的，后面的原理分析中会提到。可以说大部分限制的原因都来自引擎底层，看了下插件的最后更新时间，这些问题多半是不会修复了。</p>
<p dir="auto">除了上面提到的限制，个人在使用过程中也遇到了一些问题：</p>
<ol>
<li>粒子系统如果勾选了Play On Awake，运行时会报错：</li>
</ol>
<p dir="auto"><img src="https://s2.loli.net/2023/03/03/TZY6i7Dk5RryVxb.png" alt class=" img-responsive img-markdown" /></p>
<p dir="auto">这是由于Chronos在初始化时会将粒子系统的随机种子改为固定的值，而此时粒子已经开始播放了，Unity不支持这个操作所以报错。可以取消勾选Play On Awake，初始化完后再调用播放，注意是通过timeline的particleSystem来播放，不能直接调用。</p>
<pre><code class="language-C#">timeline.particleSystem.Play();
</code></pre>
<ol start="2">
<li>
<p dir="auto">并不能直接影响Shader中的时间速度，需要另外适配。</p>
</li>
<li>
<p dir="auto">如果有时间回溯功能，在倒带时需要注意屏蔽玩家控制，避免引起冲突，比如角色控制脚本中，仅在timeScale为正时开启玩家控制。</p>
</li>
</ol>
<h1>原理分析</h1>
<p dir="auto">只是粗略看了一下源码，可能会有一些分析得不对的地方。</p>
<p dir="auto"><img src="https://s2.loli.net/2023/03/03/Y1SDW5iKyeoLtcn.jpg" alt class=" img-responsive img-markdown" /></p>
<p dir="auto">Timekeeper、Global Clock的树形结构以及对Timeline的管理比较好理解，这部分就不看了，更值得关注的是Timeline是如何控制组件的时间流速的，这里从Timeline的源码开始阅读。</p>
<p dir="auto">在Timeline的父类TimelineEffector中，定义了一堆它已适配的组件类，每个类与Unity的内置组件相对应：</p>
<p dir="auto"><img src="https://s2.loli.net/2023/03/03/297RAiXSTjHsr3I.png" alt class=" img-responsive img-markdown" /></p>
<p dir="auto">这些XXXTimeline均继承自ComponentTimeline类，实现IComponentTimeline接口，虽然这里也命名为组件，但它们不继承Unity的Component，仅持有对应Unity组件的引用，可以看作是Unity组件的一层包装。</p>
<p dir="auto">在Awake中，调用CacheComponents方法，获取当前物体上挂载的Unity组件，将其包装成对应的Timeline组件，初始化并存入components列表中：</p>
<p dir="auto"><img src="https://s2.loli.net/2023/03/03/5JKRenEDpmxCQ6v.png" alt class=" img-responsive img-markdown" /></p>
<p dir="auto">注意这里的注释，如果运行过程中添加或移除了物体上相关的Unity组件，则需要重新调用一次这个方法。</p>
<p dir="auto">Timeline组件的初始化方法Initialize中只有一个CopyProperties的调用：</p>
<p dir="auto"><img src="https://s2.loli.net/2023/03/03/LJKXSTEwbDq4mBt.png" alt class=" img-responsive img-markdown" /></p>
<p dir="auto">CopyProperties的实现因组件而异，通常其中会记录一些与时间相关的参数，例如Animator组件记录播放速度、Audio Source组件记录音高。</p>
<p dir="auto">初始化中针对一些组件有特殊的处理逻辑，比如Rigidbody与Transform，具体可以看源码，这里就不过多介绍了。</p>
<p dir="auto">Start或OnEnable中，Timeline将应用对应时钟的时间流速timescale，并调用所有组件的AdjustProperties方法：</p>
<p dir="auto"><img src="https://s2.loli.net/2023/03/03/5peuhJaM6Vnj821.png" alt class=" img-responsive img-markdown" /></p>
<p dir="auto">AdjustProperties中应用时间流速，例如Animator组件调整播放速度、Nav Mesh Agent组件调整移动速度与转向速度、粒子系统调整simulationSpeed等等。</p>
<p dir="auto">常用的事件函数中调用所有组件的对应事件函数：</p>
<p dir="auto"><img src="https://s2.loli.net/2023/03/03/NUq5mywMK7hsGjb.png" alt class=" img-responsive img-markdown" /></p>
<p dir="auto">对大部分内置组件来说，光是调整速度还无法做到时间倒流的效果，Chronos对不同的组件采用了不同的解决方法。</p>
<p dir="auto"><strong>Animator</strong></p>
<p dir="auto">Animator是相对简单的一个，将它的播放速度设置为负数就可以倒放了。在时间正常流转时，调用Animator的StartRecording录制，在时间倒流时，倒放之前的录制结果。</p>
<p dir="auto"><strong>Transform、Rigidbody</strong></p>
<p dir="auto">对于Transform，Chronos用了一个自定义的RecorderTimeline组件，RigidbodyTimeline组件同样继承于它。时间正常流转时，按设置的录制间隔将物体的位置、旋转等信息(缩放默认被注释了，需要可以自己打开)录制成Snapshot并缓存起来，在时间倒流时逐个应用这些Snapshot。</p>
<p dir="auto"><img src="https://s2.loli.net/2023/03/03/cZBtJEikbeG6AoH.png" alt class=" img-responsive img-markdown" /></p>
<p dir="auto"><strong>Particle System</strong></p>
<p dir="auto">根据是否要支持时间回溯，Chronos将粒子系统组件分为两个，NonRewindableParticleSystemTimeline与RewindableParticleSystemTimeline。</p>
<p dir="auto">不可回溯的实现很简单，根据Timeline的timeScale调整粒子系统的simulationSpeed即可；</p>
<p dir="auto">可回溯的粒子系统是通过<a href="https://docs.unity3d.com/2020.3/Documentation/ScriptReference/ParticleSystem.Simulate.html" rel="nofollow ugc">Simulate</a>方法来实现的，Simulate方法可以让粒子系统立即达到到指定时间点的状态。时间正常流转时，记录粒子系统的播放状态（启用、禁用、播放、暂停），在倒带时还原这些播放状态。</p>
<p dir="auto">Simulate也带来了上面提到的问题：</p>
<ol>
<li>低速卡顿，<a href="https://fogbugz.unity3d.com/default.asp?694191_dso514lin4rf5vbg" rel="nofollow ugc">这个问题</a>Unity从2015年到现在都没修复，但个人测试感觉不太明显，处于可接受的范围。</li>
<li>不断调用导致模拟空间不断更新，所以粒子的模拟空间仅限本地。</li>
<li>不支持粒子的碰撞检测。</li>
</ol>
<p dir="auto"><strong>总结</strong></p>
<p dir="auto">从源码中可以得知，Chronos初始化时需要对游戏物体上的Unity组件做一层包装，常用的事件函数（Start、OnEnable、FixedUpdate、Update、OnDisable）中会遍历所有包装组件并调用相关方法，在Timeline的Update中还包含对Occurrence的处理等等。如果要支持时间回溯，在游戏运行时需要对某些组件的状态进行录制，不同组件占用的内存空间不同。</p>
<p dir="auto">如果自己的脚本需要完全接入Chronos，可以像上面哪些组件一样，继承ComponentTimeline，并加入到Timeline的初始化过程中。</p>
<p dir="auto">对于各组件中的录制功能，Timeline提供了统一的参数配置，可以调整录制间隔与录制的最大时长，并会给出预计消耗的内存：</p>
<p dir="auto"><img src="https://s2.loli.net/2023/03/03/i3PWmwdHfD8OYvF.png" alt class=" img-responsive img-markdown" /></p>
<p dir="auto">如果游戏不需要时间回溯功能，那么可以取消勾选Rewindable以节省性能。</p>
<p dir="auto">具体的性能测试还没有做，大概率鸽了。</p>
<p dir="auto"><img src="https://s2.loli.net/2023/03/04/9FWgYvf5ExtzmjH.jpg" alt class=" img-responsive img-markdown" /></p>
]]></description><link>http://designhub.top/topic/29/unity-时间控制插件chronos的基本使用与原理分析</link><generator>RSS for Node</generator><lastBuildDate>Tue, 08 Sep 2026 05:48:23 GMT</lastBuildDate><atom:link href="http://designhub.top/topic/29.rss" rel="self" type="application/rss+xml"/><pubDate>Fri, 24 Mar 2023 05:29:28 GMT</pubDate><ttl>60</ttl></channel></rss>