Sprite Kit 入门教程

 Ray Wenderlich on September 30, 2013

这篇文章还可以在这里找到 英语日语

If you're new here, you may want to subscribe to my RSS feed or follow me on Twitter. Thanks for visiting!

Sprite Kit是一个在iOS7上制作令人惊喜的2D游戏的新框架,它内置于iOS7 SDK。 它拥有材质精灵(以下将直接引用sprite),支持很酷的特效,比如视频、滤镜、遮罩等,内置了物理引擎库,还有很多其他的东西。
iOS7本来有一个很棒的Sprite Kit范例项目了,叫做冒险(Adventure),你可以马上将其下载下来。但是这个游戏有点复杂,而更多时候你需要的是一个越简单越好的例子来作为入门学习。这就是这篇教程的来由。
在这篇Sprite Kit的初学者教程里,你会从头到尾系统地学到如何为iPhone创建一个简单而有意思的2D游戏。如果你看过我们 Simple Cocos2D game 这篇教程, 这个游戏可能看起来很相似 :)
在开始之前你需要确保自己安装了最新版本的Xcode(5.X),它包含了对Sprite Kit和iOS7的支持。
对了,你可以先看教程,也可以直接跳到教程结尾运行以下完整的样例项目。如果你这样做的话将会看到忍者哦。

Sprite Kit的优点和缺点

在开始之前,我想先指出Sprite Kit 并不是你在iOS平台上制作2D游戏的唯一选择,而且它有一些优缺点是你需要事先注意的。
之后我想再回顾一下iOS上制作2D游戏其他的三种在选择并且与Sprite Kit比较一下各自的优缺点。

Sprite Kit 优点

  • 优点1:内置于iOS,因此不需要再额外下载类库也不会产生外部依赖。它是苹果官方编写的,所以可以确信它会被良好支持和持续更新。
  • 优点2:它为纹理贴图集和粒子提供了内置的工具。
  • 优点3:它可以让你做一些用其他框架很难甚至不可能做到的事情,比如把视频当作sprites来使用或者实现很炫的图片效果和遮罩。

Sprite Kit 缺点

  • 缺点1:如果你使用了Sprite Kit那么你的游戏就被限制在iOS系统上了。你可能永远也不会知道自己的游戏是否会在Android平台上变成热门。
  • 缺点2:Sprite Kit刚刚起步,所以现阶段它可能没有像其他框架那么多的实用特性,比如Cocos2D 的某些细节功能。在我看来最大的缺失就是不能直接编写OpenGL代码。

Sprite Kit vs Cocos2D-iPhone vs Cocos2D-X vs Unity

现在很多人会有疑问:“那么我到底该选择哪个2D游戏引擎呢?”
你需要根据自己的目的做出选择。这是我的观点:

  • 如果你是一个新手或是专注于iOS平台的话就选择Sprite Kit吧。它是iOS内置框架,简单易学而且完全能够胜任你的工作。
  • 如果你需要编写自己的OpenGL代码,请继续使用Cocos2D或者尝试其他的引擎,Sprite Kit当前并不支持。
  • 如果你想要制作跨平台的游戏,请选择Cocos2D-X或者Unity。Cocos2D-X好在它几乎面面俱到,为2D游戏而构建,你几乎可以用它做任何你想做的事情。Unity好在它可以带给你更大的灵活性(如果你想的话你可以为你的游戏添加一些3D元素),尽管你在用它制作2D游戏时不得不经历一些小麻烦。

在你看完以上的所有内容后,如果你认为Sprite Kit可能正是你要寻找的东西,请继续你的阅读,我们将正式开始Sprite Kit的教程。

Hello, Sprite Kit!

让我们从创建一个简单的Hello World 项目开始,它是用Xcode5内置的Sprite Kit模版创建的。
打开Xcode,选择FileNewProject,接下来选择iOSApplicationSprite Kit Game 模版,然后单击Next:

键入“SpriteKitSimpleGame”做为Product Name,设备选择iPhone,然后单击Next:

把项目保存在你硬盘上的某个位置,然后单击 Create。随后单击运行这个项目。你应该能看到下面的界面:

就像Cocos2D一样,Sprite Kit被组织在scene(场景)之上。scene是一种类似于“层级”或者“屏幕”的概念。举个例子,你可以同时创建两个scene,一个位于游戏的主显示区域,一个可以用作游戏地图展示放在其他区域,两者是并列的关系。
如果你,你会发现Sprite Kit的模版已经默认为你新建了一个scene——MyScene。打开MyScene.m 文件你会看到它包含了一些代码,这些代码实现了两个功能,把一个label放到屏幕上以及在屏幕上随意点按时添加旋转的飞船。
在这篇教程里,你将主要与MyScene打交道。但是在开始之前,你需要做一些小的改动,使得我们的游戏在横评下运行(替代默认的竖屏)。

切换成竖屏方向运行

