文章目录
动画系统
简述
利用scheduleUpdate 对update进行的封装
- 足够早的执行,在逻辑和物理处理之前
- 使用action类描述动画,提供了并行和串行动画。
- 必须依附与node进行播放
- 将更新间隔转换成百分比0~1
- 使用CC_ENABLE_STACKABLE_ACTIONS控制是否叠加,只有Move,Jump,Besizer支持叠加
架构
Scheduler每一帧传入间隔时间dt调用ActionManager 的update()方法,ActionManager根据一个Action 的执行时间,在[dt,dt+duration]时间范围内调用Action的 step()方法,在 step方法内部将这个每一帧的时间进度转化为[0.0,1.0]的范围。最后将这个比例传给update进行计算。
类结构
动作的实现
延时动作都是在init的时候计算好要改变的值,使用startValue + diffValue * percent
实现,其中percent是一个0到1的数,表示百分比,update的参数会根据初始化时的持续时间参数进行转换:
void ActionInterval::step(float dt)
{
if (_firstTick)
{
_firstTick = false;
_elapsed = 0;
}
else
{
_elapsed += dt;
}
float updateDt = std::max(0.0f,// needed for rewind. elapsed could be negative
std::min(1.0f, _elapsed / _duration));
if (sendUpdateEventToScript(updateDt, this)) return;
this->update(updateDt);
_done = _elapsed >= _duration;
}
具体看2个动作的update实现
MoveBy
使用CC_ENABLE_STACKABLE_ACTIONS
宏控制是否叠加。
void MoveBy::update(float t)
{
if (_target)
{
#if CC_ENABLE_STACKABLE_ACTIONS
Vec3 currentPos = _target->getPosition3D();
Vec3 diff = currentPos - _previousPosition;
_startPosition = _startPosition + diff;
Vec3 newPos = _startPosition + (_positionDelta * t);
_target->setPosition3D(newPos);
_previousPosition = newPos;
#else
_target->setPosition3D(_startPosition + _positionDelta * t);
#endif // CC_ENABLE_STACKABLE_ACTIONS
}
}
void RotateTo::update(float time)
{
if (_target)
{
if(_is3D)
{
_target->setRotation3D(Vec3(
_startAngle.x + _diffAngle.x * time,
_startAngle.y + _diffAngle.y * time,
_startAngle.z + _diffAngle.z * time
));
}
}
}
叠加动作的实现
如moveBy
所示,开启CC_ENABLE_STACKABLE_ACTIONS
宏后,会使用_previousPosition
变量保存本帧计算出的位置,在下次计算时,和当前位置计算出差异,这个差异就是一帧内其他动作叠加的变化,将这个差异叠加到_startPosition
上,再计算本动作的变化值。
#if CC_ENABLE_STACKABLE_ACTIONS
Vec3 currentPos = _target->getPosition3D();
Vec3 diff = currentPos - _previousPosition;
_startPosition = _startPosition + diff;
Vec3 newPos = _startPosition + (_positionDelta * t);
_target->setPosition3D(newPos);
_previousPosition = newPos;
#else
关于XXTo和XXBy的update实现
大部分都是XXTO继承自XXBy,也有XXBy继承XXTo,都是避免重写update方法,直接复用父类的方法。有些必须重写的方法还是要重写的,比如reverse
、clone
。
reverse
- XXTo 类动作是不允许调用reverse接口的,如:
JumpTo* JumpTo::reverse() const
{
CCASSERT(false, "reverse() not supported in JumpTo");
return nullptr;
}
- XXBy类接口的reverse是将参数翻转实现
- 瞬时动作,如Show、Hide、Flip的reverse是创建一个对应的反义动作。
speed——速度的控制
通过speed
可以控制ActionInterval
延时动作的速度:
auto moveBy = MoveBy::Create(5,Vec2(200,0));
sprite->runAction(Speed::Create(moveBy,2));
在Speed
的内部重写了step
方法,它直接将速度参数乘以子动画的更新时间线。
void Speed::step(float dt)
{
_innerAction->step(dt * _speed);
}
动画的回调
一般动画系统的实现都会利用事件机制,比如在播放完成时发出complete事件,以实现在动画完成时做某些逻辑的功能。cocos2dx的动画系统并没有这么做。
runAction并不提供动画完成时的回调,如果需要在动画完成时得到通知,则需要使用CallFunc即时动画来提供一个回调函数,并将它串联至一个动画后面,串联的所有Action将会被按顺序执行,最后一个动画CallFunc是一个即时动画,它立即执行指定的回调函数来实现动画完成时的功能。
除此之外,CannFunc可以放在任意一个 Action前面或者后面,用来实现如动画执行的前置条件或该动画完成时的回调等功能。
缓动函数——速度曲线的控制
Step
和update
参数使用百分比的设计另一个好处是缓动实现方便,只要在step里对百分比进行换算即可,cocos2dx的缓动函数的实现逻辑直接贴出来了,之后实现一些缓动效果可以用上:)
namespace tweenfunc {
#ifndef M_PI_X_2
#define M_PI_X_2 (float)M_PI * 2.0f
#endif
float tweenTo(float time, TweenType type, float *easingParam)
{
float delta = 0;
switch (type)
{
case CUSTOM_EASING:
delta = customEase(time, easingParam);
break;
case Linear:
delta = linear(time);
break;
case Sine_EaseIn:
delta = sineEaseIn(time);
break;
case Sine_EaseOut:
delta = sineEaseOut(time);
break;
case Sine_EaseInOut:
delta = sineEaseInOut(time);
break;
case Quad_EaseIn:
delta = quadEaseIn(time);
break;
case Quad_EaseOut:
delta = quadEaseOut(time);
break;
case Quad_EaseInOut:
delta = quadEaseInOut(time);
break;
case Cubic_EaseIn:
delta = cubicEaseIn(time);
break;
case Cubic_EaseOut:
delta = cubicEaseOut(time);
break;
case Cubic_EaseInOut:
delta = cubicEaseInOut(time);
break;
case Quart_EaseIn:
delta = quartEaseIn(time);
break;
case Quart_EaseOut:
delta = quartEaseOut(time);
break;
case Quart_EaseInOut:
delta = quartEaseInOut(time);
break;
case Quint_EaseIn:
delta = quintEaseIn(time);
break;
case Quint_EaseOut:
delta = quintEaseOut(time);
break;
case Quint_EaseInOut:
delta = quintEaseInOut(time);
break;
case Expo_EaseIn:
delta = expoEaseIn(time);
break;
case Expo_EaseOut:
delta = expoEaseOut(time);
break;
case Expo_EaseInOut:
delta = expoEaseInOut(time);
break;
case Circ_EaseIn:
delta = circEaseIn(time);
break;
case Circ_EaseOut:
delta = circEaseOut(time);
break;
case Circ_EaseInOut:
delta = circEaseInOut(time);
break;
case Elastic_EaseIn:
{
float period = 0.3f;
if (nullptr != easingParam) {
period = easingParam[0];
}
delta = elasticEaseIn(time, period);
}
break;
case Elastic_EaseOut:
{
float period = 0.3f;
if (nullptr != easingParam) {
period = easingParam[0];
}
delta = elasticEaseOut(time, period);
}
break;
case Elastic_EaseInOut:
{
float period = 0.3f;
if (nullptr != easingParam) {
period = easingParam[0];
}
delta = elasticEaseInOut(time, period);
}
break;
case Back_EaseIn:
delta = backEaseIn(time);
break;
case Back_EaseOut:
delta = backEaseOut(time);
break;
case Back_EaseInOut:
delta = backEaseInOut(time);
break;
case Bounce_EaseIn:
delta = bounceEaseIn(time);
break;
case Bounce_EaseOut:
delta = bounceEaseOut(time);
break;
case Bounce_EaseInOut:
delta = bounceEaseInOut(time);
break;
default:
delta = sineEaseInOut(time);
break;
}
return delta;
}
// Linear
float linear(float time)
{
return time;
}
// Sine Ease
float sineEaseIn(float time)
{
return -1 * cosf(time * (float)M_PI_2) + 1;
}
float sineEaseOut(float time)
{
return sinf(time * (float)M_PI_2);
}
float sineEaseInOut(float time)
{
return -0.5f * (cosf((float)M_PI * time) - 1);
}
// Quad Ease
float quadEaseIn(float time)
{
return time * time;
}
float quadEaseOut(float time)
{
return -1 * time * (time - 2);
}
float quadEaseInOut(float time)
{
time = time*2;
if (time < 1)
return 0.5f * time * time;
--time;
return -0.5f * (time * (time - 2) - 1);
}
// Cubic Ease
float cubicEaseIn(float time)
{
return time * time * time;
}
float cubicEaseOut(float time)
{
time -= 1;
return (time * time * time + 1);
}
float cubicEaseInOut(float time)
{
time = time*2;
if (time < 1)
return 0.5f * time * time * time;
time -= 2;
return 0.5f * (time * time * time + 2);
}
// Quart Ease
float quartEaseIn(float time)
{
return time * time * time * time;
}
float quartEaseOut(float time)
{
time -= 1;
return -(time * time * time * time - 1);
}
float quartEaseInOut(float time)
{
time = time*2;
if (time < 1)
return 0.5f * time * time * time * time;
time -= 2;
return -0.5f * (time * time * time * time - 2);
}
// Quint Ease
float quintEaseIn(float time)
{
return time * time * time * time * time;
}
float quintEaseOut(float time)
{
time -=1;
return (time * time * time * time * time + 1);
}
float quintEaseInOut(float time)
{
time = time*2;
if (time < 1)
return 0.5f * time * time * time * time * time;
time -= 2;
return 0.5f * (time * time * time * time * time + 2);
}
// Expo Ease
float expoEaseIn(float time)
{
return time == 0 ? 0 : powf(2, 10 * (time/1 - 1)) - 1 * 0.001f;
}
float expoEaseOut(float time)
{
return time == 1 ? 1 : (-powf(2, -10 * time / 1) + 1);
}
float expoEaseInOut(float time)
{
if(time == 0 || time == 1)
return time;
if (time < 0.5f)
return 0.5f * powf(2, 10 * (time * 2 - 1));
return 0.5f * (-powf(2, -10 * (time * 2 - 1)) + 2);
}
// Circ Ease
float circEaseIn(float time)
{
return -1 * (sqrt(1 - time * time) - 1);
}
float circEaseOut(float time)
{
time = time - 1;
return sqrt(1 - time * time);
}
float circEaseInOut(float time)
{
time = time * 2;
if (time < 1)
return -0.5f * (sqrt(1 - time * time) - 1);
time -= 2;
return 0.5f * (sqrt(1 - time * time) + 1);
}
// Elastic Ease
float elasticEaseIn(float time, float period)
{
float newT = 0;
if (time == 0 || time == 1)
{
newT = time;
}
else
{
float s = period / 4;
time = time - 1;
newT = -powf(2, 10 * time) * sinf((time - s) * M_PI_X_2 / period);
}
return newT;
}
float elasticEaseOut(float time, float period)
{
float newT = 0;
if (time == 0 || time == 1)
{
newT = time;
}
else
{
float s = period / 4;
newT = powf(2, -10 * time) * sinf((time - s) * M_PI_X_2 / period) + 1;
}
return newT;
}
float elasticEaseInOut(float time, float period)
{
float newT = 0;
if (time == 0 || time == 1)
{
newT = time;
}
else
{
time = time * 2;
if (! period)
{
period = 0.3f * 1.5f;
}
float s = period / 4;
time = time - 1;
if (time < 0)
{
newT = -0.5f * powf(2, 10 * time) * sinf((time -s) * M_PI_X_2 / period);
}
else
{
newT = powf(2, -10 * time) * sinf((time - s) * M_PI_X_2 / period) * 0.5f + 1;
}
}
return newT;
}
// Back Ease
float backEaseIn(float time)
{
float overshoot = 1.70158f;
return time * time * ((overshoot + 1) * time - overshoot);
}
float backEaseOut(float time)
{
float overshoot = 1.70158f;
time = time - 1;
return time * time * ((overshoot + 1) * time + overshoot) + 1;
}
float backEaseInOut(float time)
{
float overshoot = 1.70158f * 1.525f;
time = time * 2;
if (time < 1)
{
return (time * time * ((overshoot + 1) * time - overshoot)) / 2;
}
else
{
time = time - 2;
return (time * time * ((overshoot + 1) * time + overshoot)) / 2 + 1;
}
}
// Bounce Ease
float bounceTime(float time)
{
if (time < 1 / 2.75f)
{
return 7.5625f * time * time;
}
else if (time < 2 / 2.75f)
{
time -= 1.5f / 2.75f;
return 7.5625f * time * time + 0.75f;
}
else if(time < 2.5f / 2.75f)
{
time -= 2.25f / 2.75f;
return 7.5625f * time * time + 0.9375f;
}
time -= 2.625f / 2.75f;
return 7.5625f * time * time + 0.984375f;
}
float bounceEaseIn(float time)
{
return 1 - bounceTime(1 - time);
}
float bounceEaseOut(float time)
{
return bounceTime(time);
}
float bounceEaseInOut(float time)
{
float newT = 0;
if (time < 0.5f)
{
time = time * 2;
newT = (1 - bounceTime(1 - time)) * 0.5f;
}
else
{
newT = bounceTime(time * 2 - 1) * 0.5f + 0.5f;
}
return newT;
}
// Custom Ease
float customEase(float time, float *easingParam)
{
if (easingParam)
{
float tt = 1-time;
return easingParam[1]*tt*tt*tt + 3*easingParam[3]*time*tt*tt + 3*easingParam[5]*time*time*tt + easingParam[7]*time*time*time;
}
return time;
}
float easeIn(float time, float rate)
{
return powf(time, rate);
}
float easeOut(float time, float rate)
{
return powf(time, 1 / rate);
}
float easeInOut(float time, float rate)
{
time *= 2;
if (time < 1)
{
return 0.5f * powf(time, rate);
}
else
{
return (1.0f - 0.5f * powf(2 - time, rate));
}
}
float quadraticIn(float time)
{
return powf(time,2);
}
float quadraticOut(float time)
{
return -time*(time-2);
}
float quadraticInOut(float time)
{
float resultTime = time;
time = time*2;
if (time < 1)
{
resultTime = time * time * 0.5f;
}
else
{
--time;
resultTime = -0.5f * (time * (time - 2) - 1);
}
return resultTime;
}
float bezieratFunction( float a, float b, float c, float d, float t )
{
return (powf(1-t,3) * a + 3*t*(powf(1-t,2))*b + 3*powf(t,2)*(1-t)*c + powf(t,3)*d );
}
}
结合node运行
使用node
的runAction
接口会将action
添加到director
的actionManager
中:_actionManager = _director->getActionManager();
Action * Node::runAction(Action* action)
{
CCASSERT( action != nullptr, "Argument must be non-nil");
_actionManager->addAction(action, this, !_running);
return action;
}
actionManager
的addAction
会将action
加入action
数组中,并设置action
的target
ActionManager
执行时机
动画在游戏循环中相对于游戏状态更新逻辑应该是一个比较低阶的系统,它的优先级应该足够低,所以它被较早地执行。因为还有其他的如物理系统及后面的逻辑处理阶段,这些都有可能修改游戏对象的属性。实际上物理碰撞也是一个相对比较低阶的系统,最终这些对游戏对象的影响都可以被逻辑更新所修改。
那么,Cocos2d-x如何保证动画系统被最早地执行呢?实际上,Cocos2d-x 中的所有动画都是通过一个Schedule 来完成的,它首先保证所有动画在一起执行。然而,与其他Schedule不同的是,动画系统被指定为一个特殊的优先级:PRIORITY_SYSTEM。
在Director::init()
中,将ActionManager
加入定时器。
_actionManager = new (std::nothrow) ActionManager();
_scheduler->scheduleUpdate(_actionManager, Scheduler::PRIORITY_SYSTEM, false);
执行逻辑
在update
中,调用所有对象的action
,执行update
方法。如果action
执行完成(isDone
),就先调用action
的stop
接口,再移出数组。
// main loop
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 = static_cast<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);
}
//if some node reference 'target', it's reference count >= 2 (issues #14050)
else if (_currentTarget->target->getReferenceCount() == 1)
{
deleteHashElement(_currentTarget);
}
}
// issue #635
_currentTarget = nullptr;
}
细节
- 在遍历不同target时,设置当前正在变量的target到
_currentTarget
上,在update结束时,将_currentTarget
置空。 - 在target没有action时,移出target
- 如果target的action执行完一次后,其引用计数变成了1,表示没有其他引用了,可以移出队列。
动作队列
cocos2dx提供了3种类型的动画队列
- 一个Sequence动画包含多个子动画,Sequence中的每个子动画按顺序一个接着一个地执行。
- 一个Spawn动画包含多个子动画,Sawan中的所有子动画同时开始执行。
- 一个Repeat和RepeatForever动画包含一个子动画,Repeat对子动画重复执行指定次数,而 RepeatForever一直重复执行子动画。
Sequence
一个Sequence动画是一个连续执行的动画集合,它按照每个子Action被构造的顺序执行,如果一个动画执行完毕,立即接着执行下一个动画,当最后一个动画执行完毕后,则 Sequence动画执行完毕。
Sequence的动画执行时间是所有子动画执行时间的总和。
Sequence
本身也是一个动作,继承自延时动作基类ActionInterval
/** @class Sequence
* @brief Runs actions sequentially, one after another.
*/
class CC_DLL Sequence : public ActionInterval
{
public:
static Sequence* create(FiniteTimeAction *action1, ...) CC_REQUIRES_NULL_TERMINATION;
static Sequence* create(const Vector<FiniteTimeAction*>& arrayOfActions);
static Sequence* createWithTwoActions(FiniteTimeAction *actionOne, FiniteTimeAction *actionTwo);
virtual void update(float t) override;
protected:
FiniteTimeAction *_actions[2];
}
Sequence
是一个二叉树的结构,init接口中,如果动画数量是0则直接delete掉,返回null,不创建队列。只有一个时,构造个ExtraAction动画。大小大于1时,构造一个二叉树。
bool Sequence::init(const Vector<FiniteTimeAction*>& arrayOfActions)
{
auto count = arrayOfActions.size();
if (count == 0)
return false;
if (count == 1)
return initWithTwoActions(arrayOfActions.at(0), ExtraAction::create());
// else size > 1
auto prev = arrayOfActions.at(0);
for (int i = 1; i < count-1; ++i)
{
prev = createWithTwoActions(prev, arrayOfActions.at(i));
}
return initWithTwoActions(prev, arrayOfActions.at(count-1));
}
Sequence的update:因为只有左右2个子节点,计算好时间调用子节点的update即可。
Spawn
与Sequence相反,Sawan则让所有子动画同时执行。例如想要让一个轮子移动一段距离,由于受摩擦力作用,它应该一边旋转一边移动,例如:
auto moveBy=MoveBy::create(5,Vec2(200,O));
auto rotateBy=RotateBy::create(5,360);
auto spawn=Spawn::create(moveBy,rotateBy,NULL);
sprite->runAction(spawn);
与Sequence一样,Spawn也接收一个变长的参数列表,并以NULL 结尾。Spawn动画执行的时间是所有子动画中执行时间最长的时间。
Spawn的create过程和Sequence一样,也是一个二叉树。
相比较Sequence,Spawn的update实现就简单很多了,直接调用每个action的update即可。
void Spawn::update(float time)
{
if (_one)
{
if (!(sendUpdateEventToScript(time, _one)))
_one->update(time);
}
if (_two)
{
if (!(sendUpdateEventToScript(time, _two)))
_two->update(time);
}
}
Repeat和RepeatForever
还有一种控制动画,它用来控制子动画的重复行为。例如我们想要红灯忽闪忽闪的效果,可以使用前面讲述的Sequence来组合一个FadeIn和FadeOut的序列,然后使用RepeatForever来使其一直重复。
auto fadeIn=FadeIn: :create(0.8);
auto fadeout-Fadeout::create(O.8);
auto sequence=Sequence::create (fadeIn,fadeout,NULL);
auto repeaterForever=RepeatForever::create (sequence);
sprite->runAction(repeaterForever);
RepeatForever永远不会停止,isDone直接返回false了。step接口直接调用了action的step,之后判断动作如果执行完成则重新初始化。
void RepeatForever::step(float dt)
{
_innerAction->step(dt);
// only action interval should prevent jerk, issue #17808
if (_innerAction->isDone() && _innerAction->getDuration() > 0)
{
float diff = _innerAction->getElapsed() - _innerAction->getDuration();
if (diff > _innerAction->getDuration())
diff = fmodf(diff, _innerAction->getDuration());
_innerAction->startWithTarget(_target);
// to prevent jerk. cocos2d-iphone issue #390, 1247
_innerAction->step(0.0f);
_innerAction->step(diff);
}
}
bool RepeatForever::isDone() const
{
return false;
}