19、Cocos2dx 3.0游戏开发找小三之Action:流动的水没有形状,漂流的风找不到踪迹、、、
流动的水没有形状,漂流的风找不到踪迹、、、
直睡的陵迁谷变,石烂松枯,斗转星移,整个宇宙在不停的运动着、、、
前面详细介绍了游戏的基本组成元素--场景、 层、 精灵和渲染树等, 也详细介绍了 Node 提供的定时器。
为了让整个世界运动起来,让每一个精灵运动,可以利用定时器,不断修改节点的属性,实现简单的动态效果。
然而,这种方法会导致为了实现简单的动态效果,十分烦 琐地维护一批定时器。
Cocos2d-x 为了解决这个问题,引入了动作机制。
Action是动作类的基类
所有的动作都派生自这个类,它创建的一个对象代表了一个动作。
动作作用于Node,因此, 任何一个动作都需要由 Node 对象来执行。
以下代码实现了一个精灵用 10秒钟的时间移动到了点(100,100):
- autosprite=Sprite::create("sprite.png");
- autoaction=MoveTo::create(1.0f,Point(0,0));
- sprite->runAction(action);
需要注意的是,一个 Action 只能使用一次,
这是因为动作对象不仅描述了动作,还保存了这个动作持续过程中不断改变
的一些中间参数。
对于需要反复使用的动作对象,可以通过 copy 方法复制使用。
Action 作为一个基类,其实质是一个接口(即抽象类),
由它派生的实现类(如运动和转动等)才是我们实际使用的动
作。
Action 的绝大多数实现类都派生自 FiniteTimeAction,这个类定义了在有限时间内可以完成的动作。
FiniteTimeAction定义了 reverse 方法,通过这个方法可以获得一个与原动作相反的动作(称作逆动作),例如隐藏一
个精灵后,用逆转动作再显示出来。
当然,并非所有的动作都有对应的逆动作,
例如类似"放大到"等设置属性为常量的动
作不存在逆动作,而设置属性为相对值的动作则往往存在相应的逆动作。
由 FiniteTimeAction 派生出的两个主要类分别是瞬时动作(ActionInstant)和持续性动作(ActionInterval),
这两类动作与复合动作配合使用,能得到复杂生动的动作效果。
瞬时动作
瞬时动作是指能立刻完成的动作,是 FiniteTimeAction 中动作持续时间为 0 的特例。
更准确地说,这类动作是在下一帧会立刻执行并完成的动作,如设定位置、设定缩放等。
这些动作原本可以通过简单地对 Node 赋值完成,但是把它们包装
为动作后,可以方便地与其他动作类组合为复杂动作。
一些常用的瞬时动作:
Place
该动作用于将节点放置到某个指定位置,其作用与修改节点的 Position 属性相同。
例如,将精灵放置到屏幕坐标(100,100)处,再执行曲线运动 curveMove 的代码如下:
FiniteTimeAction*placeAction=Place::create(Point(100,100));
- Action*action=Sequence::create(placeAction,curveMove,NULL);
其中 Sequence 又称为动作序列,是一种复合动作,它在初始化时,会接受多个动作,当它被执行时,这些动作会按顺序
逐个执行,形成有序的一列动作。
FlipX 和 和 FlipY
这两个动作分别用于将精灵沿 X 和 Y 轴反向显示,其作用与设置精灵的 FlipX 和 FlipY 属性相同。
将其包装为动作类也是
为了便于与其他动作进行组合。
例如,我们想使精灵从屏幕的一端游动到另一端,然后按原路返回。
为了更自然一点
,
我们设置一个序列,精灵先执行移动的动作,在精灵到达另一端时反向显示,然后再执行移动回起点的动
作,相关代码如下:
FiniteTimeAction*flipXAction=FlipX::create(true);
- Action*action=Sequence::create(curveMove,flipXAction,curveMove->reverse(),38); background-color:inherit">其中 reverse 的作用是取得原动作的逆动作。在这个例子中,精灵沿 X 轴翻转后将会沿原路返回起点。
Show 和 和 Hide
这两个动作分别用于显示和隐藏节点,其作用与设置节点的 Visible 属性的作用一样。
例如,为了使精灵完成运动之后隐藏起来,我们使用如下代码:
FiniteTimeAction*hideAction=Hide::create();
-
CallFunc
CallFunc 系列动作包括 CallFunc、CallFuncN、__CCCallFuncND,以及 __CCCallFuncO四个动作,
当某个对象执行 CallFunc 系列动作时,就会调用一个先前被设置好的方法,以完成某些特别的功能。
在CallFunc 系列动作的 4 个类中:
CallFuncN 调用的方法包含一个 Node*类型
的参数, 表示执行动作的对象。
__CCCallFuncND
调用的方法包含两个参数, 不仅有一个节点参数, 还有一个自定义参数 (Node*
与 void*)。__CCCallFuncO调用的方法则只包含一个 Ref*类型的参数。
实际上,CallFunc 系列动作的后缀"N"表示 Node 参数,指的是执行动作的对象,
"D"表示 Data 参数,指的是用户自定义
的数据,"O"表示对象,指的是一个用户自定义的 Ref参数。
在不同的情况下,我们可以根据不同的需求来选择不同的 CallFunc 动作。
考虑一种情况,我们创建了许多会在屏幕中移动的精灵,希望精灵在移动结束之后就从游戏中删除。
为了实现这个效果,我们可以创建一系列动作:首先让精灵移动,然后调用一个 removeSelf(Node* nodeToRemove)方法
来删除 nodeToRemove 对象。在 removeSelf 方法中需要访问执行此动作的精灵,因此我们就采用 CallFuncN 来调用
removeSelf 方法。
持续性动作
持续性动作是在持续的一段时间里逐渐完成的动作,如精灵从一个点连续地移动到另一个点。
与瞬时动作相比,持续性动
作的种类更丰富。
由于这些动作将持续一段时间,所以大多数的持续性动作都会带有一个用于控制动作执行时间的实型参
数duration。
每一种持续性动作通常都存在两个不同的变种动作,分别具有 To 和 By 后缀:
后缀为 To 的动作描述了节点属性值的绝对变
化,例如 MoveTo 将对象移动到一个特定的位置;
而后缀为 By 的动作则描述了属性值相对的变化,如 MoveBy 将对象移
动一段相对位移。
根据作用效果不同,可以将持续性动作划分为以下 4 大类:
位置变化动作
视觉特效动作
控制动作
位置变化动作
针对位置(position)这一属性,引擎为我们提供了 3 种位置变化动作类型。
MoveTo 和 MoveBy:用于使节点做直线运动。设置了动作时间和终点位置后,节点就会在规定时间内,从当前位置直线
移动到设置的终点位置。它们的初始化方法分别为:
staticMoveTo*create(floatduration,constPoint&position);
- staticMoveBy*create(constPoint&deltaPosition);
其中,duration 参数表示动作持续的时间,position参数表示移动的终点或距离。
对于 MoveTo,节点会被移动到对应的位置;
对于 MoveBy,节点会相对之前的位置移动的距离。
JumpTo 和 JumpBy:使节点以一定的轨迹跳跃到指定位置。它们的初始化方法如下:
staticJumpTo*create(constPoint&position,floatheight,87); font-weight:bold; background-color:inherit">intjumps);
- staticJumpBy*create(intjumps);
其中表示跳跃的终点或距离,height 表示最大高度,jumps 表示跳跃次数。
BezierTo 和 BezierBy: 使节点进行曲线运动, 运动的轨迹由贝塞尔曲线描述。
staticBezierTo*create(floatt,153); font-weight:bold; background-color:inherit">constccBezierConfig&c);
- staticBezierBy*create(constccBezierConfig&c);
贝塞尔曲线是描述任意曲线的有力工具,
在许多软件(如 Adobe Photoshop)中,钢笔工具就是贝塞尔曲线的应用。
每一条贝塞尔曲线都包含一个起点和一个终点。
在一条曲线中,起点和终点都各自包含一个控制点,而控制点到端点的连
线称作控制线。
控制线决定了从端点发出的曲线的形状,包含角度和长度两个参数:角度决定了它所控制的曲线的方向,
即这段曲线在这一控制点的切线方向;长度控制曲线的曲率。控制线越长,它所控制的曲线离控制线越近。
使用时我们要先创建 ccBezierConfig 结构体, 设置好终点 endPosition 以及两个控制点controlPoint_1 和controlPoint_2
后,再把结构体传入 BezierTo 或 BezierBy 的初始化方法中:
ccBezierConfigbezier;
- bezier.controlPoint_1=Point(20,150);
- bezier.controlPoint_2=Point(200,30);
- bezier.endPosition=Point(160,30);
- FiniteTimeAction*beizerAction=BezierTo::create(actualDuration/4,bezier);
例如, 下面要介绍的第一个动作 ScaleTo,它会在一段时间内不断地改变游戏元素的 scale 属性,
使属性值平滑地变化到一个新值,从而使游戏元素产生缩放的动画效果。
ScaleTo 和 ScaleBy:产生缩放效果,使节点的缩放系数随时间线性变化。对应的初始化方法为:
staticScaleTo*create(floats);
- staticScaleBy*create(floats);
其中,s 为缩放系数的最终值或变化量。
RotateTo 和 RotateBy:产生旋转效果。对应的初始化方法为:
staticRotateTo*create(floatdeltaAngle);
- staticRotateBy*create(floatdeltaAngle);
deltaAngle的单位是角度,正方向为顺时针方向。
FadeIn 和 FadeOut:产生淡入淡出效果,其中前者实现了淡入效果,后者实现了淡出效果。对应的初始化方法为:
staticFadeIn*create(floatd);
- staticFadeOut*create(floatd);
以下介绍的几个类似的动作。
FadeTo:用于设置一段时间内透明度的变化效果。其初始化方法为:
staticFadeTo*create(
参数中的 Glubyte 是 8 位无符号整数,因此,opacity 可取 0 至 255 中的任意整数。
与透明度相关的动作只能应用在精灵
(Sprite)上,且子节点不会受到父节点的影响。
TintTo 和 TintBy:设置色调变化。这个动作较为少用,其初始化方法为:
static
TintTo*create(staticTintBy*create(
与 FadeTo 类似,red、green和blue的取值范围也为 0~255。
视觉特效动作
static
Blink*create(intblinks);
Animation:播放帧动画,用帧动画的形式实现动画效果。
控制动作
控制动作是一类特殊的动作,用于对一些列动作进行精细控制。
利用这一类动作可以实现一些实用的功能,因此它们是十
分常用的。
这类动作包括 DelayTime、 Repeat 和RepeatForever 等。
DelayTime可以将动作延时一定的时间,
RepeateForever
可以使一个动作不断重复下去。
事实上,控制动作与复合动作息息相关。
复合动作
简单动作显然不足以满足游戏开发的要求, 在这些动作的基础上,
Cocos2d-x 为我们提供了一套动作的复合机制,
允许我们组合各种基本动作,产生更为复杂和生动的动作效果。
复合动作是一类特殊的动作,因此它也需要使用 Node 的
runAction 方法执行。
而它的特殊之处在于,作为动作容器,复合动作可以把许多动作组合成一个复杂的动作。
因此,我们
通常会使用一个或多个动作来创建复合动作,再把动作交给节点执行。
复合动作十分灵活,这是由于复合动作本身也是动作,因此也可以作为一个普通的动作嵌套在其他复合动作中。
重复( Repeat/RepeatForever )
有的情况下,动作只需要执行一次即可,但我们还常常遇到一个动作反复执行的情况。
对于一些重复的动作,
我们可以通过 Repeat 与 RepeatForever 这两个方式重复执行:
staticRepeat*create(FiniteTimeAction*action,unsignedinttimes);
- staticRepeatForever*create(ActionInterval*action);
action参数表示需要重复的动作,第一个方法允许指定动作的重复次数,第二个方法使节点一直重复该动
作直到动作被停止。
并列( Spawn )
指的是使一批动作同时执行。Spawn 从 ActionInterval 派生而来的,它提供了两个工厂方法:
staticSpawn*create(FiniteTimeAction*action1,...)CC_REQUIRES_NULL_TERMINATION;
- staticSpawn*createWithTwoActions(FiniteTimeAction*action1,FiniteTimeAction*action2);
其中第一个静态方法可以将多个动作同时并列执行,参数表中最后一个动作后需要紧跟 NULL 表示结束。第二个则只能指定
两个动作复合, 不需要在最后一个动作后紧跟 NULL。
此外, 执行的动作必须是能够同时执行的、 继承自 FiniteTimeAction
的动作。
组合后,Spawn 动作的最终完成时间由其成员中最大执行时间的动作来决定。
序列( Sequence )
除了让动作同时并列执行,我们更常遇到的情况是顺序执行一系列动作。
Sequence 提供了一个动作队列,它会顺序执行
一系列动作。
Sequence 同样派生自 ActionInterval。
与 Spawn 一样,Squence 也提供了两个工厂方法:
staticSequence*create(FiniteTimeAction*action1,153); font-weight:bold; background-color:inherit">staticSequence*createWithTwoActions(FiniteTimeAction*actionOne,FiniteTimeAction*actionTwo);
它们的作用分别是建立多个和两个动作的顺序执行的动作序列。
同样要注意复合动作的使用条件,部分的非延时动作(如
RepeatForever)并不被支持。
在实现 Sequence 和 Spawn 两个组合动作类时,有一个非常有趣的细节:
成员变量中并没有定义一个可变长的容器来容
纳每一个动作系列, 而是定义了m_pOne和m_pTwo两个动作成员变量。 如果我们创建了两个动作的组合,
那么m_pOne与m_pTwo
就分别是这两个动作本身;
当我们创建更多动作的组合时,引擎会把动作分解为两部分来看待,
其中后一部分只包含最后
一个动作,而前一部分包含它之前的所有动作,
引擎把 m_pTwo 设置为后一部分的动作,把 m_pOne 设置为其余所有动作的
组合。
例如,
语句
sequence=Sequence::create(action1,action2,action3,action4,NULL);
就等价于:
Sequences1=Sequence::createWithTwoActions(action1,action2);
- Sequences2=Sequence::createWithTwoActions(s1,action3);
- sequence=Sequence::createWithTwoActions(s2,action4);
Spawn 与 Sequence 所采用的机制类似,在此就不再赘述了。
采用这种递归的方式,而不是直接使用容器来定义组合动
作,实际上为编程带来了极大的便利。
维护多个动作的组合是一个复杂的问题,现在我们只需要考虑两个动作组合的情况
就可以了。
下面是 Spawn 的一个初始化方法,就是利用
递归的思想
简化了编程的复杂度:
Spawn*Spawn::create(constVector<FiniteTimeAction*>&arrayOfActions)
- {
- Spawn*ret=nullptr;
- do
- {
- autocount=arrayOfActions.size();
- CC_BREAK_IF(count==0);
- autoprev=arrayOfActions.at(0);
- if(count>1)
- for(inti=1;i<arrayOfActions.size();++i)
- prev=createWithTwoActions(prev,arrayOfActions.at(i));
- }
- }
- else
-
- ret=static_cast<Spawn*>(prev);
- }while(0);
-
- returnret;
- }
众所周知,递归往往会牺牲一些效率,但能换来代码的简洁。
在这两个复合动作中,细节处理得十分优雅,
所有的操作都
只需要针对两个动作实施,多个动作的组合会被自动变换为递归
voidSpawn::update(floattime)
- if(_one)
- _one->update(time);
- if(_two)
- _two->update(time);
-
- Spawn*Spawn::reverse()const
- returnSpawn::createWithTwoActions(_one->reverse(),_two->reverse());
- }
延时( DelayTime )
DelayTime 是一个"什么都不做"的动作,类似于音乐中的休止符,
用来表示动作序列里一段空白期,通过占位的方式将不
同的动作段串接在一起。
实际上,这与一个定时期实现的延迟没有区别,
但相比之下,使用 DelayTime 动作来延时就可
以方便地利用动作序列把一套动作连接在一起。
DelayTime 只提供了一个工程方法,如下所示:
staticDelayTime*create(floatd);
其中仅包含一个实型参数d,表示动作占用的时间。
变速动作
大部分动作的变化过程是与时间成线性关系的,即一个动作经过相同时间产生的变化相同,
例如,MoveBy 会使节点在同
样长的时间内经过同样的位移。
这是因为 Cocos2d-x 把动作的速度变化控制抽离了出来,形成一个独立的机制。
普通动作配合
变速动作,可以构造出很有趣的动作效果。
与复合动作类似,变速动作也是一种特殊的动作,它可以把任何一种动作按照改变后的速度执行。
因此,在初始化变速动
作时,需要传入一个动作。
变速动作包括 Speed 动作与 Ease 系列动作,下面来详细介绍这些动作。
Speed
Speed 用于线性地改变某个动作的速度,因此,可以实现成倍地快放或慢放功能。
staticSpeed*create(ActionInterval*action,87); font-weight:bold; background-color:inherit">floatspeed);
为了改变一个动作的速度,首先需要将
目标动作包装到 Speed 动作中:
RepeatForever*repeat=RepeatForever::create(animation);
- Speed*speed=Speed::create(repeat,1.0f);
- speed->setTag(action_speed_tag);
- sprite->runAction(speed);
在上面的代码中, 我们创建了一个 animation 动作的 CCRepeatForever 复合动作 repeat, 使动画被不断地重复执行。 然后,
我们又使用 repeat 动作创建了一个 CCSpeed 变速动作。
create 初始化方法中的两个参数分别为目标动作与变速比率。
设置
变速比率为 1,目标动作的速度将不会改变。
最后,我们为 speed 动作设置了一个 tag 属性,并把动作交给精灵,让
精灵执行变速动作。
此处设置的 tag 属性与 Node 的 tag 属性类似,用于从节点中方便地查找动作。
接下来,在需要改变速度的地方,我们通过修改变速动作的 speed 属性来改变动作速度。
下面的代码将会把上面设置的动
画速度变为原来的两倍:
Speed*speed=sprite->getActionByTag(action_speed_tag);
- speed->setSpeed(2.0f);
ActionEase
虽然使用 Speed 能够改变动作的速度,然而它只能按比例改变目标动作的速度。
如果我们要实现动作由快到慢、速度随
时间改变的变速运动,
需要不停地修改它的speed属性才能实现, 显然这是一个很烦琐的方法。
下面将要介绍的ActionEase系列动作通过使用内置的多种自动速度变化来解决这一问题。
ActionEase 系列包含 15 个动作,
它们可以被概括为 5 类动作:指数缓冲、Sine 缓冲、弹性缓冲、跳跃缓冲和回震缓冲。
每一类动作都有 3 个不同时期的变换:In、Out 和 InOut。
下面使用时间变换图像表示每组 ActionEase 动作的作用效果,
其中横坐标表示实际动画时间,纵坐标表示变换后的动画时间。
因此,线性动作的图像应该是一条自左下角到右上角的直
线。
ActionEase 的使用方法与 Speed 类似。以 Sine 缓冲为例,以下代码实现了 InSine 变速运动:
EaseSineIn*sineIn=EaseSineIn::create(action);
- sineIn->setTag(action_sine_in_tag);
- sprite->runAction(sineIn);
为了抽象出独立的旋转跟踪动作,根据精灵的移动路径设置合适的旋转角度。
Action 包含两个重要的方法:step 与 update。
step 方法会在每一帧动作更新时触发,该方法接受一个表示调用时间间
隔的参数 dt,dt 的积累即为动作运行的总时间。
引擎利用积累时间来计算动作运行的进度(一个从 0 到 1 的实数),并调
用 update 方法更新动作。
update 方法是 Action 的核心,它由 step 方法调用,接受一个表示动作进度的参数,
每一个动
作都需要利用进度值改变目标节点的属性或执行其他指令。
自定义动作只需要从这两个方法入手即可,我们通常只需要修
改 update 方法就可以实现简单的动作。
- virtualvoidstep(floatdt);
- /**
- calledonceperframe.timeavaluebetween0and1
- Forexample:
- -0meansthattheactionjuststarted
- -0.5meansthattheactionisinthemiddle
- -1meansthattheactionisover
- */
- voidupdate(floattime);
让动作更平滑流畅
如何让动作看起来更加自然并优雅,
实际上,这是一个涉及玩家注意力的问题。
对于新出现的变化效果,玩家需要时间转移注意力适应这个变化,
而后如果效
果持续稳定、变化不明显,则会降低玩家的注意力,使玩家感觉疲惫。
在这种情况下,一个冗长的匀速动作效果就会造成
游戏不自然不优雅。
不妨为动作添加一些变速效果,将玩家有限的注意力集中到我们希望玩家关注的效果上。
进场动作:由快到慢,快速进入后缓慢停下,在停止前给玩家足够的视觉时间分辨清楚进入的图像。
出场动作:先慢后快,展示了出场趋势和方向后快速移出屏幕,不拖泥带水。
这个变速效果就很自然地交给前面提到的 Ease 系列动作实现了。
针对具体的需求,我们选择了 EaseExponential 动作
来实现变速效果。
Menu*menu=Menu::create(item0,item1,item2,item3,NULL);
- menu->alignItemsVerticallyWithPading(5.0f);
- menu->setPosition(ccp(size.width/2,size.height));
- menu->setTag(menu_pause_tag);
- this->addChild(menu,5);
- MoveTo*move=MoveTo::create(0.5f,Point(size.width/2,size.height/2));
- Action*action=EaseExponentialOut::create(move);
- menu->runAction(action);
Sizesize=Director::getInstance()->getWinSize();
- Menu*menu=(Menu*)this->getChildByTag(menu_pause_tag);
- Pointpoint=Point(size.width/2,size.height+menu->getContentSize().height/2);
- Action*action=EaseExponentialIn::create(move);
- menu->runAction(action);
优雅自然的动作,能加强游戏的表现性,能吸引更多的玩家、、、