【我的第一款游戏类App(《跑酷好基友》 英文名:BothLive) 登录App Store(一)】使用iOS7推出的Sprite Kit框架制作一款2D跑酷小游戏

从本篇文章开始,我将陆续用至少三篇文章介绍一下我个人的第一款上线App Store的游戏:“跑酷好基友”,英文名BothLive。从游戏制作、社交分享、App上传审核,以及版本更新迭代(如果有)几个方面来介绍。目前,这只是一个非常非常easy的超轻量级游戏。

 

说来也很有意思,本人一直从事iOS应用客户端的开发,对于iOS游戏制作从来也没花时间和心思。但是一个偶然的机会:2014年3月份公司派我去南京晓庄学院做一场开发讲座,讲座中需要向同学们演示一个小游戏的开发过程,于是我便利用iOS7推出的全新的Sprite Kit游戏引擎,制作了一个简单的游戏动画(小人在走动的场景)成功地做了演示。

回来以后我觉得Sprite Kit是个有意思的框架,既然都接触了它,为何不接着玩下去?正巧今年(2014年)夏天,微信上掀起了一阵“弱智小游戏分享大比拼”的小高潮,其实很有点类似当年“校内网”(今“人人网”)小游戏风靡的感觉。于是我便萌生了一个利用Sprite Kit移植(或者说山寨)一个网页小游戏到iOS端的想法。可以说是纯玩票!本篇文章将介绍Sprite Kit,以及所有的开发要点。

首先附上App下载链接:https://itunes.apple.com/us/app/pao-ku-hao-ji-you/id914554369?mt=8 欢迎朋友们下载试玩,完全免费哦!

同时,他也是开源项目,供大家参考:https://github.com/pigpigdaddy/BothLive

这是一款跑酷类游戏,玩家同时控制上下两个小人,跳跃避开前方到来的不同障碍,如果上下其中任何一个小人在跳跃时撞击到障碍物则游戏结束,计时器会记下游戏持续时间,并在结束后提示是否分享到微信。

 

接着切入正题,我将分为四个小节来介绍:

1:背景