首先,打开Xcode中target的设定:在项目导航栏中单击SpriteKitSimpleGame项目,选中对应的target。然后在Deployment Info区域内取消Orientation中Portrait(竖屏)的勾选,这样就只有Landscape Left 和 Landscape Right 是被选中的了,如下图所示:

编译运行项目,你会看到刚刚做的改动已经顺利完成并且生效了:

然而,事实并不如此。让我们试着添加忍者到游戏中来看看为什么这样说,到底还有什么问题呢?
首先,下载 这个项目的资源文件 并且把它们拖拽到Xcode项目中。请在拖拽后弹出的对话框中确保勾选了这个选项:“Copy items into destination group’s folder (if needed)(复制所有文件到目标group所在的文件夹)”,同时项目target也要被选中。
下一步,打开MyScene.m并且用下面的代码替换掉它原有的内容:

#import "MyScene.h"
// 1
@interface MyScene ()
@property (nonatomic) SKSpriteNode * player;
@end
@implementation MyScene
-(id)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
 
// 2
NSLog(@"Size: %@", NSStringFromCGSize(size));
 
// 3
self.backgroundColor = [SKColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0];
 
// 4
self.player = [SKSpriteNode spriteNodeWithImageNamed:@"player"];
self.player.position = CGPointMake(100, 100);
[self addChild:self.player];
 
}
return self;
}
@end

让我们一步一步解释下上面的代码。

  1. 这里你创建了一个当前类的private(私有访问权限)声明,所以可以为player声明一个私有的变量(即忍者),这就是你即将要添加到scene上0的sprite对象。
  2. 在控制台输出当前scene的大小,这样做的原因你稍后会看到。
  3. 设置当前scene的背景颜色,在Sprite Kit中只需要设置当前scene的backgoundColor属性即可。这里设置成白色的。
  4. 添加一个sprite到scene上面也很简单。你只需要调用spriteNodeWithImageNamed方法,把对应图片素材的名字作为参数传入即可。然后设置这个sprite的位置,调用addChild方法把它添加到当前scene上。把忍者sprite的位置设置成(100,100),这应该会在屏幕左下角的右上方一点。

编译运行,然后。。。

不对啊,屏幕白茫茫一片,没有忍者。你可能认为就是这样设计的,但这其实是一个有待解决的问题。如果你观察下刚刚在控制台输出的内容,你会看到下面的输出:

SpriteKitSimpleGame[3139:907] Size: {320, 568}

因此我们的scene 认为它的宽是320而高是568,但这恰好反了。
为了看看到底发生了什么,我们找到ViewController.m 的viewDidLoad方法:

- (void)viewDidLoad
{
[super viewDidLoad];
// Configure the view.
SKView * skView = (SKView *)self.view;
skView.showsFPS = YES;
skView.showsNodeCount = YES;
 
// Create and configure the scene.
SKScene * scene = [MyScene sceneWithSize:skView.bounds.size];
scene.scaleMode = SKSceneScaleModeAspectFill;
 
// Present the scene.
[skView presentScene:scene];
}

这里从skView的bounds属性获取了size,创建了相应大小的scene。然而,当viewDidLoad方法被调用时,skView还没有被加到view的层级结构上,因而它不能相应方向以及布局的改变。所以skView的bounds属性此时还不是它横屏后的正确值,而是默认竖屏所对应的值,看来这个时候不是初始化scene的好时机。

Note: 有关这个现象的更多细节,请参考Rob Mayoff 的 这个很赞的解释

解决办法是把初始化代码的运行时机后移。请用下面这个方法替换viewDidLoad:

- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
// Configure the view.
SKView * skView = (SKView *)self.view;
if (!skView.scene) {
skView.showsFPS = YES;
skView.showsNodeCount = YES;
 
// Create and configure the scene.
SKScene * scene = [MyScene sceneWithSize:skView.bounds.size];
scene.scaleMode = SKSceneScaleModeAspectFill;
 
// Present the scene.
[skView presentScene:scene];
}
}

再次编译运行,当当当当,女士们、先生们,忍者终于出现了!

现在游戏的坐标系统已经一切正常,你会把这个忍者放在他应该放的位置,也就是在屏幕左侧面朝中央。为了做这些,切换回MyScene.m并且用下面的代码替换掉已有的那一行设置了忍者位置的代码:

self.player.position = CGPointMake(self.player.size.width/2, self.frame.size.height/2);

移动的怪物

下一步将要把一些怪物添加到scene上,与现有的忍者形成战斗场景。为了使游戏更有意思,怪兽应该是移动的,否则游戏就毫无挑战性可言了!那么让我们在屏幕的右侧一点创建怪兽们,然后为它们设置action使它们能够向左移动。
在MyScene.m中添加如下方法:

