首页 > 其他 > 详细

一个简单的 Sprite Kit入门

时间:2015-10-27 17:08:40      阅读:591      评论:0      收藏:0      [点我收藏+]

作者:Ray Wenderlich
技术分享

原文出处:点击打开链接

http://www.raywenderlich.com/42699/spritekit-tutorial-for-beginners

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并且用下面的代码替换掉它原有的内容:

 

  1. #import "MyScene.h"  
  2. // 1  
  3. @interface MyScene ()  
  4. @property (nonatomic) SKSpriteNode * player;  
  5. @end  
  6. @implementation MyScene  
  7. -(id)initWithSize:(CGSize)size {      
  8.     if (self = [super initWithSize:size]) {  
  9.    
  10.         // 2  
  11.         NSLog(@"Size: %@", NSStringFromCGSize(size));  
  12.    
  13.         // 3  
  14.         self.backgroundColor = [SKColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0];  
  15.    
  16.         // 4  
  17.         self.player = [SKSpriteNode spriteNodeWithImageNamed:@"player"];  
  18.         self.player.position = CGPointMake(100, 100);  
  19.         [self addChild:self.player];  
  20.    
  21.     }  
  22.     return self;  
  23. }  
  24. @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),这应 该会在屏幕左下角的右上方一点。

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

 

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


因此我们的scene 认为它的宽是320而高是568,但这恰好反了。

为了看看到底发生了什么,我们找到ViewController.m 的viewDidLoad方法:

 

  1. - (void)viewDidLoad  
  2. {  
  3.     [super viewDidLoad];  
  4.     // Configure the view.  
  5.     SKView * skView = (SKView *)self.view;  
  6.     skView.showsFPS = YES;  
  7.     skView.showsNodeCount = YES;  
  8.    
  9.     // Create and configure the scene.  
  10.     SKScene * scene = [MyScene sceneWithSize:skView.bounds.size];  
  11.     scene.scaleMode = SKSceneScaleModeAspectFill;  
  12.    
  13.     // Present the scene.  
  14.     [skView presentScene:scene];  
  15. }  

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

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

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

 

  1. - (void)viewWillLayoutSubviews  
  2. {  
  3.     [super viewWillLayoutSubviews];  
  4.     // Configure the view.  
  5.     SKView * skView = (SKView *)self.view;  
  6.     if (!skView.scene) {  
  7.       skView.showsFPS = YES;  
  8.       skView.showsNodeCount = YES;  
  9.    
  10.       // Create and configure the scene.  
  11.       SKScene * scene = [MyScene sceneWithSize:skView.bounds.size];  
  12.       scene.scaleMode = SKSceneScaleModeAspectFill;  
  13.    
  14.       // Present the scene.  
  15.       [skView presentScene:scene];  
  16.     }  
  17. }  


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

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

 

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


移动的怪物

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

 

  1. - (void)addMonster {  
  2.     // 创建怪物Sprite  
  3.     SKSpriteNode * monster = [SKSpriteNode spriteNodeWithImageNamed:@"monster"];  
  4.    
  5.     // 决定怪物在竖直方向上的出现位置  
  6.     int minY = monster.size.height / 2;  
  7.     int maxY = self.frame.size.height - monster.size.height / 2;  
  8.     int rangeY = maxY - minY;  
  9.     int actualY = (arc4random() % rangeY) + minY;  
  10.    
  11.     // Create the monster slightly off-screen along the right edge,  
  12.     // and along a random position along the Y axis as calculated above  
  13.     monster.position = CGPointMake(self.frame.size.width + monster.size.width/2, actualY);  
  14.     [self addChild:monster];  
  15.    
  16.     // 设置怪物的速度  
  17.     int minDuration = 2.0;  
  18.     int maxDuration = 4.0;  
  19.     int rangeDuration = maxDuration - minDuration;  
  20.     int actualDuration = (arc4random() % rangeDuration) + minDuration;  
  21.    
  22.     // Create the actions  
  23.     SKAction * actionMove = [SKAction moveTo:CGPointMake(-monster.size.width/2, actualY) duration:actualDuration];  
  24.     SKAction * actionMoveDone = [SKAction removeFromParent];  
  25.     [monster runAction:[SKAction sequence:@[actionMove, actionMoveDone]]];  
  26. }  


我会慢一点把代码讲解清楚,让其尽可能容易理解。第一部分正如之前提到过的:我们需要做一些简单的计算来创建怪物对象。为它们设置合适的位置并且用和忍者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的私有声明里:

 

  1. @property (nonatomic) NSTimeInterval lastSpawnTimeInterval;  
  2. @property (nonatomic) NSTimeInterval lastUpdateTimeInterval;  


我们会使用lastSpawnTimeInterval这个属性来记录上一次生成怪物的时间, 使用lastUpdateTimeInterval这个属性来记录上一次更新的时间。

下一步,你会编写一个每帧都会调用的方法。这个方法的参数是上次更新后的时间增量。由于它不会被默认调用,你需要在下一步编写另一个方法来调用它。

 

  1. - (void)updateWithTimeSinceLastUpdate:(CFTimeInterval)timeSinceLast {  
  2.     self.lastSpawnTimeInterval += timeSinceLast;  
  3.     if (self.lastSpawnTimeInterval > 1) {  
  4.         self.lastSpawnTimeInterval = 0;  
  5.         [self addMonster];  
  6.     }  
  7. }  


在这里你只是简单地把上次更新后的时间增量加给lastSpawnTimeInterval。一旦它的值大于一秒,你就要生成一个怪物然后重置时间。