通常我们知道iOS上做2D游戏的比如cocos2D,不过今天我用的不是它,而是WWDC2013上伴随iOS7一道而来的Spirte Kit框架。节选OneVcat大神的介绍:“Sprite的中文译名就是精灵,在游戏开发中,精灵指的是以图像方式呈现在屏幕上的一个图像。这个图像也许可以移动,用户可以与其交互,也有可能仅只是游戏的一个静止的背景图。塔防游戏中敌方源源不断涌来的每个小兵都是一个精灵,我方防御塔发出的炮弹也是精灵。可以说精灵构成了游戏的绝大部分主体视觉内容,而一个2D引擎的主要工作,就是高效地组织,管理和渲染这些精灵。SpriteKit是在iOS7 SDK中Apple新加入的一个2D游戏引擎框架,在SpriteKit出现之前,iOS开发平台上已经出现了像cocos2d这样的比较成熟的2D引擎解决方案。SpriteKit展现出的是Apple将Xcode和iOS/Mac SDK打造成游戏引擎的野心,但是同时也确实与IDE有着更好的集成,减少了开发者的工作。”(http://onevcat.com/2013/06/sprite-kit-start/

那么对应我这款“跑酷好基友”游戏,“精灵”也就是“Sprite”则是上下跑动的两个小人,以及不断出现的障碍物。

 

2:创建一个Sprite Kit项目

很简单,在XCode中新建项目-->选择Sprite Kit模板-->填写项目名称等信息-->确定即可:(截图自XCode 5.1)

成功创建后我们将得到一个与singleView application结构很相似的项目,包含Appdelegate、ViewController以及一个叫MyScene的类。查看ViewController的viewDidLoad函数你会发现是如下结构:

 1 - (void)viewDidLoad
 2 {
 3     [super viewDidLoad];
 4 
 5     // Configure the view.
 6     SKView * skView = (SKView *)self.view;
 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 }

SKView:继承自UIView,这里作为viewController的view,他的作用是负责所有的动画和渲染;

SKScene:继承自SKEffectNode(:SKNode),作用是所有的“精灵”都会被放在一个SKScene实例上,再被加载在SKView上。一个SKView只能用拥有一个SKScene,但是你可以创建多个SKScene,在SKView上切换显示他们。

在模板帮我们实现好的MyScene中,我们可以看到一些操作,比如创建了一些文字用于显示、实现了SKScene的-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event函数,并创建了一些东西,我们可以猜测这是在实现玩家交互时的一些操作,下一节将介绍他们。

 

3:创建小精灵

3.0:要点介绍

查看SpriteKit.h我们会发现:

1 #import <SpriteKit/SKScene.h>
2 #import <SpriteKit/SKNode.h>
3 #import <SpriteKit/SKSpriteNode.h>
4 #import <SpriteKit/SKEmitterNode.h>
5 #import <SpriteKit/SKShapeNode.h>
6 #import <SpriteKit/SKEffectNode.h>
7 #import <SpriteKit/SKLabelNode.h>
8 #import <SpriteKit/SKVideoNode.h>
9 #import <SpriteKit/SKCropNode.h>

没错,非常多的Node。(详细官方介绍请看这里:https://developer.apple.com/library/ios/documentation/GraphicsAnimation/Conceptual/SpriteKit_PG/OtherNodeClasses/OtherNodeClasses.html#//apple_ref/doc/uid/TP40013043-CH10-SW1

SKNode:他是你在Sprite Kit中用到的最基本单元,上面各种Node(包括我们的SKScene)都是继承自SKNode。查看SKNode的接口文件,你会发现它具有:位置、缩放、速度等多种属性,也有添加、删除、运行各种“Action”等诸多方法(函数)。

聪明的你一定猜到了,我们的跑酷小人,就是一个SKNode,而且准确的说,是一个SKSpriteNode(SKSpriteNode拥有更多SKNode所没有的方法,而这些方法正是我游戏中跑酷小人所需要的)。

SKTexture:SKTexture是一个可以从图片文件读取的类型,我们可以通过SKTexture来读取图片从而创建一个SKSpriteNode,或者用于给SKSpriteNode创建动作时切换图片,比如我们这里就用了一个小人的图片去创建了一个SKSpriteNode。这样,在未给这个SKSpriteNode制定任何Action(动作)时,SKSpriteNode就是一个静态的小人。

SKTextureAtlas:Atlas顾名思义就是“图集”的意思,使用SKTextureAtlas从bundle中读取多张图片,这些图片会以某种“格式”(不清楚是否为一个个SKTexture)存储在SKTextureAtlas实例中,每次你需要通过图片名称就可以从其中取出一个SKTexture类型的实例,这样你就可以用这个SKTexture,创建或修改SKSpriteNode了。另外,因为SKTextureAtlas本身内部并不能直接获取到存有SKTexture元素的数组(只能获取到SKTexture名称数组),所以通常我们又会将SKTextureAtlas读取到的图片全部以SKTexture形式逐一取出来,存放在自己定义的数组中,便于后期使用Action(动作)操作时使用。

到此,我介绍了SKView、SKScene、SKSpriteNode、SKTextureAtlas、SKTexture,我将使用这五元虎将构成我们的静态界面。

3.1,创建SKView

我将storyboard删除,然后使用自己创建的SKView。

1 @property (nonatomic, strong)SKView *skView;
1 self.skView = [[SKView alloc] initWithFrame:self.view.bounds];
2 [self.view addSubview:self.skView];

3.2,创建SKScene

沿用官方模板中的MyScene类(里面的内容完全替换,稍后再做说明)

1 @property (nonatomic, strong)MyScene *scene;
1 self.scene = [MyScene sceneWithSize:self.skView.bounds.size];
2 self.scene.scaleMode = SKSceneScaleModeAspectFill;
3 self.scene.delegate = self;
4     
5 // Present the scene.
6 [self.skView presentScene:self.scene];

3.3,创建静态的小精灵(用SKTextureAtlas、SKTexture创建SKSpriteNode)

 有一些游戏(动画)开发经验的都应该知道,很多时候,一个物体的动作是可以分解为一帧一帧的图片的,而SKTextureAtlas读取的正是由美术设计师做出来的多幅连贯的图片。例如说一个跑的动作,我这里分解为20幅图片,并按顺序编号命名,最后加入到项目中:

注意!文件夹的命名方式,应以“.atlas”结尾。

正如3.0中我所写到的,我们要用SKTextureAtlas读取图片,再以SKTexture格式保存在自定义数组中,并用其中一个SKTexture初始化SKSpriteNode。于是:

1 @property (nonatomic, strong) SKSpriteNode *upBaby;
2 @property (nonatomic, strong) NSMutableArray *babyRunFrames
 1     //
 2     SKTextureAtlas *babyRunAnimatedAtlas = [SKTextureAtlas atlasNamed:@"babyRun"];
 3     /* 创建存放每一帧图片的数组 */
 4     self.babyRunFrames = [NSMutableArray array];
 5     /* 将每一帧图片加进数组 */
 6     for (int i=1; i <= babyRunAnimatedAtlas.textureNames.count; i++) {
 7         NSString *textureName = [NSString stringWithFormat:@"r%d@2x.png", i];
 8         SKTexture *temp = [babyRunAnimatedAtlas textureNamed:textureName];
 9         [self.babyRunFrames addObject:temp];
10     }
1     SKTexture *temp = self.babyRunFrames[0];
2     self.upBaby = [SKSpriteNode spriteNodeWithTexture:temp];
3     self.upBaby.position = CGPointMake(BABY_X_POSITION, self.size.height/2+30);
4     [self addChild:self.upBaby];

最后,将创建好的SKSpriteNode,用addChild方法加到SKScene中。(请暂时忽略摆放的位置:position,稍后将做说明)

 

4,让小精灵动起来

4.1:SKAction

Sprite Kit中管理小精灵动作的类是SKAction。

打开SKAction.h文件你马上就会发现有很多很多种“Action”,而且通俗易懂,例如:

1 + (SKAction *)moveByX:(CGFloat)deltaX y:(CGFloat)deltaY duration:(NSTimeInterval)sec;
1 + (SKAction *)rotateByAngle:(CGFloat)radians duration:(NSTimeInterval)sec;
1 + (SKAction *)resizeByWidth:(CGFloat)width height:(CGFloat)height duration:(NSTimeInterval)duration;
1 + (SKAction *)scaleBy:(CGFloat)scale duration:(NSTimeInterval)sec;
1 + (SKAction *)repeatAction:(SKAction *)action count:(NSUInteger)count;

太棒了,是不是马上就能用了?等等,在此之前,我们至少应该先来看一下如何运行这些Action吧。

显而易见,运行的主体一定是SKSpriteNode,而且支持的方法如下:

1 - (void)runAction:(SKAction *)action;
2 - (void)runAction:(SKAction *)action completion:(void (^)())block;
3 - (void)runAction:(SKAction *)action withKey:(NSString *)key;

也就是说,SKSpriteNode可以用这些方法,来“运行”创建好的SKAction。

而更重要的是,运行的方式也分为两种,一种是“序列式”,一种是“混合式”:

1 + (SKAction *)sequence:(NSArray *)actions;
2 
3 + (SKAction *)group:(NSArray *)actions;

通俗易懂,very nice!

4.2 为SKSpriteNode添加Action!

我们需要让小精灵SKSpriteNode在刚一出现的时候,就开始“跑步”。因此,我们要向它添加跑步的Action!

现在来分析一下。跑,其实是原地进行的一种动作,因为实际上是障碍物从远方移动过来,因此我们用:

1 [SKAction animateWithTextures:self.babyRunFrames
2                           timePerFrame:0.05f
3                                      resize:NO
4                                     restore:YES]

来从数组中获取每一个动作的图片(SKTexture),设置他们每0.05秒切换到下一张,以此来创建一个SKAction。

再用repeatActionForever,来表明这是一个无限循环的Action:

1 SKAction *runAction = [SKAction repeatActionForever:
2                            [SKAction animateWithTextures:self.babyRunFrames
3                                             timePerFrame:0.05f
4                                                   resize:NO
5                                                  restore:YES]];
6 [self.upBaby runAction:runAction withKey:BABY_ACTION_RUN];

最后调用SKSpriteNode的runAction方法,来启动这个动作。此时“好基友”的其中一位,就开始在原地跑步了!

除了“原地跑”,我们应该还有“向上跳并下落”的动作,这里就要稍微复杂一些了。因为可以分析得出来,向上跳、向下落,是一个小精灵位置改变的过程,与此同时,“跳”的时候,本身小精灵应该也是有一系列动作的。因此,这里我用到了上面提到的两个“组合式动作”的方法:group和sequence。

另外,锦上添花的是,为了看起来自然,我让小精灵在调到顶端最高点时,hold住一个很小的时间间隔!用到了:

1 [SKAction waitForDuration:0.08];

这样一个方法。最终如下:

 1     // 删除跑的动画
 2     [self.upBaby removeAllActions];
 3     
 4     /* 跳的动画 */
 5     SKAction *jumpAction = [SKAction animateWithTextures:self.babyJumpFrames
 6                                             timePerFrame:0.04f
 7                                                   resize:NO
 8                                                  restore:YES];
 9     float duration = 0.8;
10     /* 向上移动的动画 */
11     SKAction *moveUpAction = [SKAction moveTo:CGPointMake(self.upBaby.position.x, self.upBaby.position.y+BABY_JUMP_HEIGHT) duration:duration/2-0.04];
12     SKAction *holdAction = [SKAction waitForDuration:0.08];
13     SKAction *moveDownAction = [SKAction moveTo:CGPointMake(self.upBaby.position.x, self.upBaby.position.y) duration:duration/2-0.04];
14     
15     [self.upBaby runAction:[SKAction group:@[jumpAction, [SKAction sequence:@[moveUpAction, holdAction, moveDownAction]]]] completion:^{
16         self.isUpBabyAction = NO;
17         /* 跑的动画 */
18         SKAction *runAction = [SKAction repeatActionForever:
19                                [SKAction animateWithTextures:self.babyRunFrames
20                                                 timePerFrame:0.05f
21                                                       resize:NO
22                                                      restore:YES]];
23         [self.upBaby runAction:runAction withKey:BABY_ACTION_RUN];
24     }];

首先,我删除了之前所有的动作(在这里就是“跑”的动作);然后我创建了四个动作:1,跳的动作、2,向上位移的动作、3,hold住0.08秒的动作、4,向下的动作;

接着,通过group和sequence将他们组合在一起;最后,在做完这一系列动作后的完成回调里,回复原先的跑的动作!!

4.3,接受用户点击事件

我们的需求,是玩家可以点击屏幕上任意一个地方,让小精灵跳起来。因此这里我们可以在SKScene(继承自UIResponder)的方法

1 -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;

中,处理点击事件,让小精灵跳起来(使用4.2中的跳的action)。非常简单的操作,我在这里就不详细说明了,详见文章开头链接的源代码。

 

5,碰撞检测

5.1 创建障碍物

我们的障碍物是一堵堵高矮不一的墙,思路是将这一堵堵墙设置为SKSpriteNode,并通过一个“定时”操作,不断地从右侧出现,移动到左侧。

 1 - (void)addObstacle:(NSString *)upOrDown{
 2     // 创建怪物Sprite
 3     int i = (arc4random() % 8) + 1;
 4     SKTexture *temp = self.obstacleFrames[i-1];
 5     SKSpriteNode *obstacle = [SKSpriteNode spriteNodeWithTexture:temp];
 6     if (!self.obstacleNodes) {
 7         self.obstacleNodes = [NSMutableArray array];
 8     }
 9     [self.obstacleNodes addObject:obstacle];
10     
11     CGFloat yPoint = 0.0f;
12     
13     if ([upOrDown intValue] == 1) {
14         //
15         yPoint = self.frame.size.height/2+obstacle.size.height/2+15;
16     }else{
17         yPoint = obstacle.size.height/2+15;
18     }
19     obstacle.position = CGPointMake(self.frame.size.width + obstacle.size.width/2, yPoint);
20     [self addChild:obstacle];
21     
22     // Create the actions
23     SKAction * actionMove = [SKAction moveTo:CGPointMake(-obstacle.size.width/2, yPoint) duration:1.3];
24     SKAction * actionMoveDone = [SKAction removeFromParent];
25     __weak MyScene *blockSelf = self;
26     [obstacle runAction:[SKAction sequence:@[actionMove, actionMoveDone]] completion:^{
27         [blockSelf.obstacleNodes removeObject:obstacle];
28         [obstacle removeFromParent];
29     }];
30     
31     obstacle.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:obstacle.size]; // 1
32     obstacle.physicsBody.dynamic = YES; // 2
33     obstacle.physicsBody.categoryBitMask = obstacleCategory; // 3
34     obstacle.physicsBody.contactTestBitMask = babyCategory; // 4
35     obstacle.physicsBody.collisionBitMask = 0; // 5
36 }

为了让游戏中障碍物的出现显得变化多端,我通过获取随机数来从存有障碍物SKTexture随机的拿到某个SKTexture,用它创建SKSpriteNode障碍物,并通过设置SKAction,让障碍物从右侧移动进来直到左侧移出屏幕,并在完成这一动作后,删除这一SKSpriteNode,以及它上面的动作。

5.2,定时刷新

Sprite Kit会在没一帧都调用

1 - (void)update:(NSTimeInterval)currentTime

这个方法。这里偷了个懒,1.0版本中的刷新方法使用了http://www.raywenderlich.com/zh-hans/51919/sprite-kit-%E5%85%A5%E9%97%A8%E6%95%99%E7%A8%8B 中的刷新原理,原文也说了,这是苹果官方范例游戏Adventure中的写法,读者可以借鉴一下。其实我也有另一种刷新方式,打算在1.1版本中尝试一下。

1.0版本的刷新方法,详情请看源代码吧,我在文章最开始已经做了github链接。

5.3,设置碰撞相关属性

首先设置两个种类,分别是两位“好基友”和一系列的障碍物:

1 static const uint32_t obstacleCategory    =  0x1 << 0;
2 static const uint32_t babyCategory        =  0x1 << 1;

接着设置没有重力的物理体系,以及回调对象:

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

为“好基友”设置相关碰撞属性:

1     self.upBaby.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:self.upBaby.size.width/2];
2     self.upBaby.physicsBody.dynamic = YES;
3     self.upBaby.physicsBody.categoryBitMask = babyCategory;
4     self.upBaby.physicsBody.contactTestBitMask = obstacleCategory;
5     self.upBaby.physicsBody.collisionBitMask = 0;
6     self.upBaby.physicsBody.usesPreciseCollisionDetection = YES;

