2019独角兽企业重金招聘Python工程师标准>>>
随着苹果公司不断地创新与发展,新的iPhone 5、iPad 4以及iPad mini产品相继问世,包括iOS与Xcode在内的开发环境和开发工具也都有了更新和进步。相信有不少开发团队正紧锣密鼓地在iPhone 5和iPad mini上部署自己的应用,一切都是那么令人激动!而随着iOS 6的推出,cocos2d游戏引擎又有了新的发展。在IT行业,可谓唯一不变的便是变化。日新月异的技术需要同为开发者的你我保持强盛的好奇心和完美的学习状态,只有这样,才能不断进步,开发出更酷的游戏或应用。相信这本书的再版,一定能使你受益匪浅,帮助你实现梦想。
你的第一个游戏 第四章
在本章中,你将编写专属于你的第一个完整的游戏。它不会为你赢得什么奖项,但从中可以学到cocos2d基本要素的使用方法,而且这个游戏是很容易修改的。实际上,本书之前版本的读者已经制作了这个游戏的几个修改版并发布到了App Store中。
这个游戏是著名的“Doodle Jump”游戏的“倒版”,它被贴切地命名为“DoodleDrop”。玩家通过旋转屏幕尽可能躲避落下的障碍物。游戏的最终版本如图4-1所示,你可以在此提前了解一下将在本章创造的作品。
图 4-1 “DoodleDrop”游戏的最终版本
4.1 创建DoodleDrop项目
在第2章我们学习了如何创建支持ARC的Kobold2D和cocos2d项目。
Kobold2D从运行Kobold2D启动应用程序开始,然后选择Empty-Project 模板(见第2章中的图2-2)。使用DoodleDrop作为项目名称,基本工作就完成了。剩下的事就是选择应用程序的目标程序,选择Supported Device Orientation的portrait mode图标(见第3章中的图3-17)。由于DoodleDrop是设计在Portrait模式下玩的,因此不选择landscape mode图标。
下面的部分仅适于cocos2d用户——Kobold2D用户可以略过。
4.2 从一个支持ARC的cocos2d项目开始
cocos2d用户要创建支持ARC的cocos2d项目,就应该依照第2章中的指导内容来做。如果已经做过了,就只要把已经创建的项目复制一遍。这样做可以节省时间:保留一份由原始cocos2d项目模板转换得来的支持ARC的未修改版本,这样每次创建新项目时就会比较简单快捷。
提示:
在这本书的源代码中,可以在Cocos2D_ARC_Template_Projects文件夹下找到支持ARC的cocos2d模板项目。
在完成第2章中的指导内容之后,就有一个支持ARC的cocos2d项目了。我的项目取名为cocos2d-2.x-ARC-iOS。用Xcode打开之前先简单地将包含.xcodeproj文件的文件夹拷贝一份。但是不要自己重命名.xcodeproj文件,因为那样做会导致文件不可用。
现在你可以通过Xcode来重命名项目,这同样也会重命名.xcodeproj文件。在Project Navigator中选择cocos2d-2.x-ARC-iOS项目(第一个条目,见第2章中的图2-5),带延迟地双击来编辑该条目。就是单击一次,停顿两秒,再单击一次。这样项目名称就可以编辑了。输入DoodleDrop作为这个项目的名称。
按下Enter键确认修改之后,Xcode会问你是否确认重命名图4-2所示的那几项。确认则单击Rename。否则Xcode 仍然会重命名项目,但是其他名称不变。所以就算你这个时候发现了拼写错误,或者突然不喜欢这个名字了,也都应该单击Rename。重命名之后考虑到Prefix.pch文件,可能还会出现警告:“New name for file can not be the same”。无视这个警告即可,因为不会有什么问题。
还有最后一个需要手动重命名的条目:应用程序的计划方案。应该仍然名为cocos2d- 2.x-ARC-iOS,也就是你命名项目时用的那个名称。选择Product Manage Schemes来查看方案列表。延迟双击方案名称来选择并编辑,重命名为DoodleDrop。完成之后,关闭scheme列表。
图4-2 确认重命名项目以及相关文件
由于DoodleDrop是Portrait模式应用程序,因此需要编辑AppDelegate.m 文件。修改shouldAutorotateToInterfaceOrientation方法,使之返回YES以仅用于Portrait模式:
return UIInterfaceOrientationIsPortrait(interfaceOrientation);
现在,在Run和Stop按键右边的下拉菜单中选择DoodleDrop的项目方案,然后运行,确认一切工作正常,如图4-3所示。
图4-3 开始游戏吧!DoodleDrop项目基于第2章中的cocos2d ARC
项目,Kobold2D的Empty-Project模板也不会有太大区别
4.3 创建DoodleDrop场景
下一步你要作如下决定:是使用已有的HelloWorldLayer作为起点,之后再把名字改成现在的项目名称呢?还是创建自己的场景?是我就选择后者。因为你迟早需要添加新的场景,所以还不如现在就学习如何从头创建新的场景。
请确定已选择你准备添加新场景类的组,然后选择File | New | New File或者右击Project Navigator树中的合适位置,在弹出的菜单中选择New File,打开如图4-4所示的New File 对话框。
图4-4 添加新的CCNode派生类的最好方式是通过使用cocos2d或Kobold2D提供的类模板。
在这个例子中,因为我们要创建新的场景,所以选择的CCNode类是CCLayer的子类
cocos2d和Kobold2D为大多数重要的节点和类都提供了类模板,不使用它们太浪费了!另外,Xcode自带的Objective-C类也是很好的模板——只需要手动将基类由NSObject 改为CCLayer即可。在cocos2d v2.x的模板部分选择CCNode类,单击Next按钮,再次单击Next 按钮会弹出如图4-5所示的Save File对话框,在此之前,确认将该类设置为CCLayer 的子类。
我是将新建的文件命名为GamLayer.m。整个DoodleDrop游戏逻辑都在这个文件中实现,所以这个名字还十分合理。确保DoodleDrop目标程序复选框被选中(见图4-5)。
注意:
不检查目标设置的话可能会导致文件没有被添加到正确的目标程序里。这会引起一系列问题——其中编译错误和“file not found”错误是常见的典型错误。这种情况有时还会导致游戏运行时崩溃。把文件放进完全不需要这些文件的目标程序里,只会浪费空间。
图4-5 给新场景命名,并且确保它被添加到正确的目标程序中
目前,我们的GameLayer类是空的,为了将它设置为场景,我们要做的第一件事是在里面添加+(id) scene方法。我们在这里插入的代码和第3章的基本上一样,只是层的类名不同而已。几乎在任何一个类中都需要-(id) init方法。添加-(void) dealloc方法也无伤大雅,要是能输出对象被正确销毁的日志就更好了。监视dealloc方法能有效提前做出系统预警,防止内存泄漏。
我也是一位很谨慎的程序员,决定将第3章中介绍的日志语句添加进来。程序清单4-1是完成后的GameLayer.h,程序清单4-2是完成后的 GameLayer.m。
程序清单4-1 带场景方法的GameLayer.h
#import < Foundation/Foundation.h>
#import "cocos2d.h"
@interface GameLayer : CCLayer
{
}
+(id) scene;
@end
程序清单4-2 带场景方法以及一些标准方法的GameLayer.m
#import "GameLayer.h"
@implementation GameLayer
+(id) scene
{
CCScene *scene=[CCScene node];
CCLayer* layer=[GameLayer node];
[scene addChild:layer]; return scene;
}
-(id) init
{
if ((self=[super init]))
{
CCLOG(@"%@: %@", NSStringFromSelector(_cmd), self);
}
return self;
}
-(void) dealloc
{
CCLOG(@"%@: %@", NSStringFromSelector(_cmd), self);
}
@end
现在可以安全地删除HelloWorldLayer类。在弹出的对话框中选择Move to Trash选项,将文件彻底删除。选择HelloWorldLayer.h和HelloWorldLayer.m两个文件,在顶部菜单中选择Edit | Delete,或者右击选中的文件,在弹出的菜单中选择Delete选项。
Kobold2D用户现在只需要打开Resources组中的config.lua文件,修改FirstSceneClassName即可,如下所示:
FirstSceneClassName = "GameLayer",
但是在纯cocos2d应用程序中,必须修改AppDelegate.m,将文件中所有的HelloWorldLayer修改成GameLayer。程序清单4-3中已经突出显示了要对#import和pushScene语句进行的必要修改。
程序清单4-3 修改AppDelegate.m 文件,用GameLayer类代替HelloWorldLayer
// replace the line #import "HelloWorldLayer.h" with this one:
#import "GameLayer.h" - (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
...
// replace HelloWorldLayer with GameLayer
[director_ pushScene:[GameLayer scene]];
}
编译并运行,应该能看到空白场景。成功了!如果碰到什么问题,请将你的项目与本书附带的DoodleDrop01项目进行比较。
提示:
应用程序成功生成但是运行失败?别忘了,一个Xcode 项目中有多个目标程序,甚至一个目标程序也会有多个方案。检查Xcode 工具栏中Run 和Stop 按钮右边的方案选择/部署目标程序下拉菜单(见图2-6)。下拉菜单的左边部分用于选择当前方案。要确保选择的是DoodleDrop。大多其他方案,比如cocos2d-library是静态库。你可以生成静态库,但是不能运行它们。删除、隐藏和选择方案这些都只能由用户自己设置完成。其他的问题我都已经为你解释清楚了。
4.4 添加Player Sprite
接下来将添加Player Sprite(玩家精灵)并使用加速计控制它们的动作。要添加玩家图像,请在Xcode中选择Resources组,然后选择File | Add Files to“DoodleDrop”,或者右击并从菜单中选择Add Files to“DoodleDrop”来打开选择文件的对话框。如果不小心把文件添加到了错误的组中,那么可以在Project Navigator中把它重新拖动到正确的组中。Resources组并没有什么特别的地方,它就是按照定义应该保存不是源代码的文件的地方。
玩家图像alien.png和alien-hd.png就在随书附带的DoodleDrop项目的Resources文件夹中。也可以挑选你自己的图像,只要图像的尺寸是64×64像素或128×128像素(HD格式,带有-hd后缀)即可。cocos2d自动为带有Retina显示屏的iPhone和iPod Touch设备使用HD文件,标准分辨率(SD)的文件只用于iPhone 3GS。cocos2d还能识别另外两种文件后缀:-ipad用于iPad和iPad 2,-ipadhd用于带有Retina显示屏的第3代(或更新的)iPad。
对于专用于Retina设备和iPad的资源,cocos2d默认使用带有-hd、-ipad和ipadhd后缀的文件。普通的iOS应用程序不会使用它们。这些应用程序要想使用高分辨率图像,必须使用苹果公司的@2x文件扩展名。虽然在cocos2d应用程序中也可以使用@2x扩展名,但是cocos2d的文档警告用户不要使用该扩展名。
提示:
一个常被问起的问题是在非Retina设备上简单地缩小HD图像是否合适。这么做是不合适的,原因有两个。一个原因是内存限制。即使最早的Retina设备的内存也是非Retina设备的2倍以上。让非Retina设备加载HD图像,占用的内存将是已被缩小并打包的SD图像的4倍。另一个原因是,加载Retina图像的时间要比加载SD图像长得多,在较老、较慢的设备(比如没有Retina显示屏的那些设备)上这个问题更加明显。
反过来,在应用程序中只使用标准分辨率的资源也不是十分合适。这样将无法利用Retina显示屏的高分辨率,而且应用程序的图像质量在Retina设备上不如高分辨率的图像。放大和图像处理算法再好,也不能让标准分辨率图像在Retina设备上呈现清晰而鲜明的效果。正因如此,应该将游戏的所有资源设计为使用高分辨率,然后在需要的时候再缩小其大小。唯一需要注意的是,尺寸大小应该能够被2整除。
Xcode随后会询问添加文件的方式和位置细节,如图4-6所示。确保在Add To Targets区域选中了所有会使用该文件的目标程序,当然在这里只有DoodleDrop,不过在Kobold2D项目中最好也把该文件添加到Mac OS X目标中。如果文件还没有存储到项目的文件夹中,就应该选中“Copy items into destination group’s folder(if needed)”复选框。如果无法确定,就选中该复选框,最坏的情况也就只是有相同文件的副本。如果不选中该复选框,那么最坏的情况会是把项目添加到源代码管理程序或者在压缩并分享项目时发生文件丢失错误。
提示:
iOS游戏首选的图像文件格式是PNG(Portable Network Graphic,便携式网络图像)。这是一种压缩文件格式,然而与JPEG文件不同的是,PNG采用了无损压缩,保留了原始图像的所有像素。你也可以保存不经压缩的JPEG文件,不过对于同一图像,PNG文件的大小明显要比未经压缩的JPEG文件小。但是这只会影响应用的大小,而不会影响纹理对内存(RAM)的使用。不使用JPEG文件的另一个原因是,cocos2d在iOS上加载这些文件的速度很慢。我上次测量的结果是它们比PNG文件慢8倍。第6章将介绍TexturePacker,一个用于管理图像的工具。它允许将图像转换为各种压缩格式或减少图像的色深,同时通过抖色和其他技术保留尽可能高的图像质量。
图4-6 每次添加资源文件时都会出现这个对话框,大多数情况下你都应该使用默认设置
现在我们要向游戏场景中加入玩家精灵了。我会将它们作为CCSprite*类型的成员变量加入到GameLayer类中。就目前来看,这样做比较容易,而且游戏也足够简单,可以将所有组件都加入到相同的类中。通常不推荐这种方法,在之后的项目中我们将会创建单独的类来保存每个游戏组件,以符合好的代码设计要求。
程序清单4-4展示了如何在GameLayer的头文件中添加CCSprite*类型的成员变量。
程序清单4-4 将CCSprite*类型的成员变量添加到GameLayer类中
#import < Foundation/Foundation.h>
#import "cocos2d.h"
@interface GameLayer : 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;
}
玩家精灵已添加为层的子节点,它的标记值是1,随后我们将使用这个标记值识别它并把它与其他的精灵区分开。这里的文件名使用了标准分辨率图像的文件名,即alien.png。在Retina设备上,cocos2d会自动加载alien-hd.png。如果没有对应的-hd文件,cocos2d会加载标准分辨率图像。此时,图像在Retina设备上看起来比在非Retina设备上小。提供所有图像资源的-hd版本是一种很好的做法。
警告:
记住,在iOS设备上文件名是区分大小写的。如果你试图加载Alien.png或ALIEN.PNG,在模拟器上会成功加载文件而在iOS设备上则不会,因为文件真正的名字是alien.png,字母全部小写。这就是坚持统一的命名约定——例如强制所有文件名使用小写字母——的意义所在。为什么要用小写?因为全部大写的文件名很难辨认。
我们将position属性的x值设为屏幕宽度的一半,从而使玩家精灵的初始位置水平居中。在垂直位置上,我们想使玩家精灵纹理的底端与屏幕底端对齐。如果你记得前一章的内容,就会知道精灵纹理是以其中心点作为位置坐标值的。将精灵的垂直位置设为0会导致纹理的下半边陷入屏幕底端。这不是我们想要的,我们要把它往上挪半个纹理的高度。
可以通过调用player.texture.contentSize.height返回精灵纹理的内容尺寸。什么是内容尺寸?在第3章中,我讲到iOS中纹理尺寸的大小只能是2的方幂。但是实际的图像尺寸可能会比纹理尺寸小。例如,如果原始图像的尺寸为100×100像素,那么纹理尺寸就是128×128像素。纹理的contentSize属性会返回原始图像的尺寸,也就是100×100像素。大部分情况下,处理的都是内容尺寸而不是纹理尺寸。即使图像是2的幂,也应该使用contentSize,因为纹理可能是包含多个图像的纹理图册。第6章将详细讨论纹理图册。
将图像高度的一半设为position属性的y值后,精灵图像恰好能与屏幕底端对齐。
提示:
无论何时都要尽量避免使用固定的位置值。如果你只是把玩家位置设为(160,32),此时你就做了两个本该避免的假定。第一,你假定了屏幕宽度为320像素,但并不是每个iOS设备都是这样的。第二,你假定了图像高度是64像素,然而那也是可能会改变的。一旦你开始像这样做假定,代码就会丧失一部分灵活性,修改起来需要很多时间。
我用了很多代码来定位对象,但长远来看这样做会节约大量时间。可以将项目部署到不同的设备,也可以使用不同尺寸的图像,无论如何项目都会正常运行。你不再需要修改这段特别的代码了。程序员面对的最费时的坏事情之一就是——不得不修改那些依据假定而编写的代码。
4.5 加速计输入
最后一步,我们来使玩家精灵能够左右倾斜。在第3章中我已经说明,应该为接收加速计输入的层添加accelerometer方法。这里我使用了acceleration.x参数,将它乘以10后加到玩家精灵的位置值上,从而加速玩家精灵的运动:
-(void) accelerometer:(UIAccelerometer *)accelerometer
didAccelerate:(UIAcceleration *)acceleration
{
CGPoint pos = player.position;
pos.x + = acceleration.x * 10;
player.position = pos;
}
注意到奇怪之处了吗?上面的三行代码用一行就能写好:
// ERROR: lvalue required as left operand of assignment
player.position.x += acceleration.x * 10;
然而,与其他编程语言(如Java、C++或C#等)不同,像player.position.x += value这样的语句对Objective-C中的属性是不起作用的。position属性的类型是CGPoint,这是一个普通的C结构体。Objective-C中的属性不能直接向结构体的域赋值。问题出在Objective-C中属性的工作方式,以及Objective-C所基于的C语言的赋值机制。
player.position.x语句实际上是调用了position的getter方法[player position],这意味着你实际上获得了一个临时的position值,并试图改变这个临时的CGPoint对象的x成员变量。但之后这个临时的CGPoint对象会被丢弃。于是position的setter方法[player setPosition]就不会自动被调用了。所以只能直接对player.position进行赋值,在本例中就是赋给它一个新的CGPoint对象。在使用Objective-C时,你必须接受这个令人遗憾的问题,而且如果你有过编写Java、C++或C#的经历,没准还得改变你的编程习惯。
因此,前面的代码必须创建一个临时的CGPoint对象,修改position的x域,然后把临时的CGPoint赋给player.position。在Objective-C中是必须这么做的。
4.6 首次测试运行
现在你的项目应与本章附带的DoodleDrop02项目不相上下。马上测试一下吧。你要确认已选择在设备上运行应用,因为模拟器不会获得加速计输入。检验当前版本中的加速计输入表现如何。
如果尚未在Xcode中为这个项目安装你的开发授权,将会产生“code sign”错误。在iOS设备上运行程序时需要代码签名证书。请查阅苹果公司的文档以了解如何创建和安装必要的开发授权文档(http://developer.apple.com/ios/manage/provisioningprofiles/howto.action)。
4.7 玩家速度
注意到加速计输入哪里不正常了吗?是的,它反应迟缓,移动不畅。这是因为玩家精灵并没执行真实的加速和减速。让我们修改一下它。修改后的代码在DoodleDrop03项目里。
实现加速与减速的概念不在于直接改变玩家的位置值,而是使用单独的CGPoint变量作为速度矢量。每次接收到加速计事件时,速度矢量就加上从加速计得到的输入。当然,这意味着我们必须把速度限制在一个任意的最大值内,否则减速时就要花点时间了。不管有没有接收到加速计输入,在每一帧都把速度加到玩家位置上。
注意:
为什么不使用动作来移动玩家精灵呢?无论何时你需要频繁地——如每秒数次——改变对象的速度或方向,使用move动作都不是一个好的选择。动作适用于相对使用期较长的一次性对象,所以频繁创建新对象在分配和释放内存上增加了额外开销,这会使游戏性能大幅下降。
更糟糕的是,如果不为动作留出一点时间,动作是不会执行的。这就是在每帧添加新动作来替换前一个却没有任何效果的原因。很多cocos2d开发者都曾偶然发现过这个看似古怪的现象。
例如,如果在每帧都停止所有动作并为对象添加一个新的名为MoveBy的动作,对象不会有一丁点的移动!MoveBy动作只会在下一帧改变对象的位置。但是在下一帧你已经停止所有动作,并加入另一个新的MoveBy动作了。这样做下去只会让对象寸步不移。这就像老生常谈的关于驴的那一套故事:推得越使劲它就越犟,并且在原地不动弹。
让我们来看一下对代码所做的修改。在头文件中加入了playerVelocity变量:
@interface GameLayer : CCLayer
{
CCSprite* player;
CGPoint playerVelocity;
}
你可能想知道为何使用CGPoint替代float。这是考虑到你以后可能会加速或减速一点点。为今后的扩展做些准备总没有坏处。
程序清单4-6是加速计的代码,其中使用速度代替了对玩家位置的直接修改。这段代码采用了三个设计参数:减速值、加速计灵敏度和最大速度。这些参数没有最优值;你需要调整数值,找到最适合你游戏的设置,因此得名“设计参数”。
减速是指减少当前速度,之后速度将加上新的加速计值与灵敏度相乘后的数值。减速值越低,玩家精灵操作外星人改变方向的速度就越快。灵敏度越高,玩家精灵对加速计输入的反应就越敏感。由于这些数值是对同一数值进行修改,它们相互作用且相互影响,因此一定记得每次只调整一个值。
程序清单4-6 通过GameLayer实现得到playerVelocity
-(void) accelerometer:(UIAccelerometer *)accelerometer
didAccelerate:(UIAcceleration *)acceleration
{
// controls how quickly velocity decelerates (lower = quicker to change direction)
float deceleration = 0.4f;
// determines how sensitive the accelerometer reacts (higher = more sensitive)
float sensitivity = 6.0f;
// how fast the velocity can be at most
float maxVelocity = 100;
// adjust velocity based on current accelerometer acceleration
playerVelocity.x = playerVelocity.x * deceleration + acceleration.x *
sensitivity;
// we must limit the maximum velocity of the player sprite, in both directions
if (playerVelocity.x > maxVelocity)
{
playerVelocity.x = maxVelocity;
}
else if (playerVelocity.x - maxVelocity)
{
playerVelocity.x = - maxVelocity;
}
}
现在playerVelocity能够改变了,但如何把速度加到玩家精灵的位置值上呢?可以在GameLayer的init方法中指定如下update方法:
// schedules the –(void) update:(ccTime)delta method to be called every frame
[self scheduleUpdate];
同样需要添加–(void) update:(ccTime)delta方法,如程序清单4-7所示。已指定的update方法在每一帧都会被调用,而那就是我们要在玩家精灵的位置值上加上速度的地方。这样我们就可以做到:无论加速计频率的大小如何,都能产生流畅平滑的运动。
程序清单4-7 用当前速度更新玩家精灵的位置
-(void) update:(ccTime)delta
{
// Keep adding up the playerVelocity to the player's position
CGPoint pos = player.position;
pos.x + = playerVelocity.x;
// The Player should also be stopped from going outside the screen
CGSize screenSize = [CCDirector sharedDirector].winSize;
float imageWidthHalved = player.texture.contentSize.width * 0.5f;
float leftBorderLimit = imageWidthHalved;
float rightBorderLimit = screenSize.width - imageWidthHalved;
// preventing the player sprite from moving outside the screen
if (pos.x < leftBorderLimit)
{
pos.x = leftBorderLimit;
playerVelocity = CGPointZero;
}
else if (pos.x > rightBorderLimit)
{
pos.x = rightBorderLimit;
playerVelocity = CGPointZero;
}
// assigning the modified position back
player.position = pos;
}
边界检查可以防止玩家精灵显示在屏幕外。我们不得不再一次把玩家精灵纹理的contentSize(内容尺寸)考虑在内,因为玩家精灵是以精灵图像的中心为准计算位置的,我们不希望图像的任何一边离开屏幕。为此,计算imageWidthHalved的值,然后用它检查刚刚更新的玩家位置是否在左右边界之内。上面的代码稍显冗长,但这样更容易理解。现在构建并运行项目,体验能够控制玩家精灵的感觉。
提示:
如果玩过Tilt to Live之类的游戏,可能会注意到这里实现的简单的加速计控制无法像在那些游戏中一样有一种动态的感觉。这是因为要实现流畅、动态的加速计控制,需要对加速计输入进行过滤。在Kobolds2D中,使用KKInput类的属性可以获得高通(瞬时)和低通(平滑)过滤后的加速计输入:
float smoothed = [KKInput sharedInput].acceleration.smoothedX;
利用加速计控制的游戏通常会使用低通过滤器。“低通”意味着过滤掉加速度突然且极端的变化,从而使得到的结果值十分平滑。下面是由加速计输入值(rawX/rawY)和常量filterFactor(范围为0.0~1.0)得到新的smoothedX/smoothedY值(示例变量)的低通过滤器。0.1是一个不错的过滤因子,表示在新的平滑后的值中只考虑了10%的当前原始加速计值:
smoothedX = (rawX * filterFactor) + (smoothedX * (1.0 - filterFactor));
smoothedY = (rawY * filterFactor) + (smoothedY * (1.0 - filterFactor));
4.8 添加障碍物
在往游戏中加入一些让玩家躲避的东西之前,这个游戏还没有什么可玩性。接下来在项目里加入一些令人憎恶的东西:六足人造蜘蛛。有谁不想躲着它们吗?
与玩家精灵一样,需要把spider.png和spider-hd.png加入到Resources组中。然后在GameLayer.h的接口中加入3个新的成员变量:在程序清单4-9中出现的NSMutableArray类引用spiders,以及在程序清单4-12中使用的spiderMoveDuration和numSpidersMoved:
@interface GameLayer : CCLayer
{
CCSprite* player;
CGPoint playerVelocity;
NSMutableArray* spiders;
float spiderMoveDuration;
int numSpidersMoved;
}
警告:
在代码中应该避免使用CCArray。CCArray是NSArray和NSMutableArray的快速替代版本,但是它只是快了一点点,使用它几乎不会对帧率产生影响。而它的一些方法(如insertAtIndex或removeObjects)要比NSMutableArray的相同方法慢得多。CCArray最大的问题是过去已经存在一些严重的bug,比如与ARC的兼容性问题。它也不能支持NSArray/NSMutableArray的全部功能。例如,无法枚举带有block对象的CCArray,这使得它不适合用于并行处理(例如,通过Grand Central Dispatch来完成)。整体上看,NSMutableArray要比CCArray更可靠,兼容性更好,并且缺陷更少。任何时候,我都愿意用降低一些性能来换取高可靠性。cocos2d在内部使用了CCArray,不过就内部使用来看,CCArray经过了大量测试,证明没有问题,所以这种用法我是可以接受的。
与此同时,在GameLayer的init方法中,在scheduleUpdate后面加上对initSpiders方法的调用,我们将在后面讨论它:
-(id) init
{
if ((self = [super init]))
{
...
[self scheduleUpdate];
[self initSpiders];
}
return self;
}
随后,我们会向GameLayer类中加入一大段代码,先从initSpiders方法开始,它创建了蜘蛛精灵,如程序清单4-8所示。
程序清单4-8 为了更好地进入,蜘蛛精灵被初始化并添加到NSMutableArray中
-(void) initSpiders
{
CGSize screenSize = [CCDirector sharedDirector].winSize;
// using a temporary spider sprite is the easiest way to get the image's size
CCSprite* tempSpider = [CCSprite spriteWithFile:@"spider.png"];
float imageWidth = tempSpider.texture.contentSize.width;
// Use as many spiders as can fit next to each other over the whole screen width.
int numSpiders = screenSize.width / imageWidth;
// Initialize the spiders array using alloc.
spiders = [NSMutableArray arrayWithCapacity:numSpiders];
for (int i = 0; i < numSpiders; i++)
{
CCSprite* spider = [CCSprite spriteWithFile:@"spider.png"];
[self addChild:spider z:0 tag:2];
// Also add the spider to the spiders array.
[spiders addObject:spider];
}
// call the method to reposition all spiders
[self resetSpiders];
}
这里要说明的是,创建名为tempSpider的CCSprite对象只是为了获得精灵图像的宽度,然后用它决定蜘蛛精灵的数量。获得图像尺寸最简单的方法就是创建临时的CCSprite对象。注意,我没有把这个tempSpider对象作为子节点加到任何其他节点上,也没有把它分配给实例变量。这意味着当程序执行离开initSpiders方法后,ARC会知道tempSpider对象已经不再使用,并自动释放其内存。
与其形成对比的是名为spiders的数组,我用它来保存对所有蜘蛛精灵的引用。这个数组被分配给实例变量spiders,因此,在GameLayer对象本身被释放以前,ARC不会释放该数组对象。使用ARC时,不需要自己以任何方式释放spiders数组。
在程序清单4-8的结尾,调用了[self resetSpiders]方法,这个方法的代码在程序清单4-9中。将精灵的初始化和定位分开处理的原因在于:游戏总会结束,之后游戏将被重置。最为高效的做法就是将所有游戏对象移动到它们的初始位置。然而,一旦游戏趋于复杂,这种做法将不具备可行性。最终,最简单的做法就是以玩家的等待为代价重新加载全部场景。
警告:
在重新加载场景时,你可能想使用[[CCDirector sharedDirector] replaceScene: self];来重新加载同一场景。但是这会导致程序崩溃,因为self是当前正在运行的场景。在cocos2d中尝试用正在运行的场景替换其自身会导致程序崩溃。实际上,必须创建GameLayer类的新实例:[[CCDirector sharedDirector] replaceScene:[GameLayer scene]];。
程序清单4-9 重设蜘蛛精灵的位置
-(void) resetSpiders
{
CGSize screenSize = [CCDirector sharedDirector].winSize;
// Get any spider to get its image width
CCSprite* tempSpider = [spiders lastObject];
CGSize size = tempSpider.texture.contentSize;
int numSpiders = [spiders count];
for (int i = 0; i < numSpiders; i++)
{
// Put each spider at its designated position outside the screen
CCSprite* spider = [spiders objectAtIndex:i];
spider.position = CGPointMake(size.width * i + size.width * 0.5f,
screenSize.height + size.height);
[spider stopAllActions];
}
// Schedule the spider update logic to run at the given interval.
[self schedule:@selector(spidersUpdate:) interval:0.7f];
// reset the moved spiders counter and spider move duration (affects speed)
numSpidersMoved = 0;
spiderMoveDuration = 4.0f;
}
我再一次临时获取了某个已有的蜘蛛精灵,然后通过纹理的contentsize属性获得它的图像尺寸。这里我没有创建新的精灵,因为已有同类精灵存在了,并且由于所有的蜘蛛都使用相同尺寸大小的同一图像,我甚至不用关心获取的是哪个蜘蛛,因此我只是简单地获取了数组的最后一项。
接下来修改每个蜘蛛的位置,使它们整体横跨整个屏幕的宽度。还是同样的原因,蜘蛛精灵的纹理以其中心点作为位置,position属性的x值加上了图像宽度的一半。至于高度,每个蜘蛛也被设为高于屏幕顶端一个图像高度。这个数值是任意的,这里我要使图像不可见,能达此目的者均可。由于重置后蜘蛛仍然可能在移动,因此要停止它的全部动作。
提示:
如果不是绝对必要的话,为了节约CPU资源,最好不要在for或其他循环语句中使用方法调用作为循环条件。本例中创建numSpiders变量来保存[spiders count]的调用结果,然后将其用作for循环的循环条件。由于在循环过程中数组本身并未被修改,因此数组的计数值保持不变。这就是为何我能保存这个值并在for循环中省去对[spiders count]的重复调用。
我还指定spidersUpdate:选择器每0.7秒运行一次——这是另一个蜘蛛从屏幕顶端落下的时间间隔。如果选择器已被指定,cocos2d会用一条日志消息指出这一点,你可以忽略这条消息。cocos2d并不会再次指定选择器,而是会更新已指定选择器的时间间隔。如程序清单4-10所示,spidersUpdate:方法会随机挑选一个已经存在的蜘蛛,检查它是否空闲,然后使用一系列动作操作它从屏幕上方落下。
程序清单4-10 spridersUpdate:——让蜘蛛频繁下落的方法
-(void) spidersUpdate:(ccTime)delta
{
// Try to find a spider which isn't currently moving.
for (int i = 0; i < 10; i++)
{
int randomSpiderIndex = CCRANDOM_0_1() * spiders.count;
CCSprite* spider = [spiders objectAtIndex:randomSpiderIndex];
// If the spider isn't moving it won't have any running actions.
if (spider.numberOfRunningActions == 0)
{
// This is the sequence which controls the spiders' movement
[self runSpiderMoveSequence:spider];
// Only one spider should start moving at a time.
break;
}
}
}
我还从未对任何程序清单置之不理,是吧?你也许想知道为什么这里我要循环迭代10次来得到一个随机的蜘蛛。原因在于,我不知道随机生成的索引值对应的蜘蛛是不是活动的,所以要确认最终随机选出的蜘蛛当前是空闲的。如果10次之后——当然,这个数字是任意的——仍然没有随机选出一个空闲的蜘蛛,就会跳过这次更新,然后等待下一次。
也可以使用do/while循环进行不断尝试,直到找到空闲的蜘蛛为止。但有一种可能,即此刻所有的蜘蛛都在移动——这取决于设计参数,如新蜘蛛落下的频率。游戏会为尝试寻找空闲的蜘蛛而无限循环,从而锁死。此外,我并不喜欢太卖力;对于这个游戏而言,另一个蜘蛛等个几秒再落下也并无大碍。虽然如此,如果查看DoodleDrop03项目,就会发现我加入了日志记录语句,输出找到空闲蜘蛛的重试次数。
由于蜘蛛执行的唯一动作就是一系列的运动,我只要检查蜘蛛当前是否执行任何动作即可。如果没有执行动作,我就假定它是空闲的。然后执行程序清单4-11所示的runSpiderMoveSequence方法。
程序清单4-11 通过动作序列控制蜘蛛的运动
-(void) runSpiderMoveSequence:(CCSprite*)spider
{
// Slowly increase the spider speed over time.
numSpidersMoved++;
if (numSpidersMoved % 8 == 0 && spiderMoveDuration > 2.0f)
{
spiderMoveDuration - = 0.1f;
}
// This is the sequence which controls the spiders' movement.
CGPoint belowScreenPosition = CGPointMake(spider.position.x,
-spider.texture.contentSize.height);
CCMoveTo* move = [CCMoveTo actionWithDuration:spiderMoveDuration
position:belowScreenPosition];
CCCallBlock* callDidDrop = [CCCallBlock actionWithBlock:^void(){
// move the droppedSpider back up outside the top of the screen
CGPoint pos = spider.position;
CGSize screenSize = [CCDirector sharedDirector].winSize;
pos.y = screenSize.height + spider.texture.contentSize.height;
spider.position = pos;
}];
CCSequence* sequence = [CCSequence actions:move, callDidDrop, nil];
[spider runAction:sequence];
}
runSpiderMoveSequence方法记录下了落下的蜘蛛数目。每落下8个蜘蛛,spiderMove- Duration就降低,从而增加所有蜘蛛的速度。你也许不熟悉%,它被称为求模操作符。求模运算的结果是左操作数除以右操作数的余数,即如果numSpidersMoved可以被8整除,那么求模结果为0。
动作序列只包含了一个CCMoveTo动作和一个CCCallBlock动作。动作还有改进的空间,可以使它就像真正的六足蜘蛛人那样,下落一点点,停住,然后一路下到底。这个任务就留给你了,不过在最终版的DoodleDrop项目中,你可以找到一个示例实现。
到目前为止,唯一重要的是知道我选择了在传递给CCCallBlock动作的block函数中重置了蜘蛛的位置。这个block函数可以简单地使用与runSpiderMoveSequence方法相同的spider变量。它在蜘蛛的运动完成后调用,即蜘蛛已经掉到了玩家角色的下面。通过使用这个block函数,你就不需要花大力气找出正确的蜘蛛。而后,将蜘蛛的位置重置为屏幕上方。程序清单4-12将程序清单4-11中的block函数单独列了出来。
程序清单4-12 在CCCallBlock中重设蜘蛛位置,使之可以从屏幕上方重新落下
CCCallBlock* callDidDrop = [CCCallBlock actionWithBlock:^void(){
// move the droppedSpider back up outside the top of the screen
CGPoint pos = spider.position;
CGSize screenSize = [CCDirector sharedDirector].winSize;
pos.y = screenSize.height + spider.texture.contentSize.height;
spider.position = pos;
}];
到目前为止,一切顺利。我猜你一定迫不及待地想试玩一下。我想你会很快注意到游戏还是缺了些东西。小提示:看一下下面的标题。
《ios cocos2d 2 游戏开发实战(第三版)》试读电子书免费提供,有需要的留下邮箱。