接下来,添加如下方法来调用上面的updateWithTimeSinceLastUpdate方法 。

 

  1. <span style="color:#333333;">- (void)update:(NSTimeInterval)currentTime {  
  2.     // 获取时间增量  
  3.     // 如果我们运行的每秒帧数低于60,我们依然希望一切和每秒60帧移动的位移相同  
  4.     CFTimeInterval timeSinceLast = currentTime - self.lastUpdateTimeInterval;  
  5.     self.lastUpdateTimeInterval = currentTime;  
  6.     if (timeSinceLast > 1) { // 如果上次更新后得时间增量大于1秒  
  7.         timeSinceLast = 1.0 / 60.0;  
  8.         self.lastUpdateTimeInterval = currentTime;  
  9.     }  
  10.    
  11.     [self updateWithTimeSinceLastUpdate:timeSinceLast];  
  12.    
  13. }</span>  


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

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

发射子弹

到这里,你可以已经迫不及待的为忍者添加一些动作了,那么我们就添加攻击吧。攻击的实现方式有很多种,但在这个游戏里攻击会在玩家点击屏幕时触发,忍者会朝着点按的方向发射一个子弹。
我打算使用moveTo:action动作来实现子弹的前期运行动画,为了实现它需要一些数学运算。这是因为moveTo:需要传入子弹运行轨迹的终点, 由于用户点按触发的位置仅仅代表了子弹射出的方向,显然我们不能直接将其当作运行终点。实际上就算子弹超过了触摸点你也应该让子弹保持移动直到子弹超出屏 幕为止。
这是一张图片,它标注了这个问题:
技术分享
就像你看到的,从子弹发射原点到用户触摸点在x轴和y轴上的偏移量会形成一个小三角形。你只要以相同的比例去实现一个顶点在屏幕边缘的大三角形即可。
为了进行这部分的运算,有一些关于向量的基本数学计算方法很有帮助(比如向量间的加减法)。然而,Sprite Kit默认并没有提供,所以你需要自己来实现了。
幸运的是这很容易实现。把下面的方法添加到文件顶部:

 

  1. static inline CGPoint rwAdd(CGPoint a, CGPoint b) {  
  2.     return CGPointMake(a.x + b.x, a.y + b.y);  
  3. }  
  4. static inline CGPoint rwSub(CGPoint a, CGPoint b) {  
  5.     return CGPointMake(a.x - b.x, a.y - b.y);  
  6. }  
  7. static inline CGPoint rwMult(CGPoint a, float b) {  
  8.     return CGPointMake(a.x * b, a.y * b);  
  9. }  
  10. static inline float rwLength(CGPoint a) {  
  11.     return sqrtf(a.x * a.x + a.y * a.y);  
  12. }  
  13. // 让向量的长度(模)等于1  
  14. static inline CGPoint rwNormalize(CGPoint a) {  
  15.     float length = rwLength(a);  
  16.     return CGPointMake(a.x / length, a.y / length);  
  17. }  


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

下一步,添加一个新方法:

 

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


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

  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来做不同的处理。
  • 设置一个关联的代理。还记得刚刚提到的物理体系么?你可以为它设置一个与之相关联的代理,当两个物体发生碰撞时来接收通知。这里你将要添加一些有关于对象种类判断的代码,用来判断到底是子弹还是怪物,然后你会为它们增加碰撞的声音等效果。

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

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

Collision Detection

So now you have shurikens flying everywhere – but what your ninja really wants to do is to lay some smack down. So let’s add in some code to detect when your projectiles intersect your targets.

There are various ways to solve this with Cocos2D, including using one of the included physics libraries: Box2D or Chipmunk. However to keep things simple, you are going to implement simple collision detection yourself.

To do this, you first need to keep better track of the targets and projectiles currently in the scene. Add the following to your HelloWorldLayer class declaration:

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

NSMutableArray * _monsters;
NSMutableArray * _projectiles;

And initialize the arrays in your init method:

在你的场景初始化方法里初始化这些数组

_monsters = [[NSMutableArray alloc] init];
_projectiles = [[NSMutableArray alloc] init];

And while you’re thinking of it, clean up the memory in your dealloc method (at the time of writing this tutorial, ARC isn’t enabled by default in the Cocos2D 2.X template):

趁你还在思考这个,别忘记在dealloc方法里清除内存把,(这个教程里,Cocos2D 2.X 模板不支持ARC)

[_monsters release];
_monsters = nil;
[_projectiles release];
_projectiles = nil;

Note: Even though ARC isn’t enabled by default in the Cocos2D template, it is very easy to do so. To learn how, check out this tutorial.

Now, modify your addMonster method to add the new monster to the monsters array and set a tag for future use:

monster.tag = 1;
[_monsters addObject:monster];

And modify your ccTouchesEnded method to add the new projectile to the projectiles array and set a tag for future use:

projectile.tag = 2;
[_projectiles addObject:projectile];

Finally, modify your both of your CCCallBlockN blocks to remove the sprite from the appropriate array:

// CCCallBlockN in addMonster
[_monsters removeObject:node];
 
// CCCallBlockN in ccTouchesEnded
[_projectiles removeObject:node];

Build and run the project to make sure everything is still working OK. There should be no noticeable difference at this point, but now you have the bookkeeping you need to implement some collision detection.

Now add the following new method:

- (void)update:(ccTime)dt {
 
    NSMutableArray *projectilesToDelete = [[NSMutableArray alloc] init];
    for (CCSprite *projectile in _projectiles) {
 
        NSMutableArray *monstersToDelete = [[NSMutableArray alloc] init];
        for (CCSprite *monster in _monsters) {
 
            if (CGRectIntersectsRect(projectile.boundingBox, monster.boundingBox)) {
                [monstersToDelete addObject:monster];
            }
        }
 
        for (CCSprite *monster in monstersToDelete) {
            [_monsters removeObject:monster];
            [self removeChild:monster cleanup:YES];
        }
 
        if (monstersToDelete.count > 0) {
            [projectilesToDelete addObject:projectile];
        }
        [monstersToDelete release];
    }
 
    for (CCSprite *projectile in projectilesToDelete) {
        [_projectiles removeObject:projectile];
        [self removeChild:projectile cleanup:YES];
    }
    [projectilesToDelete release];
}

