(翻译)使用Cocos2D 2.X制作一个简单iPhone游戏教程——第1部分
原文地址:http://www.raywenderlich.com/25736/how-to-make-a-simple-iphone-game-with-cocos2d-2-x-tutorial
Ray要说:经过一周的投票表决,大家希望我将此套Cocos2D经典入门系列教程从Cocos2D 1.X升级至Cocos2D 2.X,大家的愿望就是对我的命令!:]
现在,此套系列教程已经完全升级至Cocos2D 2.X和Xcode 4.5,同时作了大量的改进,例如Retina显示屏以及对iPhone 5的4英寸屏幕支持。点击链接可以访问以前Cocos2D 1.X版本的教程,如果您需要的话!
Cocos2D是一套用于iPhone开发的强大的库,能为您建立iPhone游戏节省大量的时间。Cocos2D包含精灵支持、超酷的图形效果、动画、物理引擎库、声音引擎及其他更多内容。
回想当初我刚开始学习Cocos2D的时候,只有极少的几篇关于Cocos2D入门有用的教程,但却未能找到任何与我期望类似的教程——做一款非常简单但又包含常见功能的游戏,只需要包含动画、碰撞检测以及声音,而不需要使用太多高级的功能。
在结束了自己的第一款简单游戏的制作之后,我就决定根据自己的经验写一套系列教程,这可能会对其他新人有所帮助。
本系列教程将会从头至尾,一步一步地教您如何使用Cocos2D制作一款简单iPhone游戏。您可以按照系列教程一步步来,也可以直接跳到文章的最后下载示例程序。没错!游戏里面有忍者。
(跳转至系列教程的第二部分或第三部分。)
译者注:第二部分和第三部分的教程,待翻译完成后再增加链接。
下载并安装Cocos2D
您可以从Cocos2D-iPhone官方网站直接下载Cocos2D。
在官方网站上,您会注意到有几种版本可供下载: Cocos2D 1.X版本或是Cocos2D 2.x版本,稳定版本或是不稳定版本。下面对这几种版本做一个简单的介绍。
Cocos2D 1.X和Cocos2D 2.X
这两个版本之间主要的区别在于底层使用的引擎不同,Cocos2D 1.X使用的是OpenGL ES 1.X,而Cocos2D 2.X使用的则是OpenGL ES 2.X。除非您此前有过OpenGL相关的开发经验,否则对您而言这个区别可能没有多大意义。:]
现在,您只需知道以下几点即可:
- Cocos2D 1.X已经出现了很长一段时间。因此有大量的代码仅适用于Cocos2D 1.X。不过这种情况正在改变,越来越多的人正在转向Cocos2D 2.X!
- Cocos2D 2.X可以使用着色器。着色器在OpenGL ES 2.X中是一个很神奇的东西,允许您创建一些很酷的效果,而这些效果无法用OpenGL ES 1.X实现。
虽然这两个版本的Cocos2D都能很好地工作,并且基于这两个版本均开发有非常多优秀的游戏。不过在本教程中,您将使用的是最新、最伟大的Cocos2D 2.X。:]
Cocos2D的稳定版本和不稳定版本
在官方网站上,您还将注意到有“stable(稳定的)”和“unstable(稳定的)”两个版本。
我注意到一些有用的新功能从不稳定版本过渡至稳定版本通常会需要很长一段时间。因此我更加倾向于使用不稳定版本,以便能够使用到所有新的功能。虽然它被称为是“不稳定”的版本,不过不用担心,它通常还是相当不错地。:]
因此,在本教程中,您将使用“不稳定”版本。
结论
对于本教程,请下载Cocos2D 2.X的最新不稳定的版本。
下载之后,需要安装有用的项目模板。打开终端窗口进入Cocos2D的下载目录,然后输入如下命令:
./install-templates.sh -f -u
应该能够看到“Installing cocos2d templates”和一堆提示信息。恭喜!您已经准备好开始使用Cocos2D了!
注释:如果您曾经安装过Cocos2D 1.X,在安装Cocos2D 2.X时可能会担心覆盖掉当前Cocos2D 1.X的模板,不要惊慌!:] Cocos2D 2.X会将其模板安装在一个单独的文件夹,因此您可以同时安装Cocos2D两个版本的模板。
Hello, Cocos2D!
让我们使用刚刚安装的Cocos2D模板从最简单的Hello World项目开始吧!启动Xcode,选择iOS\cocos2d v2.X\cocos2d iOS template新建一个Cocos2D项目,并将项目命名为Cocos2DSimpleGame。
来吧!生成并运行这个由模板建立的项目,如果一切正常,将能看到如下图所示的运行效果:
Cocos2D是按照场景(scene)概念组织的,对游戏而言,场景类似于“关卡”或者“屏幕”。例如,您可能会有一个场景用于游戏的初始菜单,另一个场景用于游戏操作主界面,还有一个场景用于游戏结束。
在一个场景中,可以包含许多图层(layer),这与Photoshop的图层概念有些类似。同时图层可以包含多个节点(node),诸如:精灵、标签、菜单等。而一个节点也可以包含其他节点,例如一个精灵之中可以包含一个子精灵。
如果看一下示例项目,可以看到IntroLayer和HelloWorldLayer两个图层,而且每个图层均包含在一个场景之中。
在本教程中您将使用HelloWorldLayer图层。打开HelloWorldLayer.m并查看其init方法。可以看到其中添加了一个“Hello World”标签和一个菜单。下一步将在图层中放入一个精灵替换现有内容。
添加精灵
在添加精灵之前,需要有一些可以使用的图像。您可以点击下载教程资源,其中有一个是我亲爱的妻子为这个教程项目特意制作的。
下载资源之后,解压缩文件并将其中所有内容拖拽到Xcode中的Resources文件夹之上,在弹出的对话框中确认勾选了“Copy items into destination group’s folder (if needed)”。
现在您已经拥有了自己的图像,下面需要计算一下具体放置忍者的位置。请注意,在Cocos2D中屏幕的左下角的坐标是(0,0),x值和y值随着向右上方的移动增加。由于此项目是横版模式,这意味着如果在3.5英寸的屏幕上运行,右上角的坐标会是(480, 320),而如果在4英寸的屏幕上运行,右上角的坐标则会是(568, 320)。
注释:如果您已经有过一段时间的iOS开发经验,此时可能会想“稍等,我认为4英寸屏幕应该是1136×640像素的,而不是568×320像素的!”
就像素而言,您的想法是对的,不过Cocos2D使用的单位是“点”而不是“像素”。在Retina显示屏的设备上,1点=2像素,因此1136×640像素=568×320点。使用点作为单位是非常方便的,因为如此一来游戏在Retina显示屏和非Retina显示屏上可以使用相同的坐标!
另外请注意,默认情况下在设置一个对象的位置时,该位置是与所添加精灵的中心点相对的。因此,如果您希望忍者精灵与屏幕的左侧边缘水平方向对齐,垂直方向居中,则需要:
- 将精灵位置的x坐标设置为[player sprite's width]/2。
- 将精灵位置的y坐标设置为[window height]/2。
如下所示的图片,可以更好地说明这一点:
现在开始动手吧!打开HelloWorldLayer.m,使用如下代码替换init方法:
- (id) init { if ((self = [super init])) { CGSize winSize = [CCDirector sharedDirector].winSize; CCSprite *player = [CCSprite spriteWithFile:@"player.png"]; player.position = ccp(player.contentSize.width/2, winSize.height/2); [self addChild:player]; } return self; }
生成并运行程序,精灵应该出现在正确的位置之上。但请注意,背景默认是黑色的。而对于这张忍者图片而言,白色的背景应该会看起来好很多。
在Cocos2D中使用CCLayerColor可以方便地将图层的背景设置为自定义的颜色。马上动手,打开HelloWorldLayer.h将HelloWorld的接口声明修改为:
@interface HelloWorldLayer : CCLayerColor
然后点击打开HelloWorldLayer.m并对init方法稍加修改,将其背景颜色设置为白色,如下所示:
if ((self = [super initWithColor:ccc4(255,255,255,255)])) {
再次编译并运行,您将看到精灵出现在白色背景之上了。哈哈,您的忍者看起来已经准备行动了哦!
注释:您可能已经注意到,在资源包里实际上包含有两个版本忍者的图像,一个是player.png(27×40像素),另一个是player-hd.png(两倍大小54×80像素)。
这显示了Cocos2D一个很酷的特性——当游戏在Retina显示屏上运行时,它能够智能地选择使用高分辨率的图形!而您所需要做的只是为两倍大小的图片文件添加一个-hd后缀即可,这与UIKit支持的@2x行为非常类似。
移动妖怪
接下来,需要在场景中添加一些妖怪与忍者进行战斗。为了让游戏变得更加有趣,可以让妖怪移动起来,否则游戏就太没有挑战了!因此,让我们在屏幕右边的外侧创建一些妖怪,然后为它们设置一个动作(action)让它们向左侧移动。
在init方法的前面添加如下方法:
- (void) addMonster { CCSprite * monster = [CCSprite spriteWithFile:@"monster.png"]; // Determine where to spawn the monster along the Y axis CGSize winSize = [CCDirector sharedDirector].winSize; int minY = monster.contentSize.height / 2; int maxY = winSize.height - monster.contentSize.height/2; int rangeY = maxY - minY; int actualY = (arc4random() % rangeY) + minY; // Create the monster slightly off-screen along the right edge, // and along a random position along the Y axis as calculated above monster.position = ccp(winSize.width + monster.contentSize.width/2, actualY); [self addChild:monster]; // Determine speed of the monster int minDuration = 2.0; int maxDuration = 4.0; int rangeDuration = maxDuration - minDuration; int actualDuration = (arc4random() % rangeDuration) + minDuration; // Create the actions CCMoveTo * actionMove = [CCMoveTo actionWithDuration:actualDuration position:ccp(-monster.contentSize.width/2, actualY)]; CCCallBlockN * actionMoveDone = [CCCallBlockN actionWithBlock:^(CCNode *node) { [node removeFromParentAndCleanup:YES]; }]; [monster runAction:[CCSequence actions:actionMove, actionMoveDone, nil]]; }
下面我将详细解释这段代码以便于大家的理解。第一部分代码的意义此前我们已经讨论过:做一些简单的计算确定创建对象的位置,使用计算结果设置对象的position,然后使用与忍者精灵同样的方式将其添加至场景。
代码中新出现的元素是添加动作(action)。Cocos2D提供了很多非常方便的内置动作可以用于实现精灵的动画,例如移动动作、跳跃动作、淡入淡出动作、序列动画动作等。代码中在妖怪上使用了如下三种动作:
- CCMoveTo:使用CCMoveTo动作指示对象从屏幕右边外侧向左移动。请注意,在此可以指定移动过程的持续时间,代码中使用随机2~4秒的时间让妖怪的速度有所区别。
- CCCallBlockN:CCCallBlockN函数允许我们指定动作被执行时要运行的回调代码块。在本游戏中,您设置将要运行的操作是——当妖怪从屏幕左侧移出后将妖怪从图层中删除。这一点非常重要,这样可以避免一段时间之后由于大量无用的精灵站在屏幕之外造成内存泄漏。请注意,还有其他更好的方式来解决这一问题,例如使用精灵的可重用数组,不过本教程是面向初学者的,还是先使用简单的方式。
- CCSequence:CCSequence动作允许我们将一系列动作串联起来,按照顺序一次一个地执行。以这种方式,我们可以首先执行CCMoveTo动作,该动作一旦完成就会执行CCCallBlockN动作。
在继续下一内容之前,还有最后一件事情需要去做。实际调用该方法去创建妖怪!另外,为了增加游戏趣味性,我们让妖怪持续不断地出来。在Cocos2D中可以通过schedule定期调用一个回调函数实现这一点。每隔一秒调用一次回调函数。因此在init方法中的返回语句之前添加如下代码:
[self schedule:@selector(gameLogic:) interval:1.0];
然后简单地实现回调函数,如下所示:
-(void)gameLogic:(ccTime)dt { [self addMonster]; }
搞定!生成并运行项目,现在您应该能够看到一些妖怪快乐地移动穿梭于屏幕之上了,如下图所示:
发射暗器
进行到现在,忍者正在眼巴巴地盼望着能够有一些动作,因此让他发射暗器准备战斗吧!有许多种方式可以实现发射的动作,不过本游戏所采用的方式是当玩家点按屏幕时开始发射,从忍者所在位置向手指点按方向发射一枚暗器。
我决定使用CCMoveTo动作来实现暗器的发射,从而将教程的内容保持在初学者的水平,但是在使用之前需要做一些小小的数学题。这是因为CCMoveTo要求我们给定一个暗器的目标,而在此我们不能简单地使用触摸点,因为触摸点仅能代表相对于忍者暗器的飞行方向。而实际上您需要让暗器朝目标点方向移动并最终飞出屏幕。
下面这张图说明了这一问题:
从图中可以看到,原点和触摸点之间的x和y偏移量已经构成一个小的三角形。现在只需要做一个等比放大的三角形,同时让三角形的一个端点刚好位于屏幕的外侧。
Ok,现在开始编写代码。首先需要让图层能够接受触摸事件。在init方法中添加如下代码:
[self setIsTouchEnabled:YES];
译者注:在翻译本文时,译者所使用的cocos2d-iphone v2.1-rc1版本中,应该使用如下代码:
[self setTouchEnabled:YES];
由于我们已经允许图层能够接受触摸事件,因此已经能够接收到触摸事件的回调了。现在让我们来实现ccTouchesEnded方法,该方法在用户完成触摸后被调用,代码如下:
- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { // Choose one of the touches to work with UITouch *touch = [touches anyObject]; CGPoint location = [self convertTouchToNodeSpace:touch]; // Set up initial location of projectile CGSize winSize = [[CCDirector sharedDirector] winSize]; CCSprite *projectile = [CCSprite spriteWithFile:@"projectile.png"]; projectile.position = ccp(20, winSize.height/2); // Determine offset of location to projectile CGPoint offset = ccpSub(location, projectile.position); // Bail out if you are shooting down or backwards if (offset.x <= 0) return; // Ok to add now - we've double checked position [self addChild:projectile]; int realX = winSize.width + (projectile.contentSize.width/2); float ratio = (float) offset.y / (float) offset.x; int realY = (realX * ratio) + projectile.position.y; CGPoint realDest = ccp(realX, realY); // Determine the length of how far you're shooting int offRealX = realX - projectile.position.x; int offRealY = realY - projectile.position.y; float length = sqrtf((offRealX*offRealX)+(offRealY*offRealY)); float velocity = 480/1; // 480pixels/1sec float realMoveDuration = length/velocity; // Move projectile to actual endpoint [projectile runAction: [CCSequence actions: [CCMoveTo actionWithDuration:realMoveDuration position:realDest], [CCCallBlockN actionWithBlock:^(CCNode *node) { [node removeFromParentAndCleanup:YES]; }], nil]]; }
在代码的第一部分,从touches集合中选择一个触摸来处理,然后使用convertTouchToNodeSpace方法将触摸的坐标从视图坐标转换为当前图层的坐标。
接下来,加载暗器精灵并设置其初始位置。然后以忍者和触摸之间的向量为参照,根据之前描述的算法,确定暗器最终应该移动到的目标位置。
请注意,此处所使用的算法并不是非常理想。只是简单地要求暗器一直移动到屏幕外的X位置,即便是暗器已经从Y轴方向飞出了屏幕!有很多种方式可以解决这一问题,譬如检测飞出屏幕的最短距离,或者在游戏逻辑回调中检查飞出屏幕的暗器并负责删除它们,而不是使用回调方法等。但是就入门教程而言,我们还是尽量保持内容的简单。
最后一件事情是确定移动的持续时间。您希望无论发射方向如何,暗器都以恒定的速度飞行,因此,您还需要再做一些小小的数学题。使用勾股定理,可以计算得出暗器需要的移动距离。请记住在平面几何中有这样一条规则:直角三角形斜边的长度等于两条直角边长度的平方和的平方根。
得到移动距离之后,直接除以速度就可以得出暗器飞行所需的时间。这是因为:速度=距离/时间,换言之:时间=距离/速度。
剩下的事情就是设置动作,这与之前对妖怪所作的类似。生成并运行程序,现在您的忍者应该能够向来犯的妖怪开火了!
碰撞检测
现在您已经可以看到暗器到处乱飞了,但是您的忍者真正需要做的是放倒一些妖怪。因此,让我们添加一些代码检测暗器何时击中目标。
在Cocos2D中有很多种方式解决这一问题,包括使用Cocos2D内置的开源物理引擎:Box2D或Chipmunk。然而,为了保持内容的简单,您将自己去实现一个简单的碰撞检测。
要做到这一点,首先需要能够很好地跟踪当前场景中的妖怪和暗器。在HelloWorldLayer类的声明中添加如下代码:
NSMutableArray * _monsters;
NSMutableArray * _projectiles;
然后在init方法中初始化这些数组:
_monsters = [[NSMutableArray alloc] init];
_projectiles = [[NSMutableArray alloc] init];
另外,当您在思考这些数组时,请不要忘记清理内存,在写这篇教程时,Cocos2D 2.X模板默认还不支持ARC。在dealloc中输入如下代码:
[_monsters release]; _monsters = nil; [_projectiles release]; _projectiles = nil;
注释:尽管Cocos2D 2.X模板默认还不支持ARC ,不过这个非常容易做到。要学习如何做到的具体细节,可以查阅此教程。
译者注:此前译者也曾整理过一篇通过静态库的方式让Cocos2D 2.X支持ARC,有兴趣的朋友可以点击此处查看。
现在,修改addMonster方法,将新建的monster添加至monsters数组,并设置其tag以供后续使用:
monster.tag = 1; [_monsters addObject:monster];
同时修改ccTouchesEnded方法,将新建的projectile添加至projectiles数组,并设置其tag以供后续使用:
projectile.tag = 2; [_projectiles addObject:projectile];
最后,修改两个CCCallBlockN代码块从相应的数组中删除精灵:
// CCCallBlockN in addMonster [_monsters removeObject:node]; // CCCallBlockN in ccTouchesEnded [_projectiles removeObject:node];
生成并运行项目,确保一切仍然能够正常工作。到目前为止,运行程序应该没有明显的差别,不过接下来您就需要来实现此前提及的碰撞检测了。
现在,添加如下新的方法:
- (void)update:(ccTime)dt { NSMutableArray *projectilesToDelete = [[NSMutableArray alloc] init]; for (CCSprite *projectile in _projectiles) { NSMutableArray *monstersToDelete = [[NSMutableArray alloc] init]; for (CCSprite *monster in _monsters) { if (CGRectIntersectsRect(projectile.boundingBox, monster.boundingBox)) { [monstersToDelete addObject:monster]; } } for (CCSprite *monster in monstersToDelete) { [_monsters removeObject:monster]; [self removeChild:monster cleanup:YES]; } if (monstersToDelete.count > 0) { [projectilesToDelete addObject:projectile]; } [monstersToDelete release]; } for (CCSprite *projectile in projectilesToDelete) { [_projectiles removeObject:projectile]; [self removeChild:projectile cleanup:YES]; } [projectilesToDelete release]; }
以上代码应该非常清楚。只是遍历projectiles和monsters数组,创建其中对象边框对应的矩形,然后使用CGRectIntersectsRect检测矩形是否相交。如果发现碰撞,则从场景及数组中删除。
请注意,您必须把要删除的对象先添加至一个“toDelete”数组,因为在遍历数组的同时不能从中删除对象。同样,还有很多更优化的方式来实现上述操作,不过您现在所需要的就是简单的方式。
现在还差最后一件事情,就一切准备就绪了!在init方法中添加如下代码,尽可能频繁地调度刚刚添加的方法运行,以便及时检测到碰撞的发生:
[self schedule:@selector(update:)];
生成并运行程序,现在当暗器击中目标时,它们都会消失了!
收尾
现在,距离一个切实可行的游戏已经非常接近了,尽管有些简单。您只需要再添加一些音效和音乐(哪个游戏能没有声音呢!)以及一些简单的游戏逻辑就大功告成了!
如果以前阅读过iPhone开发之音频系列博客,当您知道使用Cocos2D在游戏中播放音效及背景音乐是如何的简单,您一定会非常高兴。
通过前面下载教程资源,您的项目中现在应该已经有了一个酷酷的背景音乐以及另外一个音效文件。您现在要做的就是播放它们!
要做到这一点,首先在HelloWorldLayer.m文件的顶部添加如下引入:
#import "SimpleAudioEngine.h"
在init方法中,使用如下代码启动背景音乐:
[[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"background-music-aac.caf"];
在ccTouchesEnded方法中,使用如下代码播放音效:
[[SimpleAudioEngine sharedEngine] playEffect:@"pew-pew-lei.caf"];
现在,让我们新建一个场景和图层来负责“You Win”或“You Lose”的提示。使用iOS\cocos2d v2.x\CCNode class模板新建一个文件,将Subclass of指定为CCLayerColor,然后单击Next按钮。将其命名为GameOverLayer,然后单击Create按钮。
然后使用如下代码替换GameOverLayer.h:
#import "cocos2d.h" @interface GameOverLayer : CCLayerColor +(CCScene *) sceneWithWon:(BOOL)won; - (id)initWithWon:(BOOL)won; @end
使用如下代码替换GameOverLayer.m:
#import "GameOverLayer.h" #import "HelloWorldLayer.h" @implementation GameOverLayer +(CCScene *) sceneWithWon:(BOOL)won { CCScene *scene = [CCScene node]; GameOverLayer *layer = [[[GameOverLayer alloc] initWithWon:won] autorelease]; [scene addChild: layer]; return scene; } - (id)initWithWon:(BOOL)won { if ((self = [super initWithColor:ccc4(255, 255, 255, 255)])) { NSString * message; if (won) { message = @"You Won!"; } else { message = @"You Lose :["; } CGSize winSize = [[CCDirector sharedDirector] winSize]; CCLabelTTF * label = [CCLabelTTF labelWithString:message fontName:@"Arial" fontSize:32]; label.color = ccc3(0,0,0); label.position = ccp(winSize.width/2, winSize.height/2); [self addChild:label]; [self runAction: [CCSequence actions: [CCDelayTime actionWithDuration:3], [CCCallBlockN actionWithBlock:^(CCNode *node) { [[CCDirector sharedDirector] replaceScene:[HelloWorldLayer scene]]; }], nil]]; } return self; } @end
请注意,此处有两种不同的对象:场景和图层。场景可以包含任意多个图层,不过在此示例中仅包含了一个图层。该图层在屏幕的中间放置了一个标签,并设置了一个动作,等待3秒钟之后,切换回Hello World场景。
最后,让我们添加一些非常基础的游戏逻辑。首先,需要记录住忍者到底干掉了的多少个妖怪。在HelloWorldLayer类的HelloWorldLayer.h中添加一个成员变量,如下所示:
int _monstersDestroyed;
在HelloWorldLayer.m中添加GameOverLayer类的引入:
#import "GameOverLayer.h"
在update方法的monstersToDelete循环中的removeChild:monster语句之后,添加如下代码,递增计数并且检查胜利条件:
_monstersDestroyed++; if (_monstersDestroyed > 30) { CCScene *gameOverScene = [GameOverLayer sceneWithWon:YES]; [[CCDirector sharedDirector] replaceScene:gameOverScene]; }
最后,让我们设定只要有一个妖怪穿过屏幕,你就输了。在addMonster:方法的CCCallBlockN回调中的removeFromParentAndCleanup语句之后,添加如下代码:
CCScene *gameOverScene = [GameOverLayer sceneWithWon:NO];
[[CCDirector sharedDirector] replaceScene:gameOverScene];
来吧!生成并运行您的游戏,当满足上述输赢条件时应该能看到游戏结束的场景了!
下一步做些什么?
点击链接可以下载本教程使用的完成源代码:简单的Cocos2D iPhone游戏。
您可以将此项目作为一个良好的基础,通过在项目中添加一些新的功能,进一步了解Cocos2D的相关知识。可以考虑添加一个条形图显示已经干掉了多少个妖怪,查阅drawPrimitivesTest示例项目可以了解相关内容。也可以考虑在干掉怪物时添加更酷的死亡动画,查阅ActionsTest、EffectsTest和EffectsAdvancedTest项目可以了解相关内容。也可以考虑添加更多的声音、图片或游戏逻辑让游戏更加好玩。创意无极限!
著作权声明:本文由http://www.cnblogs.com/liufan9翻译,欢迎转载分享。请尊重作者劳动,转载时保留该声明和作者博客链接,谢谢!