如何使用Box2D和Cocos2D制作一款像Fruit Ninja一样的游戏-第3部分
2013-03-22 08:50
691 查看
本篇教程是由iOS教程组的成员Allen Tan发布的,Allen是一位iOS开发者和White Widget的创始人。
欢迎来到系列教程的第3部分,本系列教程将教你如何制作一款类似Halfbrick Studios公司出品的水果忍者的游戏。
在第1部分中,你学会了如何制作一个纹理多边形,并基于它制作了一个西瓜。
在第2部分中,你学会了如何使用Box2D
Ray Casting 和一些数学方法来切割纹理多边形。
在本篇同时也是最后一部分中,你将把上一篇结束时的工程通过加入gameplay,特效和音效让其变得羽翼丰满。
另外,如果你是刚刚接触Cocos2D 和 Box2D的话,请先学习本网站的Cocos2D入门和Box2D入门。
另外,如果你还没有本教程的所用资源,请先下载它。你稍后将要在这个工程中添加很cool的特效!
所以此时你在工程里应该有:香蕉(Banana),葡萄(Grapes),菠萝(Pineapple),草莓(Strawberry),和西瓜(Watermelon)等水果了。
切换到PolygonSprite.h并做如下修改:
然后切换到PolygonSprite.mm并作如下修改:
你在PolygonSprite中添加一个type属性用来区分这些子类。接下来,你为每种水果分别添加了state属性。一个idle(空闲)的state意味着水果可以被向上抛,另外tossed(被抛)state意味着水果还在屏幕中运动的过程中呢。
PolygonSprites中的body对象设成sensors,这意味着Box2D只会检测这些body的碰撞而不会实际作用这些碰撞。当你把一个水果从底部抛向空中时,你并不想让他们在下落时互相碰撞,因为玩家很有可能还没看见它们就输掉了。
接下来,作如下修改:
切换回HelloWorldLayer.mm,并作如下修改:
你为每一种PloygonSprite的子类都赋予一种type,在游戏中预先创建水果(每种10个),另外3个炸弹。你并不想让它们立刻显示,所以先把它们放到屏幕之外。
编译并运行,不会看到有水果显示出来。
在游戏中,水果从屏幕下方被抛起来。我们可以采取同时或者一个接一个的向上抛的方式,对每一次抛的间隔,水果的数量,位置,高度和方向都做一些随机。
这些随机特性会让游戏变得更有趣。
切换回HelloWorldLayer.h并作如下修改:
然后,切换到HelloWorldLayer.mm并作如下修改:
你定义了方法用来输出在固定范围内的随机float和integer,并对上文提到的两种抛水果的方式定义了type。
接下来,你定义了以下游戏逻辑的变量,它们是:
nextTossTime: 这是下一次水果被抛起的时间,可以是一个或者一组水果。它总是和CACurrentMediaTime()做比较,你将其初始化为当前时间加1秒,这样在游戏开始时不会没有任何缓冲时间地马上开始抛水果。
tossInterval: 这是两次抛水果的时间间隔(秒)。在每次抛水果时,你都把这个值加到nextTossTime上。
queuedForToss: 此值表示在当前的抛水果类型中,还需要被抛的水果的随机数量。
currentTossType: 当前抛水果的类型。在simultaneous(同时) 和 consecutive(顺序)中随机选一个。
还在HelloWorldLayer.mm中,添加方法:
这个方式赋予在屏幕下方的sprite一个随机位置,并计算出一个随机的速度。min和max根据当前位置限制速度,让sprite不会太偏左,也不会太偏右。
这些值大多都是试出来的。如果sprite在最左边,速度的x在-25到75能够保持sprite仍然在屏幕范围内。如果sprite在中间,那么-50到50就可以满足了,其他情况类似。
在计算过所有的随机值后,将state设置为kStateTossed表示sprite已经被抛起了,同时启动sprite的collision mask并设置初始速度。
我在之前说正被抛到空中的sprite并不和正在下落的sprite碰撞,所以你一定会奇怪为什么我们这里调用activateCollisions。这是因为这个方法只是设置sprite的body的category和mask bits,并不会改变它是sensor的事实。
改变这些bits很重要,因为当sprite被切割后,新的形状就不再是sensor了,同时它们会继承原sprite的以上这些属性。
这个方法已经给予了每个水果随机位置和随机速度,所以接下来的逻辑就是创建每两次抛水果的随机间隔时间了。
添加方法到HelloWorldLayer.mm中:
这里发生了很多事,让我们分解成详细步骤看看:
阶段 1: 通过比较当前时间和nextTossTime,检查是否到了下一次抛水果的时间。
阶段 2: 如果在consecutive模式中还有在队列中的水果等待被抛起,那么抛起它并直接进入阶段6.
阶段 3: 从consecutive和simultaneous抛水果模式中选择其一,并设置一个被抛弃的水果的数量。
阶段 4: 同时抛起随机数量的水果。注意水果tpye的范围从0到4因为你并不想包含Bomb(炸弹)类型。
阶段 5: 与阶段2类似。检测如果是consecutive模式,就抛起第一个水果并进入阶段6.
阶段 6: 设置两次抛水果的间隔时间。当所有的水果都抛完后,你随机取一个较长的间隔时间,否则,说明你当前处在consecutive模式,那么就随机取一个较短的间隔时间。
把这个方法添加到update方法中来循环执行。在HelloWorldLayer.mm中,添加下边的一行到update方法中:
在启动游戏之前还需要做一件事。由于我们的sprite从屏幕下方被抛起,并最终落回屏幕,你应该移除被默认创建的墙。仍然在HelloWorldLayer.mm,作如下修改:
除了要移除所有的物理墙之外,你还把重力修改的更弱因为你并不希望sprite下落的太快。
编译并运行,你会看到你的水果正在上升和下落!
在游戏运行的过程中,你会发现3个问题。
最终由于cache中没有任何对象,你也没有重设水果的状态,抛水果的动作会停下来
你切割的次数越多,游戏运行效率就越低。这是因为你没有及时清除被切割的已经落到屏幕外的水果碎片,Box2D仍然在一直模拟它们。
当你切割水果后,新的碎片是粘在一起的,这是因为你只是简单的把水果分隔成两部分,而没有强行的将它们分开。
我们这就修正这些问题,在HelloWorldLayer.mm中作如下修改:
这里引入了状态处理。sprite初始为idle(空闲)状态,紧接着toss方法会改变其状态。toss方法只改变idle状态的sprite,最后你把被切割的原sprite的状态还原回idle。
在cleanSprites方法中,首先检查所有的原sprite是否是掉落到屏幕以外,如果是,就在向上抛之前重置它们的状态。接下来检查所有的被切割的碎片是否在屏幕以外,如果是,就销毁它的Box2D body并将其从场景中移除。
切换到HelloWorldLayer.h,在#define random_range(low,high)行之后添加以下内容:
切换回HelloWorldLayer.mm并对splitPolygonSprite方法作如下修改:
通过施加某种力改变对象的方向和速度,这样被切割时分成的两片就不会贴在一起了。
为了得到方向,你需要计算得到切割线两端的世界坐标和切割线的角度,再计算得到两个垂直此线的标准向量。所有的Box2D角度单位都是按弧度计算的,所以1.570796正好是角度的90的。
接下来,你得到切割线的中心坐标,以此来作为推力的作用点。
参考下面图示:
为了把两片sprite推开,你对它们分别施加了linear impulse(线性冲量),作用点为线段中心,方向相反。此冲量基于每个body的质量,所以两个物体所受的推力基本上是一致的。更大的sprite会得到更大的冲量,更小的sprite会得到更小的冲量。
编译并运行,这次水果被切割的感觉就很不错了,同时游戏可以无尽的玩下去。
添加计分系统/h2>
切换回HelloWorldLayer.mm并作如下修改:
在interface部分,你设置了切割次数和命的数量。同时你还声明了一个label用来显示玩家当前的分数。
initHUD方法在屏幕的左上角创建了3个标记用来显示玩家的命数。它还放置了一张图片代表分数,分数本身也显示在左上角。
subtractLife方法在标记命数的位置叠加一个新的标记图片,用来代表生命损失,每当此方法被调用,它还检查玩家当前是否还有足够的命数,如果没有,游戏就应该结束了。
endGame方法首先移除对update的schedule以停止游戏逻辑,然后在屏幕中添加restart按钮,如果此按钮被点击,那么游戏就会重新开始。
restart方法只是简单地重新加载场景,并回到游戏的最初的状态。
现在已经创建了所有的方法和变量,是时候把它们加到游戏逻辑中了。
还是在HelloWorldLayer.mm中,作如下修改:
当一个多边形被成功切割后,分数会增加,显示分数的label会更新。如果有没被切割过的原sprite落到屏幕下,就减少玩家的1条命。
编译并运行,这个游戏接近完成了!
让游戏更有挑战为了让游戏更有趣,你要添加一些炸弹到游戏中。在之前你已经初始化了3颗炸弹了,但目前还没有用到它们。
炸弹是独立的,它们可以在任何时间被抛起。如果一个玩家不小心划到了一颗炸弹,它会爆炸并且减少玩家1条命。
对HelloWorldLayer.mm作如下修改:
这段代码直截了当,和之前和对水果所做的类似。首先为炸弹添加抛的机制,但是这次并不计算抛的类型和已经有多少炸弹正在空中。
炸弹随机被抛起。这里使用一个随机值模8,如果结果等于0,就抛之,这里只有1/8的机会在每次间隔时抛起炸弹。
然后在splitPolygonSprite方法检查sprite是否被切割的位置同样检查炸弹,如果划到了一颗炸弹,那么就调用subtractLife减少玩家1条命。
编译并运行,炸弹就有啦!
你可以使用粒子系统丰富场景。粒子系统允许你使用大量的使用一个sprite的小对象。Cocos2D已经包含了可自定义的粒子系统,配合Particle Designer工具,可以可视化的创建粒子。
在Particle Designer中创建粒子很简单,简单到甚至不用在本教程中提及。作为替代,我已经为你创建好了你需要用到的粒子。Particle Designer将粒子导出为PLIST格式,你所需要做的就是在Cocos2D中加载它们。
如果你还没有,请先下载本教程的资源,在Xcode的Project Navigator中,右键点击Resources并选择“Add Files To CutCutCut”。添加Particles文件夹到项目中。你在做这一步操作时,同样添加Sounds文件夹到工程中。确保“Copy items into destination group’s folder”和“Create
groups for any added folders”是选中的。
以下是你需要添加到项目中的粒子文件:
banana_splurt.plist
blade_sparkle.plist
explosion.plist
grapes_splurt.plist
pineapple_splurt.plist
strawberry_splurt.plist
sun_pollen.plist
watermelon_splurt.plist
以上其中的5个是喷射的,可以称其为“splurt”,它们针对每一种水果被切割时的特效。一种炸弹爆炸时的爆炸粒子。一个跟随刀刃移动的闪光效果,和一个背景上的微尘花粉效果。
切换到HelloWorldLayer.h并在@interface中加入以下内容:
接下来,再切换到HelloWorldLayer.mm,并作如下修改:
你添加了微尘花粉特效到背景中,并跟随玩家的触摸添加闪光特效。
调用stopSystem会停止粒子系统继续喷射粒子,调用resetSystem可以重新让粒子系统喷射粒子。所有的这些粒子都是无尽的,直到你调用stopSystem为止,它们都不会停止。
接下来是喷射和爆炸特效,对PolygonSprite.h作如下修改:
切换到PolygonSprite.mm,在@implementation中添加以下内容:
接下来,对PolygonSprite的子类作如下修改(水果和炸弹):
你为每一种类型的PolygonSprite添加对应的粒子系统。
切换回HelloWorldLayer.mm并作如下修改:
在initSprite方法中把所有的粒子添加到游戏层中。另外,当水果或者炸弹被切割时,你在切割线的中间位置创建一个粒子特效。
编译并运行,粒子满天飞!
我们的声音特效不仅仅有助于愉悦心情,同时还能让玩家用来区分游戏里的各种事件。
添加resources文件夹中的Sounds文件夹到你的Xcode工程中。这里边包含了以下几个事件的声音:
炸弹爆炸
炸弹被抛起
水果按顺序的被抛起
水果同时被抛起
玩家损失一条命
玩家切割水果分隔成小块儿时
玩家重复的切割一个水果
玩家做出轻扫手势时
背景自然音效
切换到HelloWorldLayer.h并作如下修改:
再切换回HelloWorldLayer.mm,并作如下修改:
除了普通的游戏声音的代码外,你还考虑了时间因素,基于距离/时间的公式,我们只在玩家手指很快滑动的时候才播放swoosh的音效。同时,你保存了一个swoosh音效的指针,只在它没有播放的时候播放它。
你胜利了!恭喜,你已经制作了一款完整的iphone版切水果游戏!
当然,你还可以再改进此游戏。以下是一些能让游戏更好玩儿更有趣的改进点:
支持凹多边形,你需要使用三角计算法(把一个凹多边形分成多个凸多边形)。
让多边形的顶点支持多余8个。
让PolygonSprite支持使用batch nodes提升效率。
支持多点触摸和滑动。
支持iPad。
为polygon做缓存,这样可以使所用东西被重用。这能够有效提升效率。
为切割添加追尾彗星效果,并在连续切割时给予玩家额外奖励分数。
当特殊水果被切割时触发事件。
更好的随机抛水果的机制,比如从侧面抛出水果。
如果你让游戏更cool的点子,或者对此游戏有什么问题和评论,欢迎到下面的讨论区讨论!
欢迎来到系列教程的第3部分,本系列教程将教你如何制作一款类似Halfbrick Studios公司出品的水果忍者的游戏。
在第1部分中,你学会了如何制作一个纹理多边形,并基于它制作了一个西瓜。
在第2部分中,你学会了如何使用Box2D
Ray Casting 和一些数学方法来切割纹理多边形。
在本篇同时也是最后一部分中,你将把上一篇结束时的工程通过加入gameplay,特效和音效让其变得羽翼丰满。
另外,如果你是刚刚接触Cocos2D 和 Box2D的话,请先学习本网站的Cocos2D入门和Box2D入门。
准备工作
我们需要使用上一部分结束的工程,所以确保你已经有了第2部分的工程。另外,如果你还没有本教程的所用资源,请先下载它。你稍后将要在这个工程中添加很cool的特效!
向上抛水果
到目前为止你只在屏幕上画了一些静止的水果。在你加入“向上抛”这个机制前,你必须有不同种类的水果。如果你还没有准备好这些水果的类,那么你可以到resources的Classes文件夹里找到它们。所以此时你在工程里应该有:香蕉(Banana),葡萄(Grapes),菠萝(Pineapple),草莓(Strawberry),和西瓜(Watermelon)等水果了。
切换到PolygonSprite.h并做如下修改:
// Add to top of file typedef enum _State { kStateIdle = 0, kStateTossed } State; typedef enum _Type { kTypeWatermelon = 0, kTypeStrawberry, kTypePineapple, kTypeGrapes, kTypeBanana, kTypeBomb } Type; // Add inside @interface State _state; Type _type; // Add after @interface @property(nonatomic,readwrite)State state; @property(nonatomic,readwrite)Type type; |
// Add inside @implementation @synthesize state = _state; @synthesize type = _type; // Add inside the if statement of initWithTexture _state = kStateIdle; // Add inside createBodyForWorld, right after setting the maskBits of the fixture definition fixtureDef.isSensor = YES; |
PolygonSprites中的body对象设成sensors,这意味着Box2D只会检测这些body的碰撞而不会实际作用这些碰撞。当你把一个水果从底部抛向空中时,你并不想让他们在下落时互相碰撞,因为玩家很有可能还没看见它们就输掉了。
接下来,作如下修改:
// Add inside the if statement of Banana.mm self.type = kTypeBanana; // Add inside the if statement of Bomb.mm self.type = kTypeBomb; // Add inside the if statement of Grapes.mm self.type = kTypeGrapes; // Add inside the if statement of Pineapple.mm self.type = kTypePineapple; // Add inside the if statement of Strawberry.mm self.type = kTypeStrawberry; // Add inside the if statement of Watermelon.mm self.type = kTypeWatermelon; |
// Add to top of file #import "Strawberry.h" #import "Pineapple.h" #import "Grapes.h" #import "Banana.h" #import "Bomb.h" // Replace the initSprites method -(void)initSprites { _cache = [[CCArray alloc] initWithCapacity:53]; for (int i = 0; i < 10; i++) { PolygonSprite *sprite = [[Watermelon alloc] initWithWorld:world]; sprite.position = ccp(-64*(i+1),-64); [self addChild:sprite z:1]; [_cache addObject:sprite]; } for (int i = 0; i < 10; i++) { PolygonSprite *sprite = [[Strawberry alloc] initWithWorld:world]; sprite.position = ccp(-64*(i+1),-64); [self addChild:sprite z:1]; [_cache addObject:sprite]; } for (int i = 0; i < 10; i++) { PolygonSprite *sprite = [[Pineapple alloc] initWithWorld:world]; sprite.position = ccp(-64*(i+1),-64); [self addChild:sprite z:1]; [_cache addObject:sprite]; } for (int i = 0; i < 10; i++) { PolygonSprite *sprite = [[Grapes alloc] initWithWorld:world]; sprite.position = ccp(-64*(i+1),-64); [self addChild:sprite z:1]; [_cache addObject:sprite]; } for (int i = 0; i < 10; i++) { PolygonSprite *sprite = [[Banana alloc] initWithWorld:world]; sprite.position = ccp(-64*(i+1),-64); [self addChild:sprite z:1]; [_cache addObject:sprite]; } for (int i = 0; i < 3; i++) { PolygonSprite *sprite = [[Bomb alloc] initWithWorld:world]; sprite.position = ccp(-64*(i+1),-64); [self addChild:sprite z:1]; [_cache addObject:sprite]; } } |
编译并运行,不会看到有水果显示出来。
在游戏中,水果从屏幕下方被抛起来。我们可以采取同时或者一个接一个的向上抛的方式,对每一次抛的间隔,水果的数量,位置,高度和方向都做一些随机。
这些随机特性会让游戏变得更有趣。
切换回HelloWorldLayer.h并作如下修改:
// Add to top of file, below the calculate_determinant definition #define frandom (float)arc4random()/UINT64_C(0x100000000) #define frandom_range(low,high) ((high-low)*frandom)+low #define random_range(low,high) (arc4random()%(high-low+1))+low typedef enum _TossType { kTossConsecutive = 0, kTossSimultaneous }TossType; // Add inside the @interface double _nextTossTime; double _tossInterval; int _queuedForToss; TossType _currentTossType; |
// Add inside the init method _nextTossTime = CACurrentMediaTime() + 1; _queuedForToss = 0; |
接下来,你定义了以下游戏逻辑的变量,它们是:
nextTossTime: 这是下一次水果被抛起的时间,可以是一个或者一组水果。它总是和CACurrentMediaTime()做比较,你将其初始化为当前时间加1秒,这样在游戏开始时不会没有任何缓冲时间地马上开始抛水果。
tossInterval: 这是两次抛水果的时间间隔(秒)。在每次抛水果时,你都把这个值加到nextTossTime上。
queuedForToss: 此值表示在当前的抛水果类型中,还需要被抛的水果的随机数量。
currentTossType: 当前抛水果的类型。在simultaneous(同时) 和 consecutive(顺序)中随机选一个。
还在HelloWorldLayer.mm中,添加方法:
-(void)tossSprite:(PolygonSprite*)sprite { CGSize screen = [[CCDirector sharedDirector] winSize]; CGPoint randomPosition = ccp(frandom_range(100, screen.width-164), -64); float randomAngularVelocity = frandom_range(-1, 1); float xModifier = 50*(randomPosition.x - 100)/(screen.width - 264); float min = -25.0 - xModifier; float max = 75.0 - xModifier; float randomXVelocity = frandom_range(min,max); float randomYVelocity = frandom_range(250, 300); sprite.state = kStateTossed; sprite.position = randomPosition; [sprite activateCollisions]; sprite.body->SetLinearVelocity(b2Vec2(randomXVelocity/PTM_RATIO,randomYVelocity/PTM_RATIO)); sprite.body->SetAngularVelocity(randomAngularVelocity); } |
这些值大多都是试出来的。如果sprite在最左边,速度的x在-25到75能够保持sprite仍然在屏幕范围内。如果sprite在中间,那么-50到50就可以满足了,其他情况类似。
在计算过所有的随机值后,将state设置为kStateTossed表示sprite已经被抛起了,同时启动sprite的collision mask并设置初始速度。
我在之前说正被抛到空中的sprite并不和正在下落的sprite碰撞,所以你一定会奇怪为什么我们这里调用activateCollisions。这是因为这个方法只是设置sprite的body的category和mask bits,并不会改变它是sensor的事实。
改变这些bits很重要,因为当sprite被切割后,新的形状就不再是sensor了,同时它们会继承原sprite的以上这些属性。
这个方法已经给予了每个水果随机位置和随机速度,所以接下来的逻辑就是创建每两次抛水果的随机间隔时间了。
添加方法到HelloWorldLayer.mm中:
-(void)spriteLoop { double curTime = CACurrentMediaTime(); //step 1 if (curTime > _nextTossTime) { PolygonSprite *sprite; int random = random_range(0, 4); //step 2 Type type = (Type)random; if (_currentTossType == kTossConsecutive && _queuedForToss > 0) { CCARRAY_FOREACH(_cache, sprite) { if (sprite.state == kStateIdle && sprite.type == type) { [self tossSprite:sprite]; _queuedForToss--; break; } } } else { //step 3 _queuedForToss = random_range(3, 8); int tossType = random_range(0,1); _currentTossType = (TossType)tossType; //step 4 if (_currentTossType == kTossSimultaneous) { CCARRAY_FOREACH(_cache, sprite) { if (sprite.state == kStateIdle && sprite.type == type) { [self tossSprite:sprite]; _queuedForToss--; random = random_range(0, 4); type = (Type)random; if (_queuedForToss == 0) { break; } } } } //step 5 else if (_currentTossType == kTossConsecutive) { CCARRAY_FOREACH(_cache, sprite) { if (sprite.state == kStateIdle && sprite.type == type) { [self tossSprite:sprite]; _queuedForToss--; break; } } } } //step 6 if (_queuedForToss == 0) { _tossInterval = frandom_range(2,3); _nextTossTime = curTime + _tossInterval; } else { _tossInterval = frandom_range(0.3,0.8); _nextTossTime = curTime + _tossInterval; } } } |
阶段 1: 通过比较当前时间和nextTossTime,检查是否到了下一次抛水果的时间。
阶段 2: 如果在consecutive模式中还有在队列中的水果等待被抛起,那么抛起它并直接进入阶段6.
阶段 3: 从consecutive和simultaneous抛水果模式中选择其一,并设置一个被抛弃的水果的数量。
阶段 4: 同时抛起随机数量的水果。注意水果tpye的范围从0到4因为你并不想包含Bomb(炸弹)类型。
阶段 5: 与阶段2类似。检测如果是consecutive模式,就抛起第一个水果并进入阶段6.
阶段 6: 设置两次抛水果的间隔时间。当所有的水果都抛完后,你随机取一个较长的间隔时间,否则,说明你当前处在consecutive模式,那么就随机取一个较短的间隔时间。
把这个方法添加到update方法中来循环执行。在HelloWorldLayer.mm中,添加下边的一行到update方法中:
[self spriteLoop]; |
// In the initPhysics method, replace gravity.Set(0.0f, -10.0f) with gravity.Set(0.0f, -4.25f); // Comment out or remove the following code from the initPhysics method // bottom groundBox.Set(b2Vec2(0,0), b2Vec2(s.width/PTM_RATIO,0)); groundBody->CreateFixture(&groundBox,0); // top groundBox.Set(b2Vec2(0,s.height/PTM_RATIO), b2Vec2(s.width/PTM_RATIO,s.height/PTM_RATIO)); groundBody->CreateFixture(&groundBox,0); // left groundBox.Set(b2Vec2(0,s.height/PTM_RATIO), b2Vec2(0,0)); groundBody->CreateFixture(&groundBox,0); // right groundBox.Set(b2Vec2(s.width/PTM_RATIO,s.height/PTM_RATIO), b2Vec2(s.width/PTM_RATIO,0)); groundBody->CreateFixture(&groundBox,0); |
编译并运行,你会看到你的水果正在上升和下落!
在游戏运行的过程中,你会发现3个问题。
最终由于cache中没有任何对象,你也没有重设水果的状态,抛水果的动作会停下来
你切割的次数越多,游戏运行效率就越低。这是因为你没有及时清除被切割的已经落到屏幕外的水果碎片,Box2D仍然在一直模拟它们。
当你切割水果后,新的碎片是粘在一起的,这是因为你只是简单的把水果分隔成两部分,而没有强行的将它们分开。
我们这就修正这些问题,在HelloWorldLayer.mm中作如下修改:
// Add inside the splitPolygonSprite method, right before [sprite deactivateCollisions] sprite.state = kStateIdle; // Add this method -(void)cleanSprites { PolygonSprite *sprite; //we check for all tossed sprites that have dropped offscreen and reset them CCARRAY_FOREACH(_cache, sprite) { if (sprite.state == kStateTossed) { CGPoint spritePosition = ccp(sprite.body->GetPosition().x*PTM_RATIO,sprite.body->GetPosition().y*PTM_RATIO); float yVelocity = sprite.body->GetLinearVelocity().y; //this means the sprite has dropped offscreen if (spritePosition.y < -64 && yVelocity < 0) { sprite.state = kStateIdle; sprite.sliceEntered = NO; sprite.sliceExited = NO; sprite.entryPoint.SetZero(); sprite.exitPoint.SetZero(); sprite.position = ccp(-64,-64); sprite.body->SetLinearVelocity(b2Vec2(0.0,0.0)); sprite.body->SetAngularVelocity(0.0); [sprite deactivateCollisions]; } } } //we check for all sliced pieces that have dropped offscreen and remove them CGSize screen = [[CCDirector sharedDirector] winSize]; for (b2Body* b = world->GetBodyList(); b; b = b->GetNext()) { if (b->GetUserData() != NULL) { PolygonSprite *sprite = (PolygonSprite*)b->GetUserData(); CGPoint position = ccp(b->GetPosition().x*PTM_RATIO,b->GetPosition().y*PTM_RATIO); if (position.x < -64 || position.x > screen.width || position.y < -64) { if (!sprite.original) { world->DestroyBody(sprite.body); [self removeChild:sprite cleanup:YES]; } } } } } // Add inside the update method, after [self checkAndSliceObjects] [self cleanSprites]; |
在cleanSprites方法中,首先检查所有的原sprite是否是掉落到屏幕以外,如果是,就在向上抛之前重置它们的状态。接下来检查所有的被切割的碎片是否在屏幕以外,如果是,就销毁它的Box2D body并将其从场景中移除。
切换到HelloWorldLayer.h,在#define random_range(low,high)行之后添加以下内容:
#define midpoint(a,b) (float)(a+b)/2 |
// Add to the top part inside of the if (sprite1VerticesAcceptable && sprite2VerticesAcceptable) statement b2Vec2 worldEntry = sprite.body->GetWorldPoint(sprite.entryPoint); b2Vec2 worldExit = sprite.body->GetWorldPoint(sprite.exitPoint); float angle = ccpToAngle(ccpSub(ccp(worldExit.x,worldExit.y), ccp(worldEntry.x,worldEntry.y))); CGPoint vector1 = ccpForAngle(angle + 1.570796); CGPoint vector2 = ccpForAngle(angle - 1.570796); float midX = midpoint(worldEntry.x, worldExit.x); float midY = midpoint(worldEntry.y, worldExit.y); // Add after [self addChild:newSprite1 z:1] newSprite1.body->ApplyLinearImpulse(b2Vec2(2*body1->GetMass()*vector1.x,2*body1->GetMass()*vector1.y), b2Vec2(midX,midY)); // Add after [self addChild:newSprite2 z:1] newSprite2.body->ApplyLinearImpulse(b2Vec2(2*body2->GetMass()*vector2.x,2*body2->GetMass()*vector2.y), b2Vec2(midX,midY)); |
为了得到方向,你需要计算得到切割线两端的世界坐标和切割线的角度,再计算得到两个垂直此线的标准向量。所有的Box2D角度单位都是按弧度计算的,所以1.570796正好是角度的90的。
接下来,你得到切割线的中心坐标,以此来作为推力的作用点。
参考下面图示:
为了把两片sprite推开,你对它们分别施加了linear impulse(线性冲量),作用点为线段中心,方向相反。此冲量基于每个body的质量,所以两个物体所受的推力基本上是一致的。更大的sprite会得到更大的冲量,更小的sprite会得到更小的冲量。
编译并运行,这次水果被切割的感觉就很不错了,同时游戏可以无尽的玩下去。
添加计分系统/h2>
如果游戏没有明确的目标和合理的结束的话,就不能称之为游戏,所以你需要在合适的地方添加一个计分系统。
你需要根据玩家切割水果的数量计算分数。你会跟玩家3条命,或者说3次机会,当没有被切过的水果飞出屏幕时,就减1条命。
切换到HelloWorldLayer.h并作如下修改:
// Add inside @interface int _cuts; int _lives; CCLabelTTF *_cutsLabel; |
// Add inside the init method, right after [self initSprites] [self initHUD]; // Add these methods -(void)initHUD { CGSize screen = [[CCDirector sharedDirector] winSize]; _cuts = 0; _lives = 3; for (int i = 0; i < 3; i++) { CCSprite *cross = [CCSprite spriteWithFile:@"x_unfilled.png"]; cross.position = ccp(screen.width - cross.contentSize.width/2 - i*cross.contentSize.width, screen.height - cross.contentSize.height/2); [self addChild:cross z:4]; } CCSprite *cutsIcon = [CCSprite spriteWithFile:@"fruit_cut.png"]; cutsIcon.position = ccp(cutsIcon.contentSize.width/2, screen.height - cutsIcon.contentSize.height/2); [self addChild:cutsIcon]; _cutsLabel = [CCLabelTTF labelWithString:@"0" fontName:@"Helvetica Neue" fontSize:30]; _cutsLabel.anchorPoint = ccp(0, 0.5); _cutsLabel.position = ccp(cutsIcon.position.x + cutsIcon.contentSize.width/2 + _cutsLabel.contentSize.width/2,cutsIcon.position.y); [self addChild:_cutsLabel z:4]; } -(void)restart { [[CCDirector sharedDirector] replaceScene:[HelloWorldLayer scene]]; } -(void)endGame { [self unscheduleUpdate]; CCMenuItemLabel *label = [CCMenuItemLabel itemWithLabel:[CCLabelTTF labelWithString:@"RESTART"fontName:@"Helvetica Neue"fontSize:50] target:self selector:@selector(restart)]; CCMenu *menu = [CCMenu menuWithItems:label, nil]; CGSize screen = [[CCDirector sharedDirector] winSize]; menu.position = ccp(screen.width/2, screen.height/2); [self addChild:menu z:4]; } -(void)subtractLife { CGSize screen = [[CCDirector sharedDirector] winSize]; _lives--; CCSprite *lostLife = [CCSprite spriteWithFile:@"x_filled.png"]; lostLife.position = ccp(screen.width - lostLife.contentSize.width/2 - _lives*lostLife.contentSize.width, screen.height - lostLife.contentSize.height/2); [self addChild:lostLife z:4]; if (_lives <= 0) { [self endGame]; } } |
initHUD方法在屏幕的左上角创建了3个标记用来显示玩家的命数。它还放置了一张图片代表分数,分数本身也显示在左上角。
subtractLife方法在标记命数的位置叠加一个新的标记图片,用来代表生命损失,每当此方法被调用,它还检查玩家当前是否还有足够的命数,如果没有,游戏就应该结束了。
endGame方法首先移除对update的schedule以停止游戏逻辑,然后在屏幕中添加restart按钮,如果此按钮被点击,那么游戏就会重新开始。
restart方法只是简单地重新加载场景,并回到游戏的最初的状态。
现在已经创建了所有的方法和变量,是时候把它们加到游戏逻辑中了。
还是在HelloWorldLayer.mm中,作如下修改:
// Add to the splitPolygonSprite method, inside the if (sprite1VerticesAcceptable && sprite2VerticesAcceptable) statement _cuts++; [_cutsLabel setString:[NSString stringWithFormat:@"%d",_cuts]]; // Add to the cleanSprites method, inside the if (spritePosition.y < -64 && yVelocity < 0) statement if (sprite.type != kTypeBomb) { [self subtractLife]; } |
编译并运行,这个游戏接近完成了!
让游戏更有挑战为了让游戏更有趣,你要添加一些炸弹到游戏中。在之前你已经初始化了3颗炸弹了,但目前还没有用到它们。
炸弹是独立的,它们可以在任何时间被抛起。如果一个玩家不小心划到了一颗炸弹,它会爆炸并且减少玩家1条命。
对HelloWorldLayer.mm作如下修改:
// Add to the spriteLoop method, inside if (curTime > _nextTossTime), right after PolygonSprite *sprite; int chance = arc4random()%8; if (chance == 0) { CCARRAY_FOREACH(_cache, sprite) { if (sprite.state == kStateIdle && sprite.type == kTypeBomb) { [self tossSprite:sprite]; break; } } } // Add to the splitPolygonSprite method, inside the if (sprite.original) statement if (sprite.type == kTypeBomb) { [self subtractLife]; } else { //placeholder } |
炸弹随机被抛起。这里使用一个随机值模8,如果结果等于0,就抛之,这里只有1/8的机会在每次间隔时抛起炸弹。
然后在splitPolygonSprite方法检查sprite是否被切割的位置同样检查炸弹,如果划到了一颗炸弹,那么就调用subtractLife减少玩家1条命。
编译并运行,炸弹就有啦!
使用粒子特效丰富游戏
游戏逻辑完成后,你可以集中精力打磨游戏了。你确实应该为游戏添加更多的活力。目前的切割先得很乏味,炸弹不会爆炸,背景也显得不够动态。你可以使用粒子系统丰富场景。粒子系统允许你使用大量的使用一个sprite的小对象。Cocos2D已经包含了可自定义的粒子系统,配合Particle Designer工具,可以可视化的创建粒子。
在Particle Designer中创建粒子很简单,简单到甚至不用在本教程中提及。作为替代,我已经为你创建好了你需要用到的粒子。Particle Designer将粒子导出为PLIST格式,你所需要做的就是在Cocos2D中加载它们。
如果你还没有,请先下载本教程的资源,在Xcode的Project Navigator中,右键点击Resources并选择“Add Files To CutCutCut”。添加Particles文件夹到项目中。你在做这一步操作时,同样添加Sounds文件夹到工程中。确保“Copy items into destination group’s folder”和“Create
groups for any added folders”是选中的。
以下是你需要添加到项目中的粒子文件:
banana_splurt.plist
blade_sparkle.plist
explosion.plist
grapes_splurt.plist
pineapple_splurt.plist
strawberry_splurt.plist
sun_pollen.plist
watermelon_splurt.plist
以上其中的5个是喷射的,可以称其为“splurt”,它们针对每一种水果被切割时的特效。一种炸弹爆炸时的爆炸粒子。一个跟随刀刃移动的闪光效果,和一个背景上的微尘花粉效果。
切换到HelloWorldLayer.h并在@interface中加入以下内容:
CCParticleSystemQuad *_bladeSparkle; |
// Add inside the init method _bladeSparkle = [CCParticleSystemQuad particleWithFile:@"blade_sparkle.plist"]; [_bladeSparkle stopSystem]; [self addChild:_bladeSparkle z:3]; // Add inside the initBackground method CCParticleSystemQuad *sunPollen = [CCParticleSystemQuad particleWithFile:@"sun_pollen.plist"]; [self addChild:sunPollen]; //Add inside ccTouchesBegan _bladeSparkle.position = location; [_bladeSparkle resetSystem]; //Add inside ccTouchesMoved _bladeSparkle.position = location; // Add inside ccTouchesEnded [_bladeSparkle stopSystem]; |
调用stopSystem会停止粒子系统继续喷射粒子,调用resetSystem可以重新让粒子系统喷射粒子。所有的这些粒子都是无尽的,直到你调用stopSystem为止,它们都不会停止。
接下来是喷射和爆炸特效,对PolygonSprite.h作如下修改:
// Add inside the @interface CCParticleSystemQuad *_splurt; // Add after the @interface @property(nonatomic,assign)CCParticleSystemQuad *splurt; |
@synthesize splurt = _splurt; |
// Add inside Banana.mm init right after setting the type self.splurt = [CCParticleSystemQuad particleWithFile:@"banana_splurt.plist"]; [self.splurt stopSystem]; // Add inside Bomb.mm init right after setting the type self.splurt = [CCParticleSystemQuad particleWithFile:@"explosion.plist"]; [self.splurt stopSystem]; // Add inside Grapes.mm init right after setting the type self.splurt = [CCParticleSystemQuad particleWithFile:@"grapes_splurt.plist"]; [self.splurt stopSystem]; // Add inside Pineapple.mm init right after setting the type self.splurt = [CCParticleSystemQuad particleWithFile:@"pineapple_splurt.plist"]; [self.splurt stopSystem]; // Add inside Strawberry.mm init right after setting the type self.splurt = [CCParticleSystemQuad particleWithFile:@"strawberry_splurt.plist"]; [self.splurt stopSystem]; // Add inside Watermelon.mm init right after setting the type self.splurt = [CCParticleSystemQuad particleWithFile:@"watermelon_splurt.plist"]; [self.splurt stopSystem]; |
切换回HelloWorldLayer.mm并作如下修改:
// Add this line per fruit and bomb in the initSprites method [self addChild:sprite.splurt z:3]; // Add inside the splitPolygonSprite method, inside the if (sprite.original) statement b2Vec2 convertedWorldEntry = b2Vec2(worldEntry.x*PTM_RATIO,worldEntry.y*PTM_RATIO); b2Vec2 convertedWorldExit = b2Vec2(worldExit.x*PTM_RATIO,worldExit.y*PTM_RATIO); float midX = midpoint(convertedWorldEntry.x, convertedWorldExit.x); float midY = midpoint(convertedWorldEntry.y, convertedWorldExit.y); sprite.splurt.position = ccp(midX,midY); [sprite.splurt resetSystem]; |
编译并运行,粒子满天飞!
免费的音乐和音效
你知道的,作为raywenderlich.com的游戏,没有丰富的音乐和音效是不行的! :]我们的声音特效不仅仅有助于愉悦心情,同时还能让玩家用来区分游戏里的各种事件。
添加resources文件夹中的Sounds文件夹到你的Xcode工程中。这里边包含了以下几个事件的声音:
炸弹爆炸
炸弹被抛起
水果按顺序的被抛起
水果同时被抛起
玩家损失一条命
玩家切割水果分隔成小块儿时
玩家重复的切割一个水果
玩家做出轻扫手势时
背景自然音效
切换到HelloWorldLayer.h并作如下修改:
// Add to top of file #import "SimpleAudioEngine.h" // Add inside the @interface float _timeCurrent; float _timePrevious; CDSoundSource *_swoosh; // Add after the @interface @property(nonatomic,retain)CDSoundSource *swoosh; |
// Add inside @implementation @synthesize swoosh = _swoosh; // Add inside the dealloc method, before [super dealloc] [_swoosh release]; // Add inside the init method [[SimpleAudioEngine sharedEngine] preloadEffect:@"swoosh.caf"]; [[SimpleAudioEngine sharedEngine] preloadEffect:@"squash.caf"]; [[SimpleAudioEngine sharedEngine] preloadEffect:@"toss_consecutive.caf"]; [[SimpleAudioEngine sharedEngine] preloadEffect:@"toss_simultaneous.caf"]; [[SimpleAudioEngine sharedEngine] preloadEffect:@"toss_bomb.caf"]; [[SimpleAudioEngine sharedEngine] preloadEffect:@"lose_life.caf"]; _swoosh = [[[SimpleAudioEngine sharedEngine] soundSourceForFile:@"swoosh.caf"] retain]; [[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"nature_bgm.aifc"]; _timeCurrent = 0; _timePrevious = 0; // Add inside the update method _timeCurrent += dt; // Add inside the spriteLoop method, after tossing the bomb [[SimpleAudioEngine sharedEngine] playEffect:@"toss_bomb.caf"]; // Add inside the spriteLoop method, for both the consecutive tosses [[SimpleAudioEngine sharedEngine] playEffect:@"toss_consecutive.caf"]; // Add inside the spriteLoop method, for the simultaneous toss [[SimpleAudioEngine sharedEngine] playEffect:@"toss_simultaneous.caf"]; // Add inside splitPolygon if sprite is a bomb [[SimpleAudioEngine sharedEngine] playEffect:@"explosion.caf"]; // Add inside splitPolygon if sprite is not a bomb [[SimpleAudioEngine sharedEngine] playEffect:@"squash.caf"]; // Add before destroying the body in the splitPolygonSprite method [[SimpleAudioEngine sharedEngine] playEffect:@"smallcut.caf"]; // Add inside the subtractLife method [[SimpleAudioEngine sharedEngine] playEffect:@"lose_life.caf"]; // Add inside ccTouchesMoved before setting _bladeSparkle.position = location ccTime deltaTime = _timeCurrent - _timePrevious; _timePrevious = _timeCurrent; CGPoint oldPosition = _bladeSparkle.position; // Add inside ccTouchesMoved after setting _bladeSparkle.position = location if (ccpDistance(_bladeSparkle.position, oldPosition) / deltaTime > 1000) { if (!_swoosh.isPlaying) { [_swoosh play]; } } |
你胜利了!恭喜,你已经制作了一款完整的iphone版切水果游戏!
何去何从?
这是到本系列教程完整的示例工程。当然,你还可以再改进此游戏。以下是一些能让游戏更好玩儿更有趣的改进点:
支持凹多边形,你需要使用三角计算法(把一个凹多边形分成多个凸多边形)。
让多边形的顶点支持多余8个。
让PolygonSprite支持使用batch nodes提升效率。
支持多点触摸和滑动。
支持iPad。
为polygon做缓存,这样可以使所用东西被重用。这能够有效提升效率。
为切割添加追尾彗星效果,并在连续切割时给予玩家额外奖励分数。
当特殊水果被切割时触发事件。
更好的随机抛水果的机制,比如从侧面抛出水果。
如果你让游戏更cool的点子,或者对此游戏有什么问题和评论,欢迎到下面的讨论区讨论!
相关文章推荐
- 如何使用Box2D和Cocos2D制作一款像Fruit Ninja一样的游戏-第3部分
- 如何使用Box2D和Cocos2D制作一款像Fruit Ninja一样的游戏-第3部分
- 如何使用Box2D和Cocos2D制作一款像Fruit Ninja一样的游戏-第1部分
- 如何使用Box2D和Cocos2D制作一款像Fruit Ninja一样的游戏-第2部分
- 如何使用Box2D和Cocos2D制作一款像Fruit Ninja一样的游戏-第1部分
- 如何使用Box2D和Cocos2D制作一款像Fruit Ninja一样的游戏-第2部分
- 如何使用Box2D和Cocos2D制作一款像Fruit Ninja一样的游戏-第2部分
- 如何使用Box2D和Cocos2D制作一款像Fruit Ninja一样的游戏-第1部分
- 如何使用Box2D和Cocos2D制作一款像Fruit Ninja一样的游戏-第2部分
- 如何使用Box2D和Cocos2D制作一款像Fruit Ninja一样的游戏
- 如何使用Box2D和Cocos2D制作一款像Fruit Ninja一样的游戏
- (译)如何使用cocos2d和box2d来制作一个Breakout游戏:第二部分(完)
- 8.如何使用cocos2d和box2d来制作一个Breakout游戏:第二部分
- (译)如何使用cocos2d和box2d来制作一个Breakout游戏:第二部分(完)
- 【Cocos2D游戏引擎教程】如何使用Cocos2D制作一款简单的iPhone游戏(第二部分)
- (译)如何使用cocos2d和box2d来制作一个Breakout游戏:第一部分
- 使用cocos2d和box2d来制作一个Breakout游戏:第二部分
- (译)碰撞检测和收集物品:如何使用cocos2d制作基于tiled地图的游戏:第二部分
- (14)如何使用Cocos2d-x 3.0制作基于tilemap的游戏:第二部分
- 15. 如何使用cocos2d制作一个打地鼠的游戏:(第二部分。完)