免责申明(必读!):本博客提供的所有教程的翻译原稿均来自于互联网,仅供学习交流之用,切勿进行商业传播。同时,转载时不要移除本申明。如产生任何纠纷,均与本博客所有人、发表该翻译稿之人无任何关系。谢谢合作!
原文链接地址:http://www.raywenderlich.com/352/how-to-make-a-simple-iphone-game-with-cocos2d-tutorial
游戏截图:
cocos2d是一个非常强大的开源库,它可以为你开发iphone上面的游戏节省大量的时间。目前支持精灵(sprite)、动画、物理引擎、声音引擎以及许许多多非常酷的图像效果等等。
我也是刚开始学习cocos2d,目前已经有许多非常好的教程来教你如何使用cocos2d了。但是,那些都不是我想要的。我真正想要的是一个 非常简单,但是可以跑起来的游戏。这个游戏包括怎么使用动画、碰撞检测和播放声音,这就够了,并不需要使用太多高级的特性。最终,我自己做了一个这样的游 戏,因此,基于我的一些经验写了这篇教程,希望会对初学者有所帮助。
这篇教程将会从头至尾、一步一步地教你如何使用cocos2d来制作一个简单的iphone游戏。你可以按照教程一步步来,或者干脆直接跳到文章的最后,下载样例程序。没错!游戏里面有忍者。
下载并安装cocos2d
你可以从下面的链接来下载cocos2d的最新版本(以前是google,现在采用git了) the Cocos2D Google Code page.
在你下载完代码以后,你可能想安装非常有用的项目模板。打开终端窗口,然后cd到你的cocos2d解压缩目录下面,输入./install_template.sh
请注意,你可以传递一些参数给这个安装脚本,比如你把xcode安装在了一个非标准的目录下面。(或者你有多个版本的sdk安装在你的机器上面)
Hello, Cocos2D!
让我们从最简单的HelloWorld项目开始吧!启动xcode,点file->new project,然后选择cocos2d Application template来创建一个新的cocos2d项目并把它命名为 “Cocos2dSimpleGame”
继续,点编译并运行这个工程模板,如何一切都ok的话,那么你会看到下面的内容:
cocos2d是按照“场景”(scene)的概念组织的,对一个游戏来说,就好像某个关卡或者屏幕之类的。比如,你可能需要一个场景来为你的 游戏建立初使化菜单界面,另外一个场景当作玩游戏的主要界面,还有一个游戏结束的时候的界面。在一个场景里面,你可以有许多“层”(layer)(这个和 photoshop有点类似)。每一个层又可以包含一些结点,比如精灵、标签、菜单等。而且一个结点也可以包含其它的结点。(比如,一个精灵可以包含一个 子精灵)
如果你看一下样例工程,你会看到只有一个场景HelloWorldScene--我们接下来将会在这个场景里面实现我们的游戏逻辑。继续打开 它,你会在init方法里面看到,它把一个带有“Hello World”字样的标签加到了当前场景中。我们将把这些代码去掉,并且放一个精灵在上面。
增加一个精灵
在我们增加一个精灵之前,我们需要一些图片。你可以自己去创建一些,或者使用我可爱的妻子为这个项目所创建的图片: a Player image, a Projectile image, and a Target image.
一旦你获得了这些图片,你就可以把它们用鼠标拖到xcode的resource文件夹中去,并且保证“ Copy items into destination group’s folder (if needed)”勾上了。现在,我们拥有自己的图片了,我们先得计算一下,应该把player放在哪里。请注意,cocos2d坐标系的原点在屏幕的左下 角,即(0,0)位置在左下角,这与一般的窗口坐标系统在左上角有所区别。x方向往右是正,y方向往上是正。由于项目是landscape(横版)模式, 这就意味着右上角的坐标是(480,320)。
还有需要注意的是,当我们设置一个对象的位置的时候,这个位置是相对于所加精灵的中心点来加的。因此,如果我们想让我们的player精灵与屏幕的左边界水平对齐,并且垂直居中的话。
那么对于x轴:我们需要设置为[player sprite's width]/2。y坐标设置为[window height]/2。
下面有一张图,可以更加清楚一些:
好,让他开始射击吧!打开Class文件夹并点击HelloWorldScene.m,然后把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;
}
编译并运行,你的精灵这时候应该出现在正确的位置上了。但是,这个前景默认是黑的。对于这张忍者图片来说,白色的背景可能看起来更好一些。在 cocos2d里面,有一种非常简单的方式来改变层的背景颜色,那就是使用CCColoredLayer类。好,跟我来吧!点击 HelloWorldScene.h,然后把HelloWorld接口声明改成下面这样:
然后点击HelloWorldScene.m文件,在init方法里面做一些小小的修改,以便使我们能把层的背景颜色改成白的:
编译并运行,这时你将看到你的精灵是在一个白色的背景上面了。哈哈,我们的忍者看起来整装待发呢!
移动目标
接下来,我们想增加一些目标怪物来与我们的忍者战斗。为了使事情变得更加有趣,我想让这些目标可以移动--实际上这也并不是很难!因此,让我们先在屏幕的右边靠外一点点创建一些目标,然后设置一个action,并使之从右边移动到左边。
接下来,紧接着上面init函数,添加下面代码:
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]];
}
在这里我将以一种非常啰嗦的形式来介绍,目的是方便大家理解。第一部分需要解释的是我们之前已经讨论过了的:我们做一些简单的计算来决定把对象放在什么位置,然后设置对象的position,然后并把它加在场景上面,就和加载player精灵一样。
这里增加的新的元素就是actions。cocos2d里面提供了许多非常方便的内置的action,你可以使用这样action来让你的精灵 动起来。比如move action,jump action,fade action,animation action(就是播放图片序列)等等。这 里,我们对目标对象使用了3种类型的action:
- CCMoveTo: 我们使用CCMoveTo action让目标从屏幕右边一直往左移动,直到移出屏幕。注意,这里我们可以指定这个过程要花费多长时间。这里使用了变化的时间间隔2-4秒。
- CCCallFuncN: 它可以让你为某个执行此action的对象指定一个回调函数。我们指定的回调函数是:spriteMoveFinished---目前并没有,到后面会具体给了来。
- CCSequence: 它允许我们把一系列的action组成一个action序列,并且这些acton可以按顺序执行。一次执行完所有的action。在上面的例子中,我们让对象首先执行CcMoveTo,等CCMoveTo完成后,马上就会执行CCCallFuncN action。
接下来, 为CCCallFuncN action增加一个回调函数。你可以在addTarget前面增加下面的代码:
CCSprite * sprite = (CCSprite * )sender;
[self removeChild:sprite cleanup:YES];
}
这个函数的目的是当精灵飞出屏幕之后,需要移除出当前的scene。这个非常重要,这样的话我们就不会因为屏幕外面积累太多没有用到的精灵而造 成内存泄漏。注意,其实还有其它更好的方式来解决这个问题,比如使用一组可以重用的精灵等。不过,对于初学者来说,我在这篇教程里,尽量简单化。
在我们继续之前,还有最后一件事没做。我们需要调用这个方法来创建我们的目标怪物。而且,为了使事情变得更加有趣,我们会随着时间连续不断地发 射一些怪物出来。我们可以使用cocos2d的定时scheduler,并指定一个回调函数来完成此功能。一秒钟调用一次回调函数就可以了。因此,在 init函数返回之前,我们再加入下面的代码:
然后简单的实现一下这个回调函数,如下:
[self addTarget];
}
就这么多!现在编译再运行一下工作,你可以看到怪物在屏幕上面happy地移动了!
发射飞盘
在这里,我们的忍者需要有一些行动了--因此让我们增加一些射击吧!这里有许许多多实现射击的方式,但是在这个游戏里面,我们想让用户触摸一下屏幕,然后飞盘就会从player开始,沿着你触摸的位置发射出来。
我们使用CCMoveTo action来实现这个功能。但是,为了使用这个功能,我们必须首先来做一些数学题。这是因为,CCMoveTo需 要我们为飞盘指定目的地。但是我们又不能使用触摸点,因为触摸点仅仅代表飞盘飞的方向。我们实际上想让子弹超过触摸点,然后飞出屏幕之外去。
下面这张图解释了这个问题:
因此,就像你看到的,在触摸点和player之间有一个小的三角形,由origin点,offx和offy组成。我们只需要画一个更大的三角形,同时使用一样的比率就行了。然后我们就可以根据比例算出飞盘飞出屏幕的位置。
好了,让我们看看代码怎么写。首先我们需要让layer能接收touch事件。在你的init方法面添加下面一行代码:
由于我们激活了layer的touch,因此我们能够接收到touch事件的回调。这里,我们实现ccTouchesEnded方法,这是在用户完成一次touch之后调用的,代码如下:
// 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]];
}
在第一部分,我们选择一个touch来处理,获得它在当前view中的位置,然后调用convertToGL把坐标转换成我们当前层的坐标系中去。这个非常重要,因为我们使用的是landscape模式。
接下来,我们加载飞盘精灵并且设置它的初始位置。然后,我们计算出它需要飞往何处,使用player和touch之间的向量并且根据前面描述的算法计算出来。
注意,这个算法并不完美。我们强迫子弹飞出屏幕x轴的外边--即使在它已经飞出屏幕y轴的外边界了。这里有许多方向来解决这个问题,比如检查飞出屏幕的最短距离,或者使用一个游戏回调函数来检查一个飞盘是否飞出,飞出就移出场景。但是,在这里,我们尽量保持简单。
最后一件事情就是,决定飞盘移动的时间。我们想让子弹以常量速度飞行,不管飞行方向如何。因此,我们不得不再做一点点数学。我们能够使用 Pythagorean Theorem来计算我们移动了多久。记得几何学中,三角形的斜边=两个直角边的平方和再开根号。
一旦我们得到了距离,我们就可以通过除了速度来得到时间。因为速度=距离/时间。换句话说 时间=距离/速度。
余下的部分就和设置我们target一样了。编译并运行,现在忍者可以射击侵犯的敌人了!
碰撞检测
现在,我们可以看到飞镖到处乱飞了!但是,我们的忍者真正想做的,是能够放倒一些怪物。好吧,让我们增加一些代码来检测什么时候我们的飞镖与怪物相撞了。
在cocos2d里面,有许多方法可以解决这个问题,包括使用cocos2d内置的开源物理引擎box2d和chipmunk。然而,为了使事情变得简单一点,在这里我们自己实现了一个简单的碰撞检测。
为了实现这个,我们首先需要当前场景中存在的飞镖和怪物。在HelloWorldScene类里面增加下面的声明:
NSMutableArray * _projectiles;
然后在init方法里面初使化这些数组:
_projectiles = [[NSMutableArray alloc] init];
我们还需要在dealloc函数里面做一些清理工作,防止内存泄漏:
_targets = nil;
[_projectiles release];
_projectiles = nil;
现在,我们修改addTarget方法,把一个新的target加到targets数组里面,并且为这个target设置一个tag,以便将来使用:
[_targets addObject:target];
然后,修改ccTouchesEnded方法,同样的,把新增加的projectile加到projectiles数组里面,并为之设置一个tag供后面使用:
[_projectiles addObject:projectile];
最后,修改你的spriteMoveFinished方法,基于tag标签来从正确的数组中移除相应的sprite。
[_targets removeObject:sprite];
} else if (sprite.tag == 2 ) { // projectile
[_projectiles removeObject:sprite];
}
编译并运行程序,确保一切都ok。目前来说,应该没有什么可见的差别。但是,接下来我们就会去实现真正的碰撞检测了。
现在,在HelloWorldScene里面增加如下方法:
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];
}
上面的代码应该非常清楚。我们仅仅通过遍历projectiles和targets数组,为每个projectile和target创建边界矩 形,然后使用CGRectIntersectsRect来检测碰撞。如果发现有碰撞了,我们就从场景中移除精灵,同时也把它移除出数组。注意,我们不得不 添加一个toDelete数组,因为我们不能在遍历一个数组的时候去删除数组中的对象。当然,还有许多方式可以实现类似的逻辑,我只不过挑选了简单的方 法。
在你真正完成之前,还差最后一件事情。在你的init方法里面调用下面的函数:
编译并运行,现在,当你的飞镖和怪物相碰的时候,他们都会消失啦!
完成触摸事件
我们离制作一个可以玩的游戏(但是非常简单)的目标已经越来越近了。我们仅仅需要增加一些音效和背景音乐(试想哪个游戏没有声音呢!),再增加一点点简单的逻辑就更好了。
如果你之前看我的博文《关于iphone上面的音效编程》的话,你将会非常高兴,因为使用cocos2d向游戏里音效和背景音乐实在是太简单了。
首先,把一些背景音乐和音效拖到工程的resource文件夹中。你可以使用 cool background music I made 或者我的 awesome pew-pew sound effect,或者自制一些。
然后,在HelloWorldScene.m文件里导入下面的头文件:
在你的init方法里加载背景音乐:
然后,在你的ccTouchesEnded方法里面添加音效代码:
现在,让我们创建一个新的场景,来作为“You Win”或者“You Lose”的标志。右击Classes文件夹,然后选择 File/New File并选择Objective-c class。同时,确保NSObject基类被选中。点击下一步,然后输入 GameOverScene作为文件名,同时确保“Also create GameOverScene.h”复选框打上勾。
然后把GameOverScene.h里面的文件替换成下面的代码:
@interface GameOverLayer : CCColorLayer {
CCLabelTTF * _label;
}
@property (nonatomic, retain) CCLabelTTF * label;
@end
@interface GameOverScene : CCScene {
GameOverLayer * _layer;
}
@property (nonatomic, retain) GameOverLayer * layer;
@end
接下来替换掉GameOverScene.m文件里的内容:
#import " HelloWorldScene.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:[HelloWorld scene]];
}
- ( void )dealloc {
[_label release];
_label = nil;
[super dealloc];
}
@end
注意,这里有两个不同的对象:场景和层。场景可以包含任意数量的层,但是此例中只有一个层。这个层只是在屏幕的中间放置了一个label,然后运行了一个action。这个action的作用就是,等待3秒钟,然后调用一个回调函数切换回HelloWorld场景。
最后,让我们增加一些基本的游戏逻辑。首先,让我们来追踪player销毁的飞镖projectiles。接下来,在HelloWorld类里面增加一个成员变量,如下所示:
在HelloWorldScene.m里面,导入我们的GameOverScene类:
在update方法里,增加(销毁的projectile)计数,同时检测游戏胜利的条件。并在targetsToDelete循环里,紧接着removeChild:target的地方添加如下代码:
if (_projectilesDestroyed > 30 ) {
GameOverScene * gameOverScene = [GameOverScene node];
[gameOverScene.layer.label setString: @" You Win! " ];
[[CCDirector sharedDirector] replaceScene:gameOverScene];
}
最后,让我们这样设计,只要有一个怪物穿过了屏幕左边,你就输了。修改spriteMoveFinished方法,通过在tag==1里面、removeChild:sprite后面添加下面的代码:
[gameOverScene.layer.label setString: @" You Lose :[ " ];
[[CCDirector sharedDirector] replaceScene:gameOverScene];
继续,编译并运行程序。现在,你的游戏可以实现胜利或者失败的场景了!:)
获得源代码
旁边是本教程使用的完整的源代码:simple Cocos2D iPhone game 。
New:cocos2d-x源码下载。
何去何从?
这个项目对于一个cocos2d的初学者来说非常有帮助,而且你还可以自己往项目里面添加更多新的特性。或许你可以尝试一下,添加一个提示框, 提示当前你已经打中了多少个怪物了。或者你可以增加一些很酷的动画,比如怪物被击中后不是直接消失,而是用一段动画来模拟死去。(可以参考cocs2d TestBed里面的ActionsTest,EffectsTest和EffectsAdvancedTest)。或者你还可以增加更多的图片和声音资 源,或者更多的游戏逻辑。心情发挥吧!
如果你想继续这一系列的教程,可以期待一个我的下一篇翻译: 如何增加一个旋转的炮塔:)
如果你想了解更多关于cocos2d的内容,你也可以期待我的下面的一系列的翻译教程:
在cocos2d里面如何制作按钮:简单按钮、单选按钮和开关按钮,
好了,翻译完了!如果你们有什么问题,或者很好的建议,或者学习cocos2d过程中的一些心得和体会,都拿出来,大家一起分享吧!
著作权声明:本文由http://www.cnblogs.com/andyque翻译,欢迎转载分享。请尊重作者劳动,转载时保留该声明和作者博客链接,谢谢!