Cocos2D是一款功能强大的iphone游戏引擎,它可以很大程度上节约你的游戏开发时间。它包含sprite(精灵),绚丽的特效,animations(动画),物理引擎,声音引擎,以及其他一些相当实用的功能。

我也是刚接触Cocos2D,虽然目前市面上有很多cocos2d相关的教程,但我没能找到我想到的那种入门级别的教程-如何制作一款非常简单但是功能齐全的游戏。对于入门教程,这款游戏应当仅仅包含一些较为基础的功能,如animation(动画), collisions(碰撞)和声音。于是我自己制作了一款简单的游戏,并把过程中积累的点点滴滴制成了一篇3系列的教程,希望对广大刚刚接触Cocos2D的入门者有所裨益。

这一系列教程会带你逐步熟悉并学会制作一个简单iphone游戏的每一个步骤。你可以循序渐进的学习,或者干脆跳转到本教程的末尾下载示例工程,在那里,会有神奇的忍者等着你哟!

(跳转到全系列教程的 第2部分 或者 第3部分)

下载并安装Cocos2D

你可以从以下地址下载到最新的Cocos2Dthe Cocos2D Google Code page.

下载完成后,你需要安装实用的project templates(工程模板)。在mac下打开一个终端窗口, 进入到刚刚下载到的Cocos2D的目录并输入以下指令: ./install-templates.sh -f -u

注意,如果xcode没有安装在默认目录下,在这里你可以选择性的在指令后添加参数(如果你的机器曾经安装过多个版本的SDK的话,那么很可能之前你已经会用这种方法了)。

Hello, Cocos2D!

让我们从建立一个简单的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来替换它。

添加一个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放置到屏幕的横向左边缘,纵向屏幕一半的位置,需要执行以下两步:

  • x坐标,设置其为[player sprite's width]/2。
  • y坐标,设置其为[window height]/2。

图片为例:

这就试试看!打开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)] )) {

背景就顺利变为了白色。

编译并运行,主人公出现在白色背景之上了,哈哈,我们的小忍者看起来跃跃欲试了!

移动靶子Moving Targets

接下来为了让我们的忍者不至于独孤求败,我们加入一些靶子,为了让游戏更有趣,可以让靶子移动起来-否则游戏将不会有很好的挑战性。将这些靶子稍微向右移出屏幕一点儿,并给他们设置一个向左移动的动作。

在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:

  • CCMoveTo:
  • CCCallFuncN:
  • CCSequence:

接下来,添加上文提到的那个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];
}

编译并运行,你会看到靶子们欢快地在屏幕上移动着:

射击Shooting Projectiles

到这里,主人公忍者多么渴望着被添加一个射击动作。虽然有很多方法可以实现射击,但是这个游戏里,我们希望每当用户触摸屏幕时,让主人公向着触摸的方向发射一个飞镖。

我使用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。编译并运行,哈哈,你的忍者能给予冲过来的鬼头靶子致命打击了!

碰撞检测Collision Detection

飞镖四处飞,但我们的小忍者并没有看到飞镖击倒鬼头靶子。是时候加入一些检测飞镖与靶子碰撞检测的代码了。

使用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:)];

编译并运行,所有和飞镖碰撞的靶子都会灰飞烟灭了!

完成触摸Finishing Touches

我们已经非常接近制作出一个成品(但很简单)的游戏了。只需要再加入一些音效和音乐(没有游戏不带声音的!)以及一些简单的游戏逻辑。

如果你阅读过我的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];
}


在tag == 1 case中removeChild:sprite:方法之后加入:


GameOverScene *gameOverScene = [GameOverScene node];
[gameOverScene.layer.label setString:@"You Lose :["];
[[CCDirector sharedDirector] replaceScene:gameOverScene];


编译并执行,现在你胜利或失败后会进入game over场景中,根据条件的不同会显示相应信息。

源码拿来!Gimme The Code!

就到这里!这是到目前为止所有的源码下载地址simple Cocos2D iPhone game

何去何从?Where To Go From Here?

这个工程非常适合作为刚刚入门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,同样也有很多需要学习的。如果你有更好的想法或点子,完全可以以此工程为基础添加内容。