如何使用Sprite Kit制作像切绳子这样的游戏


原文地址:http://www.raywenderlich.com/72146/create-game-like-cut-rope-using-sprite-kit
泰然翻译组:志创大侠。校对:glory。

在ios7之前,如果你想要去做一个游戏,需要知道一些关于OPENGL这种深奥难懂的知识,或者要借助第三方的库做一些复杂困难的工作。当你有了一个图像引擎,你往往需要加上一个物理层,并且加上一个额外的库去模拟真实世界的行为(比如跑,跳)。这样经常会导致一些附属事物的增加,而且会要求程序员掌握其它的程序语言(比如C或者C++).

在采用IOS7的时候,苹果公司也彻底改变了这一局面。就在那个时候,开发者们一下子就得到了2D图像引擎和物理引擎的接口,所有的一切都可以使用Objective-C来操作。开发者们可以集中精力去做一个游戏而不是纠结于管理整个架构体系。

这个游戏框架就是SpriteKit。在这个教程中,你将会学习制作一个和切绳子差不多的游戏(切绳子是一个备受赞誉的基于物理规则的解密游戏)。你将会学到的有:

  • 在场景中加入对象

  • 制作序列动画

  • 加入音乐

  • 最终要的是学会如何在Sprite Kit的物理系统里让游戏运转起来

在教程的最后,你应该就能在自己的项目中很熟练地使用Sprite Kit了。

要清楚的是这篇教程不是入门级的,如果你连SKNodeSKAction都不是很了解,那么出门右拐,看一下我们的Sprite Kit基础教程吧。该教程能使你快速地学会用Sprite Kit制作游戏,然后你就可以运行游戏,给鳄鱼喂菠萝了,哈哈!

然后呢?继续看吧

开始

在这个教程,你将会做一个叫《Cut the Verlet》的游戏。这个游戏模仿的是切绳子的游戏设定,就是让你切掉一根绑着一颗糖的绳子,让糖掉到一只饥饿而且非常烦躁的生物的嘴中。每一关你都会遇到一些新的挑战,比如会有蜘蛛和锯片。这个游戏在Cocos2D框架下也被实现过,但是在这个教程中,你用的就是Sprite Kit了。

那什么是Verlet呢?verletverlet integration的缩写。而verlet integration是一种用来模拟运动粒子轨道和绳子物理特性的方法。写这个游戏的Cocos2D版本的作者Gustavo Ambrozio对verlet进行了很详细的介绍并且说明了把这个方法应用到游戏当中的思路。在读这篇教程前先浏览一下链接中的文章,还是很有必要读的。

首先,先下载一下启动项目。把它放在合适的地方然后用XCode打开看一下它的构成。

项目包括4个主文件,如下所示:

  • Classes包括一些主文件,比如主视图的控制器,场景以及绳子对象的类。你将会在阅读教程的过程中在里面加入这些类。

  • Helpers包括一些储存游戏数据的文件,它的数据将贯穿这个游戏程序。

  • Resources/Sounds包含了项目的声音文件。

  • Resources/Other包含了一些用于在场景中加入粒子的文件。

  • Other Resources包含了从第三方得到的所有资源。这篇教程将会使用一个iOS Game by Tutorials制作的插件,叫SKUtils。

  • Images.xcassets包含了图片资源。

另外,我已经加入了所有必要的#import语句到启动项目。这些#import语句都被包含在 CutTheVerlet-Prefix.pch中。

注释:A.pch就是预先编译好的头文件。预编译头文件是用来加快文件编译速度的文件。他们使在你的项目中被使用的那些文件不需要被导入。只要记住预编译头文件能隐藏在类中使用到的附属物。

关掉ResourcesOther Resources ,你不需要在这些地方进行修改的。你会在ClassesHelpers中直接使用到这些文件。

终于要开始了

加一些常数

常数就是那些一旦被设定了值就永远不会改变的数。常数能让你的代码更容易管理。全局常数有时候还能让你的代码更易读更易维护。

在这个项目中,你将会加入一些全局常量,用来定义精灵的纹理图片名称,音效文件名称,精灵节点名称,精灵的Z-order或者zPosition以及为每一个精灵定义的目录,这些目录是用来做碰撞检测的。

打开TLC ShareConstains.h并且加入如下代码在@interface上面:

typedef NS_ENUM(int, Layer)

{

    LayerBackground,

    LayerForeground,

    LayerCrocodile,

    LayerRope,

    LayerPrize

};

 

typedef NS_OPTIONS(int, EntityCategory)

{

    EntityCategoryCrocodile = 1 << 0,

    EntityCategoryRopeAttachment = 1 << 1,

    EntityCategoryRope = 1 << 2,

    EntityCategoryPrize = 1 << 3,

    EntityCategoryGround = 1 << 4

};

 

extern NSString *const kImageNameForRopeHolder;

extern NSString *const kImageNameForRopeTexture;

 

extern NSString *const kImageNameForCrocodileBaseImage;

extern NSString *const kImageNameForCrocodileMouthOpen;

extern NSString *const kImageNameForCrocodileMouthClosed;

 

extern NSString *const kSoundFileNameForCutAction;

extern NSString *const kSoundFileNameForSplashAction;

extern NSString *const kSoundFileNameForBiteAction;

 

extern NSString *const kSoundFileNameForBackgroundMusic;

 

extern NSString *const kImageNameForPrize;

extern NSString *const kNodeNameForPrize;

上面的代码声明了两个类型分别为Layerint:EntityCategorytypedef变量。当你把它们加入到场景中时,你将会使用它们去定义碰撞目录和精灵的zPosition。当然,它们的作用不只于此。

代码又用const定义了一组类型为NSString的常量。写上extern是为了让你去做一些清楚的声明,就是让你在这声明了变量并且能在任何地方设置变量的值。

问题:为什么程序猿总是在声明常量的时候在常量名上加上'k'这个前缀

对于我们为什么用k有着一个小争论,但是普遍上的解释是它起源于Hungarian notation,那时候k指代常量。或者是说k是指konstant?

什么叫在这声明一个常量并且在其他地方设定常量的值?其实在这个项目中,其他地方 就是指TLC SharedConstants.m文件。

打开TLC ShareConstaints.m并且加上在@implementation上面如下代码

NSString *const kImageNameForRopeHolder = @"ropeHolder";

NSString *const kImageNameForRopeTexture = @"ropeTexture";

 

NSString *const kImageNameForCrocodileBaseImage = @"croc";

NSString *const kImageNameForCrocodileMouthOpen = @"croc01";

NSString *const kImageNameForCrocodileMouthClosed = @"croc00";

 

NSString *const kSoundFileNameForCutAction = @"cut.caf";

NSString *const kSoundFileNameForSplashAction = @"splash.caf";

NSString *const kSoundFileNameForBiteAction = @"bite.caf";

 

NSString *const kSoundFileNameForBackgroundMusic = @"CheeZeeJungle.caf";

 

NSString *const kImageNameForPrize = @"pineapple";

