重要提示: 这是API或开发技术的一个初版文档。虽然本文档的技术准确性已被审阅过,但这还不是最终版本。这个苹果的机密信息仅用于适用的苹果开发者计划的注册会员。苹果公司提供这些机密信息来帮助你计划采用本文所述的技术和编程接口。此信息如有变更,根据这份文档实现的软件,应该用最终的操作系统软件和最终文档进行测试。本文档的新版本可能会与API或技术的未来种子一起提供。
Sprite Kit提供了一个图形渲染(rendering)和动画的基础,你可以使用它让任意纹理(textured)图像或精灵动起来。Sprite Kit采用的是传统的渲染循环,允许在渲染前处理每一帧的内容。你的游戏确定场景的内容,以及这些内容如何在每帧中变化。Sprite Kit做的工作,就是有效地利用图形硬件来渲染动画的帧。Sprite Kit优化到本质上允许对动画每一帧的任意修改。
Sprite Kit还提供了其他对游戏非常有用的功能,包括基本的声音播放支持和物理模拟。此外,Xcode中提供了内置的Sprite Kit支持,可以很容易地创建并在你的应用程序中使用复杂的特效和纹理图册(atlases)。这种框架和工具的组合,使Sprite Kit对于游戏和其他需要类似动画的应用程序是一个很好的选择。对于其他类型的用户界面动画,使用Core Animation代替。
Sprite Kit还提供了裁剪和其它特效的支持,允许你对全部或者部分内容应用这些效果。你可以在每一帧中活动(animate)或者改变这些元素。你也可以附加物理主体到将这些元素,使他们正确地支持武装和碰撞。
你的游戏中的内容会被组织成场景(scenes),用SKScene对象代表它们。场景包含精灵和其他要渲染的内容。场景也实现了每帧的逻辑和内容处理。在任何给定的时间内,视图展示一个场景。只要一个场景被呈现出来,它的动画和每帧逻辑会自动执行。
要使用Sprite Kit创建一个游戏,你要创建一个或多个的SKScene类的子类。例如,你可能会创建单独的场景类,用来分别显示主菜单、游戏画面和游戏结束后显示的内容。你可以很容易地在你的窗口中使用一个单一的SKView对象并在不同场景之间进行过渡。
SKScene类实际上是SKNode类的后代。节点是所有内容的基本构建块,而场景对象作为一个节点对象树的根节点。场景及其后代决定哪个内容被绘制以及它渲染的方式。
每个节点的位置在它的父节点定义的坐标系中指定。节点的内容的其他属性也适用于它后代的内容。例如,当一个节点是旋转,所有它的后代也跟着旋转。你可以使用节点树建立一个复杂的图像,然后通过调整最上层节点的属性旋转、缩放并融入整个图像。
SKNode类绘制任何东西,但它对后代应用于它的属性。每一种可绘制内容 由Sprite Kit的不同子类表示。其他的节点子类不直接绘制内容,但修改它们后代的行为。例如,你可以在场景中使用一个SKEffectNode对象对整个子树应用一个核心图像过滤器(Core Image filter)。通过精确控制节点树的结构,你确定节点的渲染顺序,让你可以在一个场景中布局(layer)复杂的图形效果。
所有节点对象都是响应者(responder)对象,派生(descending)自UIResponder或NSResponder,所以你可以继承任何节点类来创建接受用户输入的新类。视图类自动扩展响应链来包含场景的节点树。
纹理是用来渲染精灵的共享图像。当你需要对多个精灵应用相同的图像时,总是使用纹理。通常你通过加载存储在你的应用程序bundle的图像文件来创建纹理。然而,Sprite Kit也可以在运行时从包括核心图形图像在内的其他来源为你创建纹理,或者甚至渲染把节点树成纹理。
Sprite Kit通过处理较低级别的代码需求来加载纹理和并让它们对图形硬件可用,来简化了纹理的管理。纹理管理由Sprite Kit自动管理。但是,如果你的游戏中使用了大量的图像,你可以通过控制部分的过程来提高性能。首先,你通过提示Sprite Kit纹理很快就需要来做这个。
纹理图册是在你的游戏中一起使用的一组相关的纹理。例如,你可以使用一个纹理图册存储让一个角色活动需要的所有纹理或渲染游戏设置级别的背景需要的所有瓷砖。Sprite Kit用纹理图册来提高渲染性能。
使用动作(actions)让场景的内容动起来。每一个动作都是一个对象,由SKAction类定义。你来告诉节点执行动作。然后,当场景处理动画帧,动作就被执行。有些动作在一帧动画内完成,而另一些在完成前应用变化于多帧动画。动作最常见的用途是改变节点的属性。例如,你可以创建动作来移动、缩放或旋转节点,或使其透明。然而,动作也可以更改节点树、播放声音、甚至是执行自定义代码。
场景中也能执行自定义的每帧处理。覆盖你的场景子类的方法来执行额外的游戏任务。例如,如果一个节点需要每帧移动,你可能会直接每帧地调整其属性而不是使用一个动作来这样做。
虽然你可以控制场景中的每一个节点的确切位置,你经常想这些节点互相交流、碰撞并在这个过程中告知速度的变化。你可能还需要模拟重力和其他形式的加速度,这些都不在动作系统中处理的。要做到这一点,你可以创建物理主体(SKPhysicsBody),并将它们附加到你场景中的节点上。每个物理主体由形状、尺寸质量和其他物理特性定义。
当场景中包含物理主体,场景就在这些主体上模拟物理。一些势力(forces),如重力和摩擦力,会自动应用。你也可以对物理主体调用方法来应用自己的势力。每个主体的加速度和速度会被计算,然后主体彼此碰撞。然后,模拟完成后,相应的节点的位置和旋转的被更新。
你物理主体的交互拥有精确的控制。你确定哪些主体被允许相互碰撞 并单独决定哪些交互可以被你的应用程序调用。这些回调允许你勾(hook)到物理模拟中创建其他的游戏逻辑。例如,在一个物理主体被另一个物理主体击中时,你的游戏可能会销毁一个节点。
场景在一个附加的SKPhysicsWorld对象上定义了物理模拟的全局特性。你可以使用物理世界定义整个模拟的重力,定义模拟的速度,并在场景中查找物理主体。你还可以使用物理世界通过一个关节(SKPhysicsJoint)把物理主体连接在一起。连接的主体根据关节的类型模拟在一起。
阅读“深入Sprite Kit”获得实现Sprite Kit游戏的一个概述。然后通过其他章节学习关于Sprite Kit功能的细节。一些章节包含建议的练习,以帮助你开发你对Sprite Kit的理解。学习Sprite Kit的最好方法是实践;把一些精灵放到场景中并实验它们!
Sprite Kit编程指南(1):深入Sprite Kit
深入Sprite Kit
学习Sprite Kit最好的方法是在实践中观察它。此示例创建一对场景和各自的动画内容。通过这个例子,你将学习使用Sprite Kit内容的一些基础技术,包括:
• 场景在一个基于Sprite Kit的游戏中的角色。
• 如何组织节点树来绘制内容。
• 使用动作让场景内容动起来。
• 如何添加交互到场景。
• 场景之间的过渡。
• 在一个场景里模拟物理。
一旦你完成这个项目,你可以用它来试验其他Sprite Kit概念。你可以在这个例子的结尾找到一些建议。
你应该已经熟悉创建iOS应用程序之前通过这个项目工作。欲了解更多信息,请参阅今天开始开发iOS应用程序的。大多数Sprite Kit在这个例子中的代码是相同的OS X。
让我们开始吧
本次练习需要Xcode 5.0。使用的单一视图的应用程序模板创建一个新的iOS应用程序的Xcode项目。
在创建项目时,请使用以下值:
• 产品名称:SpriteWalkthrough
• ClassPrefix:Sprite
• 设备:iPad
添加Sprite Kit框架到项目中。
创建你的第一个场景
Sprite Kit内容被放置在一个窗口中,就像其他可视化内容那样。Sprite Kit内容由SKView类渲染呈现。SKView对象渲染的内容称为一个场景,它是一个SKScene对象。场景参与响应链,还有其他使它们适合于游戏的功能。
因为Sprite Kit内容由视图对象渲染,你可以在视图层次组合这个视图与其他视图。例如,你可以使用标准的按钮控件,并把它们放在你的Sprite Kit视图上面。或者,你可以添加交互到精灵来实现自己的按钮,选择权在你。在这个例子中,稍候你会看到如何实现场景交互。
配置视图控制器来使用Sprite Kit
1.打开项目的storyboard。它有一个单一的视图控制器(SpriteViewController)。选择视图控制器的view对象并把它的类改成SKView。
2.在视图控制器的实现文件添加一个导入行。
- #import <SpriteKit/SpriteKit.h>
3.实现视图控制器的viewDidLoad方法来配置视图。
- - (void)viewDidLoad
- {
- [super viewDidLoad];
- SKView * spriteView =(SKView *)self.view;
- spriteView.showsDrawCount = YES;
- spriteView.showsNodeCount = YES;
- spriteView.showsFPS = YES;
- }
4.代码开启了描述场景如何渲染视图的诊断信息。最重要的一块信息是帧率(spriteView.showsFPS),你希望你的游戏尽可能在一个恒定的帧率下运行。其他行展示了在视图中显示了多少个节点,以及使用多少绘画传递来渲染内容(越少越好)的详情。
接下来,添加第一个场景。
创建Hello场景
1.创建一个名为HelloScene新类并让它作为SKScene类的子类。
2.在你的视图控制器导入场景的头文件。
- #import “HelloScene.h”
3.修改视图控制器来创建场景,并在视图中呈现场景。
- - (void)viewWillAppear:(BOOL)animated
- {
- HelloScene *hello = [[HelloScene alloc] initWithSize:CGSizeMake(768,1024)];
- SKView *spriteView =(SKView *)self.view;
- [spriteView presentScene:hello];
- }
现在,构建并运行项目。该应用程序应该启动并显示一个只有诊断信息的空白屏幕。
将内容添加到场景
当设计一个基于Sprite Kit的游戏,你要为你的游戏界面各主要大块(chuck)设计不同的场景类。例如,你可以为主菜单创建一个场景而为游戏设置创建另一个单独的场景。在这里,你会遵循类似的设计。这第一个场景显示了传统的“Hello World”文本。
大多数情况下,你可以配置一个场景在它被视图首次呈现时的内容。这跟视图控制器只在视图属性被引用时加载他们的视图的方式是类似的。
在这个例子中,代码在didMoveToView:方法内部,每当场景在视图中显示时该方法会被调用。
在场景中显示Hello文本
1.添加一个新的属性到场景的实现文件中来跟踪场景是否已创建其内容。
- @interface HelloScene()
- @property BOOL contentCreated;
- @end
该属性跟踪并不需要向客户端公开的状态,所以,在实现文件中它一个私有接口声明里实现。
2.实现场景的didMoveToView:方法。
- - (self)didMoveToView:(SKView *)view
- {
- if(!self.contentCreated)
- {
- [self createSceneContents];
- self.contentCreated = YES;
- }
- }
每当视图呈现场景时,didMoveToView:方法都会被调用。但是,在这种情况下,场景的内容应只在场景第一次呈现时进行配置。因此,这段代码使用先前定义的属性(contentCreated)来跟踪场景的内容是否已经被初始化。
3. 实现场景的createSceneContents方法。
- - (void)createSceneContents
- {
- self.backgroundColor = [SKColor blueColor];
- self.scaleMode = SKSceneScaleModeAspectFit;
- [self AddChild:[self newHelloNode];
- }
场景在绘制它的子元素之前用背景色绘制视图的区域。注意使用SKColor类创建color对象。事实上,SKColor不是一个类,它是一个宏,在iOS上映射为UIColor而在OS X上它映射为NSColor。它的存在是为了使创建跨平台的代码更容易。
场景的缩放(scale)模式决定如何进行缩放以适应视图。在这个例子中,代码缩放视图,以便你可以看到场景的所有内容,如果需要使用宽屏(letterboxing)。
4. 实现场景的newHelloNode方法。
- - (SKLabelNode *)newHelloNode
- {
- SKLabelNode * helloNode = [SKLabelNode labelNodeWithFontNamed:@“Chalkduster”];
- @helloNode.text =“Hello, World!”;
- helloNode.fontSize = 42;
- helloNode.position = CGPointMake(CGRectGetMidX(self.frame),CGRectGetMidY(self.frame));
- return helloNode;
- }
你永远不用编写显式执行绘图命令的代码,而如果你使用OpenGL ES或Quartz 2D你就需要。在Sprite Kit中,你通过创建节点对象并把它们添加到场景中来添加内容。所有绘制必须由Sprite Kit中提供的类来执行。你可以自定义这些类的行为来产生许多不同的图形效果。然而,通过控制所有的绘图,Sprite Kit可以对如何进行绘图应用许多优化。
现在构建并运行该项目。你现在应该看到一个蓝色屏幕上面有“Hello, World!”。现在,你已经学会了绘制Sprite Kit内容的所有基础知识。
使用动作让场景动起来
静态文本很友好,但如果文字可以动起来,它会更有趣。大多数的时候,你通过执行动作(action)移动场景周围的东西。Sprite Kit中的大多数动作对一个节点应用变化。创建action对象来描述你想要的改变,然后告诉一个节点来运行它。然后,当渲染场景时,动作被执行,在几个帧上发生变化直到它完成。
当用户触摸场景内容,文字动起来然后淡出。
让文本动起来
1.添加以下代码到newHelloNode方法:
- helloName.name = @“helloNode”;
所有节点都有一个名称属性,你可以设置它来描述节点。当你想能够在稍后找到它,或当你想构建基于节点名称的行为时,你应该命名一个节点。稍后,你可以搜索树中与名称相匹配的节点。
在这个例子中,你给标签的一个名称以便稍后可以找到它。在实际的游戏中,你可能会得给呈现相同类型的内容的任何节点以相同的名称。例如,如果你的游戏把每个怪物呈现为一个节点,你可能会命名节点为monster。
2.重载场景类的touchesBegan:withEvent方法。当场景接收到触摸事件,它查找名为helloNode的节点,并告诉它要运行一个简短的动画。
所有节点对象都是iOS上UIResponder 或OS X上NSResponder的 的子类。这意味着你可以创建Sprite Kit节点类的子类来添加交互到场景中的任何一个节点。
- - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
- {
- SKNode *helloNode = [self childNodeWithName:@“helloNode”];
- If(helloNode != nil)
- {
- helloNode.name = nil;
- SKAction *moveUp = [SKAction moveByX:0 y:100.0 duration:0.5];
- SKAction *zoom = [SKAction scaleTo:2.0 duration:0.25];
- SKAction *pause = [SKAction waitForDuration:0.5];
- SKAction *fadeAway = SKAction fadeWithDuration:0.25];
- SKAction *remove = [SKAction removeFromParent];
- SKAction * moveSequence = [SKAction sequence:@[moveUp, zoom, pause, fadeAway, remove];
- [helloNode runAction:moveSequence];
- }
- }
为了防止节点响应重复按压,代码会清除节点的名称。然后,它构建动作对象来执行各种操作。最后,它组合这些动作创建一个动作序列;序列运行时,按顺序执行每个动作。最后,它告诉标签节点执行序列动作。
运行的应用程序。你应该看到像之前那样的文字。在屏幕的底部,节点计数应该是1。现在,点击视图内部。你应该看到文字动画并淡出。在它淡出后,节点计数应该变为0,因为节点已从父节点中删除。
场景之间的转换
Sprite Kit让场景之间的过渡变得很容易。场景之间的过渡时,你可以坚持保留它们,或清除它们。在这个例子中,你将创建第二个场景类,来学习一些其他的游戏行为。“Hello, World!”文字从屏幕上消失时,代码创建一个新的场景并过渡到它。Hello场景过渡在后会被丢弃。
创建飞船场景
1.创建一个名为SpaceshipScene的新类并让它成为SKScene类的子类。
2.实现代码来初始化飞船场景的内容。此代码类似于你为HelloScene类实现的代码。
- @interface SpaceshipScene()
- @property BOOL contentCreated;
- @end
- @implementation SpaceshipScene
- - (void)didMoveToView:(SKView *)view
- {
- If(!self.contentCreated)
- {
- [self createSceneContents];
- self.contentCreated = YES;
- }
- }
- - (void)createSceneContents
- {
- self.backgroundColor = [SKColor blackColor];
- self.scaleMode = SKSceneScaleModeAspectFit;
- }
3. 在HelloScene.m文件中导入SpaceshipScene.h头。
- #import “SpaceshipScene.h”
4. 在touchesBegan:withEvent方法中,更改runAction:的调用为新的调用runAction:completion:。实现完成处理来创建并呈现一个新的场景。
- [helloNode runAction:moveSequence completion:^ {
- SKScene * spaceshipScene = [[SpaceshipScene alloc] initWithSize:self.size];
- SKTransition *doors= [SKTransition doorsOpenVerticalWithDuration:0.5];
- [self.view presentScene:spaceshipScene transition:doors];
- }];
构建并运行该项目。当你触摸场景内部时,文字淡出,然后在视图过渡到新的场景。你应该看到一个黑色的屏幕。
使用节点构建复杂的内容
新的场景还没有任何内容,所以你准备要添加一个飞船到场景。要构建这个太空飞船,你需要使用多个SKSpriteNode对象来创造了飞船和它表面的灯光。每个精灵节点都将执行动作。
精灵节点是在一个Sprite Kit应用程序中最常见用于创建内容的类。他们可以绘制无纹理或纹理的矩形。在这个例子中,你要使用无纹理对象。稍后,这些占位符(placeholder)可以很容易地用纹理精灵进行替换,而不改变它们的行为。在实际的游戏中,你可能需要几十个或上百个节点来创建你的游戏的可视化内容。但是,从本质上说,那些精灵将使用与这个简单的例子相同的技术。
虽然你可以直接添加所有三个精灵到场景,但这并不是Sprite Kit的方式。闪烁的灯光是飞船的一部分!如果飞船移动,灯光应该和它一起移动。解决的办法是使飞船节点成为它们的父节点,同样地场景将是飞船的父节点。光的坐标将要相对于父节点的位置来指定,而父节点是在子精灵图像的中心。
添加飞船
1. 在SpaceshipScene.m中,添加代码到createSceneContents方法来创建飞船。
- SKSpriteNode *spaceship = [self newSpaceship];
- spaceship.position = CGPointMake(CGRectGetMidX(self.frame),CGRectGetMidY(self.frame)-150);
- [self addChild:spaceship];
2. 实现newSpaceship的方法。
- - (SKSpriteNode *)newSpaceship
- {
- SKSpriteNode *hull= [[SKSpriteNode alloc] initWithColor:[SKColor grayColor] size:CGSizeMake(64,32);
- SKAction *hover= [SKAction sequence:@[
- [SKAction waitForDuration:1.0]
- [SKAction moveByX:100 y:50.0 duration:1.0]
- [SKAction waitForDuration:1.0]
- [SKAction moveByX:-100.0 y:-50 duration:1.0]];
- [hull runAction:[SKAction repeatActionForever:hover];
- return hull;}
此方法创建飞船的船体,并添加了一个简短的动画。需要注意的是引入了一种新的动作。一个重复的动作不断地重复的传递给它的动作。在这种情况下,序列一直重复。
现在构建并运行应用程序来看当前的行为,你应该看到一个矩形。
在建立复杂的有孩子的节点时,把用来在构造方法后面或者甚至是在子类中创建节点的代码分离出来,是一个很好的主意。这使得它更容易改变精灵的组成和行为,而无需改变使用精灵的客户端(client)。
3.添加代码到newSpaceship方法来添加灯光。
- SKSpriteNode *light1= [self newLight];
- light1.position = CGPointMake(-28.0,6.0);
- [hull addChild:light1];
- SKSpriteNode *light2= [self newLight];
- Light2.position = CGPointMake(28.0,6.0);
- [hull addChild:light2];
4.实现newLight方法。
- - (SKSpriteNode *)newLight
- {
- SKSpriteNode *light = [[SKSpriteNode alloc] initWithColor:[SKColor yellowColor] size:CGSizeMake(8,8)];
- SKAction *blink= [SKAction sequence:@ [
- [SKAction fadeOutWithDuration:0.25]
- [SKAction fadeInWithDuration:0.25]];
- SKAction * blinkForever = [SKAction repeatActionForever:blink];
- [light runAction:blinkForever];
- return light;
- }
当你运行应用程序时,你应该看到一对灯在飞船上。当飞船移动,灯光和它一起移动。这三个节点全都是连续动画。你可以添加额外的动作,让灯光在船的周围移动,它们总是相对船体移动。
创建能交互的节点
在实际的游戏中,你通常需要节点之间能交互。把行为添加给精灵的方法有很多,所以这个例子仅展示其中之一。你将添加新节点到场景,使用物理子系统模拟它们的运动并实现碰撞效果。
Sprite Kit提供了一个完整的物理模拟,你可以使用它添加自动行为到节点。也就是说,物理在使其移动的节点上自动模拟,而不是在节点上执行动作。当它与物理系统一部分的其他节点交互时,碰撞自动计算并执行。
添加物理模拟到飞船场景
1.更改newSpaceship方法来添加一个物理体到飞船。
- hull.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:hull.size];
构建并运行应用程序。等一下!飞船垂直坠落到屏幕下方。这是因为重力施加到飞船的物理体。即使移动动作仍在运行,物理效果也被应用到飞船上。
2.更改的newSpaceship方法来防止飞船受物理交互影响。
- hull.physicsBody.dynamic = NO;
当你现在运行它时,应用程序像之前那样运行。飞船不再受重力影响。稍后,这也意味着飞船的速度将不会受到碰撞的影响。
3.添加代码到createSceneContents方法来生成大量岩石。
- SKAction * makeRocks = [SKAction sequence:@ [
- [SKAction performSelector:@selector(addRock) onTarget:self]
- [SKAction waitForDuration:0.10 withRange:0.15]
- ]];
- [self runAction:[SKAction repeatActionForever:makeRocks];
场景也是一个节点,因此它也可以运行动作。在这种情况下,自定义操作调用场景上的方法来创建岩石。序列创建一个岩石,然后等待一段随机时间。重复这个动作,场景不断产生大量新的岩石。
4.实现addRock方法。
- static inline:CGFloat skRandf() {
- return rand()/(CGFloat)RAND_MAX;
- }
- static inline CGFloat skRand(CGFloat low, CGFloat high) {
- return skRandf()*(high - low) + low;
- }
- - (void)addRock
- {
- SKSpriteNode *rock = [[SKSpriteNode alloc] initWithColor:[SKColor brownColor] size:CGSizeMake(8,8)];
- rock.position = CGPointMake(skRand(0, self.size.width),self.size.height-50);
- rock.name = @“rock”;
- rock.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:rock.size];
- rock.physicsBody.usesPreciseCollisionDetection = YES;
- [self addChild:rock];
- }
构建并运行该项目。岩石现在应该从场景上方落下来。当一块石头击中了船,岩石从船上反弹。没有添加动作来移动岩石。岩石下落并与船碰撞完全是由于物理子系统的作用。
岩石都很小且移动速度非常快,所以代码指定精确的碰撞,以确保所有的碰撞都检测到。
如果你让应用程序运行了一段时间,帧率会开始下降,即使节点计数仍然很低。这是因为节点的代码仅显示出场景中可见的节点。然而,当岩石落下到场景的底部时,它们继续存在于场景中,这意味着物理还在对它们模拟。最终,有如此多的节点正在处理以致Sprite Kit减慢了。
5. 实现场景中的didSimulatePhysics方法来当岩石移动到屏幕之外时移除它们。
- - (void)didSimulatePhysics
- {
- [self enumerateChildNodesWithName:@“rock” usingBlock:^(SKNode *node, BOOL *stop){
- if (node.position.y <0)
- [node removeFr0mParent];
- }];
- }
每次场景处理一帧,都运行动作和模拟物理。你的游戏可以挂接到这个过程中来执行其他自定义代码。在每一帧,场景将处理物理,然后移除移出屏幕底部的所有岩石。当你运行应用程序时,帧率保持不变。
在场景中,预处理及后处理与动作和物理结合的地方,就是你建立你的游戏的行为的地主。
这就是你第一次体验Sprite Kit!其他一切都是你在这里看到的基本技术的细化。
试试这个!
这里有一些东西,你可以尝试:
• 做一个OS X版本的这个例子。你在视图控制器写的代码,在OS X上通常是在一个应用程序委托中实现。响应代码需要改变来使用鼠标事件而不是触摸事件。但是,代码的其余部分应是相同的。
• 使用纹理精灵呈现船和岩石。(提示:“使用精灵”)
• 尝试在触摸事件的响应中移动飞船。(提示:“添加动作节点”和“构建场景”)。
• 添加额外的图形效果到场景(提示:“使用其他节点类型”)
• 岩石与船舶碰撞时添加其他行为。例如,使岩石发生爆炸。(提示:“模拟物理”)
第二章 使用精灵
精灵是用于创建大部分场景内容的基本构建块,所以在转到其他Sprite Kit节点类之前先了解精灵是有用的。精灵用SKSpriteNode对象表现。一个SKSpriteNode对象,要么绘制成一个由纹理映射(mapped)的矩形,要么绘制成一个彩色无纹理的矩形。纹理精灵更常见,因为它们代表了你把自定义插图引进场景的主要方式。这个自定义插图可能代表你的游戏的人物角色、背景元素甚至是用户界面元素。但基本的策略是一致的。一个美工创建图像,然后你的游戏加载它们作为纹理。然后你用那些纹理创建精灵,并把它们添加到场景中。
创建纹理精灵
创建一个纹理精灵的最简单方法是让Sprite Kit为你创建的纹理和精灵。你把插图存储在应用程序bundle中,然后在运行时加载它。清单2-1显示了这个代码是多么的简单。
清单2-1 从存储在bundle中的图像创建一个纹理的精灵
- SKSpriteNode *spaceship = [SKSpriteNode spriteNodeWithImageNamed:@“rocket.png”];
- spaceship.position = CGPointMake(100,100);
- [self addChild:spaceship];
当你以这种方式创建一个精灵,你可以免费得到很多的默认行为:
• 精灵以匹配纹理大小的框架(frame)来创建。
• 精灵以它的位置为中心来渲染。精灵的frame属性指定的矩形定义了它所涵盖的面积。
• 精灵纹理在帧缓冲区(framebuffer)中是半透明的(alpha-blended)。
• 一个SKTexture对象被创建并附加到精灵上。此纹理对象每当精灵节点在场景中时自动加载纹理数据,它是可见的,而且对渲染场景是必要的。稍后,如果精灵从场景中移除或不再可见,如果需要那些内存用于其他用途,Sprite Kit可以删除纹理数据。这种自动内存管理简化但并不能消除在管理你游戏中的美术资产(art assets)方面你需要做的工作。
默认的行为给你一个有用的基础来创建一个基于精灵的游戏。你已经懂得了足够的知识去添加插图到你的游戏,创建精灵,并运行这些精灵的动作来做一些有趣的事情。随着精灵屏幕内外移动,Sprite Kit尽可能有效地管理纹理和绘制动画的帧。如果这对你已经足够,就花点时间去探索你能对精灵做些什么。或者继续阅读SKSpriteNode类得到更深入的理解。一路上,你将获得其功能以及如何与美工和设计师交流这些功能的深入理解。并且你会学到更高级的使用纹理的方式以及如何提高基于纹理的精灵的性能。
定制纹理精灵
你可以使用精灵的每个属性独立配置四个不同的渲染阶段:
• 可以移动精灵的框架,使纹理中的不同点放置在精灵节点的位置。参阅“使用锚点移动精灵的框架”。
• 可以调整精灵的尺寸。你控制当精灵的尺寸与纹理的尺寸不匹配时纹理如何应用到精灵。参阅“调整精灵的尺寸”。
• 可以在精灵的纹理应用到的精灵时对它着色。请参阅“对精灵着色”。
• 精灵可以使用其他的混合(blend)模式来结合其内容和帧缓冲区的内容。自定义的混合模式对发光(lighting)和其他特效是有用的。请参阅“混合精灵到帧缓冲区中”。
通常情况下,配置精灵执行定位、调整尺寸、着色、混合这四个步骤要根据用于创建精灵纹理的插图。这意味着你很少脱离插图设置属性值。你与美工合作以确保你的游戏配置精灵与插图匹配。
下面是一些也行你可以遵循的策略:
• 在你的项目中用硬编码值创建精灵。这是最快但在长期最不理想的方法,因为这意味着每当美术资产变动时必须更改代码。
• 使用Sprite Kit创建自己的工具,让你可以微调精灵的属性值。一旦你有一个你想要的方式配置的精灵,保存精灵到归档中。你的游戏在运行时使用归档创建精灵。
• 在存储在你的应用程序bundle的属性列表中存储配置数据。当精灵加载时,加载属性列表并使用它的值来配置精灵。这允许美工提供正确的各个值并在不改变代码的情况下进行更改。
使用锚点移动精灵的框架
默认情况下,精灵的框架及其纹理的中心定位在精灵的位置上。然而,你可能想纹理的不同部分出现在节点的位置。经常要作出这样的决定因为纹理描绘的游戏元素不是纹理图像的中心。
精灵节点的的anchorPoint属性决定框架的哪一点定位在精灵的位置上。锚点在单位坐标系(unit coordinate system)中指定,如图2-1所示。单位坐标系的原点位于框架的左下角,而(1,1)位于框架的右上角。精灵的锚点默认为(0.5,0.5),对应于框架的中心。
图2-1 单位坐标系
虽然你想要移动框架,你这样做是因为你想纹理的相应部分处于位置的中点。图2-2显示了一对纹理图像。在第一个图像中,默认的锚点在纹理位置的中心。第二个相反地,选择了图像的顶部一个点。你可以看到,当精灵旋转时纹理图像会围绕这一点旋转。
图2-2 改变精灵的锚点
清单2-2显示了如何将火箭的锚点放在前锥体处。通常,你在精灵初始化时设置锚点,因为它与插图对应。然而,你可以在任何时候设置此属性。框架会被立即更新,并且屏幕上的精灵会在场景下一次渲染时更新。
清单2-2 设定精灵的锚点
- rocket.anchorPoint:= CGPointMake(0.5,1.0);
调整精灵的尺寸
精灵的frame属性的尺寸是由其他三个属性的值决定的:
• 精灵的size属性指定精灵基准(无缩放)尺寸。当一个精灵使用代码清单2-1初始化时,这个属性的值被初始化为于精灵的纹理的尺寸相等。
• 然后基准尺寸根据精灵从SKNode类继承来的xScale与yScale属性进行缩放。
例如,如果精灵的基准尺寸是32 × 32像素,而它的xScale的值为1.0且yScale的值为2.0,精灵的框架的尺寸是32 × 64像素。
注:场景中精灵的祖先的缩放值也用于缩放精灵。这将改变精灵的有效尺寸,而不改变它的实际框架的值。请参阅“节点的很多属性适用于其后代”。
当精灵的框架大于它的纹理时,纹理被拉伸以覆盖框架。一般情况下,纹理会在整个框架中被均匀地拉伸,如在图2-3中所示。
图2-3 纹理位伸以覆盖精灵的框架
然而,有时你想使用精灵构建用户界面元素,如按钮或健康指示器。通常,这些元素包含固定大小的元素,如结束端点(end caps),它不应该被拉伸。在这种情况下,使部分的纹理不拉伸,然后拉伸纹理框架剩下的其余部分。
精灵的centerRect属性控制缩放行为,该属性在纹理的单位坐标中指定。默认值是一个覆盖整个纹理的矩形,这就是为什么整个纹理被拉伸到整个框架的原因。如果指定了一个只涵盖了部分的纹理的矩形,你就创建了一个3x3的网格。在网格中的每个盒子有其自己的缩放行为:
• 网格的四个角中的纹理绘制的部分不进行任何缩放。
• 网格的中心在两个方向缩放。
• 中间的上下部分仅水平缩放。
• 中间的左右部分仅垂直缩放。
图2-4展示了一个纹理的特写视图,你可能会用它来绘制用户界面按钮。实际元素是28点×28点。四个角是12×12像素而中心是4×4像素。
图2-4 可伸缩的按钮纹理
清单2-3显示了这个按键精灵将如何初始化。centerRect属性根据纹理的中心矩形来计算。
清单2-3 设置中心矩形以调整拉伸行为
- SKSpriteNode *button = [SKSpriteNode spriteWithImageNamed:@”stretchable_button.png”];
- button.centerRect = CGRectMake(12.0/28.0,12.0/28.0,4.0/28.0,4.0/28.0);
- ....
图2-5展示了即使在该按钮以不同的大小绘制时四个角仍保持不变。
图2-5 对不同尺寸的按钮应用按钮纹理
对精灵着色
在把纹理应用到精灵之前,你可以使用color和colorBlendFactor属性对它着色。默认情况下的颜色混合因子为0.0,这表明纹理未经更改地使用。当你增加这个数字,更多的纹理颜色就会被混合颜色替换。例如,在你的游戏中的怪物受到伤害时,你可能要添加一个红色的色调(tint)给角色。清单2-4显示了如何将色调应用于精灵。
清单2-4 着色精灵的颜色
- monsterSprite.color = [SKColor redColor];
- monsterSprite.colorBlendFactor = 0.5;
图2-6 上色调整纹理的颜色
你也可以使用动作让颜色和颜色混合因素成为动画。清单2-5显示了如何短暂地给精灵调色,然后让它恢复正常。
清单2-5 颜色变化的动画
- SKAction *pulseRed= [SKAction sequence:@[ [SKAction colorizeWithColor:[SKColor redColor] colorBlendFacto:1.0 duration:0.15]]];
- [SKAction waitForDuration:0.1],
- [SKAction colorizeWithColorBlendFactor:0.0 duration:0.15];
- [monsterSprite runAction:pulseRed];
融合精灵到帧缓冲区
渲染的最终阶段是把精灵的纹理融合(blend)到其目标帧缓冲区。默认行为使用纹理的alpha值融合纹理与目标像素。但是,当你想添加其他的特效到场景时你可以使用其他融合模式。
你可以使用BlendMode属性来控制精灵的融合行为。例如,附加混合模式在把多个精灵结合在一起时很有用,比如开枪(fire)或发光(lighting)。清单2-6显示了如何使用附加混合改变混合模式。
清单2-6 使用附加混合模式模拟发光
- lightFlareSprite.blendMode = SKBlendModeAdd;
使用纹理对象
虽然Sprite Kit可以在你创建一个精灵时为你创建纹理,但在一些更复杂的游戏中,你需要对纹理有更多的控制。例如,你可能需要做以下任何一项:
• 多个精灵之间共享一个纹理。
• 在精灵创建后更改它的纹理。
• 通过一系列的纹理让精灵动起来。
• 用不直接存储在应用程序bundle中的数据创建纹理。
• 把节点树渲染成纹理。例如,你可能要对游戏设置进行截屏,在玩家完成了关卡(level)后展示给他或她。
你通过直接使用SKTexture对象可以做所有这些事情。纹理对象是可应用于精灵的可复用的图像。你可以创建纹理对象独立于创造精灵。然后,你可以使用纹理对象来创建新的的精灵或改变现有精灵的纹理。它跟Sprite Kit为你创建纹理相似,但你对过程有更多的控制权。
从存储在App Bundle的图像创建纹理
清单2-7显示了一个类似清单2-1中显示的例子,但使它用纹理对象。在这种情况下,代码一次创建了多支火箭,全部来自相同的纹理。通常情况下,你会加载一次纹理,并保持对它的强引用,以便每次需要创建一个新的精灵时都可以使用它。
清单2-7 从bundle中加载纹理
- SKTexture *rocketTexture = [SKTexture textureWithImageNamed:@“rocket.png”];
- for(int i = 0; i<10; i++){
- SKSpriteNode *rocket = [SKSpriteNode spriteNodeWithTexture:rocketTexture];
- rocket.position = [self randomRocketLocation];
- [self addChild:rocket];
- }
纹理对象本身只是实际的纹理数据的一个占位符。纹理数据占用(intensive)更多的资源,所以当使用它的精灵在屏幕上且可见时,Sprite Kit只保存它在内存中。
使用纹理图册收集相关的美术资产
通常情况下,存储在你的应用程序bundle的美术资产是不相干的图像,却是一起用于相同精灵 的图像的集合。例如,下面是一些常见的美术资产的集合:
• 一个角色的动画帧
• 用来创建游戏关卡或者迷宫的地形瓦片(terrain tiles)
• 用于用户界面控件的图像,如按钮、开关和滑块
如果你把这些逻辑分组看成单独的纹理,Sprite Kit和图形硬件必须运行得更加艰难来渲染场景,而且游戏的性能可能会受到影响。所以,Sprite Kit使用纹理图册把相关的图像收集起来。你指定哪些资产一起使用,然后Xcode会自动构建纹理图册。然后在你的游戏加载纹理图册时,Sprite Kit可以更好地管理性能和内存使用。
创建一个纹理图册
Xcode可以自动为你从图像集合构建纹理图册。欲了解更多信息,请参阅纹理图册帮助。
在创建一个纹理图册时,在收集太多的纹理与太少的纹理到图册之间,有一个平衡的做法。如果你使用的项目数量不足,那么纹理之间切换的开销可能仍然太大。如果你把太多的图像放在一个单一的图册中,那更多的纹理数据会存储在内存中。因为Xcode为你构建图册,它可以相对容易地在不同的图册配置之间切换。对你的纹理图册不同的配置做实验,并选择为你提供最佳性能的结合。
加载纹理纹理图册
清单2-7中的代码,也可以用来从纹理图册中加载纹理。Sprite Kit首先搜索指定的文件名的图像文件,但如果它没有找到,那么它会在内置到应用程序bundle里面任何纹理图册内部搜索。这意味着,在你的游戏中你不必作出任何编码的更改来支持它。此设计还为美工提供了这样的能力,试验新的纹理而不需要重新构建(rebuild)你的游戏。美工把纹理拖放到应用程序bundle中,就可以自动发现它们(覆盖任何之前内置到纹理图册的版本)。一旦美工对纹理满意了,然后你就可以将它们添加到项目中且合并到你的纹理图册中。
如果你想显式使用纹理图册,你可以使用SKTextureAtlas类。首先,你使用图册的名称创建一个纹理图册对象。然后,使用图册中存储的图像文件的名字查看各自的纹理。清单2-8显示了一个这样的例子。它采用了纹理图册装截一个角色的多个动画帧。代码加载这些帧,并将它们存储在一个数组中。
清单2-8 加载散步动画的纹理
- SKTextureAtlas *atlas = [SKTextureAtlas atlasNamed:@“monster.atlas”];
- SKTexture *f1 = [atlas textureNamed:@”master-walk1.png”];
- SKTexture *f2 = [atlas textureNamed:@”master-walk2.png”];
- SKTexture *f3 = [atlas textureNamed:@”master-walk3.png”];
- SKTexture *f4 = [atlas textureNamed:@”master-walk4.png”];
- NSArray *monsterWalkTextures = @[f1,f2,f3,f4];
从纹理的小部分创建纹理
如果你已经有一个SKTexture对象,你可以创建新的纹理引用它的一部分。这是非常有效的,因为新的纹理对象引用内存中相同的纹理数据。这个功能跟纹理图册是类似的。通常情况下,如果你的游戏已经有了自己的自定义纹理图册格式,你就可以这样使用。在这种情况下,你负责存储这些存储在自定义纹理图册中的各个图像的坐标。
清单2-9显示了如何提取部分的纹理。矩形的坐标在单位坐标空间中。
代码清单2-9 使用纹理的一部分
- SKTexture *bottomLeftTexture = [SKTexture textureWithRect:CGRectMake(0.0,0.0,0.5,0.5) inTexture:cornerTextures];
其他创建纹理的方法
除了从应用程序bundle加载纹理,你还可以从其他来源创建纹理:
• 使用SKTexture初始化方法通过内存中正确格式化的像素数据、核心图像或对现有的纹理应用一个Core Image过滤器来创建纹理。
• SKView类的textureFromNode方法可以把一个节点树的内容渲染成纹理。纹理被指定好尺寸,以便它可以包含节点的内容和所有它的可见后代节点。
当你从应用程序bundle中的文件之外的其他来源创建一个纹理时,纹理数据不能被清除,因为Sprite Kit不保留用于生成纹理的原始数据的引用。基于这个原因,你应该有节制地使用这些纹理。一旦不再需要它们,马上移除对它们的强引用。
更改精灵的纹理
精灵的texture属性指向它当前的纹理。你可以将此属性更改为指向一个新的纹理。下一次场景渲染一个新的帧时,它会用新的纹理来渲染。每次你更改纹理时,为了与新的纹理一致,你可能还需要更改其他的精灵属性,如size、anchorPoint和centerRect。一般,确保所有的插图都一致会更好,这样相同的值可用于所有的纹理会。也就是说,纹理应该有一个一致的大小和锚点定位,让你的游戏并不需要更新纹理以外的其他任何东西。
因为动画是一个非常常见的任务,你可以使用动作让一个精灵的一系列纹理都动起来。清单2-10中的代码显示了如何使用清单2-8创建的动画帧数组让精灵的纹理动起来。
清单2-10 通过一系列的纹理形成动画
- SKAction *walkAnimation = [SKAction animateWithTextures:monsterWalkTextures timePerFrame:0.1];
- [monster runAction:walkAnimation];
- / /在这里插入其他代码来移动怪物。
Sprite Kit提供了渠道(plumbing),让你的活动或改变精灵的纹理。它不利用你的动画系统的特定设计。但是,这意味着你需要决定精灵可能会需要什么样的动画,并设计自己的动画系统来让这些动画在运行时切换。例如,一个怪物可能有步行,战斗,停顿(idle)和死亡的动画序列。你的代码来决定何时在这些序列之间切换。
预加载纹理来提高帧率
使用Sprite Kit的一个主要优点是它自动为你执行了大量的内存管理。Sprite Kit从图像文件加载纹理,将这些数据转换成图形硬件可以使用的格式,并将其上传到图形硬件。Sprite Kit很擅长于确定当前帧纹理是否需要渲染。如果纹理不在内存中,它会加载纹理。如果纹理在内存中并且有一段时间没有使用,纹理数据会被丢弃,以便可以加载其他需要的纹理。
如果一次有太多没加载纹理的精灵变为可见,它可能无法在一个单一的动画帧内加载所有这些纹理。纹理加载的延迟可能会导致帧速率突然丢失,这是对用户可见的。Sprite Kit提供了在精灵变为可见之前预加载纹理的选项。因为你非常熟悉你的游戏的设计,你往往更清楚地知道什么时候即将要使用一套新的纹理。例如,在一个滚动的游戏中,当用户在宇宙间移动时,你知道玩家即将进入宇宙的哪一部分。然后你可以在动画的每一帧加载三两个纹理,这样当玩家到达那里时纹理已经在内存中了。清单2-11显示了如何加载纹理。
清单2-11 预加载纹理
- [newSectionTexture preload];
预加载代码的正确设计要依赖于你的游戏的引擎。这里有两种可能设计要考虑:
• 当玩家开始一个新的关卡,预加载这个关卡的所有纹理。游戏被划分成各个关卡,每个关卡能保持所有纹理资产同时在内存中。这保证了所有纹理在游戏开始前就加载好,消除任何纹理加载的延迟。
• 如果一个游戏需要比可以适合内存更多的纹理,你需要动态地预加载纹理。通常,这意味着当你能确定它很快就需要会才预加载纹理。例如,在赛车游戏中,玩家总是在在同一方向移动,所以你预加载
玩家即将看到的部分赛道的纹理。纹理在后台加载,取代赛道中最旧的纹理。在一个允许玩家时刻控制的冒险游戏中,你可能必须临时加载更多的纹理。
创建彩色精灵
虽然纹理精灵是使用SKSpriteNode类的最常见的方式,你也可以不用精灵创建精灵节点。类的在精灵缺乏纹理时发生变化:
• 没有纹理可拉伸,所以centerRect参数被忽略。
• 没有着色步骤,color属性用作精灵的颜色。
• 颜色的alpha分量被用来确定精灵如何融合到缓冲区。
其他属性(size、anchorPoint和blendMode)照旧不变。
试试这个!
现在你对精灵知道更多了,请尝试以下一些活动:
• 在一个纹理图册中添加插图到你的项目。请参阅“创建一个纹理图册”。
• 加载纹理图册,并用它来创建新的精灵。请参阅“载入纹理从纹理图册”。
• 通过多帧动画让精灵动起来。请参阅清单2-10。
• 更改你的精灵的属性,看看它们的绘图行为怎么变化。请参阅“自定义纹理精灵。”
你可以在Sprite Tour示例中找到一些有用的代码。
Sprite Kit编程指南(3):添加动作到节点
添加动作到节点
绘制精灵很有用,但是一张静态图像只是一幅画,而不是一个游戏。为了添加游戏剧本(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];
- SKAction *sequence = [SKAction sequence:@[moveUp, zoom, wait, fadeAway, removeNode];
- [node runAction:sequence];
在这个例子中有几件事情值得注意:
• wait动作是一个特殊的动作,它通常仅在序列中使用。这个动作只是等待一段时间,然后不做任何事情就结束。等待动作用于控制序列的定时。
• removeNode动作是一个瞬时动作,所以它不花时间来执行。你可以看到,虽然这个动作是序列的一部分,它不会出现在图3-1的时间轴上。作为瞬时动作,在淡入动作完成后它马上开始和结束。然后序列也结束了。
图3-1 move和zoom序列时间表
组并行地运行动作
组动作是一组在组执行时就同时开始执行的全部动作的集合(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 分组动作同时启动,但独立完成
重复动作多次执行其他的动作
重复动作允许循环另一个动作,所以可以重复多次。当执行重复动作时,其实是执行它所含动作。每当要循环的动作完成时,它又被重复动作重新启动。清单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 重复动作的计时
当你重复一组时,整组必须完成之后再重新启动该组。清单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 重复组的计时
你可能想要的是,每个动作以它自身的固有频率重复。要做到这一点,只要创建一套重复动作,然后把它们组合在一起。清单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 每个动作以其固有间隔重复
配置动作计时
默认情况下,一个动作的持续时间根据你指定的持续时间线性变化。但是,你可以通过一些属性调整动画的计时:
• 通常情况下,动画动作线性运行。动作的timingMode属性可以用来为动画选择一个非线性的计时模式。例如,你可以让动作快速开始,然后在剩余的持续时间中减速。
• 动作的speed属性改变动画播放的速率。你可以在动画默认计时上加速或减速。
speed值为1.0是正常的速度。如果动作的speed属性设置为2.0,当节点执行动作时,它速度快了一倍。要暂停动作,将值设置为0。
如果你调整那些包含其他动作(例如组、序列或重复动作)的动作的速率,速率会应用到所包含的动作。附加的动作也会受到它们自己的speed属性的作用。
• 节点的speed属性与动作的speed属性具有相同的效果,但该速率适用于该节点或景树中的任意后代所处理的所有动作。
Sprite Kit通过找到所有应用于该动作的速率并将它们相乘,决定应用于动画的速率。
使用动作的提示
动作最好的工作方式是,你创建一次然后使用多次。只要有可能,提早创建动作,并将它们保存在一个很容易地检索和执行的位置。
根据动作的类型,以下这些位置可能都是有用的:
• 节点userData属性
• 父节点的userData属性,如果几十个节点的共享同样的动作和相同的父节点
• 场景的userData的属性,动作由场景中多个节点共享
• 如果子类化,就用子类的userData属性
如果你需要设计师或美工输入节点的属性如何生成动画,可以考虑把动作创建代码移到你的自定义设计工具中。然后归档动作,并加载它到你的游戏引擎。欲了解更多信息,请参阅“Sprite Kit最佳实践”。
什么时候你不应该使用动作
虽然动作非常有效,它们不是免费的。创建动作并执行它是有成本的。如果你打算在动画的每一帧改变节点的属性,而这些变化在每帧都需要重新计算,你最好直接改变节点而不使用动作来做这些。欲了解更多关于你可能在你的游戏的哪些地方要这样做的信息,请参阅“高级场景处理。”
试试这个!
这里有一些东西可以用动作试试:
• 探索SKAction类参考,并对你的精灵尝试各种不同的动作。
• 创建一个动作组,让它使用其他动作同步移动屏幕上的一个精灵,比如,通过一系列精灵图像生成动画或旋转精灵。
• 使用命名动作创建可撤销的动作。将这些动作与你的用户界面代码连接起来。
• 创建序列,用它讲述了一个有趣的故事。例如,考虑在你的游戏启动时创建可活动的标题画面来显示。
Sprite Kit编程指南(4):构建场景
构建场景
对于场景的使用,你已经学过了很多的东西。这里对重要的事实再快速回顾一下:
· 场景(SKScene对象),用来提供SKView对象要渲染的内容。
· 场景的内容被创建成树状的节点对象。场景是根节点。
· 在场景由视图呈现时,它运行动作并模拟物理,然后渲染节点树。
· 你可以通过子类化SKScene类创建自定义的场景。
心中有了这些基本概念之后,是时候来学习更多关于节点树和建设场景的知识了。
节点给子节点提供坐标系
当一个节点被放置在节点树中时,它的position属性把它定位在由它的父节点提供的坐标系内。Sprite Kit在iOS和OS X中使用相同的坐标系。图4-2展示了Sprite Kit的坐标系。与UIKit或AppKit一样,坐标值用点来测量;如果必要,在渲染场景时会把点转换为像素。正数的x坐标在右边而正数的y坐标在屏幕上方。
图4-1 SpriteKit坐标系
Sprite Kit还有一个标准的旋转约定(rotation convention)。图4-2展示了相反的坐标约定。弧度为0的角指定正x轴。沿反时针方向是正角度。
图4-2 旋转坐标约定
当你的仅使用Sprite Kit代码时,一致的坐标系意味着你可以轻松地在游戏的iOS和OS X版本之间共享代码。然而,它更意味着当你编写特定OS专用(OS-specific)的用户界面代码时,你可能需要在操作系统的视图坐标约定与Sprite Kit坐标系之间进行转换。最常见的情况就是使用iOS视图,它们有一个不同的坐标约定。
只有某些节点包含内容
不是所有的节点都绘制内容。例如,SKSpriteNode类绘制一个精灵,但SKNode类不画任何东西。读取某个指定节点对象的frame属性,你就可以知道它是否绘制内容。节点在父节点的坐标系中绘制,frame代表了它在该坐标系中的可视区域。如果节点绘制内容,frame具有一个非零的尺寸。对于场景,frame总是反映场景坐标空间中的可见部分。
如果一个节点有绘制内容的后代节点,节点的子树也有可能提供内容,即使它本身并不提供任何内容。你可以调用节点的calculateAccumulatedFrame方法来检索一个矩形,它包括整个绘制节点及它所有后代的区域。
创建场景
场景由视图来呈现。它的很多属性对视图如何呈现场景都有影响。这些属性允许你定义场景的原点位置和场景的尺寸。如果场景的尺寸与视图不匹配,你还可以定义场景缩放方式以适合视图。
场景的尺寸定义其可见区域
在场景首次初始化时,它的size属性由指定初始化器配置。场景的尺寸以点为单位指定场景中可见部分的尺寸。这只用于指定场景的可见部分。树中的节点可以定位在该区域之外,这些节点仍由场景处理,但被渲染器(renderer)忽略。
使用锚点在视图中定位场景的坐标系
缺省情况下,一个场景的原点被放置在视图的左下角上,如图4-3中所示。因此,一个场景初始化为宽1024和高768,在左下角是原点(0,0),右上角坐标是(1024,768)。frame包含(0,0)-(1024,768)。
场景的position属性被Scene Kit忽略,因为场景始终是一个节点树的根节点。它的默认值是CGPointZero,且你不能改变它。但是,你可以通过设置场景的anchorPoint属性移动它的原点。锚点在单位坐标空间中指定,并选择封闭视图中的一个点。
图4-3 默认锚一个场景是在左下角的视图
锚点的默认值是CGPointZero,放置于左下角。场景的可见坐标空间是从(0,0)到(width,hight) 。对于不滚动场景内容的游戏,默认的锚点是最有用的。
锚点第二模式(second-mode)的值通常是(0.5,0.5),在中间的视图,如图4-4中所示,把场景的原点定在视图的中心。场景的可视坐标空间是从(-width/2,-hight/ 2)到(width/2,hight/2)。当你想轻松地相对屏幕的中心定位节点时,把场景的锚点定在中心是最有用的,比如一个滚动游戏。
图4-4 移动锚点到视图的中心
总结一下,anchorPoint和size属性用来计算场景的frame,frame包含了场景的可见部分。
缩放场景的内容以适合视图
场景渲染后,它的内容被复制到呈现视图。如果视图和场景的尺寸相同,则内容可以直接复制到视图中。如果两者不一样,那么场景会被缩放以适合视图。scaleMode属性决定内容如何缩放。
当你设计游戏时,你应该决定处理场景的size和scaleMode属性的战略。以下是一些最常见的策略:
· 以恒定尺寸实例化场景,并且永远不改变它。必要时允许Sprite Kit把内容缩放到视图。这场样景有一个可预见的坐标系统和frame。然后,你的美术资产和游戏逻辑可以基于这个坐标系。
· 调整游戏中的场景的尺寸。在必要的地方,调整你的游戏逻辑和美术资产来匹配场景的尺寸。
· 将scaleMode属性设置为SKSceneScaleModeResizeFill。Sprite Kit会自动调整场景的尺寸,使其始终与视图的尺寸相匹配。在必要的地方,调整你的游戏逻辑和美术资产来匹配场景的尺寸。
当你计划使用一个恒定尺寸的场景,清单4-1展示了一个典型的实现。与你在“深入Sprite Kit”中创建的例子一样,这个代码指定了第一次呈现场景时要执行的方法。这个方法配置场景的属性,包括它的缩放模式,然后添加内容。在此示例中,缩放模式被设置为SKSceneScaleModeAspectFit,它在两个维度上以相同的比例缩放内容,并确保所有的场景的内容都可见。在必要的地方,这种模式会添加黑边(etterboxing)。
清单4-1 对一个固定尺寸的场景使用缩放模式
- - (void)createSceneContent {
-
- self.scaleMode = SKSceneScaleModeAspectFit;
-
- self.backgroundColor = [SKColor blackColor];
-
- / /在这里添加更多的场景内容...
-
- }
如果你希望在运行时改变场景的尺寸,那么应该用初始的场景尺寸来确定要使用的美术资产,以及任何依赖于场景尺寸的游戏逻辑。你的游戏应该重写场景的didChangeSize:方法,每当场景变化尺寸时会调用此方法。当这个方法被调用时,你应该更新场景的内容,以匹配新的尺寸。
创建节点树
你可以通过创建节点之间的父子关系的方式来创建节点树。每个节点维护一个有序的子节点列表,可以通过读取节点的children属性进行引用。子节点在树中的顺序会影响场景处理的多个方面,包括碰撞测试([font=ê@˜ø\7f|ÿ]hit testing)和渲染。所以,适当地组织节点树是很重要的。
表4-1列出了构建节点树最常用的方法。完整的方法列表在SKNode类参考中提供。
表4-1 操作节点树的常用方法
方法 描述
addChild: 添加一个节点到接收者的子节点列表的末尾。
insertChild:atIndex: 插入一个孩子到接收者的子节点列表中的特定位置。
removeFromParent 从父节点中移除接收节点。
当你需要直接调整节点树,可以使用表4-2中的属性查看([font=ê@˜ø\7f|ÿ]uncover)树的结构。
表4-2 横移节点树
属性 描述
children 接收节点的子节点所形成的SKNode对象数组。
parent 如果该节点是另一个节点的子节点,这个属性指向父节点。否则,它为nil。
scene 如果该节点包含在场景中的任何地方,它返回作为节点树的根的场景节点。否则,它为nil。
理解节点树的绘制顺序
场景渲染的标准行为遵循以下一对简单的规则:
· 父节点先绘制自身的内容再渲染子节点。
· 子节点以它们在子节点数组中的顺序依次渲染。
图4-5展示了如何渲染有三个子节点的节点。
图4-5 父节点在子节点前绘制
在你在“深入Sprite Kit”写的代码中,创建了一个场景,还有一个飞船和多们岩石。两个灯被指定为飞船的子节点,而飞船和岩石又是场景的子节点。因此,场景用以下方式渲染其内容:
1. 场景渲染它本身,清除内容为它的背景色。
2. 场景渲染飞船节点。
3. 飞船节点渲染它的子节点,即飞船上的灯光。
4. 场景渲染岩石节点,它们在场景的子节点数组中的飞船节点后出现。
重要提醒:SKCropNode和SKEffectNode节点类轻微地改变了场景的渲染行为。它们不绘制自己的内容,而是改变它们的子节点在场景中的渲染方式。虽然如此,还是用相同的绘制顺序。要想了解更多信息,请参阅“使用其他节点类型”。
维护节点的子节点的顺序,有时会比你感兴趣的工作还要多。给每个节点一个明确的深度值并允许Sprite Kit管理你的绘制顺序,会更容易。你可以使用节点的zPosition属性这样做。当一个节点创建时,zPosition的属性设为0.0。通过设置节点的z轴位置,相对于它的同级节点,你让它更靠近或更远离顶层的渲染顺序。下面是z轴位置添加后场景的渲染方式:
· 父节点先绘制自身的内容再渲染子节点(不变)。
· 父节点渲染子节点从z值最大的孩子开始,并从z值最小的孩子结束。所以,z轴位置表示从子节点到一个假想的摄像机([font=ê@˜ø\7f|ÿ]camera)位置的距离。
· 如果两个子节点有相同的z值,则在数组中较早出现的那个先绘制。
图4-5展示了有三个子节点的节点。通常情况下,子节点将按它们出现在子节点数组的顺序依次渲染。然而,在这种情况下,这三个节点有自定义的深度值,导致它们以不同的顺序渲染。
图4-6 子节点按深度顺序渲染。
碰撞测试的顺序与绘制顺序相反
当Sprite Kit处理场景内的触摸或鼠标事件时,它在场景中查找想接受该事件的最接近节点。如果该节点不想处理事件,则检查下一个最接近的节点,依此类推。处理碰撞测试的顺序基本上是绘制顺序的反方向:
1. 父节点只在它的子节点传给它后才接受事件。
2. 子节点从最小的z值到最大的z值进行处理。
3. 如果两个子节点有相同的z值,先测试数组中后出现的那个。
在碰撞测试中要考虑一个节点,它的userInteractionEnabled属性必须设置为YES。场景节点以外的任何节点的默认值都是NO。要接收事件的节点需要从它的父类(iOS上的UIResponder 和OS X上的NSResponder)实现适当的响应方法。这是你在Sprite Kit中必须实现特定平台的代码为数不多的地方之一。
有时候,你也想直接查找节点,而不是依赖于标准的事件处理机制。Sprite Kit允许你问一个节点是否有任何的后代节点与坐标系的特定点相交。调用nodeAtPoint:方法找到的第一个与该点相交的后代节点,或使用nodesAtPoint:方法接收与该点相交所有节点的数组。
使用节点的深度来添加其他效果
Sprite Kit只使用zPosition的值来确定碰撞测试和绘制顺序。但是,你可以使用你指定的值来实现自己的游戏特效。例如,你可以:
· 使用的节点的深度来确定节点在屏幕上移动的速度。通过增加不同深度的节点,你可以模拟视差滚动([font=ê@˜ø\7f|ÿ]parallax scrolling)。
· 使用节点的深度来影响它渲染的方式。
搜索节点树
通过组织树中的节点来确定精确的场景渲染顺序,而不是通过那些节点在你游戏中扮演的角色。正因为如此,SKNode类提供了name属性。你可以命名一个节点,以区别于树中的其他节点,然后搜索这些节点。
节点的名称应该是没有任何标点的字母数字字符串。清单4-2展示了你可以如何命名三个不同的节点来区分它们彼此。
清单4-2 命名一组节点
- playerNode.name = @“player”;
-
- monsterNode1.name = @“goblin”;
-
- monsterNode2.name = @“ogre”;
当你的命名游戏的节点时,你应该决定名称是否是唯一的。如果你决定一个节点名是唯一的,那么该名字就是为了识别该节点而不是其他。另一方面,如果节点的名字不是你的游戏中唯一的,它通常代表了相关节点的集合。例如,在清单4-2中,可能游戏中有多个小妖精,你或许想用相同的名称识别它们。但玩家可能是游戏中唯一的节点。
在你的应用程序中,节点名称通常有两个目的:
· 你可以根据节点的名称编写自己的实现游戏逻辑的代码。例如,两个物理对象碰撞时,你可能会使用节点名称来确定碰撞如何影响游戏。
· SpriteKit还为你提供了一些强大的工具来搜索场景内的节点。
SKNode类实现了搜索节点树的两种方法:
· childNodeWithName:方法搜索节点的子节点,直到找到一个匹配的节点,然后停止并返回该节点。这种方法通常用于对具有唯一名称的节点进行搜索。
· enumerateChildNodesWithName:usingBlock:方法搜索节点的子节点,并在找到的每个匹配的节点调用一次block。当你想找到的所有节点共享同一个名称时,你可以使用此方法。
清单4-3展示了在你的场景类上你可以如何创建方法来查找玩家??节点。
清单4-3 寻找玩家节点
- - (SKNode *)playerNode {
-
- return [self childNodeWithName:@“player”];
-
- }
当这个方法在场景上调用时,场景搜索它的子节点(且仅搜索子节点)中名称属性匹配搜索字符串的节点,然后返回这个节点。当指定搜索字符串时,你可以指定节点的名称或类的名称。例如,如果你为玩家节点创建了自己的子类,并把它命名为PlayerSprite,那么你可以指定PlayerSprite作为搜索字符串代替player;它将返回相同的节点。
高级搜索
默认的搜索只搜索一个节点的子节点,而且必须完全匹配节点或类的名称。然而,Sprite Kit提供了一个表达式搜索语法,允许你进行更高级的搜索。例如,你可以像之前一样做同样的搜索,但搜索整个场景树。或者你可以搜索节点的子节点,但匹配某个模式,而不需要精确匹配。
表4-3描述了不同的语法选项。搜索使用常见的正则表达式语义。
表4-3 搜索语法选项
语法 描述
/ 当放在搜索字符串的开头时,这表示应该对树的根节点进行搜索。
// 当放在搜索字符串的开头时,这指定搜索应从根节点开始,并在整个节点树中递归进行。这在搜索字符串之外的其他地方都是不合法的。
.. 这表明搜索应该向上移到该节点的父节点中进行。
/ 当放在搜索字符串的开头以外的任何地方时,这表明搜索应该移到节点的子节点中进行。
* 搜索匹配零个或多个字符。
[以逗号或破折号分隔的字符] 搜索将匹配括号内包含的任意字符。
字母和数字字符 搜索只匹配指定的字符。
表4-4展示了一些有用的搜索字符串来帮助你入门。
表4-4 搜索示例
搜寻字串 描述
/MyName 搜索根节点的子节点并匹配名为MyName的任何节点。
//* 这个搜索字符串匹配场景中的每一个节点。
//MyName/.. 搜索整个场景并匹配每个名为MyName的节点的父节点。
A[0-9] 搜索节点的子节点并返回任何命名为A0,A1,...,A9的子节点。
Abby/Normal 搜索节点的孙子节点并返回任何名称是Normal且其父节点名为Abby的节点。
//Abby/Normal 搜索整个场景并返回任何名称是Normal且其父节点名为Abby的节点。
节点的很多属性适用于它的后代
当你改变一个节点的属性,效果往往传播到该节点的后代。净效果是一个子节点的渲染不仅基于它自身的属性,也基于它祖先的属性。
表4-5 属性影响节点的后代
属性 描述
xScale,yScale 节点的坐标系通过这两个因素缩放。该属性影响坐标转换、节点的frame、绘制和碰撞测试。它的后代也同样地缩放。
zRotation 节点的坐标系通过这个因素旋转。该属性影响坐标转换、节点的frame、绘制和碰撞测试。它的后代也同样地缩放。
alpha 如果该节点是使用混合模式渲染的,混合操作发生之前alpha值会乘以任意alpha值。它的后代也同样受到影响。
hidden 如果一个节点是隐藏的,它和它的所有后代都不渲染。
speed 一个节点处理动作的速度与该值相乘。它的后代也同样受到影响。
坐标空间之间的转换
在使用节点树时,有时你需要把位置从一个坐标空间转换到另一个。例如,当你指定物理系统中的关节([font=ê@˜ø\7f|ÿ]joints),关节位置被指定在场景坐标。所以,如果你在本地坐标系有那些点,你需要将它们转换为场景的坐标空间。
清单4-4展示了如何将一个节点的位置转换到场景坐标系中。场景被要求进行转换。记住一个节点的位置在它父节点的坐标系统中指定,所以代码传递node.parent作为要转换的节点。你可以通过调用convertPoint:toNode:方法执行反向的相同转换。
清单4-4转换节点到场景坐标系统
- CGPoint positionInScene = [node.scene convertPoint:node.position fromnode:node.parent];
你需要进行坐标转换的一个情况是在执行事件处理的时候。鼠标和触摸事件需要从window坐标转换到视图坐标,并从那里进入场景。为了简化你需要写的代码, Sprite Kit增加了一些方便的方法:
· 在iOS上,使用UITouch对象的locationInNode:和previousLocationInNode:将触摸位置转换到节点的坐标系。
· 在OS X上,使用NSEvent对象的locationInNode:方法,将鼠标事件转换到节点的坐标系。