作者:Ray Wenderlich

原文出处:点击打开链接

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

转自破船之家,原文:Sprite Kit Tutorial for Beginners

 
目录
Sprite Kit的优点和缺点
Sprite Kit vs Cocos2D-iPhone vs Cocos2D-X vs Unity
Hello, Sprite Kit!
横屏显示
移动怪兽
发射炮弹
碰撞检测: 概述
碰撞检测: 实现
收尾
何去何从?
 
在iOS 7中内置了一个新的Sprite Kit框架,该框架主要用来开发2D游戏。目前已经支持的内容包括:精灵、很酷的特效(例如视频、滤镜和遮罩),并且还集成了物理库等许多东西。iOS 7中附带了一个非常棒的Sprite Kit示例工程,名字叫做Adventure。不过这个示例工程稍微有点复杂,不太适合初学者。本文的目的就是做一个关于Sprite Kit使用的初级教程。
 
通过本文,你可以从头到尾的学习到如何为你的iPhone创建一个简单又有趣的2D游戏。如果你看过我们之前的教程:Simple Cocos2D game教程,你会发现非常的相似。在开始之前,请确保已经安装了最新版本的Xcode(5.X),里面支持Sprite Kit以及iOS 7。
 
Sprite Kit的优点和缺点
首先,我想指出在iOS中开发2D游戏Sprite Kit并不是唯一的选择,下面我们先来看看Sprite Kit的一些优点和缺点。
 
Sprite Kit的优点:
1、它是内置到iOS中的,因此并不需要下载额外的库或者其它一些外部依赖。并且它是由苹果开发的,所以对于它的支持和更新我们可以放心。
2、它内置的工具支持纹理和粒子。
3、它可以让你做一些其它框架很难做到的事情,例如把视频当做精灵一样处理,或者使用很酷的图形效果和遮罩。
 
Sprite Kit的缺点:
1、如果使用了Sprite Kit,那么你将被iOS生态圈所绑架,导致你无法很容易对你开发的游戏移植到Android上。
2、Sprite Kit现在还处于初始阶段,此时提供的功能还没有别的框架丰富,例如Cocos2D。最缺的东西应该是暂不支持写自定义的OpenGL代码。
 
Sprite Kit vs Cocos2D-iPhone vs Cocos2D-X vs Unity
此时,你可能在想“我该选择使用哪个2D框架呢?”这取决于你的实际情况,下面是我的一些想法:
 
1、如果你是一个初学者,并且只关注于iOS,那么就使用内置的Sprite Kit吧,它非常容易学习,并且完全可以把工作做好。
2、如果需要写自己的OpenGL代码,那么还是使用Cocos2D,或者其它框架吧,目前Sprite Kit并不支持自定义OpenGL代码。
3、如果要进行跨平台开发,那么选择Cocos2D-X或者Unity。Cocos2D-X非常出色,可以用它来构建2D游戏。Unity则更加的灵活(例如,如果有需要的话,你可以在游戏中添加一些3D效果)。
 
看到这里,如果你还想要继续了解Sprite Kit的话,请继续往下读吧。
 
Hello,Sprite Kit!
下面我们就开始利用Xcode 5内置的Sprite Kit模板来构建一个简单的Hello World工程吧。
 
启动Xcode,选择File\New\Project,接着选中iOS\Application\SpriteKit Game模板,然后单击Next:
 
输入Product Name为SpriteKitSimpleGame,Devices选择iPhone,接着单击Next:
 
选择工程保存的路径,然后点击Create。然后点击Xcode中的播放按钮来运行工程。稍等片刻,可以看到如下运行画面:
跟Cocos2D类似,Sprite Kit也是按照场景(scenes)来构建的,这相当于游戏中的”levels”和”screens”。例如,你的游戏中可能会有一个主游戏区的场景,以及一个世界地图的一个场景。
 
如果你观察一下创建好的工程,会发现SpriteKit Game模板已经创建好了一个默认的场景MyScene。现在打开MyScene.m,里面已经包含了一些代码,其中将一个lable放到屏幕中,并且添加了:当tap屏幕时,会在屏幕上新增一个旋转的飞船。
 
在本教程中,我们主要在MyScene中写代码。不过在开始写代码之前,需要进行一个小调整——让程序以横屏的方式运行。
 
