diff --git a/Docs/动画系统需求整理.md b/Docs/动画系统需求整理.md index 6a97617b8..d936b98a5 100644 --- a/Docs/动画系统需求整理.md +++ b/Docs/动画系统需求整理.md @@ -1,6 +1,6 @@ # 自研帧动画系统需求文档 -> 当前文档处于需求梳理阶段。目标是先把系统边界、数据结构、编辑器工作流和运行时接入方式说清楚,再进入具体技术设计与实现。 +> 本文档定义自研帧动画系统第一版的系统边界、数据结构、资产导入、编辑器工作流、运行时行为和验收标准。具体类拆分、Unity API 选型与代码组织在技术设计和实现阶段确定。 ## 1. 背景与问题 @@ -39,14 +39,13 @@ ### 1.3 文档目标 -本文档希望定义一个独立于 Unity Animator Controller 的自研帧动画系统,重点解决: +本文档定义一个独立于 Unity Animator Controller 的自研帧动画系统,重点解决: - 序列帧动画的数据结构 - PNG / Aseprite JSON 等来源的导入与刷新 -- Clip / AnimationSet / Sequence 的组织关系 +- FrameClip / FrameAnimationGraph / AnimationFlow 的组织关系 - 编辑器内预览、检查和编排工作流 - 运行时播放器接口 -- 与现有 Actor、Yarn、AnimatorCenter、存档系统的兼容与迁移路径 ## 2. 系统目标与非目标 @@ -54,33 +53,33 @@ 1. 支持 SpriteRenderer 和 UI Image 的序列帧播放。 2. 支持单个动画片段独立播放、预览、刷新和检查。 -3. 支持同一对象的一组动画片段集中管理。 -4. 支持简单演出编排,例如“播放一次动作 -> 进入 Idle 循环 -> 停在最后一帧”。 +3. 支持同一角色或对象的一组动画片段集中管理。 +4. 支持命名演出序列编排,例如“播放一次动作 -> 进入 Idle 循环”或“连续播放多个片段 -> 停在最后一帧”。 5. 支持从 Aseprite JSON / PNG 生成动画数据,并在资源更新时保持动画资产引用稳定。 6. 提供不依赖场景 GameObject 的编辑器预览能力。 7. 为运行时代码提供清晰、稳定、可迁移的播放 API。 ### 2.2 非目标 -第一阶段不尝试替代以下系统: +第一版不尝试替代以下系统: 1. Unity Timeline 的多轨演出能力。 2. DOTween 等 Tween 动效系统。 3. Cinemachine 镜头系统。 4. 骨骼动画、网格变形、复杂 Transform 曲线动画。 -5. 完整可视化节点编辑器或通用状态机编辑器。 +5. 通用状态机编辑器。 -### 2.3 第一阶段范围建议 +### 2.3 第一版范围 -第一阶段建议聚焦: +第一版包含: - `FrameClip`:单个序列帧片段。 -- `AnimationSet`:同一对象的一组 `FrameClip`。 -- 简单 `Sequence`:线性片段队列 + 结束行为。 +- `FrameAnimationGraph`:同一角色或对象的完整帧动画图资产。 +- `AnimationFlow`:`FrameAnimationGraph` 全局节点图中的命名演出流程。 - `FrameAnimationPlayer`:运行时播放器。 -- Clip / Set 的基础编辑器和预览。 +- Graph / Clip / Flow 的基础编辑器和预览。 -复杂图编辑器、条件分支、随机播放、批量迁移工具可以放到后续阶段。 +第一版完成系统自身的数据、导入、编辑器和运行时播放闭环。复杂节点类型、条件分支和随机播放不进入第一版,但数据模型保留节点式演出编排的扩展空间。 ## 3. 术语与概念 @@ -88,23 +87,24 @@ 单帧数据,描述某一时间段内应该显示的 Sprite。 -候选字段: +字段: - `Sprite sprite` -- `float duration` +- `int durationMs` - `string frameName` - `int sourceIndex` -待讨论: +规则: -- `duration` 使用秒还是毫秒? -- 是否需要记录来源 rect / pivot / tag 等导入信息? +- 帧时长使用 `int durationMs` 保存毫秒值;运行时可按需换算为秒。 +- `sprite` 允许为空;空帧表示该帧不显示 Sprite,但仍占用对应时长。 +- Frame 只保存运行时播放必要信息,导入元数据放在 Clip / Graph 的来源信息中。 ### 3.2 FrameClip 最小可播放动画片段,由一组 Frame 组成。 -候选职责: +职责: - 保存帧列表。 - 保存默认播放速度。 @@ -112,399 +112,1184 @@ - 保存导入来源。 - 提供总时长、帧数等只读信息。 -待讨论: +规则: -- Clip 是否应当保存 loop,还是只保存 default end behavior? -- 导入生成的 Clip 是否允许手动编辑帧表? -- Clip 作为独立 `.asset` 保存,还是作为 AnimationSet 的 sub-asset 保存? +- Clip 不单独保存 `bool loop`,循环统一由 `defaultEndBehavior = Loop` 表达。 +- 导入生成的 Clip 帧表只读,不允许直接手动编辑帧内容。 +- Graph 外创建的 Manual Clip 可以保存为独立 `.asset`;Graph 内导入生成的 Clip 保存为 Graph 的 sub-asset。 -### 3.3 AnimationSet +### 3.3 FrameAnimationGraph -同一对象的一组动画片段集合。例如一个角色、一个场景物件或一个 UI 元件的所有动画。 +同一角色或对象的完整帧动画图资产。它不是简单的 Clip 集合,而是承载该对象的基础片段、导入来源、全局节点图、多个命名演出流程、对外可播放对象以及编辑器配置的顶层资产。 -候选职责: +职责: -- 维护 Clip 名称到 Clip 的映射。 -- 维护默认 Idle Clip。 +- 维护 Clip / Flow 统一可播放 id 到目标对象的映射。 +- 维护全局 `AnimationNode` / `AnimationEdge` 节点图。 +- 维护多个 `AnimationFlow`。 - 维护导入来源列表。 -- 支持刷新导入来源后按名称更新已有 Clip。 -- 提供编辑器集中预览。 +- 支持刷新导入来源后按 `importSourceId + sourceTagName` 更新已有 Clip。 +- 以 Clip 或 Flow 作为对外可播放对象,例如 `Idle`、`WakeUp`、`StartTalking`。 +- 提供编辑器集中预览、检查和演出编排。 -待讨论: +规则: -- 原文中的 `Library` 是否改名为 `AnimationSet`? -- Set 内 Clip 是否必须唯一命名? -- Set 是否负责 Sequence,还是 Sequence 独立成资产? +- 顶层资产命名为 `FrameAnimationGraph`。 +- `FrameAnimationGraph` 拥有一套全局节点和连线。 +- `AnimationFlow` 是全局节点图里的命名演出流程。 +- 在 Graph 外创建的 Manual Clip 是独立资产;由 Graph 内图片 / JSON 生成的 Imported Clip 必须是所属 Graph 的 sub-asset。 -### 3.4 Sequence +整体模型: -一段演出编排,描述多个 Clip 的播放顺序和结束策略。 +- 采用“单一全局节点图 + 多个 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 节点。 -- 第一阶段是否只做线性 Sequence? -- Sequence 是否需要独立资产? -- Sequence 是否应该支持分支、随机、条件判断? - -### 3.5 Player / Controller +### 3.5 FrameAnimationPlayer 运行时负责推进时间、设置 Sprite、响应代码调用的组件。 -候选拆分: +规则: -- `FrameAnimationPlayer`:底层播放逻辑。 -- `FrameAnimationController`:挂载在 GameObject 上,对外暴露 `Play("clipName")` 等接口。 -- `IFrameAnimationTarget`:统一封装 SpriteRenderer / Image。 - -待讨论: - -- 是否需要一个类似 `AnimatorCenter` 的全局注册中心? -- 是否沿用现有 `animatorName + stateName` 的外部调用模型? +- 第一版只提供 `FrameAnimationPlayer` 组件,不额外拆分 `FrameAnimationController`。 +- `FrameAnimationPlayer` 挂载在被控制的 GameObject 上,内部包含时间推进、Graph 解析、播放状态和目标组件适配逻辑。 +- Player 必须与一个 `SpriteRenderer` 或 `UnityEngine.UI.Image` 挂在同一 GameObject;目标适配属于内部实现,不作为第一版公开绑定接口。 +- 第一版不引入全局注册中心。 +- FrameClip 和 AnimationFlow 都可以被正式播放;调用方只提供统一 playable id,由 Graph 解析具体目标类型。 +- AnimationNode 只属于 Flow 内部结构,不作为正式外部播放目标。 ## 4. 数据结构需求 ### 4.1 FrameClip 数据 -候选字段: +字段: ```csharp -string clipName; -List frames; -float speed; -EndBehavior defaultEndBehavior; -ImportSource importSource; -bool isGenerated; +string id; +string displayName; +List frames; +float speed = 1f; +FrameClipEndBehavior defaultEndBehavior; +FrameClipImportInfo importInfo; // null 表示 Manual Clip ``` -候选结束行为: +单帧数据: + +```csharp +Sprite sprite; // 可为空,null 表示空帧 +int durationMs; +string frameName; +int sourceIndex; +``` + +结束行为: -- `Stop` - `HoldLastFrame` - `Loop` - `Clear` - `HideTarget` -待讨论: +规则: -- `Stop` 和 `HoldLastFrame` 是否需要区分? -- 循环播放是 Clip 的默认属性,还是 Play 请求的属性? +- `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` 或来源类型枚举。 -### 4.2 AnimationSet 数据 - -候选字段: +导入关联信息: ```csharp -string setName; +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 clips; -string defaultClipName; -List importSources; +List nodes; +List edges; +List flows; +List importSources; + +FrameAnimationGraphSettings settings; +FrameAnimationGraphEditorData editorData; ``` -待讨论: - -- Clip 引用外部资产,还是作为 Set 的子资产? -- 如果刷新后某个 Clip 名称消失,应该删除、标记 missing,还是保留旧数据? -- 如果刷新后出现同名 Clip,如何处理冲突? - -### 4.3 Sequence 数据 - -候选字段: +#### 4.2.1 Graph 标识 ```csharp -string sequenceName; -List steps; -EndBehavior finalBehavior; -string fallbackIdleClipName; +string id; +string displayName; ``` -待讨论: +规则: -- `SequenceStep` 是否只需要 clipName + overrideEndBehavior? -- 是否需要 step-level speed、事件、等待时间? -- 是否需要在某一帧触发事件? +- `id` 是稳定 key,用于资源查找、Addressable key 或外部工具引用。 +- `displayName` 是编辑器展示名,默认与 `id` 一致。 +- `id` 和 `displayName` 都允许中文。 +- 修改 `displayName` 不影响引用;修改 `id` 需要强警告。 + +#### 4.2.2 Clip 引用 + +Graph 直接维护 `List` 引用,不增加只表达存储状态的 `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 nodes; +List edges; +``` + +规则: + +- `FrameAnimationGraph` 持有一套全局节点和连线。 +- 节点和连线不归某个 Flow 独占。 +- 一个 Graph 内允许有多个不联通子图。 +- 不同 Flow 可以共享节点,例如多个 Flow 最终进入同一个 Idle 节点。 +- 节点图用于编辑器画布展示,也用于运行时沿边推进播放流程。 + +#### 4.2.4 AnimationFlow 列表 + +```csharp +List flows; +``` + +`AnimationFlow` 是全局节点图里的命名流程。它不直接拥有 nodes / edges,只记录入口和默认播放策略;包含的节点由入口可达关系推导。 + +规则: + +- 第一版采用“单一全局节点图 + 多个 AnimationFlow”的模型。 +- Flow 内嵌在 `FrameAnimationGraph` 中,不作为第一版独立资产。 +- Flow 强依赖本 Graph 内的 Clip、Node、Edge 和命名语义,内嵌更便于校验、预览和刷新。 + +#### 4.2.5 可播放对象命名空间 + +FrameClip 与 AnimationFlow 是并列的一等可播放对象。第一版不引入 AnimationEntry 抽象,也不为 Clip 或 Flow 自动生成额外入口对象。 + +```csharp +FrameAnimationPlayableIndex +{ + Dictionary 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 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 nodeEditorData; + List 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; +FrameClipEndBehavior? endBehaviorOverride; +``` + +说明: + +- `entryNodeId` 保存入口 AnimationNode 的 `internalId`。 +- `endBehaviorOverride` 是 Flow 到达终点时的可选结束行为覆盖。 +- Flow 包含的节点集合从 `entryNodeId` 沿全局边关系遍历得到,不单独保存 `includedNodeIds`。 +- 编辑器高亮、Flow 聚焦、校验和运行时使用同一套可达关系,避免人工维护的节点集合与实际连线不一致。 +- 多个 Flow 的可达范围可以重叠,因此可以自然共享 Idle 等公共节点。 +- 同一个 AnimationNode 不能作为多个 Flow 的入口,但不同 Flow 可以在后续路径中共享该节点。 +- Flow id 属于统一可播放命名空间,不能与任何 Clip 或其他 Flow id 重名。 + +#### 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”作为导入来源,不做通用导入源抽象。 -- `Manual` -- `AsepriteJsonAndTexture` -- `ManualGridJsonAndTexture` - -候选字段: +字段: ```csharp -ImportSourceType type; -Texture2D texture; -TextAsset json; -Vector2 pivot; -bool generateSpritesIfNeeded; +FrameAnimationImportSource +{ + string internalId; + string displayName; + bool isEnabled; + + Texture2D texture; + TextAsset asepriteJson; + + Vector2 pivot; + bool manageSpriteSlicing; + FrameClipEndBehavior defaultNewClipEndBehavior; + string lastSourceHash; +} ``` -待讨论: +#### 4.4.1 导入来源范围 -- 是否允许一个 AnimationSet 管理多组 PNG / JSON? -- 切图结果是否直接修改原 Texture Importer? -- 是否需要保存上次导入摘要,用于显示差异? +规则: + +- 第一版只支持 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: +系统读取 Aseprite 导出的 JSON 后,先建立按源文件导出顺序排列的 SourceFrame 列表,再根据 `meta.frameTags` 生成或刷新 FrameClip。 -- 读取 `frames` 中的帧 rect 和 duration。 -- 读取 `meta.frameTags` 作为 Clip 名称和帧范围。 -- 支持 `forward`、`reverse`、`pingpong` 等方向。 -- 支持中文 Tag 名称。 +#### 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。 -- 是否在导入时保留 Aseprite 原始 frameName? -- `pingpong` 是否展开成实际帧列表? +#### 5.1.2 Frame 转换规则 -### 5.2 手动 Grid JSON 导入 +- Aseprite 原始 frameName 写入 `Frame.frameName`,不参与 SourceFrame 排序、Clip 匹配或节点引用;当系统管理 Sprite 切图时,它作为保留 Sprite ID 的稳定匹配键。 +- 源帧在有序 SourceFrame 列表中的位置写入 `Frame.sourceIndex`。 +- Aseprite `duration` 原样转换为 `int durationMs`,不先换算为浮点秒。 +- FrameClip 中的 Sprite 根据 SourceFrame rect 对应到切图结果。 +- `spriteSourceSize` 和 `sourceSize` 第一版只用于校验和错误提示,不进入运行时 Frame 数据。 -系统可以复用现有生成器里的手动 JSON 思路: +#### 5.1.3 Tag 转换规则 -- 配置 rows / columns / frameCount。 -- 配置全局 frameDuration。 -- 配置每个动画片段的 frameIndices。 +- 每个 `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 第一版素材限制 -- 这个格式是否继续保留? -- 是否需要提供 JSON 模板和校验工具? +- 第一版要求 Aseprite 导出时关闭裁边和旋转。 +- 任一帧 `trimmed = true` 时作为阻断错误,不导入。后续如支持,需要根据 `spriteSourceSize` 修正每帧相对原始画布的位置和 pivot。 +- 任一帧 `rotated = true` 时作为阻断错误,不导入。 -### 5.3 刷新策略 +#### 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 刷新流程 -1. 以 Clip 名称作为匹配键。 -2. 同名 Clip 原地更新帧数据。 -3. 新 Clip 自动加入。 -4. 消失的 Clip 标记为 missing,等待用户确认是否删除。 -5. 手动修改过的字段不被刷新覆盖,除非用户选择强制刷新。 +```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 FrameClip Inspector +### 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 基础能力: -- 显示 Clip 名称、总时长、帧数、默认结束行为。 -- 显示帧表:序号、Sprite、duration、来源 frameName。 -- 支持不依赖场景 GameObject 的预览。 -- 支持播放、暂停、逐帧、调整预览速度。 +- 使用 `Clips / Flows / Sources` 三个标签页切换不同资源类型。 +- 支持按 `id`、`displayName`、`sourceTagName` 和 ImportSource 搜索 Clip。 +- 支持按名称、来源、帧数、时长和 missing 状态排序。 +- 支持按 Manual / Imported、正常 / Missing、是否已被节点引用筛选。 +- 列表项使用纯文本紧凑显示,不绘制 Sprite 缩略图。 +- 列表项显示名称、帧数、总时长、来源状态、节点引用数量和必要的错误 / 警告标记。 +- 双击或使用明确命令可定位到相关节点、目标资源或 ImportSource。 +- Clip 默认按 ImportSource 分组,Manual Clip 放在独立分组;同时提供扁平列表模式。 +- 搜索时可以忽略分组,直接显示全部匹配结果。 +- 单击 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 的入口;节点已是入口时禁用创建命令并提供定位现有 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、修改 duration。 +- 支持增加、删除、复制和调整帧顺序。 +- 支持替换 Sprite、修改 `durationMs` 和创建合法空帧。 +- 支持将导入 Clip 复制为新的 Manual Clip;复制后帧表可编辑,且不再参与 ImportSource 刷新。 -导入 Clip: +Imported Clip: -- 帧表默认只读。 -- 可以跳转到 ImportSource。 -- 可以刷新来源。 +- 帧表只读,不允许局部覆盖。 +- 支持跳转到对应 ImportSource、查看 sourceTagName 和刷新来源。 +- 如需手动修改帧内容,使用“复制为 Manual Clip”命令创建脱离导入刷新的副本。 -待讨论: +#### 6.4.2 预览能力 -- 是否允许导入 Clip 局部覆盖某一帧? -- 预览区域是否需要显示透明棋盘格、原始尺寸、缩放倍率? +- Clip、Clip 节点和 AnimationFlow 都可以不依赖场景 GameObject 预览,但使用不同的展示位置和上下文。 +- 选中资源浏览区中的 Clip 时,右侧属性区显示独立 Clip 预览;它直接播放 Clip 帧表和 Clip 自身默认设置,不应用任何节点覆盖。 +- Clip 节点使用节点下方的内嵌预览区,应用该节点的速度和结束行为覆盖。 +- Flow 预览从 `entryNodeId` 开始沿实际边关系执行,通过高亮并驱动当前节点的内嵌预览呈现,不打开另一套 Flow 预览画面。 +- 预览必须复用或严格对齐运行时的时间推进与结束行为逻辑,避免编辑器效果和游戏内结果不一致。 +- 支持播放、暂停、停止、从头播放、上一帧、下一帧、时间拖动和预览速度调整。 +- 显示当前帧索引、当前帧耗时、累计时间和总时长;无限循环 Flow 的总时长显示为无限或不可确定。 +- 独立 Clip 预览支持透明棋盘格、适应窗口、原始像素尺寸、整数倍缩放和手动缩放。 +- 空帧必须按实际时长显示透明内容,不能在预览时跳过。 +- 预览设置只属于编辑器,不修改 Clip 或 Graph 的运行时速度和结束行为。 -### 6.2 AnimationSet Editor +节点自动预览策略: -基础能力: +```csharp +enum NodePreviewPolicy +{ + Static, // 全部静止,只能手动播放 + SelectedOnly, // 只循环播放主选中节点,默认值 + AllVisible // 循环播放当前画布视口内的节点 +} +``` -- 显示 Set 内所有 Clip。 -- 每个 Clip 有小预览窗口。 -- 支持搜索、排序、重命名、检查重复名。 -- 支持设置默认 Idle / 默认 Clip。 -- 支持从导入来源批量刷新。 -- 支持侧边预览完整 Sequence 或单个 Clip。 +- 默认使用 `SelectedOnly`。 +- 选中 Clip 节点时从第 0 帧开始循环预览;切换选择后,原节点停止并恢复代表帧,新节点开始播放。 +- 多选节点时只播放主选中节点;清除选择后所有节点恢复静止。 +- `AllVisible` 只更新当前画布视口中的有效 Clip 节点。 +- 自动循环只是编辑器检查策略,不改变 Clip 或节点的真实结束行为。 +- Flow 预览期间暂停普通节点自动预览,只驱动当前执行节点;Flow 停止后恢复原策略。 +- NodePreviewPolicy、预览速度和背景样式保存在本机工作区,不进入 Graph 资产。 -待讨论: +### 6.5 ImportSource 管理 -- 第一版是否做独立 EditorWindow,而不是只做 Inspector? -- 是否需要拖拽排序? -- Clip 小窗全部实时播放是否会影响编辑器性能? +- 允许查看或复制不可变 internalId,并允许编辑 displayName、Texture、Aseprite JSON、pivot、`manageSpriteSlicing` 和新 Clip 默认结束行为。 +- 显示来源 hash 状态、当前关联 Clip 数量和当前错误 / 警告数量;关联 Clip 数量由 `importInfo.importSourceId` 推导。 +- 支持刷新单个来源、刷新全部启用来源和仅计算差异。 +- 修改 TextureImporter 前必须显示将被修改的内容并要求确认。 +- 导入差异按 `新增 / 更新 / Missing / 不变 / 错误` 分组显示。 +- 差异条目可以定位到 Tag、Clip 或 ImportSource。 +- 刷新失败时保留现有 Graph 数据,并在编辑器内显示完整错误,不要求用户只通过 Console 排查。 -### 6.3 Sequence Editor +### 6.6 校验与问题定位 -第一阶段建议做轻量列表式编辑: +编辑器提供常驻、可折叠的校验结果区,不只使用一次性弹窗或 Console。校验问题分为 `Error / Warning / Info`,支持按级别和对象类型筛选。 -- 添加 Step。 -- 选择 Clip。 -- 设置 Step 播放策略。 -- 设置最终行为。 -- 一键从头预览。 +校验触发方式: -后续再考虑节点图或连线式编辑。 +- 顶部工具栏手动执行完整校验。 +- 关键字段或结构修改后执行轻量增量校验。 +- 导入刷新前执行完整的来源与命名冲突校验。 -待讨论: +第一版至少检查: -- 是否真的需要节点图? -- Sequence 是否需要与 Yarn / Timeline 联动显示? - -### 6.4 校验与错误提示 - -编辑器应能检查: - -- 空 Sprite。 +- Clip / Flow playable id 为空、各自重名或彼此冲突。 +- Clip 帧列表为空。 +- Node / Edge / ImportSource internalId 为空、格式无效或在各自命名空间内重名。 - duration 小于等于 0。 -- Clip 重名。 -- Sequence 引用不存在的 Clip。 -- ImportSource 缺少 texture 或 json。 -- JSON 中 Tag 重名。 -- 刷新后丢失的 Clip。 +- 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 播放目标 +### 7.1 FrameAnimationPlayer 与播放目标 -必须支持: +`FrameAnimationPlayer` 是挂载在场景对象上的运行时组件,形态类似 Unity `Animator`。它引用一个 `FrameAnimationGraph`,并控制同一 GameObject 上的 Sprite 显示组件。 -- `SpriteRenderer` -- `UnityEngine.UI.Image` +配置: -可选支持: +```csharp +FrameAnimationGraph graph; +bool playOnEnable = false; +float speed = 1f; +``` -- 未来扩展到其他自定义 Sprite 显示组件。 +规则: + +- 第一版同时支持 `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 -Play(string clipName); -Play(string clipName, EndBehavior endBehavior); -Loop(string clipName); -Queue(string clipName); -Stop(); -Pause(); -Resume(); -Seek(float timeSeconds); -SetSpeed(float speed); -GetCurrentClipName(); -GetPlaybackTime(); -GetDuration(); +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); ``` -待讨论: +播放参数至少包含: -- `Loop` 是否只是 `Play(..., Loop)` 的快捷方法? -- `Queue` 第一阶段是否需要? -- 是否需要异步协程接口:`IEnumerator PlayAsync(...)`? - -### 7.3 播放行为 - -需要明确: - -- 播放新 Clip 时是否从第 0 帧开始。 -- 播放同一 Clip 时是否重播。 -- Stop 后停在哪一帧。 -- Pause 是否冻结当前帧。 -- HideTarget 是否由播放器设置 GameObject active,还是只清空 Sprite / alpha。 - -### 7.4 时间推进 - -待设计: - -- 使用 `Update()` 基于 `Time.deltaTime` 推进。 -- 是否支持 unscaled time。 -- 是否支持手动 Evaluate,供编辑器预览和存档恢复使用。 - -## 8. 与现有项目系统的关系 - -### 8.1 Actor 系统 - -当前 `ActorAnima` 通过 `Animation/{ActorName}` 加载 AnimatorController,并通过状态名播放。 - -新系统需要考虑: - -- 是否新增 `ActorFrameAnima` 类型。 -- 是否替换 `ActorAnima` 内部实现。 -- 存档中 `stateName` / `stateNormalizedTime` 如何兼容。 -- Yarn 中现有 set_actor_state 等命令是否需要调整。 - -待讨论: - -- 第一批迁移对象是否选择角色立绘? -- 还是先选择孤立的场景物件 / UI 动画做试点? - -### 8.2 AnimatorCenter / AnimatorHandler - -当前通用动画调用以 `animatorName + stateName` 为核心。 - -新系统可以选择: - -1. 新增并行的 `FrameAnimationCenter`。 -2. 让 `AnimatorCenter` 逐步兼容新播放器。 -3. 保持两套系统并存,由 Yarn 命令区分调用。 - -待讨论: - -- 为减少 Yarn 改动,是否应尽量保持类似 API? -- 旧 Animator 动画是否长期保留? - -### 8.3 Yarn 命令 - -候选新命令: - -```yarn -<> -<> -<> +```csharp +FrameClipEndBehavior? endBehaviorOverride; ``` -待讨论: +规则: -- 是否复用现有 `play_animation` 命令? -- 命令是否等待播放完成? -- Loop Clip 的等待语义如何定义? +- 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 定义统一绝对时间轴。 -### 8.4 Timeline +### 7.3 播放状态与生命周期 -第一阶段不替代 Timeline。 +播放状态: -新系统只需要考虑: +```csharp +FrameAnimationPlaybackState +{ + Stopped, + Playing, + Paused +} -- Timeline 是否可以调用 FrameAnimationController。 -- Frame 动画是否需要在 Timeline 中被录制或控制。 +FrameAnimationStopMode +{ + HoldCurrentFrame, + Clear, + HideTarget +} +``` -### 8.5 存档系统 +播放规则: -需要定义快照语义: +- 每次成功 `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 同一时间最多有一个活动播放请求。 -- 当前 Clip 名称。 -- 当前播放时间或帧索引。 -- 当前播放状态:playing / paused / stopped。 -- 当前结束行为。 -- 是否保存队列。 +自然到达终点时,结束行为按以下优先级解析: -待讨论: +```text +节点 endBehaviorOverride +> Play 请求 endBehaviorOverride +> Flow endBehaviorOverride +> Clip defaultEndBehavior +``` -- 对角色立绘保存精确播放时间。 -- 对一次性演出只保存终态,避免读档后重复播放。 +结束行为语义: -## 9. 迁移计划 +- `HoldLastFrame`:保留最后一帧,播放状态变为 `Stopped`,请求以 `Completed` 完成。 +- `Loop`:当前终点 Clip 持续循环,不产生自然完成通知;直到被替换或主动停止后才结算请求。 +- `Clear`:自然完成后将目标 Sprite 设为 `null`,保持显示组件启用,请求以 `Completed` 完成。 +- `HideTarget`:自然完成后禁用显示组件,请求以 `Completed` 完成。 +- Flow 中间节点存在后继 Edge 且没有显式节点结束行为时,Clip 播放一次后立即进入后继节点;Clip 自身默认结束行为不阻止 Flow 推进。 +- Flow 节点切换时,在同一次时间求值中立即显示下一个 Clip 的第 0 帧。 -### 9.1 第一阶段:原型验证 +### 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 WaitAsync(); + void RegisterCompleted(Action callback); +} + +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`。 +- `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. 分阶段实施计划 + +### 8.1 第一阶段:原型验证 目标: @@ -519,83 +1304,81 @@ GetDuration(); - 编辑器中可直接预览 Clip。 - 修改来源后能刷新 Clip。 -### 9.2 第二阶段:AnimationSet 与运行时接入 +### 8.2 第二阶段:FrameAnimationGraph 与运行时播放 目标: -- 实现 AnimationSet。 -- 实现按名称播放 Clip。 -- 实现 AnimationSet 编辑器。 -- 在一个非核心场景物件上试点。 +- 实现 FrameAnimationGraph。 +- 实现 Graph 内 Clip、AnimationNode、AnimationEdge、AnimationFlow 的基础组织。 +- 实现按统一 playable id 播放 Clip 或 AnimationFlow。 +- 实现 FrameAnimationGraph 编辑器。 验收: -- 代码可通过名称播放 Set 内 Clip。 -- 编辑器可集中预览和检查所有 Clip。 +- 代码可通过统一 playable id 播放 Graph 内 Clip 或 AnimationFlow。 +- 编辑器可集中预览和检查 Graph 内 Clip、节点图与 AnimationFlow。 -### 9.3 第三阶段:Sequence 与演出工作流 +### 8.3 第三阶段:AnimationFlow 与演出工作流 目标: -- 实现简单线性 Sequence。 +- 实现最小节点式 AnimationFlow。 - 支持“播放一次 -> 进入 Idle loop”。 -- 提供 Sequence 预览。 +- 提供 AnimationFlow 预览。 验收: -- 策划可以不写代码配置基础演出序列。 +- 使用者可以不写代码配置并预览基础帧动画演出序列。 -### 9.4 第四阶段:Actor / Yarn 迁移 - -目标: - -- 选择一个角色或一组立绘动画试点。 -- 接入 Yarn 命令。 -- 接入存档恢复。 - -验收: - -- 角色动画可以通过新系统播放、保存、恢复。 -- 不破坏旧 Animator 动画。 - -## 10. 验收标准 +## 9. 验收标准 第一版完成时,应满足: -1. 能创建和保存 FrameClip 资产。 -2. 能在 Inspector 或 EditorWindow 中直接预览 Clip。 -3. 能从 PNG / JSON 生成或刷新 Clip。 -4. 能挂载播放器到 SpriteRenderer / Image 并播放 Clip。 -5. 能通过 AnimationSet 按名称播放 Clip。 -6. 能检查基础错误并给出明确提示。 -7. 刷新导入资源时,不破坏已存在 Clip 的引用。 +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 的引用。 -## 11. 待讨论问题清单 +## 10. 后续扩展 -优先级较高: +以下能力已明确不进入第一版,也不阻塞第一版实现: -1. `Library` 是否正式命名为 `AnimationSet`? -2. Clip 的循环/结束策略放在哪里最合适? -3. Clip 独立资产与 Set 子资产,哪种更适合项目工作流? -4. 第一阶段试点对象选角色立绘、场景物件,还是 UI 动画? -5. 是否保留现有 `AnimatorCenter` API 形状,降低 Yarn 迁移成本? -6. 存档是否需要保存动画播放中间态? +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 元数据的安全清理工具。 -优先级较低: +后续能力必须在保持第一版资产兼容的前提下单独补充需求与技术设计,不能通过改变现有字段语义隐式加入。 -1. 是否需要节点图式 Sequence 编辑器? -2. 是否需要帧事件? -3. 是否支持随机播放或条件分支? -4. 是否需要 Timeline 轨道扩展? +## 11. 已确定设计摘要 -## 12. 当前倾向 - -当前建议: +第一版已确定: 1. 第一版只做序列帧系统,不做通用动画系统。 -2. 命名采用 `FrameClip` / `AnimationSet` / `Sequence`。 +2. 命名采用 `FrameAnimationGraph` / `FrameClip` / `AnimationFlow`。 3. Clip 保存默认结束行为,但播放请求可以覆盖。 -4. 导入刷新以 Clip 名称为稳定匹配键。 -5. 第一版 Sequence 使用列表式编辑,不做节点图。 +4. 导入刷新以 `importSourceId + sourceTagName` 为稳定匹配键,不依赖当前 Clip id。 +5. FrameAnimationGraph 采用全局节点图编辑;AnimationFlow 是图中的命名演出流程。 6. 运行时底层直接按时间设置 Sprite,不使用 Unity Animator / Playables。 -7. 先新增并行系统,验证稳定后再讨论替换 ActorAnima / AnimatorCenter。 +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`;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 稳定。