NSString *const kNodeNameForPrize = @"pineapple";

你将会在这里使用字符串型的常量去设定图片和声音文件的名字。如果你玩过《切绳子》游戏,你大概就能猜出这些名字代表着什么。你也可以把用于碰撞检测的精灵节点的名字设在字符串常量里。碰撞检测在本文的碰撞检测章节中会有介绍。

注解:一开始就加入这么多东西可能有些草率,让你不知所措。但是你最好先把这些定义好,因为你将会在阅读教程的时候使用它们并且了解它们。

加入背景和前景

刚建立好的新项目提供了项目方法的初始版本,你需要在这个基础上加入自己的代码。第一步要做的就是初始化场景和加入背景。

打开TLCMyScene.m并在界面声明中加入如下代码,就是一些properties (接口),下面的properties就都先叫做接口吧。


@property (nonatomic, strong) SKNode *worldNode;

@property (nonatomic, strong) SKSpriteNode *background;

@property (nonatomic, strong) SKSpriteNode *ground;

 

@property (nonatomic, strong) SKSpriteNode *crocodile;

 

@property (nonatomic, strong) SKSpriteNode *treeLeft;

@property (nonatomic, strong) SKSpriteNode *treeRight;

你在这里定义properties是为了在场景中引用不同的节点。

现在在initWithSize中加入代码块,就是在注释/ add setup here /下面。

self.worldNode = [SKNode node];

[self addChild:self.worldNode];

 

[self setupBackground];

[self setupTrees];

上面的代码创建了一个SKNode的对象并且将该对象赋给了world接口。然后又用了addChild函数把这个节点加入到场景中。

这里有调用了两种方法,一种是为了建立背景的,另一种是为了建立树。因为这两种方法是差不多的,所以当你加入它们的时候,我会把它们放一起解释。

首先,找到setupBackground并加入如下代码:

self.background = [SKSpriteNode spriteNodeWithImageNamed:@"background"];

self.background.anchorPoint = CGPointMake(0.5, 1);

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

self.background.zPosition = LayerBackground;

 

[self.worldNode addChild:self.background];

 

self.ground = [SKSpriteNode spriteNodeWithImageNamed:@"ground"];

self.ground.anchorPoint = CGPointMake(0.5, 1);

self.ground.position = CGPointMake(self.size.width/2, self.background.frame.origin.y);

self.ground.zPosition = LayerBackground;

 

[self.worldNode addChild:self.ground];

 

SKSpriteNode *water = [SKSpriteNode spriteNodeWithImageNamed:@"water"];

water.anchorPoint = CGPointMake(0.5, 1);

water.position = CGPointMake(self.size.width/2, self.ground.frame.origin.y + 10);

water.zPosition = LayerBackground;

 

[self.worldNode addChild:water];

然后找到setupTrees并且加入下面的代码:


self.treeLeft = [SKSpriteNode spriteNodeWithImageNamed:@"treeLeft"];

self.treeLeft.anchorPoint = CGPointMake(0.5, 1);

self.treeLeft.position = CGPointMake(self.size.width * .20, self.size.height);

self.treeLeft.zPosition = LayerForeground;

 

[self.worldNode addChild:self.treeLeft];

 

self.treeRight = [SKSpriteNode spriteNodeWithImageNamed:@"treeRight"];

self.treeRight.anchorPoint = CGPointMake(0.5, 1);

self.treeRight.position = CGPointMake(self.size.width * .86, self.size.height);

self.treeRight.zPosition = LayerForeground;

 

[self.worldNode addChild:self.treeRight];

现在所有的事情都已就位,我要解释一下了。

setupBackgroundsetupTrees 中,你创建了一个SKSpriteNode 并且使用 spriteNodeWithImageNamed 方法把它初始化了,从方法的名字就可以看出,是通过图片的名字来初始化节点。

然后你就把每一个锚点从默认值(0.5,0.5)改到(0.5,1)。

注释:想要了解关于这个集成系统的更多信息,请查阅苹果的Sprite Kit 程序指导中的Woring with Sprites章节。

你也需要设置精灵的postion(位置)和zPosition(深度)。很多时候,当你要设置精灵的位置时,你仅仅需要设置屏幕的宽度和高度到合适的值。

然而ground精灵必须要被直接放到背景的底端。你可以通过使用self.background.frame.origin.y获得背景框的大小数据的方法来实现这个目的。

同样的,当你想要让water这个精灵处在 ground 下面, water 的下端和 ground 重合,上端和 ground 有一定距离,具体看下图。你可以使用 self。ground.frame.orign.y+10 来实现。哦, water 这个精灵是没有使用预先声明的变量的。

回到TLCSharedConstants.h,那里你定义了一些用于精灵的 zPosition 的常量。你在代码中使用了它们当中的两个量,就是 LayerBackgroundLayerForeground。由于 SKSpriteNode 是继承于SKNode的,所以你可以使用 SKNode 的所有接口,包括 zPosition

最后,你就把所有刚创建的精灵加入到你的世界节点中。

然而精灵是可以直接加入到场景的,增加一个世界节点来包含这些精灵能使项目的长期开发地更顺利,尤其是当你要在游戏世界中加入物理特征的时候。

你的第一次编译和运行可是有官方的支持的。因此,干嘛不先试试?

编译运行后,如果你做的都是对的话,你将会在屏幕中看到如下画面:

真是个漂亮的场景。我们把鳄鱼做出来吧。

在鳄鱼节点中增加动画

增加鳄鱼节点和增加背景和前景没什么区别。

TLCMyScene.m 中找到setupCrocodie,并且加入如下代码:

self.crocodile = [SKSpriteNode spriteNodeWithImageNamed:kImageNameForCrocodileMouthOpen];

self.crocodile.anchorPoint = CGPointMake(0.5, 1);

self.crocodile.position = CGPointMake(self.size.width * .75, self.background.frame.origin.y + (self.crocodile.size.height - 5));

self.crocodile.zPosition = LayerCrocodile;

 

[self.worldNode addChild:self.crocodile];

 

[self animateCrocodile];

上面的代码使用了你设定的两个常量: kImageNameForCrocodileMouthClosedLayerCrocodile 。程序使用背景节点的 frame.origin.y 获得第一个位置信息。和鳄鱼的大小相加得到鳄鱼位置的y值,x值是屏幕宽度的0.75,最后得到鳄鱼的位置。

就在之前的一个操作,你通过设置 zPosition 来使鳄鱼在背景层和前景层的最前面。默认的时候,Sprite Kit 都会根据精灵加入的顺序渲染画面。你可以通过赋予不同的 zPosition 来选择节点的深度。

现在该做动画了

找到 animateCrocodile 并加入如下代码:

NSMutableArray *textures = [NSMutableArray arrayWithCapacity:1];

 

for (int i = 0; i <= 1; i++) {

    NSString *textureName = [NSString stringWithFormat:@"%@0%d", kImageNameForCrocodileBaseImage, i];

    SKTexture *texture = [SKTexture textureWithImageNamed:textureName];

    [textures addObject:texture];

}

 

