前言
帧动画是以序列帧轮放的方式来表现一个动画,就像胶片电影一样,一张张画面进行切换,当切换的间隔足够小时,人眼就看不出中间的间隔,而是一个流畅的视频。cocos2d-x 中的帧动画涉及到三个类 AnimationFrame,Animation 和 Animate。AnimationFrame 是对精灵帧 SpriteFrame 的再次封装,保存了一帧画面的信息;Animation 是一个动画的数据集合,保存了所有动画帧及其它数据;Animate 是在 Animation 上封装的一个动作,播放动画时是精灵执行这个动作的过程。
AnimationFrame
AnimationFrame 是动画帧,它只是对 SpriteFrame 的简单封装,它定义三个属性
SpriteFrame *_spriteFrame;
float _delayUnits;
ValueMap _userInfo;
_delayUnits 姑且称之为延迟单元数,表示这一帧画面将持续多少单元时间,一般给它赋值 1,表示一个单元时间,一个单元时间指每一帧的间隔时间;_userInfo 表示这一帧画面显示时要广播的数据,这个属性暂时没用到,传一个空的字典 ValueMap 就行。
AnimationFrame 最重要的函数是 initWithSpriteFrame,用于设置上面那三个属性的值
bool AnimationFrame::initWithSpriteFrame(SpriteFrame* spriteFrame, float delayUnits, const ValueMap& userInfo)
{
setSpriteFrame(spriteFrame);
setDelayUnits(delayUnits);
setUserInfo(userInfo);
return true;
}
Animation
Animation 是一个动画数据类,它定义了下面六个属性
float _totalDelayUnits;
float _delayPerUnit;
float _duration;
Vector<AnimationFrame *> _frames;
bool _restoreOriginalFrame;
unsigned int _loops;
这六个属性中比较重要的三个属性是 _frames,_delayPerUnit 和 _loops;_frames 是动画帧集合,这是一个 Vector,保存了所有的 AnimateFrame;_delayPerUnit 是一帧画面的间隔时间,也就是上面提到的一个单元时间;_loops 是动画播放时的循环次数。
另外三个属性并不是不重要,而是一般不需要调用者关心而已;_restoreOriginalFrame 表示动画播放完是否恢复到第一帧的画面,默认是 false,可以通过接口来修改它的值;另外两个属性一般不需要调用者去设置或读取它的值,_totalDelayUnits 是指动画一共需要多少个单元时间,这个数是添加动画帧的时候自动计算的,前面说到动画帧有一个属性延迟单元个数 _delayUnits 表示这个动画帧需要的单元时间个数,把所有动画帧的延迟单元个数加起来就是这个动画需要的总延迟单元个数,动画帧的延迟单元个数一般为 1,所以动画的总延迟单元个数一般等于动画帧的个数;_duration 是指动画持续的时间,这个时间的计算也很简单,由总延迟单元个数乘以一个单元时间即可。
Animation 最重要的函数是初始化函数,这个函数设置三个对外属性 动画帧、单元时间 和 循环次数 的值,然后通过遍历动画帧计算出 总延迟单元个数。
bool Animation::initWithAnimationFrames(const Vector<AnimationFrame*>& arrayOfAnimationFrames, float delayPerUnit, unsigned int loops)
{
_delayPerUnit = delayPerUnit;
_loops = loops;
setFrames(arrayOfAnimationFrames);
for (auto& animFrame : _frames)
{
_totalDelayUnits += animFrame->getDelayUnits();
}
return true;
}
获取动画持续时间 _duration 的时候通过 总延迟单元个数 和 单元时间 计算出总持续时间,所以,其实上面的 _duration 定义之后并没有使用,cocos2d-x 的源代码质量其实并不高~
float Animation::getDuration(void) const
{
return _totalDelayUnits * _delayPerUnit;
}
还有一个初始化函数是直接传精灵帧集合进来,然后根据精灵帧创建动画帧保存下来,同样也会计算 总延迟单元个数 _totalDelayUnits
bool Animation::initWithSpriteFrames(const Vector<SpriteFrame*>& frames, float delay/* = 0.0f*/, unsigned int loops/* = 1*/)
{
_delayPerUnit = delay;
_loops = loops;
for (auto& spriteFrame : frames)
{
auto animFrame = AnimationFrame::create(spriteFrame, 1, ValueMap());
_frames.pushBack(animFrame);
_totalDelayUnits++;
}
return true;
}
上面两个初始化函数都是一次性初始化动画帧数据,还有一个函数是一次添加一帧数据;这个函数平时用得比较多,不用去创建一个 Vector,也不用等创建完所有动画帧或精灵帧时再初始化动画,可以每得到一个精灵帧就添加进来;另外,上面两个批量初始化函数导出的 lua 函数好像有问题,所以我一般都是用这个函数来设置动画数据
void Animation::addSpriteFrame(SpriteFrame* spriteFrame)
{
AnimationFrame *animFrame = AnimationFrame::create(spriteFrame, 1.0f, ValueMap());
_frames.pushBack(animFrame);
// update duration
_totalDelayUnits++;
}
Animate
Animation 只是一个数据类,它包含了一个帧动画所需的所有必要数据,但它不是一个可执行的动作,真正用于执行帧动画的动作是 Animate。cocos2d-x 中的所有动作都有一个共同的基类 Action, Action 下还有一个子类 FiniteTimeAction,表示有限时间内完成的动作;FiniteTimeAction 下有两个子类 ActionInstant 和 ActionInterval,分别代表瞬时动作和持续动作。毫无疑问,帧动画是一个持续动作,所以 Animate 继承自 ActionInterval。
Action
要想解析 Animate,必须先解析它的几个父类,首先是最顶层的基类 Action;Action 其实很简单,就定义该动作作用在哪个结点上而已
Node *_originalTarget;
Node *_target;
这里定义了两个 target,其实指向的是同一个结点,只不过 _target 在开始执行动作的时候赋值,停止动作时会被清空;而 _originTarget 则会一直保存它的值
void Action::startWithTarget(Node *aTarget)
{
_originalTarget = _target = aTarget;
}
void Action::stop()
{
_target = nullptr;
}
startWithTarget 很明显是开始执行动作,这里只是设置了作用的结点而已,但其子类肯定会做更多操作的,我们后面再看。
FiniteTimeAction
FiniteTimeAction 顾名思义,有限时间动作表示这个动作是可以在有限时间内完成的;这个类特别简单,只是定义了一个持续时间的属性而已
inline float getDuration() const { return _duration; }
inline void setDuration(float duration) { _duration = duration; }
ActionInterval
有限时间也分为两种,一种是时间持续为 0,立即完成的动作,也就是 ActionInstant,另一种就是时间不为 0,动作会持续一段时间的动作,也就是 ActionInterval。ActionInterval 定义两个属性
float _elapsed;
bool _firstTick;
_elapsed 保存该动作从执行到现在用了多少时间,_firstTick 表示该动作是否刚执行,动作未执行时该属性为 true,动作一旦执行了每一步,该属性就被置为 false。ActionInterval 重写 Action 的开始执行动作函数,给这个属性赋初始值
void ActionInterval::startWithTarget(Node *target)
{
FiniteTimeAction::startWithTarget(target);
_elapsed = 0.0f;
_firstTick = true;
}
ActionInterval 另一个重要的函数是动作每执行一步会调用的函数 step
void ActionInterval::step(float dt)
{
if (_firstTick)
{
_firstTick = false;
_elapsed = 0;
}
else
{
_elapsed += dt;
}
float updateDt = MAX(0, MIN(1, _elapsed / MAX(_duration, FLT_EPSILON)));
if (sendUpdateEventToScript(updateDt, this))
return;
this->update(updateDt);
}
当执行第一步时会将 _firstTick 置为 false,将 _elapsed 置为 0,之后每执行一步都会累加过去的时间 _elapsed;然后计算这一步需要的时间,调用 update 函数执行相应的操作,update 函数在具体的子类中实例化
Animate
接下来就是看我们的主角 Animate 了,先看一下它的数据域
std::vector<float>* _splitTimes;
int _nextFrame;
int _currFrameIndex;
SpriteFrame* _origFrame;
unsigned int _executedLoops;
Animation* _animation;
EventCustom* _frameDisplayedEvent;
AnimationFrame::DisplayedEventInfo _frameDisplayedEventInfo;
_splitTimes 表示每一帧开始时间占总时间的比例;_nextFrame 表示下次要显示的帧下标,\currFrameIndex 表示当前显示的帧下标;_origFrame 保存目标精灵原来的精灵帧,如果 _restoreOriginalFrame = true,则动画播放结束时会使用 _origFrame 来重置精灵的精灵帧;_executedLoops 表示当前动画执行了几次。
bool Animate::initWithAnimation(Animation *animation)
{
CCASSERT(animation != nullptr, "Animate: argument Animation must be non-nullptr");
float singleDuration = animation->getDuration();
if (ActionInterval::initWithDuration(singleDuration * animation->getLoops()))
{
_nextFrame = 0;
setAnimation(animation);
_origFrame = nullptr;
_executedLoops = 0;
_splitTimes->reserve(animation->getFrames().size());
float accumUnitsOfTime = 0;
float newUnitOfTimeValue = singleDuration / animation->getTotalDelayUnits();
auto &frames = animation->getFrames();
for (auto &frame : frames)
{
float value = (accumUnitsOfTime * newUnitOfTimeValue) / singleDuration;
accumUnitsOfTime += frame->getDelayUnits();
_splitTimes->push_back(value);
}
return true;
}
return false;
}
在 Animate 的初始化函数 initWithAnimation 中设置数据域的值,首先保存 Animation 数据,然后重置 _nextFrmae、_origFrame 和 _executedLoops,_currFrameIndex 好像只有在切换精灵帧的时候用到,其实不用保存为数据域的。最后计算 _splitTimes 的值,newUnitOfTimeValue 的值由动画总持续时间除以总延迟单元个数,其实就是一个单元时间 _delayPerUnit;accumUnitOfTime 保存当前帧之前的总延迟单元个数,而 accumUnitOfTime * newUnitOfTimeValue 则是当前帧开始的时间,除以 _duration 就得到当前帧开始时间与总时间的比例。
void Animate::startWithTarget(Node *target)
{
ActionInterval::startWithTarget(target);
Sprite *sprite = static_cast<Sprite *>(target);
CC_SAFE_RELEASE(_origFrame);
if (_animation->getRestoreOriginalFrame())
{
_origFrame = sprite->getSpriteFrame();
_origFrame->retain();
}
_nextFrame = 0;
_executedLoops = 0;
}
在开始执行函数中,把目标精灵的原始精灵帧保存下来,然后重置下一帧下标和当前循环次数。
void Animate::stop()
{
if (_animation->getRestoreOriginalFrame() && _target)
{
static_cast<Sprite *>(_target)->setSpriteFrame(_origFrame);
}
ActionInterval::stop();
}
在停止播放函数中判断是否要恢复到原始帧,如果要的话则使用前面保存的 _origFrame 来重置目标精灵的精灵帧。
void Animate::update(float t)
{
// if t==1, ignore. Animation should finish with t==1
if (t < 1.0f)
{
t *= _animation->getLoops();
// new loop? If so, reset frame counter
unsigned int loopNumber = (unsigned int)t;
if (loopNumber > _executedLoops)
{
_nextFrame = 0;
_executedLoops++;
}
// new t for animations
t = fmodf(t, 1.0f);
}
auto &frames = _animation->getFrames();
auto numberOfFrames = frames.size();
SpriteFrame *frameToDisplay = nullptr;
for (int i = _nextFrame; i < numberOfFrames; i++)
{
float splitTime = _splitTimes->at(i);
if (splitTime <= t)
{
_currFrameIndex = i;
AnimationFrame *frame = frames.at(_currFrameIndex);
frameToDisplay = frame->getSpriteFrame();
static_cast<Sprite *>(_target)->setSpriteFrame(frameToDisplay);
const ValueMap &dict = frame->getUserInfo();
if (!dict.empty())
{
if (_frameDisplayedEvent == nullptr)
_frameDisplayedEvent = new (std::nothrow) EventCustom(AnimationFrameDisplayedNotification);
_frameDisplayedEventInfo.target = _target;
_frameDisplayedEventInfo.userInfo = &dict;
_frameDisplayedEvent->setUserData(&_frameDisplayedEventInfo);
Director::getInstance()->getEventDispatcher()->dispatchEvent(_frameDisplayedEvent);
}
_nextFrame = i + 1;
}
// Issue 1438. Could be more than one frame per tick, due to low frame rate or frame delta < 1/FPS
else
{
break;
}
}
}
update 函数是真正播放动画的过程,也就是设置目标精灵的精灵帧。
runAction
虽然知道了各层级 Action 做了什么事,但要理解动作执行的过程,还需要看 Node 类的 runAction 如何处理的。先看一下 runAction 函数的定义
Action * Node::runAction(Action* action)
{
CCASSERT( action != nullptr, "Argument must be non-nil");
_actionManager->addAction(action, this, !_running);
return action;
}
runAction 只是往动作管理器 _actionManager 添加一个动作而已,动作管理器是在 Node 的构造函数中赋值的
_director = Director::getInstance();
_actionManager = _director->getActionManager();
接下来看 ActionManager 类,首先看 addAction 函数
void ActionManager::addAction(Action *action, Node *target, bool paused)
{
CCASSERT(action != nullptr, "action can't be nullptr!");
CCASSERT(target != nullptr, "target can't be nullptr!");
tHashElement *element = nullptr;
// we should convert it to Ref*, because we save it as Ref*
Ref *tmp = target;
HASH_FIND_PTR(_targets, &tmp, element);
if (! element)
{
element = (tHashElement*)calloc(sizeof(*element), 1);
element->paused = paused;
target->retain();
element->target = target;
HASH_ADD_PTR(_targets, target, element);
}
actionAllocWithHashElement(element);
CCASSERT(! ccArrayContainsObject(element->actions, action), "action already be added!");
ccArrayAppendObject(element->actions, action);
action->startWithTarget(target);
}
在 addAction 中为 target 创建一个 element 保存在哈希表中,然后在 element 的 actions 数组中添加新的 action;然后调用 action 的 startWithTarget 进行动作初始化操作
到目前为止,我们仍看不到动作是怎么执行的,runAction 做的事只是将目标和动作保存在作管理器中,然后调用动作的 startWithTarget 函数,这个函数也只是做一些动作初始化工作而已。那动作到底是怎样执行的呢,我们首先想到的就是计时器,事实上 cocos2d-x 就是使用计时器来实现动作的。在 Dirctor 的 init 函数中新创建一个动作管理器,然后为动作管理器开启一个调度器
_actionManager = new (std::nothrow) ActionManager();
_scheduler->scheduleUpdate(_actionManager, Scheduler::PRIORITY_SYSTEM, false);
所以游戏一开始,ActionManager 的 update 函数就会一直被调用
void ActionManager::update(float dt)
{
for (tHashElement *elt = _targets; elt != nullptr; )
{
_currentTarget = elt;
_currentTargetSalvaged = false;
if (! _currentTarget->paused)
{
// The 'actions' MutableArray may change while inside this loop.
for (_currentTarget->actionIndex = 0; _currentTarget->actionIndex < _currentTarget->actions->num;
_currentTarget->actionIndex++)
{
_currentTarget->currentAction = (Action*)_currentTarget->actions->arr[_currentTarget->actionIndex];
if (_currentTarget->currentAction == nullptr)
{
continue;
}
_currentTarget->currentActionSalvaged = false;
_currentTarget->currentAction->step(dt);
if (_currentTarget->currentActionSalvaged)
{
// The currentAction told the node to remove it. To prevent the action from
// accidentally deallocating itself before finishing its step, we retained
// it. Now that step is done, it's safe to release it.
_currentTarget->currentAction->release();
} else
if (_currentTarget->currentAction->isDone())
{
_currentTarget->currentAction->stop();
Action *action = _currentTarget->currentAction;
// Make currentAction nil to prevent removeAction from salvaging it.
_currentTarget->currentAction = nullptr;
removeAction(action);
}
_currentTarget->currentAction = nullptr;
}
}
// elt, at this moment, is still valid
// so it is safe to ask this here (issue #490)
elt = (tHashElement*)(elt->hh.next);
// only delete currentTarget if no actions were scheduled during the cycle (issue #481)
if (_currentTargetSalvaged && _currentTarget->actions->num == 0)
{
deleteHashElement(_currentTarget);
}
}
// issue #635
_currentTarget = nullptr;
}
在 update 函数中遍历所有目标精灵,然后遍历精灵下所有动作,如果动作可以正常执行,则调用 step 函数,这就与前面讲到的动作对应上了,也就是说最终执行动作时回调到 Action 的 step 函数,而 step 函数会回调到 Action 的 update 函数。