深入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. 在你的视图控制器导入场景的头文件。
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 removeFromParent];
- }];
- }
每次场景处理一帧,都运行动作和模拟物理。你的游戏可以挂接到这个过程中来执行其他自定义代码。在每一帧,场景将处理物理,然后移除移出屏幕底部的所有岩石。当你运行应用程序时,帧率保持不变。
在场景中,预处理及后处理与动作和物理结合的地方,就是你建立你的游戏的行为的地主。
这就是你第一次体验Sprite Kit!其它一切都是你在这里看到的基本技术的细化。
试试这个!
这里有一些东西,你可以尝试:
· 做一个OS X版本的这个例子。你在视图控制器写的代码,在OS X上通常是在一个应用程序委托中实现。响应代码需要改变来使用鼠标事件而不是触摸事件。但是,代码的其余部分应是相同的。
· 使用纹理精灵呈现船和岩石。(提示:“使用精灵”)
· 尝试在触摸事件的响应中移动飞船。(提示:“添加动作节点”和“构建场景”)。
· 添加额外的图形效果到场景(提示:“使用其他节点类型”)
· 岩石与船舶碰撞时添加其他行为。例如,使岩石发生爆炸。(提示:“模拟物理”)