CGFloat duration = RandomFloatRange(2, 4);

 

SKAction *move = [SKAction animateWithTextures:textures timePerFrame:0.25];

SKAction *wait = [SKAction waitForDuration:duration];

SKAction *rest = [SKAction setTexture:[textures objectAtIndex:0]];

 

SKAction *animateCrocodile = [SKAction sequence:@[wait, move, wait, rest]];

[self.crocodile runAction: [SKAction repeatActionForever:animateCrocodile]];

以前的代码会创造一个叫SKTexture的数组对象。你可以在里面使用SKAction对象制作动画。

你也可以使用常量去设置一个基础的图片名称并且在你的动画里用for 这个循环的方法设置图片的编号。这里只有两张图片,分别为croc00croc01 。最终,你将使用一系列的SKAction 对象去制作鳄鱼的动画。

SKAction sequence让你设置多个动作并且依次连续地运行,就是把很多动画连接起来。

一旦你定义了这个动画序列,你就可以使用 runAction 方法去在节点上运行这个动作序列。在上面的代码中,你使用repeatActionForever: 去使这个节点重复循环的永远把动画序列运行下去。

最后就是调用setupCrocodile 来加入和运行这个鳄鱼动画。你可以在initWithSize 中做这件事。

TLCMyScene.m 的最上方,找到initWithSize ,在[self setupTrees] 后加入下面的代码。

[self setupCrocodile];

好了。你可以看到目光狰狞的鳄鱼张合它的下颚想要吃掉旁边的任何东西。

编译运行这段看这段野蛮粗暴的鳄鱼动画吧。

看上去好恐怖额。。作为玩家,你需要用菠萝去满足这只鳄鱼。当然菠萝是鳄鱼最爱吃的东西。

如果你的屏幕上的东西不是和上面的图片一模一样,那么你可能已经错过了某一步或者做错了什么。

你已经得到了这个画面并且有了玩家,那么是时候制定运动规则让游戏开始吧,也就是物理运动规则。

在你的游戏世界里增加物理原则

SpriteKit 利用的就是IOS的物理引擎包。这个引擎的底层就是BOX2D。如果你使用过CoCos-2D ,那么你可能已经使用过BOX2D 来管理你的物理层面的规则。在SpriteKit 中使用Box2D 最大的区别就是你不需要用C++去使用它,因为苹果公司已经把这个库封装在了Objective C的包装类中了。

首先,在TLCMyScene.m中找到initWithSize并在[self add child:self.worldNode]下加上三行代码:

self.worldNode.scene.physicsWorld.contactDelegate = self;

self.worldNode.scene.physicsWorld.gravity = CGVectorMake(0.0,-9.8);

self.worldNode.scene.physicsWorld.speed = 1.0;

然后在@interface末尾加上下面的代码,也就是文件上端的位置:

上面的代码设置了世界节点的碰撞代理,重力和速度。要记住,这个节点包括了你所有的其它节点,因此应该把物理设置加到这里。

这里的重力和速度的值是默认的,有各自默认的接口。这个系统是先在物体上指定了重力加速度,然后模拟确定速度。由于它们是默认的值,所以你不需要指定它们,但是知道这件事情总是好的,因为你可能会需要调节你的物理规律。

你可以在SKPhysicsWorld Class Reference中找到这些接口。

准备好绳子

到现在你应该已经知道要处理一下绳子的问题了,哦,我应该说是verlets

在这个工程中,我们使用TLCGameData 类来设定绳子。在开发环境中,你会用PLIST 或者其他数据储存文件来配置水平面。

然后,你就将创建一个含有TLCGameData 对象的数组来表示你储存数据的信息。

@property (nonatomic, assign) int name;

 

@property (nonatomic, assign) CGPoint ropeLocation;

@property (nonatomic, assign) int ropeLength;

 

@property (nonatomic, assign) BOOL isPrimaryRope;

这些就将作为你的数据模型。在开发环境中,你将知道使用PLIST会比用程序创造数据更好。

回到TLCMyScene.m 并在#import 后加入下列代码:

#define lengthOfRope1 24

#define lengthOfRope2 18

#define lengthOfRope3 15

然后在其他的接口后面加入下面的两个接口来控制奖品和绳子层级的数据:

@property (nonatomic, strong) SKSpriteNode *prize;

 

@property (nonatomic, strong) NSMutableArray *ropes;

当你完成了这些东西,那就去找到setupGameData 并加入下列代码:

self.ropes = [NSMutableArray array];

 

TLCGameData *rope1 = [[TLCGameData alloc] init];

rope1.name = 0;

rope1.ropeLocation = CGPointMake(self.size.width *.12, self.size.height * .94);

rope1.ropeLength = lengthOfRope1;

rope1.isPrimaryRope = YES;

[self.ropes addObject:rope1];

 

TLCGameData *rope2 = [[TLCGameData alloc] init];

rope2.name = 1;

rope2.ropeLocation = CGPointMake(self.size.width *.85, self.size.height * .90);

rope2.ropeLength = lengthOfRope2;

rope2.isPrimaryRope = NO;

[self.ropes addObject:rope2];

 

TLCGameData *rope3 = [[TLCGameData alloc] init];

rope3.name = 2;

rope3.ropeLocation = CGPointMake(self.size.width *.86, self.size.height * .76);

rope3.ropeLength = lengthOfRope3;

rope3.isPrimaryRope = NO;

[self.ropes addObject:rope3];

上面这些代码给你绳子定义了基础的参数。最重要的就是isPrimaryRope 接口,因为它决定了绳子是怎么连接到奖品(就是菠萝)上的。当你创造你的绳子时,只有一个对象的isPrimaryRope 属性是Yes。

最后,增加两个函数:initWithSize:[self setupGameData][self setupRopes] 。当你做好了,initWithSize 下就会像下面这样:

if (self = [super initWithSize:size]) {

    /* Setup your scene here */

 

    self.worldNode = [SKNode node];

    [self addChild:self.worldNode];

 

    self.worldNode.scene.physicsWorld.contactDelegate = self;

    self.worldNode.scene.physicsWorld.gravity = CGVectorMake(0.0,-9.8);

    self.worldNode.scene.physicsWorld.speed = 1.0;

 

    [self setupSounds];

    [self setupGameData];

 

    [self setupBackground];

    [self setupTrees];

    [self setupCrocodile];

 

    [self setupRopes];

}

 

return self;

现在你可以建造绳子了。

绳子类

在这节,你将会学习创建处理绳子的类。

打开TLCRope.h,你将将在在文件中加入两端代码。在@interface 前加入第一段代码,就是代理的协议。

@protocol TLCRopeDelegate

- (void)addJoint:(SKPhysicsJointPin *)joint;

@end

@interface 下加入第二段代码,包括了对自定义init方法的声明。

@property (strong, nonatomic) id delegate;

 