横屏显示
首先,在Project Navigator中单击SpriteKitSimpleGame工程以打开target设置,选中SpriteKitSimpleGame target。然后在Deployment Info中,不要勾选Portrait,只选中Landscape和Landscape Right,如下所示:
 
编译并运行工程,会看到如下运行画面:
下面我们试着添加一个忍者(ninja)。
 
首先,下载此工程的资源文件,并将其拖拽到Xcode工程中。确保勾选上“Copy items into destination group’s folder (if needed)”和SpriteKitSimpleGame 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. // 2
  10. NSLog(@"Size: %@", NSStringFromCGSize(size));
  11. // 3
  12. self.backgroundColor = [SKColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0];
  13. // 4
  14. self.player = [SKSpriteNode spriteNodeWithImageNamed:@"player"];
  15. self.player.position = CGPointMake(100, 100);
  16. [self addChild:self.player];
  17. }
  18. return self;
  19. }
  20. @end
 
我们来看看上面的代码。
 
1.为了给player(例如忍者)声明一个私有变量,在这里创建了一个私有的interface,之后可以把这个私有变量添加到场景中。
2.在这里打印出了场景的size,至于什么原因很快你就会看到了。
3.在Sprite Kit中设置一个场景的背景色非常简单——只需要设置backgroundColor属性,在这里将其设置位白色。
4.在Sprite Kit场景中添加一个精灵同样非常简单,只需要使用spriteNodeWithImageNamed方法,并把一副图片的名称传递进去就可以创建一个精 灵。接着设置一下精灵的位置,然后调用addChild方法将该精灵添加到场景中。在代码中将忍者的位置设置为(100, 100),该位置是从屏幕的左下角到右上角计算的。
 
编译并运行,看看效果如何…
呀!屏幕是白色的,并没有看到忍者。这是为什么呢?你可能在想设计之初就是这样的,实际上这里有一个问题。
 
如果你观察一下控制台输出的内容,会看到如下内容
  1. SpriteKitSimpleGame[3139:907] Size: {320, 568}
 
可能你会认为场景的宽度是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. // Create and configure the scene.
  9. SKScene * scene = [MyScene sceneWithSize:skView.bounds.size];
  10. scene.scaleMode = SKSceneScaleModeAspectFill;
  11. // Present the scene.
  12. [skView presentScene:scene];
  13. }
 
上面的代码中利用view的边界size创建了场景。不过请注意,当viewDidLoad被调用的时候,在这之前view已经被添加到 view层次结构中了,因此它还没有响应出布局的改变。所以view的边界可能还不正确,进而在viewDidLoad中并不是开启场景的最佳时机。
 
提醒:要想了解更多相关内容,请看由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. // Create and configure the scene.
  10. SKScene * scene = [MyScene sceneWithSize:skView.bounds.size];
  11. scene.scaleMode = SKSceneScaleModeAspectFill;
  12. // Present the scene.
  13. [skView presentScene:scene];
  14. }
  15. }
 
编译并运行程序,可以看到,忍者已经显示在屏幕中了!
如上图所示,可以看到坐标系已经正确了,如果想要把忍者的位置设置为其中间靠左,那么在MyScene.m中用下面的代码来替换设置忍者位置相关的代码:
  1. self.player.position = CGPointMake(self.player.size.width/2, self.frame.size.height/2);
 
移动怪兽
接下来,我们希望在场景中添加一些怪兽,让忍者进行攻击。为了让游戏更有趣一点,希望怪兽能够移动——否则没有太大的挑战!OK,我们就在屏幕的右边,离屏的方式创建怪兽,并给怪兽设置一个动作:告诉它们往左边移动。
 
