我:今天咱们继续聊,还~有~谁~?
TargetedAction:这儿呢,这儿呢。我能让一个精灵待执行的动作在另一个精灵的动作执行完成之后执行。这可不同于Sequence,Sequence是让同一个精灵的多个动作顺序执行,而我是让另一个精灵的动作插个队。说了这么多可能听着还是有点儿绕,还是拿例子说话吧,
auto sprite1 = Sprite::create("image1.png");
auto sprite2 = Sprite::create("image2.png");
sprite1->setPosition(100,100);
sprite2->setPosition(200,200);
this->addChild(sprite1);
this->addChild(sprite2);
auto jumpby1 = JumpBy::create(3,Vec2(0,0),100,3);
auto jumpby2 = jumpby1->clone();
sprite1->runAction(Sequence::create(jumpby1,TargetedAction::create(sprite2,jumpby2),nullptr)); // sprite1跳3下之后sprite2才会跳。
实现上也很简单,就是在runAction()调用我的startWithTarget()时,我去调用您指定动作的startWithTarget();在ActionManager调用我的update()时我去调用您指定动作的update()。
我:看起来就像是个帮别人插队的代理。
我:接下来ActionFloat,你的功能是什么?
ActionFloat:我可以创建一个自定义起始和终止状态的动作,
auto actionfloat = ActionFloat::create(3,2,5,[this](float value) {
_tamara->setScale(value);
}); // 让执行动作的sprite在3秒内从2倍大小放大到5倍。
我接收规定的时间、动作的起始值、动作的终止值以及一个回调函数。回调函数的参数我会传递根据当前时间进度百分比,动作应被设定的值。以上面的例子为例,比如规定的时间过去一半时,我会向回调函数中传递2 + (5 - 2) * 0.5 = 3.5,所以此时执行动作的sprite应被放大到原尺寸的3.5倍。实现上同样很简单,首先在startWithTarget()中计算出终止值与起始值之差_delta,然后在update()中计算出当前动作应被设定的值,以此值调用回调函数。
// 当前动作应被设定的值。实现上用的是5 - (5 - 2) * 0.5 = 3.5,一样。
float value = _to - _delta * (1 - delta);
if (_callback)
{
_callback(value);
}
我:你的功能有些奇怪,看起来就像是个快捷方式,省去了在执行动作之前设置动作初始值的步骤。
我:最后就剩你了,Animate。
Animate:我能够播放一段动画,您需要提供给我一段创建好的动画,这个是由动画制作组那帮家伙负责的。规定的时间就不用了,因为创建好的动画信息中已经包含了时间的相关的信息。
auto animate = Animate::create(animation); // animation是创建好的动画。
我:关于动画你做个简单的说明。
Animate:好的。动画其实就是多张图片快速的连续播放的效果,比如您想让一个精灵跳舞,那您可以把这个精灵所做的每一个舞蹈动作制作成一张图片,然后将这些图片交给动画制作组。他们会处理图片的播放顺序、设置每张图片播放的时间等等,最终将这些图片组成一个可以播放的动画。而我就像是一个动画播放器,拿着动画制作组制作出来的这个动画文件,我就可以播放动画了。
我:你需要用到动画文件中的什么信息?
Animate:我需要用到动画播放一次的持续时间、动画播放的次数、按播放顺序排列好的图片(这里暂时先这么说,实际上是制作好的动画帧)、每一帧动画占用多少个时间片、每一个时间片占用多长时间、对于每一帧需要传递的用户数据。
我:时间片?
Animate:对,比如您的精灵所跳的这段舞蹈中有一个动作很优美,您想多看一会儿,那么您就可以给这张图片分配更多的时间片。图片拥有的时间片越多,它在动画中停留的时间就越长。
我:用户数据?
Animate:您可以理解为您对与每一个动画帧的备注,在播放动画时如果您需要,我可以把这些信息发送给您。
我:哦,好。基本的概念大致了解了,现在说说你的实现吧。
Animate:实现相比于TargetedAction和ActionFloat稍微复杂一点,还是对着源码说更清楚一些。首先是在创建我的时候,会进入我的initWithAnimation(),
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存储每一帧动画在动画播放一次的持续时间中的百分之多少时播放。
_splitTimes->reserve(animation->getFrames().size());
float accumUnitsOfTime = 0; // 此帧动画前用了多少个时间片,初始化为0。
/* 动画中每一个时间片占用多长时间 = 动画播放一次的持续时间 / 动画一共使用了多少个时间片。 * 其实直接使用Animation::getDelayPerUnit()不就好了。 */
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;
}
接着在runAction()是会调用我的startWithTarget(),
void Animate::startWithTarget(Node *target)
{
ActionInterval::startWithTarget(target);
Sprite *sprite = static_cast<Sprite*>(target);
CC_SAFE_RELEASE(_origFrame);
if (_animation->getRestoreOriginalFrame()) // 整个动画播放完成之后是否需要恢复sprite本来的样子。
{
_origFrame = sprite->getSpriteFrame(); // 先把sprite原本的样子记住。
_origFrame->retain();
}
_nextFrame = 0; // 下一次要播放哪一帧动画,初始化为第一帧。
_executedLoops = 0; // 当前是在第几次播放动画,初始化为第0次。
}
最后在动作实际运行起来后,ActionManager会调用我的update(),
void Animate::update(float t)
{
// if t==1,ignore. Animation should finish with t==1
if( t < 1.0f ) {
t *= _animation->getLoops();
// ((unsigned int)(t * 动画播放次数)) --> 当前正在第几次播放动画。
unsigned int loopNumber = (unsigned int)t;
if( loopNumber > _executedLoops ) { // 如果是进入了新的一次播放。
_nextFrame = 0; // 下一次要播放哪一帧动画,初始化为第一帧。
_executedLoops++; // 当前第几次播放动画+1。
}
t = fmodf(t,1.0f); // 转换为当次动画播放中的时间进度百分比。
}
auto& frames = _animation->getFrames();
auto numberOfFrames = frames.size(); // 动画一共有多少帧。
SpriteFrame *frameToDisplay = nullptr; // 将要播放的动画帧。
/* _nextFrame是下一次要播放的动画帧。 * 每次的update()走到这里是从下一次要播放的动画帧开始判断的,而不是从第一个动画帧。 * 这里用for()是因为怕程序的延时过长, * t传进来的时候已经是跳过了多个动画帧后的时间进度百分比。 * 用for()可以更新到当前时间应该正确被播放的动画帧。 */
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是AnimationFrame中定义的一种结构体, * 用户自定义事件以这种结构体的形式向用户发送用户数据。 */
_frameDisplayedEventInfo.target = _target;
_frameDisplayedEventInfo.userInfo = &dict;
_frameDisplayedEvent->setUserData(&_frameDisplayedEventInfo); // 填写用户自定义事件的信息。
Director::getInstance()->getEventDispatcher()->dispatchEvent(_frameDisplayedEvent); // 发送用户自定义事件。
}
_nextFrame = i+1; // 下一次要播放的动画帧+1。
}
// Issue 1438. Could be more than one frame per tick,due to low frame rate or frame delta < 1/FPS
else { // 如果下一个要播放的动画帧不应该被播放。
break;
}
}
}
我:你的实现还真是稍复杂了一些,看来要想比较好的理解你还需要找动画制作组聊聊。