免责申明(必读!):本博客提供的所有教程的翻译原稿均来自于互联网,仅供学习交流之用,切勿进行商业传播。同时,转载时不要移除本申明。如产生任何纠纷,均与本博客所有人、发表该翻译稿之人无任何关系。谢谢合作!
原文链接地址: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方法用下面的代码替换掉:
-
(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;
}
编译并运行,你的精灵这时候应该出现在正确的位置上了。但是,这个前景默认是黑的。对于这张忍者图片来说,白色的背景可能看起来更好一些。在cocos2d里面,有一种非常简单的方式来改变层的背景颜色,那就是使用CCColoredLayer类。好,跟我来吧!点击HelloWorldScene.h,然后把HelloWorld接口声明改成下面这样:
@interface HelloWorld : CCColorLayer
然后点击HelloWorldScene.m文件,在init方法里面做一些小小的修改,以便使我们能把层的背景颜色改成白的:
if
( (self
=
[super initWithColor:ccc4(
255
,
255
,
255
,
255
)] )) {
编译并运行,这时你将看到你的精灵是在一个白色的背景上面了。哈哈,我们的忍者看起来整装待发呢!
移动目标
接下来,我们想增加一些目标怪物来与我们的忍者战斗。为了使事情变得更加有趣,我想让这些目标可以移动--实际上这也并不是很难!因此,让我们先在屏幕的右边靠外一点点创建一些目标,然后设置一个action,并使之从右边移动到左边。
接下来,紧接着上面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]];
}
在这里我将以一种非常啰嗦的形式来介绍,目的是方便大家理解。第一部分需要解释的是我们之前已经讨论过了的:我们做一些简单的计算来决定把对象放在什么位置,然后设置对象的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前面增加下面的代码:
-
(
void
)spriteMoveFinished:(id)sender {
CCSprite
*
sprite
=
(CCSprite
*
)sender;
[self removeChild:sprite cleanup:YES];
}
这个函数的目的是当精灵飞出屏幕之后,需要移除出当前的scene。这个非常重要,这样的话我们就不会因为屏幕外面积累太多没有用到的精灵而造成内存泄漏。注意,其实还有其它更好的方式来解决这个问题,比如使用一组可以重用的精灵等。不过,对于初学者来说,我在这篇教程里,尽量简单化。
在我们继续之前,还有最后一件事没做。我们需要调用这个方法来创建我们的目标怪物。而且,为了使事情变得更加有趣,我们会随着时间连续不断地发射一些怪物出来。我们可以使用cocos2d的定时scheduler,并指定一个回调函数来完成此功能。一秒钟调用一次回调函数就可以了。因此,在init函数返回之前,我们再加入下面的代码:
[self schedule:@selector(gameLogic:) interval:
1.0
];
然后简单的实现一下这个回调函数,如下:
-
(
void
)gameLogic:(ccTime)dt {
[self addTarget];
}
就这么多!现在编译再运行一下工作,你可以看到怪物在屏幕上面happy地移动了!
发射飞盘
在这里,我们的忍者需要有一些行动了--因此让我们增加一些射击吧!这里有许许多多实现射击的方式,但是在这个游戏里面,我们想让用户触摸一下屏幕,然后飞盘就会从player开始,沿着你触摸的位置发射出来。
我们使用CCMoveTo action来实现这个功能。但是,为了使用这个功能,我们必须首先来做一些数学题。这是因为,CCMoveTo需要我们为飞盘指定目的地。但是我们又不能使用触摸点,因为触摸点仅仅代表飞盘飞的方向。我们实际上想让子弹超过触摸点,然后飞出屏幕之外去。
下面这张图解释了这个问题:
因此,就像你看到的,在触摸点和player之间有一个小的三角形,由origin点,offx和offy组成。我们只需要画一个更大的三角形,同时使用一样的比率就行了。然后我们就可以根据比例算出飞盘飞出屏幕的位置。
好了,让我们看看代码怎么写。首先我们需要让layer能接收touch事件。在你的init方法面添加下面一行代码:
self.isTouchEnabled
=
YES;
由于我们激活了layer的touch,因此我们能够接收到touch事件的回调。这里,我们实现ccTouchesEnded方法,这是在用户完成一次touch之后调用的,代码如下:
-
(
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]];
}
在第一部分,我们选择一个touch来处理,获得它在当前view中的位置,然后调用convertToGL把坐标转换成我们当前层的坐标系中去。这个非常重要,因为我们使用的是landscape模式。
接下来,我们加载飞盘精灵并且设置它的初始位置。然后,我们计算出它需要飞往何处,使用player和touch之间的向量并且根据前面描述的算法计算出来。
注意,这个算法并不完美。我们强迫子弹飞出屏幕x轴的外边--即使在它已经飞出屏幕y轴的外边界了。这里有许多方向来解决这个问题,比如检查飞出屏幕的最短距离,或者使用一个游戏回调函数来检查一个飞盘是否飞出,飞出就移出场景。但是,在这里,我们尽量保持简单。
最后一件事情就是,决定飞盘移动的时间。我们想让子弹以常量速度飞行,不管飞行方向如何。因此,我们不得不再做一点点数学。我们能够使用 Pythagorean Theorem来计算我们移动了多久。记得几何学中,三角形的斜边=两个直角边的平方和再开根号。
一旦我们得到了距离,我们就可以通过除了速度来得到时间。因为速度=距离/时间。换句话说 时间=距离/速度。
余下的部分就和设置我们target一样了。编译并运行,现在忍者可以射击侵犯的敌人了!
碰撞检测
现在,我们可以看到飞镖到处乱飞了!但是,我们的忍者真正想做的,是能够放倒一些怪物。好吧,让我们增加一些代码来检测什么时候我们的飞镖与怪物相撞了。
在cocos2d里面,有许多方法可以解决这个问题,包括使用cocos2d内置的开源物理引擎box2d和chipmunk。然而,为了使事情变得简单一点,在这里我们自己实现了一个简单的碰撞检测。
为了实现这个,我们首先需要当前场景中存在的飞镖和怪物。在HelloWorldScene类里面增加下面的声明:
NSMutableArray
*
_targets;
NSMutableArray
*
_projectiles;
然后在init方法里面初使化这些数组:
_targets
=
[[NSMutableArray alloc] init];
_projectiles
=
[[NSMutableArray alloc] init];
我们还需要在dealloc函数里面做一些清理工作,防止内存泄漏:
[_targets release];
_targets
=
nil;
[_projectiles release];
_projectiles
=
nil;
现在,我们修改addTarget方法,把一个新的target加到targets数组里面,并且为这个target设置一个tag,以便将来使用:
target.tag
=
1
;
[_targets addObject:target];
然后,修改ccTouchesEnded方法,同样的,把新增加的projectile加到projectiles数组里面,并为之设置一个tag供后面使用:
projectile.tag
=
2
;
[_projectiles addObject:projectile];
最后,修改你的spriteMoveFinished方法,基于tag标签来从正确的数组中移除相应的sprite。
if
(sprite.tag
==
1
) {
//
target
[_targets removeObject:sprite];
}
else
if
(sprite.tag
==
2
) {
//
projectile
[_projectiles removeObject:sprite];
}
编译并运行程序,确保一切都ok。目前来说,应该没有什么可见的差别。但是,接下来我们就会去实现真正的碰撞检测了。
现在,在HelloWorldScene里面增加如下方法:
-
(
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];
}
上面的代码应该非常清楚。我们仅仅通过遍历projectiles和targets数组,为每个projectile和target创建边界矩形,然后使用CGRectIntersectsRect来检测碰撞。如果发现有碰撞了,我们就从场景中移除精灵,同时也把它移除出数组。注意,我们不得不添加一个toDelete数组,因为我们不能在遍历一个数组的时候去删除数组中的对象。当然,还有许多方式可以实现类似的逻辑,我只不过挑选了简单的方法。
在你真正完成之前,还差最后一件事情。在你的init方法里面调用下面的函数:
[self schedule:@selector(update:)];
编译并运行,现在,当你的飞镖和怪物相碰的时候,他们都会消失啦!
完成触摸事件
我们离制作一个可以玩的游戏(但是非常简单)的目标已经越来越近了。我们仅仅需要增加一些音效和背景音乐(试想哪个游戏没有声音呢!),再增加一点点简单的逻辑就更好了。
如果你之前看我的博文《关于iphone上面的音效编程》的话,你将会非常高兴,因为使用cocos2d向游戏里音效和背景音乐实在是太简单了。
首先,把一些背景音乐和音效拖到工程的resource文件夹中。你可以使用 cool background music I made 或者我的 awesome pew-pew sound effect,或者自制一些。
然后,在HelloWorldScene.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文件夹,然后选择File/New File并选择Objective-c class。同时,确保NSObject基类被选中。点击下一步,然后输入GameOverScene作为文件名,同时确保“Also create GameOverScene.h”复选框打上勾。
然后把GameOverScene.h里面的文件替换成下面的代码:
#import
"
cocos2d.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
"
GameOverScene.h
"
#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类里面增加一个成员变量,如下所示:
int
_projectilesDestroyed;
在HelloWorldScene.m里面,导入我们的GameOverScene类:
#import
"
GameOverScene.h
"
在update方法里,增加(销毁的projectile)计数,同时检测游戏胜利的条件。并在targetsToDelete循环里,紧接着removeChild:target的地方添加如下代码:
_projectilesDestroyed
++
;
if
(_projectilesDestroyed
>
30
) {
GameOverScene
*
gameOverScene
=
[GameOverScene node];
[gameOverScene.layer.label setString:
@"
You Win!
"
];
[[CCDirector sharedDirector] replaceScene:gameOverScene];
}
最后,让我们这样设计,只要有一个怪物穿过了屏幕左边,你就输了。修改spriteMoveFinished方法,通过在tag==1里面、removeChild:sprite后面添加下面的代码:
GameOverScene
*
gameOverScene
=
[GameOverScene node];
[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里面如何制作按钮:简单按钮、单选按钮和开关按钮,
Box2D入门教程,
如何使用box2d来制作一个简单的Breakout游戏
好了,翻译完了!如果你们有什么问题,或者很好的建议,或者学习cocos2d过程中的一些心得和体会,都拿出来,大家一起分享吧!
著作权声明:本文由http://www.cnblogs.com/andyque翻译,欢迎转载分享。请尊重作者劳动,转载时保留该声明和作者博客链接,谢谢!