- (instancetype)initWithLength:(int)length usingAttachmentPoint:(CGPoint)point toNode:(SKNode*)node withName:(NSString *)name withDelegate:(id)delegate;

- (void)addRopePhysics;

- (NSUInteger)getRopeLength;

- (NSMutableArray *)getRopeNodes;

代理要一个对象定义一个包括方法的协议。而代理将回应这些方法。代理类必须声明以表示它将依据协议运行需要的方法。

注释:然而你不需要绳子对象的代理正确运行,这个教程包含着一个代理来说明如何去运行一个。最后的项目将包含一行解释性代码来说明一个在代理之前的可代替的方法。

当你完成了头文件,打开TLCRope.m 并在@interface 中加入下面的接口:


@property (nonatomic, strong) NSString *name;

 

@property (nonatomic, strong) NSMutableArray *ropeNodes;

 

@property (nonatomic, strong) SKNode *attachmentNode;

@property (nonatomic, assign) CGPoint attachmentPoint;

 

@property (nonatomic, assign) int length;

你接下来要做的就是在自定义的init中加入代码。找到 #pragma mark Init Method 并加入如下代码:

- (instancetype)initWithLength:(int)length usingAttachmentPoint:(CGPoint)point toNode:(SKNode*)node withName:(NSString *)name withDelegate:(id)delegate;

{

    self = [super init];

    if (self)

    {

        self.delegate = delegate;

 

        self.name = name;

 

        self.attachmentNode = node;

        self.attachmentPoint = point;

 

        self.ropeNodes = [NSMutableArray arrayWithCapacity:length];

 

        self.length = length;

    }

    return self;

}

很简单了:你把数值放到init里面并用它们来设置类中的私有接口。

下面要加的方法就是getRopeLengthgetRopeNodes

找到 #pragma mark Helper Methods 并加入下列代码:


- (NSUInteger)getRopeLength

{

    return self.ropeNodes.count;

}

 

- (NSMutableArray *)getRopeNodes

{

    return self.ropeNodes;

}

这两个方法是用来读取私有接口值的。

你可能已经注意到上面的代码引用的是rope nodes,是复数,也就是有很多个绳子节点。因为在这个游戏,每个绳子都是由很多个节点组成,主要是为了让它看上去和表现得是像是活动的,就是和真的绳子一样。我们来看看在实际运用中这是怎么工作的。

创造绳子的每个部分

尽管下面你写的几个方法将会是不完整的。这是有理由的。你可以去彻底地去理解发生了什么,一步一步地去做事总是很重要。

仍然在TLCRope.m 下操作,找到#pragma mark Setup Physics 并加入如下方法:


- (void)addRopePhysics

{

    // 记录追踪现在绳子子节点的位置

    CGPoint currentPosition = self.attachmentPoint;

 

    // 加入绳子所有的子部分

    for (int i = 0; i < self.length; i++) {

        SKSpriteNode *ropePart = [SKSpriteNode spriteNodeWithImageNamed:kImageNameForRopeTexture];

        ropePart.name = self.name;

        ropePart.position = currentPosition;

        ropePart.anchorPoint = CGPointMake(0.5, 0.5);

 

        [self addChild:ropePart];

        [self.ropeNodes addObject:ropePart];

 

        /* TODO - 在这加入控制物理特性的代码 */

 

        // set the next rope part position

        currentPosition = CGPointMake(currentPosition.x, currentPosition.y - ropePart.size.height);

    }

}

在上面的代码中,你在对象初始化后调用这个函数,你创建了每个绳子的部分并把它们加到ropNode 数组中。为了以后可以引用它们,你也给每一个绳子部分命名。最后,你使用addChild 把它们以子节点的身份加到TLCRope 对象中。

马上,你就将用一些代码替换上面的TODO注释,为了给这些绳子的部分自身的物理实体。

现在你让一切就绪了。你几乎可以通过编译运行来看的你的绳子了。最后的步骤就是把绳子了绑好的奖品加到主场景中。

把绳子和奖品加到场景中

由于项目给TLCRope 用了代理模式,你将要声明它在任何充当代理的类中。在这个例子里,这个类就是TLCMyScene

打开TLCMyScene.m 找到@interface 这行。把它改成下面那样:

@interface TLCMyScene() 

你将用3中方法把绳子加到场景中,并且他们都是内部连接的,这3个方法就是:setupRopes,addRopeAtPosition:withLength:withNamesetupPrizeUsingPrimaryRope

先做这步吧,找到setupRope 并加入如下代码:


// 得到绳子数据

    for (int i = 0; i < [self.ropes count]; i++) {

        TLCGameData *currentRecord = [self.ropes objectAtIndex:i];

 

        // 1

        TLCRope *rope = [self addRopeAtPosition:currentRecord.ropeLocation withLength:currentRecord.ropeLength withName:[NSString stringWithFormat:@"%i", i]];

 

        // 2

        [self.worldNode addChild:rope];

        [rope addRopePhysics];

 

        // 3

        if (currentRecord.isPrimaryRope) {

            [self setupPrizeUsingPrimaryRope:rope];

        }

    }

 

    self.prize.position = CGPointMake(self.size.width * .50, self.size.height * .80);



我先解释下:

  1. 首先你创建新的绳子时往里面传递了位置和长度。等一下你将会写这个方法。

  2. 下一步就是把绳子加入到世界中并设置物理特性。

  3. 最后一步就是当绳子是主要的那个,就把奖品设置物理特性。

找到addRopeAtPosition:withLength:withName 并用下面的代码代替里面的内容。


SKSpriteNode *ropeHolder = [SKSpriteNode spriteNodeWithImageNamed:kImageNameForRopeHolder];

 

ropeHolder.position = location;

ropeHolder.zPosition = LayerRope;

 

[self.worldNode addChild:ropeHolder];

 

CGPoint ropeAttachPos = CGPointMake(ropeHolder.position.x, ropeHolder.position.y -8);

 

TLCRope *rope = [[TLCRope alloc] initWithLength:length usingAttachmentPoint:ropeAttachPos toNode:ropeHolder withName:name withDelegate:self];

rope.zPosition = LayerRope;

rope.name = name;

 

return rope;

其实你就是用了这个方法创造了单独的绳子并把它们显示在屏幕上。

你曾经看过这段内容。首先你使用常量代表图片名称,用图片名称初始化SKSpriteNode ,然后用常量设置它的positionzPosition。这个SKSpriteNode将成为绳子的固定点。

代码继续初始化你的绳子对象并设置它的zPositionname

最后,最后一个难题就只剩下弄好你的奖品了。是不是很聪明?

找到setupPrizeUsingPrimaryRope并加入如下代码:


self.prize = [SKSpriteNode spriteNodeWithImageNamed:kImageNameForPrize];

self.prize.name = kNodeNameForPrize;

self.prize.zPosition = LayerPrize;

 

self.prize.anchorPoint = CGPointMake(0.5, 1);

 

SKNode *positionOfLastNode = [[rope getRopeNodes] lastObject];

