1
译者:杨栋
邮箱:yangdongmy@gmail.com
2
第四章
你的第一个游戏
本章将会制作你的第一个游戏。它不会为你带来任何奖项,但是你将在这里学
到如何让基本的cocos2d元素一起工作。我将一步步为你讲解制作的过程,所
以你也会学习到一些Xcode使用方面的知识。
我们将要制作的游戏和目前很出名的Doodle Jump(涂鸦跳跃)游戏相反,比
较恰当的名字应该是Doodle Drop(涂鸦降落)。玩家的目标是通过晃动iOS设
备来移动精灵,以躲开从空中飞下的蜘蛛怪物。图4-1是游戏最终完成后的效果
图:
图4-1.最终完成的DoodleDrop游戏
3
项目设置步骤
现在打开Xcode,我将一步步引导你。在Xcode里,选择File ➤ New Project…,然
后选择如图4-2所示的cocos2d程序模板。当被问及新项目的名称时,输入
DoodleDrop,然后选择一个地方保存项目文件。Xcode会自动生成一个叫
DoodleDrop的子文件夹,这样你就不用手动生成了。
图4-2. 生成基于cocos2d程序模板的项目
Xcode会为你呈现如图4-3所示的项目视图。我已经在Xcode里展开了Classes
和Resources这两个组,你将在那里添加源代码(Classes)和资源文件
(Recources)。任何不是源代码的文件都可以被认为是资源,比如图片,声
音文件,文字文件,或者plist文件,等等。我不要求你把相关的文件非常仔细
地分类,但是如果你把类似的文件放在一起的话,下次你就能更容易地找到它
们。
4
图 4-3.当前的DoodleDrop项目是基于HelloWorld的cocos2d项目模板的。你要确保将程序和资
源文件添加到相应的Classes和Resources这两个组中,让项目组织有序。
下一步你要作出一个决定:你是使用已有的HelloWorldScene作为起点,之后再
把名字改成现在的项目名称呢?还是创建自己的场景呢?我选择后者。因为你
迟早需要添加新的场景,所以还不如现在就学习如何从头创建一个新的场景。
请确定已选择Classes组,然后在顶部菜单选择File ➤ New File… 或者右键点击
Classes,在弹出菜单上选择Add ➤ New File…,打开如图4-4所示的New File对话
框。因为cocos2d自带了很多重要节点的类模板,不使用它们就太浪费了!在
User Templates下,选择cocos2d 0.99.4(或者你当前系统安装的版本),然
后选择CCNode类,请确定下方选择框中选择的是“Subclass of CCLayer”。
5
图4-4. 添加新CCNode继承类的最好方式是通过使用cocos2d提供的类模板。在我们的例子中,
因为我们要创建一个新的场景,所以选择的CCNode类是CCLayer的子类(subclass)。
如图4-5所示,New File对话框打开。我倾向于使用功能来命名类。我在这里使
用了GameScene.m来命名,因为这里是进行DoodleDrop游戏的场景。不要忘记勾
选 Also create “GameScene.h”和DoodleDrop Target之前的复选框。目标
(Targets)是Xcode用来决定要生成哪些可执行文件的方式。比如,iPad版本
的游戏通常是作为一个分开的目标(Target)来生成的。我们的例子只有一个
目标,但是如果你创建了一个iPad目标的话,你要确保iPad的高分辨率图片不
会被错误地加到iPhone或iPod Touch目标中去。
注:不检查目标设置的话可能会带来很多问
题
快递公司问题件快递公司问题件货款处理关于圆的周长面积重点题型关于解方程组的题及答案关于南海问题
,从编译错误到“无法找到文件”
错误,或者由于文件没有被添加到正确的目标里面,导致游戏运行时崩溃。或
者由于你把文件放进了错误的目标里而浪费了空间,比如你把给iPad和iPhone4
使用的高分辨率图片添加到了普通iPhone或iPod Touch的目标里面。
6
图4-5. 给新场景命名,并且确保它被添加到正确的目标(target)中。
目前,我们的GameScene类是空的,为了将它设置为一个场景,我们要做的第一
件事是在里面添加+(id)scene方法。我们在这里添加的代码和第三章的基本上
一样,只是层的类名称改变而已。几乎在任何一个类里面你都需要-(id)init和
-(void)dealloc这两个方法,所以我们现在就加进去。我也把在第三章里介绍
过的日志声明加了进去。列表4-1是完成的GameScene.h,列表4-2是完成的
GameScene.m:
列表 4–1. GameScene.h和场景方法
#import
#import "cocos2d.h"
@interface GameScene : CCLayer
{
}
+(id) scene;
@end
列表 4–2. GameScene.m和场景方法,外加一些标准方法,包括日志记录
#import "GameScene.h"
@implementation GameScene
+(id) scene
{
CCScene *scene = [CCScene node];
CCLayer* layer = [GameScene node];
7
[scene addChild:layer];
return scene;
}
-(id) init
{
if ((self = [super init]))
{
CCLOG(@"%@: %@", NSStringFromSelector(_cmd), self);
}
return self;
}
-(void) dealloc
{
CCLOG(@"%@: %@", NSStringFromSelector(_cmd), self);
// 不要忘记调用 [super dealloc]
[super dealloc];
}
@end
现在你可以放心地删除HelloWorldScene类了。选择HelloWorldScene.h和
HelloWorldScene.m两个文件,在顶部菜单选择Edit ➤ Delete,或者右键点击选
中的文件,在弹出菜单中选择Delete,然后在删除弹出对话框中,选择Also
Move to Trash选项,它会把文件从硬盘上删除。删除了HelloWorldScene类以
后,你必须把DoodleDropAppDelegate.m里对HelloWorldScene的引用改为引用
GameScene。列表4-3粗体显示了需要作出的修改。我也把设备方向改为
Portrait(纵向)模式,因为这个方向最适合这个游戏的设计。
列表4-3.把DoodleDropAppDelegate.m里对HelloWorldScene的引用改为引用GameScene
// 用以下代码替换 #import “HelloWorldScene.h”
#import "GameScene.h"
- (void) applicationDidFinishLaunching:(UIApplication*)application
{
// 设置纵向模式
[director setDeviceOrientation:kCCDeviceOrientationPortrait];
// 用GameScene替换HelloWorld
[[CCDirector sharedDirector] runWithScene: [GameScene scene]];
}
编译代码然后运行,你应该会得到...一个空白的场景。成功!如果与遇到任何
的问题,将你的代码和本书自带的DoodleDrop01源代码做个比较,这会有助于
找到问题所在。
添加主角精灵(Player Sprite)
接下去是添加主角精灵,你将会用加速计来控制精灵的动作。要添加用于显示
8
主角的图片,在Xcode里选择Resources组,然后选择Project ➤ Add to Project…,
或者右键点击Resources组,在弹出菜单中选择Add ➤ Existing Files…,打开文件
选择对话框。主角的图片在本书提供的DoodleDrop项目的Resources文件夹里。
你也可以选用自己的图片,只要它的尺寸是64x64像素即可。
注:iOS游戏首选的图片格式是PNG,Portable Network Graphics。它虽然是种
压缩格式,但是与JPG不同的是,PNG是无损压缩,它将源图片的所有像素都保
留了。你也可以把图片存为无压缩JPG格式,但是一般情况下同样的图片用PNG
格式比无压缩JPG格式要小一些。不过,这个区别只影响应用程序的大小,它和
内存的使用无关。
如图4-6所示,Xcode会问你要把图片存到哪里,以什么方式存储。请确保在Add
To Targets选择区域中,勾选所有要用到此图片的目标前的复选框。在我们的
游戏里,只有一个目标:DoodleDrop,所以我们可以直接使用默认设置。不过,
当你开始制作比较大的项目时,你最好花点时间研究一下这些设置。这是Xcode
的基础知识。苹果的Xcode文档描述了Xcode是如何管理项目文件的:
http://developer.apple.com/mac/library/documentation/DeveloperTools/Conceptual/XcodeProj
ectManagement/130-Files_in_Projects/project_files.html
图4-6. 当你添加资源文件时,你将看到这个对话框。大多数情况下,你应该使用默认设置。
现在我们将把主角精灵添加到游戏场景中。我把主角精灵作为一个 CCSprite
成员变量添加到GameScene类中。目前为止我们的游戏还很简单,所以把所有东
西都放到同一个类里不会有什么问题。一般来说,我不建议这样做。在以后的
9
几个项目里,为了培养你良好的代码设计习惯,我会为各个游戏组件创建各自
的类。
列表4-4: 演示如何在GameScene的头文件中添加 CCSprite 成员变量
#import
#import "cocos2d.h"
@interface GameScene : CCLayer
{
CCSprite* player;
}
+(id) scene;
@end
在列表4-5代码中,我添加了用于初始化精灵的 init 方法。然后将精灵赋值给
成员变量。最后将它放置在屏幕下方的中央处。在这里我也启用了加速计功能。
列表4-5.启用加速计功能,生成和放置主角精灵
-(id) init
{
if ((self = [super init]))
{
CCLOG(@"%@: %@", NSStringFromSelector(_cmd), self);
self.isAccelerometerEnabled = YES;
player = [CCSprite spriteWithFile:@"alien.png"];
[self addChild:player z:0 tag:1];
CGSize screenSize = [[CCDirector sharedDirector] winSize];
float imageHeight = [player texture].contentSize.height;
player.position = CGPointMake(screenSize.width / 2, imageHeight / 2);
}
return self;
}
主角精灵(player)作为子节点被添加到场景中。它的tag值是1,以后,这个
tag值将被用于寻找主角精灵,并且用此tag同其它精灵分开。你会注意到我没
有保留(retain)主角精灵。因为我们将主角精灵作为子节点添加到了场景中,
cocos2d会自动将其保留,也因为主角精灵需要一直存在于场景中,所以我们不
需要手动保留它。不保留对象而由另一个类或对象来管理它的内存使用,叫作
“保持弱引用”。
注:请注意,iOS设备上的文件名是区分大小写的。如果你加载Alien.png或者
ALIEN.PNG,在模拟器上运行程序不会出现问题,但是在iOS设备上就不行了,
因为我们使用的图片名是全小写的alien.png。所有的文件名都使用小写是个很
好的习惯。
我们把主角精灵X轴的值设为屏幕宽度的一半,这样精灵就会被放置在水平方向
的中央位置。纵向上我要让主角精灵的贴图同屏幕底部对齐。因为精灵的贴图
是和精灵节点的位置对齐的,所以如果将精灵在纵向上放置于(0,0)点的话,
10
精灵贴图下半部分就会处于屏幕以下。我们当然不希望发生这样的事情;我们
需要把精灵的位置向上移动贴图高度的一半大小,让精灵贴图全部显示出来。
通过调用[playertexture].contentSize.height,我们可以得到精灵的贴图大
小(contentSize)。那么什么是“精灵的贴图大小”呢?在第三章,我提到过
iOS上的贴图尺寸必须符合“2的n次方”的规定。但是实际的图片尺寸可能小于
所要求的贴图尺寸。例如,如果图片大小是100x100像素,iOS上的贴图尺寸其
实是128x128像素。contentSize属性返回的是图片的实际尺寸100x100像素。在
绝大多数情况下,你需要的是图片的实际大小尺寸,而非贴图尺寸。
通过将精灵的Y轴值设置为图片高度的一半,主角精灵的贴图就可以和屏幕底部
对齐了。
注:任何时候你都应该避免使用固定的位置。如果你只是简单的把主角精灵的
位置设为(160, 32)的话,你做了两个其实应该避免的假设。第一个假设:你假
设屏幕尺寸是320像素,但是实际上并不是每个iOS设备屏幕都是这个尺寸。第
二个假设:你假设图片的高度是64像素,但是这个高度可能会改变。一旦你开
始做出类似的假设,你会习惯于在整个项目里做出这样的假设。
我所用的代码虽然会多一些,但是从长远来说是很有好处的。你可以在不同的
设备上部署这些代码,你也可以使用不同尺寸的图片文件。你再也不需要改变
这部分代码。程序员最耗时的工作是改变那些基于假设的代码。
想像一下,项目开始三个月以后。你的游戏中有很多图片和对象,如果你想创
建一个iPad版本的话,你必须改变所有用到固定尺寸的地方,包括那些用到固
定图片尺寸的地方。如果你要创建一个iPhone4的视网膜屏版本的话,你还需要
再做一遍同样的事情。在那以后,你将会有3个不同的Xcode项目需要维护。最
终会导致“复制和粘贴”所造成的混乱,那就太糟糕了!
加速计输入
现在是最后一步,完成以后我们就可以晃动iPhone让主角精灵移动了。就如我
在第三章演示的那样,你必须在接收加速计输入的层添加加速计方法。这里我
将acceleration.x参数乘以10,然后把得到的数值和主角精灵的位置相加,乘以
10 的目的是为了加快主角精灵的移动速度。
-(void) accelerometer:(UIAccelerometer *)accelerometer
didAccelerate:(UIAcceleration *)acceleration
{
CGPoint pos = player.position;
pos.x += acceleration.x * 10;
player.position = pos;
}
注意到上述代码中奇怪的地方了吗?本来可能一行代码就足够的地方,我却用
了三行。
11
// ERROR: lvalue required as left operand of assignment
player.position.x += acceleration.x * 10;
不过,不像其它编程语言如Java,C++和C#,上面的代码不能够改变Objective-
C的属性。
这个问题与Objective-C属性的工作方式和C语言给变量赋值的方式有关。
player.position.x实际上调用的是位置的获取方法(getter method):[player
position]。这个方法会获取当前主角精灵的临时位置信息,上述一行代码实际
上是在尝试着改变这个临时CGPoint中成员变量x的值。不过这个临时的CGPoint
是要被丢弃的。在这种情况下,精灵位置的设置方法(setter method):
[player setPosition]根本不会被调用。你必须直接赋值给player.position这
个属性,这里使用的值是一个新的CGPoint。在使用Objective-C的时候,你必
须习惯于这个规则,而唯一的办法是改变你从Java,C++或C#里带来的编程习惯。
第一次测试运行
现在你的项目应该具备与DoodleDrop02项目一样的代码了。试着运行一下。因
为在模拟器下无法使用加速计,所以请务必在真实iOS设备上运行你的代码,
测试一下加速计的运行状况。
如果你的Xcode中还没有安装Development Provisioning Profiles的话,你将会
得到一个CodeSign出错信息。要想在真实iOS设备上运行你的代码,必须首先
做Code Signing。请参照以下链接了解如何生成和安装必要的Development
Provisioning Profiles:
(https://developer.apple.com/iphone/manage/provisioningprofiles/howto.action).
注:要访问上述链接,你必须拥有一个付费的苹果开发者帐号,99美元一年。
主角精灵的速率
你可能注意到加速计的反应有些慢,动作不是很流畅。那是因为主角精灵没有
做到真正的加速和减速。让我们修正一下这个问题。修改过的代码在
DoodleDrop03项目文件夹下。
实现加速和减速的方式不是直接去改变主角精灵的位置信息,而是使用一个单
独的CCPoint变量作为速度矢量。
每一次系统接收到一个加速计事件,速度变量就会累计从加速计那里得到的输
入数值。当然,这就意味着我们必须给这个速度变量设置一个最大值,以防主
角精灵在减速上花费太多时间。然后,在每一帧里面,不管系统是否接收到加
速计事件,都将速度变量和当前主角精灵的位置相加。
12
注:为什么不使用动作(actions)直接移动主角精灵呢?当一个物体需要经常改
变它的速度或方向时(每秒钟好几次),选择动作来移动它就不是个好选择了。
因为动作是设计用于存在时间相对较长的物体的,所以频繁的生成动作会增加
系统分配和释放内存的开销,降低程序运行效率。
更糟糕的是,如果你不为动作的运行留出时间的话,动作根本就不会起作用。
这也是为什么在每一帧里都用新的动作去替换旧的动作时,新动作不会产生任
何作用的原因。因为新的动作根本没有时间去替换旧的动作。很多cocos2d开发
者碰到过这个看起来很奇怪的问题。
比如,你把当前的动作都停止,然后在下一帧里添加MoveBy动作,这个MoveBy
动作就完全不会起作用!因为MoveBy动作只会在再下一帧里改变物体的位置。
但是到再下一帧的时候,你已经停止了所有的当前动作,同时添加了另一个
MoveBy动作。这样重复的动作是不会移动物体的。
让我们看一下所作的代码修改。在以下代码里,playerVelocity变量被添加到
头文件中:
@interface GameScene : CCLayer
{
CCSprite* player;
CGPoint playerVelocity;
}
如果你不理解为什么我使用了CGPoint而不是float,那是因为我们也可能在将
来让主角精灵上下移动,所以用CGPoint可以让代码的扩展性更好。
列表4-6展示了处理加速计事件的代码,我用速度变量来间接更新主角精灵的位
置而不是直接更新主角精灵。以下代码使用了三个新的“设计参数”:减速度
速率,加速计敏感度和最大速度。这三个数值没有最佳值可用:你需要做些调
整以找到符合你的游戏设计的最佳设定,这也是为什么它们被称为“设计参数”
的原因。
减速是通过降低当前速度值来实现的。在降低速度值以后,要将新的加速度值
和加速计敏感度值相乘所得的值与降低后的速度值相加。减速度速率越小,主
角精灵就能越快的改变蜘蛛的方向。加速计敏感度的值越大,主角精灵对加速
计的输入就越敏感。因为这三个参数一起影响着同一个值,所以在调整的时候
要注意每次只调整一个值。
列表4-6.GameScene的头文件设置了playerVelocity变量
-(void) accelerometer:(UIAccelerometer *)accelerometer
didAccelerate:(UIAcceleration *)acceleration
{
// 控制减速的速率(值越低=可以更快的改变方向)
float deceleration = 0.4f;
//加速计敏感度的值越大,主角精灵对加速计的输入就越敏感
float sensitivity = 6.0f;
13
// 最大速度值
float maxVelocity = 100;
// 基于当前加速计的加速度调整速度
playerVelocity.x = playerVelocity.x * deceleration + acceleration.x *
sensitivity;
// 我们必须在两个方向上都限制主角精灵的最大速度值
if (playerVelocity.x > maxVelocity)
{
playerVelocity.x = maxVelocity;
}
else if (playerVelocity.x < - maxVelocity)
{
playerVelocity.x = - maxVelocity;
}
}
现在,playerVelocity的值会发生变化了,但是我们如何使用它来影响主角精
灵的位置呢?通过在GameScene的init方法中预约更新方法来实现。将以下代码
添加到GameScene的init方法中:
// 设置在每一帧都运行的预约方法:–(void) update:(ccTime)delta
[self scheduleUpdate];
如列表4-7所示,你需要添加 -(void)update: (cc Time)delta 方法。这个预
约的更新方法每一帧都会被调用,我们就在这里用速度值来影响主角精灵的位
置。这样无论加速计输入事件发生的频率有多大,主角精灵都能很平滑流畅的
移动。
列表4-7. 用当前速度更新主角精灵的位置
-(void) update:(ccTime)delta
{
// 用playerVelocity持续增加主角精灵的位置信息
CGPoint pos = player.position;
pos.x += playerVelocity.x;
// 如果主角精灵移动到了屏幕以外的话,它应该被停止
CGSize screenSize = [[CCDirector sharedDirector] winSize];
float imageWidthHalved = [player texture].contentSize.width * 0.5f;
float leftBorderLimit = imageWidthHalved;
float rightBorderLimit = screenSize.width - imageWidthHalved;
// 以防主角精灵移动到屏幕以外
if (pos.x < leftBorderLimit)
{
pos.x = leftBorderLimit;
playerVelocity = CGPointZero;
14
}
else if (pos.x > rightBorderLimit)
{
pos.x = rightBorderLimit;
playerVelocity = CGPointZero;
}
// 将更新过的位置信息赋值给主角精灵
player.position = pos;
}
边界测试可以防止主角精灵离开屏幕。我们需要将精灵贴图的contentSize考虑
进来,因为精灵的位置在精灵贴图的中央,但是我们不想让贴图的任何一边移
动到屏幕外面。所以我们计算得到了imageWidthHalved值,并用它来检查当前
的精灵位置是不是落在左右边界里面。上述代码可能有些啰嗦,但是这样比以
前更容易理解。这就是所有与加速计处理逻辑相关的代码。
注:在计算imageWidthHalved时,我们将contentSize乘以0.5,而不是用它除以
2。这是一个有意的选择,因为除法可以用乘法来代替以得到同样的计算结果。
因为上述更新方法在每一帧都会被调用,所以所有代码必须在每一帧的时间里
以最快的速度运行。因为iOS设备使用的ARM CPU不支持直接在硬件上做除法,
乘法一般会快一些。虽然在我们的例子里效果并不明显,但是养成这个习惯对
我们很有好处。
添加障碍
如果我们的游戏没有什么东西可以让主角避让的话,那就太无聊了。
DoodleDrop04项目中包含了一个令人憎恶的东西:六条腿的大蜘蛛。谁不想避
开它们呢?
和主角精灵一样,你需要把spider.png添加到Resources组中。然后在
GameScene.h的interface中添加三个新的成员变量。第一个是CCArray*
spiders,它的类引用展示在列表4-9中。另外两个变量是spiderMoveDuration
和numSpidersMoved,它们被用于列表4-12所示的代码中。
@interface GameScene : CCLayer
{
CCSprite* player;
CGPoint playerVelocity;
CCArray* spiders;
float spiderMoveDuration;
int numSpidersMoved;
}
15
我也在GameScene的init方法中添加了对initSpiders方法的调用,紧跟
scheduleUpdate方法之后:
-(id) init
{
if ((self = [super init]))
{
…CHAPTER 4: Your First Game 79
[self scheduleUpdate];
[self initSpiders];
}
return self;
}
在GameScene类中添加了上述代码之后,我们创建如列表4-8所示的initSpiders
方法,此方法用于生成蜘蛛精灵。
列表4-8:为了方便使用,用一个CCArray将蜘蛛精灵们放在一起
-(void) initSpiders
{
CGSize screenSize = [[CCDirector sharedDirector] winSize];
// 利用一个临时的蜘蛛精灵以得到蜘蛛图片的大小
CCSprite* tempSpider = [CCSprite spriteWithFile:@"spider.png"];
float imageWidth = [tempSpider texture].contentSize.width;
// 计算可以在同一行的水平方向上同时显示的蜘蛛个数
int numSpiders = screenSize.width / imageWidth;
// 用alloc初始化蜘蛛数组
spiders = [[CCArray alloc] initWithCapacity:numSpiders];
for (int i = 0; i < numSpiders; i++)
{
CCSprite* spider = [CCSprite spriteWithFile:@"spider.png"];
[self addChild:spider z:0 tag:2];
// 将蜘蛛精灵添加到数组
[spiders addObject:spider];
}
// 调用方法以排列蜘蛛
[self resetSpiders];
}
上述代码有一些需要注意的地方。我生成了一个临时的CCSprite(tempSpider)
以得到精灵图片的宽度。在之后的代码中,此宽度用于计算在同一行的水平方
向上同时显示的蜘蛛个数。得到图片大小最简单的方法就是生成一个临时
16
CCSprite精灵。你会注意到我没有把tempSpider作为子节点添加到其它节点中。
这意味着tempSpider的内存会被自动释放。
和上述做法不同,用于放置蜘蛛精灵的数组必须使用alloc来生成;否则数组的
内存会被释放,之后访问精灵数组的话就会导致程序因为EXC_BAD_ACCESS错误
而崩溃。并且因为是我在控制精灵数组的内存,我必须在dealloc方法中释放蜘
蛛精灵数组,如下所示:
-(void) dealloc
{
CCLOG(@"%@: %@", NSStringFromSelector(_cmd), self);
// 因为蜘蛛精灵数组由[CCArray alloc]生成,此数组必须在这里被释放
[spiders release];
spiders = nil;
// 永远不要忘记调用 [super dealloc]!
[super dealloc];
}
在撰写本书时,CCArray类还没有相关的文档,但是cocos2d完全支持它。你可
以在Xcode项目中的cocos2d/Support组找到CCArray的类文件。cocos2d引擎内
部使用了CCArray。它和苹果官方的NSMutableArray类似,但是运行效率更高。
CCArray类实现了NSArray和NSMutableArray的子集,并且添加一些从NSArray初
始化到CCArray的新方法。CCArray通过将数组中的最后一个对象(nil)赋值给删
除的位置,实现了fastRemoveObject和fastRemoveObjectAtIndex两个方法。避
免了复制部份数组的内存。这让删除数组中的元素变的更快,不过这也意味着
CCArray中的对象会改变位置。如果你的程序依赖于指定的对象排列次序,你就
不应该使用fastRemoveObject这类方法。在列表4-9中,你可以看到CCArray的
类引用 – 它没有实现所有的NSArray和NSMutableArray方法:
列表4-9. CCArray的类引用
+ (id) array;
+ (id) arrayWithCapacity:(NSUInteger)capacity;
+ (id) arrayWithArray:(CCArray*)otherArray;
+ (id) arrayWithNSArray:(NSArray*)otherArray;
- (id) initWithCapacity:(NSUInteger)capacity;
- (id) initWithArray:(CCArray*)otherArray;
- (id) initWithNSArray:(NSArray*)otherArray;
- (NSUInteger) count;
- (NSUInteger) capacity;
- (NSUInteger) indexOfObject:(id)object;
- (id) objectAtIndex:(NSUInteger)index;
- (id) lastObject;
- (BOOL) containsObject:(id)object;
17
#pragma mark Adding Objects
- (void) addObject:(id)object;
- (void) addObjectsFromArray:(CCArray*)otherArray;
- (void) addObjectsFromNSArray:(NSArray*)otherArray;
- (void) insertObject:(id)object atIndex:(NSUInteger)index;
#pragma mark Removing Objects
- (void) removeLastObject;
- (void) removeObject:(id)object;
- (void) removeObjectAtIndex:(NSUInteger)index;
- (void) removeObjectsInArray:(CCArray*)otherArray;
- (void) removeAllObjects;
- (void) fastRemoveObject:(id)object;
- (void) fastRemoveObjectAtIndex:(NSUInteger)index;
- (void) makeObjectsPerformSelector:(SEL)aSelector;
- (void) makeObjectsPerformSelector:(SEL)aSelector withObject:(id)object;
- (NSArray*) getNSArray;
在列表4-8的结束处,我调用了[self resetSpiders]方法;列表4-10展示了这
个方法。将精灵初始化和放置精灵分开是因为游戏最终是要结束的。结束以后
游戏需要重新设置。最有效率的做法是将所有游戏对象移到它们的初始位置。
但是,一旦你的游戏变的更加复杂,这个做法就行不通了。最后,最简单的方
法是将整个场景重新加载,代价是玩家需要等待场景被重新加载。
列表4-10. 重设蜘蛛精灵们的位置
-(void) resetSpiders
{
CGSize screenSize = [[CCDirector sharedDirector] winSize];
// 生成一个临时蜘蛛对象,得到它的图片宽度
CCSprite* tempSpider = [spiders lastObject];
CGSize size = [tempSpider texture].contentSize;
int numSpiders = [spiders count];
for (int i = 0; i < numSpiders; i++)
{
// 将生成的蜘蛛放在屏幕外制定的位置
CCSprite* spider = [spiders objectAtIndex:i];
spider.position = CGPointMake(size.width * i + size.width * 0.5f, screenSize.height +
size.height);
[spider stopAllActions];
}
// 将预约的方法取消掉。如果没有方法之前没有被预约过的话,这行代码不会产生任何作用
[self unschedule:@selector(spidersUpdate:)];
18
// 预约蜘蛛的更新方法,用指定的时间间隔进行调用
[self schedule:@selector(spidersUpdate:) interval:0.7f];
numSpidersMoved = 0;
spiderMoveDuration = 4.0f;
}
在上述代码中我再次使用了一个临时蜘蛛精灵以得到图片尺寸。这次我没有生
成新的蜘蛛精灵,因为已经存在一个蜘蛛精灵的数组。而且因为所有蜘蛛精灵
使用的是同一个图片,所以我可以使用任意一个蜘蛛精灵。因此我直接使用了
数组的最后一个蜘蛛精灵。
然后通过修改每个蜘蛛的位置信息,我们将所有蜘蛛横向排列开来。在x坐标轴
上我们多加了半个图片的宽度(因为精灵贴图是居中放置在精灵节点的位置上
的)。至于高度的设定,每一个蜘蛛被放置在屏幕外离开屏幕顶部一个蜘蛛图
片高度的地方。这个高度是任意设置的。只要图片在屏幕之外不可见就可以了。
因为在蜘蛛位置重置的时候,有的蜘蛛有可能还在移动,所以我在这里停止了
所有的动作。
技巧:如果不是绝对必要,不要在for或者其它循环里的条件判断中调用方法,
这样可以节省几个CPU循环。在我们的例子中,我使用了一个名为 numSpiders
的变量,用于存放[spiders count]的计算结果。然后在for循环的条件判断中
使用 numSpiders 而不是[spiders count]。因为spiders数组并没有在for循环
里面被修改,所以此数组的元素个数在循环过程中是不会改变的。这也是为什
么我可以将此计算结果保存在变量中,而不至于在for循环的条件判断中反复调
用[spiders count]。
我预约了spidersUpdate:这个方法,让它每0.7秒运行一次,也就是说每0.7秒
屏幕上方将有一个蜘蛛降落下来。但是在预约之前,我通过使用:[self
unschedule:@selector(spidersUpdate:)],来确保spidersUpdate:这个方法还
没有被预约过。如果不这样做的话,你在调用resetSpiders方法的同时,
spidersUpdate:可能还是会运行,这样的话实际上是将spiderUpdate:这个方法
运行了两次,也就是让蜘蛛降落的频率增加了一倍。列表4-11中的
spidersUpdate:方法会挑选一个已存在的蜘蛛,检查一下它是不是处于闲置状
态,如果发现蜘蛛处于闲置状态,就使用一系列的动作让它从天而降:
列表4-11.spidersUpdate:方法会让蜘蛛经常从天而降
-(void) spidersUpdate:(ccTime)delta
{
// 尝试着寻找一个目前闲置不动的蜘蛛
for (int i = 0; i < 10; i++)
{
int randomSpiderIndex = CCRANDOM_0_1() * [spiders count];
CCSprite* spider = [spiders objectAtIndex:randomSpiderIndex];
19
// 如果蜘蛛不动的话,应该没有任何正在运行的动作
if ([spider numberOfRunningActions] == 0)
{
// 以下是控制蜘蛛运动的动作序列
[self runSpiderMoveSequence:spider];
// 每次只能有一只蜘蛛会开始移动
break;
}
}
}
你可能觉得奇怪,为什么我一定要经过10次循环才拿到一个随机的蜘蛛呢?因
为我并不知道与随机生成的索引相关的蜘蛛是否闲置不动,所以我想确保找到
一个目前闲置不动的蜘蛛。如果经过10次尝试(10这个数字是任意的,也可能
是20)还找不到的话,那么我就会跳过目前的更新,等待下一次更新。
我也可以使用do/while循环一直寻找闲置不动的蜘蛛。但是,这取决于我们的
“设计参数”,比如新蜘蛛被投放的频率,有可能所有的蜘蛛都在同一时间内
移动;如果那样的话,游戏就会被锁定,因为系统在无止境的寻找闲置不动的
蜘蛛。此外,即使游戏不能够在几秒钟内投放新的蜘蛛,也没有什么关系。不
过,如果你查看一下DoodleDrop04项目的代码,你会看到我添加了一个日志功
能,用于打印出发现一只闲置蜘蛛所需要尝试的次数。
因为蜘蛛们只会执行移动序列动作,所以我可以通过检查蜘蛛是否在运行任何
动作来确定它是不是闲置不动。我们接下来讨论列表4-12中与之相关的
runSpiderMoveSequence方法:
列表4-12. 蜘蛛的移动是由动作序列来 控制的
-(void) runSpiderMoveSequence:(CCSprite*)spider
{
// 随着时间慢慢增加蜘蛛的移动速度
numSpidersMoved++;
if (numSpidersMoved % 8 == 0 && spiderMoveDuration > 2.0f)
{
spiderMoveDuration -= 0.1f;
}
// 用于控制蜘蛛移动的动作序列
CGPoint belowScreenPosition = CGPointMake(spider.position.x,
20
-[spider texture].contentSize.height);
CCMoveTo* move = [CCMoveTo actionWithDuration:spiderMoveDuration
position:belowScreenPosition];
CCCallFuncN* call = [CCCallFuncN actionWithTarget:self
selector:@selector(spiderBelowScreen:)];
CCSequence* sequence = [CCSequence actions:move, call, nil];
[spider runAction:sequence];
}
RunSpiderMoveSequence方法的作用是跟踪已被放下的蜘蛛数量。每次到第八个
蜘蛛时,spiderMoveDuration的值就会被减少,从而提高所有蜘蛛的移动速
度。%这个符号叫作“余数运算子”(Modulus Operator),用于得到运用除法
以后得到的余数。比如,如果numSpidersMoved可以用8除尽,那么“余数运算
子”的计算结果就应该是0。
这里用到的动作序列只有一个CCMoveTo动作和一个CCCallFuncN动作。你可以改
进蜘蛛的行为,比如让它往下移动一点,等个几秒钟,然后一直移动到底部,
就像真的邪恶的蜘蛛通常会做的那样。我将把具体的做法留给你去发挥。我选
择CCCallFuncN的目的是给spiderBelowScreen方法传递蜘蛛精灵作为它的
sender变量。这样的话,当某只蜘蛛到达屏幕底部时,我就可以直接引用那个
蜘蛛,不需要再去到处找了。列表4-13的代码会在蜘蛛移动到超出屏幕底部以
后,通过重新设置蜘蛛位置让它回到刚好超出屏幕顶部的位置:
列表4-13. 重新设置蜘蛛位置,这样它就可以再次从天而降。
-(void) spiderBelowScreen:(id)sender
{
// 确保传进来的sender参数是我们需要的类
NSAssert([sender isKindOfClass:[CCSprite class]], @"sender is not a CCSprite!");
CCSprite* spider = (CCSprite*)sender;
// 将蜘蛛移到刚好超出屏幕顶部的位置
CGPoint pos = spider.position;
CGSize screenSize = [[CCDirector sharedDirector] winSize];
pos.y = screenSize.height + [spider texture].contentSize.height;
spider.position = pos;
}
21
注:作为一名防守型的程序员,因为我假设了sender将会是一个CCSprite,但
是传进来的参数可能不是这个类,所以我使用了NSAssert来确定sender是一个
CCSprite。
实际上,我在第一次写上述代码的时候,使用了CCCallFunc,而不是
CCCallFuncN。使用CCCallFunc的结果是sender为nil,因为CCCallFunc不会将
sender参数传进来。因为sender是nil,isKindOfClass将不会被调用,导致返
回值为nil,所以NSAssert报告了这个问题。由于NSAssert的提示,我轻松地找
到了问题所在。
一旦确认了sender是一个CCSprite,我就可以将它转换成一个CCSprite,然后
用它来调整精灵的位置。现在你应该已经熟悉调整位置的过程了。
一切顺利。你可以试着玩一下游戏,然后你就会发现少了些什么东西。提示:
请看以下标题。
碰撞测试
你可能惊奇地发现,原来碰撞测试可以如列表4-14代码所示的那样简单。当然,
以下代码只是测试了主角精灵和所有蜘蛛精灵之间的距离,这类碰撞测试只做
径向测试(radial check)。不过对于我们的游戏,这样的测试足够了。我把
对[self checkForCollision]的调用放到了-(void) update: (ccTime)delta方
法的结束处。
列表4-14. 简单的径向碰撞测试可以满足我们的要求
-(void) checkForCollision
{
// 假设:主角精灵和蜘蛛精灵使用的图片都是正方形的
float playerImageSize = [player texture].contentSize.width;
float spiderImageSize = [[spiders lastObject] texture].contentSize.width;
float playerCollisionRadius = playerImageSize * 0.4f;
float spiderCollisionRadius = spiderImageSize * 0.4f;
// 这里的碰撞距离和图片形状大约一致
float maxCollisionDistance = playerCollisionRadius + spiderCollisionRadius;
int numSpiders = [spiders count];
for (int i = 0; i < numSpiders; i++)
{
CCSprite