- (void)addMonster {
// 创建怪物Sprite
SKSpriteNode * monster = [SKSpriteNode spriteNodeWithImageNamed:@"monster"];
 
// 决定怪物在竖直方向上的出现位置
int minY = monster.size.height / 2;
int maxY = self.frame.size.height - monster.size.height / 2;
int rangeY = maxY - minY;
int actualY = (arc4random() % rangeY) + minY;
 
// Create the monster slightly off-screen along the right edge,
// and along a random position along the Y axis as calculated above
monster.position = CGPointMake(self.frame.size.width + monster.size.width/2, actualY);
[self addChild:monster];
 
// 设置怪物的速度
int minDuration = 2.0;
int maxDuration = 4.0;
int rangeDuration = maxDuration - minDuration;
int actualDuration = (arc4random() % rangeDuration) + minDuration;
 
// Create the actions
SKAction * actionMove = [SKAction moveTo:CGPointMake(-monster.size.width/2, actualY) duration:actualDuration];
SKAction * actionMoveDone = [SKAction removeFromParent];
[monster runAction:[SKAction sequence:@[actionMove, actionMoveDone]]];
}

我会慢一点把代码讲解清楚,让其尽可能容易理解。第一部分正如之前提到过的:我们需要做一些简单的计算来创建怪物对象。为它们设置合适的位置并且用和忍者sprite(player)一样的方式把它们添加到scene上。在相应的位置出现。
接下来轮到添加actions了。像Cocos2D一样,Sprite Kit提供了一些超级实用的内置actions,比如移动、旋转、淡出、动画等等。这里要在怪物身上添加3种aciton:

  • moveTo:duration: 这个action用来让怪物对象从屏幕左侧直接移动到右侧。值得注意的是你可以自己定义移动持续的时间。在这里怪物的移动速度会随机分布在2到4秒之间。
  • removeFromParent: Sprite Kit有一个方便的action能让一个node从它的父母节点上移除。当怪物不再可见时,可以用这个action来把它从scene上移除。移除操作很重要,因为如果不这样做你会面对无穷无尽的怪物而最终它们会耗尽iOS设备的所有资源。
  • sequence: sequence(系列)action允许你把很多action连到一起按顺序运行,同一时间仅仅会执行一个action。用这种方法,你可以先运行moveTo: 这个action,让怪物先移动 ,当移动结束时继续运行removeFromParent: 这个action把怪物从scene上移除。

别忘了还有件事没做呢,你需要调用addMonster方法来创建怪物!为了让游戏再有趣一点,我们让怪物们持续不断地涌现出来。
Sprite Kit不能像Cocos2D一样设置一个每几秒运行一次的回调方法。它也不能传递一个增量时间参数给update方法。然而我们可以用一小段代码来模仿类似的定时刷新方法。首先把这些属性添加到MyScene.m的私有声明里:

@property (nonatomic) NSTimeInterval lastSpawnTimeInterval;
@property (nonatomic) NSTimeInterval lastUpdateTimeInterval;

我们会使用lastSpawnTimeInterval这个属性来记录上一次生成怪物的时间, 使用lastUpdateTimeInterval这个属性来记录上一次更新的时间。
下一步,你会编写一个每帧都会调用的方法。这个方法的参数是上次更新后的时间增量。由于它不会被默认调用,你需要在下一步编写另一个方法来调用它。

- (void)updateWithTimeSinceLastUpdate:(CFTimeInterval)timeSinceLast {
self.lastSpawnTimeInterval += timeSinceLast;
if (self.lastSpawnTimeInterval > 1) {
self.lastSpawnTimeInterval = 0;
[self addMonster];
}
}

在这里你只是简单地把上次更新后的时间增量加给lastSpawnTimeInterval。一旦它的值大于一秒,你就要生成一个怪物然后重置时间。
接下来,添加如下方法来调用上面的updateWithTimeSinceLastUpdate方法 。

- (void)update:(NSTimeInterval)currentTime {
// 获取时间增量
// 如果我们运行的每秒帧数低于60,我们依然希望一切和每秒60帧移动的位移相同
CFTimeInterval timeSinceLast = currentTime - self.lastUpdateTimeInterval;
self.lastUpdateTimeInterval = currentTime;
if (timeSinceLast > 1) { // 如果上次更新后得时间增量大于1秒
timeSinceLast = 1.0 / 60.0;
self.lastUpdateTimeInterval = currentTime;
}
 
[self updateWithTimeSinceLastUpdate:timeSinceLast];
 
}

update: Sprite Kit会在每帧自动调用这个方法。

这里的代码实际上源自苹果的Adventure范例。它传入当前的时间,我们可以据此来计算出上次更新后的时间增量。值得注意的是这里做了一些必要的检查,如果出现意外致使更新的时间间隔变得超过1秒,这里会把间隔重置为1/60秒来避免奇怪的情况发生。
就是这样,编译运行之,现在你应该看到怪物们在屏幕上欢快地移动着:

发射子弹