将下面这个方法添加到MyScene.m中:
  1. - (void)addMonster {
  2. // Create sprite
  3. SKSpriteNode * monster = [SKSpriteNode spriteNodeWithImageNamed:@"monster"];
  4. // Determine where to spawn the monster along the Y axis
  5. int minY = monster.size.height / 2;
  6. int maxY = self.frame.size.height - monster.size.height / 2;
  7. int rangeY = maxY - minY;
  8. int actualY = (arc4random() % rangeY) + minY;
  9. // Create the monster slightly off-screen along the right edge,
  10. // and along a random position along the Y axis as calculated above
  11. monster.position = CGPointMake(self.frame.size.width + monster.size.width/2, actualY);
  12. [self addChild:monster];
  13. // Determine speed of the monster
  14. int minDuration = 2.0;
  15. int maxDuration = 4.0;
  16. int rangeDuration = maxDuration - minDuration;
  17. int actualDuration = (arc4random() % rangeDuration) + minDuration;
  18. // Create the actions
  19. SKAction * actionMove = [SKAction moveTo:CGPointMake(-monster.size.width/2, actualY) duration:actualDuration];
  20. SKAction * actionMoveDone = [SKAction removeFromParent];
  21. [monster runAction:[SKAction sequence:@[actionMove, actionMoveDone]]];
  22. }
 
在上面,我尽量让代码看起来容易理解。首先是通过一个简单的计算,确定怪兽出现的位置,并将该位置设置给怪兽,然后将其添加到场景中。
 
接着是添加动作(actions)。跟Cocos2D一样,Sprite Kit同样提供了很多方便的内置动作,例如移动动作、旋转动作、淡入淡出动作、动画动作等。在这里我们只需要在怪兽上使用3中动作即可:
moveTo:duration:使用这个动作可以把怪兽从屏幕外边移动到左边。移动过程中,我们可以指定移动持续的时间,上面的代码中,指定为2-4秒之间的一个随机数。
removeFromParent:在Sprite Kit中,可以使用该方法,方便的将某个node从parent中移除,能有效的从场景中删除某个对象。此处,将不再需要显示的怪兽从场景中移除。这个功能非常的重要,否则当有源源不断的怪兽出现在场景中时,会耗尽设备的所有资源。
sequence:sequence动作可以一次性就把一系列动作串联起来按照一定顺序执行。通过该方法我们就能让moveTo:方法先执行,当完成之后,在执行removeFromParent:动作。
 
最后,我们需要做的事情就是调用上面这个方法addMonster,以实际的创建出怪兽!为了更加好玩,下面我们来让怪兽随着时间持续的出现在屏幕中。
 
在Sprite Kit中,并不能像Cocos2D一样,可以配置每隔X秒就回调一下update方法。同样也不支持将从上次更新到目前为止的时间差传入方法中。(非常令人吃惊!)。
 
不过,我们可以通过一小段代码来仿造这种行为。首先在MyScene.m的private interface中添加如下属性:
  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. }
 
上面的代码中简单的将上次更新(update调用)的时间追加到self.lastSpawnTimeInterval中。一旦该时间大于1秒,就在场景中新增一个怪兽,并将lastSpawnTimeInterval重置。
 
最后,添加如下方法来调用上面的方法:
  1. - (void)update:(NSTimeInterval)currentTime {
  2. // Handle time delta.
  3. // If we drop below 60fps, we still want everything to move the same distance.
  4. CFTimeInterval timeSinceLast = currentTime - self.lastUpdateTimeInterval;
  5. self.lastUpdateTimeInterval = currentTime;
  6. if (timeSinceLast > 1) { // more than a second since last update
  7. timeSinceLast = 1.0 / 60.0;
  8. self.lastUpdateTimeInterval = currentTime;
  9. }
  10. [self updateWithTimeSinceLastUpdate:timeSinceLast];
  11. }
Sprite Kit在显示每帧时都会调用上面的update:方法。
 
上面的代码其实是来自苹果提供的Adventure示例中。该方法会传入当前的时间,在其中,会做一些计算,以确定出上一帧更新的时间。注意,在代码中做了一些合理性的检查,以避免从上一帧更新到现在已经过去了大量时间,并且将间隔重置为1/60秒,避免出现奇怪的行为。
 
现在编译并运行程序,可以看到许多怪兽从左边移动到屏幕右边并消失。
发射炮弹
现在我们开始给忍者添加一些动作,首先从发射炮弹开始!实际上有多种方法来实现炮弹的发射,不过,在这里要实现的方法时当用户tap屏幕时,从忍者的方位到tap的方位发射一颗炮弹。
 
由于本文是针对初级开发者,所以在这里我使用moveTo:动作来实现,不过这需要做一点点的数学运算——因为moveTo:方法需要指定炮弹 的目的地,但是又不能直接使用touch point(因为touch point仅仅代表需要发射的方向)。实际上我们需要让炮弹穿过touch point,直到炮弹在屏幕中消失。
 