self.prize.position = CGPointMake(positionOfLastNode.position.x, positionOfLastNode.position.y + self.prize.size.height * .30);

 

[self.worldNode addChild:self.prize];

你可能已经注意到了一个存在于setupRopes和游戏的绳子对象中的为变量isPrimaryRope而设的接口,这个接口让你的循环贯穿数据并选择合适的绳子作为主绳来绑上这个奖品。

当你把isPrimaryRope设置为YES,上面的代码会执行和找到传入绳子对象的末端。可以通过得到绳子的数组对象ropeNodes内部的末尾对象来找到绳子的末端。可以通过使用在TLCRope中的方法getRopeNodes来执行这个操作。

注释:如果你想知道为什么奖品的位置会移动到TLCRope对象的最后一个节点。因为在Sprite Kit中有一个bug。当你设置物理实体时,如果节点的位置在先前未被设置,那么实体的行为将无法预测。以前的代码使用你选择的绳子的最后的那个部分作为奖品的初始位置。

现在你还等什么,快开始编译运行你的项目吧。

等一下。。。为什么菠萝没被系在绳子上?为什么这些东西看上去这么蛋疼。

不要担心,解决方案就是设置更多的物理特性。

在绳子中增加物理实体

还记得你早先加的TODO的注释吗?是时候用物理实体代替注释来使东西移动了。

打开TLCRope.m 并找到addRopePhysics 。用如下代码代替TODO的注释:


CGFloat offsetX = ropePart.frame.size.width * ropePart.anchorPoint.x;

CGFloat offsetY = ropePart.frame.size.height * ropePart.anchorPoint.y;

 

CGMutablePathRef path = CGPathCreateMutable();

 

CGPathMoveToPoint(path, NULL, 0 - offsetX, 7 - offsetY);

CGPathAddLineToPoint(path, NULL, 7 - offsetX, 7 - offsetY);

CGPathAddLineToPoint(path, NULL, 7 - offsetX, 0 - offsetY);

CGPathAddLineToPoint(path, NULL, 0 - offsetX, 0 - offsetY);

 

CGPathCloseSubpath(path);

 

ropePart.physicsBody = [SKPhysicsBody bodyWithPolygonFromPath:path];

ropePart.physicsBody.allowsRotation = YES;

ropePart.physicsBody.affectedByGravity = YES;

 

ropePart.physicsBody.categoryBitMask = EntityCategoryRope;

ropePart.physicsBody.collisionBitMask = EntityCategoryRopeAttachment;

ropePart.physicsBody.contactTestBitMask =  EntityCategoryPrize;

 

[ropePart skt_attachDebugFrameFromPath:path color:[SKColor redColor]];

CGPathRelease(path);

上面的代码给你每个绳子的部分创建了物理实体,让你为每个节点设置一系列的物理特性。比如形状,大小,密度,重力以及摩擦力。

使用类方法SKPhysicsBody bodyWithPolygonFromPath:来创建物理实体。这种方法使用了一个参数,那就是path。可以用 SKPhysicsBody Path Generator这个在线方便的工具得到这个path(物体轮廓路径)。

注释想要知道在你的项目中使用物理实体的更多方法,可以参考 SKPhysicsBody Class Reference

另外,把物理实体设置给每个节点,上面的代码也设置了一些关键的接口来处理碰撞,也就是categoryBitMask, collisionBitMask和contactTestBitMask。每一个的值都被赋予一个你之前定义的常数值。这个教程等一下将把这些接口包括到物体的深度信息中。

如果你现在运行你的app,那么每一个绳子的部分都将掉到你的屏幕底部。那是因为你已经给绳子的每个部分并且还没有把它们连接在一起。

为了融合你的绳子,你将使用SKPhysicsJoint,在 addRopePhysics下加入如下的方法:

- (void)addRopeJoints

{

    // 给初始化的绑定点设置关节 

    SKNode *nodeA = self.attachmentNode;

    SKSpriteNode *nodeB = [self.ropeNodes objectAtIndex:0];

 

    SKPhysicsJointPin *joint = [SKPhysicsJointPin jointWithBodyA: nodeA.physicsBody

                                                           bodyB: nodeB.physicsBody

                                                          anchor: self.attachmentPoint];

 

    // 让绑定点僵硬化

    joint.shouldEnableLimits = YES;

    joint.upperAngleLimit = 0;

    joint.lowerAngleLimit = 0;

 

    [self.delegate addJoint:joint];

 

    // 为剩下的绳子子节点设置关节

    for (int i = 1; i < self.length; i++) {

        SKSpriteNode *nodeA = [self.ropeNodes objectAtIndex:i-1];

        SKSpriteNode *nodeB = [self.ropeNodes objectAtIndex:i];

        SKPhysicsJointPin *joint = [SKPhysicsJointPin jointWithBodyA: nodeA.physicsBody

                                                               bodyB: nodeB.physicsBody

                                                              anchor: CGPointMake(CGRectGetMidX(nodeA.frame),

                                                                                  CGRectGetMinY(nodeA.frame))];

        // 让关节自由转动

        joint.shouldEnableLimits = NO;

        joint.upperAngleLimit = 0;

        joint.lowerAngleLimit = 0;

 

        [self.delegate addJoint:joint];

    }

}

这些方法通过 SKPhysicsJoint类连接了所有的绳子部分。这个类能使两个连接好的实体能独自以锚点为中心旋转,这使游戏中的绳子和真的绳子一样。

你将绳子的第一个部分在attachmentPoint上连接(固定)到attachmentNode然后把其它的子序列节点加到前面的节点上。

注释: 在上面的代码中,有用调用代理的方式把关节加到场景当中。正如我先前已经提到的,这种函数的调用不是必要的。你可以简单地调用[self.scene.physicsWorld addJoint:joint]来完成这样的事。

现在把这句函数调用加到刚写好的方法的addRopePhysics下面。

[self addRopeJoints];

编译运行,开吧!

当你有了这些活动的绳子,但它们只是从屏幕上落下来。这是因为你没有在TLCScene.m给节点赋上物理实体。是时候给绳子的固定点和奖品加上物理实体了。

固定绳子的末端

打开TLCMyScene.m 找到addRopeAtPosition:withLength:withName:,在[self.worldNode addChild:ropeHolder]下加入如下的代码:

CGFloat offsetX = ropeHolder.frame.size.width * ropeHolder.anchorPoint.x;

CGFloat offsetY = ropeHolder.frame.size.height * ropeHolder.anchorPoint.y;

 

CGMutablePathRef path = CGPathCreateMutable();

 

CGPathMoveToPoint(path, NULL, 0 - offsetX, 6 - offsetY);

CGPathAddLineToPoint(path, NULL, 6 - offsetX, 6 - offsetY);

CGPathAddLineToPoint(path, NULL, 6 - offsetX, 0 - offsetY);

CGPathAddLineToPoint(path, NULL, 0 - offsetX, 0 - offsetY);

 

CGPathCloseSubpath(path);

 