The above should be pretty clear. You just iterate through your projectiles and monsters, creating rectangles corresponding to their bounding boxes, and use CGRectIntersectsRect to check for intersections. If any are found, you remove them from the scene and from the arrays.

Note that you have to add the objects to a “toDelete” array because you can’t remove an object from an array while you are iterating through it. Again, there are more optimal ways to implement this kind of thing, but you are going for the simple approach.

You just need one more thing before you’re ready to roll – schedule this method to run as often as possible by adding the following line to your init method:

[self schedule:@selector(update:)];

Give it a build and run, and now when your projectiles intersect targets they should disappear!

Finishing Touches

You’re pretty close to having a workable (but extremely simple) game now. You just need to add some sound effects and music (since what kind of game doesn’t have sound!) and some simple game logic.

If you’ve read our blog series on audio programming for the iPhone, you’ll be extremely pleased to hear how simple the Cocos2D developers have made it to play basic sound effects in your game.

You already have some cool background music I made and an awesoem pew-pew sound effect in your project, from the resources for this tutorial you downloaded earlier. You just need to play them!

To do this, add the following import to the top of HelloWorldLayer.m:

#import "SimpleAudioEngine.h"

In your init method, start up the background music as follows:

[[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"background-music-aac.caf"];

And in your ccTouchesEnded method play the sound effect as follows:

[[SimpleAudioEngine sharedEngine] playEffect:@"pew-pew-lei.caf"];

Now, let’s create a new scene and layer that will serve as your “You Win” or “You Lose” indicator. Create a new file with the iOS\cocos2d v2.x\CCNode class template, make it a subclass of CCLayerColor, and click Next. Name it GameOverLayer, and click Create.

Then replace GameOverLayer.h with the following code:

#import "cocos2d.h"
 
@interface GameOverLayer : CCLayerColor
 
+(CCScene *) sceneWithWon:(BOOL)won;
- (id)initWithWon:(BOOL)won;
 
@end

Then replace GameOverLayer.m with the following code:

#import "GameOverLayer.h"
#import "HelloWorldLayer.h"
 
@implementation GameOverLayer
 
+(CCScene *) sceneWithWon:(BOOL)won {
    CCScene *scene = [CCScene node];
    GameOverLayer *layer = [[[GameOverLayer alloc] initWithWon:won] autorelease];
    [scene addChild: layer];
    return scene;
}
 
- (id)initWithWon:(BOOL)won {
    if ((self = [super initWithColor:ccc4(255, 255, 255, 255)])) {
 
        NSString * message;
        if (won) {
            message = @"You Won!";
        } else {
            message = @"You Lose :[";
        }
 
        CGSize winSize = [[CCDirector sharedDirector] winSize];
        CCLabelTTF * label = [CCLabelTTF labelWithString:message fontName:@"Arial" fontSize:32];
        label.color = ccc3(0,0,0);
        label.position = ccp(winSize.width/2, winSize.height/2);
        [self addChild:label];
 
        [self runAction:
         [CCSequence actions:
          [CCDelayTime actionWithDuration:3],
          [CCCallBlockN actionWithBlock:^(CCNode *node) {
             [[CCDirector sharedDirector] replaceScene:[HelloWorldLayer scene]];
        }],
          nil]];
    }
    return self;
}
 
@end

Note that there are two different objects here: a scene and a layer. The scene can contain any number of layers, however in this example it just has one. The layer just puts a label in the middle of the screen, and schedules a transition to occur 3 seconds in the future back to the Hello World scene.

Finally, let’s add some extremely basic game logic. First, let’s keep track of the projectiles the player has destroyed. Add a member variable to your HelloWorldLayer class in HelloWorldLayer.h as follows:

int _monstersDestroyed;

Inside HelloWorldLayer.m, add an import for the GameOverScene class:

#import "GameOverLayer.h"

Increment the count and check for the win condition in your update method inside the monstersToDelete loop right after removeChild:monster:

_monstersDestroyed++;
if (_monstersDestroyed > 30) {
    CCScene *gameOverScene = [GameOverLayer sceneWithWon:YES];
    [[CCDirector sharedDirector] replaceScene:gameOverScene];
}

And finally let’s make it so that if even one target gets by, you lose. In addMonster:’s CCCallBlockN callback, add the following right after removeFromParentAndCleanup:

CCScene *gameOverScene = [GameOverLayer sceneWithWon:NO];
[[CCDirector sharedDirector] replaceScene:gameOverScene];

Go ahead and give it a build and run, and you should now have win and lose conditions and see a game over scene when appropriate!

Where To Go From Here?

And that’s a wrap! Here’s the full code for the simple Cocos2D iPhone game that you developed thus far.

This project could be a nice basis for playing around some more with Cocos2D by adding some new features into the project. Maybe try adding in a bar chart to show how many more targets you have to destroy before you win (check out the drawPrimitivesTest sample project for examples of how to do that). Maybe add cooler death animations for when the monsters are destroyed (see ActionsTest, EffectsTest, and EffectsAdvancedTest projects for that). Maybe add more sounds, artwork, or gameplay logic just for fun. The sky’s the limit!

If you want to keep going with this tutorial series, check out part two, How To Add A Rotating Turret, or part three, Harder Monsters and More Levels!

Also, if you’d like to keep learning more about Cocos2D, we have ton of Cocos2D tutorials on this site – this is the right place to be! :]

If you have any questions or comments about this tutorial, please join the discussion below!

 

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

Collision Detection and Physics: Implementation

Start by adding these two constants to the top of MyScene.m:

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

This is setting up the two categories you’ll need in a bit – one for the projectiles and one for the monsters.

Note: You may be wondering what the fancy syntax is here. Note that the category on Sprite Kit is just a single 32-bit integer, and acts as a bitmask. This is a fancy way of saying each of the 32-bits in the integer represents a single category (and hence you can have 32 categories max). Here you’re setting the first bit to indicate a projectile, and the next bit over to represent a monster.