如下图,演示了上面的相关内容:
如图所示,我们可以通过origin point到touch point得到一个小的三角形。我们要做的就是根据这个小三角形的比例创建出一个大的三角形——而你知道你想要的一个端点是离开屏幕的地方。
 
为了做这个计算,如果有一些基本的矢量方法可供调用(例如矢量的加减法),那么会非常有帮助,但很不幸的时Sprite Kit并没有提供相关方法,所以,我们必须自己实现。
 
不过很幸运的时这非常容易实现。将下面的方法添加到文件的顶部(implementation之前):
  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. // Makes a vector have a length of 1
  14. static inline CGPoint rwNormalize(CGPoint a) {
  15. float length = rwLength(a);
  16. return CGPointMake(a.x / length, a.y / length);
  17. }
 
上面实现了一些标准的矢量函数。如果你看得不是太明白,请看这里关于矢量方法的解释。
 
接着,在文件中添加一个新的方法:
  1. -(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
  2. // 1 - Choose one of the touches to work with
  3. UITouch * touch = [touches anyObject];
  4. CGPoint location = [touch locationInNode:self];
  5. // 2 - Set up initial location of projectile
  6. SKSpriteNode * projectile = [SKSpriteNode spriteNodeWithImageNamed:@"projectile"];
  7. projectile.position = self.player.position;
  8. // 3- Determine offset of location to projectile
  9. CGPoint offset = rwSub(location, projectile.position);
  10. // 4 - Bail out if you are shooting down or backwards
  11. if (offset.x <= 0) return;
  12. // 5 - OK to add now - we've double checked position
  13. [self addChild:projectile];
  14. // 6 - Get the direction of where to shoot
  15. CGPoint direction = rwNormalize(offset);
  16. // 7 - Make it shoot far enough to be guaranteed off screen
  17. CGPoint shootAmount = rwMult(direction, 1000);
  18. // 8 - Add the shoot amount to the current position
  19. CGPoint realDest = rwAdd(shootAmount, projectile.position);
  20. // 9 - Create the actions
  21. float velocity = 480.0/1.0;
  22. float realMoveDuration = self.size.width / velocity;
  23. SKAction * actionMove = [SKAction moveTo:realDest duration:realMoveDuration];
  24. SKAction * actionMoveDone = [SKAction removeFromParent];
  25. [projectile runAction:[SKAction sequence:@[actionMove, actionMoveDone]]];
  26. }
上面的代码中做了很多事情,我们来详细看看。
 
1.SpriteKit为我们做了很棒的一件事情就是它提供了一个UITouch的category,该category中有 locationInNode:和previousLocationInNode:方法。这两个方法可以帮助我们定位到在SKNode内部坐标系中 touch的坐标位置。这样一来,我们就可以寻得到在场景坐标系中touch的位置。
 
2.然后创建一个炮弹,并将其放置到忍者的地方,以当做其开始位置。注意,现在还没有将其添加到场景中,因为还需要先做一个合理性的检查——该游戏不允许忍者向后发射。
 
3.接着利用touch位置减去炮弹的当前位置,这样就能获得一个从当前位置到touch位置的矢量。
 
4.如果X值小于0,就意味着忍者将要向后发射,由于在这里的游戏中是不允许的(真实中的忍者是不回头的!),所以就return。
 
5.否则,将可以将炮弹添加到场景中。
 
6.调用方法rwNormalize,将offset转换为一个单位矢量(长度为1)。这样做可以让在相同方向上,根据确定的长度来构建一个矢量更加容易(因为1 * length = length)。
 
7.在单位矢量的方向上乘以1000。为什么是1000呢?因为着肯定足够超过屏幕边缘了 。
 
8.将上一步中计算得到的位置与炮弹的位置相加,以获得炮弹最终结束的位置。
 
9.最后,参照之前构建怪物时的方法,创建moveTo:和removeFromParent:两个actions。
 
编译并允许程序,现在忍者可以发射炮弹了!
 
碰撞检测和物理特性: 概述
至此我们已经可以让炮弹任意的发射了——现在我们要让忍者利用炮弹来消灭这些怪物。下面就添加一些代码来给炮弹与怪物相交做检测。
 
Sprite Kit内置了一个物理引擎,这非常的棒!该物理引擎不仅可以模拟现实运动,还能进行碰撞检测。
 
