swift 发布于 2023年09月28日
如果你恰巧也在学习 Cocos Creator 这篇内容可能至少帮你节省一天时间。
最近开发产品需要用到 Cocos Creator,分享一下遇到的问题。 如果你也正在准备使用 Cocos Creator,那这些问题大概率你也会遇到。可以收藏留作以后参考。
怎么说呢,国内不少团队开发的引擎类产品,本身做的都还算可以。 但是文档方面普遍投入的精力不足。我在了解 Cocos Creator 的过程中,也是因为文档问题消耗了不少精力,为了让后面也想要了解 Cocos Creator 的朋友不再走同样的弯路,把我遇到的问题和解决过程和大家做一个分享。
Cocos 动画系统动画系统基本是游戏引擎一个核心的内容, Cocos Creator 自然也完整的支持动画能力。 但是一个最大的问题就是文档跟不上。 我在写这篇内容时候,使用的是 Cocos Creator 3.7.1 版本,也就是最新版。
Cocos Creator 开发了一套使用 UI 工具来创建动画的功能,并且在文档中更多篇幅介绍的也是通过它的 UI 工具来创建动画:
但是我更需要了解的是如何通过代码来创建和使用动画。 这方面的内容虽然也有介绍,但是比较零散。 这个是 Cocos Creator 关于代码创建动画的文档:
https://docs.cocos.com/creator/manual/zh/animation/use-animation-curve.html
里面确实给出了如何创建 AnimationClip 的代码示例, 但这不是一个完整可运行的代码示例,只是一个片段,而且你需要联想并且反复查看引擎源代码才能大体上搞明白,下面就是官方文档中的代码示例:
const animationClip = new AnimationClip();
animationClip.duration = 1.0; // 整个动画剪辑的周期
const track = new animation.VectorTrack(); // 创建一个向量轨道
track.componentsCount = 3; // 使用向量轨道的前三条通道
track.path = new animation.TrackPath().toHierarchy('Foo').toProperty('position'); // 指定轨道路径,即指定目标对象为 "Foo" 子节点的 "position" 属性
const [x, y, z] = track.channels(); // x, y, z 是前三条通道
x.curve.assignSorted([ // 为 x 通道的曲线添加关键帧
[0.4, ({ value: 0.4 })],
[0.6, ({ value: 0.6 })],
[0.8, ({ value: 0.8 })],
]);
// 如果关键帧的组织是 [时间, 向量] 数组,可以利用解构语法赋值每一条通道曲线。
const vec3KeyFrames = [
[0.4, new Vec3(1.0, 2.0, 3.0)],
[0.6, new Vec3(1.0, 2.0, 3.0)],
[0.8, new Vec3(1.0, 2.0, 3.0)],
] as [number, Vec3][];
x.curve.assignSorted(vec3KeyFrames.map(([time, vec3]) => [time, { value: vec3.x }]));
y.curve.assignSorted(vec3KeyFrames.map(([time, vec3]) => [time, { value: vec3.y }]));
z.curve.assignSorted(vec3KeyFrames.map(([time, vec3]) => [time, { value: vec3.z }]));
// 最后将轨道添加到动画剪辑以应用
animationClip.addTrack(track);
如果想进一步了解这个文档,大家可以查看上面的文档链接。下面我把我对 Cocos 动画系统的了解以及文档中没有提到的细节和大家聊一下。
动画系统关键类Cocos Creator 3.7.1 的动画系统是通过几个关键类相互作用来实现动画效果的。主要有 AnimationClip,Animation 以及 Track。首先在这之前,你需要知道 Cocos Creator 3.x 的 Node, 也就是节点系统。
Node 和 Component 是整个引擎的基础概念, 比如你在 UI 上面的按钮,游戏中的每个 Sprite 等等。 我们要实现动画效果,也都是对 Node 做操作, 比如它的位移动画,变形动画等等。我们这篇主要讲的是动画系统,这个前置的概念这里不多介绍,但你是要了解这个概念的。
比如说,我们在游戏界面上有一个 Node, 要对它实现位移动画,就首先要为它添加 Animation 组件:
newNode.layer = Layers.Enum.UI_2D;
this.addChild(newNode);
let sprite = newNode.addComponent(Sprite);
sprite.spriteFrame = ...;
let anim = newNode.addComponent(Animation);
简单说一下上面代码, 首先 let newNode = new Node('mynode');
创建一个新的 Node, 然后设置了他的 layer 属性, 最后添加到当前节点的子节点。
- 设置 node 的 layer 属性是必须的, 如果你不设置,你的 Node 可能会无法正常显示。(这个我也踩过坑,搜索了很久才搞明白。)
然后, let sprite = newNode.addComponent(Sprite);
为当前节点添加了一个 Sprite 组件,也是游戏开发的常用概念,代表一个屏幕上可见的物体。
关于 Node 操作的内容,不多过多介绍。 接着就我们要为这个 Node 添加动画,也是我们这篇内容的主题。我们通过 let anim = newNode.addComponent(Animation);
这行代码,为 Node 添加了一个 Animation 组件。
从这里可以看出, Animation 和 Sprite 一样, 在 Cocos 中都是属于 Node 的一个组件。也是通过这个方法,才能将动画和指定的节点关联起来。 这个官方文档中目前没有明确的说明。
有了 Animation 实例, 我们还需要为它添加 AnimationClip, 一个 Animation 实例可以包含多个 AnimationClip:
animationClip.duration = 1.0;
总的来说,你可以理解为 Cocos 中 Animation 是一个 Node 上面所有动画的集合,AnimationClip 代表每一个单独的动画。比如把 Node 移动一段距离,或者旋转 Node。
我们上面创建的 AnimationClip 只指定了它的运行时间:animationClip.duration = 1.0;
也就是这个动画持续1秒钟。 我们还需要说明这个动画实际要干什么, 这就引入了 Track 的概念:
track.componentsCount = 3;
track.path = new animation.TrackPath().toProperty("position");
const [x, y, z] = track.channels();
const vec3KeyFrames = [
[0, new Vec3(newNode.position.x, 100, 0)],
[1.0, new Vec3(newNode.position.x, 500, 0)]
] as [number, Vec3][];
x.curve.assignSorted(vec2KeyFrames.map(([time, vec3]) => [time, { value: vec3.x}]));
y.curve.assignSorted(vec2KeyFrames.map(([time, vec3]) => [time, { value: vec3.y}]));
z.curve.assignSorted(vec2KeyFrames.map(([time, vec3]) => [time, { value: vec3.z}]));
上面代码看起来稍微复杂,我们一步步介绍。 Track 在 Cocos 中是一个叫做动画轨道的概念,其实你可以把它理解为就是我们的动画要做什么,比如位移动画什么时候移动到什么坐标上。
上面代码中就是一个位移动画,首先我们创建一个 VectorTrack
他是 Track 的其中一个子类,用于表示向量轨道,适用于我们要表示的位移动画。紧接着调用:
这个指定了我们要使用的向量轨道数量。 其实这个 API 对于初学者并不好理解。 VectorTrack 最多支持 4 个变量, 这个 componentsCount
表示我们要使用多少个变量,一般情况用 3 即可。 也就是 x,y,z 坐标。
接下来我们确定 Track 的路(path):
这也是一个对初学者不太友好的名词, 其实就是这些变量的变化,应用到哪个属性上。比如我们这里要实现一个位移动画,那肯定会把动画变化作用到 position 属性上。TrackPath 的一系列语法,其实就是告诉引擎我们怎么找到要进行动画变化的属性。后面关于这个 path 的细节,我们还会再详细说一下。
然后我们又获取了每一个通道:
这个通道其实又有一些迷惑性, 我们前面指定了 componentsCount
是 3 对吧。 也就是 track.channels() 会返回给我们 3 个叫做通道的东西。 所谓通道,就是我们可以在它上面指定随着时间变化的值的变化。再往下看:
[0, new Vec3(newNode.position.x, 100, 0)],
[1.0, new Vec3(newNode.position.x, 500, 0)]
] as [number, Vec3][];
x.curve.assignSorted(vec2KeyFrames.map(([time, vec3]) => [time, { value: vec3.x}]));
y.curve.assignSorted(vec2KeyFrames.map(([time, vec3]) => [time, { value: vec3.y}]));
z.curve.assignSorted(vec2KeyFrames.map(([time, vec3]) => [time, { value: vec3.z}]));
这里 vec3KeyFrames 其实就是动画的关键帧,我们这里有两个关键帧 [0, new Vec3(newNode.position.x, 100, 0)] 和 [1.0, new Vec3(newNode.position.x, 500, 0)]。
我翻译成好理解的格式大概就是这样:
- 0秒 -> 坐标(newNode.position.x, 100, 0)
- 1.0秒 -> 坐标(newNode.position.x, 500, 0)
其实就是一个动画,描述了 newNode 的 y 坐标从 100 到 500 的一个动画。 下面的三行:
y.curve.assignSorted(vec2KeyFrames.map(([time, vec3]) => [time, { value: vec3.y}]));
z.curve.assignSorted(vec2KeyFrames.map(([time, vec3]) => [time, { value: vec3.z}]));
其实就是将我们前面定义的关键帧, 应用到 x,y,z 每个通道上。最后把这个 Track 添加到我们之前创建的 AnimationClip 上:
不知道我这样解释一番,是否说清楚了。 我的感受就是 Cocos 的这种设计是考虑到一些比较复杂的需求情况。但是对于初学者,比如我仅仅需要实现一个坐标位移的动画,这套 API 的学习成本就比较大了。 下面把一些官方文档中没有,但是比较重要的发现和大家分享。
至少两个关键帧我们至少要指定两个关键帧, 也就是动画的初始状态和结束状态。 就以我们上面的位移动画为例,如果我只说明了动画的结束状态,也就是:
- 1.0秒 -> 坐标(newNode.position.x, 500, 0)
那么这个动画播放后会直接跳转到最终状态,不会有任何动画效果, 因为我们没有指定起始状态,也就是:
- 0秒 -> 坐标(newNode.position.x, 100, 0)
在 cocos 中一个可运行的动画, 必须至少指定两个关键帧,才会有动画效果。 这可能和我们以前的一些开发习惯并不一样。
轨道路径是从 Animation 所添加的 Node 开始查询官方文档上说了轨道的路径查询是从当前
节点开始, 那么这个当前到底是哪个节点,文档上并没有说明,经过实践调试,是这样:
//...
let anim = newNode.addComponent(Animation);
const animationClip = new AnimationClip();
const track = new animation.VectorTrack();
track.path = new animation.TrackPath().toProperty("position");
animationClip.addTrack(track);
anim.defaultClip = animationClip;
这里的相互从属关系大概是这样:
- Node -> Animation -> AnimationClip -> VectorTrack
我们这里的 VectorTrack 属于前面创建的 AnimationClip, AnimationClip 又属于 Node 上面的 Animation 组件。
那么这时候,我们调用 TrackPath() 查找路径,就是从 Animation 组件所属的 Node 开始的。 也就是上面代码的 newNode 实例。 所以我们只需要调用 .toProperty("position"); 查找一层属性即可。
这个路径查找,对应的就是 newNode 的 position 属性。 简单来说就是这个 Track 属于哪个 Animation 组件,那么它的 path 查找就从这个 Animation 组件所属的 Node 开始。
这个说明在官方文档中没有,如果大家遇到疑惑可以参考一下。
2D 游戏的通道数量还记得前面提到的 track.componentsCount = 3;
指定轨道数量吗。 因为我需要的其实只是 2D 场景动画, 我刚开始就试着把它设置成 2. 然后用 Vec2 二维向量来指定关键帧。 但是发现不行,动画直接失效。 所以目前我的认知, 即使是 2D 动画,也要使用 3 个通道。
这个也是官方文档上缺失的一块, 一个完整的在 Node 上实现动画的代码示例:
let newNode = new Node(nodeName);
//... 省去Node 配置的其他细节
// 创建 Animation 组件
let anim = newNode.addComponent(Animation);
// 创建 AnimationClip
const animationClip = new AnimationClip();
// 动画时间
animationClip.duration = 1.0;
// 动画轨道, 前面详细讲解过
const track = new animation.VectorTrack();
track.path = new animation.TrackPath().toProperty("position");
track.componentsCount = 3;
const [x, y, z] = track.channels();
// 关键帧
const vec3KeyFrames = [
[0, new Vec3(newNode.position.x, this.blockWidth * item.row + initYOffset, 0)],
[1.0, new Vec3(newNode.position.x, this.blockWidth * item.row, 0)]
] as [number, Vec3][];
// 应用关键帧
x.curve.assignSorted(vec3KeyFrames.map(([time, vec3]) => [time, { value: vec3.x}]));
y.curve.assignSorted(vec3KeyFrames.map(([time, vec3]) => [time, { value: vec3.y}]));
z.curve.assignSorted(vec3KeyFrames.map(([time, vec3]) => [time, { value: vec3.z}]));
// 将轨道添加到 animationClip 上。
animationClip.addTrack(track);
// 将 animationClip 添加到前面创建的 Animation 组件上
// anim.addClip(animationClip);
// 本来是应该用 addClip 方法,但是这个方法目前存在bug,只能暂用 defaultClip 属性来设置。
anim.defaultClip = animationClip;
// 播放动画
anim.play();
以上就是完整的通过代码实现动画的流程,注意到我们上面代码注释了一段 anim.addClip 方法的使用。 是因为这个方法目前存在 bug, 下面会说。
Animation.addClip 的 bug说到前面注释掉的代码:
这也是被误导好长时间的一个地方。 本来检查代码,发现所有地方都没问题,但是动画就是不能正常运行。 然后仔细调试发现,animationClip 并没有被添加进来。 可是明明调用了 addClip 方法,为什么不行呢。
然后翻阅了一下 Cocos 的源代码才发现问题,这是 github上 addClip 最新的代码:
if (js.array.contains(this._clips, clip)) {
this._clips.push(clip);
}
return this.createState(clip, name);
}
这是以前的:
if (!ArrayUtils.contains(this._clips, clip)) {
this._clips.push(clip);
}
return this.createState(clip, name);
}
如果我理解没错的话, 这段新代码的判断 if (js.array.contains(this._clips, clip)) {
永远不会执行, 因为它判断的是当前集合中包含我们要添加的 clip 才会执行添加操作。 事实上这个判断逻辑反了。
反正已经将这个问题反馈给引擎团队了,目前找到的补救方法就是通过 anim.defaultClip = animationClip;
这个属性设置来完成。
也许是 cocos 的引擎更多考虑的是比较复杂的项目需求,反倒这样一个简单的位移动画实现起来变得很麻烦。而且因为缺少更多文档的支持,学习成本就变得很高。这里也是把我在这过程中遇到的坑和文档中没有说明,通过我自己实际摸索出来的内容和大家做一个分享。 希望对你有价值。
如果你觉得这篇文章有帮助,还可以关注微信公众号 swift-cafe,会有更多我的原创内容分享给你~
本站文章均为原创内容,如需转载请注明出处,谢谢。
![]() 发现更多精彩 swift-cafe |