ropeHolder.physicsBody = [SKPhysicsBody bodyWithPolygonFromPath:path];

ropeHolder.physicsBody.affectedByGravity = NO;

ropeHolder.physicsBody.dynamic = NO;

 

ropeHolder.physicsBody.categoryBitMask = EntityCategoryRopeAttachment;

ropeHolder.physicsBody.collisionBitMask = 0;

ropeHolder.physicsBody.contactTestBitMask =  EntityCategoryPrize;

 

[ropeHolder skt_attachDebugFrameFromPath:path color:[SKColor redColor]];

CGPathRelease(path);

这里你给每一个绳子的固定点设置了SKPhysicsBody以及用于检测碰撞的接口。你要让这些固定处充当结实的稳固点,可以通过关闭它们的affectedByGravitydynamic接口来实现这个功能。

下一步就是找到代理的方法addJoint,加入下面这行:

[self.worldNode.scene.physicsWorld addJoint:joint];

上面的方法就是将你在TLCRope.m里创建的节点加入到场景中。这行代码就将绳子的每个部分连接在一起。

下一步就是把物理实体加入到奖品中并设置它的碰撞接口。

找到 setupPrizeUsingPrimaryRope:,在[self.worldNode addChild:self.prize]之前加入下列代码:

CGFloat offsetX = self.prize.frame.size.width * self.prize.anchorPoint.x;

CGFloat offsetY = self.prize.frame.size.height * self.prize.anchorPoint.y;

 

CGMutablePathRef path = CGPathCreateMutable();

 

CGPathMoveToPoint(path, NULL, 18 - offsetX, 75 - offsetY);

CGPathAddLineToPoint(path, NULL, 5 - offsetX, 65 - offsetY);

CGPathAddLineToPoint(path, NULL, 3 - offsetX, 55 - offsetY);

CGPathAddLineToPoint(path, NULL, 4 - offsetX, 34 - offsetY);

CGPathAddLineToPoint(path, NULL, 8 - offsetX, 7 - offsetY);

CGPathAddLineToPoint(path, NULL, 21 - offsetX, 2 - offsetY);

CGPathAddLineToPoint(path, NULL, 33 - offsetX, 4 - offsetY);

CGPathAddLineToPoint(path, NULL, 38 - offsetX, 20 - offsetY);

CGPathAddLineToPoint(path, NULL, 34 - offsetX, 53 - offsetY);

CGPathAddLineToPoint(path, NULL, 36 - offsetX, 62 - offsetY);

 

CGPathCloseSubpath(path);

 

self.prize.physicsBody = [SKPhysicsBody bodyWithPolygonFromPath:path];

self.prize.physicsBody.allowsRotation = YES;

self.prize.physicsBody.affectedByGravity = YES;

self.prize.physicsBody.density = 1;

self.prize.physicsBody.dynamic = NO;

 

self.prize.physicsBody.categoryBitMask = EntityCategoryPrize;

self.prize.physicsBody.collisionBitMask = 0;

self.prize.physicsBody.contactTestBitMask = EntityCategoryRope;

 

[self.prize skt_attachDebugFrameFromPath:path color:[SKColor redColor]];

CGPathRelease(path);

就像之前一样,你设置了物理实体和碰撞检测接口。

为了把奖品连接到绳子的末端,你需要做两件事情。

第一,就是找到setupRopes。在for循环的末端,加入如下代码:

//把绳子的另一端接到奖品上

[self attachNode:self.prize toRope:rope];

然后,找到attachNode:toRope并加入如下代码:

SKNode *previous = [[rope getRopeNodes] lastObject];

node.position = CGPointMake(previous.position.x, previous.position.y + node.size.height * .40);

 

SKSpriteNode *nodeAA = [[rope getRopeNodes] lastObject];

 

SKPhysicsJointPin *jointB = [SKPhysicsJointPin jointWithBodyA: previous.physicsBody

                                                        bodyB: node.physicsBody

                                                       anchor: CGPointMake(CGRectGetMidX(nodeAA.frame), CGRectGetMinY(nodeAA.frame))];

 

 

[self.worldNode.scene.physicsWorld addJoint:jointB];

上面的代码得到了绳子对象的最后一个节点并为了连接奖品创建了一个新的SKPhysicsJointPin

编译运行项目。如果你的连接点和节点都设置正确了,你应该能在屏幕上看到这样的图片:

看起来很棒吧。额。。。可能还是有点呆板。可能这就是你在游戏里想要的效果吧。如果不是,你可以让绳子看上去更加灵活柔软一些。

TLCMyScene.m的顶端并在你的#define下加入如下代码:

#define prizeIsDynamicsOnStart YES

然后找到setupRopes并改变最后两句代码:

// 重设你的奖品位置,根据你的游戏需要设置是否要是动态的

self.prize.position = CGPointMake(self.size.width * .50, self.size.height * .80);

self.prize.physicsBody.dynamic = prizeIsDynamicsOnStart;

再次编译运行这个项目。

现在能看到你的绳子是多么柔软灵活了吧。当然,如果你想要另一种形态,只要把prizeIsDynamicsOnStart改为No。毕竟这是你的游戏,随便啦,做爱做的事吧。

再增加一点物理实体

由于你已经知道这些物理实体了,那就应该给玩家和水节点设置这些实体。当你把这些东西都配置好,你最好也把物理碰撞检测也做好。

TLCMyScene.m,找到setupCrocodile并在[self.worldNode addChild:self.crocodile];前加入如下代码:

CGFloat offsetX = self.crocodile.frame.size.width * self.crocodile.anchorPoint.x;

CGFloat offsetY = self.crocodile.frame.size.height * self.crocodile.anchorPoint.y;

 

CGMutablePathRef path = CGPathCreateMutable();

CGPathMoveToPoint(path, NULL, 47 - offsetX, 77 - offsetY);

CGPathAddLineToPoint(path, NULL, 5 - offsetX, 51 - offsetY);

CGPathAddLineToPoint(path, NULL, 7 - offsetX, 2 - offsetY);

CGPathAddLineToPoint(path, NULL, 78 - offsetX, 2 - offsetY);

CGPathAddLineToPoint(path, NULL, 102 - offsetX, 21 - offsetY);

 

CGPathCloseSubpath(path);

 

self.crocodile.physicsBody = [SKPhysicsBody bodyWithPolygonFromPath:path];

 

self.crocodile.physicsBody.categoryBitMask = EntityCategoryCrocodile;

self.crocodile.physicsBody.collisionBitMask = 0;

self.crocodile.physicsBody.contactTestBitMask =  EntityCategoryPrize;

 

self.crocodile.physicsBody.dynamic = NO;

 

[self.crocodile skt_attachDebugFrameFromPath:path color:[SKColor redColor]];

CGPathRelease(path);

就和绳子节点一样,你要给你的玩家节点上的物理实体确定轮廓路径并设置它的碰撞检测接口,这些我等一下就会逐一解释。

最后的但并不是不重要的,水也需要物理实体这样当奖品是落在水中而不是鳄鱼的嘴里,你就能判断出来。

