学习Sprite Kit最好的途径是实战,下面通过一个例子来初探SpriteKit。通过这个例子,你将会学到以下内容:
- 在一个以SpriteKit框架为基础的游戏中使用场景(scenes)。
- 组织节点数并绘制内容。
- 使用动作(actions)为场景内容做动画。
- 为一个场景添加交互。
- 场景之间的切换。
- 场景中的模拟物理环境。
创建项目
整个项目需要使用Xocde5.0的集成开发环境。使用Single View Application模版创建项目。创建项目的时候使用以下参数:
- Product Name:SpriteWalkthrough
- Class Prefix:Sprite
- Devices:iPad
创建你的第一个场景
Sprite Kit框架内容像其他可视内容一样被放置于一个Window视窗里。SptiteKit框架里的内容都是通过SKView类进行渲染,通常都是首先渲染场景,场景是一个SKScene对象。场景也参与响应链,同时还拥有一些专为游戏匹配的其他特性。
因为SpriteKit框架内容是通过一个视图对象渲染出来的,所以你可以将这个视图与其他视图按照视图层级关系排列。例如你可以创建一个按钮控制层放置在SpriteKit视图之上。或者,你还可以通过按钮为sprite添加交互。之后的例子中,你将看到如何为场景添加交互。
使用Sptite Kit来配置视图控制器
- 打开项目里的storyboard。它只有一个单独的视图控制器(SpriteViewontroller)。选择视图控制器的view视图对象,将它的class改为SKView。
-
在视图控制器的实现文件头部添加下面代码。
#import <SpriteKit/SpriteKit.h>
- 实现视图控制器的ViewDidLoad方法配置视图。
这段代码打开了性能调试信息模块,用来描述场景的渲染情况。帧频(spriteView.showFPS)是最重要的信息。另外两个是描述了为视图中一共有多少个节点显示出来,渲染内容一共绘制了多少次(越少越好)。- (void)viewDidLoad { [super viewDidLoad]; SKView *spriteView = (SKView *) self.view; spriteView.showsDrawCount = YES; spriteView.showsNodeCount = YES; spriteView.showsFPS = YES; }
接下来,添加第一个场景。
创建Hello场景:
- 创建一个继承自SKScene类名为HelloScene子类。
头文件:
你不需要做任何修改#import <SpriteKit/SpriteKit.h> @interface HelloScene : SKScene @end
- 在视图控制器实现文件中导入场景类的头文件。
#import "HelloScene.h"
- 修改视图控制器,创建一个场景,并在视图中显示
- (void)viewWillAppear:(BOOL)animated { HelloScene* hello = [[HelloScene alloc] initWithSize:CGSizeMake(768,1024)]; SKView *spriteView = (SKView *) self.view; [spriteView presentScene: hello]; }
- 运行。
程序运行后会显示一个场景,但是是空的,只有性能调试信息模块。
为你的场景添加内容
当你制作一款基于SpriteKit框架的游戏时,你可能会根据模块为你的游戏创建不同的场景。例如,为你的主菜单创建一个单独的场景、为你的游戏模块分配一个场景。我们的这个例子也遵循相同的设计,第一个场景显示一个“Hello World”文本。
通常情况下,我们是在场景已经被视图显示出来的时候为场景创建内容。这个例子中,代码应该写在didMoveToView:方法中,说明场景已经被加载到视图中。
在场景中显示Hello World文本
- 在场景实现文件中添加一个属性,来判断场景是否为自身已经创建了内容。
你的实现文件应该如下:
#import "HelloScene.h" @interface HelloScene () @property BOOL contentCreated; @end @implementation HelloScene @end
这个属性并不需要暴露在外部,所以它实现了一个私有访问权限,定义在实现文件中。
- 实现场景的didMoveToView:方法。
- (void)didMoveToView: (SKView *) view { if (!self.contentCreated) { [self createSceneContents]; self.contentCreated = YES; } }
当场景已经被加载进视图的时候调用didMoveToView:方法,但是在这种情况下,内容部分只应该在场景第一次被加载时创建,所以需要使用之前定义的属性(contentCreated)来判断场景内容是否已经被创建过。
- 实现场景的createSceneContents:方法。
- (void)createSceneContents { self.backgroundColor = [SKColor blueColor]; self.scaleMode = SKSceneScaleModeAspectFit; [self addChild: [self newHelloNode]]; }
为场景定义背景颜色,这里的颜色定义使用的是SKColor,它并不是一个类,它是指向UIColor的一个宏。只是为了让代码更统一。
场景的scaleMode属性确定场景如何缩放来适应视图(这个例子中我四个属性都试了,没看出变化来,知道的讲解一下,请赐教)。
- 实现场景的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; }
在SpriteKit框架中,你不能想使用OpenGL ES或Quartz 2D那样直接执行绘制请求。而是通过创建节点对象并添加他们到场景来添加内容。所有绘制必须由SpriteKit内置类来完成,你可以自定义这些类的行为从而产生不同的图形效果。
- 运行。你将看到一个带有“Hello, World!”文本的蓝色场景。
为你的场景动画添加动作
你可以通过定义一个action对象描述一些动作,让节点运行。当场景渲染的时候,它就会执行这些动作。
当用户触摸屏幕的时候,文本会做一些动画,然后消失。
为文本添加动画:
- 接着为newHelloNode方法添加下面的代码:
所有节点都有一个描述自身的name属性。以便之后可以通过name属性寻找对应节点。helloNode.name = @"helloNode";
通常在游戏中,你可以给场景内容所有同一类型的节点一个相同的name属性。例如你将游戏中所有怪兽节点定义一个相同的name属性:“monster”。
- 重写场景类中的touchesBegan:withEvent:方法。当场景接收到触摸时间的时候,它会寻找name属性为helloNode的节点并让它运行一个短暂的动画。
在ios中,所有的节点对象都是UIResponder子类。这意味着你可以创建节点类的子类,并为场景中的节点添加交互。
为了避免节点重复响应这个进程,代码中清除了节点的name属性。然后定义一些action对象执行不同的动作。最后将这些动作合并在一个队列中,按顺序执行。- (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 fadeOutWithDuration: 0.25]; SKAction *remove = [SKAction removeFromParent]; SKAction *moveSequence = [SKAction sequence:@[moveUp, zoom, pause, fadeAway, remove]]; [helloNode runAction: moveSequence]; } }
- 运行。(效果什么样,自己敲出来看看)
场景切换
创建一个宇宙飞船场景
- 创建一个SKScene的子类,并命名为SpaceshipScene。
- 初始化太空飞船场景,代码与HelloScene类似。
@import "SpaceshipScene.h" @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; } @end
- 在HelloScene.m文件中import进SpaceShipScene.h头文件。
#import "SpaceshipScene.h"
- 在HelloScene类的touchesBegin: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对象创建一个宇宙飞船,并且在它表面有发光。每一个节点都要执行一些动作。
SKSpriteNode是SpriteKit框架中创建内容最常见的类。它们可以绘制有纹理和无纹理的矩形。这个例子中你将使用无纹理的物体。之后你也可以很容易的使用有纹理的物体将无纹理的物体替换而不需要改变它的行为。有时候,你可能需要使用几十甚至上百个节点为你的游戏创建可视化内容。但是,本质上,这些sprite都使用的是相同的方法去创建,正如我们这个例子。
虽然你可以直接将这三个sprite添加到创景当中国,但SpriteKit框架不提倡这么做。闪烁的光是宇宙飞船的一部分!如果飞船移动,光也要跟着移动。解决的办法是将光变成飞船的子视图。光的坐标就相对于它的父节点位置定位在sptite图像的中心。
添加宇宙飞船
- 在SpaceshipScene.m文件,接着在createSceneContents方法中输入一下代码来创建宇宙飞船。
SKSpriteNode *spaceship = [self newSpaceship]; spaceship.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame)-150); [self addChild:spaceship];
- 实现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; }
这个方法中创建了一个船体,并且引入了一个新的动作类型。重复的执行传入的动作。这个例子中,将无限的执行队列动作。
- 运行程序,你将看到一个单独的矩形船体。
- 在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];
- 实现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(精灵)添加行为又多种方式,这个例子只展示其中一种。你将为场景添加一些新的节点并使用物理系统模拟它们运动,实现碰撞效果。
SpriteKit框架提供了一个完整的物理模拟环境,你可以为你的接的添加自动化的行为。不需要为节点实现行为(action),模拟物理环境将自动为节点添加这些行为,使它们的移动。当一个节点与其他处于物理系统中的节点接触的时候,会自动检测碰撞。
为宇宙飞船场景添加物理环境
- 修改newSpaceship方法,为飞船添加一个物理刚体。
hull.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:hull.size];
- 运行程序。
由于重力的原因,飞船会沿着屏幕底部垂直落下。并且飞船移动的动作与重力作用同时存在。
- 修改newSpaceship方法,防止飞船受到物理影响。
现在再次运行程序,飞船不再受到重力的影响,像之前一样运动。之后,由于它属于静态物体,所以碰撞也不会影响它的速率。hull.physicsBody.dynamic = NO;
- 在creatSceneContents方法中添加以下代码,效果是创建大量岩石
场景也是一个节点,因此它也可以执行动作。在这里,由自定义的动作在场景中执行一个方法来创建一个岩石。场景会在一个随机的时间间隔中持续的创建新岩石。SKAction *makeRocks = [SKAction sequence: @[ [SKAction performSelector:@selector(addRock) onTarget:self], [SKAction waitForDuration:0.10 withRange:0.15] ]]; [self runAction: [SKAction repeatActionForever:makeRocks]];
- 实现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]; }
- 运行程序。
大量岩石会从屏幕顶端落下。当岩石碰到飞船,会被反弹开。不需要为岩石添加动作,它们会受到物理系统的作用力发生下降和碰撞。
岩石小而移动迅速,代码中指定了精准碰撞检测也确保碰撞的准确性。
如果你运行一段时间,你会发现即使节点的数量保持很低的值,但是帧频开始下降。这是因为代码中只让场景中的可见节点纳入节点数量计算当中。然而,当岩石掉落至屏幕底部之后,它们仍然存在于场景中。这意味着它们仍然存在于物理环境中。最终,大量的节点导致SpriteKit框架运行缓慢。
- 在场景中实现didSimulatePhysics方法,移除掉移动到屏幕之外的岩石。
当场景处理每一帧的时候,都会执行动作和模拟物理环境。这时你的游戏也会在这个时候执行一些其他的自定义代码。现在,当应用运行到一个新的帧动画的时,它在物理环境下,将移出屏幕外的岩石移除,所以,当程序运行时,帧频就比较稳定了。-(void)didSimulatePhysics { [self enumerateChildNodesWithName:@"rock" usingBlock:^(SKNode *node, BOOL *stop) { if (node.position.y < 0) [node removeFromParent]; }]; }