├── LICENSE
├── README.md
├── Unity_025
├── .gitignore
├── Timeline结构及其源码浅析.md
├── code
│ ├── TimelineTestForAnimationScriptBehaviour.cs
│ ├── TimelineTestForAnimationStream.cs
│ ├── TimelineTestForAnimationWeightInfo.cs
│ ├── TimelineTestForCreateGraph.cs
│ ├── TimelineTestForEvaluateAndProcessFrame.cs
│ ├── TimelineTestForIntervalTreeDemo.cs
│ ├── TimelineTestForLifeCycle.cs
│ ├── TimelineTestForOutputEvaluate.cs
│ └── TimelineTestForScriptAnimation.cs
├── image
│ ├── Add the Editor to the script.png
│ ├── AnimationPlayable weight processor1.png
│ ├── AnimationPlayable weight processor2.png
│ ├── AnimationStream.png
│ ├── Basic Playable UML Class Diagram.png
│ ├── Complete life cycle.png
│ ├── Directly display subattributes.png
│ ├── Example of the graph for lifecycle validation.png
│ ├── ExposedReference error.png
│ ├── ExposedReference example.png
│ ├── Full Timeline Process.png
│ ├── IntervalTree UML class diagram.png
│ ├── IntervalTree internal struacture example.png
│ ├── Passthrough source code comment.png
│ ├── Pasted image 20250526141105.png
│ ├── Pasted image 20250526160501.png
│ ├── Pasted image 20250526173205.png
│ ├── Pasted image 20250526173215.png
│ ├── Pasted image 20250526173228.png
│ ├── Pasted image 20250527021310.png
│ ├── Pasted image 20250527021317.png
│ ├── Pasted image 20250527021327.png
│ ├── Pasted image 20250527022427.png
│ ├── Pasted image 20250527022435.png
│ ├── Playable Connect.png
│ ├── Playable Director window.png
│ ├── Playable Graph Monitor.png
│ ├── Playable inner structure.png
│ ├── PlayableAsset Instantiate by guid.png
│ ├── PlayableAsset class uml.png
│ ├── PlayableBehaviour UML Class Diagram.jpg
│ ├── PlayableDirector.png
│ ├── PlayableGraph UML graph.png
│ ├── PlayableOutput Internal structure.png
│ ├── PlayableOutput UML diagram.png
│ ├── Runtime method call chain.png
│ ├── RuntimeClip internal structure.png
│ ├── SceneBindings in PlayableDirector.png
│ ├── SourceOutputPort meaning.png
│ ├── Subsequent data processing of AnimatorPlayableOutput.png
│ ├── Timeline Structure Comprehensive Explanation.png
│ ├── Timeline all track.png
│ ├── Timeline and Playable important components.png
│ ├── Timeline overall structure.png
│ ├── Timeline yaml file structure.png
│ ├── TimelineAsset structure.png
│ ├── Track contain Target.png
│ ├── TrackAsset need specific attributes.png
│ ├── attribute wrapped outter parameter.png
│ ├── create Timeline.png
│ ├── example_Timeline window create two track.png
│ ├── graph contain PlayableDirector.png
│ ├── invalid graph structure.png
│ ├── mix mode data flow.png
│ ├── open Timeline window.png
│ ├── passthrough example.png
│ ├── passthrough mode data flow.png
│ ├── passthrough mode data flow2.png
│ ├── root track and output track.png
│ ├── set weight by mixin curve.png
│ ├── timeline Graph in runtime.png
│ ├── two steps of graph play.png
│ └── valid graph structure.png
└── 作者RingleaderWang.md
└── obsidian.png
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Ringleader
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # UnityBlog
2 | Unity笔记分享
3 |
--------------------------------------------------------------------------------
/Unity_025/.gitignore:
--------------------------------------------------------------------------------
1 | .obsidian/
2 | .trash/
3 | .idea/
4 |
--------------------------------------------------------------------------------
/Unity_025/Timeline结构及其源码浅析.md:
--------------------------------------------------------------------------------
1 | > 注:软件版本Unity 6.0 + Timeline 1.8.7
2 | > 作者:CSDN @ RingleaderWang
3 |
4 | # 综述
5 |
6 | Unity的Timeline本质是一个包含很多可播放片段(Playable Clip)的区间树(IntervalTree),这个区间树可以排序、搜索、以及控制所有 Clip的激活与停止,最后利用底层的Playable系统,运行所激活的Clips,就是这么个东西。
7 |
8 | (当然这么定义不够准确,IntervalTree不控制clip激活,只是排序和搜索clip。IntervalTree本质是装clip的容器,不考虑性能的话用数组也行。这么定义只是方便理解Timeline的底层结构罢了)
9 |
10 | Timeline的两个主要作用:
11 | - 充当**技能编辑器**(策划程序向)
12 | - 做cg动画/过场动画/**动画演出**(导演动画向)
13 |
14 | Timeline整体结构如下:
15 |
16 | 
17 |
18 | 包括底层的Playable系统,其在c++端,封闭黑盒,暴露部分API给c#端使用,Timeline就是利用这些API实现的功能。
19 |
20 | 详细来看,`Timeline`侧主要由`PlayableAsset`、`PlayableBinding`、`TrackAsset`、`PlayableDirector`等组成;
21 |
22 | Playable系统侧主要是`Playable`、`PlayableOutput`、`PlayableGraph`三部分组成。
23 |
24 | 
25 |
26 | 因为后面会频繁出现包含"`Playable`"的单词,所以需要辨析强调下
27 | - `Playable系统`:指底层c++端关于Playable的所有功能,包含playable/playableout节点创建与连接、playableGraph的构建运行销毁等。
28 | - `PlayableAPI`:指底层Playable系统暴露给c#端的接口方法。
29 | - `PlayableGraph`:控制Playable/PlayableOutput的创建、连接与运行
30 | - `Playable节点`:playableGraph中的数据生产与传递节点,包含叶子节点、mixer节点、layer节点等,可以进行数据处理与传递、权重混合等操作
31 | - `PlayableOutput`:playableGraph中的数据输出节点,将Playable节点传递的数据交由对应的`target`运行
32 | - **Playable**:不同语境含义不同,在PlayableGraph中指playable节点,讲源码时指playable节点对应的Playable这个类/结构体,timeline语境中一般指asset创建的对应playable类/结构体,放到整个unity一般指Playable API或Playable系统
33 | - **Timeline**:
34 | - `Timeline窗口`:编排可播放片段的窗口。
35 | - `Timeline文件`:Timeline窗口编辑后生成的文件,扩展名为”.playable“。
36 | - `TimelineAsset`:指Timeline这个文件资产,或者实例化后对应的类。
37 | - `TimelinePlayable`:由TimelineAsset类在运行时调用create方法生成对应的TimelinePlayable,构建playable节点树,并管理所有轨道中的clip。
38 |
39 | - `Playable Clip`:可播放片段,比如AnimationClip、AudioClip、Prefab等
40 | - `PlayableAsset`:跟Playable有关的静态资产文件或其实例化的类,运行时可以创建对应的Playable。包含TimelineAsset、TrackAsset、PlayableClipAsset等。
41 | - `TrackAsset`:特殊的PlayableAsset,Timeline窗口中一般直接称作track轨道,每个轨道可以放很多clip,这些clip就是PlayableClipAsset,运行时TrackAsset一般会创建MixerPlayable来混合PlayableClipAsset创建的Playable。
42 | - `PlayableBinding`:绑定track和最终运行数据的target,用于创建PlayableOutput
43 | - `PlayableDirector`:控制PlayableAsset,用于创建PlayableGraph来运行构建的timeline
44 |
45 | `Timeline`主要有三块要点:
46 | - **Timeline静态与运行态**,涉及:
47 | - TimelineAsset CreatePlayable 方法
48 | - TimelinePlayable Create方法、Compile方法、PrepareFrame方法
49 | - TrackAsset CreatePlayableGraph方法、CompileClips方法
50 | - **IntervalTree与RuntimeClip内部结构**,涉及:
51 | - TrackAsset CompileClips方法
52 | - IntervalTree Rebuild方法
53 | - **ScriptPlayable生命周期**,涉及IPlayableBehaviour接口
54 |
55 | 重点结构如下图所示:
56 |
57 | 
58 |
59 | Timeline的完整流程:
60 |
61 | 
62 |
63 | 接下来会详细分析整个系统。
64 | # Timeline 操作简介
65 |
66 | 1. 打开timeline窗口:Window>Sequencing>Timeline
67 | 
68 | 2. 创建Timeline:直接选择gameobject 点击Timeline窗口的Create创建,或者Project窗口右键Create>Timeline进行创建。(timeline文件扩展名为 playable,是静态资产)
69 | 
70 | 3. Timeline窗口create timeline,对象会自动创建PlayableDirector组件并绑定timeline。手动新建的话可以直接拖动timeline资产到对象,也能自动生成PlayableDirector组件。
71 | 
72 | 4. `PlayableDirector`用于为timeline创建对应的playable graph,可以激活`play On Awake`,那样组件awake后就能play(注意PlayableDirector组件awake远早于用户脚本awake,无法在用户脚本awake方法中控制PlayableDirector的`play on awake`参数)
73 | - `Bindings`会显示特定轨道所绑定的对象,用于指明当前Track控制的对象。
74 | - `Wrap Mode`指明Timeline播放完毕后的行为,none会直接stop timeline所在的graph(就是直接停止timeline),hold的话就是持续播放timeline最后一帧,Loop的话会从头循环播放。
75 | - `Update Method` 指明Timeline play如何控制,GameTime是跟随系统自动play,Unscaled Game Time就是无视系统时间倍速的自动play(游戏pause也会播放timeline),Manual就是由用户手动evaluate进行timeline play。DSP似乎跟音频有关,但官方说跟Unscaled Game Time差不多,暂不研究。
76 |
77 | 5. Timeline窗口右键添加轨道。
78 | 主要有如下几个轨道:
79 | - `Activation Track`:控制激活的轨道
80 | - `Animation Track`:控制动画播放的轨道
81 | - `Audio Track`:控制音频播放的轨道
82 | - `Control Track`:控制prefab、粒子系统或者子timeline显示或播放的轨道
83 | - `Signal Track`:控制帧事件的轨道
84 | - `Custom Track`,就是用户自己继承`TrackAsset`、`PlayableAsset`、`PlayableBehaviour`等来实现自定义playable。
85 | 
86 |
87 | 6. 创建好track和playable后就可以用PlayableDirector自动播放或手动Play()播放。
88 |
89 | 这里只是简单介绍下Timeline的用法,还有很多细节可以自己摸索或者去官方文档查看。
90 |
91 | 下面将进入本文的主题,深入讲解Timeline的结构,同时会浅析所涉及的Timeline和Playable源码。
92 |
93 | # Timeline 静态结构分析
94 |
95 | 后面我们会了解到,在运行时,`timeline`会生成树形结构的`PlayableGraph`,那谁指导它生成呢?就是`PlayableAsset`,这即是Timeline的**静态结构**。
96 |
97 | 
98 |
99 | 在Timeline窗口编辑资产后,形成**资产树**(也是**树形结构**的,**根节点**是`TimelinePlayable`,下一层是`root track/output track`,再下一层是 `PlayableAsset`)。
100 |
101 | `PlayableDirector`添加timeline资产,然后设置`binding`后 ,director就能管理整个timeline资产了。
102 |
103 | 等后面director组件初始化 ,并根据timeline资产树实例化为对应的`TimelineAsset`后,便能以有序生成各个`Playable`。
104 |
105 | 所以想理解运行时Timeline就必须先搞明白 静态Timeline的结构,并理清各PlayableAsset类的关系,以及clip和track、playTarget与track的关系(即Binding)。
106 | ## PlayableAsset 静态资产
107 |
108 | 在Timeline窗口创建两个Track(Activation Track、Audio Track),并分别添加对应的clip。如下:
109 |
110 | 
111 |
112 | 
113 |
114 | 这个扩展名为 ".playable"的 timeline 文件,就是所谓的静态资产。
115 |
116 | timeline资产文件实例化后就是TimelineAsset类,TimelineAsset下一层是TrackAsset,TrackAsset包含数个PlayableAsset(比如AnimationPlayableAsset、AudioPlayableAsset等)。
117 |
118 | 结构图如下:
119 |
120 | 
121 |
122 | 对应的Timeline.playable yaml文件如下图所示:
123 |
124 | 
125 | 
126 |
127 | 实例化时,会根据这个`yaml`文件解析成 `PlayableAsset`节点链(利用PlayableAsset的`parent`、 `children`参数),**根节点(Root PlayableAsset)是TimelineAsset**,下一层是`Root Track`,对于`Group track`或者`AnimationTrack`允许其下还有`SubTrack`,每个Track都可以包含数个`PlayableAsset`。
128 | - `root PlayableAsset`:就是TimelineAsset,是所有`root track`的父级节点。
129 | - `root track`:就是TimelineAsset的直接子track,从timeline窗口看就是最外层track。
130 | - `output track`: 会生成`PlayableOutput`的track。除 GroupTrack、subGroupTrack、override track外的所有track 都是 output track。
131 |
132 | 
133 |
红框为root track,黄框为output track
134 |
135 | ## PlayableDirector
136 |
137 | 用Timeline窗口查看timeline资产有两种方式,产生的效果会不同:
138 | - 第一种是从Project窗口打开的独立timeline资产,它无法预览播放,track左侧也没有绑定对象;
139 | - 另一种就是从挂载`PlayableDirector`组件对象点开,那么它的timeline就是可预览播放的,track左侧也会显示`Bindings`。
140 |
141 | 第二种其实就是PlayableDirector组件做的绑定。
142 | 
143 | - `PlayableDirector`利用`key-value` 来实现绑定。
144 | - `key`就是`Track`对象(在`PlayableBinding`中被称作`sourceObject`).
145 | - `value` 就是目标对象(在`PlayableOutput`中被称作`target`),如ActivationTrack对应的就是待控制的预制体,AudioSourceTrack对应的就是Audio Source组件 ,AnimationTrack 对应的就是Animator。
146 |
147 | 
148 | PlayableDirector组件中SceneBindings值
149 |
150 | 这样,包含PlayableDirector的结构图就变成下图所示:
151 |
152 | 
153 | 包含PlayableDirector的TimelineAsset结构图
154 |
155 | ## PlayableAsset UML类图
156 |
157 | `TrackAsset` 和 `TimelineAsset` 其实都是继承自`PlayableAsset`,是一种特殊的`PlayableAsset`。
158 |
159 | **PlayableAsset UML类图**如下图所示:
160 |
161 | 
162 |
163 | - **TimelineAsset**:最特别的`PlayableAsset`,存储了所有trackAsset,包含一个`CreatePlayable`方法,指导运行时创建`TimelinePlayable`。
164 | - **ClipPlayableAsset**(蓝色) :表示实现`ITimelineClipAsset`接口的特殊`PlayableAsset`,具体类有`ActivationPlayableAsset`、`AudioPlayableAsset`、`AnimationPlayableAsset`、`ControlPlayableAsset`。这些PlayableAsset拥有一个`clipCaps`属性,表明这个片段在Timeline窗口拥有的能力,这些能力包含:
165 | - `Looping` 循环
166 | - `Extrapolation` 外推断,分Pre和post,表示两个clip中间空白区域的播放逻辑:
167 | - none 保持为开始帧
168 | - hold 保持为最后一帧
169 | - loop 循环(跳帧循环)
170 | - pingpong 镜像循环
171 | - continue 根据动画源文件的loop决定是连续循环还是保持为最后一帧
172 | - `ClipIn` 起始裁切
173 | - `SpeedMultiplier` 倍速
174 | - `blend` 混合
175 | - **PlayableAsset**都包含一个`CreatePlayable`方法,用于运行时创建对应的Playable。
176 | - `TrackAsset`(左下黄色) 拥有`CreateTrackMixer`方法,运行时也是创建Playable(重写CreateTrackMixer方法的话可以创建自定义MixerPlayable),可以接收多个input输入。
177 | - **TrackAsset** 的 `CompileClips`方法在运行时编译其所包含的所有clip,生成`RuntimeClip`并加入到`TimelinePlayable`的`IntervalTree`中,用于Timeline运行时标识哪个clip被激活。
178 | - 继承`TrackAsset`的类有GroupTrack、ActivationTrack、AudioTrack、ControlTrack、SignalTrack、AnimationTrack等,这与timeline窗口能添加的track一致。
179 | - **TrackAsset** 有两个特性(**Attribute**)
180 | - `TrackClipType`:表明Track能添加的clip类型,也就是上面继承playable且实现`ITimelineClipAsset`接口的类。
181 | - `TrackBindingType` :表明 `output Track`对应的`PlayableOutput` 其`target`参数类型,也即`PlayableBinding`的`outputTargetType`参数。
182 |
183 | ``` csharp
184 | [TrackClipType(typeof(AnimationPlayableAsset)]
185 | [TrackBindingType(typeof(Animator))]
186 | ```
187 |
188 | # Playable系统
189 |
190 | 上文介绍了Timeline的静态结构,为了更好地理解运行时Timeline,需要先了解底层的Playable系统。Playable 系统包括PlayableGraph、Playable和PlayableOutput。
191 |
192 | 下面依次介绍。
193 | ## PlayableGraph
194 |
195 | PlayableGraph管理Playable和PlayableOutput节点,控制整个节点树的运行。
196 | ### PlayableGraph 与 PlayableDirector 类
197 |
198 | Timeline 运行时会根据PlayableDirector的Playable参数创建PlayableGraph。
199 |
200 | 
201 |
202 | PlayableDirector可以用`PlayableDirector.playableGraph`属性获得创建的graph;
203 | PlayableGraph也可以用`PlayableGraph.GetResolver() as PlayableDirector` 解析出PlayableDirector。PlayableGraph的很多方法都可以直接在PlayableDirector中调用。
204 |
205 | PlayableGraph 和 PlayableDirector UML类图如下:
206 |
207 | 
208 |
209 | ### PlayableGraph的结构
210 |
211 | 对于前面的 timeline 资产:
212 |
213 | 
214 |
215 | 在运行时形成的Graph结构如下:
216 |
217 | 
218 |
219 | **PlayableGraph**的构建过程:
220 | - **PlayableDirector** `RebuildGraph`触发构建graph动作
221 | - 根据director的TimelineAsset 创建 `TimelinePlayable`
222 | - 然后依次 编译所有track和clip,创建对应的MixerPlayable、PlayableOutput和Playable。
223 | - **TimelinePlayable作为根节点**,下一层是MixerPlayable或LayerMixerPlayable,叶子节点是包含clip的Playable。
224 | - Playable之间是通过`Connect`连接,PlayableOutput通过`setSourcePlayable`与TimelinePlayable连接。
225 | - 编译clips期间会build `IntervalTree`,为后续graph play作准备。
226 |
227 | 大体过程图如下(下一章还会有更详细的步骤分析):
228 |
229 | 
230 |
231 | Timeline中关于graph的构建步骤很复杂,我们可以用如下案例了解其核心逻辑(注意一定要及时销毁graph):
232 |
233 | ``` csharp
234 | public AnimationClip clip;
235 | public AnimationClip clip2;
236 | private PlayableGraph graph;
237 |
238 | void DestoryGraph(PlayableGraph graph)
239 | {
240 | if (graph.IsValid())
241 | {
242 | graph.Destroy();
243 | }
244 | }
245 | private void CreateMixerGraph()
246 | {
247 | DestoryGraph(graph);
248 | // 1. 创建 PlayableGraph
249 | graph = PlayableGraph.Create("MixerPlayableGraph");
250 | // 2. 获取 Animator 并创建 AnimationPlayableOutput
251 | var animator = GetComponent();
252 | var animationOutput = AnimationPlayableOutput.Create(graph, "Animation", animator);
253 | // 3. 创建 AnimationClipPlayable
254 | var clipPlayable = AnimationClipPlayable.Create(graph, clip);
255 | var clipPlayable2 = AnimationClipPlayable.Create(graph, clip2);
256 | // 4. 创建一个 Mixer,管理多个动画输入
257 | AnimationMixerPlayable mixer = AnimationMixerPlayable.Create(graph, 2);
258 | mixer.ConnectInput(0, clipPlayable, 0,1);
259 | mixer.ConnectInput(1, clipPlayable2, 0,1);
260 | // 5. 设置 mixer 作为输出
261 | animationOutput.SetSourcePlayable(mixer);
262 | // 6. 播放图谱
263 | graph.Play();
264 | }
265 | public void OnDestroy()
266 | {
267 | DestoryGraph(graph);
268 | }
269 | ```
270 |
271 | 逻辑很简单,就4步:
272 | - 创建graph
273 | - 创建playable、mixerPlayable 并 connect
274 | - 创建playableOutput 并 setSourceOutput
275 | - 运行graph
276 |
277 | 使用[Playable Graph Monitor](https://openupm.com/packages/com.greenbamboogames.playablegraphmonitor/) 查看 整个graph如下。
278 | 
279 |
280 | 如果你使用官方的PlayableGraph Visualizer 的话,你会发现和 Playable Graph Monitor所展示的不一样。
281 |
282 | 比如还是这个例子:
283 |
284 | 
285 |
286 | PlayableGraph Visualizer展示:
287 | 
288 |
289 | Playable Graph Monitor展示:
290 | 
291 |
292 | 你可以发现区别很大,Visualizer从PlayableOutput往Playable构图,且两个PlayableOutput独立构图;而Monitor是从叶子Playable节点往PlayableOutput构图,两个PlayableOutput连在一个output端口上。
293 |
294 | 到底哪个对呢?
295 |
296 | 其实都对。Visualizer是从graph play角度构图的,Monitor是从graph create角度构图的。
297 |
298 | 执行遍历节点获取数据时,其发起端其实是PlayableOutput,所以Visualizer把各个PlayableOutput独立开来构图,但这种展示方式对认识graph的结构不如Monitor清晰,而且官方Visualizer不能缩放,插件还有些bug会导致系统闪退,所以直接用Monitor就行。
299 |
300 | 不过Monitor的展示还是有些问题,两个PlayableOutput连在同一个output port了,这应该是Unity Timeline自身的问题,你理解成第二个PlayableOutput连在第二个port就行,以此类推。(不影响运行)
301 |
302 | 下面将继续介绍涉及的Playable与PlayableOutput。
303 | ## Playable 的结构与类
304 |
305 | PlayableGraph 中的 Playable 节点负责 数据的生产、传递与处理,包含叶子节点、mixer节点和layer节点等。
306 | ### Playable结构
307 |
308 | Playable是一个多输入多输出的有向节点,拥有speed、time、duration、isDone、PlayState等参数标识自身的运行状态,并对外提供控制自身运行、暂停与销毁的接口。
309 |
310 | 
311 |
312 | #### 节点属性/方法
313 |
314 | - Playable节点拥有多个input port、多个output port用于连接其他Playable节点。
315 | - 节点连接前需保证port存在,否则报错`Connecting invalid input/output`,需要用`SetInputCount()/SetOutputCount()`申请端口,或者create时指明inputCount参数。
316 | - 同一port不能连接多个节点,否则报错`Cannot connect input/output port, it is already connected, the tree topology will be invalid. Disconnect it first`
317 | - Input有权重weight的概念,**默认是0**,可以在connect时指定weight或者用`Playable.SetInputWeight(inputIndex, weight)`设定输入权重
318 | - Playable节点间连接,可以使用`PlayableGraph.Connect(...)`或者扩展方法的`Playable.ConnectInput(...)`(默认权重为0)
319 |
320 | ``` Csharp
321 | PlayableGraph.Connect(...)
322 |
323 | bool Connect(
324 | U source,
325 | int sourceOutputPort,
326 | V destination,
327 | int destinationInputPort)
328 | where U : struct, IPlayable
329 | where V : struct, IPlayable
330 | ```
331 |
332 | 
333 |
334 | 允许的结构:
335 |
336 | 
337 |
338 | 不允许的结构(三个节点首尾相接)(出现死循环,如果使用graph visulizer,unity会奔溃)
339 | 
340 |
341 | #### 一些特殊的属性/方法
342 | - `GetScriptInstance()`,指明playable生命周期的回调方法
343 | - `GetClip()/SetClip`,一些包含Clip内容的Playable如 AnimationClipPlayable、AudioClipPlayable、AnimatorControllerPlayable、MaterialEffectPlayable、CameraPlayable、VideoPlayable 拥有存取此Playable绑定的可播放片段的方法。这些方法名不固定,根据Playable类型,也可以叫 Get/SetMaterial()、GetAnimatorController、Get/SetCamera()、GetAnimationClip()等。
344 |
345 | #### 运行属性/方法
346 | - **Play state**:`Puase/Playing`,标识此节点运行状态
347 | - `Speed`:playable的播放速度
348 | - `Play()`:运行playable
349 | - `Pause()`:停止自身playable运行
350 | - `IsDone`:标识此节点运行完毕
351 | - `Destroy()`:利用所属graph销毁自身Playable
352 | - **TraversalMode**:`Mix/Passthrough`,表示遍历节点input的方式,`Mix`就是将各输入进行加权;`passthrough`就是匹配input与output,第几个output来请求数据,就从第几个input获取数据。Passthrough目前只用于TimelinePlayable,使之成为graph的统一入口。
353 | #### ⭐Traversal mode 验证
354 |
355 | 对Traversal mode这个参数的理解非常重要,不明白它的话,你会误以为每个PlayableOutput都会接收所有叶子节点的数据。
356 |
357 | 我们来创建个graph案例:
358 | ``` csharp
359 | [Tooltip("false则mode是passthrough,true则mode是mix")]
360 | public bool changeTraversalMode = false;
361 | [Tooltip("false则PlayableOutput的source为playable1,true则为playable2")]
362 | public bool changeSourcePlayable = false;
363 |
364 | //newInput system绑定空格键触发
365 | public void BackspaceDown(InputAction.CallbackContext ctx)
366 | {
367 | if (ctx.performed)
368 | {
369 | ValidateTraversalMode2(changeTraversalMode,changeSourcePlayable);
370 | }
371 | }
372 |
373 | private void ValidateTraversalMode2(bool _changeTraversalMode,bool _changeSourcePlayable)
374 | {
375 | DestoryGraph(graph);
376 | graph = PlayableGraph.Create();
377 | var playable1 = Playable.Create(graph,1);
378 | var playable2 = Playable.Create(graph,2);
379 | var playable3 = Playable.Create(graph);
380 | playable3.SetTraversalMode(!_changeTraversalMode?PlayableTraversalMode.Passthrough:PlayableTraversalMode.Mix);
381 | var playable4 = Playable.Create(graph,4);
382 | var playable5 = Playable.Create(graph,5);
383 | playable3.SetInputCount(10);
384 | playable3.SetOutputCount(10);
385 | graph.Connect(playable3, 0, playable1, 0);
386 | graph.Connect(playable3, 1, playable2, 0);
387 | graph.Connect(playable4, 0, playable3, 0);
388 | graph.Connect(playable5, 0, playable3, 1);
389 | ScriptPlayableOutput output = ScriptPlayableOutput.Create(graph, "customOutput");
390 | output.SetSourcePlayable(!_changeSourcePlayable?playable1:playable2,100);
391 | graph.Play();
392 | }
393 | ```
394 |
395 | 当前中间Playable的 traversalMode是passthrough,可以看到只有port 0 这条线有数据传输。
396 | 
397 | mode为passthrough数据流向
398 |
399 | 改变PlayableOutput连在port 1 这端,数据又只接受input 1 这个port输入了。
400 | 
401 | mode为passthrough的数据流向(改变sourcePlayable)
402 |
403 | 而当 MODE 设为 MIX ,input 0 和 input 1 都接收数据。
404 | 
405 | MODE=MIX的数据流向
406 |
407 | 通过这个例子,能大概理解 `TraversalMode` 遍历模式到底是什么意思。
408 |
409 | 不过要注意,按照Passthrough源码注释的本意:
410 |
411 | 
412 |
413 | 应该是`sourceOutputPort`的`PlayableOutput`跟对应input位置的节点进行直通,但实测**sourceOutputPort没起作用**,实际上是由PlayableOutput在graph中的顺序决定,也就是下图中PlayableOutput前面的#0 #1 #2 标号(其实就是PlayableOutput数组的index)。
414 |
415 | 
416 | passthrough mode下input port匹配的其实是PlayableOutput数组标号
417 |
418 | 测试代码你可以发现`SetSourcePlayable`的port你设50设100都不影响。
419 | ``` csharp
420 | void ValidateTraversalMode()
421 | {
422 | DestoryGraph(graph);
423 | graph = PlayableGraph.Create("TraversalModeGraph");
424 | ScriptPlayableOutput output = ScriptPlayableOutput.Create(graph, "customOutput");
425 | ScriptPlayableOutput output2 = ScriptPlayableOutput.Create(graph, "customOutput2");
426 | ScriptPlayableOutput output3 = ScriptPlayableOutput.Create(graph, "customOutput3");
427 | var playable = ScriptPlayable.Create(graph);
428 | var playable2 = ScriptPlayable.Create(graph);
429 | var playable3 = ScriptPlayable.Create(graph);
430 | var playable4 = ScriptPlayable.Create(graph);
431 | var playable5 = ScriptPlayable.Create(graph);
432 | var mixer = ScriptPlayable.Create(graph);
433 | mixer.SetTraversalMode(PlayableTraversalMode.Passthrough);//默认mode为mix,这里改为passthrough
434 | mixer.SetInputCount(10);// 很重要,否则连接报错:Connecting invalid input
435 | mixer.ConnectInput(4,playable,0,1);
436 | mixer.ConnectInput(6,playable2,0,1);
437 | mixer.ConnectInput(2,playable3,0,1);
438 | mixer.ConnectInput(8,playable4,0,1);
439 | mixer.ConnectInput(1,playable5,0,1);
440 | output.SetSourcePlayable(mixer,30);// 这个SetSourcePlayable的port根本没用,完全根据PlayableOutput顺序获取的
441 | output2.SetSourcePlayable(mixer,50);
442 | output3.SetSourcePlayable(mixer,100);
443 | graph.Play();
444 | }
445 | ```
446 |
447 | 但从Animation计算权重的过程也能看出,SourceOutputPort本意就是playable的output port。但实测中Playableout获取数据时并没有使用这个参数,更没有校验,你设多少都可以(只校验了不能重复绑定,但没校验port是否存在)。
448 | 
449 | 如果是bug的话,官方可能需要做下面两个步骤进行修复:
450 | - 每connect一个节点到TimelinePlayable,TimelinePlayable的outputCount加一(这样直通才有意义)
451 | - 对每个Playableout从根节点TimelinePlayable开始遍历时,严格按照`TimelinePlayable.getInput(PlayableOutput.GetSourceOutputport())`获取绑定的Playable,而不是用`PlaybleGraph`中类似`PlayableOytput[]`数组的`index`来get input playable。(注:Timeline生成的graph中`PlayableOutput.GetSourcePlayable()`得到的就是`TimelinePlayable`)
452 |
453 | ### Playble UML类图
454 |
455 | PlayableAsset创建的playable有两种,一种是不带生命周期管理的Playable,另一种是带生命周期管理的(实现IPlayableBehaviour接口)。第二种其实就是Timeline模块中基于ScriptPlayable实现的一种官方自定义playable。
456 | #### 基础Playable 的 UML 类图
457 | 
458 |
459 | 注意这些Playable都是Struct,没有像class一样的继承能力,但这些子Playable都重写了隐/显式操作符,所以功能上等价于存在继承关系。比如下面AudioClipPlayable例子:
460 |
461 | ``` Csharp
462 | public static implicit operator Playable(AudioClipPlayable playable)
463 | {
464 | return new Playable(playable.GetHandle());
465 | }
466 |
467 | public static explicit operator AudioClipPlayable(Playable playable)
468 | {
469 | return new AudioClipPlayable(playable.GetHandle());
470 | }
471 |
472 | // 自动转换
473 | AudioClipPlayable audioClipPlayable = ...;
474 | Playable playable = audioClipPlayable;
475 |
476 | // 需要显式转换
477 | Playable playable = ...;
478 | AudioClipPlayable audioClipPlayable = (AudioClipPlayable)playable;
479 | ```
480 |
481 | - 这些Playable都拥有`Create`方法,供TrackAsset执行CompileClips方法或CompileTrackPlayable方法期间调用。
482 | - **Playable按功能大概分成两类**:
483 | - 一类如上面蓝色(AnimationClipPlayable等)和绿色(ScriptPlayable)的Playable,在graph中常作为叶子节点的,其内部是含有类似clip的属性,能getClip、setClip(有的没暴露set,只能在create时传入)。
484 | - 一类是上面黄底色的Playable,在graph中作为中间节点,可能包含多个输入,所以命名一般叫MixerPlayable、LayerPlayable,对多个输入的权重或其他数据进行处理。
485 | - 上面结构图并没有列出所有Playable,详细的可以查看IPlayable的所有实现。
486 | - `ScriptPlayable`比较特殊,能够接收`PlayableBehaviour`来**控制Playable的生命周期**,这样形成的就是**自定义Playable**(Timeline里的很多Playable本质上就是这种官方自定义Playable)。
487 |
488 | 下面就详细介绍这些所谓的“**官方自定义Playable**”。
489 | #### PlayableBehaviour 的 UML 类图
490 |
491 | 
492 |
493 | 继承`PlayableBehaviour`的类分为几种:
494 | - `TimelinePlayable`:作为graph中所有其他playable的根父节点,控制所有clip的激活与否。
495 | - Audio的两个名字比较特别,叫`AudioMixerProperties`和`AudioClipProperties`,AudioClipProperties甚至连一个回调都没有,这就涉及到PlayableBehaviour的第二种作用:**传递PlayableAsset/TrackAsset参数给运行时的各生命周期**。这种参数传递有两种用法,后面会介绍。
496 | - Activation有关的 ActivationControlPlayable 和 ActivationMixerPlayable 。
497 | 比较好玩的是 ActivationControlPlayable 只用在 ControlTrack ,而 ActivationMixerPlayable 只用在 ActivationTrack 。
498 | - `ControlTrack` 的三个playable:
499 | - 控制粒子的`ParticleControlPlayable`
500 | - 控制prefab的`PrefabControlPlayable`
501 | - 控制PlayableDirector(也就是控制子Timeline)的`DirectorControlPlayable`。
502 | ### ⭐Behaviour 与 PlayableAsset的数据交互
503 |
504 | 我们在自定义Track 自定义PlayableAsset时,可能有这么个需求,**Track或PlayableAsset自身需要拥有特殊属性**,而且能在后面的生命周期中获取这个特殊属性。
505 |
506 | 比如我的技能系统用的SkillPlayableAsset,有个参数标注这个clip用于技能的哪个阶段,就需要SkiIlPhase这么一个参数,后面behaviour回调触发时就能根据这个参数针对性做处理。
507 |
508 | 
509 | Track或PlayableAsset自身需要拥有特殊属性的场景
510 |
511 | 这就是behaviour 与 playableAsset的数据交互问题。
512 |
513 | 如果你是官方设定的基础Playable,你可以在Playable上设置这么个特殊属性,create的时候都塞进去,生命周期回调时也能正常获取。但你自定义track,用的都是ScriptPlayable,没有额外参数,怎么办?那就是用PlayableBehaviour传参!
514 |
515 | **传参有两种方式**,这同时也涉及到ScriptPlayable createPlayable的两种方式。
516 |
517 | ``` csharp
518 | // T : class, IPlayableBehaviour, new()
519 | public static ScriptPlayable Create(PlayableGraph graph, int inputCount = 0)
520 | {
521 | return new ScriptPlayable(ScriptPlayable.CreateHandle(graph, default (T), inputCount));
522 | }
523 |
524 | public static ScriptPlayable Create(PlayableGraph graph, T template, int inputCount = 0)
525 | {
526 | return new ScriptPlayable(ScriptPlayable.CreateHandle(graph, template, inputCount));
527 | }
528 | ```
529 |
530 | #### 带 new T() 的 ScriptPlayable 创建
531 | 如上ScriptPlayable 创建Playable时,可以带T template 参数,T就是实现了IPlayableBehaviour且有**无参构造函数**的类。
532 |
533 | 这样自定track或playableAsset在创建playable时就可以将含有特殊属性的T传进来,behaviour回调时就能获取到你设的特殊属性。
534 |
535 | ``` csharp
536 | // 存在 XXPlayableBehaviour,内含param1、param2两个参数
537 |
538 | public class XXPlayableAsset : PlayableAsset
539 | {
540 | public XXPlayableBehaviour template = new XXPlayableBehaviour();
541 | public override Playable CreatePlayable (PlayableGraph graph, GameObject owner){
542 | var playable = ScriptPlayable.Create(graph,template);
543 | return playable;
544 | }
545 | }
546 | XXPlayableAsset Inspector面板展示:
547 | template
548 | - param1
549 | - param2
550 | ```
551 |
552 | 这样你在XXPlayableAsset Inspector就能展示XXPlayableBehaviour的两个参数,param1和param2,但不够好看,外面会套一层参数名,像下面这样:
553 |
554 | 
555 |
556 | 解决方法有两个,一种是类似AudioTrack的处理方法,写个AudioTrackInspector手动提取出各个参数。另一个参考官方案例的自定义Attribute:NoFoldOut。
557 |
558 | ``` csharp
559 | // Custom property drawer that draws all child properties inline
560 | [CustomPropertyDrawer(typeof(NoFoldOutAttribute))]
561 | public class NoFoldOutPropertyDrawer : PropertyDrawer
562 | {
563 | public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
564 | {
565 | if (!property.hasChildren)
566 | return base.GetPropertyHeight(property, label);
567 | property.isExpanded = true;
568 | return EditorGUI.GetPropertyHeight(property, label, true) -
569 | EditorGUI.GetPropertyHeight(property, label, false);
570 | }
571 |
572 | public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
573 | {
574 | if (!property.hasChildren)
575 | EditorGUI.PropertyField(position, property, label);
576 | else
577 | {
578 | SerializedProperty iter = property.Copy();
579 | var nextSibling = property.Copy();
580 | nextSibling.Next(false);
581 | property.Next(true);
582 | do
583 | {
584 | // We need to check against nextSibling to properly stop
585 | // otherwise we will draw properties that are not child of this
586 | // foldout.
587 | if (SerializedProperty.EqualContents(property, nextSibling))
588 | break;
589 | float height = EditorGUI.GetPropertyHeight(property, property.hasVisibleChildren);
590 | position.height = height;
591 | EditorGUI.PropertyField(position, property, property.hasVisibleChildren);
592 | position.y = position.y + height;
593 | }
594 | while (property.NextVisible(false));
595 | }
596 | }
597 | }
598 | ```
599 |
600 | 这样使用时给参数加`[NoFoldOut]`特性就能直接展示子属性了。
601 | ``` csharp
602 | // 存在 XXPlayableBehaviour,内含param1、param2两个参数
603 | public class XXPlayableAsset : PlayableAsset
604 | {
605 | [NoFoldOut]
606 | public XXPlayableBehaviour template = new XXPlayableBehaviour();
607 | public override Playable CreatePlayable (PlayableGraph graph, GameObject owner){
608 | var playable = ScriptPlayable.Create(graph,template);
609 | return playable;
610 | }
611 | }
612 | ```
613 | 
614 | #### 使用 default (T) 的 ScriptPlayable 创建
615 |
616 | 如果你使用 default (T) 创建,意味着你的这些特殊属性都变成默认值。
617 |
618 | 不过你也可以手动操作改变,如下代码所示,create完ScriptPlayable后,再手动赋值。
619 |
620 | ``` csharp
621 | // 存在 XXPlayableBehaviour,内含param1、param2两个参数
622 |
623 | public class XXPlayableAsset : PlayableAsset
624 | {
625 | public override Playable CreatePlayable (PlayableGraph graph, GameObject owner)
626 | {
627 | var playable = ScriptPlayable.Create(graph);
628 | var xxPlayableBehaviour = playable.GetBehaviour();
629 | xxPlayableBehaviour.param1 = value1;
630 | xxPlayableBehaviour.param2 = value2;
631 | return playable;
632 | }
633 | }
634 | ```
635 |
636 | 不过这种在Asset还要再申明一遍param,冗杂,所以推荐用第一种。
637 |
638 | #### ExposedReference用法
639 |
640 | 因为Asset不能引用scene场景中的对象,但如果你非要引用也不是不可以,Timeline提供了`ExposedReference`方法让你能在Behaviour回调中获取场景中的对象。
641 |
642 | ``` csharp
643 | public class XXPlayableBehaviour : PlayableBehaviour
644 | {
645 | public ExposedReference exposedTransform;
646 | public override void OnGraphStart(Playable playable){
647 | Transform transform = exposedTransform.Resolve(playable.GetGraph().GetResolver());
648 | Debug.Log("exposedTransform.Resolve(playable.GetGraph().GetResolver()):"+transform.position);
649 | Debug.Log("gameobject name:" + transform.gameObject.name);
650 |
651 | }
652 | }
653 |
654 | public class XXPlayableAsset : PlayableAsset
655 | {
656 | public XXPlayableBehaviour template = new XXPlayableBehaviour();
657 | public override Playable CreatePlayable (PlayableGraph graph, GameObject owner){
658 | var playable = ScriptPlayable.Create(graph,template);
659 | return playable;
660 | }
661 | }
662 | ```
663 |
664 | 注意使用时,一定要从点击挂载PlayableDirector的对象对应的timeline窗口操作。
665 |
666 | 
667 | ExposedReference用法
668 |
669 | 运行时可以正常打印,但发现打印的对象position数据有问题,似乎有(0.22, 1.28, -1.66)的偏差,不清楚原因,暂时还是不要用`ExposedReference`。
670 |
671 | 
672 | ExposedReference用法有待进一步验证
673 |
674 | ## PlayableOutput 结构与类
675 |
676 | `PlayableOutput` 作为 PlayableGraph 中的数据输出节点,在graph Play 时负责发起 Playable节点的遍历(没连PlayableOutput 对应的Playable不运行),并将Playable节点传递的数据交由对应的`target`运行。
677 | ### PlayableOutput 结构
678 |
679 | PlayableOutput 的结构相当简单。
680 |
681 | 
682 | - ReferenceObject:对应的TrackAsset
683 | - SourcePlayable:PlayableOutput连接的Playable,使用扩展方法`PlayableOutput.SetSourcePlayable(Playable value, int port)`连接
684 | - 注意连接的SourcePlayable和port不能都一样,否则报错`Cannot set multiple PlayableOutputs to the same source playable and output port`
685 | - weight:PlayableOutput也拥有权重,默认为1,可以通过`PlayableOutput.SetWeight(float value)` 设置。
686 | - target:AnimationPlayableOutput、AudioPlayableOutput、TexturePlayableOutput拥有这个属性,表示Track所绑定的对象,用于后续处理Playable传过来的数据。
687 | ### UML 类图
688 |
689 | 相比Playable的类图,PlayableOutput就显得眉清目秀了。
690 | - 跟Playable一样,PlayableOutput也是struct结构体,它的继承也是重载显隐操作符。
691 | - 每种PlayableOutput都有个create方法来创建一个具体的PlayableOutput。
692 | - 除了ScriptPlayableOutput外,都有个target参数用于指明track binding的target对象。
693 |
694 | 
695 | # 运行时Timeline
696 |
697 | 前文介绍了Timeline编辑态流程,至于运行时,则主要包含三个步骤:
698 | - director组件初始化
699 | - graph Build
700 | - graph Play
701 |
702 | 
703 | ## 初始化 PlayableDirector
704 |
705 | 看Timeline源码最难受的就是这,看不到尾就算了,还看不到头,过程全藏在Unity c++端。
706 |
707 | 不过好在静态文件已知,create Playable步骤已知,中间无非就是graph的创建,那由谁触发呢?只有PlayableDirector了。
708 |
709 | 所以**隐藏的过程就是** :PlayableDirector 组件的初始化和 PlaybleAsset 实例化,如果 play on awake 打开,则开始构建 graph,否则等待 director 执行 Play() 或 RebuildGraph() 才开始构建 graph。
710 |
711 | 再次强调下,PlayableDirector组件awake远早于用户脚本awake,无法在用户脚本awake方法中控制PlayableDirector的`play on awake`参数。
712 |
713 | 就是说,如果`play on awake` 是 false,你在自己脚本改为true,director也不会自动运行。不过可以先让director disable,在用户脚本awake 时 enable这个director,间接实现(那还不如手动Play() )。
714 |
715 | ## 构建 Graph
716 |
717 | 构建 graph简易过程如下图:
718 |
719 | 
720 |
721 | 详细过程如下:
722 | 1. 根据`PlayableDirector` 的 TimelineAsset 执行创建Playable动作(c++端触发)
723 | 2. 创建`TimelinePlayable`节点,**设置traversalMode为Passthrough**
724 | 3. 遍历Timeline中的每个`OutputTrack`,为其创建`MixerPlayable`节点和`Playable`节点
725 | 1. 创建MixerPlayable父节点
726 | 2. 遍历TrackAsset的每个`TimelineClip`,创建对应的ClipPlayable子节点
727 | 3. 利用MixerPlayable、刚创建的ClipPlayable和对应的TimelineClip创建**RuntimeClip**,然后将RuntimeClip存储到TimelinePlayable的**区间树IntervalTree**中,同时执行ClipPlayable的Pause()方法(其实就是设置playstate为paused)
728 | 4. 连接ClipPlayable子节点和MixerPlayable父节点,**权重设置为0**
729 | 4. 将上面创建的每个MixerPlayable连接TimelinePlayable,**权重为1**
730 | 5. 紧接着对每个OutputTrack创建对应的playableOutput节点,并`SetSourcePlayable`为`TimelinePlayable`,权重设为1,`ReferenceObject`为这个`TrackAsset`。
731 | - 如果是动画track的话,最后会为PlayableOutput注册一个权重处理回调`m_EvaluateCallbacks.Add(new AnimationOutputWeightProcessor(animOutput))`,构造AnimationOutputWeightProcessor时将AnimationPlayaleOutput的**权重设为0**(默认是1)。这个Processor会在graph后续运行时触发WeightEvaluate方法处理Animation相关的权重(会重新调整权重)。
732 | - 注意playableOutput的target不是在此时添加的,这时传入的target都是null,可以通过`((AnimationPlayableOutput)graph.GetOutput(0)).GetTarget()`验证(值一定是null,假设第一个是动画的output情况下)。
733 | - 真正设置target的动作可能在构建gragh动作之后,在c++端手动根据binding信息设置(可能是通过共有的ReferenceObject 这个key来确定target的)。
734 | - 所以play阶段target是有值的(如果编辑时你确实binding了)。
735 | - 那为什么不在创建时顺便设置target呢?我猜还是静态资产没办法赋scene中对象的原因。这些TrackAsset实际是没法存target对象的。
736 |
737 | build graph的方法调用链如下(也可以看到一个明显的c++端调用):
738 |
739 | 
740 | ### AnimationPlayable的权重处理
741 |
742 | Timeline 会利用 AnimationOutputWeightProcessor Evaluate方法处理AnimationMixer 、AnimationLayer和 AnimationPlayableOutput的权重。
743 |
744 | 大概效果如下,设四个clip初始权重分别为0.26,0.14,0.19,0.13,连到各自Mixer和Layer后,如果输入权重和小于1,会**按比例放大**为:
745 | - `mixer input0 weight` :0.65= 0.26/(0.26+0.14)\*1
746 | - `mixer input1 weight `:0.35= 0.14/(0.26+0.14)\*1
747 | - `layer input0 weight` :0.56=(0.26+0.14)/(0.26+0.14+0.19+0.13)\*1
748 | - `AnimationOutput weight` : 0.72 = 0.26+0.14+0.19+0.13
749 |
750 | 
751 |
752 | 如果输入权重和≥1,则只对≥1的输入钳值到1,不等比例缩小,且AnimationOutput **weight 钳值为1**.
753 |
754 | 比如四个clip初始权重分别为0.6,0.9,0.2,0.3,AnimationOutputWeightProcessor.Evaluate() 执行完后:
755 | - `mixer input0 weight` :0.6 (0.6+0.9>1不缩放,且0.6<1 不钳值)
756 | - `mixer input1 weight` :0.9(0.6+0.9>1不缩放,且0.9<1 不钳值)
757 | - `layer input0 weight` :1 (0.6+0.9+0.2+0.3>1不缩放,且0.6+0.9>1 则钳值到1)
758 | - `layer input1 weight` : 0.5 (0.6+0.9+0.2+0.3>1不缩放,且0.2+0.3<1 不钳值)
759 | - `AnimationOutput weight`:1(0.6+0.9+0.2+0.3>1 钳值到1)
760 |
761 | 
762 |
763 | 这么处理的目的按源码注释的说法:”对动画轨道上的权重进行后处理,以正确归一化混合器权重,从而避免混合时出现默认姿势,并确保子轨道、图层以及图层图正确混合。Does a post processing of the weights on an animation track to properly normalize the mixer weights so that blending does not bring default poses and subtracks, layers and layer graphs blend correctly ”
764 |
765 | 测试用例过长,参见:🔗TimelineTestForAnimationWeightInfo.
766 |
767 | ## 运行 Graph
768 |
769 | graph play或evaluate后主要两个步骤:
770 | 1. TimelinePlayable PrepareFrame() 处理clip的激活 与权重
771 | 1. 搜索区间树,处理clip enable与disable(改变对应Playable的playstate)
772 | 2. 根据mixin/mixout curve设置激活的clip所在的mixer input weight权重
773 | 3. 执行AnimationOutputWeightProcessor.Evaluate()处理mixer layer和PlayaleOutput 的权重
774 | 2. 后序遍历Playable执行playing态的Playable的ProcessFrame方法。
775 |
776 | 
777 | graph play或evaluate后两个步骤
778 |
779 | 当然这不是终点,数据经过一个个playable处理后,传递到playableOutput,最终交由target处理。
780 | ### Playable数据传递与output处理数据
781 |
782 | ScriptPlayable数据处理在ProcessFrame中,可以实现Prefab的enable disable,video的播放暂停等功能。
783 |
784 | 而基础Playable如AnimationClipPlayable,其Animation数据处理是隐藏起来的,实际是通过`AnimationStream`结构体进行数据交互,你可以 使用 `AnimationScriptPlayable` 并实现`IAnimationJob` 的 `ProcessAnimation` 方法来自定义处理动画数据。
785 |
786 | 
787 |
788 | > [Playable使用细则](https://zhuanlan.zhihu.com/p/632890306)
789 |
790 | 至于数据传入AnimatorPlayableOutput后怎么处理更是黑盒了,可以看一下下面这位博主的分析:
791 |
792 | 
793 |
794 | > [死板地介绍Unity动画系统设计](https://zhuanlan.zhihu.com/p/305825751)
795 |
796 | Animator的高级使用估计得等到有IK或者更精细动画需求后,才可能再继续研究了。
797 | 目前在SMB+Timeline加持下已能满足大部分需求。
798 |
799 | ### IntervalTree 结构
800 |
801 | 文章一开时给 Timeline 下的定义就是 “**包含很多可播放片段(Playable Clip)的区间树(IntervalTree)**”,下面便详细解析下 IntervalTree 的结构。
802 |
803 | TimelinePlayable利用IntervalTree来管理RuntimeClip,UML类图如下:
804 |
805 | 
806 | IntervalTree UML类图
807 |
808 | - m_Entries:记录所有 RuntimeClip 和其左右边界的List
809 | 
810 | - m_Nodes:IntervalTree中的节点
811 | - node first/last: 表示存储在当前节点中的所有Runtimeclip中的首和尾index
812 | - center:排序前比较重要,表示待排序Runtimeclip左右边界中点,用于后续递归地进行二分排序
813 | - left/right:左右子节点
814 |
815 | 
816 |
817 | Build IntervalTree 逻辑:
818 |
819 | ``` csharp
820 | private void Rebuild()
821 | {
822 | m_Nodes.Clear();
823 | m_Nodes.Capacity = m_Entries.Capacity;
824 | Rebuild(0, m_Entries.Count - 1);
825 | }
826 |
827 | private int Rebuild(int start, int end)
828 | {
829 | IntervalTreeNode intervalTreeNode = new IntervalTreeNode();
830 |
831 | // minimum size, don't subdivide
832 | int count = end - start + 1;
833 | if (count < kMinNodeSize)
834 | {
835 | intervalTreeNode = new IntervalTreeNode() { center = kCenterUnknown, first = start, last = end, left = kInvalidNode, right = kInvalidNode };
836 | m_Nodes.Add(intervalTreeNode);
837 | return m_Nodes.Count - 1;
838 | }
839 |
840 | var min = Int64.MaxValue;
841 | var max = Int64.MinValue;
842 |
843 | for (int i = start; i <= end; i++)
844 | {
845 | var o = m_Entries[i];
846 | min = Math.Min(min, o.intervalStart);
847 | max = Math.Max(max, o.intervalEnd);
848 | }
849 |
850 | var center = (max + min) / 2;
851 | intervalTreeNode.center = center;
852 |
853 | // first pass, put every thing left of center, left
854 | int x = start;
855 | int y = end;
856 | while (true)
857 | {
858 | while (x <= end && m_Entries[x].intervalEnd < center)
859 | x++;
860 |
861 | while (y >= start && m_Entries[y].intervalEnd >= center)
862 | y--;
863 |
864 | if (x > y)
865 | break;
866 |
867 | var nodeX = m_Entries[x];
868 | var nodeY = m_Entries[y];
869 |
870 | m_Entries[y] = nodeX;
871 | m_Entries[x] = nodeY;
872 | }
873 |
874 | intervalTreeNode.first = x;
875 |
876 | // second pass, put every start passed the center right
877 | y = end;
878 | while (true)
879 | {
880 | while (x <= end && m_Entries[x].intervalStart <= center)
881 | x++;
882 |
883 | while (y >= start && m_Entries[y].intervalStart > center)
884 | y--;
885 |
886 | if (x > y)
887 | break;
888 |
889 | var nodeX = m_Entries[x];
890 | var nodeY = m_Entries[y];
891 |
892 | m_Entries[y] = nodeX;
893 | m_Entries[x] = nodeY;
894 | }
895 |
896 | intervalTreeNode.last = y;
897 |
898 | // reserve a place
899 | m_Nodes.Add(new IntervalTreeNode());
900 | int index = m_Nodes.Count - 1;
901 |
902 | intervalTreeNode.left = kInvalidNode;
903 | intervalTreeNode.right = kInvalidNode;
904 |
905 | if (start < intervalTreeNode.first)
906 | intervalTreeNode.left = Rebuild(start, intervalTreeNode.first - 1);
907 |
908 | if (end > intervalTreeNode.last)
909 | intervalTreeNode.right = Rebuild(intervalTreeNode.last + 1, end);
910 |
911 | m_Nodes[index] = intervalTreeNode;
912 | return index;
913 | }
914 | ```
915 |
916 | 构建思路非常简单:
917 | - 排序开始时,遍历所有Runtimeclip,确定最左端和最右端边界,然后取边界的中点作为根节点的center,这样构建的tree不至于一边短一边长。
918 | - 然后把所有Runtimeclip完全在中间值center左侧的放到m_Entries靠下位置,把所有Runtimeclip完全在中间值center右侧的放到m_Entries靠上位置,被center贯穿的Runtimeclip保留在当前节点中。
919 | - 这样就完成了初步排序,然后再递归地排序靠下的这坨,并作为左子节点;再递归地排序靠上的这坨,并作为右子节点。这样整棵IntervalTree就构建完成了。
920 |
921 | 最终结构如下图所示:
922 |
923 | 
924 | IntervalTree内部结构示例
925 |
926 | ### RuntimeClip 结构
927 |
928 | 下面就以 AnimationClip 为例,讲解IntervalTreeNode中RuntimeClip的创建逻辑:
929 |
930 | 1. 创建**TimelineClip** (主逻辑在TrackAsset)
931 | 1. 根据TrackClipTypeAttribute的定义获取限定的ClipType`GetType().GetCustomAttributes(typeof(TrackClipTypeAttribute))`,
932 | 2. 创建限定ClipType的 TimelineClip 容器`newClip = CreateNewClipContainerInternal()`
933 | 3. 把特定类型 ClipPlayableAsset 比如AnimationPlayableAsset塞进TimelineClip中asset参数中。(此时AnimationPlayableAsset中clip为null)
934 |
935 | 2. 将具体AnimationClip塞进AnimationPlayableAsset的clip变量中`AddClipOnTrack(newClip, parentTrack, candidateTime, assignableObject, state)`
936 | 3. 创建**RuntimeClip** (主逻辑在TrackAsset 的 CompileClips方法)
937 | 1. 根据timelineClip和clip对应的Playable创建RuntimeClip
938 | 2. 将新建的RuntimeClip加入区间树**IntervalTree**中
939 |
940 | 4. 重新排序区间树节点(可延迟执行)
941 |
942 | 最终,TimelineClip 与 RuntimeClip结构如下:
943 |
944 | 
945 | TimelineClip 与 RuntimeClip结构
946 |
947 | ### 运行时 IntervalTree
948 |
949 | 我们使用区间树 IntervalTree 最主要目的就是能快速搜索RuntimeClip,为什么呢?
950 |
951 | 因为每帧Timeline都会执行PrepareFrame方法,指明哪些clip在当前时间激活,哪些clip在当前时间停止,如果只是用List结构,搜索时间复杂度 `O(n)`,而使用IntervalTree,时间复杂度便降到 `O(logn)`了。
952 |
953 | `TimelinePlayable` **PrepareFrame** 方法执行逻辑:
954 |
955 | 1. 利用IntervalTree获取当前帧所有激活的RuntimeClip
956 | 2. disable上一帧激活,这一帧未激活的clip(会执行Playable的Pause()方法 )
957 | 3. enable这一帧激活的clip(会执行Playable的Play()方法 )
958 | 4. 根据mixin/mixout curve设置此clip所在的mixer input weight权重
959 |
960 | 
961 |
962 | ## PlayableGraph 的生命周期
963 |
964 | 前文运行时多次提到了 PrepareFrame、ProcessFrame,其实这就是**ScriptPlayable**在某段生命周期执行的回调函数,我们可以利用 ScriptPlayable 传入自定义**PlayableBehaviour** 来干涉Playable的生命周期。
965 |
966 | Playable的生命周期有:
967 | - `GraphStart`:graph play 开始时
968 | - `GraphStop`:graph stop时
969 | - `PlayableCreate`:Playable Create时
970 | - `PlayableDestroy`:Playable Destroy时
971 | - `BehaviourPlay`:Playable 运行时
972 | - `BehaviourPause`:Playable 暂停时
973 | - `PrepareFrame`:每帧处理数据前
974 | - `ProcessFrame`:每帧开始处理数据时
975 |
976 | 当然可能并非都是这些生命周期,比如`AnimationScriptPlayable`就有个`ProcessAnimation`和`ProcessRootMotion`过程,可能就没有所谓的ProcessFrame了。
977 |
978 | 当然对于ScriptPlayable,上面的生命周期是确定的,在Playable处于上面的生命周期时,便会执行对应的回调函数(注册在PlayableBehaviour里)。
979 |
980 | 下面的自定义PlayableBehaviour就是简单打印回调方法和playable:
981 |
982 | ``` csharp
983 | public class TimelineTestForAnimationScriptBehaviour : PlayableBehaviour
984 | {
985 | public override void OnGraphStart(Playable playable)
986 | {
987 | DebugLog(playable,"OnGraphStart");
988 | }
989 |
990 | private static void DebugLog(Playable playable, string methodName)
991 | {
992 | Debug.Log($"Playable by in/output cnt_{playable.GetInputCount()}{playable.GetOutputCount()} Behaviour:{methodName}");
993 | }
994 |
995 | public override void OnGraphStop(Playable playable)
996 | {
997 | DebugLog(playable,"OnGraphStop");
998 | }
999 |
1000 | public override void OnPlayableCreate(Playable playable)
1001 | {
1002 | DebugLog(playable,"OnPlayableCreate");
1003 | }
1004 |
1005 | public override void OnPlayableDestroy(Playable playable)
1006 | {
1007 | DebugLog(playable,"OnPlayableDestroy");
1008 | }
1009 |
1010 | public override void OnBehaviourPlay(Playable playable, FrameData info)
1011 | {
1012 | DebugLog(playable,"OnBehaviourPlay");
1013 | }
1014 |
1015 | public override void OnBehaviourPause(Playable playable, FrameData info)
1016 | {
1017 | DebugLog(playable,"OnBehaviourPause");
1018 | }
1019 |
1020 | public override void PrepareFrame(Playable playable, FrameData info)
1021 | {
1022 | DebugLog(playable,"PrepareFrame");
1023 | }
1024 |
1025 | public override void ProcessFrame(Playable playable, FrameData info, object playerData)
1026 | {
1027 | DebugLog(playable,"ProcessFrame");
1028 | }
1029 | }
1030 | ```
1031 |
1032 | ### 生命周期验证
1033 |
1034 | 上面生命周期的理解还比较模糊,尤其父子节点回调执行顺序是什么需要验证。
1035 |
1036 | 验证代码如下(给不同节点设置不同端口数能很方便辨析是哪个节点的回调):
1037 |
1038 | ``` csharp
1039 | using System;
1040 | using Scenes.TimelineTest.scripts;
1041 | using UnityEditor;
1042 | using UnityEngine;
1043 | using UnityEngine.Playables;
1044 |
1045 | public class TimelineTestForLifeCycle : MonoBehaviour
1046 | {
1047 | public PlayableGraph graph;
1048 |
1049 | private ScriptPlayable playable3;
1050 | public void CreateGraph()
1051 | {
1052 | DestroyGraph();
1053 | graph = PlayableGraph.Create("TimelineTestForLifeCycle");
1054 | ScriptPlayableOutput output = ScriptPlayableOutput.Create(graph,"LifeCycleTestOutput");
1055 | var playable5 = ScriptPlayable.Create(graph,5);
1056 | var playable4 = ScriptPlayable.Create(graph,4);
1057 | playable3 = ScriptPlayable.Create(graph,3);
1058 | var playable2 = ScriptPlayable.Create(graph,2);
1059 | var playable1 = ScriptPlayable.Create(graph,1);
1060 | output.SetSourcePlayable(playable5,0);
1061 | output.SetWeight(1);
1062 | playable5.ConnectInput(0,playable1,0,1);
1063 | playable5.ConnectInput(1,playable2,0,0.5f);
1064 | playable3.Pause();
1065 | playable5.ConnectInput(2,playable3,0,1);
1066 | playable5.ConnectInput(3,playable4,0,0);
1067 |
1068 | var playable9 = ScriptPlayable.Create(graph,9);
1069 | var playable8 = ScriptPlayable.Create(graph,8);
1070 | playable9.ConnectInput(0,playable8,0,1);
1071 | }
1072 |
1073 | public void PlayPlayable3()
1074 | {
1075 | playable3.Play();
1076 | }
1077 | public void PausePlayable3()
1078 | {
1079 | playable3.Pause();
1080 | }
1081 | public void DestroyPlayable3()
1082 | {
1083 | playable3.Destroy();
1084 | }
1085 |
1086 | public void PlayGraph()
1087 | {
1088 | graph.Play();
1089 | }
1090 | private void OnDestroy()
1091 | {
1092 | DestroyGraph();
1093 | }
1094 |
1095 | public void DestroyGraph()
1096 | {
1097 | if (graph.IsValid())
1098 | {
1099 | Debug.Log("Execute Graph Destroy");
1100 | graph.Destroy();
1101 | Debug.Log("Graph destroyed");
1102 | }
1103 | }
1104 | }
1105 |
1106 | [CustomEditor(typeof(TimelineTestForLifeCycle))]
1107 | class TimelineTestForLifeCycleEditor : Editor
1108 | {
1109 | public override void OnInspectorGUI()
1110 | {
1111 | base.OnInspectorGUI();
1112 | TimelineTestForLifeCycle script = (TimelineTestForLifeCycle) target;
1113 | if (GUILayout.Button("CreateGraph"))
1114 | {
1115 | script.CreateGraph();
1116 | }
1117 | if (GUILayout.Button("PlayGraph"))
1118 | {
1119 | script.PlayGraph();
1120 | }
1121 | if (GUILayout.Button("Destroy graph"))
1122 | {
1123 | script.DestroyGraph();
1124 | }
1125 | if (GUILayout.Button("Play Playable3"))
1126 | {
1127 | script.PlayPlayable3();
1128 | }
1129 |
1130 | if (GUILayout.Button("Pause Playable3"))
1131 | {
1132 | script.PausePlayable3();
1133 | }
1134 | if (GUILayout.Button("Destroy Playable3"))
1135 | {
1136 | script.DestroyPlayable3();
1137 | }
1138 | }
1139 | }
1140 |
1141 | ```
1142 |
1143 | 点击 “CreateGraph” 后的 graph 结构(图中标注的是节点设置的play state和连接权重):
1144 |
1145 | 
1146 | 生命周期验证示例的graph图
1147 |
1148 | 当前脚本主要测试graph的 create与play(除了prepareFrame、processFrame),验证:
1149 | - graph不play,playable节点是否会运行
1150 | - 没有playableOutput,playable节点是否会运行
1151 | - 输出权重为0,playable节点是否会运行
1152 | - pause状态,playable节点是否会运行
1153 | - PrepareFrame、ProcessFrame顺序
1154 | - Graph destroy执行效果
1155 |
1156 | 
1157 | 打印结果:
1158 | ``` csharp
1159 | // 打印
1160 | Playable by in/output cnt_51 Behaviour:OnPlayableCreate
1161 | Playable by in/output cnt_41 Behaviour:OnPlayableCreate
1162 | Playable by in/output cnt_31 Behaviour:OnPlayableCreate
1163 | Playable by in/output cnt_21 Behaviour:OnPlayableCreate
1164 | Playable by in/output cnt_11 Behaviour:OnPlayableCreate
1165 | Playable by in/output cnt_91 Behaviour:OnPlayableCreate
1166 | Playable by in/output cnt_81 Behaviour:OnPlayableCreate
1167 | Playable by in/output cnt_51 Behaviour:OnGraphStart
1168 | Playable by in/output cnt_51 Behaviour:OnBehaviourPlay
1169 | Playable by in/output cnt_11 Behaviour:OnGraphStart
1170 | Playable by in/output cnt_11 Behaviour:OnBehaviourPlay
1171 | Playable by in/output cnt_21 Behaviour:OnGraphStart
1172 | Playable by in/output cnt_21 Behaviour:OnBehaviourPlay
1173 | Playable by in/output cnt_31 Behaviour:OnGraphStart
1174 | Playable by in/output cnt_31 Behaviour:OnBehaviourPause
1175 | Playable by in/output cnt_41 Behaviour:OnGraphStart
1176 | Playable by in/output cnt_41 Behaviour:OnBehaviourPlay
1177 |
1178 | Playable by in/output cnt_51 Behaviour:PrepareFrame
1179 | Playable by in/output cnt_11 Behaviour:PrepareFrame
1180 | Playable by in/output cnt_21 Behaviour:PrepareFrame
1181 | Playable by in/output cnt_41 Behaviour:PrepareFrame
1182 | Playable by in/output cnt_11 Behaviour:ProcessFrame
1183 | Playable by in/output cnt_21 Behaviour:ProcessFrame
1184 | Playable by in/output cnt_41 Behaviour:ProcessFrame
1185 | Playable by in/output cnt_51 Behaviour:ProcessFrame
1186 |
1187 | Graph destroyed
1188 | Playable by in/output cnt_51 Behaviour:OnBehaviourPause
1189 | Playable by in/output cnt_11 Behaviour:OnBehaviourPause
1190 | Playable by in/output cnt_21 Behaviour:OnBehaviourPause
1191 | Playable by in/output cnt_41 Behaviour:OnBehaviourPause
1192 | Playable by in/output cnt_51 Behaviour:OnGraphStop
1193 | Playable by in/output cnt_51 Behaviour:OnPlayableDestroy
1194 | Playable by in/output cnt_41 Behaviour:OnGraphStop
1195 | Playable by in/output cnt_41 Behaviour:OnPlayableDestroy
1196 | Playable by in/output cnt_31 Behaviour:OnGraphStop
1197 | Playable by in/output cnt_31 Behaviour:OnPlayableDestroy
1198 | Playable by in/output cnt_21 Behaviour:OnGraphStop
1199 | Playable by in/output cnt_21 Behaviour:OnPlayableDestroy
1200 | Playable by in/output cnt_11 Behaviour:OnGraphStop
1201 | Playable by in/output cnt_11 Behaviour:OnPlayableDestroy
1202 | Playable by in/output cnt_91 Behaviour:OnPlayableDestroy
1203 | Playable by in/output cnt_81 Behaviour:OnPlayableDestroy
1204 | ```
1205 |
1206 | 结合Graph Monitor 可以得出结论:
1207 | - graph不play,playable节点不会运行
1208 | - 没有连ScriptPlayableOutput,ScriptPlayable节点不会运行(playable time不随时间增加),且除了OnPlayableCreate和OnPlayableDestroy,其他回调方法不会触发
1209 | - 输出权重为0,playable节点依然运行
1210 | - pause状态,playable节点不会运行
1211 | - graph未play,playable执行pause并不会触发OnBehaviourPause
1212 | - graph 执行Play() 后:若playable是playing状态时,则触发OnBehaviourPlay回调;是paused状态时,则触发OnBehaviourPause回调
1213 | - 会以**前序**遍历(父节点优先)的方式触发OnGraphStart 和 OnBehaviourPlay/Pause
1214 | - 会以**前序**遍历(父节点优先)的方式触发PrepareFrame回调
1215 | - 会以**后序**遍历(子节点优先)的方式触发ProcessFrame回调
1216 | - 没有连接playableOutput的playable节点不会触发OnGraphStart/OnGraphStop回调
1217 | - 直接Destroy graph时,如果graph未stop会先触发OnBehaviourPause、OnGraphStop回调,这两个回调的触发逻辑与先Stop Graph后Destroy Graph 略有不同,如下日志:
1218 |
1219 | ``` csharp
1220 | // 先Stop graph 再 Destroy graph的日志(与直接Destroy graph有差异)
1221 | graph.Stop()
1222 |
1223 | Playable by in/output cnt_51 Behaviour:OnBehaviourPause
1224 | Playable by in/output cnt_51 Behaviour:OnGraphStop
1225 | Playable by in/output cnt_11 Behaviour:OnBehaviourPause
1226 | Playable by in/output cnt_11 Behaviour:OnGraphStop
1227 | Playable by in/output cnt_21 Behaviour:OnBehaviourPause
1228 | Playable by in/output cnt_21 Behaviour:OnGraphStop
1229 | Playable by in/output cnt_31 Behaviour:OnBehaviourPause
1230 | Playable by in/output cnt_31 Behaviour:OnGraphStop
1231 | Playable by in/output cnt_41 Behaviour:OnBehaviourPause
1232 | Playable by in/output cnt_41 Behaviour:OnGraphStop
1233 |
1234 | Graph.Destroy()
1235 |
1236 | Playable by in/output cnt_51 Behaviour:OnPlayableDestroy
1237 | Playable by in/output cnt_41 Behaviour:OnPlayableDestroy
1238 | Playable by in/output cnt_31 Behaviour:OnPlayableDestroy
1239 | Playable by in/output cnt_21 Behaviour:OnPlayableDestroy
1240 | Playable by in/output cnt_11 Behaviour:OnPlayableDestroy
1241 | Playable by in/output cnt_91 Behaviour:OnPlayableDestroy
1242 | Playable by in/output cnt_81 Behaviour:OnPlayableDestroy
1243 | ```
1244 |
1245 | 完整生命周期:
1246 | 
1247 |
1248 | # 总结
1249 |
1250 | Timeline和Playable系统,本文还是花了不少精力的:
1251 | - 详细展示了Timeline和Playable各个类的UML关系
1252 | - 系统梳理了Timeline静态和运行时结构与逻辑
1253 | - 详细分析了Playable、区间树IntervalTree等内部结构
1254 | - 验证了很多细节,比如AnimationOutput weight作用、ScriptPlayable生命周期、Passthrough TraversalMode的效果等
1255 |
1256 | 核心都在下面这张图中,再回顾下:
1257 |
1258 | 
1259 |
1260 | 吃透了本文,自定义Track就是易如反掌了。
1261 |
1262 | 下一篇可能写点技能编辑器相关。
1263 |
1264 | 记得点赞、关注、收藏哦~
1265 |
--------------------------------------------------------------------------------
/Unity_025/code/TimelineTestForAnimationScriptBehaviour.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Reflection;
3 | using UnityEngine;
4 | using UnityEngine.Playables;
5 |
6 | namespace Scenes.TimelineTest.scripts
7 | {
8 | [Serializable]
9 | public class TimelineTestForAnimationScriptBehaviour : PlayableBehaviour
10 | {
11 | public override void OnGraphStart(Playable playable)
12 | {
13 | DebugLog(playable,"OnGraphStart");
14 | }
15 |
16 | private static void DebugLog(Playable playable, string methodName)
17 | {
18 | Debug.Log($"Playable by in/output cnt_{playable.GetInputCount()}{playable.GetOutputCount()} Behaviour:{methodName}");
19 | }
20 |
21 | public override void OnGraphStop(Playable playable)
22 | {
23 | DebugLog(playable,"OnGraphStop");
24 | }
25 |
26 | public override void OnPlayableCreate(Playable playable)
27 | {
28 | DebugLog(playable,"OnPlayableCreate");
29 | }
30 |
31 | public override void OnPlayableDestroy(Playable playable)
32 | {
33 | DebugLog(playable,"OnPlayableDestroy");
34 | }
35 |
36 | public override void OnBehaviourPlay(Playable playable, FrameData info)
37 | {
38 | DebugLog(playable,"OnBehaviourPlay");
39 | }
40 |
41 | public override void OnBehaviourPause(Playable playable, FrameData info)
42 | {
43 | DebugLog(playable,"OnBehaviourPause");
44 | }
45 |
46 | public override void PrepareFrame(Playable playable, FrameData info)
47 | {
48 | DebugLog(playable,"PrepareFrame");
49 | }
50 |
51 | public override void ProcessFrame(Playable playable, FrameData info, object playerData)
52 | {
53 | DebugLog(playable,"ProcessFrame");
54 | }
55 |
56 | private IntPtr GetHandle(Playable playable)
57 | {
58 | var handle = playable.GetHandle();
59 | // 通过反射获取 internal 字段 m_Handle
60 | FieldInfo fieldInfo = typeof(PlayableHandle).GetField("m_Handle", BindingFlags.NonPublic | BindingFlags.Instance);
61 | if (fieldInfo != null)
62 | {
63 | return (IntPtr)fieldInfo.GetValue(handle);
64 | }
65 | throw new Exception("Failed to get m_Handle field via reflection.");
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Unity_025/code/TimelineTestForAnimationStream.cs:
--------------------------------------------------------------------------------
1 | using UnityEngine;
2 | using UnityEngine.Animations;
3 | using UnityEngine.Playables;
4 |
5 | public class TimelineTestForAnimationStream : MonoBehaviour
6 | {
7 | public Animator animator;
8 | public Transform bone;
9 |
10 | PlayableGraph graph;
11 | AnimationScriptPlayable jobPlayable;
12 |
13 | void Start()
14 | {
15 | graph = PlayableGraph.Create("MyGraph");
16 | graph.SetTimeUpdateMode(DirectorUpdateMode.GameTime);
17 |
18 | var job = new MyJob
19 | {
20 | boneHandle = animator.BindStreamTransform(bone)
21 | };
22 |
23 | jobPlayable = AnimationScriptPlayable.Create(graph, job);
24 |
25 | var output = AnimationPlayableOutput.Create(graph, "Animation", animator);
26 | output.SetSourcePlayable(jobPlayable);
27 |
28 | graph.Play();
29 | }
30 | void OnDestroy()
31 | {
32 | graph.Destroy();
33 | }
34 | }
35 | public struct MyJob : IAnimationJob
36 | {
37 | public TransformStreamHandle boneHandle;
38 |
39 | public void ProcessAnimation(AnimationStream stream)
40 | {
41 | Vector3 pos = boneHandle.GetPosition(stream);
42 | pos += Vector3.up * 0.01f; // 每帧让骨骼上升
43 | boneHandle.SetPosition(stream, pos);
44 | }
45 |
46 | public void ProcessRootMotion(AnimationStream stream) { }
47 | }
48 |
--------------------------------------------------------------------------------
/Unity_025/code/TimelineTestForAnimationWeightInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using UnityEditor;
3 | using UnityEngine;
4 | using UnityEngine.Animations;
5 | using UnityEngine.InputSystem;
6 | using UnityEngine.Playables;
7 | using UnityEngine.Serialization;
8 |
9 | /**
10 | * 测试 Animation WeightInfo
11 | * 也能测试 多AnimationTrack下动画效果
12 | * 搭配 WeightControllerEditor 使用
13 | * @Author RingleaderWang
14 | */
15 | public class TimelineTestForAnimationWeightInfo : MonoBehaviour
16 | {
17 | public AnimationClip clip;
18 | public AnimationClip clip2;
19 | public AnimationClip clip3;
20 | public AnimationClip clip4;
21 | public AnimationClip clip5;
22 | public AnimationClip clip6;
23 | public AnimationClip clip7;
24 | public AnimationClip clip8;
25 | private PlayableGraph graph;
26 |
27 | // 用new InputSystem
28 | public void BackspaceDown(InputAction.CallbackContext ctx)
29 | {
30 | if (ctx.performed)
31 | {
32 | TestAnimationWeightInfo();
33 | }
34 | }
35 |
36 | public void OnDestroy()
37 | {
38 | DestoryGraph(graph);
39 | }
40 |
41 | void DestoryGraph(PlayableGraph graph)
42 | {
43 | if (graph.IsValid())
44 | {
45 | graph.Destroy();
46 | }
47 | }
48 | [Range(0, 2)]
49 | public float weight_p1 = 1;
50 | [Range(0, 2)]
51 | public float weight_p2 = 1;
52 | [Range(0, 2)]
53 | public float weight_p3 = 1;
54 | [Range(0, 2)]
55 | public float weight_p4 = 1;
56 | [Range(0, 2)]
57 | public float weight_p5 = 1;
58 | [Range(0, 2)]
59 | public float weight_p6 = 1;
60 | [Range(0, 2)]
61 | public float weight_p7 = 1;
62 | [Range(0, 2)]
63 | public float weight_p8 = 1;
64 |
65 |
66 | [Range(0, 2)]
67 | public float weight_mixer12 = 1;
68 | [Range(0, 2)]
69 | public float weight_mixer34 = 1;
70 | [Range(0, 2)]
71 | public float weight_mixer56 = 1;
72 | [Range(0, 2)]
73 | public float weight_mixer78 = 1;
74 |
75 | [Range(0, 2)]
76 | public float weight_max = 1;
77 |
78 | public bool createOutput1Last = false;
79 |
80 | List m_Mixers1 = new List();
81 | List m_Mixers2 = new List();
82 | private void TestAnimationWeightInfo()
83 | {
84 | DestoryGraph(graph);
85 | graph = PlayableGraph.Create("TestAnimationWeightInfo");
86 | var animator = GetComponent();
87 | var timelinePlayable = Playable.Create(graph,5);
88 | timelinePlayable.SetOutputCount(5);
89 | timelinePlayable.SetTraversalMode(PlayableTraversalMode.Passthrough);
90 |
91 | AnimationPlayableOutput animationOutput1;
92 | AnimationPlayableOutput animationOutput2;
93 | // 构建图中的各节点
94 | if (!createOutput1Last)
95 | {
96 | animationOutput1 = CreateOutput1(animator,timelinePlayable);
97 | animationOutput2 = CreateOutput2(animator,timelinePlayable);
98 | }
99 | else
100 | {
101 | // 让animationOutput1最后建立,这样1会覆盖2
102 | animationOutput2 = CreateOutput2(animator,timelinePlayable);
103 | animationOutput1 = CreateOutput1(animator,timelinePlayable);
104 | }
105 |
106 | // 设animationOutput weight 为 0,并构建各自的 WeightInfo
107 | ProcessWeightInfo(animationOutput1, m_Mixers1);
108 | ProcessWeightInfo(animationOutput2, m_Mixers2);
109 |
110 |
111 | Evaluate(animationOutput1, m_Mixers1);
112 | Evaluate(animationOutput2, m_Mixers2);
113 | // 5. 播放图谱
114 | graph.Play();
115 | }
116 | private AnimationPlayableOutput CreateOutput1(Animator animator, Playable timelinePlayable)
117 | {
118 | var animationOutput1 = AnimationPlayableOutput.Create(graph, "Animation", animator);
119 | // p1 p2 连接mixer12
120 | var p1 = AnimationClipPlayable.Create(graph, clip);
121 | p1.SetInputCount(1);
122 | var p2 = AnimationClipPlayable.Create(graph, clip2);
123 | p2.SetInputCount(2);
124 | var mixer12 = AnimationMixerPlayable.Create(graph,2);
125 | mixer12.ConnectInput(0,p1,0, weight_p1);
126 | mixer12.ConnectInput(1,p2,0, weight_p2);
127 |
128 | // p3 p4 连接mixer34
129 | var p3 = AnimationClipPlayable.Create(graph, clip3);
130 | p3.SetInputCount(3);
131 | var p4 = AnimationClipPlayable.Create(graph, clip4);
132 | p4.SetInputCount(4);
133 | var mixer34 = AnimationMixerPlayable.Create(graph,2);
134 | mixer34.ConnectInput(0,p3,0, weight_p3);
135 | mixer34.ConnectInput(1,p4,0, weight_p4);
136 |
137 | // 连接 mixer12 mixer34 到 layer 1
138 | var layer1 = AnimationLayerMixerPlayable.Create(graph,2,false);
139 | layer1.ConnectInput(0,mixer12,0, weight_mixer12);
140 | layer1.ConnectInput(1,mixer34,0, weight_mixer34);
141 |
142 | // 连接layer 到 timelinePlayable
143 | timelinePlayable.ConnectInput(0,layer1,0, 1); // 默认权重为1
144 |
145 | // 将 timelinePlayable 连接 playableOutput
146 | animationOutput1.SetSourcePlayable(timelinePlayable);
147 | return animationOutput1;
148 | }
149 | private AnimationPlayableOutput CreateOutput2(Animator animator, Playable timelinePlayable)
150 | {
151 | var animationOutput2 = AnimationPlayableOutput.Create(graph, "Animation", animator);
152 |
153 | // p5 p6 连接mixer56
154 | var p5 = AnimationClipPlayable.Create(graph, clip5);
155 | p5.SetInputCount(5);
156 | var p6 = AnimationClipPlayable.Create(graph, clip6);
157 | p6.SetInputCount(6);
158 | var mixer56 = AnimationMixerPlayable.Create(graph,2);
159 | mixer56.ConnectInput(0,p5,0, weight_p5);
160 | mixer56.ConnectInput(1,p6,0, weight_p6);
161 |
162 | // p7 p8 连接mixer78
163 | var p7 = AnimationClipPlayable.Create(graph, clip7);
164 | p7.SetInputCount(7);
165 | var p8 = AnimationClipPlayable.Create(graph, clip8);
166 | p8.SetInputCount(8);
167 | var mixer78 = AnimationMixerPlayable.Create(graph,2);
168 | mixer78.ConnectInput(0,p7,0, weight_p7);
169 | mixer78.ConnectInput(1,p8,0, weight_p8);
170 |
171 | // 连接 mixer56 mixer78 到 layer 2
172 | var layer2 = AnimationLayerMixerPlayable.Create(graph,2,false);
173 | layer2.ConnectInput(0,mixer56,0, weight_mixer56);
174 | layer2.ConnectInput(1,mixer78,0, weight_mixer78);
175 | // 连接layer 到 timelinePlayable
176 | timelinePlayable.ConnectInput(1,layer2,0, 1);//默认权重为1
177 |
178 | // 将 timelinePlayable 连接 playableOutput
179 | animationOutput2.SetSourcePlayable(timelinePlayable,1);
180 | return animationOutput2;
181 | }
182 |
183 |
184 | // 拷贝Timeline中Animation相关的处理
185 |
186 | #region weight info
187 | struct WeightInfo
188 | {
189 | public Playable mixer;
190 | public Playable parentMixer;
191 | public int port;
192 | }
193 | void ProcessWeightInfo(AnimationPlayableOutput output, List mixers)
194 | {
195 | output.SetWeight(0);
196 | FindMixers(output, mixers);
197 | }
198 |
199 | void FindMixers(AnimationPlayableOutput output, List mixers)
200 | {
201 | var playable = output.GetSourcePlayable();
202 | var outputPort = output.GetSourceOutputPort();
203 | mixers.Clear();
204 | FindMixers(playable, outputPort, playable.GetInput(outputPort), mixers);
205 | }
206 |
207 | // Recursively accumulates mixers.
208 | void FindMixers(Playable parent, int port, Playable node, List mixers)
209 | {
210 | if (!node.IsValid())
211 | return;
212 |
213 | var type = node.GetPlayableType();
214 | if (type == typeof(AnimationMixerPlayable) || type == typeof(AnimationLayerMixerPlayable))
215 | {
216 | // use post fix traversal so children come before parents
217 | int subCount = node.GetInputCount();
218 | for (int j = 0; j < subCount; j++)
219 | {
220 | FindMixers(node, j, node.GetInput(j), mixers);
221 | }
222 |
223 | // if we encounter a layer mixer, we assume there is nesting occuring
224 | // and we modulate the weight instead of overwriting it.
225 | var weightInfo = new WeightInfo
226 | {
227 | parentMixer = parent,
228 | mixer = node,
229 | port = port,
230 | };
231 | mixers.Add(weightInfo);
232 | }
233 | else
234 | {
235 | var count = node.GetInputCount();
236 | for (var i = 0; i < count; i++)
237 | {
238 | FindMixers(parent, port, node.GetInput(i), mixers);
239 | }
240 | }
241 | }
242 |
243 | void Evaluate(AnimationPlayableOutput output, List mixers)
244 | {
245 | float weight = 1;
246 | output.SetWeight(1);
247 | for (int i = 0; i < mixers.Count; i++)
248 | {
249 | var mixInfo = mixers[i];
250 | weight = NormalizeMixer(mixInfo.mixer);
251 | mixInfo.parentMixer.SetInputWeight(mixInfo.port, weight);
252 | }
253 |
254 | // only write the final weight in player/playmode. In editor, we are blending to the appropriate defaults
255 | // the last mixer in the list is the final blend, since the list is composed post-order.
256 | if (Application.isPlaying)
257 | output.SetWeight(weight);
258 | }
259 | // Given a mixer, normalizes the mixer if required
260 | // returns the output weight that should be applied to the mixer as input
261 | private float NormalizeMixer(Playable mixer)
262 | {
263 | if (!mixer.IsValid())
264 | return 0;
265 | int count = mixer.GetInputCount();
266 | float weight = 0.0f;
267 | for (int c = 0; c < count; c++)
268 | {
269 | weight += mixer.GetInputWeight(c);
270 | }
271 |
272 | if (weight > Mathf.Epsilon && weight < 1)
273 | {
274 | for (int c = 0; c < count; c++)
275 | {
276 | mixer.SetInputWeight(c, mixer.GetInputWeight(c) / weight);
277 | }
278 | }
279 | return Mathf.Clamp01(weight);
280 | }
281 |
282 | #endregion
283 |
284 | }
285 | [CustomEditor(typeof(TimelineTestForAnimationWeightInfo))]
286 | public class WeightControllerEditor : Editor
287 | {
288 | public override void OnInspectorGUI()
289 | {
290 | DrawDefaultInspector(); // 显示默认变量
291 |
292 | TimelineTestForAnimationWeightInfo wc = (TimelineTestForAnimationWeightInfo)target;
293 | float weight_max = wc.weight_max;
294 |
295 | GUILayout.Space(10);
296 | GUILayout.Label("批量操作", EditorStyles.boldLabel);
297 |
298 | if (GUILayout.Button("重置为 0"))
299 | {
300 | SetAllWeights(wc, 0f);
301 | }
302 |
303 | if (GUILayout.Button("重置为 1"))
304 | {
305 | SetAllWeights(wc, 1f);
306 | }
307 |
308 | if (GUILayout.Button("随机 0~1"))
309 | {
310 | wc.weight_p1 = Round2(Random.Range(0f, weight_max));
311 | wc.weight_p2 = Round2(Random.Range(0f, weight_max));
312 | wc.weight_p3 = Round2(Random.Range(0f, weight_max));
313 | wc.weight_p4 = Round2(Random.Range(0f, weight_max));
314 | wc.weight_p5 = Round2(Random.Range(0f, weight_max));
315 | wc.weight_p6 = Round2(Random.Range(0f, weight_max));
316 | wc.weight_p7 = Round2(Random.Range(0f, weight_max));
317 | wc.weight_p8 = Round2(Random.Range(0f, weight_max));
318 | EditorUtility.SetDirty(wc);
319 | }
320 | }
321 | float Round2(float value)
322 | {
323 | return Mathf.Round(value * 100f) / 100f;
324 | }
325 |
326 | void SetAllWeights(TimelineTestForAnimationWeightInfo wc, float value)
327 | {
328 | wc.weight_p1 = value;
329 | wc.weight_p2 = value;
330 | wc.weight_p3 = value;
331 | wc.weight_p4 = value;
332 | wc.weight_p5 = value;
333 | wc.weight_p6 = value;
334 | wc.weight_p7 = value;
335 | wc.weight_p8 = value;
336 | EditorUtility.SetDirty(wc); // 标记脏数据以支持保存
337 | }
338 | }
339 |
--------------------------------------------------------------------------------
/Unity_025/code/TimelineTestForCreateGraph.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Unity.VisualScripting;
3 | using UnityEngine;
4 | using UnityEngine.Animations;
5 | using UnityEngine.InputSystem;
6 | using UnityEngine.Playables;
7 |
8 | public class TimelineTestForCreateGraph : MonoBehaviour
9 | {
10 | public AnimationClip clip;
11 | private PlayableGraph graph;
12 | public GameObject directorplayerGO;
13 | private PlayableDirector director;
14 | public bool enableDirector = true;
15 | private void Awake()
16 | {
17 | director = directorplayerGO.GetComponent();
18 | director.enabled = enableDirector;
19 | }
20 |
21 | private void Start()
22 | {
23 | Debug.Log("director started");
24 | }
25 |
26 | public void OnEnable()
27 | {
28 | director.played += OnPlayed;
29 | Debug.Log("director enabled");
30 | }
31 | void OnPlayed(PlayableDirector director)
32 | {
33 | Debug.Log("播放开始了!名字:" + director.name);
34 | }
35 |
36 | public void BackspaceDown(InputAction.CallbackContext ctx)
37 | {
38 | if (ctx.performed)
39 | {
40 | PlayDirector();
41 | }
42 | }
43 |
44 | public void OnDisable()
45 | {
46 | if (graph.IsValid())
47 | {
48 | graph.Destroy();
49 | }
50 |
51 | }
52 |
53 | private void PlayDirector()
54 | {
55 | director.Play();
56 | }
57 |
58 | private void CreateNewGraph()
59 | {
60 | // 1. 创建一个 PlayableGraph 实例
61 | graph = PlayableGraph.Create("MyPlayableGraph");
62 | Debug.Log("****************************** 开始自定义Timeline ***************************");
63 | // 2. 创建一个 AnimationPlayableOutput,并将其输出连接到当前 GameObject 的 Animator
64 | var animationOutput = AnimationPlayableOutput.Create(graph, "Animation", GetComponent());
65 |
66 | // 3. 将 AnimationClip 包装成 Playable
67 | var clipPlayable = AnimationClipPlayable.Create(graph, clip);
68 |
69 | // 4. 将 Playable 连接到输出
70 | animationOutput.SetSourcePlayable(clipPlayable);
71 |
72 | // 5. 播放图谱
73 | graph.Play();
74 | Debug.Log("xxxxxxxxxxxxxxxxxx 结束自定义Timeline xxxxxxxxxxxxxxxxxxxxxx");
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/Unity_025/code/TimelineTestForEvaluateAndProcessFrame.cs:
--------------------------------------------------------------------------------
1 | using UnityEditor;
2 | using UnityEngine;
3 | using UnityEngine.Animations;
4 | using UnityEngine.Playables;
5 |
6 | public class TimelineTestForEvaluateAndProcessFrame : MonoBehaviour
7 | {
8 | private PlayableGraph graph;
9 | public AnimationClip clip1;
10 | [Range(0,1)]
11 | public float deltaTime;
12 |
13 | private void OnDestroy()
14 | {
15 | DestroyGraph();
16 | }
17 |
18 | public void DestroyGraph()
19 | {
20 | if (graph.IsValid())
21 | {
22 | graph.Destroy();
23 | }
24 | }
25 |
26 | public void CreateGraph()
27 | {
28 | graph = PlayableGraph.Create("evaluate graph");
29 | var animationClipPlayable = AnimationClipPlayable.Create(graph,clip1);
30 | var output = AnimationPlayableOutput.Create(graph, "anim",GetComponent());
31 | output.SetSourcePlayable(animationClipPlayable);
32 | }
33 |
34 | public void Evaluate()
35 | {
36 | graph.Evaluate(deltaTime);
37 | }
38 | }
39 | [CustomEditor(typeof(TimelineTestForEvaluateAndProcessFrame))]
40 | class TimelineTestForEvaluateAndProcessFrameEditor : Editor
41 | {
42 | public override void OnInspectorGUI()
43 | {
44 | base.OnInspectorGUI();
45 | TimelineTestForEvaluateAndProcessFrame script = (TimelineTestForEvaluateAndProcessFrame)target;
46 | if (GUILayout.Button("CreateGraph"))
47 | {
48 | script.CreateGraph();
49 | }
50 |
51 | if (GUILayout.Button("Evaluate"))
52 | {
53 | script.Evaluate();
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Unity_025/code/TimelineTestForIntervalTreeDemo.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using UnityEditor;
4 | using UnityEngine;
5 | using UnityEngine.Serialization;
6 |
7 | public class TimelineTestForIntervalTreeDemo : MonoBehaviour
8 | {
9 | public int num;
10 | [Range(1,10)]
11 | public int lengthRange = 10;
12 | [Range(10,50)]
13 | public int posMaxRange = 50;// 生成的cube start(即左端点)最大值范围
14 | [Range(0,1)]
15 | public float gap = 0.3f;//长方体间y轴间隙
16 |
17 | [Range(1,10)]
18 | public int _minNodeSize = 3; // the minimum number of entries to have subnodes
19 |
20 |
21 | internal struct Entry
22 | {
23 | public Int64 intervalStart;
24 | public Int64 intervalEnd;
25 | }
26 | struct IntervalTreeNode // interval node,
27 | {
28 | public Int64 center; // midpoint for this node
29 | public int first; // index of first element of this node in m_Entries
30 | public int last; // index of the last element of this node in m_Entries
31 | public int left; // index in m_Nodes of the left subnode
32 | public int right; // index in m_Nodes of the right subnode
33 | }
34 |
35 | const int kInvalidNode = -1;
36 | const Int64 kCenterUnknown = Int64.MaxValue; // center hasn't been calculated. indicates no children
37 |
38 | List m_Entries = new List();
39 | public List m_cubes = new List();
40 | List m_Nodes = new List();
41 |
42 | private GameObject cubeRoot;
43 |
44 | [ContextMenu("生成随机Cube并构建区间树")]
45 | public void CreateRandomNumRect()
46 | {
47 | // 清除旧的
48 | if (cubeRoot != null)
49 | {
50 | #if UNITY_EDITOR
51 | DestroyImmediate(cubeRoot);
52 | #else
53 | Destroy(cubeRoot);
54 | #endif
55 | }
56 |
57 | cubeRoot = new GameObject("CubeRoot");
58 | m_Entries.Clear();
59 | m_Nodes.Clear();
60 | m_cubes.Clear();
61 |
62 | for (int i = 0; i < num; i++)
63 | {
64 | int start = UnityEngine.Random.Range(0, posMaxRange);
65 | int length = UnityEngine.Random.Range(1, lengthRange + 1);
66 | int end = start + length;
67 |
68 | m_Entries.Add(new Entry
69 | {
70 | intervalStart = start,
71 | intervalEnd = end
72 | });
73 |
74 | GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
75 | cube.transform.localScale = new Vector3(length, 0.5f, 1);
76 | cube.transform.position = new Vector3(start + length / 2f, i * (0.5f + gap), 0);
77 | cube.name = $"Cube_{i}_{start}_{end}";
78 | cube.transform.SetParent(cubeRoot.transform);
79 | m_cubes.Add(cube);
80 | }
81 | Debug.Log($"构建完成,节点数: {m_Entries.Count}");
82 | }
83 |
84 | public void Rebuild()
85 | {
86 | m_Nodes.Clear();
87 | m_Nodes.Capacity = m_Entries.Capacity;
88 | Rebuild(0, m_Entries.Count - 1);
89 | }
90 |
91 | private int Rebuild(int start, int end)
92 | {
93 | IntervalTreeNode intervalTreeNode = new IntervalTreeNode();
94 |
95 | // minimum size, don't subdivide
96 | int count = end - start + 1;
97 | if (count < _minNodeSize)
98 | {
99 | intervalTreeNode = new IntervalTreeNode() { center = kCenterUnknown, first = start, last = end, left = kInvalidNode, right = kInvalidNode };
100 | m_Nodes.Add(intervalTreeNode);
101 | return m_Nodes.Count - 1;
102 | }
103 |
104 | var min = Int64.MaxValue;
105 | var max = Int64.MinValue;
106 |
107 | for (int i = start; i <= end; i++)
108 | {
109 | var o = m_Entries[i];
110 | min = Math.Min(min, o.intervalStart);
111 | max = Math.Max(max, o.intervalEnd);
112 | }
113 |
114 | var center = (max + min) / 2;
115 | intervalTreeNode.center = center;
116 |
117 | // first pass, put every thing left of center, left
118 | int x = start;
119 | int y = end;
120 | while (true)
121 | {
122 | while (x <= end && m_Entries[x].intervalEnd < center)
123 | x++;
124 |
125 | while (y >= start && m_Entries[y].intervalEnd >= center)
126 | y--;
127 |
128 | if (x > y)
129 | break;
130 |
131 | var nodeX = m_Entries[x];
132 | var nodeY = m_Entries[y];
133 |
134 | m_Entries[y] = nodeX;
135 | m_Entries[x] = nodeY;
136 | ChangeCubeYPos(x, y, m_cubes);
137 | }
138 |
139 | intervalTreeNode.first = x;
140 |
141 | // second pass, put every start passed the center right
142 | y = end;
143 | while (true)
144 | {
145 | while (x <= end && m_Entries[x].intervalStart <= center)
146 | x++;
147 |
148 | while (y >= start && m_Entries[y].intervalStart > center)
149 | y--;
150 |
151 | if (x > y)
152 | break;
153 |
154 | var nodeX = m_Entries[x];
155 | var nodeY = m_Entries[y];
156 |
157 | m_Entries[y] = nodeX;
158 | m_Entries[x] = nodeY;
159 | ChangeCubeYPos(x, y, m_cubes);
160 | }
161 |
162 | intervalTreeNode.last = y;
163 |
164 | // reserve a place
165 | m_Nodes.Add(new IntervalTreeNode());
166 | int index = m_Nodes.Count - 1;
167 |
168 | intervalTreeNode.left = kInvalidNode;
169 | intervalTreeNode.right = kInvalidNode;
170 |
171 | if (start < intervalTreeNode.first)
172 | intervalTreeNode.left = Rebuild(start, intervalTreeNode.first - 1);
173 |
174 | if (end > intervalTreeNode.last)
175 | intervalTreeNode.right = Rebuild(intervalTreeNode.last + 1, end);
176 |
177 | m_Nodes[index] = intervalTreeNode;
178 | return index;
179 | }
180 |
181 | // 交换两个cube 的y值
182 | private void ChangeCubeYPos(int i, int j, List mCubes)
183 | {
184 | var transformI = m_cubes[i].transform;
185 | var transformJ = m_cubes[j].transform;
186 | var temp = transformI.position.y;
187 | transformI.position = new Vector3(transformI.position.x, transformJ.position.y, transformI.position.z);
188 | transformJ.position = new Vector3(transformJ.position.x, temp, transformJ.position.z);
189 | // Swap via deconstruction通 过解构进行交换 gameObject
190 | (m_cubes[j], m_cubes[i]) = (m_cubes[i], m_cubes[j]);
191 | }
192 | }
193 | [CustomEditor(typeof(TimelineTestForIntervalTreeDemo))]
194 | class TimelineTestForIntervalTreeDemoEditor : Editor
195 | {
196 | public override void OnInspectorGUI()
197 | {
198 | base.OnInspectorGUI();
199 | var script = (TimelineTestForIntervalTreeDemo)target;
200 | if (GUILayout.Button("create"))
201 | {
202 | script.CreateRandomNumRect();
203 | }
204 |
205 | if (GUILayout.Button("rebuild"))
206 | {
207 | script.Rebuild();
208 | }
209 | }
210 | }
211 |
--------------------------------------------------------------------------------
/Unity_025/code/TimelineTestForLifeCycle.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Scenes.TimelineTest.scripts;
3 | using UnityEditor;
4 | using UnityEngine;
5 | using UnityEngine.Animations;
6 | using UnityEngine.Playables;
7 |
8 | public class TimelineTestForLifeCycle : MonoBehaviour
9 | {
10 | public PlayableGraph graph;
11 |
12 | private ScriptPlayable playable3;
13 | private ScriptPlayable playable5;
14 | public void CreateGraph()
15 | {
16 | DestroyGraph();
17 | graph = PlayableGraph.Create("TimelineTestForLifeCycle");
18 | ScriptPlayableOutput output = ScriptPlayableOutput.Create(graph,"LifeCycleTestOutput");
19 | playable5 = ScriptPlayable.Create(graph,5);
20 | playable5.SetOutputCount(2);
21 | var playable4 = ScriptPlayable.Create(graph,4);
22 | playable3 = ScriptPlayable.Create(graph,3);
23 | var playable2 = ScriptPlayable.Create(graph,2);
24 | var playable1 = ScriptPlayable.Create(graph,1);
25 | output.SetSourcePlayable(playable5,0);
26 | output.SetWeight(1);//验证下默认权重多少,我猜是1
27 | playable5.ConnectInput(0,playable1,0,1);
28 | playable5.ConnectInput(1,playable2,0,0.5f);
29 | playable5.ConnectInput(2,playable3,0,1);
30 | playable5.ConnectInput(3,playable4,0,0);
31 |
32 | var playable9 = ScriptPlayable.Create(graph,9);
33 | var playable8 = ScriptPlayable.Create(graph,8);
34 | playable9.ConnectInput(0,playable8,0,1);
35 |
36 | animationScriptPlayable = AnimationScriptPlayable.Create(graph,new DebugAnimationJob(),1);
37 | animationScriptPlayable.SetOutputCount(2);
38 | animationPlayableOutput = AnimationPlayableOutput.Create(graph,"animationScriptPlayable", GetComponent());
39 |
40 | // 验证触发遍历的是playableOutput而不是root playable
41 | var playable6 = ScriptPlayable.Create(graph,6);
42 | playable6.ConnectInput(0,playable5,1);
43 | }
44 |
45 | private AnimationScriptPlayable animationScriptPlayable;
46 | private AnimationPlayableOutput animationPlayableOutput;
47 |
48 | public void ConnectAnimationScript2Output()
49 | {
50 | animationPlayableOutput.SetSourcePlayable(animationScriptPlayable,0);
51 | }
52 | public void ConnectAnimationScript2Playable5()
53 | {
54 | playable5.ConnectInput(4,animationScriptPlayable,1,1);
55 | }
56 | public void PlayPlayable3()
57 | {
58 | playable3.Play();
59 | }
60 | public void PausePlayable3()
61 | {
62 | playable3.Pause();
63 | }
64 | public void DestroyPlayable3()
65 | {
66 | playable3.Destroy();
67 | }
68 |
69 | public void PlayGraph()
70 | {
71 | graph.Play();
72 | }
73 | public void StopGraph()
74 | {
75 | graph.Stop();
76 | }
77 | private void OnDestroy()
78 | {
79 | DestroyGraph();
80 | }
81 |
82 | public void DestroyGraph()
83 | {
84 | if (graph.IsValid())
85 | {
86 | Debug.Log("Execute Graph Destroy");
87 | graph.Destroy();
88 | Debug.Log("Graph destroyed");
89 | }
90 | }
91 | }
92 |
93 | [CustomEditor(typeof(TimelineTestForLifeCycle))]
94 | class TimelineTestForLifeCycleEditor : Editor
95 | {
96 | public override void OnInspectorGUI()
97 | {
98 | base.OnInspectorGUI();
99 | TimelineTestForLifeCycle script = (TimelineTestForLifeCycle) target;
100 | if (GUILayout.Button("CreateGraph"))
101 | {
102 | script.CreateGraph();
103 | }
104 | if (GUILayout.Button("PlayGraph"))
105 | {
106 | script.PlayGraph();
107 | }
108 | if (GUILayout.Button("Stop graph"))
109 | {
110 | script.StopGraph();
111 | }
112 | if (GUILayout.Button("Destroy graph"))
113 | {
114 | script.DestroyGraph();
115 | }
116 |
117 | if (GUILayout.Button("Play Playable3"))
118 | {
119 | script.PlayPlayable3();
120 | }
121 |
122 | if (GUILayout.Button("Pause Playable3"))
123 | {
124 | script.PausePlayable3();
125 | }
126 | if (GUILayout.Button("Destroy Playable3"))
127 | {
128 | script.DestroyPlayable3();
129 | }
130 |
131 | if (GUILayout.Button("连接AnimScript 2 Output"))
132 | {
133 | script.ConnectAnimationScript2Output();
134 | }
135 | if (GUILayout.Button("连接AnimScript 2 Playable5"))
136 | {
137 | script.ConnectAnimationScript2Playable5();
138 | }
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/Unity_025/code/TimelineTestForOutputEvaluate.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using Scenes.TimelineTest.scripts;
4 | using Unity.VisualScripting;
5 | using UnityEngine;
6 | using UnityEngine.Animations;
7 | using UnityEngine.InputSystem;
8 | using UnityEngine.Playables;
9 | using UnityEngine.Timeline;
10 |
11 | public class TimelineTestForOutputEvaluate : MonoBehaviour
12 | {
13 | public AnimationClip clip;
14 | public AnimationClip clip2;
15 | public int port;
16 | private PlayableGraph graph;
17 | private PlayableGraph notconnectOutputGraph;
18 | private PlayableDirector director;
19 |
20 | [Tooltip("false则mode是passthrough,true则mode是mix")]
21 | public bool changeTraversalMode = false;
22 | [Tooltip("false则PlayableOutput的source为playable1,true则为playable2")]
23 | public bool changeSourcePlayable = false;
24 |
25 |
26 | public void BackspaceDown(InputAction.CallbackContext ctx)
27 | {
28 | if (ctx.performed)
29 | {
30 | // CreateNewGraph();
31 | // CreateMixerGraph();
32 | // ValidateTraversalMode();
33 | // CreateMultiRootNode();
34 | // ValidateTraversalMode2(changeTraversalMode,changeSourcePlayable);
35 | // TopoTest1();
36 | // TestPlayPlayable();
37 | }
38 | }
39 | public void ShiftDown(InputAction.CallbackContext ctx)
40 | {
41 | if (ctx.performed)
42 | {
43 | // NoOutput();
44 | }
45 | }
46 |
47 | public void OnDestroy()
48 | {
49 | DestoryGraph(graph);
50 | DestoryGraph(notconnectOutputGraph);
51 | }
52 |
53 | void DestoryGraph(PlayableGraph graph)
54 | {
55 | if (graph.IsValid())
56 | {
57 | graph.Destroy();
58 | }
59 | }
60 | // 验证下直接对含animation clip的playable play() 会怎样?
61 | private float cnt = 0;
62 | private void TestPlayPlayable()
63 | {
64 | DestoryGraph(graph);
65 | graph = PlayableGraph.Create();
66 | var playable = AnimationClipPlayable.Create(graph,clip);
67 | var output = AnimationPlayableOutput.Create(graph, "TestPlayPlayableGraph", GetComponent());
68 | output.SetSourcePlayable(playable);
69 | cnt += 0.1f;
70 | playable.Pause();
71 | playable.SetTime(cnt);
72 | }
73 | // 会产生回环,建立会出现死循环,unity editor会奔溃闪退
74 | private void TopoTest2()
75 | {
76 | // DestoryGraph(graph);
77 | // graph = PlayableGraph.Create();
78 | // var playable1 = Playable.Create(graph,1);
79 | // var playable2 = Playable.Create(graph,2);
80 | // var playable3 = Playable.Create(graph,3);
81 | // playable2.ConnectInput(0,playable3,0);
82 | // playable3.ConnectInput(0,playable1,0);
83 | // playable1.ConnectInput(0,playable2,0);
84 | //
85 | // ScriptPlayableOutput output = ScriptPlayableOutput.Create(graph, "customOutput");
86 | // output.SetSourcePlayable(playable2);
87 | // graph.Play();
88 | }
89 | // 父子循环
90 | private void TopoTest3()
91 | {
92 | // DestoryGraph(graph);
93 | // graph = PlayableGraph.Create();
94 | // var playable1 = Playable.Create(graph,1);
95 | // var playable2 = Playable.Create(graph,2);
96 | // var playable3 = Playable.Create(graph,3);
97 | // playable3.SetOutputCount(2);
98 | // playable2.ConnectInput(0,playable3,0);
99 | // playable3.ConnectInput(0,playable1,0);
100 | // playable1.ConnectInput(0,playable3,1);
101 | //
102 | // ScriptPlayableOutput output = ScriptPlayableOutput.Create(graph, "customOutput");
103 | // output.SetSourcePlayable(playable2);
104 | // graph.Play();
105 | }
106 |
107 | private void TopTes()
108 | {
109 | DestoryGraph(graph);
110 | graph = PlayableGraph.Create();
111 | var playable1 = Playable.Create(graph,1);
112 | var playable2 = Playable.Create(graph,2);
113 | playable1.ConnectInput(0, playable2,0,2);
114 | }
115 | private void TopoTest1()
116 | {
117 | DestoryGraph(graph);
118 | graph = PlayableGraph.Create();
119 | var playable1 = Playable.Create(graph,1);
120 | playable1.SetOutputCount(2);
121 | var playable2 = Playable.Create(graph,2);
122 | var playable3 = Playable.Create(graph,3);
123 | var playable4 = Playable.Create(graph,4);
124 | playable3.ConnectInput(0, playable1,0);
125 | playable4.ConnectInput(0, playable1,1);
126 | playable2.ConnectInput(0, playable3,0);
127 | playable2.ConnectInput(1, playable4,0);
128 | ScriptPlayableOutput output = ScriptPlayableOutput.Create(graph, "customOutput");
129 | output.SetSourcePlayable(playable2);
130 | graph.Play();
131 | }
132 | private void ValidateTraversalMode2(bool _changeTraversalMode,bool _changeSourcePlayable)
133 | {
134 | DestoryGraph(graph);
135 | graph = PlayableGraph.Create();
136 | var playable1 = Playable.Create(graph,1);
137 | var playable2 = Playable.Create(graph,2);
138 | var playable3 = Playable.Create(graph);
139 | playable3.SetTraversalMode(!_changeTraversalMode?PlayableTraversalMode.Passthrough:PlayableTraversalMode.Mix);
140 | var playable4 = Playable.Create(graph,4);
141 | var playable5 = Playable.Create(graph,5);
142 |
143 | playable3.SetInputCount(3);
144 | playable3.SetOutputCount(4);
145 |
146 | graph.Connect(playable3, 0, playable1, 0);
147 | graph.Connect(playable3, 1, playable2, 0);
148 | graph.Connect(playable4, 0, playable3, 0);
149 | graph.Connect(playable5, 0, playable3, 1);
150 |
151 | ScriptPlayableOutput output = ScriptPlayableOutput.Create(graph, "customOutput");
152 | output.SetSourcePlayable(!_changeSourcePlayable?playable1:playable2,100);//应该就playable4运行
153 | graph.Play();
154 | }
155 |
156 | private void CreateMultiRootNode()
157 | {
158 | DestoryGraph(graph);
159 | graph = PlayableGraph.Create();
160 | var playable1 = Playable.Create(graph, 2);
161 | var playable2 = Playable.Create(graph, 2);
162 | var subPlayable = Playable.Create(graph, 1);
163 |
164 | Debug.Log($"playable1: {playable1.GetHandle().GetHashCode()}");
165 | Debug.Log($"playable2: {playable2.GetHandle().GetHashCode()}");
166 | Debug.Log($"subPlayable: {subPlayable.GetHandle().GetHashCode()}");
167 |
168 | for (int i = 0; i < graph.GetRootPlayableCount(); i++)
169 | {
170 | var rootPlayable = graph.GetRootPlayable(i);
171 | Debug.Log($"rootPlayable: {rootPlayable.GetHandle().GetHashCode()}");
172 | }
173 | Debug.Log("******** 添加子节点");
174 | graph.Connect(subPlayable, 0, playable1, 0);
175 | for (int i = 0; i < graph.GetRootPlayableCount(); i++)
176 | {
177 | var rootPlayable = graph.GetRootPlayable(i);
178 | Debug.Log($"rootPlayable: {rootPlayable.GetHandle().GetHashCode()}");
179 | }
180 | Debug.Log("******** 将playable1添加outputPlayable");
181 | var animationOutput = AnimationPlayableOutput.Create(graph, "Animation", GetComponent());
182 | animationOutput.SetSourcePlayable(playable1);
183 | for (int i = 0; i < graph.GetRootPlayableCount(); i++)
184 | {
185 | var rootPlayable = graph.GetRootPlayable(i);
186 | Debug.Log($"rootPlayable: {rootPlayable.GetHandle().GetHashCode()}");
187 | }
188 | Debug.Log("******** playable2添加到sub playable ");
189 | graph.Connect(playable2, 0, subPlayable, 0);
190 | for (int i = 0; i < graph.GetRootPlayableCount(); i++)
191 | {
192 | var rootPlayable = graph.GetRootPlayable(i);
193 | Debug.Log($"rootPlayable: {rootPlayable.GetHandle().GetHashCode()}");
194 | }
195 | }
196 |
197 | private void CreateNewGraph()
198 | {
199 | DestoryGraph(graph);
200 | // 1. 创建一个 PlayableGraph 实例
201 | graph = PlayableGraph.Create("MyPlayableGraph");
202 | // 2. 创建一个 AnimationPlayableOutput,并将其输出连接到当前 GameObject 的 Animator
203 | var animationOutput = AnimationPlayableOutput.Create(graph, "Animation", GetComponent());
204 | // 3. 将 AnimationClip 包装成 Playable
205 | var clipPlayable = AnimationClipPlayable.Create(graph, clip);
206 | var clipPlayable2 = AnimationClipPlayable.Create(graph, clip2);
207 | // 4. 将 Playable 连接到输出
208 | animationOutput.SetSourcePlayable(clipPlayable2,1);
209 | animationOutput.SetSourcePlayable(clipPlayable,0);
210 | // 5. 播放图谱
211 | graph.Play();
212 | }
213 |
214 | private void CreateMixerGraph()
215 | {
216 | DestoryGraph(graph);
217 | // 1. 创建 PlayableGraph
218 | graph = PlayableGraph.Create("MixerPlayableGraph");
219 | // 2. 获取 Animator 并创建 AnimationPlayableOutput
220 | var animator = GetComponent();
221 | var animationOutput = AnimationPlayableOutput.Create(graph, "Animation", animator);
222 | // 3. 创建 AnimationClipPlayable
223 | var clipPlayable = AnimationClipPlayable.Create(graph, clip);
224 | var clipPlayable2 = AnimationClipPlayable.Create(graph, clip2);
225 | // 4. 创建一个 Mixer,管理多个动画输入
226 | AnimationMixerPlayable mixer = AnimationMixerPlayable.Create(graph, 2);
227 | mixer.ConnectInput(0, clipPlayable, 0,1);
228 | mixer.ConnectInput(1, clipPlayable2, 0,1);
229 | // 5. 设置 mixer 作为输出
230 | animationOutput.SetSourcePlayable(mixer);
231 | // 6. 播放图谱
232 | graph.Play();
233 | }
234 | void NoOutput()
235 | {
236 | DestoryGraph(notconnectOutputGraph);
237 | // 1. 创建一个 PlayableGraph 实例
238 | notconnectOutputGraph = PlayableGraph.Create("NoOutputGraph");
239 | // 2. 创建一个 AnimationPlayableOutput,并将其输出连接到当前 GameObject 的 Animator
240 | var animationOutput = AnimationPlayableOutput.Create(notconnectOutputGraph, "Animation", GetComponent());
241 | // 3. 将 AnimationClip 包装成 Playable
242 | var clipPlayable = AnimationClipPlayable.Create(notconnectOutputGraph, clip);
243 | // 4. 不将 Playable 连接到输出
244 | // animationOutput.SetSourcePlayable(clipPlayable);
245 | // 5. 播放图谱
246 | notconnectOutputGraph.Play();
247 | }
248 |
249 | void ValidateTraversalMode()
250 | {
251 | DestoryGraph(graph);
252 | graph = PlayableGraph.Create("TraversalModeGraph");
253 | ScriptPlayableOutput output = ScriptPlayableOutput.Create(graph, "customOutput");
254 | ScriptPlayableOutput output2 = ScriptPlayableOutput.Create(graph, "customOutput2");
255 | ScriptPlayableOutput output3 = ScriptPlayableOutput.Create(graph, "customOutput3");
256 | var playable = ScriptPlayable.Create(graph);
257 | var playable2 = ScriptPlayable.Create(graph);
258 | var playable3 = ScriptPlayable.Create(graph);
259 | var playable4 = ScriptPlayable.Create(graph);
260 | var playable5 = ScriptPlayable.Create(graph);
261 | var mixer = ScriptPlayable.Create(graph);
262 | mixer.SetTraversalMode(PlayableTraversalMode.Passthrough);//默认mode为mix,这里改为passthrough
263 | mixer.SetInputCount(10);// 很重要,否则连接报错:Connecting invalid input
264 | mixer.ConnectInput(4,playable,0,1);
265 | mixer.ConnectInput(6,playable2,0,1);
266 | mixer.ConnectInput(2,playable3,0,1);
267 | mixer.ConnectInput(8,playable4,0,1);
268 | mixer.ConnectInput(1,playable5,0,1);
269 | output.SetSourcePlayable(mixer,30);// 这个SetSourcePlayable的port根本没用,完全根据PlayableOutput顺序获取的
270 | output2.SetSourcePlayable(mixer,50);
271 | output3.SetSourcePlayable(mixer,100);
272 | Debug.Log($"output.GetSourcePlayable().GetHandle() == mixer.GetHandle() : {output.GetSourcePlayable().GetHandle() == mixer.GetHandle()}");//true
273 | Debug.Log($"output2.GetSourcePlayable().GetHandle() == mixer.GetHandle() : {output2.GetSourcePlayable().GetHandle() == mixer.GetHandle()}");//true
274 | Debug.Log($"output3.GetSourcePlayable().GetHandle() == mixer.GetHandle(): {output3.GetSourcePlayable().GetHandle() == mixer.GetHandle()}");//true
275 | graph.Play();
276 | }
277 | }
278 |
--------------------------------------------------------------------------------
/Unity_025/code/TimelineTestForScriptAnimation.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Reflection;
3 | using Scenes.TimelineTest.scripts;
4 | using Unity.Collections;
5 | using UnityEditor;
6 | using UnityEngine;
7 | using UnityEngine.Animations;
8 | using UnityEngine.Playables;
9 |
10 | public class TimelineTestForScriptAnimation : MonoBehaviour
11 | {
12 |
13 | public AnimationClip clipA;
14 | public AnimationClip clipB;
15 | public bool useAnimationScript;
16 |
17 | private PlayableGraph graph;
18 | private AnimationScriptPlayable scriptPlayableConectClipA2Mixer;
19 | private AnimationScriptPlayable scriptPlayableConectMixerA2Layer;
20 |
21 | public void Start()
22 | {
23 | DestroyGraph();
24 | var animator = GetComponent();
25 | graph = PlayableGraph.Create("TestGraph");
26 | graph.SetTimeUpdateMode(DirectorUpdateMode.GameTime);
27 |
28 | // scriptPlayableConectClipA2Mixer = AnimationScriptPlayable.Create(graph, new DebugAnimationJob("ClipA2Mixer"),1);
29 | scriptPlayableConectClipA2Mixer = AnimationScriptPlayable.Create(graph, new DebugAnimationJob(),1);
30 | scriptPlayableConectClipA2Mixer.SetInputCount(2);
31 | scriptPlayableConectClipA2Mixer.SetOutputCount(2);
32 | // scriptPlayableConectMixerA2Layer = AnimationScriptPlayable.Create(graph, new DebugAnimationJob("MixerA2Layer"),1);
33 | scriptPlayableConectMixerA2Layer = AnimationScriptPlayable.Create(graph, new DebugAnimationJob(),1);
34 | scriptPlayableConectMixerA2Layer.SetInputCount(4);
35 | scriptPlayableConectMixerA2Layer.SetOutputCount(4);
36 | var layerMixerPlayable = AnimationLayerMixerPlayable.Create(graph, 2);
37 | var mixerPlayable = AnimationMixerPlayable.Create(graph, 2);
38 | var clipPlayableA = AnimationClipPlayable.Create(graph, clipA);
39 | var clipPlayableB = AnimationClipPlayable.Create(graph, clipB);
40 |
41 | if (useAnimationScript)
42 | {
43 | layerMixerPlayable.ConnectInput(0,scriptPlayableConectMixerA2Layer,0, 1);
44 | scriptPlayableConectMixerA2Layer.ConnectInput(0, mixerPlayable, 0, 1);
45 |
46 | // scriptPlayableConectMixerA2Layer.GetHandle().SetScriptInstance(m_ClipProperties.Clone());// internal 方法无法直接调用
47 | SetBehaviourInternal(scriptPlayableConectMixerA2Layer);
48 | SetBehaviourInternal(scriptPlayableConectClipA2Mixer);
49 |
50 |
51 | mixerPlayable.ConnectInput(0, scriptPlayableConectClipA2Mixer, 0, 1);
52 | mixerPlayable.ConnectInput(1, clipPlayableB, 0, 1);
53 | scriptPlayableConectClipA2Mixer.ConnectInput(0,clipPlayableA, 0, 1);
54 | }
55 | else
56 | {
57 | layerMixerPlayable.ConnectInput(0, mixerPlayable, 0, 1);
58 | mixerPlayable.ConnectInput(0,clipPlayableA,0, 1);
59 | mixerPlayable.ConnectInput(1,clipPlayableB,0, 1);
60 | }
61 |
62 | var output = AnimationPlayableOutput.Create(graph, "Animation", animator);
63 | output.SetSourcePlayable(layerMixerPlayable);
64 |
65 | graph.Play();
66 | }
67 |
68 | private void SetBehaviourInternal(AnimationScriptPlayable animationScriptPlayable)
69 | {
70 | // 获取 internal extern 方法 SetScriptInstance
71 | var method = typeof(PlayableHandle).GetMethod("SetScriptInstance",
72 | BindingFlags.NonPublic | BindingFlags.Instance);
73 |
74 | if (method != null)
75 | {
76 | method.Invoke(animationScriptPlayable.GetHandle(), new object[] { new TimelineTestForAnimationScriptBehaviour() });
77 | }
78 | else
79 | {
80 | Debug.LogError("SetScriptInstance not found.");
81 | }
82 | }
83 |
84 | void OnDestroy()
85 | {
86 | DestroyGraph();
87 | }
88 |
89 | void DestroyGraph()
90 | {
91 | if (graph.IsValid())
92 | {
93 | graph.Destroy();
94 | }
95 | }
96 | }
97 | public struct DebugAnimationJob : IAnimationJob
98 | {
99 | private string info;
100 |
101 | // public DebugAnimationJob(string _info)
102 | // {
103 | // info = _info;
104 | // }
105 | public NativeArray.ReadOnly boneHandles;
106 |
107 | public void ProcessAnimation(AnimationStream stream)
108 | {
109 | Debug.Log("ProcessAnimation");
110 | // 每次GetInputStream,都会触发对输入子树的评估,开销很高,
111 | // 所以这里先缓存输入流,不在每个骨骼循环中反复调用GetInputStream
112 | Span inputStreams = stackalloc AnimationStream[stream.inputStreamCount];
113 | for (int i = 0; i < stream.inputStreamCount; i++)
114 | {
115 | inputStreams[i] = stream.GetInputStream(i);
116 | }
117 |
118 | for (int i = 0; i < boneHandles.Length; i++)
119 | {
120 | var boneHandle = boneHandles[i];
121 | for (int j = 0; j < inputStreams.Length; j++)
122 | {
123 | var inputStream = inputStreams[j];
124 | // TODO: 在这里完成自定义混合逻辑,例如惯性混合……
125 | }
126 | }
127 | }
128 |
129 | public void ProcessRootMotion(AnimationStream stream)
130 | {
131 | // throw new NotImplementedException();
132 | }
133 |
134 | public void ProcessAnimation2(AnimationStream stream)
135 | {
136 | Vector3 streamVelocity = stream.velocity;
137 | Debug.Log($"{info}输入时 stream.velocity:{streamVelocity}");
138 | if (info.Equals("ClipA2Mixer"))
139 | {
140 | streamVelocity = new Vector3(streamVelocity.x, streamVelocity.y+1.2223f, streamVelocity.z);
141 | Debug.Log($"{info}修改后 stream.velocity:{streamVelocity}");
142 | }
143 | Debug.Log($">> [DebugAnimationJob] ProcessAnimation called : {info}");
144 | }
145 | }
146 |
147 | [CustomEditor(typeof(TimelineTestForScriptAnimation))]
148 | public class TimelineTestForScriptAnimationEditor : Editor
149 | {
150 | public override void OnInspectorGUI()
151 | {
152 | // 显示原有 Inspector
153 | DrawDefaultInspector();
154 |
155 | TimelineTestForScriptAnimation script = (TimelineTestForScriptAnimation)target;
156 |
157 | // 添加按钮
158 | if (GUILayout.Button("▶️ 触发 TimelineTest 方法"))
159 | {
160 | script.Start();
161 | }
162 | }
163 | }
164 |
165 |
--------------------------------------------------------------------------------
/Unity_025/image/Add the Editor to the script.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/Add the Editor to the script.png
--------------------------------------------------------------------------------
/Unity_025/image/AnimationPlayable weight processor1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/AnimationPlayable weight processor1.png
--------------------------------------------------------------------------------
/Unity_025/image/AnimationPlayable weight processor2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/AnimationPlayable weight processor2.png
--------------------------------------------------------------------------------
/Unity_025/image/AnimationStream.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/AnimationStream.png
--------------------------------------------------------------------------------
/Unity_025/image/Basic Playable UML Class Diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/Basic Playable UML Class Diagram.png
--------------------------------------------------------------------------------
/Unity_025/image/Complete life cycle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/Complete life cycle.png
--------------------------------------------------------------------------------
/Unity_025/image/Directly display subattributes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/Directly display subattributes.png
--------------------------------------------------------------------------------
/Unity_025/image/Example of the graph for lifecycle validation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/Example of the graph for lifecycle validation.png
--------------------------------------------------------------------------------
/Unity_025/image/ExposedReference error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/ExposedReference error.png
--------------------------------------------------------------------------------
/Unity_025/image/ExposedReference example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/ExposedReference example.png
--------------------------------------------------------------------------------
/Unity_025/image/Full Timeline Process.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/Full Timeline Process.png
--------------------------------------------------------------------------------
/Unity_025/image/IntervalTree UML class diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/IntervalTree UML class diagram.png
--------------------------------------------------------------------------------
/Unity_025/image/IntervalTree internal struacture example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/IntervalTree internal struacture example.png
--------------------------------------------------------------------------------
/Unity_025/image/Passthrough source code comment.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/Passthrough source code comment.png
--------------------------------------------------------------------------------
/Unity_025/image/Pasted image 20250526141105.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/Pasted image 20250526141105.png
--------------------------------------------------------------------------------
/Unity_025/image/Pasted image 20250526160501.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/Pasted image 20250526160501.png
--------------------------------------------------------------------------------
/Unity_025/image/Pasted image 20250526173205.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/Pasted image 20250526173205.png
--------------------------------------------------------------------------------
/Unity_025/image/Pasted image 20250526173215.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/Pasted image 20250526173215.png
--------------------------------------------------------------------------------
/Unity_025/image/Pasted image 20250526173228.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/Pasted image 20250526173228.png
--------------------------------------------------------------------------------
/Unity_025/image/Pasted image 20250527021310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/Pasted image 20250527021310.png
--------------------------------------------------------------------------------
/Unity_025/image/Pasted image 20250527021317.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/Pasted image 20250527021317.png
--------------------------------------------------------------------------------
/Unity_025/image/Pasted image 20250527021327.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/Pasted image 20250527021327.png
--------------------------------------------------------------------------------
/Unity_025/image/Pasted image 20250527022427.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/Pasted image 20250527022427.png
--------------------------------------------------------------------------------
/Unity_025/image/Pasted image 20250527022435.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/Pasted image 20250527022435.png
--------------------------------------------------------------------------------
/Unity_025/image/Playable Connect.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/Playable Connect.png
--------------------------------------------------------------------------------
/Unity_025/image/Playable Director window.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/Playable Director window.png
--------------------------------------------------------------------------------
/Unity_025/image/Playable Graph Monitor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/Playable Graph Monitor.png
--------------------------------------------------------------------------------
/Unity_025/image/Playable inner structure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/Playable inner structure.png
--------------------------------------------------------------------------------
/Unity_025/image/PlayableAsset Instantiate by guid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/PlayableAsset Instantiate by guid.png
--------------------------------------------------------------------------------
/Unity_025/image/PlayableAsset class uml.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/PlayableAsset class uml.png
--------------------------------------------------------------------------------
/Unity_025/image/PlayableBehaviour UML Class Diagram.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/PlayableBehaviour UML Class Diagram.jpg
--------------------------------------------------------------------------------
/Unity_025/image/PlayableDirector.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/PlayableDirector.png
--------------------------------------------------------------------------------
/Unity_025/image/PlayableGraph UML graph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/PlayableGraph UML graph.png
--------------------------------------------------------------------------------
/Unity_025/image/PlayableOutput Internal structure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/PlayableOutput Internal structure.png
--------------------------------------------------------------------------------
/Unity_025/image/PlayableOutput UML diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/PlayableOutput UML diagram.png
--------------------------------------------------------------------------------
/Unity_025/image/Runtime method call chain.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/Runtime method call chain.png
--------------------------------------------------------------------------------
/Unity_025/image/RuntimeClip internal structure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/RuntimeClip internal structure.png
--------------------------------------------------------------------------------
/Unity_025/image/SceneBindings in PlayableDirector.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/SceneBindings in PlayableDirector.png
--------------------------------------------------------------------------------
/Unity_025/image/SourceOutputPort meaning.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/SourceOutputPort meaning.png
--------------------------------------------------------------------------------
/Unity_025/image/Subsequent data processing of AnimatorPlayableOutput.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/Subsequent data processing of AnimatorPlayableOutput.png
--------------------------------------------------------------------------------
/Unity_025/image/Timeline Structure Comprehensive Explanation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/Timeline Structure Comprehensive Explanation.png
--------------------------------------------------------------------------------
/Unity_025/image/Timeline all track.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/Timeline all track.png
--------------------------------------------------------------------------------
/Unity_025/image/Timeline and Playable important components.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/Timeline and Playable important components.png
--------------------------------------------------------------------------------
/Unity_025/image/Timeline overall structure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/Timeline overall structure.png
--------------------------------------------------------------------------------
/Unity_025/image/Timeline yaml file structure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/Timeline yaml file structure.png
--------------------------------------------------------------------------------
/Unity_025/image/TimelineAsset structure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/TimelineAsset structure.png
--------------------------------------------------------------------------------
/Unity_025/image/Track contain Target.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/Track contain Target.png
--------------------------------------------------------------------------------
/Unity_025/image/TrackAsset need specific attributes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/TrackAsset need specific attributes.png
--------------------------------------------------------------------------------
/Unity_025/image/attribute wrapped outter parameter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/attribute wrapped outter parameter.png
--------------------------------------------------------------------------------
/Unity_025/image/create Timeline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/create Timeline.png
--------------------------------------------------------------------------------
/Unity_025/image/example_Timeline window create two track.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/example_Timeline window create two track.png
--------------------------------------------------------------------------------
/Unity_025/image/graph contain PlayableDirector.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/graph contain PlayableDirector.png
--------------------------------------------------------------------------------
/Unity_025/image/invalid graph structure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/invalid graph structure.png
--------------------------------------------------------------------------------
/Unity_025/image/mix mode data flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/mix mode data flow.png
--------------------------------------------------------------------------------
/Unity_025/image/open Timeline window.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/open Timeline window.png
--------------------------------------------------------------------------------
/Unity_025/image/passthrough example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/passthrough example.png
--------------------------------------------------------------------------------
/Unity_025/image/passthrough mode data flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/passthrough mode data flow.png
--------------------------------------------------------------------------------
/Unity_025/image/passthrough mode data flow2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/passthrough mode data flow2.png
--------------------------------------------------------------------------------
/Unity_025/image/root track and output track.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/root track and output track.png
--------------------------------------------------------------------------------
/Unity_025/image/set weight by mixin curve.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/set weight by mixin curve.png
--------------------------------------------------------------------------------
/Unity_025/image/timeline Graph in runtime.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/timeline Graph in runtime.png
--------------------------------------------------------------------------------
/Unity_025/image/two steps of graph play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/two steps of graph play.png
--------------------------------------------------------------------------------
/Unity_025/image/valid graph structure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/Unity_025/image/valid graph structure.png
--------------------------------------------------------------------------------
/Unity_025/作者RingleaderWang.md:
--------------------------------------------------------------------------------
1 | 作者 CSDN、bilibili @ RingleaderWang
--------------------------------------------------------------------------------
/obsidian.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RingleaderWang/UnityBlog/bc280ef997dc3be3c963f31afcf3ba51826bd82e/obsidian.png
--------------------------------------------------------------------------------