找到setupBackground,在[self.worldNode addChild:water];上,加入如下代码:

// 让整个尺寸看上去短一些,为了让奖品看上去是真的落到了水里

CGSize bodySize = CGSizeMake(water.frame.size.width, water.frame.size.height -100);

 

water.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:bodySize];

water.physicsBody.dynamic = NO;

 

water.physicsBody.categoryBitMask = EntityCategoryGround;

water.physicsBody.collisionBitMask = EntityCategoryPrize;

water.physicsBody.contactTestBitMask =  EntityCategoryPrize;

再一次,你加了一个物理实体,但是这次你用的是bodyWithRectangleOfSize:。你也要设置碰撞检测接口。

你已经注意到你把EntityCategoryGround赋给了水对象的categoryBitMask(类识别掩码)。事实上EntityCategoryGround代表着你接受水果失败了,水果撞上地面的情况。如果你想加一些其他的陷阱比如旋转锯,你将会把EntityCategoryGround赋给它的类识别掩码。

注释:你可能注意到在大多数物理实体的设置中,调用了skt_attachDebugFrameFromPath:。这是一个从SKNode+SKTDebugDraw来的方法。它是Sprite Kit 工具集中的一部分。而这个工具集是Razeware做的。这个特殊的方法在调试物理实体的过程中起作用。为了把它打开,可以打开SKNode+SKTDebugDraw.m并把BOOL SKTDebugDrawEnabledno改成yes。它会画出一个形状来代表你的物理实体。当你把事情完成了,不要忘记把它关掉。

把切绳子的功能做出来

如果说你的verlets不能被切,那么切Verlet这个功能就不存在了。

在这节,你将学习如何使用触摸方法去让你的玩家切绳子。第一步就是第一一些基础的变量。

仍然实在TLCMyScene.m中操作,在@interface下加入下面的接口:

@property (nonatomic, assign) CGPoint touchStartPoint;

@property (nonatomic, assign) CGPoint touchEndPoint;

@property (nonatomic, assign) BOOL touchMoving;

你将在追踪玩家的触摸位置中使用它们。

然后在TLCMyScene.m上方加入你最后的定义

#define canCutMultipleRopesAtOnce NO

如果你想对游戏的功能做一些改变,你将需要它。

IOS包括一些处理触摸事件的方法。你将使用3种方法,分别是touchesBegan:withEvent:, touchesEnded:withEvent:touchesMoved:withEvent:

找到touchesBegan:withEvent:并加入如下代码:

self.touchMoving = NO;

 

for (UITouch *touch in touches) {

    self.touchStartPoint = [touch locationInNode:self];

}

上面的代码设置了基于玩家触摸位置的值。

下一步,找到touchesEnded:withEvent:并加入下列代码:

for (UITouch *touch in touches) {

    if (touches.count == 1 && self.touchMoving) {

        self.touchEndPoint = [touch locationInNode:self];

 

        if (canCutMultipleRopesAtOnce) {

            /* 允许几根绳子被切 */

 

            [self.worldNode.scene.physicsWorld enumerateBodiesAlongRayStart:self.touchStartPoint end:self.touchEndPoint usingBlock:^(SKPhysicsBody *body, CGPoint point, CGVector normal, BOOL *stop)

             {

                 [self checkRopeCutWithBody:body];

             }];

        }

        else {

            /* 允许只有一根绳子被切 */

 

            SKPhysicsBody *body = [self.worldNode.scene.physicsWorld bodyAlongRayStart:self.touchStartPoint end:self.touchEndPoint];

            [self checkRopeCutWithBody:body];

        }

    }

}

 

self.touchMoving = NO;

代码做了下面几件事。首先,它确保了玩家正用一个手指触摸屏幕,然后它判断了玩家是否正在移动手指。最后,它得到数据并将其赋给了touchEndPoint。有了这个数据,你可以基于你是允许在一次划动过程中是让一根绳子被切还是让多根绳子被切来执行相应的动作。

当要切多根绳子,你使用SKPhysicsWorldenumerateBodiesAlongRayStart:end:usingBlock:方法去得到多个触摸点。当要切单根绳子的时候,你使用bodyAlongRayStart:end:方法去得到第一个触摸点。然后你就把信息传给自制的方法:checkRopeCutWithBody:

最后,找到touchesMoved:withEvent:,并加入如下代码:

if (touches.count == 1) {

    for (UITouch *touch in touches) {

        NSString *particlePath = [[NSBundle mainBundle] pathForResource:@"TLCParticle" ofType:@"sks"];

        SKEmitterNode *emitter = [NSKeyedUnarchiver unarchiveObjectWithFile:particlePath];

        emitter.position = [touch locationInNode:self];

        emitter.zPosition = LayerRope;

        emitter.name = @"emitter";

 

        [self.worldNode addChild:emitter];

 

        self.touchMoving = YES;

    }

}

从技术上来说,上面的绝大多数代码你并不需要,但是它可以提供很多帅酷霸气吊的效果当你划过屏幕的时候。并且你需要把touchMoving接口设为YES。正如你早些时候看到的,你设置这个值来决定玩家是否在移动手指。

那么剩下的代码是干嘛的?

这里使用了SKEmitterNode去自动把帅气的绿色粒子显示在屏幕上当玩家划动的时候。

上面的代码载入了粒子文件并把它加到worldNode。但粒子反射器的介绍不在本次的教程范围内。额。。。你还是知道这个东西的存在的吧。好吧,你还有其他事情要做。

触摸事件的相关函数完成后,你应该去完成在touchesEnded:withEvent:里调用的方法。

找到checkRopeCutWithBody:并加入下列代码:

SKNode *node = body.node;

if (body) {

    self.prize.physicsBody.affectedByGravity = YES;

    self.prize.physicsBody.dynamic = YES;

 

    [self.worldNode enumerateChildNodesWithName:node.name usingBlock:^(SKNode *node, BOOL *stop)

     {

         for (SKPhysicsJoint *joint in body.joints) {

             [self.worldNode.scene.physicsWorld removeJoint:joint];

         }

 

         SKSpriteNode *ropePart = (SKSpriteNode *)node;

 

         SKAction *fadeAway = [SKAction fadeOutWithDuration:0.25];

         SKAction *removeNode = [SKAction removeFromParent];

 

         SKAction *sequence = [SKAction sequence:@[fadeAway, removeNode]];

         [ropePart runAction: sequence];

     }];

}

上面代码枚举了worldNode所有的子节点,而且如果它得到了相应的绳子关节,它把这些绳子关节删掉了。当然不会是突然就删掉它们,代码使用了SKAction的序列让节点先淡出,然后删除。

编译运行项目,你应该能划动屏幕并切掉这三根绳子和奖品(目前是这样的)。切换canCutMultipleRopesAtOnce的设置来看整个行为有什么不一样。还有,这些产生的粒子看上去不是很好看吗?

碰撞检测

