大家好,今天我们来聊一聊关于特效动画的播放机制。满满都是干货,赶紧拿起笔记来划重点吧!

一. 简介

动画在2D游戏里用得十分广泛, 根据这些动画的特点,我们可以大概归为3类:

1. 粒子动画

这种动画是由几百甚至上千个粒子构成, 所有粒子都共享一个纹理, 这些粒子都是从一个发射器发出, 加以一定的随机因素, 在不同发射速度和重力等外力作用下,每个粒子呈现不一样的运动状态, 大量粒子可以组合成各种各样不一样的效果, 比如烟花, 火焰。粒子动画的实现一般都会使用批次渲染和对象池来保证性能。

2. 骨骼动画

这种动画通常用于表现有多个动作的角色,它通常是由骨骼(bone)和绑定在骨骼上的蒙皮(skin/mesh)构成。动画师通常在spine(2d)或者3dmax等工具里面对骨骼动作进行设计, 同时对蒙皮进行编辑。

3. 特效动画

特效动画不需要或者难以使用骨骼进行表达, 比如一个刀光效果或者一闪一闪的星星, 我们可以使用最原始的实现方式, 对动画的每一帧都画一张图片, 依次连续展示这些图片就可以达到动画效果。

但是这种方法实现的动画过于浪费空间和内存。 其中有非常多的特效我们可以通过关键帧动画的方式来实现,常使用Flash工具进行关键帧动画的设计。

本文中下面只讨论关键帧动画的实现。

二. 关键帧动画介绍

1. 动画举例

我们先来看下面这样一个动画:

iOS 开发 粒子动画 粒子动画是什么意思_android

动画设计师进行编辑的时候, 是这样的:

 

iOS 开发 粒子动画 粒子动画是什么意思_动画_02

 设计师把动画分成了4层,每一层里带有黑色小点的就是关键帧。

  • 底座: 这一层就只有一个关键帧, 放入了一个静态的底座图片
  • 铁锤: 这一层就放了一个铁锤, 铁锤在每个关键帧里都具备不同的位置和角度, 在动画播放过程中, 在2个关键帧之间的铁锤的位置和角度, 自动进行插值运算, 这个地方一般使用线性插值, 也可以使用更复杂的贝塞尔曲线插值.
  • 火花2: 前面几帧是空白的,到后面铁锤敲打在底座上时, 会在后面几帧产生火花, 由于这几帧火花使用的都是不同的图片, 而且间隔最多1-2帧,所以这个地方不需要进行插值运算
  • 火花1: 同火花2

2. 关键帧动画的好处

从上面的一个动画分析,我们可以看到关键帧动画的好处:

  • 节省了资源
  • 动画分层设计, 逻辑清晰

3. 关键帧动画的适用范围

我们可以看到 2个关键帧之间, 元件可以对下面的几种属性进行插值计算从而实现动画的平滑过渡:

  • 位置(x,y)
  • 旋转和倾斜(rotation/skew)
  • 缩放(scale)
  • 透明度(alpha)
  • 颜色(color-rgb)

如果我们要做的动画不在上面说的这几种范围内(比如对元件进行Z轴翻转), 那么就不适合使用关键帧动画.

三. 播放机制的实现

1. 特效结构图

从flash编辑器里的动画分层图, 我们可以直接脑补出以下这张结构图:

 

iOS 开发 粒子动画 粒子动画是什么意思_android_03

2. 播放步骤

1) 创建4层空的容器层

2) 一帧一帧往后解析, 对于每一个容器层:

1.容器层当前为空时, 如果遇到关键帧则创建该关键帧对应图片放入

2.容器层当前不为空, 预先判断下一个关键帧内容:

  • 如果下一个关键帧是对本帧图片进行了属性修改(5种属性), 那么根据当前帧位置进行插值计算, 修改本帧图片的属性
  • 如果下一个关键帧是只是更换成另外一张图片,那么本帧保持不变直到播放到下一个关键帧时替换图片
  • 如果当前帧遇到空白帧, 则删除容器里的所有内容

3. Cocos2d-x的实现