下面我们就在游戏中使用Sprite Kit的物理引擎来检测炮弹与怪物的碰撞。首先,我们来看看需要做些神马事情:
1.物理世界的配置。物理世界是一个模拟的空间,用来进行物理计算。默认情况下,在场景(scene)中已经创建好了一个,我们可以对其做一些属性配置,例如重力感应。
 
2.为精灵(sprite)创建对应的物体(physics bodies)。在Sprite Kit中,为了碰撞检测,我们可以为每个精灵创建一个相应的形状,并设置一些属性,这就称为物体(physics body)。注意:图文的形状不一定跟精灵的外形一模一样。一般情况,这个形状都是简单的、大概的(而不用精确到像素级别)——毕竟这已经足以够大多数游 戏使用了。
 
3.将精灵分类。在物体(physics body)上可以设置的一个属性是category,该属性是一个位掩码(bitmask)。通过该属性可以将精灵分类。在本文的游戏中,有两个类别—— 一类是炮弹,另一类则是怪物。设置之后,当两种物体相互碰撞时,就可以很容易的通过类别对精灵做出相应的处理。
 
4..设 置一个contact(触点) delegate。还记得上面提到的物理世界吗?我们可以在物理世界上设置一个contact delegate,通过该delegate,当两个物体碰撞时,可以收到通知。收到通知后,我们可以通过代码检查物体的类别,如果是怪物和炮弹,那么就做 出相应的动作!
 
上面大致介绍了一下游戏策略,下面就来看看如何实现!
 
碰撞检测和物理特性: 实现
首先在MyScene.m文件顶部添加如下两个常量:
  1. static const uint32_t projectileCategory     =  0x1 << 0;
  2. static const uint32_t monsterCategory        =  0x1 << 1;
 
上面设置了两个类别,记住需要用位(bit)的方式表达——一个用于炮弹,另一个则是怪物。
 
注意:看到上面的语法你可能感到奇怪。在Sprite Kit中category是一个32位整数,当做一个位掩码(bitmask)。这种表达方法比较奇特:在一个32位整数中的每一位表示一种类别(因此最 多也就只能有32类)。在这里,第一位表示炮弹,下一位表示怪兽。
 
接着,在initWithSize中,将下面的代码添加到位置:添加player到场景涉及代码的后面。
  1. self.physicsWorld.gravity = CGVectorMake(0,0);
  2. self.physicsWorld.contactDelegate = self;
 
上面的代码将物理世界的重力感应设置为0,并将场景设置位物理世界的代理(当有两个物体碰撞时,会受到通知)。
 
在addMonster方法中,将如下代码添加创建怪兽相关代码后面:
  1. monster.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:monster.size]; // 1
  2. monster.physicsBody.dynamic = YES; // 2
  3. monster.physicsBody.categoryBitMask = monsterCategory; // 3
  4. monster.physicsBody.contactTestBitMask = projectileCategory; // 4
  5. monster.physicsBody.collisionBitMask = 0; // 5
 
来看看上面代码意思:
为怪兽创建一个对应的物体。此处,物体被定义为一个与怪兽相同尺寸的矩形(这样与怪兽形状比较接近)。
 
将怪兽设置位dynamic。这意味着物理引擎将不再控制这个怪兽的运动——我们自己已经写好相关运动的代码了。
 
将categoryBitMask设置为之前定义好的monsterCategory。
 
contactTestBitMask表示与什么类型对象碰撞时,应该通知contact代理。在这里选择炮弹类型。
 
collisionBitMask表示物理引擎需要处理的碰撞事件。在此处我们不希望炮弹和怪物被相互弹开——所以再次将其设置为0。
 
接着在touchesEnded:withEvent:方法中设置炮弹位置的代码后面添加如下代码。
  1. projectile.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:projectile.size.width/2];
  2. projectile.physicsBody.dynamic = YES;
  3. projectile.physicsBody.categoryBitMask = projectileCategory;
  4. projectile.physicsBody.contactTestBitMask = monsterCategory;
  5. projectile.physicsBody.collisionBitMask = 0;
  6. projectile.physicsBody.usesPreciseCollisionDetection = YES;
 
