From 26443e1d4a28ec165ef91a615752207d4db109c2 Mon Sep 17 00:00:00 2001
From: zzz <1638644816@qq.com>
Date: Fri, 4 Jul 2025 18:23:21 +0800
Subject: [PATCH] 7.4
---
1Project/中原精英/RoadMap.md | 33 -
1Project/宠物宇宙/地图编辑器/流程组件.md | 45 -
1Project/宠物宇宙/地图编辑器/需要做的优化.md | 14 +-
.../宠物宇宙/地图编辑器/需要支持的地图功能.md | 47 -
1Project/宠物宇宙/版本需求.md | 26 -
1Project/构建新框架/MVVM/扩展其他组件.md | 8 +-
1Project/租房讨回押金/处理流程.md | 5 +-
2Areas/习惯养成/拖延症.md | 8 +-
2Areas/习惯养成/规范日常行为.md | 31 +-
2Areas/哲学与心理/他者贡献.md | 3 +-
2Areas/哲学与心理/目的论与原因论.md | 10 +-
.../游戏开发/Timeline结构及其源码浅析.md | 1268 +++++++++++++++++
InBox/渲染/软渲染/图片/FAQ.md | 142 --
13 files changed, 1310 insertions(+), 330 deletions(-)
delete mode 100644 1Project/中原精英/RoadMap.md
delete mode 100644 1Project/宠物宇宙/地图编辑器/流程组件.md
delete mode 100644 1Project/宠物宇宙/地图编辑器/需要支持的地图功能.md
create mode 100644 3Resources/游戏开发/Timeline结构及其源码浅析.md
delete mode 100644 InBox/渲染/软渲染/图片/FAQ.md
diff --git a/1Project/中原精英/RoadMap.md b/1Project/中原精英/RoadMap.md
deleted file mode 100644
index 30bda64..0000000
--- a/1Project/中原精英/RoadMap.md
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-# 优先级
- - 场景战斗 => 游戏流程 => 业务
-
-## 需求
-- [ ] 玩家控制器
- - 人物动画
- - [x] 人物镜头旋转(平视-稍微仰视)
- - [ ] 能量塔,玩家HP,需要特效
- - [ ] 子弹拖尾
-- [ ] 分身AI
- - [ ] 自动攻击敌人
-- [ ] 敌人生成寻路攻击
- - [x] 寻路
- - [x] 攻击
- - 毁灭
- - 动画
-- [ ] UI
- - 开始游戏
- - 游戏中
- - 积分
- - 血量
- - 其他道具 金币
- - 结束
-- 业务功能
- - [ ] 金钱可以购买辅助武器防守
- - [ ] 光灵塔
- - [ ] 电网
- - [ ] 金钱可以升级辅助武器
- - [ ] 玩家可以金币复活或者看广告
-- 优化
- - [ ] 武器发射要够炫酷
\ No newline at end of file
diff --git a/1Project/宠物宇宙/地图编辑器/流程组件.md b/1Project/宠物宇宙/地图编辑器/流程组件.md
deleted file mode 100644
index 78e38ad..0000000
--- a/1Project/宠物宇宙/地图编辑器/流程组件.md
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
-
-抽象成管线
- 找出运行时和编辑器共有的模块
- 把这些模块做成组件
- 运行时和编辑器分别实例不同的组件
-
-#### 编辑器
-Provider
- - 加载配置
- - 生成地图
- - 创建地块
- - 地块渲染提供器
- - 普通Texture图
- - 编辑器的白膜
- - 创建摆件
- - 创建碰撞盒
- - 创建怪物
- - 创建标志
-
-#### 运行时
-- 加载配置
-- 生成地图
-- 创建地块
- - 地块渲染提供器
- - 普通的Texture图
-- 创建摆件
- - 渲染
- - 注册到管理
- - Behaviour
- - 地图初始化回调
- - 进入房间回调
-- 创建碰撞盒
- - 注册到管理
- - 地图初始化回调
- - 进入房间回调
-- 创建怪物
- - 注册到管理
- - 地图初始化回调
- - 进入房间回调
-- 创建标志
- - 注册到管理
- - 地图初始化回调
- - 进入房间回调
\ No newline at end of file
diff --git a/1Project/宠物宇宙/地图编辑器/需要做的优化.md b/1Project/宠物宇宙/地图编辑器/需要做的优化.md
index edb67f8..0868efe 100644
--- a/1Project/宠物宇宙/地图编辑器/需要做的优化.md
+++ b/1Project/宠物宇宙/地图编辑器/需要做的优化.md
@@ -1,13 +1,13 @@
##### 流程组件
-- [ ] **构建[[流程组件]]**
+- [x] **构建[[流程组件]]**
##### 数据结构更换
-- [ ] 更换为新的数据结构
+- [x] 更换为新的数据结构
- 路线数据变更
- 旧版本: 由文件夹下Roomline记录
- 新版本: 用链表实现,在 **关卡表-房间表** 中指定首个房间,随后用房间指向下个房间组成链表实现路线
-- [ ] 接入Luban的表格
+- [x] 接入Luban的表格
- 旧的数据结构包括
- 文件夹下
- SO文件
@@ -29,16 +29,12 @@
- 新的数据结构
- 文件夹下
- 所有地板以及地板摆放的物体.json
- - 一份关于地图的数据,地图Id,地图名字
- 预览图
- - 配置表
- - 波次数据
- - 房间从1001开头用 多主键联合的方式
-- [ ] 编辑器窗口需要随着数据变更而改动
- - [ ] EditManager流程
##### 单块地板编辑
- 只专注于一个地块的编辑
- 数据,地图大小(瓦片),环境物体
- 更换地块图片
- [ ] importer时更改数据width,height
+
+##### 瓦片地图
\ No newline at end of file
diff --git a/1Project/宠物宇宙/地图编辑器/需要支持的地图功能.md b/1Project/宠物宇宙/地图编辑器/需要支持的地图功能.md
deleted file mode 100644
index 2154510..0000000
--- a/1Project/宠物宇宙/地图编辑器/需要支持的地图功能.md
+++ /dev/null
@@ -1,47 +0,0 @@
-
-
-> [!important] 优先级 **1**
-
-
-
- - [x] IExtension
- - [x] 创建地块的Width和Height
- - [x] A*报错
- - ![[img_v3_02n6_ff40a1a4-d813-45f7-b132-6f14a74cfd7g.jpg]]
- - [x] 松鼠异常
-
- - [ ] 地图地板图片导入 自动修改图片大小
- - 更换地板图片用
- - [x] 添加白膜地图
- - 创建100 - 100的地图,宽高跟随Rect大小变化
-
- - [x] 碰撞盒需要添加旋转
-
-
-> [!note] 优先级 **2**
-
- - [ ] id不再自增长
- - [x] 将RoomConfig改到==关卡表==里
- - [ ] 枚举改为英文名
- - [ ] 瓦片地图
- - [ ] 优化重构
- [[需要做的优化]]
-
-
-> [!note] 优先级 **3**
-
- - [x] 提取单个地块编辑
- - [x] 添加脏标单个保存地块
- - [x] 编辑地块的操作逻辑
-
-> [!note] 优先级 **4**
-
- - [ ] 过关门动画优化
- - [ ] 地图大背景优化移动方案
-
-
- > [!success] 已完成
-
- - [x] 添加RoomComponents
- - [x] 生成怪物测试
- - [x] 碰撞盒和标志需要添加自定义功能
\ No newline at end of file
diff --git a/1Project/宠物宇宙/版本需求.md b/1Project/宠物宇宙/版本需求.md
index 088bca7..b9716df 100644
--- a/1Project/宠物宇宙/版本需求.md
+++ b/1Project/宠物宇宙/版本需求.md
@@ -1,29 +1,3 @@
-
-> # 此版本
-
-
-- [x] 皮肤导入
- - [x] 上海限定 C:\Users\1\Desktop\cache\57015
- - 游戏中
- - [x] C:\Users\1\Desktop\cache\红/蓝/绿皮肤7.0
- - [x] 胳膊导入
- - 结算
- - [x] C:\Users\1\Desktop\cache\角色胜利结算
- - 卡面
- - [x] C:\Users\1\Desktop\cache\第三弹裁切输出
- - UI名字
- - [x] C:\Users\1\Desktop\cache\皮肤
-- [x] 枪械宠物UI导入
-- [x] 测试网络点数 开机检测
-- [x] 地图编辑器 [[需要支持的地图功能]]
- - [x] Boss立绘
- - [x] C:\Users\1\Desktop\cache\雷鸟血条UI
-- [x] 积分系统
-
----
-
-
-
> ## 下个版本
- [ ] 网络状态3\4
diff --git a/1Project/构建新框架/MVVM/扩展其他组件.md b/1Project/构建新框架/MVVM/扩展其他组件.md
index f464374..f8ef401 100644
--- a/1Project/构建新框架/MVVM/扩展其他组件.md
+++ b/1Project/构建新框架/MVVM/扩展其他组件.md
@@ -1,9 +1,7 @@
- [ ] 扩展嵌套列表
- - [ ] 列表优化添加 移除
+ - [x] 列表优化添加 移除
- [ ] 自定义事件
-
-- [ ] 类型Switch
- - [x] Button:
- - 需要加入OnClicked绑定命令
+ - [ ] 绑定View-ViewModel的报错
+ - [ ] 优化V和VM的绑定
diff --git a/1Project/租房讨回押金/处理流程.md b/1Project/租房讨回押金/处理流程.md
index c64b755..a489ade 100644
--- a/1Project/租房讨回押金/处理流程.md
+++ b/1Project/租房讨回押金/处理流程.md
@@ -9,4 +9,7 @@
# 起诉
- 起诉状姓名身份证信息需要完整
-- 打印出来之后需要手写签名拍照,手写的签名才具有法律效应
\ No newline at end of file
+- 打印出来之后需要手写签名拍照,手写的签名才具有法律效应
+
+# 偷税漏税
+ - 要求对方提供房租发票,如拒绝,可以向税务局举报
\ No newline at end of file
diff --git a/2Areas/习惯养成/拖延症.md b/2Areas/习惯养成/拖延症.md
index 4125e14..7e2bf3a 100644
--- a/2Areas/习惯养成/拖延症.md
+++ b/2Areas/习惯养成/拖延症.md
@@ -1,8 +1,14 @@
+---
+tags:
+ - 心理
+ - 习惯养成
+---
+
### 使用多巴胺来激励自己的行动
- 大脑需要判断有价值的行动目标。而判断是否有价值的这个体系就是多巴胺系统
### 那么如何利用这个机制呢
- 幻想一个场景,用幻想来刺激
- 比如:
- - 写完这几段代码,然后舒舒服服的躺上3分钟,边躺还要边玩会手机。
+ - 如果写完了这几段代码,完事儿之后就可以享受某个成果。
- 想象干净整洁的家,所有东西都被放在了该放的地方那种舒适感。
\ No newline at end of file
diff --git a/2Areas/习惯养成/规范日常行为.md b/2Areas/习惯养成/规范日常行为.md
index df8df8a..c74f37f 100644
--- a/2Areas/习惯养成/规范日常行为.md
+++ b/2Areas/习惯养成/规范日常行为.md
@@ -1,16 +1,19 @@
+---
+tags:
+ - 心理
+ - 习惯养成
+---
-#习惯养成
+# 为什么释道儒三道都强调日常行为
+- 因为**日常行为的成果**能提升人的自信心。
+- 类似的行为还有
+ - 军队会被要求每天早起折叠豆腐块。
+ - 宗教表现为 成员必须每日做好虔诚的动作,既是规范宗教的向心力,更是为了培养出某个动作模式
+ - 道家表现为 从打扫卫生开始,每天做好自己的家务活
+- 这些行为都是为了一件事情,增加人的自信
+- 具体原理为人能从每个行为,以及对应的结果中获得对应的反馈
+ - 将家里打扫干净,看到一个干净卫生的环境
+ - **一点一点的积累这种成功,可以对一个人的行为模式产生变化**
-释道儒三道都强调日常行为
-
-因为日常行为能提升人的自信心
-
-宗教表现为 成员必须每日做好虔诚的动作,既是规范宗教的向心力,更是为了培养出某个动作模式
-道家表现为 从打扫卫生开始,每天做好自己的家务活
-
-这些行为都是为了一件事情,增加人的自信
-
-具体原理为 人能从每个行为,以及对应的结果中获得对应的反馈,比如:将家里打扫干净,结果为看到一个干净卫生的环境,从中培训出“好好打算,并且获得了自己满意的结果”=>==一点一点的积累这种成功,可以对一个人的行为模式产生变化==
-
-启示
-1、代码养成良好的习惯,最后回顾自己的代码,有精致规整以及符合自己心意的结构的时候感受这种成就感
+# 启示
+- 代码养成良好的习惯,最后回顾自己的代码,有精致规整以及符合自己心意的结构的时候感受这种成就感
diff --git a/2Areas/哲学与心理/他者贡献.md b/2Areas/哲学与心理/他者贡献.md
index 935dd4b..ef16a21 100644
--- a/2Areas/哲学与心理/他者贡献.md
+++ b/2Areas/哲学与心理/他者贡献.md
@@ -1,2 +1 @@
-
-> 劳动不是我为他人效劳,而是体会我的价值而采取的手段
\ No newline at end of file
+- 劳动不是我为他人效劳,而是体会我的价值而采取的手段
\ No newline at end of file
diff --git a/2Areas/哲学与心理/目的论与原因论.md b/2Areas/哲学与心理/目的论与原因论.md
index 4a132f6..6dd402a 100644
--- a/2Areas/哲学与心理/目的论与原因论.md
+++ b/2Areas/哲学与心理/目的论与原因论.md
@@ -1,8 +1,8 @@
-原因论认为:
- 过去的心理创伤会影响现在的行为,比如过去因为样貌受到了攻击,那么在未来,这个人会因为这个攻击对自己的样貌自卑。
+# 原因论认为:
+- 过去的心理创伤会影响现在的行为,比如过去因为样貌受到了攻击,那么在未来,这个人会因为这个攻击对自己的样貌自卑。
-而目的论认为:
- 这个人自卑是因为自己想自卑,因为这样当别人再次攻击自己的时候,就可以再次归因为自己样貌。
- 但,假如给这个人整容,样貌不再是困扰的时候,这个人就不会受到攻击了吗?
+# 而目的论认为:
+- 这个人自卑是因为自己想自卑,因为这样当别人再次攻击自己的时候,就可以再次归因为自己样貌。
+- 但,假如给这个人整容,样貌不再是困扰的时候,这个人就不会受到攻击了吗?
diff --git a/3Resources/游戏开发/Timeline结构及其源码浅析.md b/3Resources/游戏开发/Timeline结构及其源码浅析.md
new file mode 100644
index 0000000..dee0480
--- /dev/null
+++ b/3Resources/游戏开发/Timeline结构及其源码浅析.md
@@ -0,0 +1,1268 @@
+---
+原文发布链接: https://github.com/RingleaderWang/UnityBlog/blob/main/Unity_025/Timeline%E7%BB%93%E6%9E%84%E5%8F%8A%E5%85%B6%E6%BA%90%E7%A0%81%E6%B5%85%E6%9E%90.md
+tags:
+ - Timeline
+---
+
+
+# 综述
+
+Unity的Timeline本质是一个包含很多可播放片段(Playable Clip)的区间树(IntervalTree),这个区间树可以排序、搜索、以及控制所有 Clip的激活与停止,最后利用底层的Playable系统,运行所激活的Clips,就是这么个东西。
+
+(当然这么定义不够准确,IntervalTree不控制clip激活,只是排序和搜索clip。IntervalTree本质是装clip的容器,不考虑性能的话用数组也行。这么定义只是方便理解Timeline的底层结构罢了)
+
+Timeline的两个主要作用:
+- 充当**技能编辑器**(策划程序向)
+- 做cg动画/过场动画/**动画演出**(导演动画向)
+
+Timeline整体结构如下:
+
+
+
+包括底层的Playable系统,其在c++端,封闭黑盒,暴露部分API给c#端使用,Timeline就是利用这些API实现的功能。
+
+详细来看,`Timeline`侧主要由`PlayableAsset`、`PlayableBinding`、`TrackAsset`、`PlayableDirector`等组成;
+
+Playable系统侧主要是`Playable`、`PlayableOutput`、`PlayableGraph`三部分组成。
+
+
+
+因为后面会频繁出现包含"`Playable`"的单词,所以需要辨析强调下
+- `Playable系统`:指底层c++端关于Playable的所有功能,包含playable/playableout节点创建与连接、playableGraph的构建运行销毁等。
+- `PlayableAPI`:指底层Playable系统暴露给c#端的接口方法。
+- `PlayableGraph`:控制Playable/PlayableOutput的创建、连接与运行
+- `Playable节点`:playableGraph中的数据生产与传递节点,包含叶子节点、mixer节点、layer节点等,可以进行数据处理与传递、权重混合等操作
+- `PlayableOutput`:playableGraph中的数据输出节点,将Playable节点传递的数据交由对应的`target`运行
+- **Playable**:不同语境含义不同,在PlayableGraph中指playable节点,讲源码时指playable节点对应的Playable这个类/结构体,timeline语境中一般指asset创建的对应playable类/结构体,放到整个unity一般指Playable API或Playable系统
+- **Timeline**:
+ - `Timeline窗口`:编排可播放片段的窗口。
+ - `Timeline文件`:Timeline窗口编辑后生成的文件,扩展名为”.playable“。
+ - `TimelineAsset`:指Timeline这个文件资产,或者实例化后对应的类。
+ - `TimelinePlayable`:由TimelineAsset类在运行时调用create方法生成对应的TimelinePlayable,构建playable节点树,并管理所有轨道中的clip。
+
+- `Playable Clip`:可播放片段,比如AnimationClip、AudioClip、Prefab等
+- `PlayableAsset`:跟Playable有关的静态资产文件或其实例化的类,运行时可以创建对应的Playable。包含TimelineAsset、TrackAsset、PlayableClipAsset等。
+- `TrackAsset`:特殊的PlayableAsset,Timeline窗口中一般直接称作track轨道,每个轨道可以放很多clip,这些clip就是PlayableClipAsset,运行时TrackAsset一般会创建MixerPlayable来混合PlayableClipAsset创建的Playable。
+- `PlayableBinding`:绑定track和最终运行数据的target,用于创建PlayableOutput
+- `PlayableDirector`:控制PlayableAsset,用于创建PlayableGraph来运行构建的timeline
+
+`Timeline`主要有三块要点:
+- **Timeline静态与运行态**,涉及:
+ - TimelineAsset CreatePlayable 方法
+ - TimelinePlayable Create方法、Compile方法、PrepareFrame方法
+ - TrackAsset CreatePlayableGraph方法、CompileClips方法
+- **IntervalTree与RuntimeClip内部结构**,涉及:
+ - TrackAsset CompileClips方法
+ - IntervalTree Rebuild方法
+- **ScriptPlayable生命周期**,涉及IPlayableBehaviour接口
+
+重点结构如下图所示:
+
+
+
+Timeline的完整流程:
+
+
+
+接下来会详细分析整个系统。
+# Timeline 操作简介
+
+1. 打开timeline窗口:Window>Sequencing>Timeline
+ 
+2. 创建Timeline:直接选择gameobject 点击Timeline窗口的Create创建,或者Project窗口右键Create>Timeline进行创建。(timeline文件扩展名为 playable,是静态资产)
+ 
+3. Timeline窗口create timeline,对象会自动创建PlayableDirector组件并绑定timeline。手动新建的话可以直接拖动timeline资产到对象,也能自动生成PlayableDirector组件。
+ 
+4. `PlayableDirector`用于为timeline创建对应的playable graph,可以激活`play On Awake`,那样组件awake后就能play(注意PlayableDirector组件awake远早于用户脚本awake,无法在用户脚本awake方法中控制PlayableDirector的`play on awake`参数)
+ - `Bindings`会显示特定轨道所绑定的对象,用于指明当前Track控制的对象。
+ - `Wrap Mode`指明Timeline播放完毕后的行为,none会直接stop timeline所在的graph(就是直接停止timeline),hold的话就是持续播放timeline最后一帧,Loop的话会从头循环播放。
+ - `Update Method` 指明Timeline play如何控制,GameTime是跟随系统自动play,Unscaled Game Time就是无视系统时间倍速的自动play(游戏pause也会播放timeline),Manual就是由用户手动evaluate进行timeline play。DSP似乎跟音频有关,但官方说跟Unscaled Game Time差不多,暂不研究。
+
+5. Timeline窗口右键添加轨道。
+ 主要有如下几个轨道:
+ - `Activation Track`:控制激活的轨道
+ - `Animation Track`:控制动画播放的轨道
+ - `Audio Track`:控制音频播放的轨道
+ - `Control Track`:控制prefab、粒子系统或者子timeline显示或播放的轨道
+ - `Signal Track`:控制帧事件的轨道
+ - `Custom Track`,就是用户自己继承`TrackAsset`、`PlayableAsset`、`PlayableBehaviour`等来实现自定义playable。
+ 
+
+6. 创建好track和playable后就可以用PlayableDirector自动播放或手动Play()播放。
+
+这里只是简单介绍下Timeline的用法,还有很多细节可以自己摸索或者去官方文档查看。
+
+下面将进入本文的主题,深入讲解Timeline的结构,同时会浅析所涉及的Timeline和Playable源码。
+
+# Timeline 静态结构分析
+
+后面我们会了解到,在运行时,`timeline`会生成树形结构的`PlayableGraph`,那谁指导它生成呢?就是`PlayableAsset`,这即是Timeline的**静态结构**。
+
+
+
+在Timeline窗口编辑资产后,形成**资产树**(也是**树形结构**的,**根节点**是`TimelinePlayable`,下一层是`root track/output track`,再下一层是 `PlayableAsset`)。
+
+`PlayableDirector`添加timeline资产,然后设置`binding`后 ,director就能管理整个timeline资产了。
+
+等后面director组件初始化 ,并根据timeline资产树实例化为对应的`TimelineAsset`后,便能以有序生成各个`Playable`。
+
+所以想理解运行时Timeline就必须先搞明白 静态Timeline的结构,并理清各PlayableAsset类的关系,以及clip和track、playTarget与track的关系(即Binding)。
+## PlayableAsset 静态资产
+
+在Timeline窗口创建两个Track(Activation Track、Audio Track),并分别添加对应的clip。如下:
+
+
+
+
+
+这个扩展名为 ".playable"的 timeline 文件,就是所谓的静态资产。
+
+timeline资产文件实例化后就是TimelineAsset类,TimelineAsset下一层是TrackAsset,TrackAsset包含数个PlayableAsset(比如AnimationPlayableAsset、AudioPlayableAsset等)。
+
+结构图如下:
+
+
+
+对应的Timeline.playable yaml文件如下图所示:
+
+
+
+
+实例化时,会根据这个`yaml`文件解析成 `PlayableAsset`节点链(利用PlayableAsset的`parent`、 `children`参数),**根节点(Root PlayableAsset)是TimelineAsset**,下一层是`Root Track`,对于`Group track`或者`AnimationTrack`允许其下还有`SubTrack`,每个Track都可以包含数个`PlayableAsset`。
+- `root PlayableAsset`:就是TimelineAsset,是所有`root track`的父级节点。
+- `root track`:就是TimelineAsset的直接子track,从timeline窗口看就是最外层track。
+- `output track`: 会生成`PlayableOutput`的track。除 GroupTrack、subGroupTrack、override track外的所有track 都是 output track。
+
+
+
红框为root track,黄框为output track
+
+## PlayableDirector
+
+用Timeline窗口查看timeline资产有两种方式,产生的效果会不同:
+- 第一种是从Project窗口打开的独立timeline资产,它无法预览播放,track左侧也没有绑定对象;
+- 另一种就是从挂载`PlayableDirector`组件对象点开,那么它的timeline就是可预览播放的,track左侧也会显示`Bindings`。
+
+第二种其实就是PlayableDirector组件做的绑定。
+
+- `PlayableDirector`利用`key-value` 来实现绑定。
+- `key`就是`Track`对象(在`PlayableBinding`中被称作`sourceObject`).
+- `value` 就是目标对象(在`PlayableOutput`中被称作`target`),如ActivationTrack对应的就是待控制的预制体,AudioSourceTrack对应的就是Audio Source组件 ,AnimationTrack 对应的就是Animator。
+
+
+PlayableDirector组件中SceneBindings值
+
+这样,包含PlayableDirector的结构图就变成下图所示:
+
+
+包含PlayableDirector的TimelineAsset结构图
+
+## PlayableAsset UML类图
+
+`TrackAsset` 和 `TimelineAsset` 其实都是继承自`PlayableAsset`,是一种特殊的`PlayableAsset`。
+
+**PlayableAsset UML类图**如下图所示:
+
+
+
+- **TimelineAsset**:最特别的`PlayableAsset`,存储了所有trackAsset,包含一个`CreatePlayable`方法,指导运行时创建`TimelinePlayable`。
+- **ClipPlayableAsset**(蓝色) :表示实现`ITimelineClipAsset`接口的特殊`PlayableAsset`,具体类有`ActivationPlayableAsset`、`AudioPlayableAsset`、`AnimationPlayableAsset`、`ControlPlayableAsset`。这些PlayableAsset拥有一个`clipCaps`属性,表明这个片段在Timeline窗口拥有的能力,这些能力包含:
+ - `Looping` 循环
+ - `Extrapolation` 外推断,分Pre和post,表示两个clip中间空白区域的播放逻辑:
+ - none 保持为开始帧
+ - hold 保持为最后一帧
+ - loop 循环(跳帧循环)
+ - pingpong 镜像循环
+ - continue 根据动画源文件的loop决定是连续循环还是保持为最后一帧
+ - `ClipIn` 起始裁切
+ - `SpeedMultiplier` 倍速
+ - `blend` 混合
+- **PlayableAsset**都包含一个`CreatePlayable`方法,用于运行时创建对应的Playable。
+- `TrackAsset`(左下黄色) 拥有`CreateTrackMixer`方法,运行时也是创建Playable(重写CreateTrackMixer方法的话可以创建自定义MixerPlayable),可以接收多个input输入。
+- **TrackAsset** 的 `CompileClips`方法在运行时编译其所包含的所有clip,生成`RuntimeClip`并加入到`TimelinePlayable`的`IntervalTree`中,用于Timeline运行时标识哪个clip被激活。
+- 继承`TrackAsset`的类有GroupTrack、ActivationTrack、AudioTrack、ControlTrack、SignalTrack、AnimationTrack等,这与timeline窗口能添加的track一致。
+- **TrackAsset** 有两个特性(**Attribute**)
+ - `TrackClipType`:表明Track能添加的clip类型,也就是上面继承playable且实现`ITimelineClipAsset`接口的类。
+ - `TrackBindingType` :表明 `output Track`对应的`PlayableOutput` 其`target`参数类型,也即`PlayableBinding`的`outputTargetType`参数。
+
+``` csharp
+[TrackClipType(typeof(AnimationPlayableAsset)]
+[TrackBindingType(typeof(Animator))]
+```
+
+# Playable系统
+
+上文介绍了Timeline的静态结构,为了更好地理解运行时Timeline,需要先了解底层的Playable系统。Playable 系统包括PlayableGraph、Playable和PlayableOutput。
+
+下面依次介绍。
+## PlayableGraph
+
+PlayableGraph管理Playable和PlayableOutput节点,控制整个节点树的运行。
+### PlayableGraph 与 PlayableDirector 类
+
+Timeline 运行时会根据PlayableDirector的Playable参数创建PlayableGraph。
+
+
+
+PlayableDirector可以用`PlayableDirector.playableGraph`属性获得创建的graph;
+PlayableGraph也可以用`PlayableGraph.GetResolver() as PlayableDirector` 解析出PlayableDirector。PlayableGraph的很多方法都可以直接在PlayableDirector中调用。
+
+PlayableGraph 和 PlayableDirector UML类图如下:
+
+
+
+### PlayableGraph的结构
+
+对于前面的 timeline 资产:
+
+
+
+在运行时形成的Graph结构如下:
+
+
+
+**PlayableGraph**的构建过程:
+- **PlayableDirector** `RebuildGraph`触发构建graph动作
+- 根据director的TimelineAsset 创建 `TimelinePlayable`
+- 然后依次 编译所有track和clip,创建对应的MixerPlayable、PlayableOutput和Playable。
+ - **TimelinePlayable作为根节点**,下一层是MixerPlayable或LayerMixerPlayable,叶子节点是包含clip的Playable。
+ - Playable之间是通过`Connect`连接,PlayableOutput通过`setSourcePlayable`与TimelinePlayable连接。
+- 编译clips期间会build `IntervalTree`,为后续graph play作准备。
+
+大体过程图如下(下一章还会有更详细的步骤分析):
+
+
+
+Timeline中关于graph的构建步骤很复杂,我们可以用如下案例了解其核心逻辑(注意一定要及时销毁graph):
+
+``` csharp
+public AnimationClip clip;
+public AnimationClip clip2;
+private PlayableGraph graph;
+
+void DestoryGraph(PlayableGraph graph)
+{
+ if (graph.IsValid())
+ {
+ graph.Destroy();
+ }
+}
+ private void CreateMixerGraph()
+ {
+ DestoryGraph(graph);
+ // 1. 创建 PlayableGraph
+ graph = PlayableGraph.Create("MixerPlayableGraph");
+ // 2. 获取 Animator 并创建 AnimationPlayableOutput
+ var animator = GetComponent();
+ var animationOutput = AnimationPlayableOutput.Create(graph, "Animation", animator);
+ // 3. 创建 AnimationClipPlayable
+ var clipPlayable = AnimationClipPlayable.Create(graph, clip);
+ var clipPlayable2 = AnimationClipPlayable.Create(graph, clip2);
+ // 4. 创建一个 Mixer,管理多个动画输入
+ AnimationMixerPlayable mixer = AnimationMixerPlayable.Create(graph, 2);
+ mixer.ConnectInput(0, clipPlayable, 0,1);
+ mixer.ConnectInput(1, clipPlayable2, 0,1);
+ // 5. 设置 mixer 作为输出
+ animationOutput.SetSourcePlayable(mixer);
+ // 6. 播放图谱
+ graph.Play();
+ }
+public void OnDestroy()
+{
+ DestoryGraph(graph);
+}
+```
+
+逻辑很简单,就4步:
+- 创建graph
+- 创建playable、mixerPlayable 并 connect
+- 创建playableOutput 并 setSourceOutput
+- 运行graph
+
+使用[Playable Graph Monitor](https://openupm.com/packages/com.greenbamboogames.playablegraphmonitor/) 查看 整个graph如下。
+
+
+如果你使用官方的PlayableGraph Visualizer 的话,你会发现和 Playable Graph Monitor所展示的不一样。
+
+比如还是这个例子:
+
+
+
+PlayableGraph Visualizer展示:
+
+
+Playable Graph Monitor展示:
+
+
+你可以发现区别很大,Visualizer从PlayableOutput往Playable构图,且两个PlayableOutput独立构图;而Monitor是从叶子Playable节点往PlayableOutput构图,两个PlayableOutput连在一个output端口上。
+
+到底哪个对呢?
+
+其实都对。Visualizer是从graph play角度构图的,Monitor是从graph create角度构图的。
+
+执行遍历节点获取数据时,其发起端其实是PlayableOutput,所以Visualizer把各个PlayableOutput独立开来构图,但这种展示方式对认识graph的结构不如Monitor清晰,而且官方Visualizer不能缩放,插件还有些bug会导致系统闪退,所以直接用Monitor就行。
+
+不过Monitor的展示还是有些问题,两个PlayableOutput连在同一个output port了,这应该是Unity Timeline自身的问题,你理解成第二个PlayableOutput连在第二个port就行,以此类推。(不影响运行)
+
+下面将继续介绍涉及的Playable与PlayableOutput。
+## Playable 的结构与类
+
+PlayableGraph 中的 Playable 节点负责 数据的生产、传递与处理,包含叶子节点、mixer节点和layer节点等。
+### Playable结构
+
+Playable是一个多输入多输出的有向节点,拥有speed、time、duration、isDone、PlayState等参数标识自身的运行状态,并对外提供控制自身运行、暂停与销毁的接口。
+
+
+
+#### 节点属性/方法
+
+- Playable节点拥有多个input port、多个output port用于连接其他Playable节点。
+- 节点连接前需保证port存在,否则报错`Connecting invalid input/output`,需要用`SetInputCount()/SetOutputCount()`申请端口,或者create时指明inputCount参数。
+- 同一port不能连接多个节点,否则报错`Cannot connect input/output port, it is already connected, the tree topology will be invalid. Disconnect it first`
+- Input有权重weight的概念,**默认是0**,可以在connect时指定weight或者用`Playable.SetInputWeight(inputIndex, weight)`设定输入权重
+- Playable节点间连接,可以使用`PlayableGraph.Connect(...)`或者扩展方法的`Playable.ConnectInput(...)`(默认权重为0)
+
+``` Csharp
+PlayableGraph.Connect(...)
+
+bool Connect(
+ U source,
+ int sourceOutputPort,
+ V destination,
+ int destinationInputPort)
+ where U : struct, IPlayable
+ where V : struct, IPlayable
+```
+
+
+
+允许的结构:
+
+
+
+不允许的结构(三个节点首尾相接)(出现死循环,如果使用graph visulizer,unity会奔溃)
+
+
+#### 一些特殊的属性/方法
+- `GetScriptInstance()`,指明playable生命周期的回调方法
+- `GetClip()/SetClip`,一些包含Clip内容的Playable如 AnimationClipPlayable、AudioClipPlayable、AnimatorControllerPlayable、MaterialEffectPlayable、CameraPlayable、VideoPlayable 拥有存取此Playable绑定的可播放片段的方法。这些方法名不固定,根据Playable类型,也可以叫 Get/SetMaterial()、GetAnimatorController、Get/SetCamera()、GetAnimationClip()等。
+
+#### 运行属性/方法
+- **Play state**:`Puase/Playing`,标识此节点运行状态
+- `Speed`:playable的播放速度
+- `Play()`:运行playable
+- `Pause()`:停止自身playable运行
+- `IsDone`:标识此节点运行完毕
+- `Destroy()`:利用所属graph销毁自身Playable
+- **TraversalMode**:`Mix/Passthrough`,表示遍历节点input的方式,`Mix`就是将各输入进行加权;`passthrough`就是匹配input与output,第几个output来请求数据,就从第几个input获取数据。Passthrough目前只用于TimelinePlayable,使之成为graph的统一入口。
+#### ⭐Traversal mode 验证
+
+对Traversal mode这个参数的理解非常重要,不明白它的话,你会误以为每个PlayableOutput都会接收所有叶子节点的数据。
+
+我们来创建个graph案例:
+``` csharp
+[Tooltip("false则mode是passthrough,true则mode是mix")]
+public bool changeTraversalMode = false;
+[Tooltip("false则PlayableOutput的source为playable1,true则为playable2")]
+public bool changeSourcePlayable = false;
+
+//newInput system绑定空格键触发
+public void BackspaceDown(InputAction.CallbackContext ctx)
+{
+ if (ctx.performed)
+ {
+ ValidateTraversalMode2(changeTraversalMode,changeSourcePlayable);
+ }
+}
+
+private void ValidateTraversalMode2(bool _changeTraversalMode,bool _changeSourcePlayable)
+{
+ DestoryGraph(graph);
+ graph = PlayableGraph.Create();
+ var playable1 = Playable.Create(graph,1);
+ var playable2 = Playable.Create(graph,2);
+ var playable3 = Playable.Create(graph);
+ playable3.SetTraversalMode(!_changeTraversalMode?PlayableTraversalMode.Passthrough:PlayableTraversalMode.Mix);
+ var playable4 = Playable.Create(graph,4);
+ var playable5 = Playable.Create(graph,5);
+ playable3.SetInputCount(10);
+ playable3.SetOutputCount(10);
+ graph.Connect(playable3, 0, playable1, 0);
+ graph.Connect(playable3, 1, playable2, 0);
+ graph.Connect(playable4, 0, playable3, 0);
+ graph.Connect(playable5, 0, playable3, 1);
+ ScriptPlayableOutput output = ScriptPlayableOutput.Create(graph, "customOutput");
+ output.SetSourcePlayable(!_changeSourcePlayable?playable1:playable2,100);
+ graph.Play();
+}
+```
+
+当前中间Playable的 traversalMode是passthrough,可以看到只有port 0 这条线有数据传输。
+
+mode为passthrough数据流向
+
+改变PlayableOutput连在port 1 这端,数据又只接受input 1 这个port输入了。
+
+mode为passthrough的数据流向(改变sourcePlayable)
+
+而当 MODE 设为 MIX ,input 0 和 input 1 都接收数据。
+
+MODE=MIX的数据流向
+
+通过这个例子,能大概理解 `TraversalMode` 遍历模式到底是什么意思。
+
+不过要注意,按照Passthrough源码注释的本意:
+
+
+
+应该是`sourceOutputPort`的`PlayableOutput`跟对应input位置的节点进行直通,但实测**sourceOutputPort没起作用**,实际上是由PlayableOutput在graph中的顺序决定,也就是下图中PlayableOutput前面的#0 #1 #2 标号(其实就是PlayableOutput数组的index)。
+
+
+passthrough mode下input port匹配的其实是PlayableOutput数组标号
+
+测试代码你可以发现`SetSourcePlayable`的port你设50设100都不影响。
+``` csharp
+void ValidateTraversalMode()
+{
+ DestoryGraph(graph);
+ graph = PlayableGraph.Create("TraversalModeGraph");
+ ScriptPlayableOutput output = ScriptPlayableOutput.Create(graph, "customOutput");
+ ScriptPlayableOutput output2 = ScriptPlayableOutput.Create(graph, "customOutput2");
+ ScriptPlayableOutput output3 = ScriptPlayableOutput.Create(graph, "customOutput3");
+ var playable = ScriptPlayable.Create(graph);
+ var playable2 = ScriptPlayable.Create(graph);
+ var playable3 = ScriptPlayable.Create(graph);
+ var playable4 = ScriptPlayable.Create(graph);
+ var playable5 = ScriptPlayable.Create(graph);
+ var mixer = ScriptPlayable.Create(graph);
+ mixer.SetTraversalMode(PlayableTraversalMode.Passthrough);//默认mode为mix,这里改为passthrough
+ mixer.SetInputCount(10);// 很重要,否则连接报错:Connecting invalid input
+ mixer.ConnectInput(4,playable,0,1);
+ mixer.ConnectInput(6,playable2,0,1);
+ mixer.ConnectInput(2,playable3,0,1);
+ mixer.ConnectInput(8,playable4,0,1);
+ mixer.ConnectInput(1,playable5,0,1);
+ output.SetSourcePlayable(mixer,30);// 这个SetSourcePlayable的port根本没用,完全根据PlayableOutput顺序获取的
+ output2.SetSourcePlayable(mixer,50);
+ output3.SetSourcePlayable(mixer,100);
+ graph.Play();
+}
+```
+
+但从Animation计算权重的过程也能看出,SourceOutputPort本意就是playable的output port。但实测中Playableout获取数据时并没有使用这个参数,更没有校验,你设多少都可以(只校验了不能重复绑定,但没校验port是否存在)。
+
+如果是bug的话,官方可能需要做下面两个步骤进行修复:
+- 每connect一个节点到TimelinePlayable,TimelinePlayable的outputCount加一(这样直通才有意义)
+- 对每个Playableout从根节点TimelinePlayable开始遍历时,严格按照`TimelinePlayable.getInput(PlayableOutput.GetSourceOutputport())`获取绑定的Playable,而不是用`PlaybleGraph`中类似`PlayableOytput[]`数组的`index`来get input playable。(注:Timeline生成的graph中`PlayableOutput.GetSourcePlayable()`得到的就是`TimelinePlayable`)
+
+### Playble UML类图
+
+PlayableAsset创建的playable有两种,一种是不带生命周期管理的Playable,另一种是带生命周期管理的(实现IPlayableBehaviour接口)。第二种其实就是Timeline模块中基于ScriptPlayable实现的一种官方自定义playable。
+#### 基础Playable 的 UML 类图
+
+
+注意这些Playable都是Struct,没有像class一样的继承能力,但这些子Playable都重写了隐/显式操作符,所以功能上等价于存在继承关系。比如下面AudioClipPlayable例子:
+
+``` Csharp
+public static implicit operator Playable(AudioClipPlayable playable)
+{
+ return new Playable(playable.GetHandle());
+}
+
+public static explicit operator AudioClipPlayable(Playable playable)
+{
+ return new AudioClipPlayable(playable.GetHandle());
+}
+
+// 自动转换
+AudioClipPlayable audioClipPlayable = ...;
+Playable playable = audioClipPlayable;
+
+// 需要显式转换
+Playable playable = ...;
+AudioClipPlayable audioClipPlayable = (AudioClipPlayable)playable;
+```
+
+- 这些Playable都拥有`Create`方法,供TrackAsset执行CompileClips方法或CompileTrackPlayable方法期间调用。
+- **Playable按功能大概分成两类**:
+ - 一类如上面蓝色(AnimationClipPlayable等)和绿色(ScriptPlayable)的Playable,在graph中常作为叶子节点的,其内部是含有类似clip的属性,能getClip、setClip(有的没暴露set,只能在create时传入)。
+ - 一类是上面黄底色的Playable,在graph中作为中间节点,可能包含多个输入,所以命名一般叫MixerPlayable、LayerPlayable,对多个输入的权重或其他数据进行处理。
+- 上面结构图并没有列出所有Playable,详细的可以查看IPlayable的所有实现。
+- `ScriptPlayable`比较特殊,能够接收`PlayableBehaviour`来**控制Playable的生命周期**,这样形成的就是**自定义Playable**(Timeline里的很多Playable本质上就是这种官方自定义Playable)。
+
+下面就详细介绍这些所谓的“**官方自定义Playable**”。
+#### PlayableBehaviour 的 UML 类图
+
+
+
+继承`PlayableBehaviour`的类分为几种:
+- `TimelinePlayable`:作为graph中所有其他playable的根父节点,控制所有clip的激活与否。
+- Audio的两个名字比较特别,叫`AudioMixerProperties`和`AudioClipProperties`,AudioClipProperties甚至连一个回调都没有,这就涉及到PlayableBehaviour的第二种作用:**传递PlayableAsset/TrackAsset参数给运行时的各生命周期**。这种参数传递有两种用法,后面会介绍。
+- Activation有关的 ActivationControlPlayable 和 ActivationMixerPlayable 。
+ 比较好玩的是 ActivationControlPlayable 只用在 ControlTrack ,而 ActivationMixerPlayable 只用在 ActivationTrack 。
+- `ControlTrack` 的三个playable:
+ - 控制粒子的`ParticleControlPlayable`
+ - 控制prefab的`PrefabControlPlayable`
+ - 控制PlayableDirector(也就是控制子Timeline)的`DirectorControlPlayable`。
+### ⭐Behaviour 与 PlayableAsset的数据交互
+
+我们在自定义Track 自定义PlayableAsset时,可能有这么个需求,**Track或PlayableAsset自身需要拥有特殊属性**,而且能在后面的生命周期中获取这个特殊属性。
+
+比如我的技能系统用的SkillPlayableAsset,有个参数标注这个clip用于技能的哪个阶段,就需要SkiIlPhase这么一个参数,后面behaviour回调触发时就能根据这个参数针对性做处理。
+
+
+Track或PlayableAsset自身需要拥有特殊属性的场景
+
+这就是behaviour 与 playableAsset的数据交互问题。
+
+如果你是官方设定的基础Playable,你可以在Playable上设置这么个特殊属性,create的时候都塞进去,生命周期回调时也能正常获取。但你自定义track,用的都是ScriptPlayable,没有额外参数,怎么办?那就是用PlayableBehaviour传参!
+
+**传参有两种方式**,这同时也涉及到ScriptPlayable createPlayable的两种方式。
+
+``` csharp
+// T : class, IPlayableBehaviour, new()
+public static ScriptPlayable Create(PlayableGraph graph, int inputCount = 0)
+{
+ return new ScriptPlayable(ScriptPlayable.CreateHandle(graph, default (T), inputCount));
+}
+
+public static ScriptPlayable Create(PlayableGraph graph, T template, int inputCount = 0)
+{
+ return new ScriptPlayable(ScriptPlayable.CreateHandle(graph, template, inputCount));
+}
+```
+
+#### 带 new T() 的 ScriptPlayable 创建
+如上ScriptPlayable 创建Playable时,可以带T template 参数,T就是实现了IPlayableBehaviour且有**无参构造函数**的类。
+
+这样自定track或playableAsset在创建playable时就可以将含有特殊属性的T传进来,behaviour回调时就能获取到你设的特殊属性。
+
+``` csharp
+// 存在 XXPlayableBehaviour,内含param1、param2两个参数
+
+public class XXPlayableAsset : PlayableAsset
+{
+ public XXPlayableBehaviour template = new XXPlayableBehaviour();
+ public override Playable CreatePlayable (PlayableGraph graph, GameObject owner){
+ var playable = ScriptPlayable.Create(graph,template);
+ return playable;
+ }
+}
+XXPlayableAsset Inspector面板展示:
+template
+- param1
+- param2
+```
+
+这样你在XXPlayableAsset Inspector就能展示XXPlayableBehaviour的两个参数,param1和param2,但不够好看,外面会套一层参数名,像下面这样:
+
+
+
+解决方法有两个,一种是类似AudioTrack的处理方法,写个AudioTrackInspector手动提取出各个参数。另一个参考官方案例的自定义Attribute:NoFoldOut。
+
+``` csharp
+// Custom property drawer that draws all child properties inline
+[CustomPropertyDrawer(typeof(NoFoldOutAttribute))]
+public class NoFoldOutPropertyDrawer : PropertyDrawer
+{
+ public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
+ {
+ if (!property.hasChildren)
+ return base.GetPropertyHeight(property, label);
+ property.isExpanded = true;
+ return EditorGUI.GetPropertyHeight(property, label, true) -
+ EditorGUI.GetPropertyHeight(property, label, false);
+ }
+
+ public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
+ {
+ if (!property.hasChildren)
+ EditorGUI.PropertyField(position, property, label);
+ else
+ {
+ SerializedProperty iter = property.Copy();
+ var nextSibling = property.Copy();
+ nextSibling.Next(false);
+ property.Next(true);
+ do
+ {
+ // We need to check against nextSibling to properly stop
+ // otherwise we will draw properties that are not child of this
+ // foldout.
+ if (SerializedProperty.EqualContents(property, nextSibling))
+ break;
+ float height = EditorGUI.GetPropertyHeight(property, property.hasVisibleChildren);
+ position.height = height;
+ EditorGUI.PropertyField(position, property, property.hasVisibleChildren);
+ position.y = position.y + height;
+ }
+ while (property.NextVisible(false));
+ }
+ }
+}
+```
+
+这样使用时给参数加`[NoFoldOut]`特性就能直接展示子属性了。
+``` csharp
+// 存在 XXPlayableBehaviour,内含param1、param2两个参数
+public class XXPlayableAsset : PlayableAsset
+{
+ [NoFoldOut]
+ public XXPlayableBehaviour template = new XXPlayableBehaviour();
+ public override Playable CreatePlayable (PlayableGraph graph, GameObject owner){
+ var playable = ScriptPlayable.Create(graph,template);
+ return playable;
+ }
+}
+```
+
+#### 使用 default (T) 的 ScriptPlayable 创建
+
+如果你使用 default (T) 创建,意味着你的这些特殊属性都变成默认值。
+
+不过你也可以手动操作改变,如下代码所示,create完ScriptPlayable后,再手动赋值。
+
+``` csharp
+// 存在 XXPlayableBehaviour,内含param1、param2两个参数
+
+public class XXPlayableAsset : PlayableAsset
+{
+ public override Playable CreatePlayable (PlayableGraph graph, GameObject owner)
+ {
+ var playable = ScriptPlayable.Create(graph);
+ var xxPlayableBehaviour = playable.GetBehaviour();
+ xxPlayableBehaviour.param1 = value1;
+ xxPlayableBehaviour.param2 = value2;
+ return playable;
+ }
+}
+```
+
+不过这种在Asset还要再申明一遍param,冗杂,所以推荐用第一种。
+
+#### ExposedReference用法
+
+因为Asset不能引用scene场景中的对象,但如果你非要引用也不是不可以,Timeline提供了`ExposedReference`方法让你能在Behaviour回调中获取场景中的对象。
+
+``` csharp
+public class XXPlayableBehaviour : PlayableBehaviour
+{
+ public ExposedReference exposedTransform;
+ public override void OnGraphStart(Playable playable){
+ Transform transform = exposedTransform.Resolve(playable.GetGraph().GetResolver());
+ Debug.Log("exposedTransform.Resolve(playable.GetGraph().GetResolver()):"+transform.position);
+ Debug.Log("gameobject name:" + transform.gameObject.name);
+
+ }
+}
+
+public class XXPlayableAsset : PlayableAsset
+{
+ public XXPlayableBehaviour template = new XXPlayableBehaviour();
+ public override Playable CreatePlayable (PlayableGraph graph, GameObject owner){
+ var playable = ScriptPlayable.Create(graph,template);
+ return playable;
+ }
+}
+```
+
+注意使用时,一定要从点击挂载PlayableDirector的对象对应的timeline窗口操作。
+
+
+ExposedReference用法
+
+运行时可以正常打印,但发现打印的对象position数据有问题,似乎有(0.22, 1.28, -1.66)的偏差,不清楚原因,暂时还是不要用`ExposedReference`。
+
+
+ExposedReference用法有待进一步验证
+
+## PlayableOutput 结构与类
+
+`PlayableOutput` 作为 PlayableGraph 中的数据输出节点,在graph Play 时负责发起 Playable节点的遍历(没连PlayableOutput 对应的Playable不运行),并将Playable节点传递的数据交由对应的`target`运行。
+### PlayableOutput 结构
+
+PlayableOutput 的结构相当简单。
+
+
+- ReferenceObject:对应的TrackAsset
+- SourcePlayable:PlayableOutput连接的Playable,使用扩展方法`PlayableOutput.SetSourcePlayable(Playable value, int port)`连接
+- 注意连接的SourcePlayable和port不能都一样,否则报错`Cannot set multiple PlayableOutputs to the same source playable and output port`
+- weight:PlayableOutput也拥有权重,默认为1,可以通过`PlayableOutput.SetWeight(float value)` 设置。
+- target:AnimationPlayableOutput、AudioPlayableOutput、TexturePlayableOutput拥有这个属性,表示Track所绑定的对象,用于后续处理Playable传过来的数据。
+### UML 类图
+
+相比Playable的类图,PlayableOutput就显得眉清目秀了。
+- 跟Playable一样,PlayableOutput也是struct结构体,它的继承也是重载显隐操作符。
+- 每种PlayableOutput都有个create方法来创建一个具体的PlayableOutput。
+- 除了ScriptPlayableOutput外,都有个target参数用于指明track binding的target对象。
+
+
+# 运行时Timeline
+
+前文介绍了Timeline编辑态流程,至于运行时,则主要包含三个步骤:
+- director组件初始化
+- graph Build
+- graph Play
+
+
+## 初始化 PlayableDirector
+
+看Timeline源码最难受的就是这,看不到尾就算了,还看不到头,过程全藏在Unity c++端。
+
+不过好在静态文件已知,create Playable步骤已知,中间无非就是graph的创建,那由谁触发呢?只有PlayableDirector了。
+
+所以**隐藏的过程就是** :PlayableDirector 组件的初始化和 PlaybleAsset 实例化,如果 play on awake 打开,则开始构建 graph,否则等待 director 执行 Play() 或 RebuildGraph() 才开始构建 graph。
+
+再次强调下,PlayableDirector组件awake远早于用户脚本awake,无法在用户脚本awake方法中控制PlayableDirector的`play on awake`参数。
+
+就是说,如果`play on awake` 是 false,你在自己脚本改为true,director也不会自动运行。不过可以先让director disable,在用户脚本awake 时 enable这个director,间接实现(那还不如手动Play() )。
+
+## 构建 Graph
+
+构建 graph简易过程如下图:
+
+
+
+详细过程如下:
+1. 根据`PlayableDirector` 的 TimelineAsset 执行创建Playable动作(c++端触发)
+2. 创建`TimelinePlayable`节点,**设置traversalMode为Passthrough**
+3. 遍历Timeline中的每个`OutputTrack`,为其创建`MixerPlayable`节点和`Playable`节点
+ 1. 创建MixerPlayable父节点
+ 2. 遍历TrackAsset的每个`TimelineClip`,创建对应的ClipPlayable子节点
+ 3. 利用MixerPlayable、刚创建的ClipPlayable和对应的TimelineClip创建**RuntimeClip**,然后将RuntimeClip存储到TimelinePlayable的**区间树IntervalTree**中,同时执行ClipPlayable的Pause()方法(其实就是设置playstate为paused)
+ 4. 连接ClipPlayable子节点和MixerPlayable父节点,**权重设置为0**
+4. 将上面创建的每个MixerPlayable连接TimelinePlayable,**权重为1**
+5. 紧接着对每个OutputTrack创建对应的playableOutput节点,并`SetSourcePlayable`为`TimelinePlayable`,权重设为1,`ReferenceObject`为这个`TrackAsset`。
+ - 如果是动画track的话,最后会为PlayableOutput注册一个权重处理回调`m_EvaluateCallbacks.Add(new AnimationOutputWeightProcessor(animOutput))`,构造AnimationOutputWeightProcessor时将AnimationPlayaleOutput的**权重设为0**(默认是1)。这个Processor会在graph后续运行时触发WeightEvaluate方法处理Animation相关的权重(会重新调整权重)。
+ - 注意playableOutput的target不是在此时添加的,这时传入的target都是null,可以通过`((AnimationPlayableOutput)graph.GetOutput(0)).GetTarget()`验证(值一定是null,假设第一个是动画的output情况下)。
+ - 真正设置target的动作可能在构建gragh动作之后,在c++端手动根据binding信息设置(可能是通过共有的ReferenceObject 这个key来确定target的)。
+ - 所以play阶段target是有值的(如果编辑时你确实binding了)。
+ - 那为什么不在创建时顺便设置target呢?我猜还是静态资产没办法赋scene中对象的原因。这些TrackAsset实际是没法存target对象的。
+
+build graph的方法调用链如下(也可以看到一个明显的c++端调用):
+
+
+### AnimationPlayable的权重处理
+
+Timeline 会利用 AnimationOutputWeightProcessor Evaluate方法处理AnimationMixer 、AnimationLayer和 AnimationPlayableOutput的权重。
+
+大概效果如下,设四个clip初始权重分别为0.26,0.14,0.19,0.13,连到各自Mixer和Layer后,如果输入权重和小于1,会**按比例放大**为:
+- `mixer input0 weight` :0.65= 0.26/(0.26+0.14)\*1
+- `mixer input1 weight `:0.35= 0.14/(0.26+0.14)\*1
+- `layer input0 weight` :0.56=(0.26+0.14)/(0.26+0.14+0.19+0.13)\*1
+- `AnimationOutput weight` : 0.72 = 0.26+0.14+0.19+0.13
+
+
+
+如果输入权重和≥1,则只对≥1的输入钳值到1,不等比例缩小,且AnimationOutput **weight 钳值为1**.
+
+比如四个clip初始权重分别为0.6,0.9,0.2,0.3,AnimationOutputWeightProcessor.Evaluate() 执行完后:
+- `mixer input0 weight` :0.6 (0.6+0.9>1不缩放,且0.6<1 不钳值)
+- `mixer input1 weight` :0.9(0.6+0.9>1不缩放,且0.9<1 不钳值)
+- `layer input0 weight` :1 (0.6+0.9+0.2+0.3>1不缩放,且0.6+0.9>1 则钳值到1)
+- `layer input1 weight` : 0.5 (0.6+0.9+0.2+0.3>1不缩放,且0.2+0.3<1 不钳值)
+- `AnimationOutput weight`:1(0.6+0.9+0.2+0.3>1 钳值到1)
+
+
+
+这么处理的目的按源码注释的说法:”对动画轨道上的权重进行后处理,以正确归一化混合器权重,从而避免混合时出现默认姿势,并确保子轨道、图层以及图层图正确混合。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 ”
+
+测试用例过长,参见:🔗TimelineTestForAnimationWeightInfo.
+
+## 运行 Graph
+
+graph play或evaluate后主要两个步骤:
+1. TimelinePlayable PrepareFrame() 处理clip的激活 与权重
+ 1. 搜索区间树,处理clip enable与disable(改变对应Playable的playstate)
+ 2. 根据mixin/mixout curve设置激活的clip所在的mixer input weight权重
+ 3. 执行AnimationOutputWeightProcessor.Evaluate()处理mixer layer和PlayaleOutput 的权重
+2. 后序遍历Playable执行playing态的Playable的ProcessFrame方法。
+
+
+graph play或evaluate后两个步骤
+
+当然这不是终点,数据经过一个个playable处理后,传递到playableOutput,最终交由target处理。
+### Playable数据传递与output处理数据
+
+ScriptPlayable数据处理在ProcessFrame中,可以实现Prefab的enable disable,video的播放暂停等功能。
+
+而基础Playable如AnimationClipPlayable,其Animation数据处理是隐藏起来的,实际是通过`AnimationStream`结构体进行数据交互,你可以 使用 `AnimationScriptPlayable` 并实现`IAnimationJob` 的 `ProcessAnimation` 方法来自定义处理动画数据。
+
+
+
+> [Playable使用细则](https://zhuanlan.zhihu.com/p/632890306)
+
+至于数据传入AnimatorPlayableOutput后怎么处理更是黑盒了,可以看一下下面这位博主的分析:
+
+
+
+> [死板地介绍Unity动画系统设计](https://zhuanlan.zhihu.com/p/305825751)
+
+Animator的高级使用估计得等到有IK或者更精细动画需求后,才可能再继续研究了。
+目前在SMB+Timeline加持下已能满足大部分需求。
+
+### IntervalTree 结构
+
+文章一开时给 Timeline 下的定义就是 “**包含很多可播放片段(Playable Clip)的区间树(IntervalTree)**”,下面便详细解析下 IntervalTree 的结构。
+
+TimelinePlayable利用IntervalTree来管理RuntimeClip,UML类图如下:
+
+
+IntervalTree UML类图
+
+- m_Entries:记录所有 RuntimeClip 和其左右边界的List
+
+- m_Nodes:IntervalTree中的节点
+ - node first/last: 表示存储在当前节点中的所有Runtimeclip中的首和尾index
+ - center:排序前比较重要,表示待排序Runtimeclip左右边界中点,用于后续递归地进行二分排序
+ - left/right:左右子节点
+
+
+
+Build IntervalTree 逻辑:
+
+``` csharp
+private void Rebuild()
+{
+ m_Nodes.Clear();
+ m_Nodes.Capacity = m_Entries.Capacity;
+ Rebuild(0, m_Entries.Count - 1);
+}
+
+private int Rebuild(int start, int end)
+{
+ IntervalTreeNode intervalTreeNode = new IntervalTreeNode();
+
+ // minimum size, don't subdivide
+ int count = end - start + 1;
+ if (count < kMinNodeSize)
+ {
+ intervalTreeNode = new IntervalTreeNode() { center = kCenterUnknown, first = start, last = end, left = kInvalidNode, right = kInvalidNode };
+ m_Nodes.Add(intervalTreeNode);
+ return m_Nodes.Count - 1;
+ }
+
+ var min = Int64.MaxValue;
+ var max = Int64.MinValue;
+
+ for (int i = start; i <= end; i++)
+ {
+ var o = m_Entries[i];
+ min = Math.Min(min, o.intervalStart);
+ max = Math.Max(max, o.intervalEnd);
+ }
+
+ var center = (max + min) / 2;
+ intervalTreeNode.center = center;
+
+ // first pass, put every thing left of center, left
+ int x = start;
+ int y = end;
+ while (true)
+ {
+ while (x <= end && m_Entries[x].intervalEnd < center)
+ x++;
+
+ while (y >= start && m_Entries[y].intervalEnd >= center)
+ y--;
+
+ if (x > y)
+ break;
+
+ var nodeX = m_Entries[x];
+ var nodeY = m_Entries[y];
+
+ m_Entries[y] = nodeX;
+ m_Entries[x] = nodeY;
+ }
+
+ intervalTreeNode.first = x;
+
+ // second pass, put every start passed the center right
+ y = end;
+ while (true)
+ {
+ while (x <= end && m_Entries[x].intervalStart <= center)
+ x++;
+
+ while (y >= start && m_Entries[y].intervalStart > center)
+ y--;
+
+ if (x > y)
+ break;
+
+ var nodeX = m_Entries[x];
+ var nodeY = m_Entries[y];
+
+ m_Entries[y] = nodeX;
+ m_Entries[x] = nodeY;
+ }
+
+ intervalTreeNode.last = y;
+
+ // reserve a place
+ m_Nodes.Add(new IntervalTreeNode());
+ int index = m_Nodes.Count - 1;
+
+ intervalTreeNode.left = kInvalidNode;
+ intervalTreeNode.right = kInvalidNode;
+
+ if (start < intervalTreeNode.first)
+ intervalTreeNode.left = Rebuild(start, intervalTreeNode.first - 1);
+
+ if (end > intervalTreeNode.last)
+ intervalTreeNode.right = Rebuild(intervalTreeNode.last + 1, end);
+
+ m_Nodes[index] = intervalTreeNode;
+ return index;
+}
+```
+
+构建思路非常简单:
+- 排序开始时,遍历所有Runtimeclip,确定最左端和最右端边界,然后取边界的中点作为根节点的center,这样构建的tree不至于一边短一边长。
+- 然后把所有Runtimeclip完全在中间值center左侧的放到m_Entries靠下位置,把所有Runtimeclip完全在中间值center右侧的放到m_Entries靠上位置,被center贯穿的Runtimeclip保留在当前节点中。
+- 这样就完成了初步排序,然后再递归地排序靠下的这坨,并作为左子节点;再递归地排序靠上的这坨,并作为右子节点。这样整棵IntervalTree就构建完成了。
+
+最终结构如下图所示:
+
+
+IntervalTree内部结构示例
+
+### RuntimeClip 结构
+
+下面就以 AnimationClip 为例,讲解IntervalTreeNode中RuntimeClip的创建逻辑:
+
+1. 创建**TimelineClip** (主逻辑在TrackAsset)
+ 1. 根据TrackClipTypeAttribute的定义获取限定的ClipType`GetType().GetCustomAttributes(typeof(TrackClipTypeAttribute))`,
+ 2. 创建限定ClipType的 TimelineClip 容器`newClip = CreateNewClipContainerInternal()`
+ 3. 把特定类型 ClipPlayableAsset 比如AnimationPlayableAsset塞进TimelineClip中asset参数中。(此时AnimationPlayableAsset中clip为null)
+
+2. 将具体AnimationClip塞进AnimationPlayableAsset的clip变量中`AddClipOnTrack(newClip, parentTrack, candidateTime, assignableObject, state)`
+3. 创建**RuntimeClip** (主逻辑在TrackAsset 的 CompileClips方法)
+ 1. 根据timelineClip和clip对应的Playable创建RuntimeClip
+ 2. 将新建的RuntimeClip加入区间树**IntervalTree**中
+
+4. 重新排序区间树节点(可延迟执行)
+
+最终,TimelineClip 与 RuntimeClip结构如下:
+
+
+TimelineClip 与 RuntimeClip结构
+
+### 运行时 IntervalTree
+
+我们使用区间树 IntervalTree 最主要目的就是能快速搜索RuntimeClip,为什么呢?
+
+因为每帧Timeline都会执行PrepareFrame方法,指明哪些clip在当前时间激活,哪些clip在当前时间停止,如果只是用List结构,搜索时间复杂度 `O(n)`,而使用IntervalTree,时间复杂度便降到 `O(logn)`了。
+
+`TimelinePlayable` **PrepareFrame** 方法执行逻辑:
+
+1. 利用IntervalTree获取当前帧所有激活的RuntimeClip
+2. disable上一帧激活,这一帧未激活的clip(会执行Playable的Pause()方法 )
+3. enable这一帧激活的clip(会执行Playable的Play()方法 )
+4. 根据mixin/mixout curve设置此clip所在的mixer input weight权重
+
+
+
+## PlayableGraph 的生命周期
+
+前文运行时多次提到了 PrepareFrame、ProcessFrame,其实这就是**ScriptPlayable**在某段生命周期执行的回调函数,我们可以利用 ScriptPlayable 传入自定义**PlayableBehaviour** 来干涉Playable的生命周期。
+
+Playable的生命周期有:
+- `GraphStart`:graph play 开始时
+- `GraphStop`:graph stop时
+- `PlayableCreate`:Playable Create时
+- `PlayableDestroy`:Playable Destroy时
+- `BehaviourPlay`:Playable 运行时
+- `BehaviourPause`:Playable 暂停时
+- `PrepareFrame`:每帧处理数据前
+- `ProcessFrame`:每帧开始处理数据时
+
+当然可能并非都是这些生命周期,比如`AnimationScriptPlayable`就有个`ProcessAnimation`和`ProcessRootMotion`过程,可能就没有所谓的ProcessFrame了。
+
+当然对于ScriptPlayable,上面的生命周期是确定的,在Playable处于上面的生命周期时,便会执行对应的回调函数(注册在PlayableBehaviour里)。
+
+下面的自定义PlayableBehaviour就是简单打印回调方法和playable:
+
+``` csharp
+public class TimelineTestForAnimationScriptBehaviour : PlayableBehaviour
+ {
+ public override void OnGraphStart(Playable playable)
+ {
+ DebugLog(playable,"OnGraphStart");
+ }
+
+ private static void DebugLog(Playable playable, string methodName)
+ {
+ Debug.Log($"Playable by in/output cnt_{playable.GetInputCount()}{playable.GetOutputCount()} Behaviour:{methodName}");
+ }
+
+ public override void OnGraphStop(Playable playable)
+ {
+ DebugLog(playable,"OnGraphStop");
+ }
+
+ public override void OnPlayableCreate(Playable playable)
+ {
+ DebugLog(playable,"OnPlayableCreate");
+ }
+
+ public override void OnPlayableDestroy(Playable playable)
+ {
+ DebugLog(playable,"OnPlayableDestroy");
+ }
+
+ public override void OnBehaviourPlay(Playable playable, FrameData info)
+ {
+ DebugLog(playable,"OnBehaviourPlay");
+ }
+
+ public override void OnBehaviourPause(Playable playable, FrameData info)
+ {
+ DebugLog(playable,"OnBehaviourPause");
+ }
+
+ public override void PrepareFrame(Playable playable, FrameData info)
+ {
+ DebugLog(playable,"PrepareFrame");
+ }
+
+ public override void ProcessFrame(Playable playable, FrameData info, object playerData)
+ {
+ DebugLog(playable,"ProcessFrame");
+ }
+ }
+```
+
+### 生命周期验证
+
+上面生命周期的理解还比较模糊,尤其父子节点回调执行顺序是什么需要验证。
+
+验证代码如下(给不同节点设置不同端口数能很方便辨析是哪个节点的回调):
+
+``` csharp
+using System;
+using Scenes.TimelineTest.scripts;
+using UnityEditor;
+using UnityEngine;
+using UnityEngine.Playables;
+
+public class TimelineTestForLifeCycle : MonoBehaviour
+{
+ public PlayableGraph graph;
+
+ private ScriptPlayable playable3;
+ public void CreateGraph()
+ {
+ DestroyGraph();
+ graph = PlayableGraph.Create("TimelineTestForLifeCycle");
+ ScriptPlayableOutput output = ScriptPlayableOutput.Create(graph,"LifeCycleTestOutput");
+ var playable5 = ScriptPlayable.Create(graph,5);
+ var playable4 = ScriptPlayable.Create(graph,4);
+ playable3 = ScriptPlayable.Create(graph,3);
+ var playable2 = ScriptPlayable.Create(graph,2);
+ var playable1 = ScriptPlayable.Create(graph,1);
+ output.SetSourcePlayable(playable5,0);
+ output.SetWeight(1);
+ playable5.ConnectInput(0,playable1,0,1);
+ playable5.ConnectInput(1,playable2,0,0.5f);
+ playable3.Pause();
+ playable5.ConnectInput(2,playable3,0,1);
+ playable5.ConnectInput(3,playable4,0,0);
+
+ var playable9 = ScriptPlayable.Create(graph,9);
+ var playable8 = ScriptPlayable.Create(graph,8);
+ playable9.ConnectInput(0,playable8,0,1);
+ }
+
+ public void PlayPlayable3()
+ {
+ playable3.Play();
+ }
+ public void PausePlayable3()
+ {
+ playable3.Pause();
+ }
+ public void DestroyPlayable3()
+ {
+ playable3.Destroy();
+ }
+
+ public void PlayGraph()
+ {
+ graph.Play();
+ }
+ private void OnDestroy()
+ {
+ DestroyGraph();
+ }
+
+ public void DestroyGraph()
+ {
+ if (graph.IsValid())
+ {
+ Debug.Log("Execute Graph Destroy");
+ graph.Destroy();
+ Debug.Log("Graph destroyed");
+ }
+ }
+}
+
+[CustomEditor(typeof(TimelineTestForLifeCycle))]
+class TimelineTestForLifeCycleEditor : Editor
+{
+ public override void OnInspectorGUI()
+ {
+ base.OnInspectorGUI();
+ TimelineTestForLifeCycle script = (TimelineTestForLifeCycle) target;
+ if (GUILayout.Button("CreateGraph"))
+ {
+ script.CreateGraph();
+ }
+ if (GUILayout.Button("PlayGraph"))
+ {
+ script.PlayGraph();
+ }
+ if (GUILayout.Button("Destroy graph"))
+ {
+ script.DestroyGraph();
+ }
+ if (GUILayout.Button("Play Playable3"))
+ {
+ script.PlayPlayable3();
+ }
+
+ if (GUILayout.Button("Pause Playable3"))
+ {
+ script.PausePlayable3();
+ }
+ if (GUILayout.Button("Destroy Playable3"))
+ {
+ script.DestroyPlayable3();
+ }
+ }
+}
+
+```
+
+点击 “CreateGraph” 后的 graph 结构(图中标注的是节点设置的play state和连接权重):
+
+
+生命周期验证示例的graph图
+
+当前脚本主要测试graph的 create与play(除了prepareFrame、processFrame),验证:
+- graph不play,playable节点是否会运行
+- 没有playableOutput,playable节点是否会运行
+- 输出权重为0,playable节点是否会运行
+- pause状态,playable节点是否会运行
+- PrepareFrame、ProcessFrame顺序
+- Graph destroy执行效果
+
+
+打印结果:
+``` csharp
+// 打印
+Playable by in/output cnt_51 Behaviour:OnPlayableCreate
+Playable by in/output cnt_41 Behaviour:OnPlayableCreate
+Playable by in/output cnt_31 Behaviour:OnPlayableCreate
+Playable by in/output cnt_21 Behaviour:OnPlayableCreate
+Playable by in/output cnt_11 Behaviour:OnPlayableCreate
+Playable by in/output cnt_91 Behaviour:OnPlayableCreate
+Playable by in/output cnt_81 Behaviour:OnPlayableCreate
+Playable by in/output cnt_51 Behaviour:OnGraphStart
+Playable by in/output cnt_51 Behaviour:OnBehaviourPlay
+Playable by in/output cnt_11 Behaviour:OnGraphStart
+Playable by in/output cnt_11 Behaviour:OnBehaviourPlay
+Playable by in/output cnt_21 Behaviour:OnGraphStart
+Playable by in/output cnt_21 Behaviour:OnBehaviourPlay
+Playable by in/output cnt_31 Behaviour:OnGraphStart
+Playable by in/output cnt_31 Behaviour:OnBehaviourPause
+Playable by in/output cnt_41 Behaviour:OnGraphStart
+Playable by in/output cnt_41 Behaviour:OnBehaviourPlay
+
+Playable by in/output cnt_51 Behaviour:PrepareFrame
+Playable by in/output cnt_11 Behaviour:PrepareFrame
+Playable by in/output cnt_21 Behaviour:PrepareFrame
+Playable by in/output cnt_41 Behaviour:PrepareFrame
+Playable by in/output cnt_11 Behaviour:ProcessFrame
+Playable by in/output cnt_21 Behaviour:ProcessFrame
+Playable by in/output cnt_41 Behaviour:ProcessFrame
+Playable by in/output cnt_51 Behaviour:ProcessFrame
+
+Graph destroyed
+Playable by in/output cnt_51 Behaviour:OnBehaviourPause
+Playable by in/output cnt_11 Behaviour:OnBehaviourPause
+Playable by in/output cnt_21 Behaviour:OnBehaviourPause
+Playable by in/output cnt_41 Behaviour:OnBehaviourPause
+Playable by in/output cnt_51 Behaviour:OnGraphStop
+Playable by in/output cnt_51 Behaviour:OnPlayableDestroy
+Playable by in/output cnt_41 Behaviour:OnGraphStop
+Playable by in/output cnt_41 Behaviour:OnPlayableDestroy
+Playable by in/output cnt_31 Behaviour:OnGraphStop
+Playable by in/output cnt_31 Behaviour:OnPlayableDestroy
+Playable by in/output cnt_21 Behaviour:OnGraphStop
+Playable by in/output cnt_21 Behaviour:OnPlayableDestroy
+Playable by in/output cnt_11 Behaviour:OnGraphStop
+Playable by in/output cnt_11 Behaviour:OnPlayableDestroy
+Playable by in/output cnt_91 Behaviour:OnPlayableDestroy
+Playable by in/output cnt_81 Behaviour:OnPlayableDestroy
+```
+
+结合Graph Monitor 可以得出结论:
+- graph不play,playable节点不会运行
+- 没有连ScriptPlayableOutput,ScriptPlayable节点不会运行(playable time不随时间增加),且除了OnPlayableCreate和OnPlayableDestroy,其他回调方法不会触发
+- 输出权重为0,playable节点依然运行
+- pause状态,playable节点不会运行
+- graph未play,playable执行pause并不会触发OnBehaviourPause
+- graph 执行Play() 后:若playable是playing状态时,则触发OnBehaviourPlay回调;是paused状态时,则触发OnBehaviourPause回调
+- 会以**前序**遍历(父节点优先)的方式触发OnGraphStart 和 OnBehaviourPlay/Pause
+- 会以**前序**遍历(父节点优先)的方式触发PrepareFrame回调
+- 会以**后序**遍历(子节点优先)的方式触发ProcessFrame回调
+- 没有连接playableOutput的playable节点不会触发OnGraphStart/OnGraphStop回调
+- 直接Destroy graph时,如果graph未stop会先触发OnBehaviourPause、OnGraphStop回调,这两个回调的触发逻辑与先Stop Graph后Destroy Graph 略有不同,如下日志:
+
+``` csharp
+// 先Stop graph 再 Destroy graph的日志(与直接Destroy graph有差异)
+graph.Stop()
+
+Playable by in/output cnt_51 Behaviour:OnBehaviourPause
+Playable by in/output cnt_51 Behaviour:OnGraphStop
+Playable by in/output cnt_11 Behaviour:OnBehaviourPause
+Playable by in/output cnt_11 Behaviour:OnGraphStop
+Playable by in/output cnt_21 Behaviour:OnBehaviourPause
+Playable by in/output cnt_21 Behaviour:OnGraphStop
+Playable by in/output cnt_31 Behaviour:OnBehaviourPause
+Playable by in/output cnt_31 Behaviour:OnGraphStop
+Playable by in/output cnt_41 Behaviour:OnBehaviourPause
+Playable by in/output cnt_41 Behaviour:OnGraphStop
+
+Graph.Destroy()
+
+Playable by in/output cnt_51 Behaviour:OnPlayableDestroy
+Playable by in/output cnt_41 Behaviour:OnPlayableDestroy
+Playable by in/output cnt_31 Behaviour:OnPlayableDestroy
+Playable by in/output cnt_21 Behaviour:OnPlayableDestroy
+Playable by in/output cnt_11 Behaviour:OnPlayableDestroy
+Playable by in/output cnt_91 Behaviour:OnPlayableDestroy
+Playable by in/output cnt_81 Behaviour:OnPlayableDestroy
+```
+
+完整生命周期:
+
+
+# 总结
+
+Timeline和Playable系统,本文还是花了不少精力的:
+- 详细展示了Timeline和Playable各个类的UML关系
+- 系统梳理了Timeline静态和运行时结构与逻辑
+- 详细分析了Playable、区间树IntervalTree等内部结构
+- 验证了很多细节,比如AnimationOutput weight作用、ScriptPlayable生命周期、Passthrough TraversalMode的效果等
+
+核心都在下面这张图中,再回顾下:
+
+
+
+吃透了本文,自定义Track就是易如反掌了。
+
+下一篇可能写点技能编辑器相关。
+
+记得点赞、关注、收藏哦~
diff --git a/InBox/渲染/软渲染/图片/FAQ.md b/InBox/渲染/软渲染/图片/FAQ.md
deleted file mode 100644
index 34beed0..0000000
--- a/InBox/渲染/软渲染/图片/FAQ.md
+++ /dev/null
@@ -1,142 +0,0 @@
-# The quicktype FAQ
-
-- [What is this?](#what-is-this)
-- [How does this work?](#how-does-this-work)
-- [How do I use this with my code?](#how-do-i-use-this-with-my-code)
-- [No code appears when I paste my JSON](#why-does-quicktype-complain-about-my-json)
-- [I think I found a bug!](#i-think-i-found-a-bug)
-- [When will you support my favorite language?](#when-will-you-support-my-favorite-language)
-- [Why do my types have weird names?](#why-do-my-types-have-weird-names)
-- [I'd like the output to be a little different.](#id-like-the-output-to-be-a-little-different)
-- [Am I allowed to use the generated code in my software?](#am-i-allowed-to-use-the-generated-code-in-my-software)
-- [This map/dictionary should be a class!](#this-mapdictionary-should-be-a-class)
-- [This class should be a map/dictionary!](#this-class-should-be-a-mapdictionary)
-- [Where can I learn more about JSON Schema?](#where-can-i-learn-more-about-json-schema)
-- [I'd like to customize the output for my particular application.](#id-like-to-customize-the-output-for-my-particular-application)
-- [How can I control the property order in JSON Schema?](#how-can-i-control-the-property-order-in-json-schema)
-- [quicktype is awesome, I'd like to support it!](#quicktype-is-awesome-id-like-to-support-it)
-- [How is this different from other JSON converters?](#how-is-quicktype-different-from-other-json-converters)
-
-## What is this?
-
-[quicktype](https://app.quicktype.io) produces nice types and JSON (de)serializers for many programming languages. It can infer types from JSON but also takes types from JSON Schema, TypeScript, and GraphQL.
-
-## How does this work?
-
-You paste JSON on the left, and code appears on the right. [This video](https://www.youtube.com/watch?v=19bMU7jZ27w) gives a quick demonstration.
-
-## How is this different from other JSON converters?
-
-`quicktype` is superior to other JSON converters in many important ways:
-
-- **Type inference**: quicktype infers optionals, dates, UUIDs, enums, integers, and unions. It also infers maps (versus objects) using a Markov chain.
-- **Careful naming**: quicktype’s naming system creates nice, legal, unique names for types and properties, handling reserved words and tricky corner cases (e.g. `{ “”: “this is legal”, “null”: “so is this”, "1;DROP TABLE users”: “and this" }`).
-- **Heterogeneous data**: JSON data is often heterogenous. quicktype infers this, and creates union types in languages that support them, or synthetic union types in languages that don’t (e.g. try quicktyping `[0, “zero”]` as Swift and Go).
-- **Type unification**. This works across multiple samples, so you can quicktype a directory of API data, for example, and unify types across all responses (e.g. you’ll get just one `Customer` type, even if customer data occurs in many samples). You can also provide multiple samples for the same type for better coverage.
-- **Marshalling code**: In addition to types, quicktype generates functions for marshalling your types to and from JSON.
-- Supports dynamic languages: quicktype can add dynamic typechecks for JavaScript, TypeScript, Flow, Python, and Ruby.
-- **Convenient CLI**: Run `quicktype https://blockchain.info/latestblock -o LatestBlock.ts` to quicktype a Bitcoin API in TypeScript.
-- **Client-side**: [The web version of quicktype](https://app.quicktype.io/) runs on the client, so servers never see your data (most JSON converters send your JSON to their server)
-- **Typed input**: Feed quicktype TypeScript or JSON Schema instead of JSON for better control over generated types.
-- **Code quality**: quicktype emits clean code
-
-## How do I use this with my code?
-
-The generated code has comments at the start with a short code sample that shows how to convert a JSON string to instances of the generated types. You can also go the other way, which is very easy, too, but you'll have to look at the generated code to see how it works.
-
-## No code appears when I paste my JSON
-
-This is probably because your JSON is invalid. The most common issues we're seeing are
-
-- Trailing commas in arrays and objects: `[1, 2, 3]` is valid JSON, while `[1, 2, 3,]` is not.
-
-- Unquoted property keys in objects: `{ "name": "Mark" }` is valid JSON, while `{ name: "Mark" }` is not.
-
-- Comments: JSON does not support comments.
-
-If you're unsure whether your JSON is valid, please use [JSONLint](https://jsonlint.com).
-
-## I think I found a bug!
-
-Please [file an issue on GitHub](https://github.com/quicktype/quicktype/issues). Give as much context as you can so that we can reproduce it. Assume we know nothing about what you're trying to do (because we don't).
-
-## When will you support my favorite language?
-
-Please check whether there is [a pull request](https://github.com/quicktype/quicktype/pulls) that adds support for your language. If there is, please consider helping it along. If there isn't, please consider contributing one.
-
-## Why do my types have weird names?
-
-Sometimes quicktype has trouble giving names to your types. There are a couple of causes for this:
-
-- The name quicktype would like to give to your type conflicts with a name that's already used in the target language, such as `String` in C#.
-
-- Two or more types in your data have the same name.
-
-- A type has so many potential names that quicktype can't find a commonality between them.
-
-If you're using JSON Schema, you can use the `title` property to give a type a name that quicktype tries hard to use.
-
-## I'd like the output to be a little different.
-
-Check out the "Language" and "Other" tabs in the options panel. What you're looking for might just be there:
-
-
-
-If it isn't, then depending on your coding skills, you might be able to [customize the output](https://blog.quicktype.io/customizing-quicktype/).
-
-## Am I allowed to use the generated code in my software?
-
-Yes, there are no intellectual property restrictions on the code that quicktype generates.
-
-## This map/dictionary should be a class!
-
-quicktype has [advanced heuristics](https://blog.quicktype.io/markov/) to decide whether a JSON object should be represented by a class or a map, but sometimes it gets it wrong. If it generates a map, but you'd rather have a class, you can disable map detection in the options panel:
-
-
-
-## This class should be a map/dictionary!
-
-quicktype has [advanced heuristics](https://blog.quicktype.io/markov/) to decide whether a JSON object should be represented by a class or a map, but sometimes it gets it wrong. If it generates a class, but you need a map, first make sure that you have the "Detect maps" option set in the options panel. Also, check that all properties in the object have the same exact type because quicktype only makes a map if that's the case. If that still doesn't do it, you have two options:
-
-- Duplicate some of the properties and/or change the property names to look more random.
-
-- Output JSON Schema, modify it to produce a map, and then use the schema as the input to quicktype.
-
-## Where can I learn more about JSON Schema?
-
-The [JSON Schema homepage](http://json-schema.org) contains many links and resources.
-
-## I'd like to customize the output for my particular application.
-
-We have [a blog post](https://blog.quicktype.io/customizing-quicktype/) on that very topic.
-
-## How can I control the property order in JSON Schema?
-
-There is a custom schema field `quicktypePropertyOrder` which can be used to specify the order of properties for quicktype.
-
-For example:
-```json
- ...
- "Location": {
- "quicktypePropertyOrder": [ "latitude", "longitude" ],
- "type": "object",
- "properties": {
- "latitude": {
- "type": "number",
- "description": "The latitude component of the location",
- "example": -32.204754
- },
- ...
- },
- "required": [ "latitude", "longitude" ]
- },
- ...
-```
-
-## quicktype is awesome, I'd like to support it!
-
-There are many ways you can support quicktype:
-
-- Tell all your friends about it! Show it around, tweet about it, write a blog post, present it at a lightning talk.
-
-- quicktype is open source - please contribute! We need documentation at least as much as code, so you don't need strong coding skills to make an impact. If you do, we have [lots of open issues](https://github.com/quicktype/quicktype/issues) that need resolving, almost all of our target languages can be improved, and there are many, many programming languages that quicktype doesn't support yet. Talk to us [on Slack](http://slack.quicktype.io) if you're interested - we're always happy to help.