Next, inside initWithSize add these lines after adding the player to the scene:

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

This sets up the physics world to have no gravity, and sets the scene as the delegate to be notified when two physics bodies collide.

Inside the addMonster method, add these lines right after creating the monster sprite:

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

Let’s go over what this does line by line.

  1. Creates a physics body for the sprite. In this case, the body is defined as a rectangle of the same size of the sprite, because that’s a decent approximation for the monster.
  2. Sets the sprite to be dynamic. This means that the physics engine will not control the movement of the monster – you will through the code you’ve already written (using move actions).
  3. Sets the category bit mask to be the monsterCategory you defined earlier.
  4. The contactTestBitMask indicates what categories of objects this object should notify the contact listener when they intersect. You choose projectiles here.
  5. The collisionBitMask indicates what categories of objects this object that the physics engine handle contact responses to (i.e. bounce off of). You don’t want the monster and projectile to bounce off each other – it’s OK for them to go right through each other in this game – so you set this to 0.

Next add some similar code to touchesEnded:withEvent:, right after the line setting the projectile’s position:

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;

As a test, see if you can understand each line here and what it does. If not, just refer back to the points explained above!

As a second test, see if you can spot two differences. Answer below!

Solution Inside: What Are the Differences?Show
 
   
 

Next, add a method that will be called when the projectile collides with the monster. Note that nothing calls this automatically, you will be calling this later.

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

All you do here is remove the projectile and monster from the scene when they collide. Pretty simple, eh?

Now it’s time to implement the contact delegate method. Add the following new method to the file:

- (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];
    }
}

Since you set the scene as the contactDelegate of the physics world earlier, this method will be called whenever two physics bodies collide (and their contactTestBitMasks are set appropriately).

There are two parts to this method:

  1. This method passes you the two bodies that collide, but does not guarantee that they are passed in any particular order. So this bit of code just arranges them so they are sorted by their category bit masks so you can make some assumptions later. This bit of code came from Apple’s Adventure sample.
  2. Finally, it checks to see if the two bodies that collide are the projectile and monster, and if so calls the method you wrote earlier.

One last step – mark the private interface as implementing SKPhysicsContactDelegate to make the compiler happy:

@interface MyScene () <SKPhysicsContactDelegate>

Give it a build and run, and now when your projectiles intersect targets they should disappear!

Finishing Touches

You’re pretty close to having a workable (but extremely simple) game now. You just need to add some sound effects and music (since what kind of game doesn’t have sound!) and some simple game logic.

Sprite Kit does not come with an audio engine like Cocos2D does, but the good news it does come with a simple way to play sound effects via actions, and that you can play background music pretty easily with AVFoundation.

You already have some cool background music I made and an awesome pew-pew sound effect in your project, from the resources for this tutorial you added to your project earlier. You just need to play them!

To do this, add the following import to the top of ViewController.m:

@import AVFoundation;

This demonstrates the new modules feature in iOS 7 – basically by using the new @import keyword, you can bring in the header files (and library) for a framework in a single, efficient step. To learn more about this, check out Chapter 10 in iOS 7 by Tutorials, What’s New with Objective-C and Foundation.

Next add a new property and private interface:

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

Add this code to viewWillLayoutSubviews, right after the call to [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 of touchesEnded:withEvent::

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

Pretty handy, eh? You can play a sound effect with one line!

Now, let’s create a new scene and layer that will serve as your “You Win” or “You Lose” indicator. Create a new file with the iOS\Cocoa Touch\Objective-C class template, name the class GameOverScene, make it a subclass of SKScene, and click Next and then Create.

Then replace GameOverScene.h with the following code:

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

Here you imported the Sprite Kit header and marked that you are implementing a special initializer that takes a parameter of whether the user won the level or not in addition to the size.

Then replace GameOverLayer.m with the following code:

#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

There are four parts to this method:

  1. Sets the background color to white, same as you did for the main scene.
  2. Based on the won parameter, sets the message to either “You Won” or “You Lose”.
  3. This is how you display a label of text to the screen with Sprite Kit. As you can see, it’s pretty easy – you just choose your font and set a few parameters.
  4. Finally, this sets up and runs a sequence of two actions. I’ve included them all inline here to show you how handy that is (instead of having to make separate variables for each action). First it waits for 3 seconds, then it uses the runBlock action to run some arbitrary code.
  5. This is how you transition to a new scene in Sprite Kit. First you can pick from a variety of different animated transitions for how you want the scenes to display – you choose a flip transition here that takes 0.5 seconds. Then you create the scene you want to display, and use the presentScene:transition: method on the self.view property.

So far so good, now you just need to set up your main scene to load the game over scene when appropriate.

To do this, first add an import for the new scene at the top of MyScene.m:

#import "GameOverScene.h"

Then inside addMonster, replace the last line that runs the actions on the monster with the following:

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]]];

This creates a new “lose action” that displays the game over scene when a monster goes off-screen. See if you understand each line here, if not refer to the explanation for the previous code block.

Also, another pop-quiz for you: why do you run the loseAction before actionMoveDone? Try reversing them to see what happens if you don’t know.

Solution Inside: Why is Lose Action First?Show
 
   
 

Now you should handle the win case too – don’t be cruel to your players! :] Add a new property to the private interface:

@property (nonatomic) int monstersDestroyed;

And add this to the bottom of 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];
}

Go ahead and give it a build and run, and you should now have win and lose conditions and see a game over scene when appropriate!

Where To Go From Here?

And that’s a wrap! Here is the full source code for this Sprite Kit tutorial for beginners.

I hope you enjoyed learning about Sprite Kit and are inspired to make your own game!

If you want to learn more about Sprite Kit, you should check out our book iOS Games by Tutorials. We’ll teach you everything you need to know – from physics, to tile maps, to particle systems, and even making your own level editor.