在 上面的代码中跟之前的类似,只不过有些不同,我们来看看: 1. 为了更好的效果,炮弹的形状是圆形的。 2. usesPreciseCollisionDetection属性设置为YES。这对于快速移动的物体非常重要(例如炮弹),如果不这样设置的话,有可能 快速移动的两个物体会直接相互穿过去,而不会检测到碰撞的发生。
 
接着,添加如下方法,当炮弹与怪物发生碰撞时,会被调用。注意这个方法是不会被自动调用,稍后会看到我们如何调用它。
  1. - (void)projectile:(SKSpriteNode *)projectile didCollideWithMonster:(SKSpriteNode *)monster {
  2. NSLog(@"Hit");
  3. [projectile removeFromParent];
  4. [monster removeFromParent];
  5. }
 
当怪物和炮弹发生碰撞,上面的代码会将他们从场景中移除。很简单吧!
 
下面该实现contact delegate方法了。将如下方法添加到文件中:
  1. - (void)didBeginContact:(SKPhysicsContact *)contact
  2. {
  3. // 1
  4. SKPhysicsBody *firstBody, *secondBody;
  5. if (contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask)
  6. {
  7. firstBody = contact.bodyA;
  8. secondBody = contact.bodyB;
  9. }
  10. else
  11. {
  12. firstBody = contact.bodyB;
  13. secondBody = contact.bodyA;
  14. }
  15. // 2
  16. if ((firstBody.categoryBitMask & projectileCategory) != 0 &&
  17. (secondBody.categoryBitMask & monsterCategory) != 0)
  18. {
  19. [self projectile:(SKSpriteNode *) firstBody.node didCollideWithMonster:(SKSpriteNode *) secondBody.node];
  20. }
  21. }
 
还记得之前给物理世界设置的contactDelegate吗?当两个物体发生碰撞之后,就会调用上面的方法。
 
在上面的方法中,可以分为两部分来理解:
该方法会传递给你发生碰撞的两个物体,但是并不一定符合特定的顺序(如炮弹在前,或者炮弹在后)。所以这里的代码是通过物体的category bit mask来对其进行排序,以便后续做出正确的判断。注意,这里的代码来自苹果提供的Adventure示例。
 
最后,检测一下这两个碰撞的物体是否就是炮弹和怪物,如果是的话就调用之前的方法。
 
最后一步,为了编译器没有警告,确保private interface 中添加一下SKPhysicsContactDelegate:
  1. @interface MyScene () <SKPhysicsContactDelegate>
现在编译并运行程序,可以发现,当炮弹与怪物接触时,他们就会消失!
 
收尾
现在,本文的游戏快完成了。接下来我们就来为游戏添加音效和音乐,以及一些简单的游戏逻辑吧。
 
苹果提供的Sprite Kit里面并没有音频引擎(Cocos2D中是有的),不过我们可以通过action来播放音效,并且可以使用AVFoundation播放后台音乐。
 
在工程中我已经准备好了一些音效和很酷的后台音乐,在本文开头已经将resources添加到工程中了,现在只需要播放它们即可!
 
首先在ViewController.m文件顶部添加如下import:
  1. @import AVFoundation;
上 面的语法是iOS 7中新的modules功能 —— 只需要使用新的关键字@import,就可以框架的头文件和库文件添加到工程中,这功能非常方便。要了解更多相关内容,请看到iOS 7 by Tutorials中的第十章内容中的:What’s New with Objective-C and Foundation。
 
接着添加一个新的属性和private interface:
  1. @interface ViewController ()
  2. @property (nonatomic) AVAudioPlayer * backgroundMusicPlayer;
  3. @end
 
接着将下面的代码添加到viewWillLayoutSubviews方法中(在[super viewWillLayoutSubviews]后面):
  1. NSError *error;
  2. NSURL * backgroundMusicURL = [[NSBundle mainBundle] URLForResource:@"background-music-aac" withExtension:@"caf"];
  3. self.backgroundMusicPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:backgroundMusicURL error:&error];
  4. self.backgroundMusicPlayer.numberOfLoops = -1;
  5. [self.backgroundMusicPlayer prepareToPlay];
  6. [self.backgroundMusicPlayer play];
 
上面的代码会开始无限循环的播放后台音乐。
 