到这里,你可以已经迫不及待的为忍者添加一些动作了,那么我们就添加攻击吧。攻击的实现方式有很多种,但在这个游戏里攻击会在玩家点击屏幕时触发,忍者会朝着点按的方向发射一个子弹。
我打算使用moveTo:action动作来实现子弹的前期运行动画,为了实现它需要一些数学运算。这是因为moveTo:需要传入子弹运行轨迹的终点,由于用户点按触发的位置仅仅代表了子弹射出的方向,显然我们不能直接将其当作运行终点。实际上就算子弹超过了触摸点你也应该让子弹保持移动直到子弹超出屏幕为止。
这是一张图片,它标注了这个问题:

就像你看到的,从子弹发射原点到用户触摸点在x轴和y轴上的偏移量会形成一个小三角形。你只要以相同的比例去实现一个顶点在屏幕边缘的大三角形即可。
为了进行这部分的运算,有一些关于向量的基本数学计算方法很有帮助(比如向量间的加减法)。然而,Sprite Kit默认并没有提供,所以你需要自己来实现了。
幸运的是这很容易实现。把下面的方法添加到文件顶部:

static inline CGPoint rwAdd(CGPoint a, CGPoint b) {
return CGPointMake(a.x + b.x, a.y + b.y);
}
static inline CGPoint rwSub(CGPoint a, CGPoint b) {
return CGPointMake(a.x - b.x, a.y - b.y);
}
static inline CGPoint rwMult(CGPoint a, float b) {
return CGPointMake(a.x * b, a.y * b);
}
static inline float rwLength(CGPoint a) {
return sqrtf(a.x * a.x + a.y * a.y);
}
// 让向量的长度(模)等于1
static inline CGPoint rwNormalize(CGPoint a) {
float length = rwLength(a);
return CGPointMake(a.x / length, a.y / length);
}

这些是向量运算方法的标准实现。如果你对此感到疑惑或者没有学习过向量的数学知识,可以到这里恶补一下 vector math explanation.
下一步,添加一个新方法:

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
 
// 1 - 选择其中的一个touch对象
UITouch * touch = [touches anyObject];
CGPoint location = [touch locationInNode:self];
 
// 2 - 初始化子弹的位置
SKSpriteNode * projectile = [SKSpriteNode spriteNodeWithImageNamed:@"projectile"];
projectile.position = self.player.position;
 
// 3- 计算子弹移动的偏移量
CGPoint offset = rwSub(location, projectile.position);
 
// 4 - 如果子弹是向后射的那就不做任何操作直接返回
if (offset.x <= 0) return;
 
// 5 - 好了,把子弹添加上吧,我们已经检查了两次位置了
[self addChild:projectile];
// 6 - 获取子弹射出的方向
CGPoint direction = rwNormalize(offset);
 
// 7 - 让子弹射得足够远来确保它到达屏幕边缘
CGPoint shootAmount = rwMult(direction, 1000);
 
// 8 - 把子弹的位移加到它现在的位置上
CGPoint realDest = rwAdd(shootAmount, projectile.position);
 
// 9 - 创建子弹发射的动作
float velocity = 480.0/1.0;
float realMoveDuration = self.size.width / velocity;
SKAction * actionMove = [SKAction moveTo:realDest duration:realMoveDuration];
SKAction * actionMoveDone = [SKAction removeFromParent];
[projectile runAction:[SKAction sequence:@[actionMove, actionMoveDone]]];
 
}

到这里已经做了很多事情,我们来一步一步回顾一下。

  1. Sprite Kit包括了UITouch类的一个category扩展,有两个方法locationInNode:和previousLocationInNode,这非常酷哦。他们可以让你获取到一次触摸操作相对于某个SKNode对象的坐标体系的坐标。
  2. 然后你创建了一个子弹并且把它放在忍者发射它的地方。注意你还没有把它添加到scene上,原因是你需要做一些合理性检查,我们的游戏可不允许玩家向后发射子弹。
  3. 把触摸的坐标和子弹当前的位置做减法来获得相应的向量。
  4. 如果它在x轴的偏移量小于零就代表玩家在尝试向后发射子弹。这是游戏里不允许的(真正的忍者绝不回头!),不做任何操作直接返回。
  5. 如果没有向后发射,那么就把子弹添加到scene上吧。
  6. 调用rwNormalize方法把偏移量转换成一个单位的向量(即长度为1)。这会使得在同一个方向上生成一个固定长度的向量更容易,因为1乘以它本身的长度还是等于它本身的长度。
  7. 把你想要发射的方向上的单位向量乘以1000,然后赋值给shootAmount.为啥是1000?因为这绝对足够到达屏幕边缘了:)
  8. 为了知道子弹从哪里飞出屏幕,需要把上一步计算好的shootAmount于当前的子弹位置做加法。
  9. 最后一步,像之前一样创建moveTo: 和removeFromParent 这两个action。

编译运行之,现在你的忍者可以对着飞来的怪物发射了手裏剑了!

碰撞监测和物理特性一览