1,为“小人”sprite创建物理外形;

2,将“小人”物理外形的dynamic(动态)属性置为YES。这表示它的移动不会被物理引擎所控制;

3,把“小人”物理外形的种类掩码设为刚刚定义的babyCategory;

4,当发生碰撞时,当前小人对象会通知它contactTestBitMask 这个属性所代表的category。这里应该把障碍物的种类掩码obstacleCategory

赋给它;

5,collisionBitMask这个属性表示哪些种类的对象与当前小人对象相碰撞时物理引擎要让其有所反应(比如回弹效果)。你并不想让小人和障碍物彼此之间发生回弹,设置这个属性为0吧。当然这在其他游戏里是可能的。

6,usesPreciseCollisionDetection属性设置为YES。这对于快速移动的物体非常重要(例如炮弹),如果不这样设置的话,有可能快速移动的两个物体会直接相互穿过去,而不会检测到碰撞的发生。

(以上注释借鉴了:http://www.raywenderlich.com/zh-hans/51919/sprite-kit-%E5%85%A5%E9%97%A8%E6%95%99%E7%A8%8B

另外,在5.1中,我们也对障碍物进行了响应的碰撞属性设置。

最后,一旦发生碰撞,就会通过回调反映出来:

1 - (void)didBeginContact:(SKPhysicsContact *)contact

从中我们可以分析出具体是哪一个“小人”与哪一个障碍物发生了碰撞,并作出“游戏结束”的通知。

 

参考资料:

http://onevcat.com/2013/06/sprite-kit-start/
http://blog.csdn.net/kobbbb/article/details/9093601
https://developer.apple.com/library/ios/documentation/GraphicsAnimation/Conceptual/SpriteKit_PG/Introduction/Introduction.html
http://www.raywenderlich.com/zh-hans/51919/sprite-kit-%E5%85%A5%E9%97%A8%E6%95%99%E7%A8%8B
http://beyondvincent.com/blog/2013/10/12/114-spritekit-tutorial-for-beginners-3/

posted @ 2014-09-15 23:43  pigpigdaddy  阅读(1256)  评论(0编辑  收藏  举报