对于容器层我们不需要创建实际的显示节点, 我们可以画出一个特效动画在某一瞬间的显示树结构:

 

iOS 开发 粒子动画 粒子动画是什么意思_iOS 开发 粒子动画_04

四. 性能优化

1. 使用纹理集textureAtlas

我们可以把以上例子中使用到的散图, 整合到一张大图上(sprite sheet), 减少多次的io读文件, 让动画播放更加流畅, 也为下一步的批次渲染优化打下基础。

2. 尽可能批次渲染

我们知道在opengl进行绘图的时候, 如果我们几个图形都有一样的显示状态( 纹理, shader及其uniform参数, blend方式), 那么我们通过一次draw就可以同时画出这几个图形。

在Cocos2d-xv3.x版本里, 底层会自动做判断合并多次draw为一次批次渲染, 而在v2.x里, 我们需要自己实现, 一个小成本的做法就是, 当判断可以批次渲染的时候, 在原本Node.addChild(sprite)的地方, 给改成 batchNode.addChild(sprite)即可。

渲染树结构如下:

 

iOS 开发 粒子动画 粒子动画是什么意思_动画_05

3. 合理使用对象池

如果特效是长时间的不断的循环播放, 那么我们在remove元件的时候, 最好不要马上销毁, 可以把它放入一个对象池里, 需要使用的时候,重新初始化元件拿出来使用就可以了。

五. 功能扩展: 嵌套子特效

为了节省资源, 动画设计师可能会在某一层里放入以前做过的另外一个特效, 我们可以简单的调整代码就可以做到嵌套播放, 播放时的一个渲染树结构如下:

 

iOS 开发 粒子动画 粒子动画是什么意思_android_06

 由于子特效很可能使用跟父特效不一样的纹理, 如果我们仍旧想使用批次渲染,

我们有2种做法:

  • A. 动态合并特效纹理: 除非我们有太多的draw call需要合并, 不然动态合并纹理的开销明显不合算
  • B. 尽可能把使用相同纹理的相邻层(个数超过1个才有合并的意义)进行批次渲染。我们选择B做法,那么调整过后, 渲染树结构如下:

 

iOS 开发 粒子动画 粒子动画是什么意思_动画_07

六. 功能扩展: 动态子元件

考虑如下场景:

动画师设计了个抽卡动画特效, 他在设计的时候, 卡牌是画死的, 但是我们在游戏里使用这个特效的时候, 需要这个卡牌可以动态替换成我们要的卡牌。

要做到这个功能也不麻烦, 需要:

在导出特效的时候, 需要剔除掉这个画死的卡牌图, 免得浪费资源, 同时对这个资源做一个标记, 表示这个元件需要外部创建。

在实现播放特效的代码里, 在创建元件的地方(通常我们会使用工厂模式来实现), 发现某元件是需要外部创建的, 那么调用之前埋入的外部创建器进行元件生成。

七. 功能扩展: 遮罩实现

考虑以下的动画效果:

 

iOS 开发 粒子动画 粒子动画是什么意思_android_08

动画分2层, 下面一层是背景图层, 上面一层是一个圆形遮罩, 圆形遮罩会做一个从左到右的移动, 而只有在圆形覆盖下的背景区域才会显示出来.

 

iOS 开发 粒子动画 粒子动画是什么意思_动画_09

opengl渲染管线里, 在fragment shader之后, 写入frame buffer之前, 可以进行stencil test, 它可以剔除不要的像素, 依据是stencil buffer里对应的取值(1或者0).

对应这个功能,cocos2d-x里有一个clippingNode类, 我们可以设置它的模板(stencil), 那么它里面的子节点, 只有stencil覆盖范围内, 才会被渲染出来, 这就可以实现我们的遮罩功能了。

加入ClippingNode之后我们的渲染树如下:

 

iOS 开发 粒子动画 粒子动画是什么意思_动画_10

到这里为止我们已经基本了解动画特效的实现原理。下周我们会对上面提到的5种属性做更深入的介绍。

如果你看得不过瘾,或是还有其他问题,欢迎在Cocos论坛里发帖,我们都会及时回复哦。