If you have any questions or comments about this tutorial, please join the discussion below!

    1. 这里设置了两个种类,等下就会用到。一个是子弹的,一个是怪物的。  
    2. <strong>注意:你可能对这种语法感到奇怪。你只要明白在Sprite Kit中category是一个32位的整型然后被用作掩码就好了。这是种用32位整型表示一个category的简单方式(所以你最多能创建32个category)。这里你用首位来表示子弹,用下一位来表示怪物。<
    3. </div>  
    4. <p style="border:0px; font-family:‘Open Sans‘,sans-serif; font-size:16px; margin-top:0px; margin-bottom:0px; outline:0px; padding-top:0px; padding-bottom:10px; vertical-align:baseline; line-height:25.875px">  
    5. 下一步,在initWithSize方法中,把忍者加到scene的代码后面再加入如下两行代码:</p>  
    6. <p style="border:0px; font-family:‘Open Sans‘,sans-serif; font-size:16px; margin-top:0px; margin-bottom:0px; outline:0px; padding-top:0px; padding-bottom:10px; vertical-align:baseline; line-height:25.875px">  
    7. </p>  
    8. <pre code_snippet_id="172739" snippet_file_name="blog_20140127_12_3719958" name="code" class="objc">self.physicsWorld.gravity = CGVectorMake(0,0);  
    9. self.physicsWorld.contactDelegate = self;</pre><br>  
    10. 这里设置了一个没有重力的物理体系,为了收到两个物体碰撞的消息需要把当前的scene设为它的代理。  
    11. <p></p>  
    12. <p style="border:0px; font-family:‘Open Sans‘,sans-serif; font-size:16px; margin-top:0px; margin-bottom:0px; outline:0px; padding-top:0px; padding-bottom:10px; vertical-align:baseline; line-height:25.875px">  
    13. 在addMonster方法中创建完怪物后添加如下代码:</p>  
    14. <p style="border:0px; font-family:‘Open Sans‘,sans-serif; font-size:16px; margin-top:0px; margin-bottom:0px; outline:0px; padding-top:0px; padding-bottom:10px; vertical-align:baseline; line-height:25.875px">  
    15. </p>  
    16. <pre code_snippet_id="172739" snippet_file_name="blog_20140127_13_5911785" name="code" class="objc">monster.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:monster.size]; // 1  
    17. monster.physicsBody.dynamic = YES; // 2  
    18. monster.physicsBody.categoryBitMask = monsterCategory; // 3  
    19. monster.physicsBody.contactTestBitMask = projectileCategory; // 4  
    20. monster.physicsBody.collisionBitMask = 0; 
    21. 让我们逐行看看上面的代码到底做了什么。  
    22. 为怪物sprite 创建物理外形。在这里,这个外形被定义成和怪物sprite大小一致的矩形,与怪物自身大致相匹配。</li><li style="border:0px; font-family:inherit; font-style:inherit; font-weight:inherit; margin:0px 0px 10px; outline:0px; padding:0px; vertical-align:baseline">  
    23. 将怪物物理外形的dynamic(动态)属性置为YES。这表示怪物的移动不会被物理引擎所控制。你可以在这里不受影响而继续使用之前的代码(指之前怪物的移动action)。</li><li style="border:0px; font-family:inherit; font-style:inherit; font-weight:inherit; margin:0px 0px 10px; outline:0px; padding:0px; vertical-align:baseline">  
    24. 把怪物物理外形的种类掩码设为刚刚定义的 <code style="border:0px; font-family:‘Droid Sans Mono‘,sans-serif; font-size:14px; font-style:inherit; font-weight:bold; margin:0px; outline:0px; padding:0px; vertical-align:baseline; color:rgb(0,104,55)">monsterCategory</code> 。</li><li style="border:0px; font-family:inherit; font-style:inherit; font-weight:inherit; margin:0px 0px 10px; outline:0px; padding:0px; vertical-align:baseline">  
    25. <code style="border:0px; font-family:‘Droid Sans Mono‘,sans-serif; font-size:14px; font-style:inherit; font-weight:bold; margin:0px; outline:0px; padding:0px; vertical-align:baseline; color:rgb(0,104,55)">当发生碰撞时,当前怪物对象会通知它contactTestBitMask</code> 这个属性所代表的category。这里应该把子弹的种类掩码projectileCategory赋给它。</li><li style="border:0px; font-family:inherit; font-style:inherit; font-weight:inherit; margin:0px 0px 10px; outline:0px; padding:0px; vertical-align:baseline">  
    26. <code style="border:0px; font-family:‘Droid Sans Mono‘,sans-serif; font-size:14px; font-style:inherit; font-weight:bold; margin:0px; outline:0px; padding:0px; vertical-align:baseline; color:rgb(0,104,55)">collisionBitMask</code> 这个属性表示哪些种类的对象与当前怪物对象相碰撞时物理引擎要让其有所反应(比如回弹效果)。你并不想让怪物和子弹彼此之间发生回弹,设置这个属性为0吧。当然这在其他游戏里是可能的。</li></ol>  
    27. <p style="border:0px; font-family:‘Open Sans‘,sans-serif; font-size:16px; margin-top:0px; margin-bottom:0px; outline:0px; padding-top:0px; padding-bottom:10px; vertical-align:baseline; line-height:25.875px">  
    28. 下一步添加一些相似的代码到<span style="border:0px; font-family:inherit; font-style:inherit; margin:0px; outline:0px; padding:0px; vertical-align:baseline; color:rgb(0,104,55)"><strong>touchesEnded:withEvent:方法里,</strong></span>就在设置子弹位置的代码之后:</p>  
    29. 试试看你是否能理解这里的每行代码,如果不能,请参照之前怪物代码的解释。  
    30. <p></p>  
    31. <p style="border:0px; font-family:‘Open Sans‘,sans-serif; font-size:16px; margin-top:0px; margin-bottom:0px; outline:0px; padding-top:0px; padding-bottom:10px; vertical-align:baseline; line-height:25.875px">  
    32. 下一步,添加一个在子弹和怪物发生碰撞后会被调用的方法。注意这个方法不会被自动调用,你将要在稍后调用它。</p>  
    33. <p style="border:0px; font-family:‘Open Sans‘,sans-serif; font-size:16px; margin-top:0px; margin-bottom:0px; outline:0px; padding-top:0px; padding-bottom:10px; vertical-align:baseline; line-height:25.875px">  
    34. </p>  
    35. <pre code_snippet_id="172739" snippet_file_name="blog_20140127_15_5104207" name="code" class="objc">- (void)projectile:(SKSpriteNode *)projectile didCollideWithMonster:(SKSpriteNode *)monster {  
    36.     NSLog(@"Hit");  
    37.     [projectile removeFromParent];  
    38.     [monster removeFromParent];  
    39. }
    40. 这里做的都是为了在子弹和怪物发生碰撞时把它们从当前的scene上移除。是不是非常简单?  
    41. <p></p>  
    42. <p style="border:0px; font-family:‘Open Sans‘,sans-serif; font-size:16px; margin-top:0px; margin-bottom:0px; outline:0px; padding-top:0px; padding-bottom:10px; vertical-align:baseline; line-height:25.875px">  
    43. 到了实现接触后代理方法的时候了,将下面的代码添加到文件里:</p>  
    44. <p style="border:0px; font-family:‘Open Sans‘,sans-serif; font-size:16px; margin-top:0px; margin-bottom:0px; outline:0px; padding-top:0px; padding-bottom:10px; vertical-align:baseline; line-height:25.875px">  
    45. </p>  
    46. <pre code_snippet_id="172739" snippet_file_name="blog_20140127_16_4995523" name="code" class="objc">- (void)didBeginContact:(SKPhysicsContact *)contact  
    47. {  
    48.     // 1  
    49.     SKPhysicsBody *firstBody, *secondBody;  
    50.    
    51.     if (contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask)  
    52.     {  
    53.         firstBody = contact.bodyA;  
    54.         secondBody = contact.bodyB;  
    55.     }  
    56.     else  
    57.     {  
    58.         firstBody = contact.bodyB;  
    59.         secondBody = contact.bodyA;  
    60.     }  
    61.    
    62.     // 2  
    63.     if ((firstBody.categoryBitMask & projectileCategory) != 0 &&  
    64.         (secondBody.categoryBitMask & monsterCategory) != 0)  
    65.     {  
    66.         [self projectile:(SKSpriteNode *) firstBody.node didCollideWithMonster:(SKSpriteNode *) secondBody.node];  
    67.     }  
    68. }</pre><br>  
    69. 由于你将当前的scene设为了物理体系发生碰撞后的代理( <code style="border:0px; font-family:‘Droid Sans Mono‘,sans-serif; font-size:14px; font-style:inherit; font-weight:bold; margin:0px; outline:0px; padding:0px; vertical-align:baseline; color:rgb(0,104,55)">contactDelegate),这个方法会在两个物理外形发生碰撞时被调用(调用的条件还有它们的<code style="border:0px; font-family:‘Droid Sans Mono‘,sans-serif; font-style:inherit; margin:0px; outline:0px; padding:0px; vertical-align:baseline">contactTestBitMask</code>s属性也要被正确设置)。</code>  
    70. <p></p>  
    71. <p style="border:0px; font-family:‘Open Sans‘,sans-serif; font-size:16px; margin-top:0px; margin-bottom:0px; outline:0px; padding-top:0px; padding-bottom:10px; vertical-align:baseline; line-height:25.875px">  
    72. 这个方法分成两部分:</p>  
    73. <ol style="border:0px; font-family:‘Open Sans‘,sans-serif; font-size:16px; margin:0px 0px 11px 30px; outline:0px; padding:0px; vertical-align:baseline; list-style-position:initial; line-height:25.875px">  
    74. <li style="border:0px; font-family:inherit; font-style:inherit; font-weight:inherit; margin:0px 0px 10px; outline:0px; padding:0px; vertical-align:baseline">  
    75. 这 个方法传给你发生碰撞的两个物理外形(子弹和怪物),但是不能保证它们会按特定的顺序传给你。所以有一部分代码是用来把它们按各自的种类掩码进行排序的。 这样你稍后才能针对对象种类做操作。这部分的代码来源于苹果官方Adventure例子。</li><li style="border:0px; font-family:inherit; font-style:inherit; font-weight:inherit; margin:0px 0px 10px; outline:0px; padding:0px; vertical-align:baseline">  
    76. 方法的后一部分是用来检查这两个外形是否一个是子弹另一个是怪物,如果是就调用刚刚写的方法(指把它们从scene上移除的方法)。</li></ol>  
    77. <p style="border:0px; font-family:‘Open Sans‘,sans-serif; font-size:16px; margin-top:0px; margin-bottom:0px; outline:0px; padding-top:0px; padding-bottom:10px; vertical-align:baseline; line-height:25.875px">  
    78. 最后一步,在MyScene的私有声明上让其实现<code style="border:0px; font-family:‘Droid Sans Mono‘,sans-serif; font-size:14px; font-style:inherit; font-weight:bold; margin:0px; outline:0px; padding:0px; vertical-align:baseline; color:rgb(0,104,55)">SKPhysicsContactDelegate这个代理协议,这样才能编译通过。</code></p>  
    79. <p style="border:0px; font-family:‘Open Sans‘,sans-serif; font-size:16px; margin-top:0px; margin-bottom:0px; outline:0px; padding-top:0px; padding-bottom:10px; vertical-align:baseline; line-height:25.875px">  
    80. </p>  
    81. <pre code_snippet_id="172739" snippet_file_name="blog_20140127_17_11251" name="code" class="objc">@interface MyScene ()</pre><br>  
    82. 编译运行,然后子弹在碰到目标(怪物)时它们就会一起消失了!  
    83. <p></p>  
    84. <h2 style="border:0px; font-family:Bitter,serif; font-size:26px; margin:-3px 0px 0px; outline:0px; padding:5px 0px 20px; vertical-align:baseline; color:rgb(0,104,55); line-height:25.875px"><a name="t7"></a>  
    85. 即将完成</h2>  
    86. <p style="border:0px; font-family:‘Open Sans‘,sans-serif; font-size:16px; margin-top:0px; margin-bottom:0px; outline:0px; padding-top:0px; padding-bottom:10px; vertical-align:baseline; line-height:25.875px">  
    87. 你马上就要完成这个简单的游戏了。只要再添加一些音效(哪种游戏也不能没有声音啊!)和一些简单的游戏逻辑即可。<br>  
    88. Sprite Kit没有像Cocos2D一样提供声音引擎,但值得庆幸的是它可以通过动作这种简便的方式来实现。并且你可以通过同样很简单的AVFoundation类库来播放背景音乐。<br>  
    89. 你的项目里已经有一些我做的背景音乐(很酷哦)和一个给力的piu~piu~音效了。它们是从这个教程的资源包里添加的。你只要播放就好了!<br>  
    90. 为了实现这些,将下面的代码添加到Viewcontroller.m文件里:</p>  
    91. <p style="border:0px; font-family:‘Open Sans‘,sans-serif; font-size:16px; margin-top:0px; margin-bottom:0px; outline:0px; padding-top:0px; padding-bottom:10px; vertical-align:baseline; line-height:25.875px">  
    92. </p>  
    93. <pre code_snippet_id="172739" snippet_file_name="blog_20140127_18_8266363" name="code" class="objc">@import AVFoundation;</pre>  
    94. <p></p>  
    95. <p style="border:0px; font-family:‘Open Sans‘,sans-serif; font-size:16px; margin-top:0px; margin-bottom:0px; outline:0px; padding-top:0px; padding-bottom:10px; vertical-align:baseline; line-height:25.875px">  
    96. 这里使用了iOS的新特性,通过使用新的@import 关键字,你可以更简单、更高效地引入头文件(还有类库)。想要进一步了解它,请阅读我们在<a target="_blank" href="http://www.raywenderlich.com/store/ios-7-by-tutorials" style="border:0px; font-family:inherit; font-style:inherit; font-weight:inherit; margin:0px; outline:0px; padding:0px; vertical-align:baseline; color:rgb(0,104,55)">iOS  
    97.  7 by Tutorials</a>里的第10章——Objective-C 和 iOS基础类库有什么新玩意。<br>  
    98. 下一步,添加一个新的属性和一个私有声明:</p>  
    99. <p style="border:0px; font-family:‘Open Sans‘,sans-serif; font-size:16px; margin-top:0px; margin-bottom:0px; outline:0px; padding-top:0px; padding-bottom:10px; vertical-align:baseline; line-height:25.875px">  
    100. </p>  
    101. <pre code_snippet_id="172739" snippet_file_name="blog_20140127_19_9029731" name="code" class="objc">@interface ViewController ()  
    102. @property (nonatomic) AVAudioPlayer * backgroundMusicPlayer;  
    103. @end</pre><br>  
    104. 将如下代码添加到 <span style="font-family:inherit; border:0px; font-style:inherit; margin:0px; outline:0px; padding:0px; vertical-align:baseline; color:rgb(0,104,55)"><strong>viewWillLayoutSubviews方法中,添加到</strong></span> <code style="border:0px; font-family:‘Droid Sans Mono‘,sans-serif; font-size:14px; font-style:inherit; font-weight:bold; margin:0px; outline:0px; padding:0px; vertical-align:baseline; color:rgb(0,104,55)">[super  
    105.  viewWillLayoutSubviews]的后面:</code>  
    106. <p></p>  
    107. <p style="border:0px; font-family:‘Open Sans‘,sans-serif; font-size:16px; margin-top:0px; margin-bottom:0px; outline:0px; padding-top:0px; padding-bottom:10px; vertical-align:baseline; line-height:25.875px">  
    108. </p>  
    109. <pre code_snippet_id="172739" snippet_file_name="blog_20140127_20_8713303" name="code" class="objc">NSError *error;  
    110. NSURL * backgroundMusicURL = [[NSBundle mainBundle] URLForResource:@"background-music-aac" withExtension:@"caf"];  
    111. self.backgroundMusicPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:backgroundMusicURL error:&error];  
    112. self.backgroundMusicPlayer.numberOfLoops = -1;  
    113. [self.backgroundMusicPlayer prepareToPlay];  
    114. [self.backgroundMusicPlayer play];</pre>  
    115. <p></p>  
    116. <p style="border:0px; font-family:‘Open Sans‘,sans-serif; font-size:16px; margin-top:0px; margin-bottom:0px; outline:0px; padding-top:0px; padding-bottom:10px; vertical-align:baseline; line-height:25.875px">  
    117. 以上这段代码是开启无线循环的背景音乐</p>  
    118. <p style="border:0px; font-family:‘Open Sans‘,sans-serif; font-size:16px; margin-top:0px; margin-bottom:0px; outline:0px; padding-top:0px; padding-bottom:10px; vertical-align:baseline; line-height:25.875px">  
    119. 至于音效,转到<span style="font-family:‘Open Sans‘,sans-serif; font-size:16px; line-height:25.875px"> </span><span style="font-family:inherit; font-size:16px; font-style:inherit; line-height:25.875px; border:0px; margin:0px; outline:0px; padding:0px; vertical-align:baseline"><span style="color:#006837"><strong>MyScene.m</strong></span><span style="color:#666666">在</span><span style="color:rgb(0,104,55); font-family:inherit; font-size:16px; font-style:inherit; line-height:25.875px; border:0px; margin:0px; outline:0px; padding:0px; vertical-align:baseline"><strong><strong>touchesEnded:withEvent:</strong></strong></span><span style="font-family:‘Open Sans‘,sans-serif; font-size:16px; line-height:25.875px"><span style="color:#006837"><strong>:</strong></span><span style="color:#333333">函数的最上面添加如下代码</span></span></span></p>  
    120. <p style="border:0px; font-family:‘Open Sans‘,sans-serif; font-size:16px; margin-top:0px; margin-bottom:0px; outline:0px; padding-top:0px; padding-bottom:10px; vertical-align:baseline; line-height:25.875px">  
    121. <span style="font-family:inherit; font-size:16px; font-style:inherit; line-height:25.875px; border:0px; margin:0px; outline:0px; padding:0px; vertical-align:baseline"><span style="font-family:‘Open Sans‘,sans-serif; font-size:16px; line-height:25.875px"><span style="color:#333333"></span></span></span></p>  
    122. <pre code_snippet_id="172739" snippet_file_name="blog_20140127_21_2508315" name="code" class="objc">[self runAction:[SKAction playSoundFileNamed:@"pew-pew-lei.caf" waitForCompletion:NO]];</pre>  
    123. <p></p>  
    124. <p style="border:0px; font-family:‘Open Sans‘,sans-serif; font-size:16px; margin-top:0px; margin-bottom:0px; outline:0px; padding-top:0px; padding-bottom:10px; vertical-align:baseline; line-height:25.875px">  
    125. 了解?你可以仅仅用一行代码来播放一个声音!<br>  
    126. 让我们来创建一个新的scene来展现游戏输赢的结果。在新建界面中按 iOSCocoa TouchObjective-C class的方式从模版创建新类,将之命名为GameOverScene,使其继承自
    127.  然后单击下一步(Next)而后创建(Create)
    128. 然后用下面的代码替换GameOverScene.h中原有的代码:
    129. @interface GameOverScene : SKScene  
    130. -(id)initWithSize:(CGSize)size won:(BOOL)won;  
    131. @end
    132. 这里你引入了Sprite Kit的头文件并且声明了一个特殊的初始化方法,这个初始化方法除了需要传入size大小外还要传入用户的游戏结果(布尔值,表示输赢)。  
    133. #import "MyScene.h"  
    134. @implementation GameOverScene  
    135. -(id)initWithSize:(CGSize)size won:(BOOL)won {  
    136.     if (self = [super initWithSize:size]) {  
    137.    
    138.         // 1  
    139.         self.backgroundColor = [SKColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0];  
    140.    
    141.         // 2  
    142.         NSString * message;  
    143.         if (won) {  
    144.             message = @"You Won!";  
    145.         } else {  
    146.             message = @"You Lose :[";  
    147.         }  
    148.    
    149.         // 3  
    150.         SKLabelNode *label = [SKLabelNode labelNodeWithFontNamed:@"Chalkduster"];  
    151.         label.text = message;  
    152.         label.fontSize = 40;  
    153.         label.fontColor = [SKColor blackColor];  
    154.         label.position = CGPointMake(self.size.width/2, self.size.height/2);  
    155.         [self addChild:label];  
    156.    
    157.         // 4  
    158.         [self runAction:  
    159.             [SKAction sequence:@[  
    160.                 [SKAction waitForDuration:3.0],  
    161.                 [SKAction runBlock:^{  
    162.                     // 5  
    163.                     SKTransition *reveal = [SKTransition flipHorizontalWithDuration:0.5];  
    164.                     SKScene * myScene = [[MyScene alloc] initWithSize:self.size];  
    165.                     [self.view presentScene:myScene transition: reveal];  
    166.                 }]  
    167.             ]]  
    168.         ];  
    169.    
    170.     }  
    171.     return self;  
    172. }  
    173. @end
    174. 这是上述代码的解释:  
    175. 将背景颜色设置为白色,与主要的scene(MyScene)相同。
    176. 根据传入的输赢参数,设置弹出的消息字符串“You Won”或者 “You Lose”。
    177. 这部分代码表示在Sprite Kit下如何把文本标签显示到屏幕上。和你看到的一样,相当简单,只需要选择字体然后设置一些参数即可。
    178. 最后,创建并且运行一个系列类型动作,它包含两个子动作。为了让你更好的理解,我把它们都写在了一起(而没有采用以不同变量分开表示不同aciton的方式)。这里第一个action仅仅是等待3秒钟,然后会执行runBlock中的第二个action来做一些马上会执行的操作。
    179. 这里实现的是在Sprite Kit下如何转场(从现有场景转到新的场景)。首先你可以从多种转场特效动画中挑选一个自己喜欢的用来展示,这里选了一个0.5秒的翻转特效。然后创建即将要被显示的scene,使用self.view的
    180. 到现在为止一切顺利,你只要在游戏结束时用你的主场景(MyScene)来加载游戏结束的场景(GameOverScene)就好了。<br>  
    181. 为了实现这个功能,首先要把新的scene引入到MyScene.m文件中:</p>  
    182. 然后在addMonster方法中用下面的action替换最后一行的action:  
    183. 这里创建了一个新的“失败action”用来展示游戏结束的场景,当怪物移动到屏幕边缘时游戏就结束了。看看你是否理解了这里的代码,如果没有就翻看之前block的解释吧(指GameOverScene中的系列action)。  
    184. 还有一个很常见的问题:为什么你要在actionMoveDone 动作之前运行loseAction动作?如果你不知道为什么,那就手动改变两者的顺序试试吧。
    185. 现在你应该相应也了解了赢得游戏的情形,不要对玩家太残忍哦!:] 像MyScene.m的私有声明中添加一个新的属性:
    186. 然后把下面的代码添加到
    187. 编译运行之,游戏会在对应的情况下展示输赢场景了!  

一个简单的 Sprite Kit入门

原文:http://www.cnblogs.com/A--G/p/4914407.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!