现在游戏里有了满天飞的手裏剑,但是你的忍者真正要做的是把怪物打下来。所以让我们添加一些代码来监测子弹是否打到了目标。
Sprite Kit一个好处是它已经内置了物理引擎。物理引擎不仅仅非常有助于模拟现实中的移动,同时也对碰撞监测提供了很好的支持。
让我们把Sprite Kit的物理引擎引入到游戏中来监测怪物和子弹的碰撞。大体上讲,下面是你准备要做的:

  • 创建物理体系(physics world)。一个物理体系是用来进行物理计算的模拟空间,它是被默认创建在scene上的,你可以配置一些它的属性,比如重力。
  • 为每个sprite创建物理上的外形。在Sprite Kit中,你可以为每个sprite关联一个物理形状来实现碰撞监测功能,并且可以直接设置相关的属性值。这个“形状”就叫做“物理外形”(physics body)。注意物理外形可以不必与sprite自身的形状(即显示图像)一致。相对于sprite自身形状来说,通常物理外形更简单,只需要差不多就可以,并不要精确到每个像素点,而这已经足够适用大多数游戏了。
  • 为碰撞的两种sprite(即子弹和怪物)分别设置对应的种类(category)。这个种类是你需要设置的物理外形的一个属性,它是一个“位掩码”(bitmask)用来区分不同的物理对象组。在这个游戏中,你将会有两个种类:一个是子弹的,另一个是怪物的。当这两种sprite的物理外形发生碰撞时,你可以根据category很简单的区分出他们是子弹还是怪物,然后针对不同的sprite来做不同的处理。
  • 设置一个关联的代理。还记得刚刚提到的物理体系么?你可以为它设置一个与之相关联的代理,当两个物体发生碰撞时来接收通知。这里你将要添加一些有关于对象种类判断的代码,用来判断到底是子弹还是怪物,然后你会为它们增加碰撞的声音等效果。

现在你理解了战斗(指子弹打怪物的过程)的计划,是时候付诸行动了!

碰撞监测和物理特性的实现

让我们添加两个常量开始。将它们添加到MyScene.m中:

static const uint32_t projectileCategory     =  0x1 << 0;
static const uint32_t monsterCategory = 0x1 << 1;

这里设置了两个种类,等下就会用到。一个是子弹的,一个是怪物的。

注意:你可能对这种语法感到奇怪。你只要明白在Sprite Kit中category是一个32位的整型然后被用作掩码就好了。这是种用32位整型表示一个category的简单方式(所以你最多能创建32个category)。这里你用首位来表示子弹,用下一位来表示怪物。

下一步,在initWithSize方法中,把忍者加到scene的代码后面再加入如下两行代码:

self.physicsWorld.gravity = CGVectorMake(0,0);
self.physicsWorld.contactDelegate = self;

这里设置了一个没有重力的物理体系,为了收到两个物体碰撞的消息需要把当前的scene设为它的代理。
在addMonster方法中创建完怪物后添加如下代码:

monster.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:monster.size]; // 1
monster.physicsBody.dynamic = YES; // 2
monster.physicsBody.categoryBitMask = monsterCategory; // 3
monster.physicsBody.contactTestBitMask = projectileCategory; // 4
monster.physicsBody.collisionBitMask = 0; // 5

让我们逐行看看上面的代码到底做了什么。

  1. 为怪物sprite 创建物理外形。在这里,这个外形被定义成和怪物sprite大小一致的矩形,与怪物自身大致相匹配。
  2. 将怪物物理外形的dynamic(动态)属性置为YES。这表示怪物的移动不会被物理引擎所控制。你可以在这里不受影响而继续使用之前的代码(指之前怪物的移动action)。
  3. 把怪物物理外形的种类掩码设为刚刚定义的 monsterCategory 。
  4. 当发生碰撞时,当前怪物对象会通知它contactTestBitMask 这个属性所代表的category。这里应该把子弹的种类掩码projectileCategory赋给它。
  5. collisionBitMask 这个属性表示哪些种类的对象与当前怪物对象相碰撞时物理引擎要让其有所反应(比如回弹效果)。你并不想让怪物和子弹彼此之间发生回弹,设置这个属性为0吧。当然这在其他游戏里是可能的。

下一步添加一些相似的代码到touchesEnded:withEvent:方法里,就在设置子弹位置的代码之后:

projectile.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:projectile.size.width/2];
projectile.physicsBody.dynamic = YES;
projectile.physicsBody.categoryBitMask = projectileCategory;
projectile.physicsBody.contactTestBitMask = monsterCategory;
projectile.physicsBody.collisionBitMask = 0;
projectile.physicsBody.usesPreciseCollisionDetection = YES;

试试看你是否能理解这里的每行代码,如果不能,请参照之前怪物代码的解释。
再试试你是否能发现两者之间细微的区别并回答下面的问题。

Solution Inside: 它们有何区别? Show
 
   
 