你几乎已经把碰撞检测做好了。你完成了划动时有的效果,物理实体,而且你已经设置了所有的碰撞接口,但是这些碰撞接口是干什么的?它们怎么运作的?更重要的是它们之间是怎么运作的?

这里有一些重要的事要提:

  1. 你需要让TLCMyScene充当一个contact delegate(触点代理):SKPhysicsContactDelegate

  2. 你需要给世界节点设置代理:self.worldNode.scene.physicsWorld.contactDelegate = self

  3. 你需要设置一个categoryBitMask, 一个collisionBitMask 和一个contactTestBitMask

  4. 你需要运行这个代理方法。

你在给worldNode设置物理的时候做了前两件事情。当你给节点们设置了物理实体时做了第三件事情。既然这些都被你在之前的操作中做了,那你还是很有远见的。

那么就是剩下第四件事了:运行这些方法。在做这些是之前,你要创建一些接口。

TLCMyScene.m下的@implementation中,加入下列代码:


@property (nonatomic, assign) BOOL scoredPoint;

@property (nonatomic, assign) BOOL hitGround;

现在你可以修改代理方法了。

找到didBeginContact:并加入如下代码:

SKPhysicsBody *other = (contact.bodyA.categoryBitMask == EntityCategoryPrize ? contact.bodyB : contact.bodyA);

if (other.categoryBitMask == EntityCategoryCrocodile) {

    if (!self.hitGround) {

        NSLog(@"scoredPoint");

        self.scoredPoint = YES;

    }

 

    return;

}

else if (other.categoryBitMask == EntityCategoryGround) {

    if (!self.scoredPoint) {

        NSLog(@"hitGround");

        self.hitGround = YES;

        return;

    }

}

上面的代码在任何时间都会不断执行场景的physicsWorld(物理世界)的碰撞检测。它检测实体的categoryBitMask并基于这个值,得一分并注册一个地面碰撞。

三种设置:categoryBitMask, collisionBitMaskcontactTestBitMask都是互相工作的。

  • categoryBitMask定义了这个精灵是属于哪个类别的。

  • collisionBitMask设置了可以与之碰撞的精灵。

  • contactTestBitMask设置了那些类别可以触发通知发给代理。

浏览SKPhysicsBody Class Reference以学到更多。

这个游戏用了五个类别,在TLCSharedConstants类中定义了的。打开TLCSharedConstants.m来看一下。你将看到一些在之前设置了的碰撞类别。


EntityCategoryCrocodile = 1 << 0,

EntityCategoryRopeAttachment = 1 << 1,

EntityCategoryRope = 1 << 2,

EntityCategoryPrize = 1 << 3,

EntityCategoryGround = 1 << 4

你得去检测奖品和鳄鱼碰撞的情况以及奖品和水碰撞的情况。你不是基于绳子的接触点来奖励分数或者结束游戏,而是基于为绳子的附属点设置的类别。而这些附属点就是为了让绳子在这些点上看上去更为真实。

注释:这里有一些你在本教程不需要的代理方法。你可以阅读SKPhysicsContact Class ReferenceSKPhysicsContactDelegate Protocol Reference来学习它们。

编译运行工程。当你切绳子的时候,当奖品落地,你将会看到记录被打出来。

奖励动画!

当你做出游戏的大致样子,玩家们是不会看着命令窗口来知道他们是不是赢了。而且,当你切了正确的绳子,水果会掉到鳄鱼的嘴里并且鳄鱼会吃水果。玩家会想让鳄鱼吃掉菠萝的。

是时候用动画实现这个功能了。你可以通过修改nomnomnomActionWithDelay:.来达成。

[self.crocodile removeAllActions];

 

SKAction *openMouth = [SKAction setTexture:[SKTexture textureWithImageNamed:kImageNameForCrocodileMouthOpen]];

SKAction *wait = [SKAction waitForDuration:duration];

SKAction *closeMouth = [SKAction setTexture:[SKTexture textureWithImageNamed:kImageNameForCrocodileMouthClosed]];

 

SKAction *nomnomnomAnimation = [SKAction sequence:@[openMouth, wait, closeMouth]];

 

[self.crocodile runAction: [SKAction repeatAction:nomnomnomAnimation count:1]];

 

if (!self.scoredPoint) {

    [self animateCrocodile];

}

上面的代码使用removeAllActions移除了现在运行在鳄鱼节点上的所有动画。然后它加了鳄鱼张开嘴和不关闭嘴的动画并把它加到动画序列中,把序列在鳄鱼节点上运行出来。在这时,如果玩家没有得分,它将运行animateCrocodile来重设鳄鱼的开关下颚的动作。

接下来就是找到checkRopeCutWithBody:并且在self.prize.physicsBody.dynamic = YES;后加入下列代码:

[self nomnomnomActionWithDelay:1];

这段代码在玩家每次切绳子的时候执行。它运行了你刚刚创建的方法。然后这段动画就会给你一种错觉让你感觉鳄鱼好像是在张开嘴期待美味掉入它的嘴里。

你也需要运行didBeginContact的方法,为了让奖品落到鳄鱼嘴中的时候,鳄鱼能张嘴吃它。

didBeginContact:中,在self.scoredPoint = YES;后加入如下代码:

[self nomnomnomActionWithDelay:.15];

就像之前那样,你运行了nomnomnomActionWithDelay,,只是在这次,你是在当奖品接触到鳄鱼的时候才运行它。这让鳄鱼看上去在吃奖品。

编译运行

食物掉下的时候就从鳄鱼上穿过去了。只要做一些小小的改变,你就可以解决这个问题。

找到checkForScore并加入如下代码:


if (self.scoredPoint) {

    self.scoredPoint = NO;

 

    SKAction *shrink = [SKAction scaleTo:0 duration:0.08];

    SKAction *removeNode = [SKAction removeFromParent];

 

    SKAction *sequence = [SKAction sequence:@[shrink, removeNode]];

    [self.prize runAction: sequence];

}

上面的代码解决了scoredPoint的接口。如果被设为YES,代码就将值设为NO,使用SKAction sequence播放nomnomnom(这是个拟声词,和喵喵喵的性质一样)的声音并将奖品从场景中去掉。

如果你想让代码继续执行并且能追踪更新你的数值,你需要改变update函数。

找到update并加入如下代码:

[self checkForScore];

update:执行在每一帧动画播放之前。你可以在这里调用方法来检查是否玩家得到了分数。

你要做的下一件事情就是检查一下地面碰撞。找到checkForGroundHit并加入下列代码:

if (self.hitGround) {

    self.hitGround = NO;

 

    SKAction *shrink = [SKAction scaleTo:0 duration:0.08];

    SKAction *removeNode = [SKAction removeFromParent];

 

    SKAction *sequence = [SKAction sequence:@[shrink, removeNode]];

    [self.prize runAction: sequence];

}

几乎就像checkForScore一样,这段代码检查了hitGround的值。如果这个值为YES,代码就把它重置为NO,通过SKAction sequence.播放

标签: sprite kit

?>