下面我们来看看如何处理音效。切换到MyScene.m文件中,并将下面这行代码添加到touchesEnded:withEvent:方法的顶部:
  1. [self runAction:[SKAction playSoundFileNamed:@"pew-pew-lei.caf" waitForCompletion:NO]];
如上,一行代码就可以播放音效了,很简单吧!
 
下面,我们创建一个新的创建和layer,用来显示你赢了(You Win)或你输了(You Lose)。用模板iOS\Cocoa Touch\Objective-C class创建一个新的文件,将其命名为GameOverScene,并让其继承自SKScene,然后点击Next和Create。
 
接着用如下代码替换GameOverScene.h中的内容:
  1. #import <SpriteKit/SpriteKit.h>
  2. @interface GameOverScene : SKScene
  3. -(id)initWithSize:(CGSize)size won:(BOOL)won;
  4. @end
 
在上面的代码中导入了Sprite Kit头文件,并声明了一个特定的初始化方法,该方法的第一个参数用来定位显示的位置,第二个参数won用来判断用户是否赢了。
 
接着用下面的代码替换GameOverLayer.m中的内容:
  1. #import "GameOverScene.h"
  2. #import "MyScene.h"
  3. @implementation GameOverScene
  4. -(id)initWithSize:(CGSize)size won:(BOOL)won {
  5. if (self = [super initWithSize:size]) {
  6. // 1
  7. self.backgroundColor = [SKColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0];
  8. // 2
  9. NSString * message;
  10. if (won) {
  11. message = @"You Won!";
  12. } else {
  13. message = @"You Lose :[";
  14. }
  15. // 3
  16. SKLabelNode *label = [SKLabelNode labelNodeWithFontNamed:@"Chalkduster"];
  17. label.text = message;
  18. label.fontSize = 40;
  19. label.fontColor = [SKColor blackColor];
  20. label.position = CGPointMake(self.size.width/2, self.size.height/2);
  21. [self addChild:label];
  22. // 4
  23. [self runAction:
  24. [SKAction sequence:@[
  25. [SKAction waitForDuration:3.0],
  26. [SKAction runBlock:^{
  27. // 5
  28. SKTransition *reveal = [SKTransition flipHorizontalWithDuration:0.5];
  29. SKScene * myScene = [[MyScene alloc] initWithSize:self.size];
  30. [self.view presentScene:myScene transition: reveal];
  31. }]
  32. ]]
  33. ];
  34. }
  35. return self;
  36. }
  37. @end
 
上面的代码可以分为4部分内容,我们来分别看看:
 
将背景色设置为白色(与主场景一样颜色)。
 
根据won参数,将信息设置为”You Won”或”You Lose”。
 
这里的代码是利用Sprite Kit将一个文本标签显示到屏幕中。如代码所示,只需要选择一个字体,并设置少量的参数即可,也非常简单。
 
设置并运行有个有两个action的sequence。为了看起来方便,此处我将它们放到一块(而不是为每个action创建单独的一个变量)。首先是等待3秒,然后是利用runBlockaction来运行一些代码。
 
演示了在Sprite Kit中如何过渡到新的场景。首先可以选择任意的一种不同的动画过渡效果,用于场景的显示,在这里选择了翻转效果(持续0.5秒)。然后是创建一个想要显 示的场景,接着使用self.view的方法presentScene:transition:来显示出场景。
 
OK,万事俱备,只欠东风了!现在只需要在主场景中,适当的情况下加载game over scene就可以了。
 
首先,在MyScene.m中导入新的场景:
  1. #import "GameOverScene.h"
 
然后,在addMonster中,用下面的代码替换最后一行在怪物上运行action的代码:
  1. SKAction * loseAction = [SKAction runBlock:^{
  2. SKTransition *reveal = [SKTransition flipHorizontalWithDuration:0.5];
  3. SKScene * gameOverScene = [[GameOverScene alloc] initWithSize:self.size won:NO];
  4. [self.view presentScene:gameOverScene transition: reveal];
  5. }];
  6. [monster runAction:[SKAction sequence:@[actionMove, loseAction, actionMoveDone]]];
 
上面创建了一个”lose action”,当怪物离开屏幕时,显示game over场景。
 
在这里为什么loseAction要在actionMoveDone之前运行呢? 原因在于如果将一个精灵从场景中移除了,那么它就不在处于场景的层次结构中了,也就不会有action了。所以需要过渡到lose场景之后,才能将精灵移 除。不过,实际上actionMoveDone永远都不会被调用——因为此时已经过渡到新的场景中了,留在这里就是为了达到教学的目的。
 
