Files

214 lines
9.0 KiB
Markdown
Raw Permalink Normal View History

2026-06-06 11:33:19 +08:00
# CC 3.8.x AnimationClip (.anim) 文件结构速查表
> 目标读者:读写 .anim 文件的 AI / 工具开发者。
> .anim 和 .prefab 都是「JSON 数组 + `__id__` 交叉引用」的同构格式,parse/write 复用 [prefab-schema.md](./prefab-schema.md) 里描述的规则;本文只补 .anim 独有的对象类型与字段。
> 样本来源:`assets/packages/module/game/merge/effect/component/prefab/Skwjquchuquchu.anim`。
---
## 1. 整体结构
```
objects[0] = cc.AnimationClip ← 文件头(引用 _tracks / _embeddedPlayers / _additiveSettings
objects[1..N-3] = Tracks + TrackPaths + Channels + Curves + HierarchyPath/ComponentPath
objects[N-2] = cc.AnimationClipAdditiveSettings
objects[N-1] = (可选)EmbeddedPlayer / EmbeddedAnimationClipPlayable
```
依赖链:
```
AnimationClip
└─ _tracks[] → Track
├─ _binding.path → TrackPath
│ └─ _paths[] → HierarchyPath / ComponentPath / string(propName)
└─ _channel / _channels → Channel
└─ _curve → RealCurve / ObjectCurve
└─ _times[] / _values[](对齐等长)
```
---
## 2. Track 类型与字段(**最容易踩的坑**)
| `__type__` | 通道数 | 字段名 | 额外字段 | 典型属性 |
|---|---|---|---|---|
| `cc.animation.RealTrack` | 1 | **`_channel`**(单数) | — | `opacity`, `active`, 单个标量属性 |
| `cc.animation.ObjectTrack` | 1 | **`_channel`**(单数) | — | `spriteFrame`, 资产引用 |
| `cc.animation.VectorTrack` | 2/3/4 | **`_channels`**(复数数组) | `_nComponents` | `position`, `scale`, `eulerAngles` |
| `cc.animation.ColorTrack` | 4 | **`_channels`**(复数数组) | — | `color`r/g/b/a |
### ⚠️ 历史踩过的坑
**给 `RealTrack` 写 `_channels: [ref(idx)]`(复数数组)而不是 `_channel: ref(idx)`(单数)。**
CC3 编辑器按 schema 验证 .anim 文件,RealTrack / ObjectTrack 的通道字段必须是 `_channel`(单数、单对象)。字段名写错会:
- **编辑器里看不到这条轨道的关键帧**(属性列表不显示)
- **运行时不播**(track 被忽略)
- **文件能写进去、也不报错**(CC3 对未知/缺失字段静默忽略)
排查路径:打开 .anim 看节点 UIOpacity.opacity / spriteFrame 这类单值属性没显示时,第一件事是检查 `_channel` vs `_channels`
**正确写法请用 [cli/src/anim-primitives.js](../cli/src/anim-primitives.js) 的 `makeRealTrack` / `makeObjectTrack` / `makeVectorTrack` / `makeColorTrack` 工厂函数**,绑定了正确的字段名,编译期避免拼错。
---
## 3. 曲线与关键帧
### cc.RealCurve(浮点曲线)
```json
{
"__type__": "cc.RealCurve",
"_times": [0, 0.1, 0.4666...],
"_values": [{RealKeyframeValue}, ...],
"preExtrapolation": 1,
"postExtrapolation": 1
}
```
- `_times` 递增秒数,和 `_values` 等长
- `preExtrapolation` / `postExtrapolation``0=LINEAR / 1=CLAMP / 2=REPEAT / 3=PINGPONG`,最常用 `1`
### cc.RealKeyframeValue
```json
{
"__type__": "cc.RealKeyframeValue",
"interpolationMode": 1,
"tangentWeightMode": 0,
"value": 255,
"rightTangent": 0, "rightTangentWeight": 1,
"leftTangent": 0, "leftTangentWeight": 1,
"easingMethod": 0,
"__editorExtras__": null
}
```
- `interpolationMode`**`0=LINEAR / 1=CONSTANT / 2=CUBIC`**
- LINEAR:两关键帧之间平滑线性过渡(视觉上看到淡入淡出)
- CONSTANT:保持当前值到下一帧瞬变(FGUI 非 tween item 语义)
- CUBIC:带缓动曲线(`easingMethod``cc.EasingMethod` 枚举)
### cc.ObjectCurve(对象引用曲线,如 spriteFrame 序列)
和 RealCurve 结构一样,`_values` 换成 `{ __uuid__, __expectedType__ }` 资产引用。
---
## 4. TrackPath 路径寻址
TrackPath 定位"哪个节点 → 哪个组件 → 哪个属性":
```json
{
"__type__": "cc.animation.TrackPath",
"_paths": [
{ "__id__": hierIdx }, // HierarchyPath: { path: "n4" }
{ "__id__": compIdx }, // ComponentPath: { component: "cc.UIOpacity" }
"opacity" // 属性名字符串
]
}
```
规则:
- **根节点自身**`path === ""`):省略 HierarchyPath,直接 `[ComponentPath, propName]``[propName]`Node 本身的属性如 position
- **Node 本身属性**`position` / `scale` / `eulerAngles` / `active`):不需要 ComponentPath`[HierarchyPath, propName]`
- **组件属性**`UIOpacity.opacity` / `Sprite.color` / `Sprite.spriteFrame`):需要 ComponentPath`[HierarchyPath, ComponentPath, propName]`
---
## 5. EmbeddedPlayermovieclip 子动画触发)
FGUI 的 movieclip 节点在 CC3 里用「主 clip 挂 EmbeddedPlayer + 目标节点自己的 cc.Animation 播子 clip」这种双层结构表达。
```
主 AnimationClip
└─ _embeddedPlayers[] → cc.animation.EmbeddedPlayer
├─ begin, end ← 主 clip 时间线上的起止秒
└─ playable → EmbeddedAnimationClipPlayable
├─ path = "n4" ← 目标节点(从主 clip 挂载节点算相对路径)
└─ clip = UUID ← 子 clip 资产 UUID
```
### ⚠️ 子 clip 的关键帧时序
子 clip 的 `t=0` **必须对应 `EmbeddedPlayer.begin` 那一刻**,不能把主 clip 的绝对时间当子 clip 的时间来写。
举例:FGUI 在 frame 30.1s)触发 movieclip 播放,子 clip 20 帧。
- ❌ 错:子 clip keyframes `[t=0, t=0.1, t=0.133, ...]`(t=0 是静态占位、t=0.1 才是播放命令)→ EmbeddedPlayer begin=0.1 时,子 clip 从自己的 t=0 开始,frame 0 会显示 0.1 秒才切 frame 1
- ✅ 对:子 clip keyframes `[t=0, t=0.033, t=0.067, ...]`t=0 直接是第一帧),sub-clip `_duration = transitionDuration - begin`
### ⚠️ 主 clip / 子 clip 职责划分
**主 clip 做所有"业务逻辑"动画**fade in/out、rotation、scale、position 等),**子 clip 只做 spriteFrame 逐帧切换**。原因:
- 节点的 cc.Animation 播子 clip 时,子 clip 的 TrackPath 写空 HierarchyPath 直接驱动节点本身;主 clip 通过 HierarchyPath 也能驱动该节点其他属性
- 两边同时驱动同一属性会产生混合(blending)冲突
- 把业务轨道留在主 clip,子 clip 只管图片切换,最干净
---
## 6. 初始值关键帧
**非 tween 轨道的首个关键帧若不在 `t=0`,必须额外补一帧 `t=0` 表示初始值**。否则 `preExtrapolation=CLAMP` 会把首帧值倒推到 `-∞`,产生视觉异常。
典型例子:Rotation 只在 `t=0.1``Z=269°`,要在 `t=0``Z=0`,否则 clip 一开始节点就已经是旋转 269°。
Alpha 通常 FGUI 自己会写 `time=0, value=0`,所以 opacity 轨道天然有初始帧;Rotation/Scale 常常缺首帧,需要转换器主动补。
---
## 7. 用 anim-primitives 构建完整 .anim
最小示例:给 "n4" 节点 `cc.UIOpacity.opacity` 加一条 `[0→0, 0.1→255, 0.467→0]` 的 CONSTANT 轨道。
```js
const { parsePrefab, writePrefab, anim, ref } = require('./cli/src');
const {
makeHierarchyPath, makeComponentPath, makeTrackPath,
makeRealKeyframe, makeRealCurve, makeChannel, makeRealTrack,
makeAdditiveSettings, makeAnimationClip,
InterpolationMode, hashString,
} = anim;
const objects = [];
objects.push(null); // [0] 占位 AnimationClip
const hierIdx = objects.length; objects.push(makeHierarchyPath('n4')); // [1]
const compIdx = objects.length; objects.push(makeComponentPath('cc.UIOpacity')); // [2]
const pathIdx = objects.length;
objects.push(makeTrackPath([ref(hierIdx), ref(compIdx), 'opacity'])); // [3]
const curveIdx = objects.length;
objects.push(makeRealCurve({
times: [0, 0.1, 0.4667],
values: [
makeRealKeyframe({ value: 0, interpolationMode: InterpolationMode.CONSTANT }),
makeRealKeyframe({ value: 255, interpolationMode: InterpolationMode.CONSTANT }),
makeRealKeyframe({ value: 0, interpolationMode: InterpolationMode.CONSTANT }),
],
})); // [4]
const chIdx = objects.length; objects.push(makeChannel(curveIdx)); // [5]
const trackIdx = objects.length; objects.push(makeRealTrack(pathIdx, chIdx)); // [6]
const additiveIdx = objects.length; objects.push(makeAdditiveSettings()); // [7]
objects[0] = makeAnimationClip({
name: 'demo',
sample: 30,
duration: 0.7333,
hash: hashString('demo'),
trackIndices: [trackIdx],
additiveIdx,
});
require('fs').writeFileSync('demo.anim', JSON.stringify(objects, null, 2));
```
**关键点**`makeRealTrack` 自动生成 `_channel`(单数)字段,保证编辑器能识别;改用 `makeVectorTrack` / `makeColorTrack` 自动生成 `_channels`(复数数组)+ 必要的 `_nComponents`