Cocos2D是一款功能强大的iphone游戏引擎,它可以很大程度上节约你的游戏开发时间。它包含sprite(精灵),绚丽的特效,animations(动画),物理引擎,声音引擎,以及其他一些相当实用的功能。
我也是刚接触Cocos2D,虽然目前市面上有很多cocos2d相关的教程,但我没能找到我想到的那种入门级别的教程-如何制作一款非常简单但是功能齐全的游戏。对于入门教程,这款游戏应当仅仅包含一些较为基础的功能,如animation(动画), collisions(碰撞)和声音。于是我自己制作了一款简单的游戏,并把过程中积累的点点滴滴制成了一篇3系列的教程,希望对广大刚刚接触Cocos2D的入门者有所裨益。
这一系列教程会带你逐步熟悉并学会制作一个简单iphone游戏的每一个步骤。你可以循序渐进的学习,或者干脆跳转到本教程的末尾下载示例工程,在那里,会有神奇的忍者等着你哟!
(跳转到全系列教程的 第2部分 或者 第3部分)
你可以从以下地址下载到最新的Cocos2Dthe Cocos2D Google Code page.
下载完成后,你需要安装实用的project templates(工程模板)。在mac下打开一个终端窗口,
进入到刚刚下载到的Cocos2D的目录并输入以下指令: ./install-templates.sh -f -u
注意,如果xcode没有安装在默认目录下,在这里你可以选择性的在指令后添加参数(如果你的机器曾经安装过多个版本的SDK的话,那么很可能之前你已经会用这种方法了)。
让我们从建立一个简单的Hello World程序开始吧!
启动Xcode,选择cocos2d Application template建立一个新工程。将其命名为 “Cocos2DSimpleGame”。
编译(Cmd+B)并运行(Cmd+R)之,如果一切顺利,你将看到如下图所示:
Cocos2D是以场景(scenes)组织的,对一个游戏来说,场景可以是关卡或者是屏幕。比如游戏一开始的主菜单场景,游戏中运行起来的场景,还有游戏结束game over的场景。在场景中,可以有很多的层layers(很像photoshop中的层),层中又可以包含很多节点nodes比如精灵sprite,文本labels,菜单menus和其他的。同时每个节点又可以包含其他的节点(例如一个sprite节点可以包含另一个sprite节点作为他的child)。
观察下Hello World示例工程,会发现里边只有一个层-HelloWorldLayer-我们准备在这儿实现我们主要的游戏逻辑。打开这个文件,会看到在init方法里边被加入了一个写着”Hello World”的label,现在删除掉它,我们以后将用一个sprite来替换它。
在添加一个sprie之前,我们需要一些图片,你可以使用自己创建的,或者直接使用我(ray)可爱的妻子为这个项目制作的图片资源: a Player image, a Projectile image, and a Target image.
获得这些图片后,把它们从finder里拖拽进Xcode工程的resources文件夹下,确保”Copy items into destination group’s folder (if needed)是选中的。
好的,现在有了图片资源,下面要计算出该往哪里放置我们的主人公。需要注意的是在Cocos2D里,屏幕的左下角是(0,0)点,随着你往右上方向移动,x和y值会随之增加。因为本项目是landscape模式的(手机横向放置),所以这意味着右上角的坐标是(480,320)。
还需要注意的是,每当设置一个对象的坐标,默认情况下设置的是该对象自身中心的位置。所以如果想把主人公sprite放置到屏幕的横向左边缘,纵向屏幕一半的位置,需要执行以下两步:
图片为例:
这就试试看!打开Classes文件夹并点击HelloWorldLayer.m,用以下内容替换掉init方法:
-(id) init { if( (self=[super init] )) { CGSize winSize = [[CCDirector sharedDirector] winSize]; CCSprite *player = [CCSprite spriteWithFile:@"Player.png" rect:CGRectMake(0, 0, 27, 40)]; player.position = ccp(player.contentSize.width/2, winSize.height/2); [self addChild:player]; } return self; } |
编译并运行,主人公sprite跃然屏幕之上,但注意背景默认是黑色的,白色看起来也许会更好点儿。使用CCLayerColor class可以简单的完成这一目标。打开HelloWorldLayer.h并修改其interface 声明部分:
@interface HelloWorldLayer : CCLayerColor |
然后打开HelloWorldLayer.m,对init方法做一个轻微的修改:
if( (self=[super initWithColor:ccc4(255,255,255,255)] )) { |
背景就顺利变为了白色。
编译并运行,主人公出现在白色背景之上了,哈哈,我们的小忍者看起来跃跃欲试了!
接下来为了让我们的忍者不至于独孤求败,我们加入一些靶子,为了让游戏更有趣,可以让靶子移动起来-否则游戏将不会有很好的挑战性。将这些靶子稍微向右移出屏幕一点儿,并给他们设置一个向左移动的动作。
在init方法之前添加以下方法:
-(void)addTarget { CCSprite *target = [CCSprite spriteWithFile:@"Target.png" rect:CGRectMake(0, 0, 27, 40)]; // Determine where to spawn the target along the Y axis CGSize winSize = [[CCDirector sharedDirector] winSize]; int minY = target.contentSize.height/2; int maxY = winSize.height - target.contentSize.height/2; int rangeY = maxY - minY; int actualY = (arc4random() % rangeY) + minY; // Create the target slightly off-screen along the right edge, // and along a random position along the Y axis as calculated above target.position = ccp(winSize.width + (target.contentSize.width/2), actualY); [self addChild:target]; // Determine speed of the target int minDuration = 2.0; int maxDuration = 4.0; int rangeDuration = maxDuration - minDuration; int actualDuration = (arc4random() % rangeDuration) + minDuration; // Create the actions id actionMove = [CCMoveTo actionWithDuration:actualDuration position:ccp(-target.contentSize.width/2, actualY)]; id actionMoveDone = [CCCallFuncN actionWithTarget:self selector:@selector(spriteMoveFinished:)]; [target runAction:[CCSequence actions:actionMove, actionMoveDone, nil]]; } |
为了让事情更容易理解,我使用了有点儿冗长的方式来讲解。这一部分其实就是和之前主人公sprite同样的方式来计算对象该放到哪儿,设置坐标并将其加入到场景中去。
唯一不同的是这里加入了actions(动作)。Cocos2D内部提供了许多极为便利的动作来给你的sprite动起来,例如move actions(移动),jump actions(跳跃),fade actions(渐隐渐现),animation actions(动画)等等,这里我们只用其中的三个action:
接下来,添加上文提到的那个CCCallFuncN action中需要的回调函数,在addTarget:方法之前添加以下内容:
-(void)spriteMoveFinished:(id)sender { CCSprite *sprite = (CCSprite *)sender; [self removeChild:sprite cleanup:YES]; } |
此方法的目的是一旦sprite超出屏幕范围,就将其从屏幕上移除。随着靶子数量的不断增加,如果不及时清理之,将会有严重的内存泄漏问题出现。解决这个问题也可以使用另一种更为高端的手段-使用数组存储一系列可以重复使用的sprites对象,但对于这篇初学者教学来说,我们用目前这个基础的方法即可。
在继续前还有一件事儿要做。我们需要实际调用创建靶子的方法。为了让游戏更有趣,我们让靶子每隔一小段时间就出现。通过每隔一段时间就会被调用的schedule(调度)回调函数,就能完成这个目标。每隔一秒刷新一个靶子,在init方法里的return之前加入以下内容:
[self schedule:@selector(gameLogic:) interval:1.0]; |
接下来在回调函数里填入如下内容:
-(void)gameLogic:(ccTime)dt { [self addTarget]; } |
编译并运行,你会看到靶子们欢快地在屏幕上移动着:
到这里,主人公忍者多么渴望着被添加一个射击动作。虽然有很多方法可以实现射击,但是这个游戏里,我们希望每当用户触摸屏幕时,让主人公向着触摸的方向发射一个飞镖。
我使用CCMoveTo action来让所有逻辑尽量简单,但是使用它还是需要一丁点儿数学计算。CCMoveTo需要被指定一个目标点,我们不能直接使用用户触摸屏幕的点,因为这个点仅仅能表示主人公射击靶子的方向,实际上我们想让飞镖持续飞行到一直飞出屏幕为止。
这里是一个说明该问题的图片:
你可以看到,有一个由主人公起始点到触摸点组成的较小的三角,这个三角延伸到屏幕边缘组成的一个较大的三角,我们需要的就是这个大三角与屏幕边缘的交汇点。
回到代码部分,首先我们在层里启用触摸,在init方法中加入:
self.isTouchEnabled = YES; |
由于启用了触摸,在层里会接收到相应的触摸回调函数。每当用户的手指从屏幕抬起,将会触发ccTouchesEnded方法,我们这就实现它:
- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { // Choose one of the touches to work with UITouch *touch = [touches anyObject]; CGPoint location = [touch locationInView:[touch view]]; location = [[CCDirector sharedDirector] convertToGL:location]; // Set up initial location of projectile CGSize winSize = [[CCDirector sharedDirector] winSize]; CCSprite *projectile = [CCSprite spriteWithFile:@"Projectile.png" rect:CGRectMake(0, 0, 20, 20)]; projectile.position = ccp(20, winSize.height/2); // Determine offset of location to projectile int offX = location.x - projectile.position.x; int offY = location.y - projectile.position.y; // Bail out if we are shooting down or backwards if (offX <= 0) return; // Ok to add now - we've double checked position [self addChild:projectile]; // Determine where we wish to shoot the projectile to int realX = winSize.width + (projectile.contentSize.width/2); float ratio = (float) offY / (float) offX; int realY = (realX * ratio) + projectile.position.y; CGPoint realDest = ccp(realX, realY); // Determine the length of how far we'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], [CCCallFuncN actionWithTarget:self selector:@selector(spriteMoveFinished:)], nil]]; } |
在第一部分里,我们从touches对象集合中选择一个,得到他在当前屏幕上的坐标,通过调用convertToGL来把此坐标转换为当前屏幕模式适应的,这一点很重要,因为我们使用的是landscape模式。
接下来加载飞镖sprite并像往常一样设置其初始坐标。根据之前讨论过的相似三角形算法,我们计算得到飞镖的终点。
注意这个算法并不完美。即使飞镖的Y坐标已经出了屏幕,它还是会被强制移动到X坐标移出屏幕为止。想解决这个有很多手段,包括检测射击点到屏幕边缘的最短距离,在游戏逻辑的回调里检测飞镖坐标是否在屏幕之外(比如visit),虽然这些方法都可以解决它,但简单起见,这篇初学者教程还是会使用之前的方式。
最后一件事儿是决定运动所需的时间。飞镖最好是能以固定的速率射出,所以我们还需要一丁点儿数学计算。使用 勾股定理可以轻松地得到斜边的长度。
一旦有了距离,只需要将其除以一个固定的速率,便可得到时间。
剩余部分就像我们之前做的,给靶子设置上actions。编译并运行,哈哈,你的忍者能给予冲过来的鬼头靶子致命打击了!
飞镖四处飞,但我们的小忍者并没有看到飞镖击倒鬼头靶子。是时候加入一些检测飞镖与靶子碰撞检测的代码了。
使用Cocos2D有很多途径解决这个,包括使用其中一个引擎包含的物理引擎:Box2D或者Chipmunk。为了有效说明问题的本质并使事情简单,我们将自己实现一个简单的碰撞检测。
首先,我们需要记录所有在当前场景里的靶子和飞镖对象。在HelloWorldLayer类的声明中加入:
NSMutableArray *_targets; NSMutableArray *_projectiles; |
并在init方法里加入初始化数组的代码:
_targets = [[NSMutableArray alloc] init]; _projectiles = [[NSMutableArray alloc] init]; |
在dealloc方法里边清理之:
[_targets release]; _targets = nil; [_projectiles release]; _projectiles = nil; |
现在,修改addTarget方法,在靶子数组中加入一个新靶子并设置其标识(tag)留待后用:
target.tag = 1; [_targets addObject:target]; |
同样的,把在ccTouchesEnded里新建的飞镖加入到飞镖数组中并设置tag留待后用:
projectile.tag = 2; [_projectiles addObject:projectile]; |
最后,修改spriteMoveFinished方法,根据tag分类把即将删除的对象从数组中也移除掉:
if (sprite.tag == 1) { // target [_targets removeObject:sprite]; } else if (sprite.tag == 2) { // projectile [_projectiles removeObject:sprite]; } |
编译并运行,确保到目前为止一切都OK。虽然目前还看不到什么显著的变化,但我们已经有了做碰撞检测的基础了。
添加以下方法到HelloWorldLayer:
- (void)update:(ccTime)dt { NSMutableArray *projectilesToDelete = [[NSMutableArray alloc] init]; for (CCSprite *projectile in _projectiles) { CGRect projectileRect = CGRectMake( projectile.position.x - (projectile.contentSize.width/2), projectile.position.y - (projectile.contentSize.height/2), projectile.contentSize.width, projectile.contentSize.height); NSMutableArray *targetsToDelete = [[NSMutableArray alloc] init]; for (CCSprite *target in _targets) { CGRect targetRect = CGRectMake( target.position.x - (target.contentSize.width/2), target.position.y - (target.contentSize.height/2), target.contentSize.width, target.contentSize.height); if (CGRectIntersectsRect(projectileRect, targetRect)) { [targetsToDelete addObject:target]; } } for (CCSprite *target in targetsToDelete) { [_targets removeObject:target]; [self removeChild:target cleanup:YES]; } if (targetsToDelete.count > 0) { [projectilesToDelete addObject:projectile]; } [targetsToDelete release]; } for (CCSprite *projectile in projectilesToDelete) { [_projectiles removeObject:projectile]; [self removeChild:projectile cleanup:YES]; } [projectilesToDelete release]; } |
以上内容很清晰。我们遍历了一下飞镖和靶子数组,在遍历中得到每一个对象的bounding box(碰撞框),使用CGRectIntersectsRect来检测两个矩形是否相交。如果有相交,则将飞镖和靶子分别从场景和数组中移除。注意我们必须将这些对象加入到“toDelete”结尾的数组中,因为我们没法在当前循环中从数组中删掉它。还是一样,有很多更优的方法来解决这类问题,简单起见,我使用最简单的方法。
你仅仅需要一步就可以欢呼了,使用schedule调度这个方法,使其在每一帧都执行:
[self schedule:@selector(update:)]; |
编译并运行,所有和飞镖碰撞的靶子都会灰飞烟灭了!
我们已经非常接近制作出一个成品(但很简单)的游戏了。只需要再加入一些音效和音乐(没有游戏不带声音的!)以及一些简单的游戏逻辑。
如果你阅读过我的blog series on audio programming for the iPhone这篇教程,你会非常欣喜地发现在Cocos2D中播放声音原来如此简单。
首先,把背景音乐和一个射击音效拖拽进你的工程的resources文件夹。你可以随意使用以下资源cool background music I made or my awesome pew-pew sound effect, 或者制作你自己的。
接下来在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”的游戏结束场景。右键点击Classes文件夹,选择FileNew File,并选择Objective-C class,确保继承的类是NSObject。点击Next,在filename位置输入GameOverScene作为文件名,确保“Also create GameOverScene.h”是选中的。
用以下内容替换掉GameOverScene.h:
#import "cocos2d.h" @interface GameOverLayer : CCLayerColor { CCLabelTTF *_label; } @property (nonatomic, retain) CCLabelTTF *label; @end @interface GameOverScene : CCScene { GameOverLayer *_layer; } @property (nonatomic, retain) GameOverLayer *layer; @end |
用以下内容替换掉GameOverScene.m:
#import "GameOverScene.h" #import "HelloWorldLayer.h" @implementation GameOverScene @synthesize layer = _layer; - (id)init { if ((self = [super init])) { self.layer = [GameOverLayer node]; [self addChild:_layer]; } return self; } - (void)dealloc { [_layer release]; _layer = nil; [super dealloc]; } @end @implementation GameOverLayer @synthesize label = _label; -(id) init { if( (self=[super initWithColor:ccc4(255,255,255,255)] )) { CGSize winSize = [[CCDirector sharedDirector] winSize]; self.label = [CCLabelTTF labelWithString:@"" 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], [CCCallFunc actionWithTarget:self selector:@selector(gameOverDone)], nil]]; } return self; } - (void)gameOverDone { [[CCDirector sharedDirector] replaceScene:[HelloWorldLayer scene]]; } - (void)dealloc { [_label release]; _label = nil; [super dealloc]; } @end |
注意这里有两个不同的对象:一个scene和一个layer。一个scene可以包含很多layer。不过目前这个只有一个,这个layer只是放置了一个label在屏幕中心,并schedule了一个3秒会自动返回Hello World场景的事件。
最后,加入一些极为简单的游戏逻辑进去。首先,记录一下被主人公的飞镖干掉的靶子。在HelloWorldLayer类中加入一个成员变量,在HelloWorldLayer.h中的@interface块儿加入如下:
int _projectilesDestroyed; |
在HelloWorldLayer.m里,导入GameOverScene类:
#import "GameOverScene.h" |
在update方法中,增加飞镖摧毁靶子的数量并检测获得胜利的条件,在removeChild:target:之后加入:
_projectilesDestroyed++; if (_projectilesDestroyed > 30) { GameOverScene *gameOverScene = [GameOverScene node]; _projectilesDestroyed = 0; [gameOverScene.layer.label setString:@"You Win!"]; [[CCDirector sharedDirector] replaceScene:gameOverScene]; } |
当有鬼头靶子越过了屏幕左侧,你就输了,修改spriteMoveFinished方法,在tag == 1 case中removeChild:sprite:方法之后加入:
GameOverScene *gameOverScene = [GameOverScene node]; [gameOverScene.layer.label setString:@"You Lose :["]; [[CCDirector sharedDirector] replaceScene:gameOverScene]; |
编译并执行,现在你胜利或失败后会进入game over场景中,根据条件的不同会显示相应信息。
就到这里!这是到目前为止所有的源码下载地址simple Cocos2D iPhone game
这个工程非常适合作为刚刚入门Cocos2D的程序员,你完全可以添加些新的功能进来。比如可以添加一个条形图表,用来指示在你获得胜利前一共击毁了多少靶子(参考Cocos2D的test工程里的drawPrimitivesTest),还可以为鬼头靶子加入一个很炫的死亡动画(参考ActionsTest,EffectsTest和EffectsAdvancedTest),也可以加入一些新的声音,素材或者游戏逻辑。尽情发挥你无边无际的想象力吧!
如果你想继续这个系列教程,请看第二篇 如何添加一个旋转炮台,或者第三篇 更难的怪物和更多的关卡!
另外,如果你想学习更多有关Cocos2D的内容,请跟随我的其他系列教学how to create buttons in Cocos2D(如何在Cocos2D中创建按钮), intro to Box2D(Box2D入门), 或者 how to create a simple Breakout game(如何制作一个简单的消除游戏).
我自己也是刚刚接触Cocos2D,同样也有很多需要学习的。如果你有更好的想法或点子,完全可以以此工程为基础添加内容。