下一步,添加一个在子弹和怪物发生碰撞后会被调用的方法。注意这个方法不会被自动调用,你将要在稍后调用它。

- (void)projectile:(SKSpriteNode *)projectile didCollideWithMonster:(SKSpriteNode *)monster {
NSLog(@"Hit");
[projectile removeFromParent];
[monster removeFromParent];
}

这里做的都是为了在子弹和怪物发生碰撞时把它们从当前的scene上移除。是不是非常简单?
到了实现接触后代理方法的时候了,将下面的代码添加到文件里:

- (void)didBeginContact:(SKPhysicsContact *)contact
{
// 1
SKPhysicsBody *firstBody, *secondBody;
 
if (contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask)
{
firstBody = contact.bodyA;
secondBody = contact.bodyB;
}
else
{
firstBody = contact.bodyB;
secondBody = contact.bodyA;
}
 
// 2
if ((firstBody.categoryBitMask & projectileCategory) != 0 &&
(secondBody.categoryBitMask & monsterCategory) != 0)
{
[self projectile:(SKSpriteNode *) firstBody.node didCollideWithMonster:(SKSpriteNode *) secondBody.node];
}
}

由于你将当前的scene设为了物理体系发生碰撞后的代理( contactDelegate),这个方法会在两个物理外形发生碰撞时被调用(调用的条件还有它们的contactTestBitMasks属性也要被正确设置)。
这个方法分成两部分:

  1. 这个方法传给你发生碰撞的两个物理外形(子弹和怪物),但是不能保证它们会按特定的顺序传给你。所以有一部分代码是用来把它们按各自的种类掩码进行排序的。这样你稍后才能针对对象种类做操作。这部分的代码来源于苹果官方Adventure例子。
  2. 方法的后一部分是用来检查这两个外形是否一个是子弹另一个是怪物,如果是就调用刚刚写的方法(指把它们从scene上移除的方法)。

最后一步,在MyScene的私有声明上让其实现SKPhysicsContactDelegate这个代理协议,这样才能编译通过。

@interface MyScene ()

编译运行,然后子弹在碰到目标(怪物)时它们就会一起消失了!

即将完成

你马上就要完成这个简单的游戏了。只要再添加一些音效(哪种游戏也不能没有声音啊!)和一些简单的游戏逻辑即可。
Sprite Kit没有像Cocos2D一样提供声音引擎,但值得庆幸的是它可以通过动作这种简便的方式来实现。并且你可以通过同样很简单的AVFoundation类库来播放背景音乐。
你的项目里已经有一些我做的背景音乐(很酷哦)和一个给力的piu~piu~音效了。它们是从这个教程的资源包里添加的。你只要播放就好了!
为了实现这些,将下面的代码添加到Viewcontroller.m文件里:

@import AVFoundation;

这里使用了iOS的新特性,通过使用新的@import 关键字,你可以更简单、更高效地引入头文件(还有类库)。想要进一步了解它,请阅读我们在iOS 7 by Tutorials里的第10章——Objective-C 和 iOS基础类库有什么新玩意。
下一步,添加一个新的属性和一个私有声明:

@interface ViewController ()
@property (nonatomic) AVAudioPlayer * backgroundMusicPlayer;
@end

将如下代码添加到 viewWillLayoutSubviews方法中,添加到 [super viewWillLayoutSubviews]的后面:

NSError *error;
NSURL * backgroundMusicURL = [[NSBundle mainBundle] URLForResource:@"background-music-aac" withExtension:@"caf"];
self.backgroundMusicPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:backgroundMusicURL error:&error];
self.backgroundMusicPlayer.numberOfLoops = -1;
[self.backgroundMusicPlayer prepareToPlay];
[self.backgroundMusicPlayer play];

This is some simple code to start the background music playing with endless loops.
As for the sound effect, switch back to MyScene.m and add this line to the top oftouchesEnded:withEvent::

[self runAction:[SKAction playSoundFileNamed:@"pew-pew-lei.caf" waitForCompletion:NO]];

了解?你可以仅仅用一行代码来播放一个声音!
让我们来创建一个新的scene来展现游戏输赢的结果。在新建界面中按 iOSCocoa TouchObjective-C class的方式从模版创建新类,将之命名为GameOverScene,使其继承自  SKScene ,然后单击下一步(Next)而后创建(Create)。
然后用下面的代码替换GameOverScene.h中原有的代码:

#import <SpriteKit/SpriteKit.h>
@interface GameOverScene : SKScene
-(id)initWithSize:(CGSize)size won:(BOOL)won;
@end

这里你引入了Sprite Kit的头文件并且声明了一个特殊的初始化方法,这个初始化方法除了需要传入size大小外还要传入用户的游戏结果(布尔值,表示输赢)。
然后用以下代码替换 GameOverLayer.m中的原有代码:

#import "GameOverScene.h"
#import "MyScene.h"
@implementation GameOverScene
-(id)initWithSize:(CGSize)size won:(BOOL)won {
if (self = [super initWithSize:size]) {
 
// 1
self.backgroundColor = [SKColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0];
 
// 2
NSString * message;
if (won) {
message = @"You Won!";
} else {
message = @"You Lose :[";
}
 
// 3
SKLabelNode *label = [SKLabelNode labelNodeWithFontNamed:@"Chalkduster"];
label.text = message;
label.fontSize = 40;
label.fontColor = [SKColor blackColor];
label.position = CGPointMake(self.size.width/2, self.size.height/2);
[self addChild:label];
 
// 4
[self runAction:
[SKAction sequence:@[
[SKAction waitForDuration:3.0],
[SKAction runBlock:^{
// 5
SKTransition *reveal = [SKTransition flipHorizontalWithDuration:0.5];
SKScene * myScene = [[MyScene alloc] initWithSize:self.size];
[self.view presentScene:myScene transition: reveal];
}]
]]
];
 
}
return self;
}
@end

这是上述代码的解释:

  1. 将背景颜色设置为白色,与主要的scene(MyScene)相同。
  2. 根据传入的输赢参数,设置弹出的消息字符串“You Won”或者 “You Lose”。
  3. 这部分代码表示在Sprite Kit下如何把文本标签显示到屏幕上。和你看到的一样,相当简单,只需要选择字体然后设置一些参数即可。
  4. 最后,创建并且运行一个系列类型动作,它包含两个子动作。为了让你更好的理解,我把它们都写在了一起(而没有采用以不同变量分开表示不同aciton的方式)。这里第一个action仅仅是等待3秒钟,然后会执行runBlock中的第二个action来做一些马上会执行的操作。
  5. 这里实现的是在Sprite Kit下如何转场(从现有场景转到新的场景)。首先你可以从多种转场特效动画中挑选一个自己喜欢的用来展示,这里选了一个0.5秒的翻转特效。然后创建即将要被显示的scene,使用self.view的presentScene:transition: 方法进行转场即可。

到现在为止一切顺利,你只要在游戏结束时用你的主场景(MyScene)来加载游戏结束的场景(GameOverScene)就好了。
为了实现这个功能,首先要把新的scene引入到MyScene.m文件中:

#import "GameOverScene.h"

然后在addMonster方法中用下面的action替换最后一行的action:

SKAction * loseAction = [SKAction runBlock:^{
SKTransition *reveal = [SKTransition flipHorizontalWithDuration:0.5];
SKScene * gameOverScene = [[GameOverScene alloc] initWithSize:self.size won:NO];
[self.view presentScene:gameOverScene transition: reveal];
}];
[monster runAction:[SKAction sequence:@[actionMove, loseAction, actionMoveDone]]];

这里创建了一个新的“失败action”用来展示游戏结束的场景,当怪物移动到屏幕边缘时游戏就结束了。看看你是否理解了这里的代码,如果没有就翻看之前block的解释吧(指GameOverScene中的系列action)。
还有一个很常见的问题:为什么你要在actionMoveDone 动作之前运行loseAction动作?如果你不知道为什么,那就手动改变两者的顺序试试吧。

Solution Inside: Why is Lose Action First? Show
 
   
 

现在你应该相应也了解了赢得游戏的情形,不要对玩家太残忍哦!:] 像MyScene.m的私有声明中添加一个新的属性:

@property (nonatomic) int monstersDestroyed;

然后把下面的代码添加到 projectile:didCollideWithMonster: 方法底部:

self.monstersDestroyed++;
if (self.monstersDestroyed > 30) {
SKTransition *reveal = [SKTransition flipHorizontalWithDuration:0.5];
SKScene * gameOverScene = [[GameOverScene alloc] initWithSize:self.size won:YES];
[self.view presentScene:gameOverScene transition: reveal];
}

编译运行之,游戏会在对应的情况下展示输赢场景了!

接下来该去向何方?

这个阶段完成了!这里是当前Sprite Kit初学者入门教程的 完整代码 。
我希望你能够沉浸在学习Sprite Kit和制作你自己的游戏之中。
如果你打算学习更多有关于Sprite Kit的知识,你可以购买我们的书iOS Games by Tutorials。我们会从物理特性、瓦片地图(tile maps)、粒子系统等方面让你了解一切你需要了解的东西。甚至专门为你选择合适水平的编辑(来撰写文章)。
如果你对这片教程有问题或者想要吐槽,请加入我们下面的讨论组!