现在,需要处理一下赢了的情况。在private interface中添加一个新的属性:
  1. @property (nonatomic) int monstersDestroyed;
 
然后将如下代码添加到projectile:didCollideWithMonster:的底部:
  1. self.monstersDestroyed++;
  2. if (self.monstersDestroyed > 30) {
  3. SKTransition *reveal = [SKTransition flipHorizontalWithDuration:0.5];
  4. SKScene * gameOverScene = [[GameOverScene alloc] initWithSize:self.size won:YES];
  5. [self.view presentScene:gameOverScene transition: reveal];
  6. }
 
编译并运行程序,尝试一下赢了和输了会看到的画面!
 
何去何从?
至此Sprite Kit教程:初学者结束!这里可以下到完整的代码
 
希望本文能帮助你学习Sprite Kit,并写出你自己的游戏!
 
如果你希望学习更多相关Sprite Kit内容,可以看看这本书:iOS Games by Tutorials。本书会告诉你需要知道的内容——从物理,到tile map,以及特定的系统,甚至是制作自己的关卡编辑器。

Sprite Kit教程:初学者的更多相关文章

  1. ios游戏开发 Sprite Kit教程:初学者 1

    注:本文译自Sprite Kit Tutorial for Beginners 目录 Sprite Kit的优点和缺点 Sprite Kit vs Cocos2D-iPhone vs Cocos2D- ...

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  9. iOS Sprite Kit教程之编敲代码以及Xcode的介绍

    iOS Sprite Kit教程之编敲代码以及Xcode的介绍 Xcode界面介绍 一个Xcode项目由非常多的文件组成,比如代码文件.资源文件等.Xcode会帮助开发人员对这些文件进行管理.所以,X ...

随机推荐

  1. 14.6.6 Configuring Thread Concurrency for InnoDB 配置线程并发

    14.6.6 Configuring Thread Concurrency for InnoDB 配置线程并发 InnoDB 使用操作系统线程来处理请求(用户事务) 事务可能执行很多次在它们提交或者回 ...

  2. bzoj2734

    非常巧妙地题目对于一个数x列出这样的矩阵x 2x 4x 8x ……3x 6x 12x 24x ………………………………不难方案数就是求取数不相邻的方案数考虑矩阵宽不超过logn,所以可以用状压dp解决 ...

  3. BZOJ1602: [Usaco2008 Oct]牧场行走

    1602: [Usaco2008 Oct]牧场行走 Time Limit: 5 Sec  Memory Limit: 64 MBSubmit: 1084  Solved: 556[Submit][St ...

  4. Welcome Back to C++ (Modern C++)

    http://msdn.microsoft.com/en-us/library/hh279654.aspx 看看你是古代还是现代Cpper重点强调 Stack-based scope instead ...

  5. myeclipse如何修改Web项目名称

    1. 使用myeclipse进行Web项目开发时,有时候项目名称重命名了,但是WEB项目在tomcat里边的名称还是原来的名称,是需要重新命名的.下边详细介绍一下如果和修改web项目名称,myecli ...

  6. HDOJ/HDU 2710 Max Factor(素数快速筛选~)

    Problem Description To improve the organization of his farm, Farmer John labels each of his N (1 < ...

  7. bzoj 3156 防御准备(斜率DP)

    3156: 防御准备 Time Limit: 10 Sec  Memory Limit: 512 MBSubmit: 837  Solved: 395[Submit][Status][Discuss] ...

  8. ContentProvider初阶Cookbook

    在Android世界里,ContentProvider将数据存储抽象成了类似SQL的形式,通过insert, delete, update, query等接口实现对数据的增删改查.通过ContentP ...

  9. socket programming Max size of tcp/ip socket Buffer?

    TCP data is buffered at both sender and receiver. The size of the receiver's socket receive buffer d ...

  10. Java反射获取类和对象信息全解析

    反射可以解决在编译时无法预知对象和类是属于那个类的,要根据程序运行时的信息才能知道该对象和类的信息的问题. 在两个人协作开发时,你只要知道对方的类名就可以进行初步的开发了. 获取类对象 Class.f ...