添加动作到节点
绘制精灵很有用,但是一张静态图像只是一幅画,而不是一个游戏。为了添加游戏剧本(game play),你需要能够让精灵在屏幕周围移动并执行其他逻辑。Sprite Kit让场景动起来所使用的主要机制是动作。到目前为止,你已经看过了动作子系统的某些部分。现在,是时候更深入地研究如何构造和执行动作了。
一个动作就是定义你想对场景所作的改变的对象。在大多数情况下,一个动作对执行该动作的节点应用其变化。因此,举例来说,如果你想在屏幕上移动精灵,你创建一个移动动作,并告诉精灵节点运行该动作。Sprite Kit自动动态改变精灵的位置直到动作完成。
动作是自包含的对象
每一个动作是一个不透明的(opaque)对象,描述你想对场景作的改变。一切动作都是由SKAction类实现,它没有可见的子类。相反地,不同类型的动作都使用类方法来实例化。例如,下面是你用动作来做的最常见的事情:
• 改变一个节点的位置和方向
• 改变节点的尺寸或缩放属性
• 改变节点的可视性或使其半透明
• 改变一个精灵节点的内容,以便它可以通过一系列的纹理动起来
• 给精灵节点着色
• 播放简单的声音
• 从节点树中移除一个节点
• 自定义动作来调用一个块(block)或调用对象上的选择器(selector)
一旦你创建了一个动作,它的类型就不能再改变,并且你只有有限的能力来改变其属性。Sprite Kit利用动作不可变的性质非常有效地执行它们。
提示: 因为动作是有效不可变的对象,你可以在节点树的多个节点上安全地同时运行相同的动作。出于这个原因,如果你有一个在你的游戏中要反复使用的动作,构建一个单一的动作实例,然后每当你需要一个节点来执行它时再重用它。
动作可以是瞬时的或非瞬时的:
• 瞬时动作在一帧动画内开始并完成。例如,从其父节点中移除节点的动作是一个瞬时动作,因为不能部分地移除一个节点。相反地,执行该动作时,节点会被立即移除。
• 非瞬时动作有一个动画效果的持续时间。在执行时,该动作将在动画的每一帧进行处理,直到动作完成。
用来创建动作类方法的完整列表在SKAction类参考中描述,但你只有在准备好进行详细查看如何配置具体动作时,才需要去那里。
节点运行动作
一个动作只在你告诉一个节点运行它后才会执行。运行一个动作最简单的方法是调用的节点的runAction:方法。清单3-1创建了一个新的移动动作,然后告诉节点来执行它。
清单3-1 运行一个动作
- [SKAction *moveNodeUp = [SKAction moveByX:0.0 y:100.0 duration:1.0];
- [rocketNode runAction:moveNodeUp];
移动动作有一个持续时间,所以这个动作在动画的多个帧中由场景处理,直到流逝的时间超过了动作的持续时间。在动画完成后,动作就从节点中移除。
你可以在任何时候运行动作。然而,如果你添加动作到节点时场景正在处理动作,直到下一帧前新的动作可能不会执行。场景用来处动作的步骤,在“高级场景处理”中有更详细地描述。
一个节点可以同时运行多个动作,即使那些动作在不同的时间执行。场景跟踪每个动作还要多久才完成并计算动作对节点产生的效果。例如,如果你运行两个动作移动相同的节点,这两个动作对每一帧都应用变化。如果移动动作大小相等、方向相反,则该节点将保持静止。
因为动作处理绑定到场景,只有当节点是呈现场景的节点树的一部分时动作才会被处理。你可以这样利用此特性:通过创建一个节点并分配动作给它,但等到以后再添加节点到场景。后来,当节点加入到了场景时,会立即开始执行它的动作。这种模式特别有用,因为在复制节点时,一个节点正在运行的动作也被复制和归档。
如果一个节点在运行任何动作,它的hasActions属性返回YES。
取消运行动作
要取消某个节点正在运行的动作,调用它的removeAllActions方法。所有动作都立即从节点中移除。如果移除动作有持续时间,任何对节点已经作出的更改将保持不变,但不执行进一步的变化。
在动作完成时接收回调
runAction:completion:方法与runAction:方法是相同,但动作完成后,你的块被调用。这个回调只在动作运行到完成时被调用。如果动作完成之前被移除,完成处理程序(handler)永远不会被调用。
使用命名动作来精确控制动作
通常情况下,你看不到某个节点的哪些动作在执行,而如果你想移除动作,你必须移除所有的动作。如果你需要查看特定动作是否在执行或移除一个指定的动作,你必须使用命名动作(named actions)。命名的动作使用一个唯一的键名来识别该动作。你可以启动、移除、查找、更换节点上的命名动作。
清单3-2与 清单3-1相似,但现在的动作用一个键标识,ignition。
清单3-2 运行命名动作
- [SKAction *moveNodeRight = [SKAction moveByX:100.0 y:0.0 duration:1.0];
- [spaceship runAction:moveNodeRight withKey:”ignition”];
以下基于键的方法可用:
• runAction:withKey: 方法用于运行动作。如果已经有一个动作使用相同键的动作在执行,它会在新的动作添加之前先被移除掉。
• actionForKey:方法用于确定是否已经有一个使用那个键的动作在运行。
• removeActionForKey:方法用于移除动作。
清单3-3展示了如何使用一个命名动作来控制精灵的运动。当用户点击场景的内部,方法会被调用。该方法确定点击发生的位置,然后告诉精灵运行一个动作移动到那个位置。提前计算了持续时间,从而使精灵总是表现为以固定的速度在移动。因为此代码使用runAction:withKey:方法,如果精灵已经在移动,之前的移动会在中途停止而新的动作使精灵从当前位置移动到新位置。
清单3-3 移动精灵到最新的鼠标点击位置
- - (void)MouseDown:(NSEvent *)theEvent
- {
- CGPoint clickPoint = [theEvent locationInNode:self.playerNode.parent];
- CGPoint charPos = self.playerNode.position;
- CGFloat distance = sqrtf((clickPoint.x charPos.x)*(clickPoint.x charPos.x)+
- (clickPoint.y charPos.y)*(clickPoint.y charPos.y));
- SKAction *moveToClick = [SKAction moveTo:clickPoint duration:distance/characterSpeed];
- [self.playerNode runAction:moveToClick withKey:@“moveToClick”];
- }
创建运行其他动作的动作
Sprite Kit提供了许多标准的动作类型用来改变在你的场景中的节点的属性。但动作真正的力量是发生在动作结合在一起的时候。你可以通过结合动作创建复杂和有表现力的动画,这些动画仍然通过运行一个单一的动作来执行。一个复合动作与任何基本动作类型的使用同样的容易。考虑到这一点,现在是时候学习序列动作、组动作和重复动作了。
• 序列动作(sequence action)具有多个子动作。序列中的每一个动作在前一个动作结束后开始。
• 组动作(group action)具有多个子动作。存储在该组中的所有动作在同一时间开始执行。
• 重复动作(repeating action)只有一个子动作。当子动作完成后,它重新启动。
序列运行一系列的动作
序列是一个连续运行的动作的集合(set)。当一个节点运行一个序列,动作以连续的顺序触发。当一个动作完成后,立即开始下一个动作。当序列中的最后一个动作完成,序列动作也完成。
清单3-4展示了使用一个其他动作的数组来创建序列。
清单3-4 创建动作的序列
- SKAction *moveUp = [SKAction moveByX:0 y:100.0 duration:1.0];
- SKAction *zoom = [SKAction scaleTo:2.0 duration:0.25];
- SKAction *wait = [SKAction waitForDuration:0.5];
- SKAction *fadeAway = SKAction fadeOutWithDuration:0.25];
- SKAction *removeNode = [SKAction removeFr0mParent]; //(译者注:把0换成o,发布时被告诉“含有危险字符”,坑爹吗!)
- SKAction *sequence = [SKAction sequence:@[moveUp, zoom, wait, fadeAway, removeNode];
- [node runAction:sequence];
在这个例子中有几件事情值得注意:
• wait动作是一个特殊的动作,它通常仅在序列中使用。这个动作只是等待一段时间,然后不做任何事情就结束。等待动作用于控制序列的定时。
• removeNode动作是一个瞬时动作,所以它不花时间来执行。你可以看到,虽然这个动作是序列的一部分,它不会出现在图3-1的时间轴上。作为瞬时动作,在淡入动作完成后它马上开始和结束。然后序列也结束了。
图3-1 move和zoom序列时间表
![](http://www.cocoachina.com/cms/uploads/allimg/130822/4196_130822101725_1.png)
组并行地运行动作
组动作是一组在组执行时就同时开始执行的全部动作的集合(collection)。当你想要动作同时发生时你可以使用组。例如,代码清单3-5中旋转并移动一个精灵形成车轮在屏幕上滚动的错觉。使用组(而不是运行两个独立的动作)强调,这两个动作是密切相关的。
清单3-5 使用一组动作来旋转一个车轮
- SKSpriteNode *wheel = (SKSpriteNode *)[self childNodeWithName:@”wheel”];
- CGFloat circumference = wheel.size.height * M_PI;
- SKAction oneRevolution = [SKAction rotateByAngle:-M_PI*2 duration:2.0];
- SKAction *moveRight = [SKAction moveByX:circumference y:0 duration:2.0];
- SKAction *group = [SKAction group:@[oneRevolution, moveRight];
- [wheel runAction:group];
虽然在组中的动作同时开始,组要直到组中的最后一个动作结束运行时才算完成。清单3-6展示了一个更复杂的组,它包含的动作有不同的时间值。精灵通过纹理形成动画并在屏幕上向下移动两秒。然而,当组执行时精灵放大并从透明淡入到完全可见,以完全可见。图3-2展示了使精灵出现的这两个动作,只完成组的动画的一半。组将继续进行,直到另外两个动作完成。
清单3-6 用不同的时间值创建一组动作
- [sprite setScale:0];
- SKAction *animate = [SKActio animateWithTextures:textures timePerFrame:2.0/numberOfTextures];
- SKAction *moveDown = [SKAction moveByX:0 y:-200 duration:2.0];
- SKAction *scale = [SKAction scaleTo:1.0 duration:1.0];
- SKAction *fadeIn = [SKAction fadeInWithDuration:1.0];
- SKAction *group= [SKAction group:[animate, moveDown, scale, fadeIn];
- [sprite runAction:group];
图3-2 分组动作同时启动,但独立完成
![](http://www.cocoachina.com/cms/uploads/allimg/130822/4196_130822101626_1.png)
重复动作多次执行其他的动作
重复动作允许循环另一个动作,所以可以重复多次。当执行重复动作时,其实是执行它所含动作。每当要循环的动作完成时,它又被重复动作重新启动。清单3-7展示了创建重复动作的方法。你可以创建一个动作重复有限次数或无限次数。
清单3-7 创建重复动作
- SKAction *fadeout = [SKAction fadeOutWithDuration:0.25];
- SKAction *fadeIn = [SKAction fadeInWithDuration:0.25];
- SKAction *pulse = [SKAction seqence:@[fadeout, fadeIn];
- SKAction *pulseFiveTimes = [SKAction repeatAction:pluse count:5];
- SKAction *pulseForever = [SKAction repeatActionForever:pulse];
图3-3展示了的序列的计时安排(timing arrangement)。你可以看到整个序列完成然后重复。
图3-3 重复动作的计时
![](http://www.cocoachina.com/cms/uploads/allimg/130822/4196_130822101551_1.png)
当你重复一组时,整组必须完成之后再重新启动该组。清单3-8创建了一个组,它移动一个精灵并通过纹理形成动画,但在这个例子中,两个动作有不同的持续时间。图3-4展示组重复时的计时图。你可以看到,纹理动画运行完成后,然后直到该组重复前都没有动画发生。
清单3-8 重复一组动画
- SKAction *animate = [SKAction animateWithTextures:textures timePerFrame:1.0/numberOfImages];
- SKAction *moveDown = [SKAction moveByX:0 y:-200 duration:2.0];
- SKAction *group = [SKAction group:@[animate, moveDown]];
图3-4 重复组的计时
![](http://www.cocoachina.com/cms/uploads/allimg/130822/4196_130822101530_1.png)
你可能想要的是,每个动作以它自身的固有频率重复。要做到这一点,只要创建一套重复动作,然后把它们组合在一起。清单3-9展示了你如何实现图3-5所示的计时。
清单3-9 把一套重复动作组合
- SKAction *animate = [SKAction animateWithTextures:textures timePerFrame:1.0/numberOfImages];
- SKAction *moveDown = [SKAction moveByX:0 y:-200 duration:2.0];
- SKAction *repeatAnimation = [SKAction repeatActionForever:animate];
- SKAction *repeatMove = [SKAction repeatActionForever:moveDown];
- SKAction *group = [SKAction group:@[repeatAnimation, repeatMove];
图3-5 每个动作以其固有间隔重复
![](http://www.cocoachina.com/cms/uploads/allimg/130822/4196_130822101433_1.png)
配置动作计时
默认情况下,一个动作的持续时间根据你指定的持续时间线性变化。但是,你可以通过一些属性调整动画的计时:
• 通常情况下,动画动作线性运行。动作的timingMode属性可以用来为动画选择一个非线性的计时模式。例如,你可以让动作快速开始,然后在剩余的持续时间中减速。
• 动作的speed属性改变动画播放的速率。你可以在动画默认计时上加速或减速。
speed值为1.0是正常的速度。如果动作的speed属性设置为2.0,当节点执行动作时,它速度快了一倍。要暂停动作,将值设置为0。
如果你调整那些包含其他动作(例如组、序列或重复动作)的动作的速率,速率会应用到所包含的动作。附加的动作也会受到它们自己的speed属性的作用。
• 节点的speed属性与动作的speed属性具有相同的效果,但该速率适用于该节点或景树中的任意后代所处理的所有动作。
Sprite Kit通过找到所有应用于该动作的速率并将它们相乘,决定应用于动画的速率。
使用动作的提示
动作最好的工作方式是,你创建一次然后使用多次。只要有可能,提早创建动作,并将它们保存在一个很容易地检索和执行的位置。
根据动作的类型,以下这些位置可能都是有用的:
• 节点userData属性
• 父节点的userData属性,如果几十个节点的共享同样的动作和相同的父节点
• 场景的userData的属性,动作由场景中多个节点共享
• 如果子类化,就用子类的userData属性
如果你需要设计师或美工输入节点的属性如何生成动画,可以考虑把动作创建代码移到你的自定义设计工具中。然后归档动作,并加载它到你的游戏引擎。欲了解更多信息,请参阅“Sprite Kit最佳实践”。
什么时候你不应该使用动作
虽然动作非常有效,它们不是免费的。创建动作并执行它是有成本的。如果你打算在动画的每一帧改变节点的属性,而这些变化在每帧都需要重新计算,你最好直接改变节点而不使用动作来做这些。欲了解更多关于你可能在你的游戏的哪些地方要这样做的信息,请参阅“高级场景处理。”
试试这个!
这里有一些东西可以用动作试试:
• 探索SKAction类参考,并对你的精灵尝试各种不同的动作。
• 创建一个动作组,让它使用其他动作同步移动屏幕上的一个精灵,比如,通过一系列精灵图像生成动画或旋转精灵。
• 使用命名动作创建可撤销的动作。将这些动作与你的用户界面代码连接起来。
• 创建序列,用它讲述了一个有趣的故事。例如,考虑在你的游戏启动时创建可活动的标题画面来显示。