Sprite Kit 入门教程的更多相关文章

  1. swift语言注册非免费苹果账号iOS游戏框架Sprite Kit基础教程

    swift语言注册非免费苹果账号iOS游戏框架Sprite Kit基础教程 1.2.3  注册非免费苹果账号swift语言注册非免费苹果账号iOS游戏框架Sprite Kit基础教程 免费的苹果账号在 ...

  2. Swift版iOS游戏框架Sprite Kit基础教程下册

    Swift版iOS游戏框架Sprite Kit基础教程下册 试读下载地址:http://pan.baidu.com/s/1qWBdV0C 介绍:本教程是国内唯一的Swift版的Spritekit教程. ...

  3. iOS Sprite Kit教程之滚动场景

    iOS Sprite Kit教程之滚动场景 滚动场景 在很多的游戏中,场景都不是静止的,而是滚动的,如在植物大战僵尸的游戏中,它的场景如图2.26所示. 图2.26  植物大战僵尸 在图2.26中,用 ...

  4. iOS Sprite Kit教程之场景的切换

    iOS Sprite Kit教程之场景的切换 Sprite Kit中切换场景 每一个场景都不是单独存在的.玩家可以从一个场景中切换到另外一个场景中.本小节,我们来讲解场景切换.在每一个游戏中都会使用到 ...

  5. iOS Sprite Kit教程之场景的设置

    iOS Sprite Kit教程之场景的设置 Sprite Kit中设置场景 在图2.8所示的效果中,可以看到新增的场景是没有任何内容的,本节将讲解对场景的三个设置,即颜色的设置.显示模式的设置以及测 ...

  6. iOS Sprite Kit教程之真机测试以及场景的添加与展示

    iOS Sprite Kit教程之真机测试以及场景的添加与展示 IOS实现真机测试 在进行真机测试之前,首先需要确保设备已经连在了Mac(或者Mac虚拟机)上,在第1.9.1小节开始,设备就一直连接在 ...

  7. iOS Sprite Kit教程之申请和下载证书

    iOS Sprite Kit教程之申请和下载证书 模拟器虽然可以实现真机上的一些功能,但是它是有局限的.例如,在模拟器上没有重力感应.相机机等.如果想要进行此方面的游戏的开发,进行程序测试时,模拟器显 ...

  8. iOS Sprite Kit教程之使用帮助文档以及调试程序

    iOS Sprite Kit教程之使用帮助文档以及调试程序 IOS中使用帮助文档 在编写代码的时候,可能会遇到很多的方法.如果开发者对这些方法的功能,以及参数不是很了解,就可以使用帮助文档.那么帮助文 ...

  9. iOS Sprite Kit教程之编写程序以及Xcode的介绍

    iOS Sprite Kit教程之编写程序以及Xcode的介绍 Xcode界面介绍 一个Xcode项目由很多的文件组成,例如代码文件.资源文件等.Xcode会帮助开发者对这些文件进行管理.所以,Xco ...

随机推荐

  1. 更强的微光闪烁效果--第三方开源--Shimmer-android

    Shimmer-android在github上的项目主页是:https://github.com/RomainPiel/Shimmer-android Shimmer-android干脆在Androi ...

  2. Ubuntu 下安装 Oracle JDK

    sudo add-apt-repository ppa:webupd8team/javasudo apt-get updatesudo apt-get install oracle-java8-ins ...

  3. jquery.unobtrusive-ajax.js的扩展,做到片段式加载

    //ajax支持库 /*! ** Unobtrusive Ajax support library for jQuery ** Copyright (C) Microsoft Corporation. ...

  4. Autofac的注入和web.config配合

    public static void BuildMvcContainer() { var builder = new ContainerBuilder(); var assemblys = AppDo ...

  5. linux 下的使用 ln 创建 软链接 和 硬链接

    linux 下的一个指令 ln 作用: 创建软链接或者硬链接 Linux 系统下每创建一个文件,系统都会为此文件生成一个 index node 简称(inode) ,而每一个文件都包含用户数据(use ...

  6. Oracle表结构转换SqlSERVER表结构 脚本

    在审计工作中,有时需要将Oracle的表结构修改后再SqlSERVER中创建表结构,然后将数据导入到SqlSERVER中,在修改表结构的过程中方法狠多.手工修改,最蠢的方法,或者用工具UE批量修改,还 ...

  7. GraphLab面向机器学习的并行框架『针对图数据处理模型』

    最近在做文本处理知识的梳理,关注了CMU提出的GraphLab开源分布式计算系统 这是关于GraphLab的PPT:Distributed GraphLab『 http://cheng-qihang- ...

  8. 编写可维护的JavaScript之简易模版

    /* * 正则替换%s * @para arg1(text) 需要替换的模版 * @para arg2 替换第一处%s * @para arg3 替换第二处%s * 返回替换后的字符串 */ var ...

  9. Git操作指南(2) —— Git Gui for Windows的建库、克隆、上传

    本教程将讲述:gitk的Git Gui的部分常用功能和使用方法,包括:建库.克隆(clone).上传(push).下载(pull - fetch).合并(pull - merge). ———————— ...

  10. LD_PRELOAD

    下面的helloworld会在屏幕上打印出什么内容? 1 2 3 4 5 6 #include <stdio.h> int main(int argc, char* argv[], cha ...