1465 lines
73 KiB
Markdown
1465 lines
73 KiB
Markdown
# 自研帧动画系统需求文档
|
||
|
||
> 本文档定义自研帧动画系统第一版的系统边界、数据结构、资产导入、编辑器工作流、运行时行为和验收标准。具体类拆分、Unity API 选型与代码组织在技术设计和实现阶段确定。
|
||
|
||
## 1. 背景与问题
|
||
|
||
### 1.1 项目背景
|
||
|
||
本项目是 2D 像素风格 AVG / 视觉小说游戏,动画主要服务于角色立绘、场景物件和少量 UI 表现。当前项目中,大部分序列帧动画显示在 `SpriteRenderer` 上,少量显示在 UI `Image` 上。
|
||
|
||
现阶段主要使用 Unity 原生 Animator / AnimationClip / Timeline 等系统完成表现。其中 Animator 更适合状态机式角色动作,而本项目大量需求更接近“演出编排”:播放一个片段、接另一个片段、最后进入 Idle,或者停留在某一帧。
|
||
|
||
### 1.2 当前痛点
|
||
|
||
#### Unity Animator / AnimationClip 相关
|
||
|
||
1. 状态机模型与项目需求不匹配
|
||
Unity Animator 的核心是状态机,适合玩家可控角色在多个状态之间切换;本项目常见需求是线性演出、片段编排、播完后进入 Idle 或停在终帧。
|
||
|
||
2. 资产结构臃肿
|
||
简单序列帧动画也需要 `Animator Controller`、`Animation Clip`、Sprite 绑定等多层资源。资源关系依赖 Unity 序列化引用,更新和排查成本高。
|
||
|
||
3. 预览体验差
|
||
Animator Controller 难以直接预览,Animation Clip 预览通常需要依附 GameObject。策划或美术无法轻松检查一个片段或一组片段的最终效果。
|
||
|
||
4. 帧内容不可读、不可维护
|
||
Animation Clip 内部通过 Unity 对象引用记录 Sprite,难以直观看到每一帧具体使用了哪个 Sprite、持续多久,也不方便做批量替换或差异检查。
|
||
|
||
#### 美术资产导入相关
|
||
|
||
1. Aseprite 导入链路不稳定
|
||
当前美术使用 Aseprite 生产资源。Unity Aseprite Importer 存在中文 Tag 支持不佳、资源更新后 Clip 重新生成导致引用关系变化等问题。
|
||
|
||
2. 现有 AnimationClip 生成器只能部分解决问题
|
||
项目中已有 PNG + JSON -> 切图 -> 生成 AnimationClip 的工具,但最终产物仍然是 Unity AnimationClip,仍然保留了 Animator / Clip 体系的维护成本。
|
||
|
||
3. 美术更新成本高
|
||
理想流程应当是:美术更新 PNG / JSON 后,策划或开发点击刷新,已有动画资产保持引用不变,只更新帧数据和 Sprite 映射。
|
||
|
||
### 1.3 文档目标
|
||
|
||
本文档定义一个独立于 Unity Animator Controller 的自研帧动画系统,重点解决:
|
||
|
||
- 序列帧动画的数据结构
|
||
- PNG / Aseprite JSON 等来源的导入与刷新
|
||
- FrameClip / FrameAnimationGraph / AnimationFlow 的组织关系
|
||
- 编辑器内预览、检查和编排工作流
|
||
- 运行时播放器接口
|
||
|
||
## 2. 系统目标与非目标
|
||
|
||
### 2.1 核心目标
|
||
|
||
1. 支持 SpriteRenderer 和 UI Image 的序列帧播放。
|
||
2. 支持单个动画片段独立播放、预览、刷新和检查。
|
||
3. 支持同一角色或对象的一组动画片段集中管理。
|
||
4. 支持命名演出序列编排,例如“播放一次动作 -> 进入 Idle 循环”或“连续播放多个片段 -> 停在最后一帧”。
|
||
5. 支持从 Aseprite JSON / PNG 生成动画数据,并在资源更新时保持动画资产引用稳定。
|
||
6. 提供不依赖场景 GameObject 的编辑器预览能力。
|
||
7. 为运行时代码提供清晰、稳定、可迁移的播放 API。
|
||
|
||
### 2.2 非目标
|
||
|
||
第一版不尝试替代以下系统:
|
||
|
||
1. Unity Timeline 的多轨演出能力。
|
||
2. DOTween 等 Tween 动效系统。
|
||
3. Cinemachine 镜头系统。
|
||
4. 骨骼动画、网格变形、复杂 Transform 曲线动画。
|
||
5. 通用状态机编辑器。
|
||
|
||
### 2.3 第一版范围
|
||
|
||
第一版包含:
|
||
|
||
- `FrameClip`:单个序列帧片段。
|
||
- `FrameAnimationGraph`:同一角色或对象的完整帧动画图资产。
|
||
- `AnimationFlow`:`FrameAnimationGraph` 全局节点图中的命名演出流程。
|
||
- `FrameAnimationPlayer`:运行时播放器。
|
||
- Graph / Clip / Flow 的基础编辑器和预览。
|
||
|
||
第一版完成系统自身的数据、导入、编辑器和运行时播放闭环。复杂节点类型、条件分支和随机播放不进入第一版,但数据模型保留节点式演出编排的扩展空间。
|
||
|
||
## 3. 术语与概念
|
||
|
||
### 3.1 Frame
|
||
|
||
单帧数据,描述某一时间段内应该显示的 Sprite。
|
||
|
||
字段:
|
||
|
||
- `Sprite sprite`
|
||
- `int durationMs`
|
||
- `string frameName`
|
||
- `int sourceIndex`
|
||
|
||
规则:
|
||
|
||
- 帧时长使用 `int durationMs` 保存毫秒值;运行时可按需换算为秒。
|
||
- `sprite` 允许为空;空帧表示该帧不显示 Sprite,但仍占用对应时长。
|
||
- Frame 只保存运行时播放必要信息,导入元数据放在 Clip / Graph 的来源信息中。
|
||
|
||
### 3.2 FrameClip
|
||
|
||
最小可播放动画片段,由一组 Frame 组成。
|
||
|
||
职责:
|
||
|
||
- 保存帧列表。
|
||
- 保存默认播放速度。
|
||
- 保存默认结束行为。
|
||
- 保存导入来源。
|
||
- 提供总时长、帧数等只读信息。
|
||
|
||
规则:
|
||
|
||
- Clip 不单独保存 `bool loop`,循环统一由 `defaultEndBehavior = Loop` 表达。
|
||
- 导入生成的 Clip 帧表只读,不允许直接手动编辑帧内容。
|
||
- Graph 外创建的 Manual Clip 可以保存为独立 `.asset`;Graph 内导入生成的 Clip 保存为 Graph 的 sub-asset。
|
||
|
||
### 3.3 FrameAnimationGraph
|
||
|
||
同一角色或对象的完整帧动画图资产。它不是简单的 Clip 集合,而是承载该对象的基础片段、导入来源、全局节点图、多个命名演出流程、对外可播放对象以及编辑器配置的顶层资产。
|
||
|
||
职责:
|
||
|
||
- 维护 Clip / Flow 统一可播放 id 到目标对象的映射。
|
||
- 维护全局 `AnimationNode` / `AnimationEdge` 节点图。
|
||
- 维护多个 `AnimationFlow`。
|
||
- 维护导入来源列表。
|
||
- 支持刷新导入来源后按 `importSourceId + sourceTagName` 更新已有 Clip。
|
||
- 以 Clip 或 Flow 作为对外可播放对象,例如 `Idle`、`WakeUp`、`StartTalking`。
|
||
- 提供编辑器集中预览、检查和演出编排。
|
||
|
||
规则:
|
||
|
||
- 顶层资产命名为 `FrameAnimationGraph`。
|
||
- `FrameAnimationGraph` 拥有一套全局节点和连线。
|
||
- `AnimationFlow` 是全局节点图里的命名演出流程。
|
||
- 在 Graph 外创建的 Manual Clip 是独立资产;由 Graph 内图片 / JSON 生成的 Imported Clip 必须是所属 Graph 的 sub-asset。
|
||
|
||
整体模型:
|
||
|
||
- 采用“单一全局节点图 + 多个 AnimationFlow”的模型。
|
||
- 一个 Graph 内允许存在多个不联通子图。
|
||
- 不同 Flow 可以从不同入口节点开始,也可以共享部分节点,例如多个 Flow 最终进入同一个 Idle 节点。
|
||
- 之前考虑过“多个独立子编排各自保存 nodes / edges”的模型,但它不如全局图贴合节点编辑器心智,也不利于跨 Flow 共享节点。
|
||
- Clip 和 Flow 都是一等可播放对象,共享同一个可播放 id 命名空间,彼此不能重名。
|
||
- 外部正式播放通过统一的 playable id 调用,不需要显式区分目标是 Clip 还是 Flow。
|
||
- 第一版 Clip 浏览按 ImportSource / Manual 分组并支持扁平模式;自定义分组属于后续扩展。
|
||
|
||
### 3.4 AnimationFlow
|
||
|
||
`AnimationFlow` 是 `FrameAnimationGraph` 全局节点图中的一个命名演出流程。它不是独立图,也不直接拥有节点和连线;它通过入口节点和默认播放策略,定义一段可预览、可调用的动画流程。
|
||
|
||
一个 `FrameAnimationGraph` 可以包含多个 `AnimationFlow`。每个 Flow 可以对应一段线性演出,也可以对应全局图中的一个可达子图。多个 Flow 可以共享节点和连线。
|
||
|
||
`AnimationFlow` 的核心语义不是“线性序列”,而是“演出流程”。它可以在第一版主要表现为 Clip 节点串联,但数据结构不应限制后续出现分支、等待、随机、事件和回环。
|
||
|
||
能力范围:
|
||
|
||
- 线性播放:`Clip A -> Clip B -> Clip C`
|
||
- 播完进入 Idle:`Intro -> Idle(loop)`
|
||
- 播完停在最后一帧
|
||
- 播完隐藏目标
|
||
|
||
第一版只实现 Clip 节点;没有后继 Edge 的 Clip 节点就是流程终点,不额外提供 End 节点。
|
||
|
||
### 3.5 FrameAnimationPlayer
|
||
|
||
运行时负责推进时间、设置 Sprite、响应代码调用的组件。
|
||
|
||
规则:
|
||
|
||
- 第一版只提供 `FrameAnimationPlayer` 组件,不额外拆分 `FrameAnimationController`。
|
||
- `FrameAnimationPlayer` 挂载在被控制的 GameObject 上,内部包含时间推进、Graph 解析、播放状态和目标组件适配逻辑。
|
||
- Player 必须与一个 `SpriteRenderer` 或 `UnityEngine.UI.Image` 挂在同一 GameObject;目标适配属于内部实现,不作为第一版公开绑定接口。
|
||
- 第一版不引入全局注册中心。
|
||
- FrameClip 和 AnimationFlow 都可以被正式播放;调用方只提供统一 playable id,由 Graph 解析具体目标类型。
|
||
- AnimationNode 只属于 Flow 内部结构,不作为正式外部播放目标。
|
||
|
||
## 4. 数据结构需求
|
||
|
||
### 4.1 FrameClip 数据
|
||
|
||
字段:
|
||
|
||
```csharp
|
||
string id;
|
||
string displayName;
|
||
List<FrameAnimationFrame> frames;
|
||
float speed = 1f;
|
||
FrameClipEndBehavior defaultEndBehavior;
|
||
FrameClipImportInfo importInfo; // null 表示 Manual Clip
|
||
```
|
||
|
||
单帧数据:
|
||
|
||
```csharp
|
||
Sprite sprite; // 可为空,null 表示空帧
|
||
int durationMs;
|
||
string frameName;
|
||
int sourceIndex;
|
||
```
|
||
|
||
结束行为:
|
||
|
||
- `HoldLastFrame`
|
||
- `Loop`
|
||
- `Clear`
|
||
- `HideTarget`
|
||
|
||
规则:
|
||
|
||
- `id` 同时用于 Graph 内 AnimationNode 引用和外部统一播放调用;导入刷新不依赖 `id`。
|
||
- `displayName` 是编辑器展示名,默认与 `id` 一致。
|
||
- `id` 和 `displayName` 都允许中文。
|
||
- 从 Aseprite Tag 导入时,默认 `id = tagName`,`displayName = tagName`。
|
||
- 修改 `displayName` 不影响引用。
|
||
- 允许修改 `id`,但 Clip id 是外部播放契约。编辑器必须检查与所有 Clip / Flow 的冲突,显示受影响的内部引用数量,并对无法自动修复的外部字符串引用给出强警告。
|
||
- 用户确认后,编辑器原子更新 Clip id 和 Graph 内全部 AnimationNode 引用;Graph 外部字符串引用由用户负责迁移。
|
||
- 不单独保存 `bool loop`,循环由 `defaultEndBehavior = Loop` 表达。
|
||
- Clip 保存默认结束行为,但在 `AnimationFlow` 内播放时服从节点配置。
|
||
- 结束行为只在没有后继 Edge 的终点 Clip 节点生效,解析顺序为:节点覆盖 > 播放请求覆盖 > Flow 覆盖 > Clip 默认行为。
|
||
- 中间节点存在后继 Edge 且没有显式节点结束行为时,Clip 播放一次后沿 Edge 推进,Clip 默认的 Loop / Hold 等行为不阻止流程。
|
||
- 导入生成的 Clip 帧表只读,不允许直接手动改帧。
|
||
- 如需修改导入 Clip 的帧内容,应修改源 PNG / JSON 后刷新,或使用编辑器命令复制为 Manual Clip。
|
||
- 空 Sprite 帧合法,不作为错误处理。
|
||
- 帧列表为空的 Clip 可以作为编辑中的中间状态保存,但完整校验结果为 Error,不能正式预览或播放;运行时 `Play()` 应立即以 `Failed` 返回。
|
||
- `Stop` 是播放器控制操作,不属于 `FrameClipEndBehavior`;`HoldLastFrame` 表示自然播完后的终点显示行为。
|
||
- Frame 里只保存运行时必要信息;导入 rect、tag 等元数据放在 `importInfo` 或 Graph 的 `ImportSource` 中。
|
||
- 导入 Clip 的 `id` 被修改后,刷新仍通过 `importSourceId + sourceTagName` 定位并更新该 Clip,不会按 Tag 名重复创建。
|
||
- `importInfo == null` 是 Manual Clip;`importInfo != null` 是 Imported Clip。第一版不再保存额外的 `isGenerated` 或来源类型枚举。
|
||
|
||
导入关联信息:
|
||
|
||
```csharp
|
||
FrameClipImportInfo
|
||
{
|
||
string importSourceId; // 指向 ImportSource.internalId
|
||
string sourceTagName;
|
||
bool isMissingFromSource;
|
||
}
|
||
```
|
||
|
||
对于 Graph 内通过 PNG / JSON 生成的 Clip,Clip 自身只保存“来自哪个 `ImportSource`、源 Tag 名称和 Missing 状态”;完整的 Texture / JSON / pivot 等来源资源由 `FrameAnimationImportSource` 保存。源帧索引已经逐帧保存在 `Frame.sourceIndex`,不再在 Clip 级重复保存列表。
|
||
|
||
复制为 Manual Clip 的行为:
|
||
|
||
- 第一版提供“复制为 Manual Clip”编辑器命令。
|
||
- 复制操作创建新的 Clip,不修改原导入 Clip,也不自动重定向现有 AnimationNode 引用。
|
||
- 新 Clip 深拷贝帧列表,并保留当前 `displayName`、`speed` 和 `defaultEndBehavior` 作为初始值;Sprite 资源仍按引用复用,不复制 Texture 或 Sprite 资产。
|
||
- 新 Clip 必须清除 ImportSource 关联、Missing 状态和导入刷新所有权,之后不再随源 JSON 刷新,并在资源浏览器中归入 Manual 分组。
|
||
- 数据上表现为新 Clip 的 `importInfo = null`。
|
||
- 新 Clip id 必须在 Clip / Flow 统一命名空间内唯一。编辑器可以生成建议 id,但创建前必须允许用户确认或修改。
|
||
- 从 Graph Editor 执行该命令时,新 Manual Clip 默认保存为当前 Graph 的 sub-asset;Graph 外创建的 Manual Clip 仍可保存为独立 `.asset`。
|
||
|
||
### 4.2 FrameAnimationGraph 数据
|
||
|
||
`FrameAnimationGraph` 是一个角色或对象的完整帧动画工作资产。它服务于编辑器管理、导入刷新和运行时可播放对象解析。
|
||
|
||
字段:
|
||
|
||
```csharp
|
||
string id;
|
||
string displayName;
|
||
|
||
List<FrameClip> clips;
|
||
List<AnimationNode> nodes;
|
||
List<AnimationEdge> edges;
|
||
List<AnimationFlow> flows;
|
||
List<FrameAnimationImportSource> importSources;
|
||
|
||
FrameAnimationGraphSettings settings;
|
||
FrameAnimationGraphEditorData editorData;
|
||
```
|
||
|
||
#### 4.2.1 Graph 标识
|
||
|
||
```csharp
|
||
string id;
|
||
string displayName;
|
||
```
|
||
|
||
规则:
|
||
|
||
- `id` 是稳定 key,用于资源查找、Addressable key 或外部工具引用。
|
||
- `displayName` 是编辑器展示名,默认与 `id` 一致。
|
||
- `id` 和 `displayName` 都允许中文。
|
||
- 修改 `displayName` 不影响引用;修改 `id` 需要强警告。
|
||
|
||
#### 4.2.2 Clip 引用
|
||
|
||
Graph 直接维护 `List<FrameClip>` 引用,不增加只表达存储状态的 `FrameClipRef` 包装层。Clip 是独立 `.asset` 还是 Graph sub-asset 由 Unity 资产关系推导,不重复序列化 `storageMode`、`isGeneratedByGraph` 等字段。
|
||
|
||
规则:
|
||
|
||
- ImportSource 生成的 Imported Clip 必须保存为所属 Graph 的 sub-asset,不能被其他 Graph 引用。
|
||
- Graph 内创建或复制的 Manual Clip 默认保存为 Graph sub-asset,也可以在创建时选择保存为独立 `.asset`。
|
||
- 只有独立 `.asset` 形式的 Manual Clip 可以被多个 Graph 共享;Graph sub-asset Clip 只能由所属 Graph 使用。
|
||
- 同一个 Graph 不能重复添加同一个 FrameClip 引用。
|
||
- 外部共享 Manual Clip 的帧表、速度和默认结束行为发生修改时,会影响所有引用它的 Graph;编辑器必须显示共享引用提示。
|
||
- 刷新后消失的 Imported Clip 在 `importInfo` 中标记 Missing,不自动删除。
|
||
- 新 Tag 生成的 Clip id 与任何已有 Clip 或 Flow id 冲突时,刷新报错并停止,不自动添加前缀或改名。
|
||
- 同一 ImportSource 内、不同 ImportSource 之间的 Aseprite Tag 名均不得重复;冲突信息必须指出双方来源和 Tag。
|
||
|
||
移除与删除规则:
|
||
|
||
- 从 Graph 移除 Clip 前,必须检查 AnimationNode 引用和 `defaultPlayableId`;仍存在 Graph 内引用时阻止移除,并提供定位。
|
||
- 移除外部 Manual Clip 时只移除 Graph 引用,不删除 `.asset`。
|
||
- 移除 Graph sub-asset Manual Clip 或 Imported Clip 时同时删除该 sub-asset,并要求明确确认。
|
||
- Missing Imported Clip 只能由用户手动删除;普通刷新永不自动删除 Clip 或 Sprite。
|
||
- 第一版不提供 sub-asset 原地提取为外部资产。需要转换存储方式时,使用“复制为外部 Manual Clip”,不自动替换现有节点引用。
|
||
|
||
#### 4.2.3 全局节点图
|
||
|
||
```csharp
|
||
List<AnimationNode> nodes;
|
||
List<AnimationEdge> edges;
|
||
```
|
||
|
||
规则:
|
||
|
||
- `FrameAnimationGraph` 持有一套全局节点和连线。
|
||
- 节点和连线不归某个 Flow 独占。
|
||
- 一个 Graph 内允许有多个不联通子图。
|
||
- 不同 Flow 可以共享节点,例如多个 Flow 最终进入同一个 Idle 节点。
|
||
- 节点图用于编辑器画布展示,也用于运行时沿边推进播放流程。
|
||
|
||
#### 4.2.4 AnimationFlow 列表
|
||
|
||
```csharp
|
||
List<AnimationFlow> flows;
|
||
```
|
||
|
||
`AnimationFlow` 是全局节点图里的命名流程。它不直接拥有 nodes / edges,只记录入口和默认播放策略;包含的节点由入口可达关系推导。
|
||
|
||
规则:
|
||
|
||
- 第一版采用“单一全局节点图 + 多个 AnimationFlow”的模型。
|
||
- Flow 内嵌在 `FrameAnimationGraph` 中,不作为第一版独立资产。
|
||
- Flow 强依赖本 Graph 内的 Clip、Node、Edge 和命名语义,内嵌更便于校验、预览和刷新。
|
||
|
||
#### 4.2.5 可播放对象命名空间
|
||
|
||
FrameClip 与 AnimationFlow 是并列的一等可播放对象。第一版不引入 AnimationEntry 抽象,也不为 Clip 或 Flow 自动生成额外入口对象。
|
||
|
||
```csharp
|
||
FrameAnimationPlayableIndex
|
||
{
|
||
Dictionary<string, FrameAnimationPlayableRef> playables;
|
||
}
|
||
|
||
FrameAnimationPlayableRef
|
||
{
|
||
FrameAnimationPlayableType type; // Clip / Flow,仅供 Graph 内部解析
|
||
FrameClip clip;
|
||
AnimationFlow flow;
|
||
}
|
||
```
|
||
|
||
该索引是可以从 clips / flows 重建的运行时缓存,不作为新的序列化资产层。目标类型只由 Graph 内部解析使用,不要求正式调用方提供。
|
||
|
||
规则:
|
||
|
||
- Clip id 和 Flow id 共享同一个 Graph 级可播放命名空间,彼此不能重名。
|
||
- `displayName` 仅用于展示,可以重复。
|
||
- 正式调用统一使用 `Play(playableId)`,调用方不显式区分 Clip 或 Flow。
|
||
- Graph 根据唯一 id 将调用解析为 FrameClip 或 AnimationFlow。
|
||
- AnimationNode 不进入可播放命名空间,只能由 Flow 内部执行或由编辑器调试预览。
|
||
- 单 Clip 动画可以直接播放,不要求包装成单节点 Flow。
|
||
- 将一个 Clip 升级为同名 Flow 时,需要先移除或重命名旧 Clip;外部 playable id 可以保持不变。
|
||
|
||
#### 4.2.6 ImportSource 列表
|
||
|
||
```csharp
|
||
List<FrameAnimationImportSource> importSources;
|
||
```
|
||
|
||
Graph 内的 PNG / JSON 来源都保存在 `importSources` 中。Clip 不重复保存完整 Texture / JSON 引用,只通过 `FrameClipImportInfo.importSourceId` 指向来源。
|
||
|
||
```csharp
|
||
FrameAnimationImportSource
|
||
{
|
||
string internalId;
|
||
string displayName;
|
||
bool isEnabled;
|
||
Texture2D texture;
|
||
TextAsset asepriteJson;
|
||
Vector2 pivot;
|
||
bool manageSpriteSlicing;
|
||
FrameClipEndBehavior defaultNewClipEndBehavior;
|
||
string lastSourceHash;
|
||
}
|
||
```
|
||
|
||
#### 4.2.7 Settings
|
||
|
||
```csharp
|
||
FrameAnimationGraphSettings
|
||
{
|
||
string defaultPlayableId;
|
||
FrameClipEndBehavior newManualClipDefaultEndBehavior;
|
||
}
|
||
```
|
||
|
||
规则:
|
||
|
||
- Graph 需要 `defaultPlayableId`,用于 Player 自动播放、编辑器默认预览和未指定目标时的默认播放项;它可以指向 Clip 或 Flow。
|
||
- Graph 不提供运行时结束行为兜底;`newManualClipDefaultEndBehavior` 只作为编辑器中新建 Manual Clip 的初始值。
|
||
|
||
#### 4.2.8 EditorData
|
||
|
||
编辑器布局、节点位置、折叠状态、预览偏好等不混入运行时播放数据,必须单独保存。
|
||
|
||
```csharp
|
||
FrameAnimationGraphEditorData
|
||
{
|
||
List<AnimationNodeEditorData> nodeEditorData;
|
||
List<AnimationFlowEditorData> flowEditorData;
|
||
}
|
||
```
|
||
|
||
其中,节点位置、Flow 分组或颜色等需要团队共享的编辑数据随 Graph 保存;面板宽度、画布缩放、上次选中对象、搜索词和预览缩放等个人工作区状态保存在本机 EditorPrefs / SessionState,不进入版本控制资产。
|
||
|
||
#### 4.2.9 命名唯一性
|
||
|
||
规则:
|
||
|
||
- Clip id 与 AnimationFlow id 共享可播放命名空间,在同一个 Graph 内必须全局唯一且彼此不能重名。
|
||
- AnimationNode internalId 在 Node 命名空间内必须唯一。
|
||
- AnimationEdge internalId 在 Edge 命名空间内必须唯一。
|
||
- ImportSource internalId 在本 Graph 内必须唯一。
|
||
- Node / Edge / ImportSource 的 internalId 使用创建后不可修改的 GUID,只用于序列化引用和编辑器定位,不要求与 playable id 互斥。
|
||
- Clip / Flow 的 `displayName` 可以重复。
|
||
- Clip id 和 Flow id 都可能被外部系统依赖,修改时必须显示强警告。
|
||
|
||
### 4.3 AnimationFlow 与节点图数据
|
||
|
||
`AnimationFlow` 本身不是独立节点图,而是 `FrameAnimationGraph` 全局节点图中的命名流程定义。本节同时描述全局节点、连线和 Flow 的数据结构。
|
||
|
||
#### 4.3.1 AnimationFlow 数据
|
||
|
||
字段:
|
||
|
||
```csharp
|
||
string id;
|
||
string displayName;
|
||
string entryNodeId;
|
||
AnimationFlowAsyncCompletionMode asyncCompletionMode;
|
||
FrameClipEndBehavior? endBehaviorOverride;
|
||
```
|
||
|
||
说明:
|
||
|
||
- `entryNodeId` 保存入口 AnimationNode 的 `internalId`。
|
||
- `asyncCompletionMode` 只控制终点为 Loop 时的 `PlayAsync` 等待边界:进入终点并显示首帧,或等待终点首轮回绕;零值默认采用前者。
|
||
- `endBehaviorOverride` 是 Flow 到达终点时的可选结束行为覆盖。
|
||
- Flow 包含的节点集合从 `entryNodeId` 沿全局边关系遍历得到,不单独保存 `includedNodeIds`。
|
||
- 编辑器高亮、Flow 聚焦、校验和运行时使用同一套可达关系,避免人工维护的节点集合与实际连线不一致。
|
||
- 多个 Flow 的可达范围可以重叠,因此可以自然共享 Idle 等公共节点。
|
||
- 同一个 AnimationNode 不能作为多个 Flow 的入口,但不同 Flow 可以在后续路径中共享该节点。
|
||
- Flow id 属于统一可播放命名空间,不能与任何 Clip 或其他 Flow id 重名。
|
||
- 终点为非 Loop 时忽略 `asyncCompletionMode`,异步播放始终等待 Flow 自然结束。
|
||
|
||
#### 4.3.2 AnimationNode 数据
|
||
|
||
字段:
|
||
|
||
```csharp
|
||
string internalId;
|
||
string displayName;
|
||
AnimationNodeType type;
|
||
string clipId;
|
||
FrameClipEndBehavior? endBehaviorOverride;
|
||
float? speedOverride;
|
||
```
|
||
|
||
第一版节点类型:
|
||
|
||
- `Clip`:播放一个 FrameClip。
|
||
|
||
规则:
|
||
|
||
- 第一版只实现 `Clip` 节点,不实现独立 End 节点。
|
||
- 没有后继 Edge 的 Clip 节点就是 Flow 终点,不需要用 End 节点重复表达结束。
|
||
- `Clip` 节点通过 `clipId` 引用 Graph 内的 FrameClip。
|
||
- `endBehaviorOverride` 用于覆盖 Clip 默认结束行为。
|
||
- `speedOverride` 用于覆盖 Clip 默认播放速度;为空时使用 Clip 自身速度。
|
||
- Flow 节点的实际推进速度为 `Player.speed * (Node.speedOverride ?? Clip.speed)`;直接播放 Clip 时为 `Player.speed * Clip.speed`。
|
||
- `speedOverride` 不允许小于 0;等于 0 时只停止时间推进,不改变播放状态。
|
||
- 普通 Idle 循环通过 `Clip` 节点的 `endBehaviorOverride = Loop` 表达,不需要专门的 IdleLoop 节点。
|
||
- 节点存在后继 Edge 且没有显式 `endBehaviorOverride` 时,Clip 播放一次后推进;Clip 默认结束行为只在终点生效。
|
||
- 节点显式设置任何 `endBehaviorOverride` 都表示它应当成为终点;同时存在后继 Edge 属于配置错误。
|
||
|
||
后续可扩展节点类型:
|
||
|
||
- `Wait`
|
||
- `Random`
|
||
- `Branch`
|
||
- `Event`
|
||
- `SetParameter`
|
||
- `Jump`
|
||
|
||
#### 4.3.3 AnimationEdge 数据
|
||
|
||
字段:
|
||
|
||
```csharp
|
||
string internalId;
|
||
string fromNodeId; // 指向 AnimationNode.internalId
|
||
string toNodeId; // 指向 AnimationNode.internalId
|
||
string exitName; // default / success / cancel 等,第一版可仅支持 default
|
||
AnimationEdgeCondition condition;
|
||
```
|
||
|
||
第一版规则:
|
||
|
||
- Edge 只支持顺序连接。
|
||
- `condition` 预留,第一版只实现 `Always`。
|
||
- 普通 Clip 循环不通过自环表达,而通过节点 `endBehaviorOverride = Loop` 表达。
|
||
- 第一版禁止自连接和由多个 Edge 构成的环路。
|
||
|
||
规则:
|
||
|
||
- 第一版 Edge 只做 `Always` 顺序连接。
|
||
- 第一版每个节点最多一个后继 Edge,因此从 Flow 入口出发的执行路径是确定的。
|
||
- 第一版检测到自连接或多节点 Edge 环路时作为 Error;合法的无限播放只由终点 Clip 节点显式 `Loop` 表达。
|
||
- 后续如需要多 Clip 循环,应增加明确的 Flow 级循环、循环次数或 `LoopBack` 语义,不通过任意连线隐式形成环。
|
||
- 条件分支、随机分支、事件边等只作为后续扩展。
|
||
- Flow 节点范围只通过 `entryNodeId` 和 Edge 可达关系推导,不保存独立成员列表。
|
||
- 第一版不实现帧事件;后续如需要,优先考虑放在 `AnimationNode` 上,而不是放在 `FrameClip` 上。
|
||
|
||
### 4.4 ImportSource 数据
|
||
|
||
`FrameAnimationImportSource` 是 `FrameAnimationGraph` 内的一组 Aseprite 外部素材来源。第一版只支持“一张 Texture + 一个 Aseprite JSON”作为导入来源,不做通用导入源抽象。
|
||
|
||
字段:
|
||
|
||
```csharp
|
||
FrameAnimationImportSource
|
||
{
|
||
string internalId;
|
||
string displayName;
|
||
bool isEnabled;
|
||
|
||
Texture2D texture;
|
||
TextAsset asepriteJson;
|
||
|
||
Vector2 pivot;
|
||
bool manageSpriteSlicing;
|
||
FrameClipEndBehavior defaultNewClipEndBehavior;
|
||
string lastSourceHash;
|
||
}
|
||
```
|
||
|
||
#### 4.4.1 导入来源范围
|
||
|
||
规则:
|
||
|
||
- 第一版只支持 Aseprite JSON + Texture 导入。
|
||
- 不需要 `ImportSourceType`。
|
||
- `internalId` 在创建时生成 GUID,之后不可修改;`displayName` 可自由修改,不影响 Clip 关联。
|
||
- 手动 Clip 不属于 ImportSource;手动 Clip 没有外部刷新来源。
|
||
- 一个 `FrameAnimationGraph` 可以包含多个 ImportSource。
|
||
- 每个 ImportSource 对应一对图片和 JSON。
|
||
- 每个 ImportSource 可以生成多个 Clip。
|
||
|
||
示例:
|
||
|
||
```text
|
||
Peipei_FrameAnimationGraph
|
||
├─ body.png + body.json -> Idle / Turn / WakeUp
|
||
├─ face.png + face.json -> Blink / Smile / Shock
|
||
└─ special.png + special.json -> Glitch / Break
|
||
```
|
||
|
||
#### 4.4.2 切图设置
|
||
|
||
```csharp
|
||
Vector2 pivot;
|
||
bool manageSpriteSlicing;
|
||
```
|
||
|
||
规则:
|
||
|
||
- `pivot` 用于控制根据 Aseprite JSON 切出的 Sprite pivot。
|
||
- `manageSpriteSlicing = false` 时,系统绝不修改 TextureImporter,只查找并使用 Texture 中已有的 Sprite。
|
||
- `manageSpriteSlicing = true` 时,系统可以根据 Aseprite JSON 创建或更新 Sprite 切图元数据。
|
||
- 自动切图会修改 TextureImporter,编辑器必须先显示差异并要求确认。TextureImporter 更新失败时,不修改 Graph 或 Clip 数据。
|
||
- `defaultNewClipEndBehavior` 只用于首次创建导入 Clip,默认值为 `HoldLastFrame`。
|
||
- Aseprite Tag 的 `direction` 只决定帧顺序,不表达 Clip 是否循环。
|
||
|
||
#### 4.4.3 刷新策略
|
||
|
||
规则:
|
||
|
||
- 第一版不序列化 `ImportRefreshPolicy`;新增 Tag、Missing 标记和 Imported Clip 帧表覆盖均为固定刷新规则。
|
||
- 新增 Aseprite Tag 自动生成新 Clip。
|
||
- 源中消失的 Tag 对应 Clip 标记 `importInfo.isMissingFromSource = true`,不自动删除。
|
||
- 导入生成的 Clip 帧表只读,刷新时可以覆盖生成帧。
|
||
- 手动 Clip 不受 ImportSource 刷新影响。
|
||
- 刷新只覆盖导入器拥有的字段,不覆盖用户已经设置的 `displayName`、`speed` 和 `defaultEndBehavior`。
|
||
|
||
#### 4.4.4 导入状态
|
||
|
||
规则:
|
||
|
||
- 第一版保存 `lastSourceHash`。
|
||
- `lastSourceHash` 用于判断来源是否变化和显示刷新差异。
|
||
- hash 的具体计算方式留到实现阶段决定,可以基于 JSON 文本、Texture asset guid、TextureImporter 状态等信息。
|
||
- 不保存 `lastImportedAt` 和 `lastImportedClipIds`。当前来源关联的 Clip 通过 `clip.importInfo.importSourceId == source.internalId` 推导,避免 Clip 改名后产生过期缓存。
|
||
|
||
#### 4.4.5 Clip 关联方式
|
||
|
||
Graph 内导入生成的 Clip 通过 `FrameClipImportInfo` 关联 ImportSource:
|
||
|
||
```csharp
|
||
FrameClipImportInfo
|
||
{
|
||
string importSourceId; // 指向 ImportSource.internalId
|
||
string sourceTagName;
|
||
bool isMissingFromSource;
|
||
}
|
||
```
|
||
|
||
规则:
|
||
|
||
- `importSourceId` 指向 `FrameAnimationGraph.importSources` 中不可变的 `internalId`。
|
||
- Aseprite `frameTags.name` 是第一版稳定匹配键。
|
||
- 从 Aseprite Tag 导入时,默认 `Clip.id = tagName`,`Clip.displayName = tagName`。
|
||
- 刷新时按 `importSourceId + sourceTagName` 匹配源 Tag 和已有 Clip,不依赖当前 `Clip.id`。
|
||
- Tag 存在:原地更新对应 Clip。
|
||
- Tag 新增:创建新 Clip。
|
||
- Tag 消失:标记对应 Clip missing,不删除。
|
||
- missing Tag 重新出现:原地更新原 Clip 并清除 missing 标记。
|
||
- Tag 改名:视为旧 Clip missing + 新 Clip 新增。
|
||
- Graph 内所有启用 ImportSource 参与导入的 Tag 名必须唯一。
|
||
- Tag 或新 Clip id 与现有 Clip / Flow playable id 发生冲突时视为阻断错误,不自动生成前缀或改名。
|
||
- 第一版不提供 Tag 改名后的手动关联或迁移工具;需要保留原 Clip 和内部引用时,应在 Aseprite 中保持原 Tag 名。
|
||
|
||
#### 4.4.6 Sprite 与 TextureImporter 所有权
|
||
|
||
规则:
|
||
|
||
- Graph 只拥有动画数据,不拥有 Sprite 或 Texture。自动切出的 Sprite 始终是 Texture 资产的 sub-asset。
|
||
- Frame 只保存 Sprite 的 Unity 序列化引用;删除 Graph、Clip 或 ImportSource 都不自动删除 Texture 或 Sprite。
|
||
- 复制为 Manual Clip 只复制帧表,不复制 Texture 或 Sprite。需要完全独立的图像资源时,由用户显式复制 Texture。
|
||
- `manageSpriteSlicing = true` 的 ImportSource 是该 Texture 切图元数据的写入所有者。同一 Texture 在整个项目中最多只能有一个可写 ImportSource。
|
||
- 其他 ImportSource 可以在 `manageSpriteSlicing = false` 时只读复用同一 Texture 中已有的 Sprite。
|
||
- 修改 TextureImporter 前检查本 Graph 和其他 FrameAnimationGraph 的可写来源;存在多个写入所有者时阻止操作并列出冲突来源。
|
||
- 自动更新切图时,以 Aseprite `frameName` 作为 Sprite 的稳定匹配键。已存在同名 Sprite 时保留稳定 Sprite ID,只更新 rect、pivot 等元数据;新 frameName 创建新 Sprite ID。
|
||
- SourceFrame 的 frameName 必须非空且在同一 ImportSource 内唯一,否则导入失败。
|
||
- 源中消失的 frameName 第一版不自动删除对应 Sprite 元数据,避免 Missing Clip 或 Manual Clip 的 Sprite 引用立即断裂。清理未使用 Sprite 不属于普通刷新流程。
|
||
- `manageSpriteSlicing = false` 时,找不到与 JSON frameName / rect 对应的唯一 Sprite 应视为阻断错误,不自动回退到修改 TextureImporter。
|
||
|
||
## 5. 资产导入与刷新需求
|
||
|
||
### 5.1 Aseprite JSON 导入
|
||
|
||
系统读取 Aseprite 导出的 JSON 后,先建立按源文件导出顺序排列的 SourceFrame 列表,再根据 `meta.frameTags` 生成或刷新 FrameClip。
|
||
|
||
#### 5.1.1 支持的 JSON 数据
|
||
|
||
- 支持 `frames` 为 JSON Object 或 JSON Array 两种导出形式。
|
||
- 两种形式都必须转换为统一的有序 SourceFrame 列表。
|
||
- JSON Object 形式必须保留属性在源文件中的出现顺序,不允许根据 frameName 重新排序。
|
||
- 读取每帧的 `frame` rect、`duration`、`rotated`、`trimmed`、`spriteSourceSize` 和 `sourceSize`。
|
||
- 读取 `meta.size`、`meta.image` 和 `meta.frameTags`。
|
||
- 完整保留 UTF-8 中文 Tag 和 frameName。
|
||
|
||
#### 5.1.2 Frame 转换规则
|
||
|
||
- Aseprite 原始 frameName 写入 `Frame.frameName`,不参与 SourceFrame 排序、Clip 匹配或节点引用;当系统管理 Sprite 切图时,它作为保留 Sprite ID 的稳定匹配键。
|
||
- 源帧在有序 SourceFrame 列表中的位置写入 `Frame.sourceIndex`。
|
||
- Aseprite `duration` 原样转换为 `int durationMs`,不先换算为浮点秒。
|
||
- FrameClip 中的 Sprite 根据 SourceFrame rect 对应到切图结果。
|
||
- `spriteSourceSize` 和 `sourceSize` 第一版只用于校验和错误提示,不进入运行时 Frame 数据。
|
||
|
||
#### 5.1.3 Tag 转换规则
|
||
|
||
- 每个 `frameTags` 条目生成或刷新一个 FrameClip。
|
||
- Tag 的 `from` / `to` 引用 SourceFrame 索引。
|
||
- Tag 范围允许重叠;同一个 SourceFrame 可以被多个 Clip 使用。
|
||
- `forward` 展开为 `from -> to`。
|
||
- `reverse` 展开为 `to -> from`。
|
||
- `pingpong` 展开为正向帧后接反向内部帧,不重复首尾端点。例如 `0,1,2,3,2,1`。
|
||
- `pingpong_reverse` 按相反起始方向使用同样规则展开。
|
||
- 展开结果直接保存为普通 FrameClip 帧表,运行时不需要理解 Aseprite direction;`sourceIndex` 允许重复。
|
||
- direction 只决定帧排列顺序,不隐含 `Loop` 结束行为。
|
||
|
||
#### 5.1.4 第一版素材限制
|
||
|
||
- 第一版要求 Aseprite 导出时关闭裁边和旋转。
|
||
- 任一帧 `trimmed = true` 时作为阻断错误,不导入。后续如支持,需要根据 `spriteSourceSize` 修正每帧相对原始画布的位置和 pivot。
|
||
- 任一帧 `rotated = true` 时作为阻断错误,不导入。
|
||
|
||
#### 5.1.5 导入校验
|
||
|
||
以下情况属于阻断错误:
|
||
|
||
- `frames` 缺失或为空。
|
||
- SourceFrame 的 frameName 为空或在同一 ImportSource 内重复。
|
||
- Texture 尺寸与 `meta.size` 不一致。
|
||
- Frame rect 越出 Texture 范围。
|
||
- `duration` 不是大于 0 的整数毫秒值。
|
||
- Tag 名为空或发生重名。
|
||
- Tag 的 `from` / `to` 越界或范围无效。
|
||
- direction 不是系统明确支持的值。
|
||
- 不同 ImportSource 的 Tag 名冲突,或新 Tag 生成的 Clip id 与已有 Clip / Flow playable id 冲突。
|
||
- 当前 Texture 存在其他 `manageSpriteSlicing = true` 的 ImportSource 写入所有权。
|
||
- `manageSpriteSlicing = false` 时,已有 Sprite 无法按 frameName / rect 唯一匹配 SourceFrame。
|
||
|
||
以下情况只显示警告:
|
||
|
||
- JSON 中没有任何 Tag,此 ImportSource 不生成 Clip。
|
||
- `meta.image` 与当前绑定 Texture 的文件名不同;Unity 内重命名资源可能造成这种情况。
|
||
|
||
导入必须先完成解析和全部校验,再修改 Graph。存在阻断错误时,本次 ImportSource 不产生任何部分更新。
|
||
|
||
### 5.2 刷新策略
|
||
|
||
刷新时应尽量保持已有资产引用稳定。
|
||
|
||
#### 5.2.1 刷新流程
|
||
|
||
```text
|
||
读取来源
|
||
-> 完整解析
|
||
-> 校验来源与全 Graph 命名冲突
|
||
-> 计算差异
|
||
-> 应用修改
|
||
-> 更新 hash 和导入记录
|
||
```
|
||
|
||
- 第一版采用手动刷新。编辑器可以通过 hash 显示“来源已变化”,但文件变化不自动修改 Graph。
|
||
- 刷新单个 ImportSource 时,该来源存在阻断错误则该来源完全不变。
|
||
- 刷新全部 ImportSource 时,必须先校验所有启用来源;任一来源存在阻断错误时,整次刷新不应用。
|
||
- 应用修改前计算 `新增 / 更新 / Missing / 不变 / 错误` 差异摘要。
|
||
- 普通刷新不要求二次确认;删除 Clip、修改 TextureImporter 等影响更大的操作需要明确确认。
|
||
- 需要修改 TextureImporter 时,先计算并确认切图差异,再更新 TextureImporter;只有 TextureImporter 更新成功后才应用 Graph / Clip 变化。
|
||
|
||
#### 5.2.2 匹配与资产稳定性
|
||
|
||
- 使用 `importSourceId + sourceTagName` 匹配导入来源与已有 Clip。
|
||
- 已有 Clip 必须原地更新,不删除并重新创建 Clip 子资产。
|
||
- 用户修改导入 Clip 的 `id` 后,刷新仍更新原 Clip。
|
||
- 新 Tag 创建新 Clip;消失的 Tag 标记 missing;重新出现时清除 missing。
|
||
- Tag 改名视为旧 Clip missing + 新 Clip 新增。
|
||
- 刷新不自动删除任何 Clip,也不自动解决命名冲突。
|
||
|
||
#### 5.2.3 字段所有权
|
||
|
||
导入器拥有并可在刷新时覆盖:
|
||
|
||
- `frames`
|
||
- `importInfo.importSourceId`
|
||
- `importInfo.sourceTagName`
|
||
- `importInfo.isMissingFromSource`
|
||
|
||
用户拥有,刷新不得覆盖:
|
||
|
||
- `id`
|
||
- `displayName`
|
||
- `speed`
|
||
- `defaultEndBehavior`
|
||
|
||
新 Clip 首次创建时,`id` 和 `displayName` 默认取 Tag 名,`defaultEndBehavior` 取 ImportSource 的 `defaultNewClipEndBehavior`。这些字段创建后归用户控制。
|
||
|
||
#### 5.2.4 Clip / Flow 重命名
|
||
|
||
- Clip 和 Flow id 可以重命名,但新 id 必须在统一可播放命名空间内唯一。
|
||
- Clip id 只能通过 Graph Editor 的正式重命名流程修改;普通 Inspector 中只读。
|
||
- Clip 只被一个 Graph 引用时允许重命名,编辑器必须显示受影响的 AnimationNode 引用数量,并原子更新该 Graph 内相关引用。
|
||
- 外部 Manual Clip 被多个 Graph 引用时禁止重命名;编辑器列出引用它的 Graph,并提示先移除其他引用或复制为新的 Manual Clip。
|
||
- Flow 重命名时,编辑器必须同步更新 `defaultPlayableId` 等 Graph 内引用。
|
||
- Clip / Flow id 都是正式播放契约;修改时必须强警告它可能破坏外部字符串调用。
|
||
- 编辑器只能自动修复 Graph 内部引用,不能假设可以安全修改项目中的所有外部字符串。
|
||
|
||
## 6. 编辑器需求
|
||
|
||
### 6.1 编辑器形态与总体布局
|
||
|
||
第一版提供独立的 `FrameAnimationGraph EditorWindow` 作为主要工作环境。普通 Unity Inspector 只显示 Graph / Clip 的摘要信息和“打开 Graph Editor”入口,不承担完整编排工作。
|
||
|
||
编辑器沿用项目现有章节编辑器的基本操作心智:顶部工具栏、可调整宽度的侧栏、中央 GraphView 节点画布、选中对象属性面板。动画编辑器在此基础上增加资源浏览、节点内预览、Clip 独立预览、导入差异和校验结果区域。
|
||
|
||
默认布局:
|
||
|
||
```text
|
||
顶部工具栏
|
||
├─ Graph 资产 / 保存状态
|
||
├─ 刷新来源 / 全部刷新
|
||
├─ 校验
|
||
└─ Flow 预览与节点预览策略
|
||
|
||
主工作区
|
||
├─ 左侧:资源浏览区
|
||
├─ 中央:全局节点画布
|
||
└─ 右侧:选中对象属性区;选中 Clip 时包含独立预览
|
||
|
||
底部可折叠区域
|
||
├─ 导入差异
|
||
└─ 校验结果
|
||
```
|
||
|
||
布局要求:
|
||
|
||
- 左右侧栏和底部区域支持拖拽调整尺寸,并允许折叠。
|
||
- 中央画布始终对应当前 FrameAnimationGraph 的唯一全局节点图,不为每个 AnimationFlow 创建独立画布。
|
||
- 编辑器同一时间编辑一个 FrameAnimationGraph;第一版不要求多个 Graph 同屏。
|
||
- 选中资源列表项、节点、连线或校验结果时,各区域必须同步定位和显示同一对象。
|
||
- 编辑器重新打开时恢复用户上次使用的布局和本机工作区状态。
|
||
|
||
### 6.2 资源浏览区
|
||
|
||
资源浏览区集中管理当前 Graph 内的:
|
||
|
||
- FrameClip
|
||
- AnimationFlow
|
||
- FrameAnimationImportSource
|
||
|
||
基础能力:
|
||
|
||
- 使用 `Clips / Flows / Sources` 三个标签页切换不同资源类型。
|
||
- 支持按 `id`、`displayName`、`sourceTagName` 和 ImportSource 搜索 Clip。
|
||
- 支持按名称、来源、帧数、时长和 missing 状态排序。
|
||
- 支持按 Manual / Imported、正常 / Missing、是否已被节点引用筛选。
|
||
- 列表项使用纯文本紧凑显示,不绘制 Sprite 缩略图。
|
||
- 列表项显示名称、帧数、总时长、来源状态、节点引用数量和必要的错误 / 警告标记。
|
||
- 双击或使用明确命令可定位到相关节点、目标资源或 ImportSource。
|
||
- Clip 默认按 ImportSource 分组,Manual Clip 放在独立分组;同时提供扁平列表模式。
|
||
- 搜索时可以忽略分组,直接显示全部匹配结果。
|
||
- 单击 Clip 只选中资源并在右侧显示属性和独立预览,不自动定位某个节点,因为同一 Clip 可能被多个节点引用。
|
||
- 支持“定位引用”:只有一个引用时直接定位节点,存在多个引用时显示引用列表。
|
||
- 支持将 Clip 拖到画布,在释放位置创建引用该 Clip 的节点。
|
||
|
||
第一版不要求 Clip 拖拽排序;搜索、筛选和稳定排序优先。资源浏览区本身不播放动画,也不执行持续的 Sprite 重绘。
|
||
|
||
### 6.3 全局节点画布与 AnimationFlow
|
||
|
||
节点画布用于编辑 Graph 持有的全局 AnimationNode / AnimationEdge。第一版沿用 Unity GraphView 风格的缩放、平移、框选、拖动和连线操作。
|
||
|
||
#### 6.3.1 节点创建与连接
|
||
|
||
- 第一版只创建 `Clip` 节点。
|
||
- Clip 可以从资源浏览区拖入画布生成 Clip 节点,也可以通过画布右键菜单创建后选择 Clip。
|
||
- 同一个 FrameClip 可以被多个 AnimationNode 引用。
|
||
- 第一版 Clip 节点最多提供一个默认顺序出口;没有后继 Edge 的节点就是 Flow 终点。
|
||
- 连接节点时立即更新 Graph 的 AnimationEdge 数据。
|
||
- 节点显式设置结束行为后不允许再连接后继 Edge;已有后继 Edge 时设置结束行为也必须提示冲突。
|
||
- 删除节点或连线前检查 Flow 和其他节点引用;存在引用时显示影响范围并要求确认。
|
||
- 节点移动、创建、删除、连接和断开都必须支持 Unity Undo / Redo。
|
||
|
||
Clip 节点至少显示:
|
||
|
||
- 节点 displayName。
|
||
- 引用的 Clip id。
|
||
- 默认或覆盖后的结束行为和速度。
|
||
- Missing Clip、无效引用等状态标记。
|
||
- 被哪些 AnimationFlow 使用的简要标记。
|
||
- 位于节点内容下方的固定尺寸预览区。
|
||
|
||
节点预览要求:
|
||
|
||
- 预览区具有稳定的宽高比和尺寸,不因不同帧的 Sprite 尺寸变化而改变节点布局。
|
||
- Sprite 使用适应区域的方式显示;静止状态默认显示第一个非空帧。
|
||
- 空帧显示透明棋盘格并保留实际时长。
|
||
- 节点预览应用该 AnimationNode 的 `speedOverride` 和 `endBehaviorOverride`。
|
||
|
||
#### 6.3.2 Flow 查看与编辑
|
||
|
||
- 画布提供“显示全部”和“聚焦某个 AnimationFlow”两种查看模式。
|
||
- 显示全部时展示 Graph 的所有节点和连线,包括互不联通的子图。
|
||
- 选择某个 Flow 时,从 `entryNodeId` 沿 Edge 实时计算可达节点和连线并高亮,降低其他节点的视觉权重,但不切换或复制节点图。
|
||
- 编辑器必须明确标识 Flow 的入口节点。
|
||
- 节点允许同时属于多个 Flow,以支持共享 Idle 等公共节点。
|
||
- Flow 不保存或人工维护节点成员列表;修改连线后,其可达范围和高亮结果立即重新计算。
|
||
- 提供将选中节点设置为 Flow 入口的命令。
|
||
- 两个 Flow 从不同入口到达同一节点时,该节点自然属于两个 Flow,不需要额外登记共享关系。
|
||
|
||
Flow 创建与入口管理:
|
||
|
||
- Flow 只能通过“选中一个 Clip 节点 -> 从选中节点创建 Flow”建立。
|
||
- 创建时必须且只能选中一个节点,编辑器自动将该节点写入 `entryNodeId`。
|
||
- Flow id 默认取节点名称,但创建前允许修改;它必须与所有 Clip / Flow playable id 唯一。
|
||
- Flow 属性区提供 `Async Completion` 配置,并说明它只影响终点为 Loop 的异步播放。
|
||
- 同一个节点不能作为多个 Flow 的入口;节点已是入口时禁用创建命令并提供定位现有 Flow 的操作。
|
||
- Flow 入口允许存在前驱节点;从该 Flow 播放时直接从入口开始,入口之前的节点不属于其可达范围。
|
||
- 修改入口时,先选中目标节点并执行“设置为当前 Flow 入口”;若目标已是其他 Flow 入口则阻止修改。
|
||
- 删除 Flow 只删除 Flow 定义,不删除节点或 Edge。
|
||
- 删除 Flow 入口节点时,不允许静默留下失效 Flow;编辑器只提供“同时删除 Flow”或“取消”。如需保留 Flow,用户必须先修改入口。
|
||
|
||
第一版连线约束:
|
||
|
||
- 禁止节点自连接和多个节点组成的 Edge 环路。
|
||
- 终点 Clip 节点显式 `Loop` 是第一版唯一合法的无限 Flow。
|
||
- 创建 Edge 时立即执行针对该连接的循环检查;发现环路时标记 Error,并阻止完整 Flow 预览。
|
||
|
||
#### 6.3.3 画布辅助能力
|
||
|
||
- 支持聚焦选中对象、聚焦当前 Flow 和显示全部节点。
|
||
- 支持对选中节点或当前 Flow 做基础自动布局;自动布局必须进入 Undo。
|
||
- 节点位置随 Graph 的共享 EditorData 保存。
|
||
- 第一版允许保存暂时不完整或不联通的图,不能因为编辑中间态而阻止保存;错误通过校验面板持续提示。
|
||
|
||
### 6.4 属性编辑与独立预览
|
||
|
||
右侧属性区根据当前选中对象显示 FrameClip、AnimationNode、AnimationEdge、AnimationFlow 或 ImportSource 的编辑界面。属性修改必须使用 SerializedObject / Undo,并立即同步到资源列表、节点和预览。
|
||
|
||
#### 6.4.1 FrameClip 编辑
|
||
|
||
通用信息:
|
||
|
||
- 显示并编辑 `displayName`、`speed` 和 `defaultEndBehavior`;Clip id 只通过带引用检查的正式重命名命令修改。
|
||
- 显示总时长、Frame 数量、来源类型和 Missing 状态。
|
||
- 显示帧表:序号、Sprite、`durationMs`、`frameName`、`sourceIndex`。
|
||
- 重命名 id 时检查统一 playable 命名空间冲突,显示将同步修改的 Node 引用数量,并提示无法自动修复的外部字符串引用。
|
||
- 外部 Manual Clip 被多个 Graph 共享时,显示引用 Graph 列表和共享修改提示,并禁用 id 重命名。
|
||
|
||
Manual Clip:
|
||
|
||
- 帧表可编辑。
|
||
- 支持增加、删除、复制和调整帧顺序。
|
||
- 支持替换 Sprite、修改 `durationMs` 和创建合法空帧。
|
||
- 支持将导入 Clip 复制为新的 Manual Clip;复制后帧表可编辑,且不再参与 ImportSource 刷新。
|
||
|
||
Imported Clip:
|
||
|
||
- 帧表只读,不允许局部覆盖。
|
||
- 支持跳转到对应 ImportSource、查看 sourceTagName 和刷新来源。
|
||
- 如需手动修改帧内容,使用“复制为 Manual Clip”命令创建脱离导入刷新的副本。
|
||
|
||
#### 6.4.2 预览能力
|
||
|
||
- Clip、Clip 节点和 AnimationFlow 都可以不依赖场景 GameObject 预览,但使用不同的展示位置和上下文。
|
||
- 选中资源浏览区中的 Clip 时,右侧属性区显示独立 Clip 预览;它直接播放 Clip 帧表和 Clip 自身默认设置,不应用任何节点覆盖。
|
||
- Clip 节点使用节点下方的内嵌预览区,应用该节点的速度和结束行为覆盖。
|
||
- Flow 预览从 `entryNodeId` 开始沿实际边关系执行,通过高亮并驱动当前节点的内嵌预览呈现,不打开另一套 Flow 预览画面。
|
||
- 预览必须复用或严格对齐运行时的时间推进与结束行为逻辑,避免编辑器效果和游戏内结果不一致。
|
||
- 支持播放、暂停、停止、从头播放、上一帧、下一帧、时间拖动和预览速度调整。
|
||
- 显示当前帧索引、当前帧耗时、累计时间和总时长;无限循环 Flow 的总时长显示为无限或不可确定。
|
||
- 独立 Clip 预览支持透明棋盘格、适应窗口、原始像素尺寸、整数倍缩放和手动缩放。
|
||
- 空帧必须按实际时长显示透明内容,不能在预览时跳过。
|
||
- 预览设置只属于编辑器,不修改 Clip 或 Graph 的运行时速度和结束行为。
|
||
|
||
节点自动预览策略:
|
||
|
||
```csharp
|
||
enum NodePreviewPolicy
|
||
{
|
||
Static, // 全部静止,只能手动播放
|
||
SelectedOnly, // 只循环播放主选中节点,默认值
|
||
AllVisible // 循环播放当前画布视口内的节点
|
||
}
|
||
```
|
||
|
||
- 默认使用 `SelectedOnly`。
|
||
- 选中 Clip 节点时从第 0 帧开始循环预览;切换选择后,原节点停止并恢复代表帧,新节点开始播放。
|
||
- 多选节点时只播放主选中节点;清除选择后所有节点恢复静止。
|
||
- `AllVisible` 只更新当前画布视口中的有效 Clip 节点。
|
||
- 自动循环只是编辑器检查策略,不改变 Clip 或节点的真实结束行为。
|
||
- Flow 预览期间暂停普通节点自动预览,只驱动当前执行节点;Flow 停止后恢复原策略。
|
||
- NodePreviewPolicy、预览速度和背景样式保存在本机工作区,不进入 Graph 资产。
|
||
|
||
### 6.5 ImportSource 管理
|
||
|
||
- 允许查看或复制不可变 internalId,并允许编辑 displayName、Texture、Aseprite JSON、pivot、`manageSpriteSlicing` 和新 Clip 默认结束行为。
|
||
- 显示来源 hash 状态、当前关联 Clip 数量和当前错误 / 警告数量;关联 Clip 数量由 `importInfo.importSourceId` 推导。
|
||
- 支持刷新单个来源、刷新全部启用来源和仅计算差异。
|
||
- 修改 TextureImporter 前必须显示将被修改的内容并要求确认。
|
||
- 导入差异按 `新增 / 更新 / Missing / 不变 / 错误` 分组显示。
|
||
- 差异条目可以定位到 Tag、Clip 或 ImportSource。
|
||
- 刷新失败时保留现有 Graph 数据,并在编辑器内显示完整错误,不要求用户只通过 Console 排查。
|
||
|
||
### 6.6 校验与问题定位
|
||
|
||
编辑器提供常驻、可折叠的校验结果区,不只使用一次性弹窗或 Console。校验问题分为 `Error / Warning / Info`,支持按级别和对象类型筛选。
|
||
|
||
校验触发方式:
|
||
|
||
- 顶部工具栏手动执行完整校验。
|
||
- 关键字段或结构修改后执行轻量增量校验。
|
||
- 导入刷新前执行完整的来源与命名冲突校验。
|
||
|
||
第一版至少检查:
|
||
|
||
- Clip / Flow playable id 为空、各自重名或彼此冲突。
|
||
- Clip 帧列表为空。
|
||
- Node / Edge / ImportSource internalId 为空、格式无效或在各自命名空间内重名。
|
||
- duration 小于等于 0。
|
||
- Clip speed、Node speedOverride 或其他参与播放的速度小于 0。
|
||
- AnimationNode 引用不存在或 Missing 的 Clip。
|
||
- AnimationEdge 的起点、终点不存在或连接规则无效。
|
||
- AnimationFlow 的入口节点不存在。
|
||
- 同一个 AnimationNode 被多个 Flow 用作入口。
|
||
- AnimationFlow 从入口出发的可达路径存在无效连接或无法按第一版规则继续执行。
|
||
- Edge 自连接或形成多节点环路。
|
||
- `defaultPlayableId` 不存在,或没有指向 Clip / Flow。
|
||
- ImportSource 缺少 Texture 或 JSON。
|
||
- Imported Clip 不属于其 ImportSource 所在 Graph 的 sub-asset,或 Graph sub-asset Clip 被其他 Graph 引用。
|
||
- 同一个 Graph 重复引用同一个 FrameClip。
|
||
- 同一 Texture 存在多个 `manageSpriteSlicing = true` 的 ImportSource。
|
||
- SourceFrame frameName 为空或在同一 ImportSource 内重复。
|
||
- 导入生成的非空源帧未找到对应 Sprite。
|
||
- JSON 内或多个 ImportSource 之间的 Tag 重名。
|
||
- Aseprite Frame 使用第一版不支持的 trim 或 rotate。
|
||
- 来源变化尚未刷新、Tag 消失或 Clip 处于 Missing 状态。
|
||
- 节点或子图未被任何 Flow 使用。
|
||
|
||
行为要求:
|
||
|
||
- 点击问题条目必须定位并选中对应 Clip、节点、Flow 或 ImportSource。
|
||
- Error 可以阻止导入刷新或完整 Flow 预览,但不阻止保存编辑中的 Graph。
|
||
- Warning 不阻止保存、刷新或预览。
|
||
- 校验结果必须说明问题对象、原因和建议处理方式,不能只给出通用错误文本。
|
||
|
||
性能要求:
|
||
|
||
- 不在 `OnGUI`、节点重绘或每个预览帧中执行完整校验。
|
||
- 创建 Edge 时只检查从目标节点沿后继链是否能够回到起点;第一版单后继结构下,该检查最多遍历一次节点链。
|
||
- 节点移动只更新 EditorData,不触发运行时结构或导入来源校验。
|
||
- 连续字段修改使用短暂防抖,并复用上一次校验结果;Graph 结构或相关字段未变化时不重复计算。
|
||
- 完整校验只在用户点击 Validate、开始 Flow 预览、刷新来源或进入正式运行前执行。
|
||
- Aseprite 解析、Texture 校验和来源 hash 只在对应来源变化或执行导入刷新时计算,不因普通节点编辑触发。
|
||
|
||
### 6.7 编辑器状态、保存与恢复
|
||
|
||
必须随 Graph 资产共享并进入版本控制:
|
||
|
||
- 节点位置。
|
||
- Flow 的编辑器颜色或其他团队需要共享的图形信息。
|
||
|
||
只保存在本机工作区:
|
||
|
||
- 左右面板宽度和底部区域高度。
|
||
- 画布缩放、平移和当前查看模式。
|
||
- 上次选中的资源和 Flow。
|
||
- 搜索词、筛选条件和排序方式。
|
||
- 节点预览策略,以及预览缩放、背景和播放速度。
|
||
|
||
所有会修改 Graph 或其子资产的操作必须:
|
||
|
||
- 支持 Undo / Redo。
|
||
- 正确标记资源 dirty。
|
||
- 在窗口关闭、脚本重编译或 Domain Reload 后保留已经保存的数据。
|
||
- 不因切换 Graph、切换 Flow 或刷新列表而静默丢失修改。
|
||
|
||
### 6.8 第一版编辑器范围
|
||
|
||
第一版必须完成:
|
||
|
||
- 单 Graph 主工作台。
|
||
- Clip / Flow / ImportSource 浏览与编辑。
|
||
- Clip 节点的全局画布编排。
|
||
- Clip 独立预览、节点内预览和在节点图上执行的 Flow 预览。
|
||
- 导入刷新、差异摘要和可定位校验。
|
||
- Undo / Redo 与编辑器状态保存。
|
||
|
||
第一版不要求:
|
||
|
||
- Branch、Random、Event 等扩展节点的完整编辑能力。
|
||
- 多个 FrameAnimationGraph 同屏编辑。
|
||
- 资源浏览区显示 Clip 图形或持续动态播放。
|
||
- 复杂分组、注释框、子图折叠和发布级自动排版。
|
||
|
||
## 7. 运行时需求
|
||
|
||
### 7.1 FrameAnimationPlayer 与播放目标
|
||
|
||
`FrameAnimationPlayer` 是挂载在场景对象上的运行时组件,形态类似 Unity `Animator`。它引用一个 `FrameAnimationGraph`,并控制同一 GameObject 上的 Sprite 显示组件。
|
||
|
||
配置:
|
||
|
||
```csharp
|
||
FrameAnimationGraph graph;
|
||
bool playOnEnable = false;
|
||
float speed = 1f;
|
||
```
|
||
|
||
规则:
|
||
|
||
- 第一版同时支持 `SpriteRenderer` 和 `UnityEngine.UI.Image`。
|
||
- Player 必须与且仅与一个受支持的显示组件挂在同一 GameObject。
|
||
- Player 初始化时查找并缓存同对象上的显示组件,不在播放过程中反复查找。
|
||
- Player 不搜索父物体或子物体,也不提供运行时 `BindTarget()` 或目标切换能力。
|
||
- 同时存在 `SpriteRenderer` 和 `Image` 时视为配置冲突,不采用隐式优先级。
|
||
- 没有有效显示组件时,编辑器校验报错;运行时 `Play()` 返回失败结果,不进入空转播放。
|
||
- 实现通过 `[DisallowMultipleComponent]`、自定义 Inspector、`OnValidate()` 和运行时初始化校验共同约束组件配置。Unity `[RequireComponent]` 无法直接表达 SpriteRenderer / Image 二选一。
|
||
- 第一版不支持播放过程中动态替换 Graph。
|
||
- `playOnEnable` 默认关闭。开启时,组件每次启用都从头播放 Graph 的 `defaultPlayableId`。
|
||
- Player 不提供实例级默认 playable 覆盖;未指定 id 时始终使用 Graph 的 `defaultPlayableId`。
|
||
- `OnDisable()` 终止当前播放请求;再次启用时不恢复旧进度,仅根据 `playOnEnable` 决定是否重新播放默认项。
|
||
- `HideTarget` 只设置 `SpriteRenderer.enabled = false` 或 `Image.enabled = false`,不调用 `GameObject.SetActive(false)`。下次成功播放时,Player 重新启用目标组件并显示起始帧。
|
||
- 空帧对两类目标都表现为 `sprite = null`。
|
||
|
||
其他 Sprite 显示组件属于后续扩展,不进入第一版。
|
||
|
||
### 7.2 播放接口
|
||
|
||
第一版公开接口:
|
||
|
||
```csharp
|
||
FrameAnimationPlaybackHandle Play();
|
||
FrameAnimationPlaybackHandle Play(string playableId);
|
||
FrameAnimationPlaybackHandle Play(string playableId, FrameAnimationPlayOptions options);
|
||
|
||
void Stop(FrameAnimationStopMode mode = FrameAnimationStopMode.HoldCurrentFrame);
|
||
void Pause();
|
||
void Resume();
|
||
void SetSpeed(float speed);
|
||
```
|
||
|
||
播放参数至少包含:
|
||
|
||
```csharp
|
||
FrameClipEndBehavior? endBehaviorOverride;
|
||
```
|
||
|
||
规则:
|
||
|
||
- FrameClip 和 AnimationFlow 都是正式可播放对象。
|
||
- Clip / Flow id 在 Graph 内共享唯一命名空间,调用方只传 `playableId`,不显式区分目标类型。
|
||
- Graph 找到 FrameClip 时直接播放帧表;找到 AnimationFlow 时从 `entryNodeId` 开始沿 Edge 执行。
|
||
- AnimationNode 不作为正式外部播放对象,只由 Flow 执行或由编辑器调试预览。
|
||
- `Play()` 未传 id 时使用 Graph 的 `defaultPlayableId`。
|
||
- 不提供独立 `Loop(string playableId)`;循环通过 `Play()` 的 `endBehaviorOverride = Loop` 表达。
|
||
- 对 AnimationFlow 使用播放请求的 Loop 覆盖时,含义是让终点 Clip 循环,不是让整个 Flow 从头循环。第一版不支持整个 Flow 循环。
|
||
- 第一版不提供 `Queue()`。需要由策划确定的连续演出应使用 AnimationFlow;运行时代码临时排队不纳入第一版。
|
||
- 第一版不公开运行时 `Seek()`。编辑器内部可以按 Clip 时间或帧定位,用于预览和测试,但不为 Flow 定义统一绝对时间轴。
|
||
|
||
### 7.3 播放状态与生命周期
|
||
|
||
播放状态:
|
||
|
||
```csharp
|
||
FrameAnimationPlaybackState
|
||
{
|
||
Stopped,
|
||
Playing,
|
||
Paused
|
||
}
|
||
|
||
FrameAnimationStopMode
|
||
{
|
||
HoldCurrentFrame,
|
||
Clear,
|
||
HideTarget
|
||
}
|
||
```
|
||
|
||
播放规则:
|
||
|
||
- 每次成功 `Play()` 都立即从目标 Clip 的第 0 帧开始显示,不等待下一次 `Update()`。
|
||
- 重播同一个 playable 时仍创建新的播放请求并从头开始,旧请求以 `Replaced` 完成。
|
||
- 播放其他 playable 时立即替换当前请求,旧请求以 `Replaced` 完成。
|
||
- Graph、playable id、目标组件或数据无效时,新请求立即以 `Failed` 完成,但不打断当前正在正常播放的请求。
|
||
- `Pause()` 冻结当前帧和帧内累计时间;`Resume()` 从该位置继续。
|
||
- `Stop()` 默认保留当前显示帧,不执行当前 Clip / Flow 的终点结束行为,并将当前请求以 `Stopped` 完成。
|
||
- `Stop(Clear)` 清空目标 Sprite;`Stop(HideTarget)` 禁用目标显示组件。
|
||
- `OnDisable()` 和销毁 Player 时,尚未完成的请求按 `Stopped` 结算。
|
||
- 一个 Player 同一时间最多有一个活动播放请求。
|
||
|
||
自然到达终点时,结束行为按以下优先级解析:
|
||
|
||
```text
|
||
节点 endBehaviorOverride
|
||
> Play 请求 endBehaviorOverride
|
||
> Flow endBehaviorOverride
|
||
> Clip defaultEndBehavior
|
||
```
|
||
|
||
结束行为语义:
|
||
|
||
- `HoldLastFrame`:保留最后一帧,播放状态变为 `Stopped`,请求以 `Completed` 完成。
|
||
- `Loop`:当前终点 Clip 持续循环,不产生自然完成通知;直到被替换或主动停止后才结算请求。
|
||
- `Clear`:自然完成后将目标 Sprite 设为 `null`,保持显示组件启用,请求以 `Completed` 完成。
|
||
- `HideTarget`:自然完成后禁用显示组件,请求以 `Completed` 完成。
|
||
- Flow 中间节点存在后继 Edge 且没有显式节点结束行为时,Clip 播放一次后立即进入后继节点;Clip 自身默认结束行为不阻止 Flow 推进。
|
||
- Flow 节点切换时,在同一次时间求值中立即显示下一个 Clip 的第 0 帧。
|
||
|
||
### 7.4 时间推进与速度
|
||
|
||
运行时使用 `Update()` 提供时间,底层通过统一求值逻辑推进:
|
||
|
||
```csharp
|
||
Evaluate(double deltaTimeSeconds);
|
||
```
|
||
|
||
规则:
|
||
|
||
- 第一版运行时只使用 `Time.deltaTime`,动画默认受 `Time.timeScale` 影响。
|
||
- 第一版不提供 unscaled time、运行时 Manual 模式或单次播放时间源覆盖。
|
||
- 编辑器预览使用编辑器时钟驱动同一套内部求值逻辑,但不把 Manual 时间模式暴露为运行时 API。
|
||
- 每帧将时间累计到当前动画帧;达到帧时长后减去该帧时长并保留余量,而不是把累计时间清零。
|
||
- 一次 `Evaluate()` 可以跨过多帧和多个 Flow 节点。发生较大 deltaTime 时,应落到正确的最终帧,不能只前进一帧。
|
||
- Frame 的 `durationMs` 在求值时转换参与计算;内部累计时间使用 `double`,降低长时间累计误差。
|
||
- 直接播放 Clip 时,实际推进速度为 `Player.speed * Clip.speed`。
|
||
- 通过 Flow 节点播放时,实际推进速度为 `Player.speed * (Node.speedOverride ?? Clip.speed)`;Node speedOverride 只替换 Clip speed,不替换 Player speed。
|
||
- Player speed、Clip speed 和 Node speedOverride 都不允许小于 0;编辑器校验和运行时接口都必须拒绝负值。
|
||
- speed 等于 0 时不做特殊状态转换;播放器保持原状态,只因推进量为 0 而停在当前帧。速度恢复为正数后继续。
|
||
- `Time.timeScale = 0` 时同样不改变播放状态,只因 `Time.deltaTime = 0` 而停止推进。
|
||
- 不使用 `FixedUpdate()`、`WaitForSeconds()` 或逐帧协程作为核心计时方式。
|
||
|
||
### 7.5 播放 Handle、异步等待与完成通知
|
||
|
||
异步等待和播放完成通知属于第一版核心能力。每次 `Play()` 都返回独立的 `FrameAnimationPlaybackHandle`,不因 playable id 相同而复用请求。
|
||
|
||
接口:
|
||
|
||
```csharp
|
||
FrameAnimationPlaybackHandle : CustomYieldInstruction
|
||
{
|
||
long RequestId { get; }
|
||
bool IsCompleted { get; }
|
||
FrameAnimationPlaybackResult Result { get; }
|
||
|
||
Task<FrameAnimationPlaybackResult> WaitAsync();
|
||
void RegisterCompleted(Action<FrameAnimationPlaybackResult> callback);
|
||
}
|
||
|
||
FrameAnimationPlaybackAwaitable : CustomYieldInstruction
|
||
{
|
||
FrameAnimationPlaybackHandle PlaybackHandle { get; }
|
||
}
|
||
|
||
FrameAnimationPlaybackAwaitable PlayAsync();
|
||
FrameAnimationPlaybackAwaitable PlayAsync(string playableId);
|
||
FrameAnimationPlaybackAwaitable PlayAsync(string playableId, FrameAnimationPlayOptions options);
|
||
|
||
FrameAnimationPlaybackResult
|
||
{
|
||
long requestId;
|
||
string playableId;
|
||
FrameAnimationCompletionReason reason;
|
||
FrameAnimationPlaybackError error;
|
||
}
|
||
|
||
FrameAnimationPlaybackError
|
||
{
|
||
FrameAnimationPlaybackErrorCode code;
|
||
string message;
|
||
}
|
||
|
||
FrameAnimationPlaybackErrorCode
|
||
{
|
||
None,
|
||
PlayerNotReady,
|
||
GraphMissing,
|
||
TargetMissing,
|
||
TargetConflict,
|
||
PlayableIdEmpty,
|
||
PlayableNotFound,
|
||
InvalidPlayableData,
|
||
InvalidSpeed
|
||
}
|
||
|
||
FrameAnimationCompletionReason
|
||
{
|
||
Completed,
|
||
Replaced,
|
||
Stopped,
|
||
Failed
|
||
}
|
||
```
|
||
|
||
要求:
|
||
|
||
- Handle 可以直接用于 `yield return handle`。
|
||
- `PlayAsync()` 调用时立即开始播放,返回值可直接用于 `yield return`;其 `PlaybackHandle` 仍表示底层播放请求的完整生命周期。
|
||
- 非循环 playable 的异步等待随播放自然结束。直接 Loop Clip 固定等待第一轮;终点为 Loop 的 Flow 按 `AnimationFlow.AsyncCompletionMode` 在终点 Loop 首帧显示后或第一轮回绕后解除等待。
|
||
- Awaitable 解除等待不等于底层 Loop 播放完成;Loop 的 `PlaybackHandle` 继续保持未完成,直到被替换、停止、禁用或销毁。
|
||
- `WaitAsync()` 使用标准 `Task`,项目第一版不引入 UniTask 依赖。
|
||
- `RegisterCompleted()` 是正式完成通知接口。请求尚未完成时登记回调;请求已经完成时立即用既有结果调用,避免同步失败或极短动画造成通知丢失。
|
||
- 同一个播放请求只能从未完成状态结算一次;协程、Task 和回调必须观察到同一个结果。
|
||
- 循环播放不会自然完成;Handle 保持未完成,直到请求被替换、主动停止、Player 禁用或销毁。
|
||
- 完成回调由 Player 在 Unity 主线程触发。
|
||
- 调用方可以多次注册完成回调,每个注册独立调用一次;单个注册不得重复触发。
|
||
- 播放失败通过 `Failed` 结果表达,不使用异常表示正常的资源、目标、参数或 id 校验失败。
|
||
- `Failed` 结果必须带非 `None` 的结构化错误码和可读消息;非失败结果的错误码必须为 `None`。
|
||
- 错误消息用于日志和调试,不作为程序分支依据;调用方应根据 `FrameAnimationPlaybackErrorCode` 判断失败类型。
|
||
- 将来如果项目整体采用 UniTask,可以增加独立适配层,不修改 Handle 的核心状态和结算机制。
|
||
|
||
### 7.6 运行时状态查询
|
||
|
||
Player 提供以下只读状态,供调试、界面显示和后续系统接入使用:
|
||
|
||
```csharp
|
||
FrameAnimationPlaybackState State { get; }
|
||
string CurrentPlayableId { get; }
|
||
string CurrentClipId { get; }
|
||
string CurrentNodeId { get; }
|
||
int CurrentFrameIndex { get; }
|
||
float Speed { get; }
|
||
```
|
||
|
||
语义:
|
||
|
||
- `CurrentPlayableId` 是当前播放请求指定的一级 Clip 或 Flow id。
|
||
- `CurrentClipId` 是当前真正提供显示帧的 Clip id。
|
||
- `CurrentNodeId` 只在播放 Flow 时有值;直接播放 Clip 时为空。
|
||
- 没有当前播放内容时,字符串属性为空,`CurrentFrameIndex = -1`。
|
||
- 这些属性只反映状态,外部不能通过修改它们控制播放位置。
|
||
- 第一版不提供含义模糊的统一 `GetPlaybackTime()` 或 `GetDuration()`。Clip 时长可以通过明确的 Clip 数据查询接口获得;Flow 可能包含无限循环,未来加入分支后也不保证存在唯一总时长。
|
||
|
||
## 8. 分阶段实施计划
|
||
|
||
实施过程由同一套整体架构约束,但按可独立验证的阶段推进。第一阶段即确定完整序列化数据模型,后续阶段不得通过改变既有字段语义完成扩展;如需调整已落地资产结构,必须先补充迁移方案。
|
||
|
||
每个阶段完成后应至少执行代码编译、该阶段针对性测试和对应验收,再进入下一阶段。阶段交付可以暂时不具备完整第一版编辑体验,但不得以一次性临时代码绕过资产所有权、引用稳定性、Undo / Redo 或运行时语义。
|
||
|
||
### 8.1 第一阶段:数据模型与运行时核心
|
||
|
||
目标:
|
||
|
||
- 确定并实现第一版完整序列化数据模型,包括 FrameClip、FrameAnimationGraph、AnimationNode、AnimationEdge、AnimationFlow、ImportSource、Settings 和 EditorData。
|
||
- 实现 Graph 的统一 playable id 解析、Flow 可达路径计算和基础数据校验。
|
||
- 实现 FrameAnimationPlayer 对 SpriteRenderer / Image 的播放支持。
|
||
- 实现直接 Clip 播放和线性 AnimationFlow 播放。
|
||
- 实现播放替换、暂停、恢复、停止、结束行为、速度计算和大 deltaTime 跨帧推进。
|
||
- 实现 FrameAnimationPlaybackHandle、完成回调、协程等待、标准 Task 等待和结构化失败结果。
|
||
|
||
验收:
|
||
|
||
- 可以使用手工构造的 FrameAnimationGraph,不依赖 AnimatorController 播放 Clip 和线性 Flow。
|
||
- SpriteRenderer 与 Image 的首帧显示、空帧、结束行为和 HideTarget 语义一致。
|
||
- 同一播放请求只结算一次,Completed / Replaced / Stopped / Failed 结果符合文档定义。
|
||
- 大 deltaTime 可以跨过多帧和多个 Flow 节点并落到正确结果。
|
||
- 无效 Graph、playable id、显示目标、空 Clip 和非法速度返回对应结构化错误。
|
||
|
||
### 8.2 第二阶段:Aseprite 导入与稳定刷新
|
||
|
||
目标:
|
||
|
||
- 实现 Aseprite JSON Object / Array 两种 frames 格式解析,并完整保留源顺序与 UTF-8 名称。
|
||
- 实现 forward、reverse、pingpong 和 pingpong_reverse 的确定性帧序列展开。
|
||
- 实现来源、Tag、Frame、Texture 尺寸、trimmed / rotated 和 playable id 冲突校验。
|
||
- 实现新增、更新、Missing、不变和错误的导入差异模型。
|
||
- 实现按 importSourceId + sourceTagName 对 Imported Clip 进行原地刷新。
|
||
- 实现 Imported Clip sub-asset 的创建、Missing 恢复和字段所有权保护。
|
||
- 实现 manageSpriteSlicing=false 时的只读 Sprite 匹配。
|
||
- 实现 manageSpriteSlicing=true 时的 TextureImporter 差异确认、写入所有权检查和稳定 Sprite ID 更新。
|
||
- 实现来源 hash 与单来源 / 全部来源的原子刷新流程。
|
||
|
||
验收:
|
||
|
||
- Object / Array 两种 Aseprite JSON 均能得到相同语义的有序 SourceFrame。
|
||
- 中文 Tag 和 frameName 在导入、刷新和资产保存后保持不变。
|
||
- 新增、消失、重新出现和用户已重命名的 Tag 对应 Clip 均按稳定匹配规则处理。
|
||
- 刷新 Imported Clip 不替换其 sub-asset,不破坏已有 Node 和 Flow 引用。
|
||
- 自动切图更新同名 frameName 时保留 Sprite ID,并阻止同一 Texture 出现多个写入所有者。
|
||
- 任一阻断错误出现时,不产生部分 Graph / Clip 更新;TextureImporter 更新失败时也不应用 Graph 变化。
|
||
|
||
### 8.3 第三阶段:Graph 资产管理工作台
|
||
|
||
目标:
|
||
|
||
- 实现单 Graph FrameAnimationGraph EditorWindow 的工具栏、资源浏览区、属性区、导入差异区和校验结果区。
|
||
- 实现 Clips / Flows / Sources 浏览、搜索、筛选、排序、分组和对象定位。
|
||
- 实现 Manual Clip 创建与帧表编辑,以及 Imported Clip 只读展示和复制为 Manual Clip。
|
||
- 实现 Clip / Flow 正式重命名流程、Graph 内引用原子更新和外部字符串强警告。
|
||
- 实现 Clip 移除、sub-asset 删除、共享外部 Manual Clip 提示和引用保护。
|
||
- 实现 ImportSource 编辑、差异查看、单来源刷新和全部来源刷新入口。
|
||
- 实现可定位的 Error / Warning / Info 校验面板。
|
||
- 确保全部资产修改正确接入 SerializedObject、Undo / Redo、dirty 标记和保存恢复。
|
||
|
||
验收:
|
||
|
||
- 可以在主工作台中创建、打开、保存和管理 FrameAnimationGraph 的 Clip、Flow 与 ImportSource。
|
||
- Manual / Imported、外部资产 / Graph sub-asset 的编辑和删除权限符合资产所有权规则。
|
||
- 重命名、复制和删除操作不会静默留下无效 Graph 内引用。
|
||
- 导入差异和校验问题可以定位到具体 Clip、Flow、Node 或 ImportSource。
|
||
- Undo / Redo、窗口重开和脚本重编译不会丢失已经保存的数据。
|
||
|
||
### 8.4 第四阶段:节点画布与 AnimationFlow 编排
|
||
|
||
目标:
|
||
|
||
- 实现 FrameAnimationGraph 的唯一全局节点画布。
|
||
- 实现从 Clip 拖入或右键创建 Clip 节点,以及节点创建、移动、删除、连接和断开。
|
||
- 实现每个节点最多一个后继 Edge、自连接 / 多节点环路阻止和显式终点行为约束。
|
||
- 实现从单个节点创建 Flow、修改 Flow 入口和入口引用保护。
|
||
- 实现“显示全部”和“聚焦 Flow”,并按入口可达关系高亮共享节点与连线。
|
||
- 实现节点引用定位、基础自动布局和节点位置共享保存。
|
||
- 使画布操作全部支持 Undo / Redo,并与资源浏览区、属性区和校验区同步选择。
|
||
|
||
验收:
|
||
|
||
- 使用者可以不写代码配置“播放一次 -> 进入 Idle loop”等基础线性演出。
|
||
- 多个 Flow 可以从不同入口共享后续节点,且不保存额外节点成员列表。
|
||
- 非法连接在创建时被阻止或形成可定位 Error,完整 Flow 预览不会执行非法路径。
|
||
- 删除入口节点、被引用 Clip 或连线时会显示影响范围,不会静默破坏 Flow。
|
||
- 节点位置、连接和 Flow 入口在保存、窗口重开和脚本重编译后保持一致。
|
||
|
||
### 8.5 第五阶段:预览、恢复与完整验收
|
||
|
||
目标:
|
||
|
||
- 实现 FrameClip 独立预览、AnimationNode 内嵌预览和 AnimationFlow 图上预览。
|
||
- 让编辑器预览复用或严格对齐运行时的帧推进、速度和结束行为求值逻辑。
|
||
- 实现播放、暂停、停止、从头播放、逐帧、时间拖动、缩放、棋盘格和空帧显示。
|
||
- 实现 NodePreviewPolicy、Flow 预览期间的自动预览切换和本机工作区状态保存。
|
||
- 完成面板尺寸、画布视图、选择、搜索、筛选和预览偏好的恢复。
|
||
- 对第一版完整验收标准执行回归验证,并补充必要的使用说明与测试资产。
|
||
|
||
验收:
|
||
|
||
- Clip、节点和 Flow 的编辑器预览结果与相同数据的运行时播放一致。
|
||
- Flow 预览能在全局画布中正确驱动当前节点并展示节点切换。
|
||
- Domain Reload、窗口重开、切换 Graph 和刷新来源不会造成已保存数据或工作区状态异常丢失。
|
||
- 文档第 9 节全部验收标准通过,第一版形成可用于实际动画资产生产和运行时播放的完整闭环。
|
||
|
||
## 9. 验收标准
|
||
|
||
第一版完成时,应满足:
|
||
|
||
1. 能创建、打开和保存 FrameAnimationGraph,并管理其中的 Clip、Flow 和 ImportSource。
|
||
2. 能在独立 EditorWindow 中浏览 Clip,并直接预览 Clip、节点和 AnimationFlow。
|
||
3. 能在全局节点画布中创建 Clip 节点、连接顺序、设置 Flow 入口并查看共享节点。
|
||
4. 能从 PNG / Aseprite JSON 生成或刷新 Clip,并显示刷新差异。
|
||
5. 能通过可定位的校验面板发现命名、引用、导入和 Missing 问题。
|
||
6. 编辑器数据修改支持 Undo / Redo,窗口重开或脚本重编译后不丢失已保存内容。
|
||
7. 能挂载播放器到 SpriteRenderer / Image 并播放动画。
|
||
8. 能通过 FrameAnimationGraph 的统一 playable id 播放 Clip 或 AnimationFlow,调用方不需要区分目标类型。
|
||
9. 播放器能正确处理重播、替换、暂停、停止、终点结束行为以及大 deltaTime 跨帧推进。
|
||
10. 播放 Handle 能通过协程、标准 Task 和完成回调等待,并对 Completed / Replaced / Stopped / Failed 各结算一次。
|
||
11. 运行时播放空 Clip、无效 id、无效目标或非法速度时返回带结构化错误信息的 Failed 结果。
|
||
12. 能将导入 Clip 复制为不再参与刷新且帧表可编辑的 Manual Clip。
|
||
13. 外部 Manual Clip 能被多个 Graph 共享;Graph sub-asset Clip 不会被其他 Graph 非法引用,移除操作遵守资产所有权。
|
||
14. 自动切图刷新能为同名 frameName 保留稳定 Sprite ID,并阻止多个 ImportSource 同时写入同一 TextureImporter。
|
||
15. 刷新导入资源时,不破坏已存在 Clip、Node 和 Flow 的引用。
|
||
|
||
## 10. 后续扩展
|
||
|
||
以下能力已明确不进入第一版,也不阻塞第一版实现:
|
||
|
||
1. 其他导入格式,例如手动 Grid JSON、rows / columns / frameCount、全局 frameDuration 和自定义 frameIndices。
|
||
2. Wait、Random、Branch、Event、SetParameter、Jump 等节点,以及帧事件、条件分支和随机播放。
|
||
3. 多 Clip 循环、Flow 整体循环、循环次数和明确的 LoopBack 语义。
|
||
4. 其他 Sprite 显示组件、unscaled time、运行时 Seek、运行时 Queue 和 UniTask 适配。
|
||
5. 大型 Graph 的自定义分组、注释框、子图折叠、发布级自动排版和多个 Graph 同屏编辑。
|
||
6. trimmed / rotated Aseprite 帧支持,以及未使用 Sprite 元数据的安全清理工具。
|
||
|
||
后续能力必须在保持第一版资产兼容的前提下单独补充需求与技术设计,不能通过改变现有字段语义隐式加入。
|
||
|
||
## 11. 已确定设计摘要
|
||
|
||
第一版已确定:
|
||
|
||
1. 第一版只做序列帧系统,不做通用动画系统。
|
||
2. 命名采用 `FrameAnimationGraph` / `FrameClip` / `AnimationFlow`。
|
||
3. Clip 保存默认结束行为,但播放请求可以覆盖。
|
||
4. 导入刷新以 `importSourceId + sourceTagName` 为稳定匹配键,不依赖当前 Clip id。
|
||
5. FrameAnimationGraph 采用全局节点图编辑;AnimationFlow 是图中的命名演出流程。
|
||
6. 运行时底层直接按时间设置 Sprite,不使用 Unity Animator / Playables。
|
||
7. 第一版 Aseprite 导入兼容 Object / Array 两种 frames 格式,但不支持 trimmed 或 rotated 帧。
|
||
8. 删除 AnimationEntry;Clip 和 Flow 都是一等可播放对象,共享统一且互斥的 playable id 命名空间,正式调用不区分目标类型。
|
||
9. 第一版使用单 Graph 独立 EditorWindow,采用紧凑资源浏览、全局节点画布、属性区、节点内预览、Clip 独立预览和可定位校验组成的工作台。
|
||
10. AnimationFlow 不保存节点成员列表;编辑器和运行时都从入口沿 Edge 推导可达节点,并在同一全局画布上高亮显示。
|
||
11. Flow 从单个选中节点创建,同一个节点不能作为多个 Flow 的入口;第一版禁止 Edge 自连接和多节点环路。
|
||
12. Graph 直接保存 `List<FrameClip>`;Clip 的 `.asset` / sub-asset 存储关系由 Unity 资产关系推导,不序列化重复状态。
|
||
13. `FrameClip.importInfo == null` 表示 Manual Clip,非空表示 Imported Clip;Missing 状态保存在 importInfo 内,不再保存 isGenerated 等重复字段。
|
||
14. Node、Edge 和 ImportSource 使用不可变 GUID internalId;Clip、Flow 和 Graph 保留面向外部调用的可读 id。
|
||
15. Graph 不拥有 Sprite 或 Texture;可写 ImportSource 负责维护 TextureImporter 切图数据,并尽量保持 Sprite ID 稳定。
|