Cocos2d开发系列(九)

最后更新于:2022-04-01 11:39:10

Learn IPhoneand iPad Cocos2d Game Delevopment》第10章 。 相册空间已满,无法直接贴站外图片。要查看图片,请点击链接。 ## 使用 Tilemaps 接下来两章,我将介绍基于贴图的游戏世界。你也许玩过Ultima这样的角色扮演游戏,或者刚刚把你Facebook上的朋友加进了Farmville。那么我可以肯定,你已经玩过了使用tilemap技术的游戏。 在tilemap游戏中,图形由小图片组成,称作tiles(贴片),它们是紧挨在一起的;把它们放入一个个小格子里这就组成了我们的游戏世界。这个概念令人兴奋,因为相比把整个世界当成一个贴图来绘制,这样更节约内存,同时允许更多的变化。 本章将使用所有贴图种类中最简单的一种贴图:直角贴图,来介绍一般的贴图概念。它由正方形或矩形的贴片组成图形,并以顶视图的方式呈现游戏世界。例如Ultima系列一直以来都使用了贴图技术。Ultima1-5使用正方形贴片,顶视图视角;Ultima6-7 仍然使用直角贴图,但使用了半等角投影透视视角。Ultima8:Pagan,是整个系列中唯一使用等角投影贴图的游戏。等角投影贴图在下一章讨论。 我还会解释如何滚动一个tilemap地图,如何让一个贴片始终保持在地图中心,如何保持屏幕不会移出tilemap区域。触摸你不希望聚焦的贴片会导致滚动,这意味着你会学到如何判断被触摸的贴片是哪一个。 ## 贴图是什么? 贴图Tilemaps是用一个个贴片去组成2D游戏世界的技术。仅仅用几张有着相同尺寸的图片就可以创建出庞大的世界地图。这意味着在大地图中使用贴图能有效地节省内存。这种技术应用于早期的电脑游戏。许多传统RPG类游戏用正方形的贴片创建精彩的游戏世界。这些Tilemaps游戏看起来如图所示: [点击打开链接](http://img.ph.126.net/mm7_kvre9APwuDkdf3Yl2A==/2749166097549894375.png) Tilemaps通常用编辑器生成,有一种名叫Tiled(QT) 的编辑器可以直接支持cocos2d。Tiled是免费的,开源,并且允许你在多个图层中编辑直角贴图和等角投影贴图。Tiled也允许你加入触发器区域和对象,以及编辑贴片属性——这样你可以在代码中判断贴片的类型。 提示:Qt指诺基亚Qt框架,其内置了Tiled。因为还有一个Tiled的java版,因此用Tiled(Qt)加以区别。java版Tiled已不再更新,但其中包含的几个特殊的功能仍然值得一看。但在这两章里,我使用和讨论的仍然是Tile(Qt)。   随着时间的推移,方块贴图技术也得到一些改进,通过使用另一种贴图技术——过渡贴片。例如,在紧挨草地贴片的地方,不直接使用水的贴片,而是使用额外的过渡贴片(例如,这个贴片中一边包含了水,一边包含了草地,中间是二者的分际线),这样便可以创建出一种更平滑的过渡效果。如果不这样做,你就要使用更多的贴片,花更多的心思考虑一个贴片如何才能过渡到另外的一个贴片,并让贴片种类保持在一定水平。过渡贴片是值得一提的。 上图中使用了许多过渡贴片。在其名为Desert(沙漠)的贴片集中只有4种地形的贴片:沙土、砾石(在tilemap的下半部分)、砖石(在左上部)、泥土(在右上部)。除了沙土之外的3种贴片,每一种都有12种额外的贴片用于过渡到沙土背景贴片。 贴片并不一定得是正方形;也可以创建矩形贴片的直角贴图。在亚洲地区的RPG游戏里,经常使用这种贴图,例如DragonQuest4-6。当使用直角透视的时候,这使设计者创建的对象看起来高比宽长。这制造并呈现了深度感。等角透视贴图则通过斜45度透视来加深这一点。它使用伪3D风格的贴片,使游戏世界获得视觉深度。等角透视贴图能够“欺骗”我们的大脑,仿佛这就是一个3D世界,尽管所有的图片仍然是平面的。等角透视贴图通过用一个个菱形的贴图达到深度感,并允许距离观察者较近的贴图遮挡住较远的。下图为一个等角透视贴图的例子。 [点击打开链接](http://img.ph.126.net/QDbPuMBLKmvdzrYsvwOhbw==/2696811751881710575.png) 等角透视贴图地图说明tilemaps地图不一定是平面的。使用方块贴图技术你可以达到这样的效果:仿佛每个贴片天生就严丝合缝地放在其他贴片上面。因此,Tiled支持多图层以创建一种类似3D的效果,如下图所示。 [点击打开链接](http://img.ph.126.net/DL-QaDaPI0VW7olY5sR9nw==/1543045822344729403.png) 在等角透视贴图中,能够使用分层的贴片,许多Farmville玩家视频展现这一效果。有的Farmville玩家仅仅在庄稼地里不用一砖一石就建造出房屋甚至摩天大厦。其实就是利用了人的错觉,用等角透视贴图很容易做到这一点。 ## 使用Zwoptex准备图片 在本章的Tilemap01项目的Resouces/individualtile images目录中,你会找到许多方块状的贴片图像。把所有图片加到Zwoptex中,并把画布大小设为256*256——这个大小已经足够。点击Apply按钮,Zwoptex自动把它们安排妥当。结果显示如下: [点击打开链接](http://img.ph.126.net/wMzo5eJU7J7usoo2YifjxQ==/3092002619183471046.png) 注意,Zwoptex用随机顺序排放这些贴片。很不幸,写这本书的时候,Zwoptex1.04还不支持按名称排列贴片。否则,这个布局应当是按照贴片在磁盘中的文件名排放的。这个功能对许多Zwoptex用户来说很重要,因此在以后的版本可能会支持这个功能。查看你的Zwoptex版本是否支持这个功能,如果支持,首先分别编辑你的贴片文件,然后用Zwoptex从这些贴图文件中创建排序的贴图集。 你仍然可以使用随机排序的图片,但挡你添加或删除贴片并点击“Apply”之后,这些贴片又恢复到原来的位置。Zwoptex好像会对贴片进行随机重排。在使用CCSpriteBatchNode时,这根本不成为问题,因为你可以通过名称引用某个图片。 对于Tiled,则不一样了。保持贴片位置不变是至关重要的,因为Tiled是通过位置+偏移来引用贴片的。 这意味着,如果贴片改变了在贴图集中的位置,使用该贴图集的tilemap地图将完全变成另外一个样子。tilemap仍然会引用贴片在贴图集中的同一位置,但那个位置已经替换成一个水的贴片,而原来是一个草地贴片。 办法是加一些空的贴片填充到贴图集中(贴图集大小至少要和你需要的一样)。目的是简单地做出一个绘图空间。关键是把所有的空贴片加到Zwoptex以创建一个贴图集结构,其中包含了贴片所占据的空间,但贴片实际是空的。然后关闭Zwoptex,你不再需要它了,因为你可以用任何图片编辑程序打开这个贴图纹理集,并且在图片不透明的地方进行编辑。Zwoptex已经在贴图集中标明了每个贴片所在的原始位置。 如果你比我更有艺术天分,可能会用图形处理程序直接创建tilemap地图。那么你需要保证图形的背景必须是透明的。这可防止地图显示在游戏中时,在贴片的边缘出现缝隙。而且,所有的贴片都应是同样的宽和高,并且每个贴片之间的间隔也必须是固定的。 使用图形处理程序可能比仅仅创建一些空白的贴片,然后用Zwoptex对齐要花更多的时间。后者只需处理一次,而且更加快捷。 ## Tiled 地图编辑器 创建cocos2d使用的tilemaps地图,最常用的工具是Tiled地图编辑器。它生成的TMX文件被cocos2d引擎所支持。Tiled的免费的,在编写本书的时候,版本是0.5。你在它的主页www.mapeditor.org上就可以下载它。 如果你愿意支持Tiled的开发工作,请捐助该项目: http://sourceforge.net/donate/index.php?group_id=161281. ## 新建 Tilemap 下载Tiled后,解压并安装。启动Tiled,选择View菜单并勾选Tilesets和Layers选项。这将显示图层列表,并在Tiled窗口右边显示当前贴片集。然后选择File ➤ New 创建一个 tilemap。这将弹出新地图对话框: [点击打开链接](http://img.ph.126.net/3cVPx9OubWmGiIHL0CnG4Q==/2861474613257445171.png) 当前,Tiled支持直角贴图和等角透视贴图。地图的尺寸是以贴片数为单位,而不是像素。比如这里,新地图将包括30*20个贴片,贴片大小为32*32像素。贴片尺寸必须和你的贴片文件尺寸吻合,否则它们会被对齐。 新地图是空的,而且也没有加载任何贴片集。通过菜单 Map ➤ New 可以加载贴片集。这会打开 NewTileset dialog 对话框: [点击打开链接](http://img.ph.126.net/83h1Qgd9blBbkg6-AQixMg==/567453553065598251.png) 在其中,你可以浏览正确的贴片集图片。一个贴片集是一个图片文件名,在该图片中包含了多个等大贴片,因此你也可以称之为只包含等大图片的贴图集。 我将使用dg_grounds32.png贴片集。这些贴片由David E. Gervais 创作,并依据 Creative Commons License 发布, 这意味着你在尊重原作者的期刊下,你可以任意分享和编辑这些图片。在[http://pousse.rapiere.free.fr/tome/index.htm](http://pousse.rapiere.free.fr/tome/index.htm) 你可以下载到他的更多作品. 在上图中,我已经通过Browse按钮加入了dg_grounds32.png贴片集,它就位于Tilemap01工程的Resources目录下。如果你钩上“Use transparent color” 勾选框, 透明区域被绘制为粉红色(默认)。你可以保持不选择该选项,因为目前使用的贴片没有透明区域。 贴片的宽、高是每个贴片在贴片集中的大小。它们应当是32*32像素,等同于你创建地图时的贴片大小。Margin和Spacing分别指定贴片边框的宽度,以及贴片之间的间距。在这里,没有Margin和Spacing,我都设为0。 如果你用Zwoptex对齐贴片并创建了贴图集结构,你必须用Zwoptex的Margin和Spacing值来设定这两个值。默认,Zwoptex使用2个像素的边距。 载入贴片集图片时,确保其位于项目的资源目录下。还要确保把tilemapTMX文件保存到和贴片集文件的同一目录。否则Cocos2d无法加载贴片集,加载TMX文件时会导致运行时错误。这种错误是由于TMX文件引用贴片集时采用了相对路径。如果它们不在同一目录,当程序被安装到模拟器或设备后,cocos2d找不到图片,因为目录结构不存在。 ## 编辑 Tilemap 贴片集加载后,你会看到一个空白地图,激发你的创意并完成一个tilemap地图。有一个办法可以去掉这个空白地图。使用一个默认的贴图地图是很好的开始。这里,我使用油漆桶工具(BucketFill)并选择青草贴片,因此我的地图现在是一片葱茏的草地: [点击打开链接](http://img.ph.126.net/_KKJfaXiv1M_FNgGL48TFA==/2882585236510744468.png) Tiled有4中编辑模式,在工具栏最右边有4个图标: 1、Stamp Brush(快捷键B) 它允许你用贴片集中选择的贴片进行绘图; 2、Bucket Fill(快捷键F) 允许你用指定贴片填充区域; 3、Eraser(快捷键E) 擦除贴片; 4、Rectangular Select(快捷键R) 允许你选择一个范围,然后拷贝、粘贴选区内的贴片。 大部分时候,你在从贴片集中选择贴片,然后用Stamp Brush在地图上绘制。通过放置一个个贴片绘制基于贴片的游戏世界。 你还可以在多个图层中编辑贴片,通过在图层面板,你可以加入更多的图层。选择菜单Layer->Add Tile Layer可以创建新图层。用多图层的方式,你能在cocos2d中在地图的不同区域中切换。在TileMap01项目中,我用图层的方式,在冬夏之间进行切换。 你也可以用菜单Layer->Add Object Layer增加一个层,用于加入对象。在Tiled中对象是一些简单的矩形,你可以通过代码在其中绘制并读取。你可以用它们触发某些事件——例如,当玩家进入某个区域时产生怪。我随机加入了几个以显示它们用cocos2d代码是如何工作的。 Tiled还有一些功能是在右键菜单中。例如:刚才提到的矩形对象通过右键->RemoveObject可以删除掉。注意,只有Layers面板中的某个图层处于选中状态时,右键菜单才有效。 通过右键并点击属性项,你也可以编辑对象、图层、贴片的属性。使用菜单Layer➤ AddTile Layer,创建一个图层,将其命名为 GameEventLayer。选中 GameEventLayer, 选择 Map ➤ New Tileset ,加载 game-events.png(和 dg_grounds32.png在同一目录)。 其中有3个贴片。 在其中某个贴片上右键,选 Tile Properties, 然后添加一个isWater 属性, 如图所示。 [点击打开链接](http://img.ph.126.net/MVBuJoI2G6VRo-aHrFs4tQ==/1024850390220412583.png) 提示: 注意每创建一个图层都会带来额外的开销,尤其是你把贴片放在多个图层的同一地方。这将导致两个图层都被绘制,并影响游戏性能。推荐尽可能地减少图层的数量。对大多数游戏来说2-4个图层足矣。加入新的tile图层后应随时注意游戏在设备上运行时的帧率。 现在,你可以在地图中使用这些带有isWater属性的贴片了。画出一条河吧。如果你想看看当前绘制的图层下面是什么,可以在Layer面板中通过滑块改变GameEventLayer的透明度,或者点击图层前面的“隐藏/取消隐藏”检查框。 确认在保存TMX tilemap地图前所有图层的检查框都是选中的。cocoas2d不会加载未勾选该检查框的图层。 最终,tilemap大概如图所示。 [点击打开链接](http://img.ph.126.net/yaaGJ_geSjsp9O79sFv8ZQ==/2772809995593586268.png) 把它保存在Resources文件夹,和贴图集图片放在一起。 在Cocos2d中使用直角贴图 要在Cocos2d中使用TMX贴图,首先要将TMX文件和相应的贴图集图片文件加到Xcode项目的Resources组中。在TileMap01项目中,我加入了orthogonal.tmx和 dg_grounds32.png 、game- events.png。加载和显示tilemap地图是很简单的;只要在TileMapLayer类的init方法中加入以下代码: ~~~ CCTMXTiledMap* tileMap = [CCTMXTiledMaptiledMapWithTMXFile:@"orthogonal.tmx"]; [self addChild:tileMap z:-1 tag:TileMapNode]; CCTMXLayer* eventLayer = [tileMaplayerNamed:@"GameEventLayer"]; eventLayer.visible = NO; ~~~ CCTMXTiledMap类用TMX文件名进行初始化并以tag值为标记加到了self中。你也可以把它申明为成员变量。接下来通过layerNamed方法获得GameEventLayer对象。GameEventLayer是在Tiled中的图层名。因为gameevents 图层是通过代码方式来决定某些贴片的属性的,所以这个图层不应当显示出来。注意,如果你在Tiled中取消了某个图层的选择框,它也不会显示,但你也无法访问其贴片及贴片属性。 如果现在运行该项目,你会看到如下界面: [点击打开链接](http://img.ph.126.net/ZZIvtMknkCLwWrTS6edrFw==/1328843365067921332.png) 现在你还不能用这个地图做些什么,但我会改变这一点。在TileMap02项目,我会找到isWater贴片。我增加了ccTouchesBegan方法,如下所示,作用是判断玩家是否碰到了某个贴片。 ~~~ -(void) ccTouchesBegan:(NSSet *)toucheswithEvent:(UIEvent *)event { CCNode* node = [self getChildByTag:TileMapNode]; NSAssert([node isKindOfClass:[CCTMXTiledMapclass]], @"not a CCTMXTiledMap"); CCTMXTiledMap* tileMap = (CCTMXTiledMap*)node; // 把触摸点位置转换为贴片坐标 CGPoint touchLocation = [selflocationFromTouches:touches]; CGPoint tilePos = [selftilePosFromLocation:touchLocation tileMap:tileMap]; // 检查玩家是否碰到了水 (e.g., 通过贴片的 isWater 属性) bool isTouchOnWater = NO; CCTMXLayer* eventLayer = [tileMaplayerNamed:@"GameEventLayer"]; int tileGID = [eventLayer tileGIDAt:tilePos]; } if (tileGID != 0) { NSDictionary* properties = [tileMappropertiesForGID:tileGID]; if (properties) { NSString* isWaterProperty = [propertiesvalueForKey:@"isWater"]; isTouchOnWater = ([isWaterPropertyboolValue] == YES); } // 如果玩家碰到了水,进行某些动作 if (isTouchOnWater) { } } else { } [[SimpleAudioEngine sharedEngine]playEffect:@"alien-sfx.caf"]; // 取得winter图层,并将它变成可视状态 CCTMXLayer* winterLayer = [tileMaplayerNamed:@"WinterLayer"]; winterLayer.visible =!winterLayer.visible; ~~~ 获取CCTMXTiledMap 对象没有什么特别的地方。触摸位置首先转换为屏幕坐标,然后使用tilePosFromLocation方法很快就把屏幕坐标转换成贴片坐标(tilemap中的贴片索引)。 这里提到了全局标识GIDs的概念,它是指分配给每个贴片的唯一整型值(在一个tilemap中)。在地图中,贴片被以从1开始的连续数字编号。GID为0,表示空贴片。CCTMXLayer的tileGIDAt方法会根据指定的贴片坐标返回贴片的GID。 然后,从tilemap获得名为GameEventLayer的CCTMXLayer。这是那个定义了isWater贴片并以河流图片绘制过的图层。tileGIDAt方法返回贴片的唯一id。如果id为0,意味着在图层的这个位置没有任何贴片——如果这样,说明该贴片已经移出,则触摸到的贴片也不会是一个isWater贴片。 CCTMXTileMap有一个propertiesForGID方法,它返回一个NSDictionary,包含了该GID所代表的贴片的有效的属性——在Tiled中我们曾经编辑过这些属性。dictionary把所有的键值对都当作NSSTring储存。如果你想看看某个NSDictionary都有些什么,可以用CCLOG语句打印出来: ~~~ CCLOG(@"NSDictionary 'properties'contains:\n%@", properties); ~~~ 这将在控制台窗口中打印类似如下的内容: ~~~ 2010–08-30 19:50:52.344 Tilemap[978:207]NSDictionary 'properties' contains: { isWater = 1; } ~~~ 你在处理tilempas的过程中,会与各种NSDictionary对象打交道。打印它们的内容可以让你快速查看NSDictionary或其他任何iPhoneSDK集合类中的内容 。有时,这是一种有用的技巧。 NSDictionary中的每个属性通过NSDictionary的valueForKey方法来检索,并返回NSString。要想从NSString转换为bool值,只需使用NSString的boolValue方法。类似地,NSString的intValue和floatValue方法可得到整数和浮点数。 ccTouchesBegan方法结尾,判断了玩家是否触碰到了水,是的话则发出某个声音。然后,检索WinterLayer图层并让其显示。季节变化当然没有这么简单。这只是演示如何利用Tiled中的多图层改变整个地图,而无需单独加载一个完整的tilemap地图。 如果只想单个贴片,可以使用removeTileAt和setTileGID方法移除或替换某个图层的贴片: ~~~  [winterLayerremoveTileAt:tilePos]; [winterLayer setTileGID:tileGID at:tilePos]; ~~~ 定位触摸的贴片位置 Locating Touched Tiles 在这两行代码中,我曾提到过tilePosFromLocation方法: ~~~ // 把触摸点位置转换为贴片坐标 CGPoint touchLocation = [selflocationFromTouches:touches]; CGPoint tilePos = [selftilePosFromLocation:touchLocation tileMap:tileMap]; ~~~ 首先,触摸位置被转换成屏幕坐标。这句代码以前就学习过,但我仍然重新罗列一下具体的实现代码以供参考: ~~~ -(CGPoint) locationFromTouch:(UITouch*)touch { CGPoint touchLocation = [touch locationInView:[touch view]]; return [[CCDirector sharedDirector]convertToGL:touchLocation]; } -(CGPoint) locationFromTouches:(NSSet*)touches { return [self locationFromTouch:[touchesanyObject]]; } ~~~ 在把触摸点位置转换为屏幕坐标后,tilePosFromLocation方法被调用。它需要两个参数:触摸位置以及一个tileMap指针。这个方法包含了一些数学运算,我会作一些简单解释: ~~~ -(CGPoint) tilePosFromLocation:(CGPoint)locationtileMap:(CCTMXTiledMap*)tileMap { // 必须减去地图的位置,因为地图是滚动的 CGPoint pos = ccpSub(location,tileMap.position); //必须转换为int,因为返回结果是整数 pos.x = (int)(pos.x / tileMap.tileSize.width); pos.y = (int)((tileMap.mapSize.height *tileMap.tileSize.height - pos.y) /tileMap.tileSize.height); CCLOG(@"touch at (%.0f, %.0f) is attileCoord (%i, %i)", location.x, location.y, (int)pos.x, (int)pos.y); NSAssert(pos.x >= 0 && pos.y >= 0&& pos.x < tileMap.mapSize.width && pos.y <tileMap.mapSize.height, @"%@: coordinates (%i, %i) out ofbounds!", NSStringFromSelector(_cmd), (int)pos.x, (int)pos.y); return pos; } ~~~ 如果你曾经用过tilemaps,这些代码你会很熟悉,否则,你可能会一片茫然。等我来解释一下。首先是将触摸位置减去当前地图的位置。在后面的Tilemap03项目中使用了贴图滚动,因此地图的位置很多时候并不是0,0。 为了使视角能够向上(北)、右(东)进行滚动,你必须把地图位置改变为负数。因为tilemap从位置0,0开始,即屏幕左下角。地图的0,0点和屏幕的0,0点是重合的。如果你把地图移到100,100,看起来好像是把视点向左下移。你经常会以为自己正在移动视角,其实没有。移动的是tilemap图层,要向地图中心滚动,你必须把地图坐标向负轴方向偏移。 接下来是简单计算:要获得tilemap的偏移量(我们知道永远是负值),我们必须让触摸位置和tileMap.position相减。减去一个负数实际上是加上一个正数: location(240, 160) – tileMap.position(-100,-100) = pos(340, 260) 因为地图图层从(0,0)移到了(-100,-100),而触摸位置在(240,160),这整个偏移就应当是(340,260)。 如果考虑进滚动的偏移量,我们就能得到贴片的坐标。另外,你要知道(0,0)贴片的贴片坐标是在地图的左上角。于此不同,屏幕坐标原点(0,0)却位于屏幕左下角,而地图坐标是从左上角开始。下图显示了一系列贴片的x,y坐标。这张截图是在Tiledjava版中启用 View ➤ Show Coordinates菜单得到的,这个功能Tiled Qt版不支持。 [点击打开链接](http://img.ph.126.net/fZrjQeepMMITryFJ8TAZNw==/2534963640273081368.png) 因此为免混淆,使用下行代码计算贴片的x坐标: ~~~ pos.x = (int)(pos.x / tileMap.tileSize.width);   ~~~ tileMap.tileSize属性是贴图集中贴片大小(在这里是32*32)。如果触摸点的x坐标是340,则上面的代码会计算: ~~~ 340 / 32 = 10.625 ~~~ 这当然不对,我们所有的贴片坐标都没有小数!因为触摸点位于贴片的内部(例如在一个32*32的方块内)。简单地把计算结果去除小数部分转换成int值: ~~~ pos.x = (int)10.625 // pos.x == 10 ~~~ 这个转换把小数点后面的数字消除。把小数部分消去是安全的,因为它们不但无用——反而有害。如果你不去掉小数部分,直接使用非整型的坐标检索一个贴片,例如10.625,将导致一个运行时错误,因为只有x坐标为10和11的贴片,不存在贴片x坐标为10.625的贴片。 计算贴片的y坐标则更复杂一些: ~~~ pos.y = (int)((tileMap.mapSize.height *tileMap.tileSize.height - pos.y) / tileMap.tileSize.height); ~~~ 注意括号的使用,这将确保最后才进行除运算。如果使用数字这个公式可能更容易理解: ~~~ pos.y = (int)((20 * 32 – 260) / 32) ~~~ 在上式中,tileMap.mapsize是30*20个贴片,而每个贴片为32*32像素。 用tileMap.tileSize.height乘以tileMap.mapSize.height,得到tilemap的像素高度。这是必需的,因为tilemap的y轴是从上到下开始计算,而屏幕的y轴是从下到上的。通过计算出tilemap的最下端的y轴坐标,然后减去当前y坐标260,就能得到当前触点在tilemap中的y坐标(像素)。由于这个结果是像素坐标,你需要除以tileSize.height然后取整,以再次折算成贴片坐标。 CCLOG和NSAssert用于在控制台窗口查看计算结果,并确保贴片坐标不会出现不合理的值。这是一种学习手段,也是一种预防措施。 代码优化和提高可读性 由于地图尺寸是固定不变的,你可以通过在类中增加一个成员变量来减少计算量,用该变量来保存地图的像素高度: ~~~ floattileMapHeightInPixels; ~~~ 在init方法中,在地图被加载的时候,计算一次tileMapHeightInPixels就行了: ~~~ CCTMXTiledMap*tileMap=[CCTMXTiledMap tiledMapWithTMXFile:@"orthogonal.tmx"]; tileMapHeightInPixels= tileMap.mapSize.height * tileMap.tileSize.height; ~~~ 现在你可以把计算公式进行重写,这样每次调用tilePosFromLocation方法时能够节省一次乘法运算: ~~~ pos.y =(int)((tileMapHeightInPixels - pos.y) / tileMap.tileSize.height); ~~~ 当然,这只能导致一个很小的性能改善,不能帮你赢得任何性能优化的奖项。但通过一个可读性更好的变量名,能使计算公式更加简单,易于阅读。 使用 Object Layer 本章,我创建了一个包含了objectlayer(图层名ObjectLayer)的例子:orthogonal.tmx。使用Layer->Add Object Layer菜单,可以创建Object层。然后点击tilemap并在其中绘制一个矩形框。我觉得objectlayer这个名字有点让人混淆,因为绝大部分游戏其实是把它当作一个“陷阱区域”使用,而不是真正意思上的对象。 在Tilemap03项目中,我在ccTouchesBegan方法中增加了许多代码与objectlayer互动。下面列出了其中一部分代码(在isWater判断之后): ~~~ // 检查是否触摸到某个矩形对象 CCTMXObjectGroup*objectLayer = [tileMap objectGroupNamed:@"ObjectLayer"]; boolisTouchInRectangle = NO;  int numObjects = [objectLayer.objectscount]; for (int i =0; i < numObjects; i++) { NSDictionary* properties = [objectLayer.objectsobjectAtIndex:i]; CGRect rect = [self getRectFromObjectProperties:properties tileMap:tileMap]; if (CGRectContainsPoint(rect, touchLocation)) { isTouchInRectangle = YES; break; } } ~~~ 因为object layers是一种特别的层,你不能用tilemap的layerNamed方法获取objectlayer。在cocos2d,object layer其实是CCTMXObjectGroup类,这又是一个命名不当的例子,因为Tiled把它引用为objectlayer,而不是object group。通过tilemap的objectGroupNamed方法你可以获得object layer对应的CCTMXObjectGroup,你只需要指定该objectlayer在Tiled中的名字。 紧接着,我遍历了objectLayer的objecdts数组,它包含了由NSDictionary对象组成的列表。想起来了吗?在前面我们曾经提到过的,tilemap的propertiesForGID方法返回的是NSDictionary属性集,这里和它其实是同样的东西。但有一点不同,propertiesForGID方法返回的是只读的NSDictionary。 这些NSDictionary只是简单地包含了每个矩形框的坐标。用getRectFromObjectProperties方法可以返回这个矩形: ~~~ -(CGRect)getRectFromObjectProperties:(NSDictionary*)dict tileMap:(CCTMXTiledMap*)tileMap{ float x, y, width, height; x = [[dict valueForKey:@"x"] floatValue]+ tileMap.position.x; y = [[dict valueForKey:@"y"] floatValue]+ tileMap.position.y; width = [[dict valueForKey:@"width"]floatValue]; height = [[dict valueForKey:@"height"]floatValue]; return CGRectMake(x, y, width, height); } ~~~ 键x,y,width,height的值由Tiled赋值。通过valueForKey可以轻易地检索它们的值,然后用floatValue方法把它们从NSString转换为浮点值。x,y值需要加上tileMap的位置,因为矩形需要跟随tilemap一起移动。最后,调用CGRectMake函数返回一个CGRect。 ccTouchesBegan方法中剩下来的代码简单地通过CGRectContainsPoint方法判断触摸点是否包含在这个矩形区域内。如果是,isTounchInRectangle标志置为true,并且退出for循环。因为没有必要再判断其他矩形是否包含了触点了。在ccTouchesBegan最后,isTouchInRectangle标志被用于判断是否在触点位置显示特殊效果。如果你触摸到矩形范围,这段代码会产生粒子爆炸效果: ~~~ if (isTouchOnWater) { [[SimpleAudioEnginesharedEngine] playEffect:@"alien-sfx.caf"]; } else if(isTouchInRectangle)  {   CCParticleSystem* system =[CCQuadParticleSystem particleWithFile: @"fx-explosion.plist"]; system.autoRemoveOnFinish= YES; system.position= touchLocation; [selfaddChild:system z:1]; } ~~~ 绘制Object Layer 当你运行Tilemap03项目时,你会注意到对象层的矩形框已经绘制在tilemap上了。 [点击打开链接](http://img.ph.126.net/aSw2VkIgCedtQR81FnBOMg==/639229672126814676.png) 这不是tilemaps或者对象层的标准特性。这是用OpenGL ES代码绘制的矩形框。每个CCNode都会有一个–(void)draw 方法,你可以覆盖该方法,加入自己的OpenGL ES代码。我习惯于用这些代码进行调试,画一些看得见的线、圆、或者矩形,以便于碰撞测试或者查看物体间距离。在这个例子里通过这种方法,能够实实际际地看见对象层的位置。用可见的方式胜于在调试器中查看坐标值,因为可视化的方式要比比较和计算数值更直观。 -(void) draw 方法会在播放帧时自动调用。但是,要有限度地使用该方法去改变节点的属性,因为这会对节点的绘制造成影响。下面是TileMapLayer类的draw方法。 ~~~ -(void) draw { CCNode* node = [self getChildByTag:TileMapNode]; NSAssert([node isKindOfClass:[CCTMXTiledMapclass]], @"not a CCTMXTiledMap"); CCTMXTiledMap* tileMap = (CCTMXTiledMap*)node; // 获取对象层 CCTMXObjectGroup* objectLayer = [tileMapobjectGroupNamed:@"ObjectLayer"]; // 线宽:3 像素 glLineWidth(3.0f); glColor4f(1, 0, 1, 1); int numObjects = [[objectLayer objects] count]; for (int i = 0; i < numObjects; i++) { NSDictionary* properties = [[objectLayerobjects] objectAtIndex:i]; CGRect rect = [selfgetRectFromObjectProperties:properties tileMap:tileMap]; [self drawRect:rect]; } glLineWidth(1.0f); glColor4f(1, 1, 1, 1); } ~~~ 首先,通过tag获得一个tilemap,并调用objectGroupNamed方法获得对象层的CCTMXObjectGroup对象。然后把线宽设为3个像素(glLineWidth方法),颜色设置为紫色(glColor4f方法)。这将影响后续的OpenGLES画线的线宽和颜色——不仅仅是当前方法,也会对其他用OpenGL ES节点绘制的行为有影响(例如,任何定义在CCDrawingPrimitives.h头文件中的用于画线、圆、多边形的方法)。这也是为什么我在画完之后又重置glLineWidth和glColor4f的原因。在OpenGL代码中保持使用前的状态是一种良好的风格,否则,你可能会改变其他绘制代码的输出结果。OpenGL采用了状态机制,因此你所改变的每个设置都会被记住并且会影响到下一个绘制方法。为此,你对OpenGL设置进行改变之后,应当在你绘制完毕后把它们设置回默认状态。 注意: draw方法中的代码总是在z顺序为0的地方绘制。而且它会在所有z顺序为0的其他节点之前绘制。这意味着任何OpenGLES节点都会被z顺序0的其他节点所覆盖。为此,我不得不把tileMap放在了z顺序-1,因为矩形框要绘制在tilemap之上。 我遍历了所有对象层中的对象,从他们的NSDictionary属性集中获得对象的CGRect,然后传递给drawRect方法。但不幸的是,cocos2d遗漏了这个有用的方法,因此我使用ccDrawLine简单实现了这个方法: ~~~ drawn before all other nodes at z-order 0, whichmeans that any -(void) drawRect:(CGRect)rect { // 矩形由4个点线构成:pos1、pos2、pos3、pos4 pos1 = CGPointMake(rect.origin.x,rect.origin.y); pos2 = CGPointMake(rect.origin.x, rect.origin.y+ rect.size.height); pos3 = CGPointMake(rect.origin.x +rect.size.width, rect.origin.y + rect.size.height); pos4 = CGPointMake(rect.origin.x +rect.size.width, rect.origin.y); ccDrawLine(pos1, pos2); ccDrawLine(pos2, pos3);ccDrawLine(pos3, pos4); ccDrawLine(pos4, pos1); } ~~~ 用CGPoint创建了矩形的4个顶点,然后用ccDrawLine方法把两点连成线段。你可能需要把这个方法放在安全的地方并记住,因为很可能再次用到它。 注意,draw方法和drawRect方法用 #ifdef DEBUG和 #endif 语句包括起来。这表示在编译发布版本时对象层的矩形不会被绘制,因为我只需要在调试时需要它们,而最终用户并不会看见它们。 ~~~ #ifdef DEBUG -(void) drawRect:(CGRect)rect { ... } -(void) draw{ } #endif ~~~ ## 滚动地图 终于来到最后的部分:滚动。实际上这很简单,因为只需移动CCTMXTiledMap就行了。在Tilemap04工程中,我在捕捉到了触摸点的贴片坐标之后,在ccTouchesBegan方法中调用了centerTileMapOnTileCoord方法: ~~~ ccTouchesBegan:(NSSet *)toucheswithEvent:(UIEvent *)event{ ... // 从触摸点获得贴片坐标 CGPoint touchLocation = [selflocationFromTouches:touches]; CGPoint tilePos = [selftilePosFromLocation:touchLocation tileMap:tileMap]; // 移动tilemap,使得触摸点位于屏幕的中心  [self centerTileMapOnTileCoord:tilePos tileMap:tileMap];  ... } ~~~ 下面是 centerTileMapOnTileCoord 方法, 它移动了tilemap并使触摸到的贴片居于屏幕的中心,并且如果地图已经到达屏幕边缘则停止滚动。 ~~~ -(void) centerTileMapOnTileCoord:(CGPoint)tilePostileMap:(CCTMXTiledMap*)tileMap{ // 把 tilemap 中心对齐指定的贴片位置 CGSize screenSize = [[CCDirector sharedDirector]winSize]; CGPoint screenCenter =CGPointMake(screenSize.width * 0.5f, screenSize.height * 0.5f); // 贴片坐标以左上角为坐标原点 tilePos.y = (tileMap.mapSize.height - 1) -tilePos.y; // 屏幕坐标以左下角为原点 CGPoint scrollPosition = CGPointMake(-(tilePos.x* tileMap.tileSize.width), -(tilePos.y * tileMap.tileSize.height)); // 贴片中心和屏幕中心的偏移点 scrollPosition.x += screenCenter.x -tileMap.tileSize.width * 0.5f; scrollPosition.y += screenCenter.y -tileMap.tileSize.height * 0.5f; // 确保地图滚动到地图边缘的时候停止 scrollPosition.x = MIN(scrollPosition.x, 0); scrollPosition.x = MAX(scrollPosition.x,-screenSize.width); scrollPosition.y = MIN(scrollPosition.y, 0); scrollPosition.y = MAX(scrollPosition.y,-screenSize.height); CCAction* move = [CCMoveToactionWithDuration:0.2f position: scrollPosition]; [tileMap stopAllActions]; [tileMap runAction:move]; } ~~~ 计算出屏幕中心位置后,我改变了tilePos的y坐标,因为tilemap的y轴方向是从上到下。而屏幕的y轴方向是从下向上。实际上,我转换了tilePos的y轴,使它的方向从下向上。另外,我把地图的高度减去一,因为贴片坐标实际上是从0开始。也就是说,如果地图的高度是10,它的贴片坐标只能是0-9之间。 接下来,创建了一个scrollPosition变量,用于计算地图将移动到的位置。第1步是把贴片坐标和地图的贴片大小相乘。你可能奇怪,为什么我让贴片的像素坐标取负值。因为如果我想将贴片从右上端向左下运动,必须减少地图的坐标值。 接着,修改了scrollPosition的坐标,使贴片与屏幕中心点对齐。你要考虑到贴片自己的中心是位于贴片大小一半的地方,需要从screenCenter中扣除。 通过O-C的MIN和MAX宏,我们保证了scrollPosition的位置一定在地图的边界范围内,不会显示任何地图以外的东西。MIN和MAX返回两个参数中最小和最大的值,它们比使用if…else语句进行条件赋值要简练。 最后,用一个CCMoveTo动作滚动地图节点,以使触摸到的贴片位于屏幕中央。这将使地图滚动到你轻击贴片的位置。你可以用同样的方法滚动地图到任何贴片上——比如,玩家所在的位置。 ## 小结 你现在已经对tilemaps有一个不错的概念了,并且知道如何用Tiled地图编辑器创建多图层的tilemap,并在游戏中运用图层属性。 用cocos2d加载和显示tilemap是件简单的事情,但获取贴片和对象层,读取并修改它们的属性则显得有些复杂。你也学到了如何判断触摸点的贴片坐标,并且使用贴片坐标进行地图的滚动,以便触摸点贴片位于屏幕的中央。 我还讲解了一点点的OpenGL ES编程知识,用它我们可以自己在tilemap上绘制对象层矩形,以便调试。
';

Cocos2d开发系列(八)

最后更新于:2022-04-01 11:39:08

Learn IPhoneand iPad Cocos2d Game Delevopment》第9章 。 由于相册空间已满,csdn不允许发布站外图片,所以所有图片以链接方式显示。 为了产生粒子效果,程序员们不得不使用粒子系统。粒子系统喷射大量细小的粒子并对其进行高效的渲染,这要比sprite高效得多,因此你可以模拟出雨、烟、火、雪、爆炸、蒸汽尾迹等效果。 粒子系统由大量属性驱动。所谓大量的意思,指超过30种,它们不仅影响着每个粒子的外观和行为,而且影响着整体的效果。粒子效果是所有粒子共同创造出来的可视化效果。单个粒子无法产生火焰效果,哪怕10个粒子也远远不够。如果不是数百,起码也要成打的粒子才能产生火焰效果。 创建可观的粒子效果是一个需要不断尝试的过程。在代码中尝试改变各种属性,编译,根据运行的结果调整粒子系统,不断改变并重复这些过程——无论如何,这都是十分繁琐的。粒子设计工具在这里派上了用场,我称之为粒子设计器,本章我们将讲述如何使用它。 ## 一、粒子效果示例 Cocos2d提供了大量内置的粒子效果,这给你提供了一个很好的概念。在CCParticleExamples.m中有一些例子,你可以把它们作为模板,然后在你的游戏或者子类中修改它们,你仅仅需要做一些微小的调整。这些例子不需要任何特别的技术,你可以创建这些示例粒子效果,如同普通的CCNode对象。实际上,它们派生自CCNode。 我创建了一个ParticleEffects01项目,演示了cocos2d的所有示例粒子效果。你轻触屏幕就可以快速地翻看这些效果,也可以通过拖曳动作移动它们。有许多粒子效果当它们开始移动后看起来就完全不一样了,看起来效果不错的大粒子束,在开始移动后很可能变得很吃力。 只有一种效果无法移动,这就是CCParticleExplosion效果,它是一次性效果,它一次性喷射所有粒子然后终止喷射,如图所示: [](http://img.ph.126.net/HfbEdaRE9qgx35T5BFYAwA==/567735028042252740.png)[http://img.ph.126.net/HfbEdaRE9qgx35T5BFYAwA==/567735028042252740.png](http://img.ph.126.net/HfbEdaRE9qgx35T5BFYAwA==/567735028042252740.png) 其他效果则可以持续运行,当有粒子生命结束并被删除后又会创建出新的粒子。在这种情况下的挑战是要维持屏幕中粒子数量的平衡。   面则显示了示例程序ParticleEffects01中使用的一些有关粒子效果的方法。particleType作为switch语句的开关变量,表示了要创建的内置粒子系统类型。注意CCParticleSystem指针用于存储粒子系统,因此我在runEffect方法末尾把它传给addChild方法调用。每个示例的粒子效果都继承自CCParticleSystem。 ~~~ -(void) runEffect { // remove any previous particle FX [self removeChildByTag:1 cleanup:YES]; CCParticleSystem* system; switch (particleType) { case ParticleTypeExplosion: system =[CCParticleExplosion node]; break; case ParticleTypeFire: system =[CCParticleFire node]; break; case ParticleTypeFireworks: system =[CCParticleFireworks node]; break; case ParticleTypeFlower: system =[CCParticleFlower node]; break; case ParticleTypeGalaxy: system =[CCParticleGalaxy node]; break; case ParticleTypeMeteor: system =[CCParticleMeteor node]; break; case ParticleTypeRain: system =[CCParticleRain node]; break; case ParticleTypeSmoke: system =[CCParticleSmoke node]; break; case ParticleTypeSnow: system =[CCParticleSnow node]; break; case ParticleTypeSpiral: system =[CCParticleSpiral node]; break; case ParticleTypeSun: system =[CCParticleSun node]; break; default: // do nothing break; } [self addChild:system z:1 tag:1]; [label setString:NSStringFromClass([system class])]; } -(void) setNextParticleType { particleType++; if(particleType == ParticleTypes_MAX) { particleType = 0; } } ~~~ 注意: NSStringFromClass方法在这个例子中很有用,我们用它来显示类名,而不需要每个类都指定一个字符串。这是很酷的Object-c 运行时特性,你可以得到一个NSString表述的类名。如果用C++,你只能去咬自己的脚趾了。这对游戏的逻辑代码没有任何用处,但这是非常有用的调试和日志输出技巧。你可以从苹果的基础框架参考中找到这些方法的完整列表和描述: http://developer.apple.com/mac/library/documentation/Cocoa/Reference/Foundation/Miscellaneous/Foundation_Functions/Reference/reference.html. 如果你想在自己的工程中使用这些示例效果,你可能会看到一些丑陋的方块,如下图所示。 [http://img.ph.126.net/OVlPXIW-AGFQx-PR7vqXXg==/1042864788729840959.png](http://img.ph.126.net/OVlPXIW-AGFQx-PR7vqXXg==/1042864788729840959.png) 这是因为内建的例子试图加载指定的贴图fire.png,而它已经随同cocos2d-iphone一起发布在Resources/Images目录下。当然,不使用贴图也可以创建出炫目的粒子效果——通过指定粒子的大小。但要想看到内置的示例原本的效果,就需要把fire.png加到你的Xcode工程中。 ## 二、创建粒子效果 很容易派生出一个CCParticleSystem的子类。但是用它去创建出可观且最终效果接近于最初设想的粒子效果就不那么容易了。以下列出了影响粒子系统外观和行为的属性列表: ~~~ ¥  emitterMode = gravity   gravity,centerOfGravity  radialAccel, radialAccelVar speed, speedVar tangentialAccel, tangentialAccelVar ¥  emitterMode = radius   startRadius,startRadiusVar, endRadius, endRadiusVar ␣  rotatePerSecond, rotatePerSecondVar ¥  duration ¥  posVar ¥  positionType   ¥  startSize, startSizeVar, endSize, endSizeVar ␣ ¥  angle, angleVar ¥  life, lifeVar ¥  emissionRate  ¥  startColor, startColorVar, endColor, endColorVar ¥  blendFunc, blendAdditive ¥  texture ~~~ 正如你所想的,这里有许多变量。当然,需要对每个属性的用途有一个彻底的了解。先让我们从创建一个CCParticleSystem派生类开始。 实际上,你可以从CCPointParticleSystem或CCQuadParticleSystem进行派生。PointParticle在1代或2代iOS设备上运行得更快,但在3代和4代设备上(iPhone3GS、iPad、iPhone4)表现不佳。这是由于CPU架构的不同。3代和4代苹果使用ARMv7CPU架构的新技术和特性,比如矢量浮点处理器以及SIMD(单指令多数据技术,用于提高3D图形运算效率)指令集(NEON),因此使用CCQuadParticleSystem能充分利用这些特性。 如果有疑问的话,不管什么设备总是用CCQuadParticelSystem去创建可视效果,或者让cocos2d替你决定buildtarget。你可以查看我在ParticleEffects02项目中加入的ParticleEffectSelfMade文件: ~~~ #import <Foundation/Foundation.h> #import "cocos2d.h" // Depending on the targeted device the ParticleEffectSelfMadeclass will either derive // from CCPointParticleSystem orCCQuadParticleSystem (preferred for iOS 3rd and 4th Generation) @interface ParticleEffectSelfMade : ARCH_OPTIMAL_PARTICLE_SYSTEM { } ~~~ 预处理定义ARCH_OPTIML_PARTICLE_SYSTEM取代了真实的基类名称,它会在编译动态决定从哪一个粒子系统继承。Cocos2d基于处理器架构采用CCQuadParticleSystem或者CCPointParticleSystem给ARCH_OPTIML_PARTICLE_SYSTEM赋值: ~~~ // build each architecture with the optimalparticle system #ifdef __ARM_NEON__ // armv7 #define ARCH_OPTIMAL_PARTICLE_SYSTEM CCQuadParticleSystem #elif __arm__ || TARGET_IPHONE_SIMULATOR // armv6 or simulator #define ARCH_OPTIMAL_PARTICLE_SYSTEM CCPointParticleSystem #else #error(unknown architecture) #endif ~~~ 现在来看ParticleEffectSelMade类的实现,它使用了所有可用的属性。我企图示范每个属性的使用,但你一一对每个属性进行些修改学习效果会更佳,所以我建议你在这个项目中调整这些属性值。同时,你也可以在代码中看到关于这些参数的简短描述: ~~~ @implementation ParticleEffectSelfMade -(id) init { return [self initWithTotalParticles:250]; } -(id) initWithTotalParticles:(int)numParticles { if((self = [super initWithTotalParticles:numParticles])) { //DURATION 时长 //大部分特效使用无限时长 self.duration = kCCParticleDurationInfinity; //对于有时间限制的特效,使用粒子喷射时间的秒数, //self.duration= 2.0f; //如果粒子系统运行达到指定时间,当其所有粒子死亡后将被移出父节点 ﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽adee﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽ ////       // 对于无限时长的粒子系统,该属性无效 self.autoRemoveOnFinish = YES; //MODE 模式 //喷射粒子会受重力的影响 self.emitterMode = kCCParticleModeGravity; //粒子以环行运动 //self.emitterMode= kCCParticleModeRadius; //某些属性只能在特定的emitterMode下使用 if (self.emitterMode == kCCParticleModeGravity) { // 重心会让你以为是粒子显示时的偏移位置 ,实际上是指节点的位置 self.centerOfGravity = CGPointMake(-15, 0); // gravity 会影响粒子x和y方向的速度 self.gravity = CGPointMake(-50, -90); // 径向加速决定了粒子离开发射器后的移动速度,正值表示粒子从发 // 射器出来后呈加速运动,负值表示粒子从发射器出来后作减速运动 self.radialAccel = -90; self.radialAccelVar = 20; // 切向加速使粒子围绕加速器作旋转运动,且旋转速度会越来越快 // (弹弓效应) self.tangentialAccel = 120; self.tangentialAccelVar = 10; // 粒子产生时的运动速度 self.speed = 15; self.speedVar = 4; } else if (self.emitterMode == kCCParticleModeRadius) { // 开始半径:从发射的位置到发射器的距离 self.startRadius = 100; self.startRadiusVar = 0; // 粒子移动的结束半径。如果比开始半径小,则粒子向内旋转,如果 // 比开始半径大,作外向旋转。可以用 // kCCParticleStartRadiusEqualToEndRadius 常量做出完 //  整的环形运动效果 self.endRadius = 10; self.endRadiusVar = 0; // 旋转速度 self.rotatePerSecond = 180; self.rotatePerSecondVar = 0; } //EMITTER POSITION 喷射器位置 //喷射器位置默认在节点中心 //这是粒子产生后的显示位置 self.position = CGPointZero; self.posVar = CGPointZero; //positionType 决定了当节点移动时已喷射出的粒子是否要重新定位 //(kCCPositionTypeGrouped) ,或者保持在原地 //   (kCCPositionTypeFree). self.positionType = kCCPositionTypeFree; //PARTICLE SIZE 粒子大小 //每个粒子的大小(以像素为单位) self.startSize = 40.0f; self.startSizeVar = 0.0f; self.endSize = kCCParticleStartSizeEqualToEndSize; self.endSizeVar = 0; //ANGLE (DIRECTION) 角度或方向 //粒子喷射角度,0表示正上方 self.angle = 0; self.angleVar = 0; //PARTICLE LIFETIME 粒子寿命 //粒子在屏幕上显示的时间 self.life = 5.0f; self.lifeVar = 0.0f; //PARTICLE EMISSION RATE 粒子喷射速度 //每秒中喷射的粒子数目,当self.particleCount>= // self.totalParticles时,会停止创建新粒子--如果你想制造间歇式爆 //   发的效果,在两次爆发间会产生一个暂停 self.emissionRate = 30; //通常设为该值,你可以改变它 self.totalParticles = 250; //PARTICLE COLOR 粒子颜色 //开始颜色必须设置一个有效的颜色值,否则粒子会不可见。其他颜色 //是可选的。这些颜色决定了粒子在生命周期开始和结束时的颜色。 startColor.r = 1.0f; startColor.g = 0.25f; startColor.b = 0.12f; startColor.a = 1.0f; startColorVar.r = 0.0f; startColorVar.g = 0.0f; startColorVar.b = 0.0f; startColorVar.a = 0.0f; endColor.r = 0.0f; endColor.g = 0.0f; endColor.b = 0.0f; endColor.a = 1.0f; endColorVar.r = 0.0f; endColorVar.g = 0.0f; endColorVar.b = 1.0f; endColorVar.a = 0.0f; //BLEND FUNC 混合函数 //混合函数用于计算透明色,参数1是源,参数2是目 //有效的参数包括:GL_ZERO  GL_ONE  GL_SRC_COLOR  //GL_ONE_MINUS_SRC_COLOR  GL_SRC_ALPHA // GL_ONE_MINUS_SRC_ALPHA   GL_DST_ALPHA  // GL_ONE_MINUS_DST_ALPHA self.blendFunc = (ccBlendFunc){GL_SRC_ALPHA, GL_DST_ALPHA}; //等同于把blendFunc设置为: {GL_SRC_ALPHA, GL_ONE} //self.blendAdditive= YES; //PARTICLE TEXTURE 粒子贴图 self.texture = [[CCTextureCache sharedTextureCache] addImage: @"fire.png"]; } return self; } @end ~~~ 1、可变属性 有许多属性以Var后缀命名,这些是可变属性,它们为属性的取值定义了一个模糊的范围。例如life=5 和 lifeVar=1,意味着每个粒子的寿命为5秒,并在5±1的范围内波动。 如果你不想使用可变属性,将其设置为0。可变属性使粒子呈现更有机和模糊的行为及外观。但当你在设计一种新效果时,除非你相当的有经验,可变属性会造成一定的干扰。我建议初学者不使用可变属性或者将可变属性设置为比较小的值。 2、粒子数 totalParticles属性控制了粒子效果的粒子数目,它通常用initWithTotalParticles方法初始化,但你可以在此后修改。粒子数直接影响粒子效果的外观和性能。 ~~~ -(id) init { return [selfinitWithTotalParticles:250]; } ~~~ 粒子数太小无法得到很炫的效果,太多则导致除了一个白块外你什么也看不到。而且,太多的粒子会浪费帧率。因此,粒子设计工具不会让你创建超过2000个粒子的效果。 3、发射器周期 duration属性定义粒子发射的时间。如果设置为2,则只发射2秒钟的新粒子然后终止: ~~~ self.duration= 2.0f; ~~~ 如果你想让粒子系统在停止发射而且最后一个粒子已经消失时,将粒子效果节点从父节点删除,可将autoRemoveOnFinish属性设置为 YES: ~~~ self.autoRemoveOnFinish= YES; ~~~ autoRemoveOnFinish属性只能和有限运行的粒子系统一起使用。Cocos2d定义了一个kCCParticleDurationInfinity常量(等于-1),用于无限运行的粒子效果。大部分粒子效果属于此类。 ~~~ self.duration= kCCParticleDurationInfinity; ~~~ 4、发射器模式 有两种发射器模式:重力模式和径向模式,由emitterMode属性指定。这两种模式使用类似的参数实现了截然不同两种效果,如后面两张图所示。它们有一两个独特 ~~~ ParticleEffects[6332:207]*** Terminating app due to uncaught exception'NSInternalInconsistencyException', reason: 'Particle Mode should be Radius' ~~~ 技巧:一般,针对你的目标效果,你应当使用最少的粒子数。粒子尺寸也很重要——每个粒子的尺寸越小,其性能就越好。 (1)重力模式 重力模式使粒子趋向或背离某个圆心运动。使用下面的代码设置重力模式: ~~~ self.emitterMode= kCCParticleModeGravity; ~~~ 重力模式使用以下独有属性(只能在 self.emitterMode= kCCParticleModeGravity 时使用): ~~~ self.centerOfGravity= CGPointMake(-15, 0);  self.gravity = CGPointMake(-50, -90); self.radialAccel= -90; self.radialAccelVar= 20; self.tangentialAccel= 120; self.tangentialAccelVar= 10; self.speed =15; self.speedVar= 4; ~~~ centerOfGravity定义了一个CGPoint,距离新粒子出现位置的偏移量。centerOfGravity这个名称很容易让人误解。真正的重心其实是节点的位置,centerOfGravity实际上是重心的某个偏移位置。而gravity属性定义了粒子在x和y轴上的加速度。由于重心的作用,粒子的重力加速度不能太大,而centerOfGravity也不应该偏离的太远。上面这些数值能给你一个比较好的参考。 radialAccel 属性定义了粒子从发射器出来的加速度。该参数为负时,粒子的移动速度会逐渐减慢。tangentialAccel属性与此类似,但它指的是粒子围绕发射器旋转的加速度——负值呈正时针旋转,正值呈反时针旋转。 很显然,speed属性就是粒子的速度。下图显示了重力模式下的粒子效果。 [http://img.ph.126.net/ukhAO3gFmhjlork4tOcvfA==/2560014913200276340.png](http://img.ph.126.net/ukhAO3gFmhjlork4tOcvfA==/2560014913200276340.png) 2)径向模式 径向模式使粒子呈圆环运动。它还可以做出螺旋效果(内旋或者外旋)。使用下面代码设置径向模式: self.emitterMode= kCCParticleModeRadius; 如同重力模式,径向模式也有几个独有的属性,只能用于 self.emitterMode = kCCParticleModeRadius的情况: ~~~ self.startRadius= 100; self.startRadiusVar= 0; self.endRadius= 10; self.endRadiusVar= 0; self.rotatePerSecond= -180; self.rotatePerSecondVar= 0; ~~~ startRadius属性定义粒子将被喷射的位置距离重心(粒子效果节点的位置)的距离。类似,endRadius定义粒子旋转结束的位置到重心的距离。如果你想到达闭环运动的效果,可以将endRadius和startRadius设置为相同的常量值: ~~~ self.endRadius= kCCParticleStartRadiusEqualToEndRadius; ~~~ rotatePerSecond属性影响粒子移动方向和速度,如果startReadius和endRadius不同,则是指旋转的圈数。 [http://img.ph.126.net/DUj0vgcCdols7DaMfyB3uw==/2847400864421855304.png](http://img.ph.126.net/DUj0vgcCdols7DaMfyB3uw==/2847400864421855304.png) 重力模式和径向模式的效果如图所示,你会发现尽管除了各自独有的属性外其他属性完全相同,但两者的显示结果却完全不同。要测试这点,把ParticleEffectSelMade类中以下语句取消注释: ~~~ //self.emitterMode= kCCParticleModeRadius; ~~~ ## 粒子位置 通过移动节点,你可以移动效果。但效果又一个posVar属性决定了新粒子产生时的位置的可变范围。默认情况下,position和posVar都位于节点中心: ~~~ self.position= CGPointZero; self.posVar =CGPointZero; ~~~ 粒子位置的一个非常重要的作用是粒子是否会随着节点的移动而动。例如,如果你想创建一个星点围绕玩家角色运动的效果,你可能想让星点随着玩家一起运动,这就要设置如下属性: ~~~ self.positionType= kCCPositionTypeGrouped; ~~~ 另外,如果想创建玩家着火并且让粒子呈现尾迹效果(粒子会留在原地,不跟随角色移动),则需要这样设置positionType属性: ~~~ self.positionType= kCCPositionTypeFree; ~~~ 自由移动常用于蒸汽、火焰、发动机尾气等效果, 粒子在喷射后就不再和发射器联系在一起,但会在跟随在物体后面。 ## 粒子大小 粒子大小用startSize和endSize属性描述(以像素为单位),这两个属性分别指定了粒子被喷射出来和粒子被移除时的粒子大小。粒子大小会从startSize逐渐向endSize变化。 ~~~ self.startSize= 40.0f;  self.startSizeVar = 0.0f; self.endSize= kCCParticleStartSizeEqualToEndSize; self.endSizeVar= 0; ~~~ 常量kCCParticleStartSizeEqualToEndSize用于确保粒子在整个生命周期中不会变化。 ## 粒子方向 angle属性设定粒子最初的发射方向,为0表示粒子向正上方发射(发射模式为重力模式时)。对于径向模式,它决定了喷射点在开始半径上的位置,随着该值的增加,喷射点会沿着开始半径(startRadius)作反时针移动。 ~~~ self.angle =0; self.angleVar= 0; ~~~ ## 粒子寿命 粒子寿命即粒子从开始到结束所经过的秒数,当粒子生命终止时,粒子会简单的淡出并消失。粒子寿命使用life属性设置。注意,粒子寿命越长,同一时间内在屏幕上显示的粒子越多。当达到粒子数上限时,粒子喷射停止直到有粒子死亡。 ~~~ self.life =5.0f; self.lifeVar= 1.0f; ~~~ 喷射速度emissionRate指每秒钟能生成的粒子数目。同粒子存活数totalParticles属性一起,二者对粒子效果的外观有重要影响。 ~~~ self.emissionRate= 30; self.totalParticles= 250; ~~~ 一般,你需要平衡emissionRate和totalParticles两个属性。你可以用totalParticles除以life作为emissionRate: ~~~ self.emissionRate= self.totalParticles / self.life; ~~~ 小技巧:通过调整粒子寿命,粒子存活数,以及喷射速度,你可以创建间歇喷发效果,由于屏幕中的粒子数已达到最大,新粒子不再产生,导致粒子束呈现周期性的中断。如果你不希望见到粒子流中出现间断,则需要增加粒子存活数,或者减少粒子寿命/或喷发速度。当然,你也可以使用emissionRate = totalParticles/ life。 ## 粒子颜色 每个粒子都会从起始色向终止色进行颜色渐变,创建一种生动的彩色效果。你至少要设置一个startColor,否则粒子不可见-因为默认被设置为黑色。颜色值是典型的ccColor4F结构体,即用4个浮点数分别表示r,g,b颜色和alpha值。每个浮点数取值范围在0-1之间,代表该颜色数从0到100%之间,或者alpha值在完全透明到完全不透明之间。例如,一种白色塑料的颜色可以用如下方法表示: ~~~ startColor.r= 1.0f; startColor.g= 0.25f;  startColor.b = 0.12f; startColor.a= 1.0f; startColorVar.r= 0.0f; startColorVar.g= 0.0f; startColorVar.b= 0.0f; startColorVar.a= 0.0f; endColor.r =0.0f; endColor.g =0.0f; endColor.b =0.0f; endColor.a =1.0f; endColorVar.r= 0.0f; endColorVar.g= 0.0f; endColorVar.b= 1.0f; endColorVar.a= 0.0f; ~~~ ## 粒子混合模式 混合(blend)是指粒子在显示前需要计算其所有像素。blendFunc的取值是一个ccBlendFunc结构体,用以指定源混合模式和目标混合模式: self.blendFunc= (ccBlendFunc){GL_SRC_ALPHA, GL_DST_ALPHA}; 混合时采用源图像(粒子)的r,g,b以及alpha值与屏幕图片原有的颜色值进行混合(粒子被渲染时)。实际上,粒子是以某种方式和其背景混合在一起的,blendFunc属性决定了源图像的哪些及多少颜色需要和背景的哪些及多少颜色进行混合。 blendFunc属性对于粒子的显示有深刻影响。通过在源图像和目标图像上配合使用下列混合模式,你可以创建出乎意料的效果,或者仅仅是一个个黑色颗粒。这需要大量的经验和练习。 ~~~ GL_ZERO GL_ONE  GL_SRC_COLOR   GL_ONE_MINUS_SRC_COLOR  GL_SRC_ALPHA  GL_ONE_MINUS_SRC_ALPHA  GL_DST_ALPHA ␣ GL_ONE_MINUS_DST_ALPHA ~~~ 在[www.khronos.org/opengles/documentation/opengles1_0/html/glBlendFunc.html](http://www.khronos.org/opengles/documentation/opengles1_0/html/glBlendFunc.html),你能找到许多关于OpenGL混合模式的信息及细节。 源图和目标图配合使用GL_SRC_ALPHA和GL_ONE模式常用于创建叠加式混合,在绘制大量粒子摞在一起时会导致一种非常亮甚至白色的效果。 ~~~ self.blendFunc= (ccBlendFunc){GL_SRC_ALPHA, GL_ONE}; ~~~ 当然,你也可以简单地将blendAdditive 属性设为 YES, 等同于将 blendFunc 设为 GL_SRC_ALPHA 和 GL_ONE: ~~~ self.blendAdditive= YES; ~~~ 通常,创建透明的粒子需要使用GL_SRC_ALPHA 和 GL_ONE_MINUS_SRC_ALPHA 模式: ~~~ self.blendFunc= (ccBlendFunc){GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA}; ~~~ ## 粒子贴图 如果不使用贴图,所有的粒子都会是平淡的有色颗粒。要在粒子效果中使用贴图,用CCTextTureCache的addImage方法增加一个图片,该方法会将指定的图片文件加载并返回一个CCTexture2D对象: ~~~ self.texture= [[CCTextureCache sharedTextureCache] addImage:@"fire.png"]; ~~~ 粒子图片最好看起来像云块,或者接近于球体。如果粒子图片存在有强烈对比区域、呈不规则形状,例如图片redcorss.png(一个红叉形状), 通常不利于粒子效果的显示。这会很容易导致颗粒感的出现,因为粒子间很难混合。某些效果可以这样干,比如前面提到的在角色头顶旋转的小星点。 粒子贴图的最重要的一点,是图片大小不得超过64x64像素。贴图尺寸越小,粒子效果的性能越好。   ## 粒子设计器 粒子设计器是一种在cocos2d和iOSOpenGL下创建粒子效果的图形工具。可以在[http://particledesigner.71squared.com](http://particledesigner.71squared.com/)下载其试用版。 这是一个难得的工具,为你在创建粒子效果时省去大量的时间。它强大的地方是当你一改变粒子效果的属性,就可以从屏幕上看到其效果。粒子设计器的用户界面默认显示了一个可视化的粒子效果列表。通过选择,可以编辑粒子效果,通过点击或者点击右上角的Emitter Config按钮可以切换到Emitter Config视图: [http://img.ph.126.net/TZd-2tQGaIzQXW9Ka8JjBQ==/3102417193321709960.png](http://img.ph.126.net/TZd-2tQGaIzQXW9Ka8JjBQ==/3102417193321709960.png) 你应该通过前面的介绍认出这些参数了。粒子设计器中只有很少的几个属性是无效的,不能编辑。一个是positionType。另一个是endRadiusVar,这是在径向模式中使用的。后者意味着你不能创建外向旋转的粒子效果(径向模式)。但你还是可以从粒子设计器加载效果,然后通过代码修改某些属性。这只是一个可以忽略不计的小缺陷。 仅有的一个特别地方是粒子贴图Particle Texture。这里既没有可以加载图片的按钮,双击这个地方也没有任何反应。窍门是,Particle Texture栏只接受拖拽。吧图片从Finder拖拽到ParticleTexture栏,如果它变成了绿色,就可以了。当你拖拽图片后,图片就会应用于该粒子效果。如果你使用了超过64*64的图片,粒子设计器会警告你,但仍然会使用该图片,不过会将图片缩放为64*64,而不管源图的比例。 粒子设计器的预览窗口如下,就像一个模拟器窗口: [http://img.ph.126.net/4OpprKS8nVOqKT8RaUjS0g==/1561060220854158265.png](http://img.ph.126.net/4OpprKS8nVOqKT8RaUjS0g==/1561060220854158265.png) 也可以将它设置为iPad的屏幕,可以通过点击这个iPad/iPhone来改变屏幕朝向,也可以通过设计器菜单栏上的Orientation按钮操作。 点击并在预览窗口中进行拖动,粒子效果也会随之而动,这将使你更容易看出移动的粒子效果。 注意,BackgroundColor设置对真实的粒子效果没有影响,它只能改变预览窗口的背景色。这对于设计一个在明亮背景下显示的昏暗的粒子效果是有帮助的,因为你可以在预览窗口中看到效果。 如果你缺少创意,你可以使用Ramdomize按钮。你可能会奇怪ramdomize 在粒子设计器中是什么意思。Urban词典告诉我, ramdomize是random的俚语。我猜开发人员是不是因为使用ramdomize会显得更酷一些。 这绝对是一个创意,但它并不能做到任何属性都随机变化。例如,Ramdomize的Emitter Type、Emitter Location和一些与特定Emitter Type相关的参数始终不会改变。  一旦你寻找到自己的创意,你可以拖动滑动条,然后在预览窗口查看效果。不停地调整其效果直到你满意。尽管这非常有趣,但你很快就发现你制作出来的新效果只能让你高兴一下子。 提示:设计粒子效果的时候一定要小心!要牢记,游戏还需要计算和渲染大量其他对象。如果你在预览窗口查看效果时有60fps的帧率,这并不意味着在游戏中这个粒子效果也能达到60fps。别忘记在你的游戏中测试你的粒子效果并始终关注它的帧率。还要保证在设备上也要进行同样的测试。在模拟器上体现出来的游戏性能经常是不真实的,别把它当真。粒子设计器预览窗口也同样如此。     ## 使用粒子设计器效果 假设几个小时后,你做出了一个完美的粒子效果。现在,你想把它用在cocos2d中。首先需要保存粒子效果。当你点击粒子设计器保存按钮时,弹出如下对话框: [http://img.ph.126.net/VWiYCf3Nqh7IcyKUTeqGqg==/612208074362537220.png](http://img.ph.126.net/VWiYCf3Nqh7IcyKUTeqGqg==/612208074362537220.png) 要保存为cocos2d能够使用的粒子效果,你必须使用cocos2d格式(plist)进行保存。你也可以钩上Embedtexture选项,这会将纹理贴图保存在plist文件中。这个好处是你只需要把plist文件加到Xcode项目就可以了,坏处是如果不在粒子设计器中加载粒子效果,你将无法改变粒子贴图。 保存之后,你可以把效果的plist以及png文件(如果未使用Embedtexture)加到Xcode项目的Resources组。在ParticleEffects03项目中,我加入了两个效果,一个效果带有一个单独的png贴图,而另一个效果的贴图则嵌在了plist文件中。 下面显示了对runEffect方法的修改,以加载用粒子设计器设计出来的效果。 ~~~ -(void) runEffect{ // 删除原先的粒子效果 [selfremoveChildByTag:1 cleanup:YES]; CCParticleSystem*system; switch (particleType){ caseParticleTypeDesignedFX: system = [CCQuadParticleSystemparticleWithFile:@"fx1.plist"]; break; caseParticleTypeDesignedFX2: system = [CCQuadParticleSystemparticleWithFile:@"fx2.plist"]; system.positionType = kCCPositionTypeFree; break; caseParticleTypeSelfMade: system = [ParticleEffectSelfMade node]; break; default: break; } CGSizewinSize = [[CCDirector sharedDirector] winSize]; system.position= CGPointMake(winSize.width / 2, winSize.height / 2); [self addChild:system z:1tag:1]; [label setString:NSStringFromClass([systemclass])]; } ~~~ 初始化CCParticleSystem时,用particleWithFile方法加载粒子设计器效果的plist文件。在case语句中,我用CCQuadParitcleSystem的原因是在所有的iOS文件中它都能运行。你也可以使用ARCh_OPTIMAL_PARTICLE_SYSTEM来代替实际的类名: ~~~ system =[ARCH_OPTIMAL_PARTICLE_SYSTEM particleWithFile:@"fx1.plist"]; ~~~ 提示:粒子设计器的效果必须用CCQuadParticleSystem或者CCPointParticleSystem。 尽管 CCParticleSystem是前两者的父类, 也实现了 particleWithFile 方法, 但除非你使用前面的两个子类,加载设计器的效果后它不会显示任何东西。 补充一下,我把两行代码移到了switchcase之外以避免代码重复,它们是用于把粒子系统节点的位置放到屏幕中心的。   ## 共享粒子效果 对于粒子设计器这是一个很酷的功能,你可以把你的创意和其他用户共享。在设计器菜单里,只需选择Share->Share Emitter,就弹出一个对话框,让你输入要共享的效果的标题、描述: [http://img.ph.126.net/maz-k4aWBqMLRRJ-Gb1mkg==/25332747920817771.png](http://img.ph.126.net/maz-k4aWBqMLRRJ-Gb1mkg==/25332747920817771.png) 共享出来的粒子效果不一定完全满足你的要求,但也能提供一个好的开始,以此开始你自己的创意。它们能帮助你较快地实现需要的效果,至少能激发创意。 我建议你浏览一下这些效果列表,试着找一些感觉。 ## 射击游戏和粒子效果 我现在想看看这些效果在游戏中的样子。让我们继续推进这个射击游戏的开发进程。在这章的ShootEmUp04项目,你会看到如图所示的效果: [http://img.ph.126.net/InHErGyRu8mBOq3T6jU40w==/2485986994325373221.png](http://img.ph.126.net/InHErGyRu8mBOq3T6jU40w==/2485986994325373221.png) 在EnemyEntity类中,gotHit方法是加入粒子爆炸效果的最佳地方,如下所示。我决定为Boss使用一种专门的效果,因为它是如此的大。 ~~~ -(void)gotHit { hitPoints--; if (hitPoints <= 0) { self.visible= NO; // 当敌人被摧毁时,播放粒子效果 CCParticleSystem* system; if (type == EnemyTypeBoss) { system = [ARCH_OPTIMAL_PARTICLE_SYSTEMparticleWithFile:@"fx-explosion2.plist"]; } else {  system= [ARCH_OPTIMAL_PARTICLE_SYSTEM particleWithFile:@"fx-explosion.plist"]; } // 设置不能在 Particle Designer 中设置的参数 system.positionType = kCCPositionTypeFree; system.autoRemoveOnFinish = YES; system.position = self.position;  [[GameScenesharedGameScene] addChild:system]; } } ~~~ 粒子效果文件 fx-explosion.plist和 fx-explosion2.plist 必须加到项目的Resources组中。粒子系统的初始化在前面已经提过。因为粒子效果应该也必须独立于敌人,不需要为创建它做过多的工作。首先,aurtoRemoveOnFinish设为YES,以便效果能自动移除。效果也应该定位到敌人的当前位置,以便爆炸时显示在合理位置。 我把粒子效果加到GameScene,因为敌人是无法主动显示粒子效果的。当敌人刚刚出现时是不可见的,它的出现会被粒子效果打断。最主要的是,所有的EnemyEntity对象都被加到了CCSpriteBatchNode中,后者不允许你加入任何非CCSprite对象。如果粒子效果被加到EnemyEntity对象,会导致一个运行时异常。 在玩这个带有粒子效果的游戏时,你会注意到当第1次显示粒子效果时会有短暂的停顿。这是因为cocos2d需要加载粒子贴图——无论贴图是嵌入在plist中的,还是单独的贴图文件,这都是个缓慢的过程。为了避免这种情况,我在GameScene中加入了一种预加载的机制:在init方法中针对每一个粒子效果都调用preloadParticleEffect方法: ~~~ // 预加载粒子贴图,每个粒子效果调用一次 [self preloadParticleEffects:@"fx-explosion.plist"]; [self preloadParticleEffects:@"fx-explosion2.plist"];  preloadParticleEffects 方法只是创建粒子效果。因为返回的是autorelease对象,它的内存将自动释放。但它所加载的贴图仍然存在于CCTextureCache中。 -(void)preloadParticleEffects:(NSString*)particleFile { [ARCH_OPTIMAL_PARTICLE_SYSTEM particleWithFile:particleFile]; } ~~~ 如果你没有使用在plist中嵌入贴图,你可以通过调用CCTextureCache的addImage方法预加载粒子贴图: ~~~  [[CCTextureCache sharedTextureCache]addImage:particleFile]; ~~~ ## 结论 本章真是一个视觉盛宴。Cocos2d提供了大量的粒子效果,极好地说明了所能达到的效果,它们是高效而且易于使用的。 但使用源代码创建粒子效果是十分痛苦的事情。有那么多的属性需要设置;有一些又是某种emitter mode专有的;有一些从名称上容易混淆无法理解其真实含义。但是在对每个属性进行过一些讲解后,你应该对这些属性放到一个效果中有什么作用有一个基本的了解,同时知道其中最为关键的参数是哪些。 然后我们介绍了粒子设计器。这个工具非常有用——也很有趣。当你移动滑块,然后观察屏幕上的结果时,你突然就对粒子效果有了一个完整的理解,甚至你还可以吧你的作品与其他人共享,并体验他人的成果。 最终,我们的射击游戏也得到了改进,现在敌人被摧毁时能够播放粒子效果了。这为游戏增色不少。 下一章,我们将介绍tilemaps。
';

Cocos2d开发系列(七)

最后更新于:2022-04-01 11:39:06

Learn IPhoneand iPad Cocos2d Game Delevopment》第8章 。 这种类型的游戏(shoot’emup游戏)最重要的是什么?射击的目标和需要躲避的子弹。本章,将为游戏添加一些敌人以及一个大 boss。 敌人和玩家将使用新的BulletCache 类射击不同的子弹,这些子弹来自同一个 pool。这个缓冲类会重用无效的子弹,以避免重复的内存分配和释放动作。同样,敌人会使用EnemyCache 类,因为待会屏幕上会出现成堆的敌人。 显然玩家可以向敌人射击。我会介绍基于组件编程的概念,用一种模板化的方式扩展游戏角色。除了 shooting 组件和 moving 组件,我们还会为 boss 老怪创建 healthbar 组件(生命值,俗称“血槽”)。毕竟,老怪不应该是一下就能pk 掉的,其生命值总是被一点点减少直至彻底干掉它。 ## 一、添加 BulletCache 类 该类在是 “一站式” 的,可以一次性生成许多子弹。原来这些代码是放在 GameScene 类中,但这(指生成子弹)显然不该由 GameScene 来管。下面显示 BulletCache 的头文件,它包括了CCSpriteBatchNode 对象和无效子弹计数器nextInactiveBullet: ~~~ #import "cocos2d.h" @interface BulletCache : CCNode { CCSpriteBatchNode* batch;  int nextInactiveBullet; } -(void) shootBulletAt: (CGPoint)startPositionvelocity:(CGPoint)velocity frameName:(NSString*)frameName; @end ~~~ 为了把 代码重构到 GameScene类之外,我需要把 initialization 方法和射击子弹的方法移到 BulletCache 类(代码见后)。接着,我决定使用一个CCSpriteBatchNode 变量,以免在每次需要这个对象时就得调用一次[CCNode CCSpriteBatchNode]方法。这会带来细微的性能优化。由于我会在类 GameScene 中加入 BulletCache 对象,因此很容易就能把 sprite batch node 传给 BulletCache。 注意,新的 BulletCache有一个问题,增加了scene的层次——一个额外的 CCNode。如果你担心这点,你也可以把 sprite batch node放在GameScene类中,用一个方法从BulletCahce 获取这个 sprite batch node。 但是,额外的函数调用开销有可能会使性能得以下降。如果你怀疑是不是真的对性能由影响,那就让你的代码可读性更好些。当有必要进行性能优化的时候再重构你的代码。 ~~~ #import "BulletCache.h" #import "Bullet.h" @implementation BulletCache -(id) init { if ((self = [super init])) { // 从当前贴图集中获得角色帧 CCSpriteFrame* bulletFrame =[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"bullet.png"]; // 使用角色帧的贴图构建CCSpriteBatchNode batch = [CCSpriteBatchNodebatchNodeWithTexture:bulletFrame.texture];  [self addChild:batch]; // 创建子弹并加到 batch for (int i = 0; i < 200; i++) { Bullet* bullet =[Bullet bullet]; bullet.visible =NO; [batchaddChild:bullet]; } return self; }}  -(void) shootBulletAt:(CGPoint)startPositionvelocity:(CGPoint)velocity frameName:(NSString*)frameName{ CCArray* bullets = [batch children]; CCNode* node = [bullets objectAtIndex:nextInactiveBullet]; NSAssert([node isKindOfClass:[Bulletclass]], @"not a Bullet!"); Bullet* bullet = (Bullet*)node; [bullet shootBulletAt:startPositionvelocity:velocity frameName:frameName]; nextInactiveBullet++; if (nextInactiveBullet >= [bulletscount]) { nextInactiveBullet= 0; } } @end ~~~ shootBulletAt方法已经完全变了。它有3个参数:startPosition,velocity和frameName——取代 Ship类指针。然后这些参数被传递给 Bullet 类的 shootBulletAt 方法,这个方法现在已经变为: ~~~ -(void) shootBulletAt:(CGPoint)startPositionvelocity:(CGPoint)vel frameName:(NSString*)frameName { self.velocity = vel; self.position = startPosition; self.visible = YES; // 改变子弹的贴图,设置一个不同的角色帧去显示 CCSpriteFrame *frame = [[CCSpriteFrameCachesharedSpriteFrameCache] spriteFrameByName:frameName]; [self setDisplayFrame:frame]; [self scheduleUpdate]; } ~~~ velocity 和position 被直接赋值给 bullet。这意味着调用 shootBulletAt 方法的代码必需自己决定子弹的位置、方向和速度。这出于这样的考虑:子弹射击的动作会适应更多的变化,包括可以改变子弹的角色帧(用setDisplayFrame 方法)。因为子弹使用的是相同的贴图集、相同的贴图,所以需要通过设置相应的贴图帧来改变子弹的显示。实际上,渲染贴图的不同部分很轻松,并不会带来额外的开销。 在编辑 Bullet 类时,我还修正了一个边界问题——只有子弹移出屏幕右边时,才会设为不可见并被放会重用列表(其实这是一个bug)。通过在update方法中使用 CGRectIntersectsRect 检查子弹的边框和屏幕矩形,任何完全移出屏幕的子弹都会被标记为重用: ~~~ // 子弹离开屏幕后,设为不可见  if (CGRectIntersectsRect([self boundingBox], screenRect) ==NO) { …… } ~~~ screenRect变量出于方便和性能的原因,被存储为static 变量,因此它能被其他类访问,并不需要每次使用的时候创建。static 变量在类实现文件中声明并有效,比如 screenRect。它们就像类的全局变量,任何类实例都可以读取和修改。成员变量则不同,它们只存在于每个实例对象中。因为屏幕尺寸在游戏期间永远不会变,所有的子弹都需要用到它,把它存储为所有实例的static变量显然是行得通的。第一个实例负责给 screeenRect 赋值。 CGRectIsEmpty 方法负责检查 screenRect 变量是否未初始化——因为是static变量,只需要初始化一次就行了。 ~~~ static CGRect screenRect; ...... // 确保只初始化一次 if (CGRectIsEmpty(screenRect)) { CGSize screenSize = [[CCDirectorsharedDirector] winSize]; screenRect = CGRectMake(0, 0,screenSize.width, screenSize.height); } ~~~ 接下来,移除GameScene 类中原有的用于射击子弹的代码。此外,需要用初始化 BulletCache 来替换初始化 CCSpriteBatchNode (在GameScene 的 init 方法中): ~~~ BulletCache* bulletCache = [BulletCache node]; [self addChild:bulletCache z:1tag:GameSceneNodeTagBulletCache]; ~~~ 还需要为 bulletCache 添加一个访问方法以便其他类通过GameScene访问BulletCache实例: ~~~ -(BulletCache*) bulletCache { CCNode* node = [self getChildByTag:GameSceneNodeTagBulletCache];NSAssert([node isKindOfClass:[BulletCache class]], @"not aBulletCache"); return (BulletCache*)node; } ~~~ InputLayer 现在可以用BulletCache 发射子弹了。 子弹的位置、速度和所用的角色帧这些属性, 应当在 InputLayer 的update方法里传递给射击方法: ~~~ if (fireButton.active && totalTime> nextShotTime) { nextShotTime = totalTime + 0.5f; GameScene* game = [GameScenesharedGameScene]; Ship* ship = [game defaultShip]; BulletCache* bulletCache = [gamebulletCache]; // 射击前设置 position, velocity h和 spriteframe CGPoint shotPos = CGPointMake(ship.position.x+ [ship contentSize].width * 0.5f, ship.position.y); float spread = (CCRANDOM_0_1() - 0.5f) *0.5f; CGPoint velocity = CGPointMake(1, spread);  [bulletCache shootBulletAt:shotPos velocity:velocityframeName:@"bullet.png"]; } ~~~ 重构后的射击过程添加了一些非常必要的灵活性。你可以设想一下,敌人现在可以使用同样的代码发射它们自己的子弹了。 ## 二、敌人 此刻,对于敌人我们仅有一个模糊的概念,它们是干什么的?它们的行为是什么?对于敌人,最重要的是——你永远不知道他们该干什么。 就游戏而言,这意味着一切都要从头开始,要策划出你想让敌人做的事情,从而分析需要编写的代码。与真实世界不同,你完全控制着你的敌人们。是不是觉得自己很伟大?但在你或者其他人感到好笑之前,你需要为统治世界想出一个计划。 我创建了3种不同类型的敌人的图片。这里,我只知道其中一个应该是Boss。看一眼下面的图片,然后想象一下这些敌人分别能干些什么: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-04_572a0ba38ada0.gif) 在写代码之前,先了解一下这些敌人有哪些行为是共性的,这样有些代码只用编写一次。代码复用是最重要的编码规范。我们先来看看敌人们都有哪些共性: ¥  发射子弹 ¥  何时何地发射子弹的判断逻辑 ¥  能被玩家的子弹击中 ¥  不能被其他敌人的子弹击中 ¥  能被多次击中(有生命值) ¥  有固定的行为和移动方式 ¥  死亡时显示特定的行为或动画 ¥  从屏幕以外进入屏幕后将会显示 ¥  当移出屏幕后将不再显示 你可能注意到,上面有些特性也符合玩家飞船。飞船也可以射击子弹,它也可能经受多次射击;当它被摧毁时也应该呈现某个行为或动画,它给人的感觉类似一个特殊的敌人。 扫描上述列表,会有3种实现方式。可以创建一个类,把飞船、敌人、Boss都包含在其中。代码将是有选择地执行部分代码,这取决于敌人的类型。例如,射击代码可能为不同的类型提供不同的分支。对于对象有限的游戏,这是不错的办法——但它无法面对大规模的对象。随着游戏中加入越来越多地对象,你的游戏代码必将变得肥大臃肿。对这个类的任何部分进行修改,都会潜在地对敌人或者飞船的行为带来不希望的影响。用一个变量——敌人类型来决定代码执行路径是一种古老的C 编程方式,不符合 O-C 的面向对象特性。 这种方式至今仍然非常有用,但一定要慎用。 第二种方式,是创建一个类层次。用一个Entity类作为基类,从它派生出一个飞船类、2个怪物类、1个Boss类。很多程序员常这样干,对于游戏对象不多的情况这种方式也非常好用。但本质上,这和第一种方式没什么不同。基类封装了子类要用到的一些通用代码,但不是全部代码。当Entity类中的代码开始基于某个子类的类型执行某个分支时,情况变得糟糕——跟第一种方法一样了。如果小心一点,你应该确保把针对某种敌人的代码放在某个子类里,但在修改的时候很容易会把很多改动放到Entity类里。 第3种方式,是使用组件编程。这意味着不同的代码路径从Entity类层次结构中分离出来,这部分代码仅仅加到所需的子类中。比如一个“血槽”组件。基于组件的编程可以单独写成一本书,对于射击游戏这类项目而言,这显得有些杀鸡用牛刀了,因此我会混合后面两种方式一起使用,这里只是给出一个概念: 如何组合游戏对象而不是各自为政,以及这样做的好处。 我想说明的是,不存在最好的编码方式。选择某种方式完全是主观的,取决于个人经验和偏好。如果你愿意随着对手上开发的游戏的逐渐理解,不断重构你的代码库,能运行的代码比干净的代码更可取。经验让你不经过计划就能做出这些决定,让你能更快地创建更多复杂游戏。要想达到这个目的,从完成一个小游戏开始,然后慢慢地挑战自己的极限。这是个需要学习的过程,很不幸的,在这个过程中你的学习兴趣也很容易被好高骛远消灭掉。为什么每个老练的游戏编程人员会告诉新人,从简单入手,去重写经典的电玩游戏比如俄罗斯方块、帕克人、小行星。 ## 三、Entity类 Entity 类是继承自 CCSprite,只包含了Ship类中的setPosition方法定义,以使所有的Entity 实例始终在屏幕内移动。我只对代码做了一小点改动(其实就是如下面代码所示的if语句,原来的代码是没有if语句的),屏幕外的对象可以移动到屏幕内,但一旦进入屏幕后,它们不能再离开屏幕区域。在这个射击类游戏中,敌人不会从你身边走开,而是站在屏幕中间为了演示一下EnemyCache,进行简单的介绍。屏幕区域检查只是简单检查一下sprite的边框是否完全被屏幕边框所包含,如果是的话,代码将让sprite始终保持在屏幕边框内: ~~~ -(void) { } setPosition:(CGPoint)pos // 如果当前位置在屏幕外,则不需要让位置调整到屏幕内 // 这会允许对象从屏幕外部移动到屏幕内部 if (CGRectContainsRect([GameScene screenRect], [selfboundingBox])) { ...  [supersetPosition:pos]; } ~~~ ShipEntity类取代了Ship类。由于Entity类已经包含了setPosition方法,ShipEntity类只剩下了initWithShipImage方法。该方法的代码没有改变。 ## 四、EnemyEntity类 我们需要继续深入到EnemyEntity类,首先是头文件: ~~~ #import <Foundation/Foundation.h> #import"Entity.h" typedef enum{ EnemyTypeBreadman = 0, EnemyTypeSnake, EnemyTypeBoss, EnemyType_MAX, } EnemyTypes; @interface EnemyEntity : Entity { EnemyTypes type; } +(id) enemyWithType:(EnemyTypes)enemyType; +(int) getSpawnFrequencyForEnemyType:(EnemyTypes)enemyType; -(void) spawn; @end ~~~ 没有什么特别的。EnemyTypes 枚举用于3种不同的敌人类型,EnemyType_MAX用于在遍历时标志结束。EnemyEntity类使用了一个EnemyTypes变量存储类型,因此我可以用switch命令基于敌人的类型构建分支语句。EnemyEntity的实现包含许多代码,我会把它分成几个主题,并只显示相关的代码。首先是initWithType方法: ~~~ -(id) initWithType:(EnemyTypes)enemyType { type = enemyType; NSString* frameName; NSString* bulletFrameName; int shootFrequency = 300; switch (type) { case EnemyTypeBreadman: frameName= @"monster-a.png"; bulletFrameName= @"candystick.png"; break; case EnemyTypeSnake: frameName= @"monster-b.png"; bulletFrameName= @"redcross.png"; shootFrequency= 200; break; case EnemyTypeBoss: frameName= @"monster-c.png"; bulletFrameName= @"blackhole.png"; shootFrequency= 100; break; default: [NSException exceptionWithName:@"EnemyEntityException" reason:@"unhandled enemytype" userInfo:nil]; } if((self = [super initWithSpriteFrameName:frameName])) { //Create the game logic components [self addChild:[StandardMoveComponent node]]; StandardShootComponent* shootComponent = [StandardShootComponent node]; shootComponent.shootFrequency= shootFrequency; shootComponent.bulletFrameName= bulletFrameName; [self addChild:shootComponent]; //enemies start invisible self.visible = NO; [self initSpawnFrequency]; } return self; } ~~~ 方法一开始是变量赋值,根据敌人的类型,使用switch语句为每种类型提供默认值:敌人的角色帧名以及子弹的角色帧名。switch的default分支抛出异常,因为其他类型在Enemytypes枚举中未定义。这样,如果你定义了一种新的敌人类型,但是如果它不会动,或者发射出了错误的子弹,那么你会得到一个错误警告:哈,你忘记修改某些东西了! 最后别忘了调用[super init…]方法,否则super无法正确初始化并导致一个奇怪的错误然后崩溃。 接下来创建了一个组件,并把它加到EnemyEntity中。后面我会访问这个组件,在此你只需要知道StandardMoveComponent 能让敌人移动并射击。 把注意力放到initSpawnFrequency方法。 ~~~ -(void) initSpawnFrequency { // initialize how frequent the enemies willspawn if(spawnFrequency == nil) { spawnFrequency = [[CCArray alloc] initWithCapacity:EnemyType_MAX]; [spawnFrequency insertObject:[NSNumber numberWithInt:80] atIndex:EnemyTypeBreadman]; [spawnFrequency insertObject:[NSNumber numberWithInt:260] atIndex:EnemyTypeSnake]; [spawnFrequency insertObject:[NSNumber numberWithInt:1500] atIndex:EnemyTypeBoss]; //spawn one enemy immediately [self spawn]; } } +(int) getSpawnFrequencyForEnemyType:(EnemyTypes)enemyType { NSAssert(enemyType < EnemyType_MAX, @"invalidenemy type"); NSNumber* number = [spawnFrequency objectAtIndex:enemyType]; return [number intValue]; } -(void) dealloc { [spawnFrequency release]; spawnFrequency = nil; [super dealloc]; } ~~~ 我们把每种类型的敌人的出场频率记录在静态数组spawnFrequency里。第一个EnemyEntity实例负责初始化CCArray数组。CCArray不能存储原始数据类型比如整型,因此使用了NSNumber类。使用insertObject方法而不用addObject方法是为了保证对象加入时的顺序,同时别人看到这个枚举值也映射了对应的敌人类型。 dealloc方法释放了CCArray对象,并将其设为nil,这点非常重要。作为静态变量,第一个EnemyEntity对象在运行其dealloc方法时会释放spawnFrequency的内存,如果spawnFrequency不被设为nil,下一个EnemyEntity对象的dealloc方法将视图再次释放,这会“过度释放”spawnFrequency对象,导致程序崩溃。如果spawnFrequency为nil,任何发给它的消息都会被忽略,包括release消息。 spawn方法用于“生成”一个游戏对象: ~~~ -(void) spawn { CCLOG(@"spawn enemy"); // Select a spawn location just outside theright side of the screen, with random y position CGRect screenRect = [GameScene screenRect]; CGSize spriteSize = [self contentSize]; float xPos = screenRect.size.width + spriteSize.width * 0.5f; float yPos = CCRANDOM_0_1() * (screenRect.size.height - spriteSize.height)+ spriteSize.height * 0.5f; self.position = CGPointMake(xPos, yPos); // Finally set yourself to be visible, this alsoflag the enemy as "in use" self.visible = YES; } ~~~ 因为EnemyCache用于统一创建所有的敌人,这里整个spawn 方法只是设定一个随机数的y坐标,x坐标是在右侧屏幕以外。visible属性在其他地方会用到,尤其是在组件类中,用于判断EnemyEntity当前是否已使用。如果visible为NO,它可以被“生出”并显示,如果为YES,它就会按照固定的逻辑运行。 ## 五、EnemyCache类 从名字上看,这会让你想到BulletCache类,它也持有了大量已初始化对象,以便快速和简单地重用,减少了游戏时对象的创建、释放动作,而这恰恰是导致游戏流畅性下降的原因之一。尤其是动作游戏,这种不流畅给玩家体验带来了灾难性后果。以下是EnemyCache的头文件。 ~~~ #import <Foundation/Foundation.h> #import "cocos2d.h" @interface EnemyCache : CCNode { CCSpriteBatchNode* batch; CCArray* enemies; int updateCount; } @end ~~~ CCSpriteBatchNode对象包含全部敌人角色(sprite),CCArray则储存了每种敌人的列表。updateCount变量在每帧生成一个敌人时自动增加。init方法与BulletCache的init方法十分类似: ~~~ -(id) init { if((self = [super init])) { //从贴图集缓存中得到图片 CCSpriteFrame* frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"monster-a.png"]; batch = [CCSpriteBatchNode batchNodeWithTexture:frame.texture]; [self addChild:batch]; [self initEnemies]; [self scheduleUpdate]; } return self; }   ~~~ 但initEnemies方法就复杂多了: ~~~ -(void) initEnemies { // 创建enemies 数组,用于存放每种类型的敌人 enemies = [[CCArray alloc] initWithCapacity:EnemyType_MAX]; // 有多少种敌人,就创建多少个数组 for (int i = 0; i < EnemyType_MAX; i++) { //根据敌人种类的不同,设置不同的数组容量。 int capacity; switch (i) { case EnemyTypeBreadman: capacity = 6; break; case EnemyTypeSnake: capacity = 3; break; case EnemyTypeBoss: capacity = 1; break; default: [NSException exceptionWithName:@"EnemyCacheException" reason:@"unhandled enemytype" userInfo:nil]; break; } //不需要alloc数组,当数组被加到enemies数组时会自动retain CCArray* enemiesOfType = [CCArray arrayWithCapacity:capacity]; [enemies addObject:enemiesOfType]; } for (int i = 0; i < EnemyType_MAX; i++) { CCArray* enemiesOfType = [enemies objectAtIndex:i]; int numEnemiesOfType = [enemiesOfType capacity]; for (int j = 0; j < numEnemiesOfType;j++) { EnemyEntity* enemy = [EnemyEntity enemyWithType:i]; [batch addChild:enemy z:0 tag:i]; [enemiesOfTypeaddObject:enemy]; } } } ~~~ 有意思的是,CCArray enemies 对象自身包含了多个CCArray对象,每种类型的敌人使用一个CCArray。这是一个典型的 2 维数组。enemies 变量需要用alloc 分配内存,否则initEnemies 方法一结束它的内存会被释放。相反,enimies数组中的CCAray 元素对象不需要alloc,因为当它被add 到enemies数组中时会被自动retain。每种敌人所用的CCArray数组,其初始容量为该类型一次允许加到屏幕中的个数。每种敌人的CCArray数组使用addObject方法加到enemies数组。用这种方式可以创建层次深度。事实上,cocos2d结点层次结构也是通过在CCNode 类中定义一个CCArray* children成员变量来构建的。 我将enimies数组的创建和初始化分别放在在两个单独的循环体中,尽管它们其实也可以在一个循环中进行,但它们明显是属于不同的任务,应该保持分离——至于因此导致的性能上的额外开销,是微乎其微的。 根据在CCArray初始化时的初始容量,相同数目的敌人被构建出来并加入到CCSpriteBatchNode中,然后又加到对应的某种敌人使用的CCArray中。通过CCSpriteBatchNode也能访问到敌人,但单独把这些敌人放在分开的数组中更方便处理,代码列表如下所示: ~~~ -(void) spawnEnemyOfType:(EnemyTypes)enemyType { CCArray* enemiesOfType = [enemies objectAtIndex:enemyType]; EnemyEntity* enemy; CCARRAY_FOREACH(enemiesOfType, enemy) { //查找可重建的敌人,重用 if (enemy.visible== NO) { //CCLOG(@"spawn enemy type %i",enemyType); [enemy spawn]; break; } } }   -(void) update:(ccTime)delta { updateCount++;   for (int i = EnemyType_MAX- 1; i >= 0; i--) { int spawnFrequency = [EnemyEntity getSpawnFrequencyForEnemyType:i]; if (updateCount % spawnFrequency == 0) { [self spawnEnemyOfType:i]; break; } } } ~~~ update方法使计数器updateCount加1。这并不会多花费多少时间,但却是值得的,因为他会使我们接下来更轻松一些。 For循环比较奇怪,循环变量i从EnemyType_MAX开始递减,一直到i为负值。这个目的是为了让EnemyTypes 更大的怪物更早出生。例如,当boss怪和蛇同时出现时,首先让boss怪出生。否则会导致这样的事情发生,蛇会和boss争抢出生机会,甚至阻塞了Boss的出生。这个出生逻辑有一个副作用,我把它保留给你自己去解决,如果你要写一个自己的射击游戏,你可能不得不自己实现一些东西。 spawnFrequency被EnemyEntity 的getSpawnFrequncyForEnemyType方法所赋值。 ~~~ +(int) getSpawnFrequencyForEnemyType:(EnemyTypes)enemyType { NSAssert(enemyType < EnemyType_MAX, @"invalidenemy type"); NSNumber* number = [spawnFrequency objectAtIndex:enemyType]; return [number intValue]; } ~~~ 这个方法首先断言enemyType是否是有效值。然后从spawnFrequency数组中取出指定类型的敌人的NSNumber对象并返回其intValue值。 回到update方法,接下来使用取模运算%,计算updateCount能否被spawnFrequency所整除,意思是只有updateCount数到指定的数时(updateCount是个计数器),某个怪才会降生。 spanEnemyOfType方法从enemies数组中取出对应的CCArray,然后只需要遍历指定的类型的CCArray数组,而不用去遍历整个CCSrpiteBatchNode: ~~~ -(void) spawnEnemyOfType:(EnemyTypes)enemyType { CCArray* enemiesOfType = [enemies objectAtIndex:enemyType]; EnemyEntity* enemy; CCARRAY_FOREACH(enemiesOfType, enemy) { //find the first free enemy and respawn it if (enemy.visible== NO) { //CCLOG(@"spawn enemy type %i",enemyType); [enemy spawn]; break; } } } ~~~ 如果找到一个visible为NO的怪,调用其spawn方法。如果所有的该类怪的visible都是YES,当前屏幕上该类怪的数目已经达到最大,不再产生这种类别的怪,这样就限制了屏幕上同一种怪的数量。 ## 六、Component类 Component类在游戏逻辑中被视作插件。如果把一个component(组件)加在一个entity类,则该entity可以执行组件的行为:移动,射击,动画,显示生命值等等。编写组件的好处是它能自动工作,因为它们与父容器(CCNode)交互,并尽可能地不对父容器做出要求。有时候组件要求父容器必须是一个EnemyEntity类,但实际上你可以在任何类型的EnemyEntity(子类)上使用它。组件类可根据使用组件的类来配置。例如,这是一个在EnemyEntity中使用StandarShoortComponent组件的例子: ~~~ StandardShootComponent* shootComponent = [StandardShootComponent node]; shootComponent.shootFrequency= shootFrequency; shootComponent.bulletFrameName= bulletFrameName; [self addChild:shootComponent]; ~~~   shootFrequency和bulletFrameName变量是根据EnemyType来初始化的。把StandartShootComponent添加到EnemyEntity类,该类将会拥有射击的能力。因为组件类未对父容器做任何限制,你甚至可以把组件加到ShipEntity,使玩家飞船以指定射速进行自动射击。通过简单地激活或失活射击组件,你可以用很少的代码实现给玩家更换武器的效果。你仅仅是把射击代码隔离出来,然后把组建植入游戏对象并设置一些参数而已。 让武器失效并切换武器的逻辑很简单。甚至,你可以把组件使用到其他游戏。组件在封装可重用代码时非常有用,在许多游戏引擎中组件是一种标准机制。如果你想进一步了解游戏组件,请到我的blog([www.learn-cocos2d.com/2010/06/prefer-composition-inheritance/](http://www.learn-cocos2d.com/2010/06/prefer-composition-inheritance/))。 StandardShootComponent的头文件如下: ~~~ @interface StandardShootComponent : CCSprite { int updateCount; int shootFrequency; NSString* bulletFrameName; } @property (nonatomic) int shootFrequency; @property (nonatomic, copy) NSString* bulletFrameName; @end ~~~ 有两件事情值得注意。首先StandardShootComponent派生自CCSprite,尽管它没有使用任何贴图纹理。因为CCSpriteBatchNode只能包含CCSprite对象,而所有的EnemyEntity对象都被加到了CCSpriteBatchNode,而且EnemyEntity的子节点,这些都是StandardShotComponent的作用对象。因此StandardShootComponent需要从CCSprite继承以满足CCSpriteBatchNode的要求。 第2是一个NSString 指针,bulletFrameName,用@property关键字封装成了属性。如果你足够细心,应该发现在@property定义中的copy关键字。这说明只要给这个属性赋值,将产生一个复制操作。这样做对于确保这个字符串始终可用很重要, 因为字符串通常都是autorelease对象。我们也可以用retain对象,问题在于,如果源字符串被改变,这将影响到bulletFrameName,这可能不是我们希望的。 当然,copy关键字还意味着我们要负责在dealloc中释放它,如下所示。 ~~~ @implementation StandardShootComponent @synthesize shootFrequency; @synthesize bulletFrameName; -(id) init { if((self = [super init])) { [self scheduleUpdate]; } return self; } -(void) dealloc { [bulletFrameName release]; [super dealloc]; } -(void) update:(ccTime)delta { if(self.parent.visible) { updateCount++; if (updateCount >= shootFrequency) { //CCLOG(@"enemy %@ shoots!",self.parent); updateCount = 0; GameScene* game = [GameScene sharedGameScene]; CGPoint startPos = ccpSub(self.parent.position, CGPointMake(self.parent.contentSize.width * 0.5f, 0)); [game.bulletCache shootBulletFrom:startPos velocity:CGPointMake(-2, 0) frameName:bulletFrameName]; } } } @end ~~~ 真正的射击代码首先要检查父对象是否visible为YES,否则射击代码显然不应该被调用。BulletCache发射子弹时使用组件bulletFrameName 属性和固定的速度进行发射。 开始位置startPos并不是指组件自己的位置,而是使用父容器的位置和contentSize计算出来的:子弹位于角色的左边。 对于常规的怪,一个startPos就足够了,但对于Boss来说,用它的嘴或者鼻子来发射子弹,这才酷呢!我把这个工作也留给了你:为组件增加一个属性,以便子弹的初始位置可以被设置。当然,你也可以创建一种单独的BossShootComponent类,专门给Boss设计一种更复杂的射击模式。StandardMoveComponents 也是一样的, boss怪也可能需要在屏幕右边的某个位置不停盘旋。 ## 七、击中物体 几乎忘记了——你其实是想向怪物们开火并击中它们,不是吗? BulletCache类是检查子弹击中物体的理想地点。我把方法加在了BulletCache中。实际上是3个方法,2个是public的,1个是private方法,如下所示。使用这两个方法:isPlayerBulletCollidingWithRect和isEnemyBulletCollidingWithRect方法的目的是为了隐藏根据子弹的主类进行碰撞检测的内部细节。 ~~~ -(bool) isPlayerBulletCollidingWithRect:(CGRect)rect { return [self isBulletCollidingWithRect:rect usePlayerBullets:YES]; } -(bool) isEnemyBulletCollidingWithRect:(CGRect)rect { return [self isBulletCollidingWithRect:rect usePlayerBullets:YES]; } -(bool) isBulletCollidingWithRect:(CGRect)rect usePlayerBullets:(bool)usePlayerBullets { bool isColliding = NO; Bullet* bullet; CCARRAY_FOREACH([batch children], bullet) { if (bullet.visible&& usePlayerBullets == bullet.isPlayerBullet) { if(CGRectIntersectsRect([bullet boundingBox],rect)) { isColliding = YES; //remove the bullet bullet.visible= NO; break; } } } return isColliding; } ~~~   你也可以把usePlayerBullets 参数暴露给其他类,但这样把这个参数由bool类型改变为enum类型时只会更难,一旦你想使用第3种子弹怎么办? 只对看得见的子弹进行检测,同时要检查isPlayerBullet 属性,确保怪物们不会被自己的子弹击中。其实碰撞检测是件简单的事情,你可以使用CGRectIntersectsRect,如果子弹真的击中了什么,子弹自身也应该“消失”。 EnemyCache类持有所有的EenemyEntity对象,这里也是调用方法去检测是否有怪物被玩家击中的好地方。现在EnemyCache类增加了checkForBulletCollisions方法(会由update方法来调用): ~~~ -(void) checkForBulletCollisions { EnemyEntity* enemy; CCARRAY_FOREACH([batch children], enemy) { if (enemy.visible) { BulletCache* bulletCache = [[GameScene sharedGameScene] bulletCache]; CGRect bbox = [enemy boundingBox]; if([bulletCache isPlayerBulletCollidingWithRect:bbox]) { //This enemy got hit ... [enemy gotHit]; } } } } ~~~   在这里,很方便遍历所有的怪物,并忽略那些当前不可见的。使用BulletCache的isPlayerBulletCollidingWithRect方法以及怪物的boundingBox属性进行检测,我们能快速地发现一个怪是否被玩家子弹击中;如果击中,就调用EnemyEntity的gotHist方法,该方法只是简单地把怪变为不可见。 我把飞船被怪物子弹击中的练习留给了你。你必须在ShipEntity方法中调用update方法,然后实现checkForBulletCollisions方法并在update方法中调用它。你还要改变isPlayerBulletCollidingWithRect方法和isEnemyBulletColligingWithRect方法,当子弹击中时播放声效。 ## 八、Boss的血槽 作为Boss,不应该一枪毙命。应该向玩家显示boss 的生命值,当boss被击中时血槽中的数值就减少一点。首先,需要在EnemyEntity类中增加一个hitPoints成员变量(即血点),用于表明怪物需要多少次击中才会KO。initialHitPoints变量储存怪物满血状态下的血点值,因为怪物被杀死后我们需要恢复它原来的血点(别忘记,我们的怪都是可以被“重用”的)。对头文件所做的修改如下: ~~~ @interface EnemyEntity : Entity { EnemyTypes type; int initialHitPoints; int hitPoints; } @property (readonly, nonatomic) int hitPoints; ~~~ 为了表现血槽,我们需要一个组件类。很显然这就是HealthbarComponent类: ~~~ @interface HealthbarComponent : CCSprite { } -(void) reset; @end ~~~ HealthComponent类的实现则比较有趣。HealthBarComponent 根据怪物的剩余血点更新它的scaleX属性(这个scaleX来自于CCNode)。 ~~~ -(id) init { if((self = [super init])) { self.visible = NO; [self scheduleUpdate]; } return self; } -(void) reset { float parentHeight = self.parent.contentSize.height; float selfHeight = self.contentSize.height; self.position = CGPointMake(self.parent.anchorPointInPixels.x, parentHeight + selfHeight); self.scaleX = 1; self.visible = YES; } -(void) update:(ccTime)delta { if(self.parent.visible) { NSAssert([self.parent isKindOfClass:[EnemyEntity class]], @"nota EnemyEntity"); EnemyEntity* parentEntity = (EnemyEntity*)self.parent; self.scaleX = parentEntity.hitPoints/ (float)parentEntity.initialHitPoints; } else if (self.visible) { self.visible = NO; } } @end ~~~   血槽可以根据父对象的visible属性在可视/不可视之间切换。reset方法把血槽放到怪物角色的顶上。因为血点减少是通过修改scaleX属性来显示的,scaleX也应当被重置。 update方法中,当血槽的父对象是可视时,首先判断父对象是不是EnemyEntity类,因为血槽组件要使用到在EnemyEntity中才有效的某些属性,我们必须确保它的父类必须是EnemyEntity类。我把scaleX属性修改为百分数值:用当前血点除以满血点。因为不知道什么时候血点会变,我们只有在每一帧都进行这个计算,不管血点到底有没有发生变化。这样做有点性能上的浪费,对于复杂计算而言,最好是从EnemyEntity的onHit方法去调用血槽组件的方法。 在EnemyEntity的init方法中,如果怪物类型为EnemyTypeBoss,则把组件HealthbarComponent加到EnemyEntity对象。 注意:parentEntity.initialHitPoints被强制转换为float,否则”/”是进行整数除法,这样的结果永远是0。将除数使用float类型就可以保证除法是小数点除法,以得到非0的小数。 ~~~ if (type == EnemyTypeBoss) { HealthbarComponent*healthbar = [HealthbarComponent spriteWithSpriteFrameName: @"healthbar.png"]; [self addChild:healthbar]; } ~~~ spawn方法进行了扩展,包括把血点重置为满血,调用子组件中的所有血槽组件的reset方法(如果由多个的话)。我省略了对怪物类型的判断,因为血槽是很通用的,可以被任何怪物用到。 ~~~ -(void) spawn { //CCLOG(@"spawn enemy"); // 出生地点选择在屏幕右边,y坐标值为随机数 CGRect screenRect = [GameScene screenRect]; CGSize spriteSize = [self contentSize]; float xPos = screenRect.size.width + spriteSize.width * 0.5f; float yPos = CCRANDOM_0_1() * (screenRect.size.height - spriteSize.height)+ spriteSize.height * 0.5f; self.position = CGPointMake(xPos, yPos); // 出生后就表示看得见了 self.visible = YES; // 重置血点,因为我们重用的对象很可能才被打死 hitPoints = initialHitPoints; // 重置一些组件,如血槽 CCNode* node; CCARRAY_FOREACH([self children], node) { if ([node isKindOfClass:[HealthbarComponent class]]) { HealthbarComponent* healthbar = (HealthbarComponent*)node; [healthbarreset]; } } } ~~~   ## 九、结论 做出一个完整并优雅的游戏是一个很大的成果,包括大量的重构,修改代码改进射击以及允许更多的特性并让它们和谐相处。本章,学习了BulletCache和EnemyCache类的作用,使用它们对某个类的所有实例进行管理,便于在一个地方集中访问这些实例。同时起到一种“实例池”的作用,有助于改善性能。 Entity类层次示范了如何把你的类分离出来,而不需要每个游戏对象都设计一个类。使用组件类和cocos2d结点这样的层次结构的好处在于,你可以把一些很特别的功能创建为即插即用的类。这有助于用复合的方式而非继承的方式构造你的游戏对象。以这种方式编写游戏逻辑能更“柔性”,同时代码的复用性更好。最后,还学习了如何向怪物射击,以及BulletCache和EnemyCache类如何以一种直接的方式完成这个目的。HealthbarComponent提供了一个组件编程的极好例子。 这个游戏到这里还有几件事情等你完成。首先最主要的是,玩家从来不会被子弹击中。可能你想为蛇加上一个血槽,或者为boss的行为写一些特殊的移动和射击组件。总之,这是一个开始编写滚屏游戏的绝佳起点,需要的只是不断去改进它。下一章,我将讲如果使用粒子特效为这个射击游戏增加炫目的视觉效果。
';

Cocos2d开发系列(六)

最后更新于:2022-04-01 11:39:03

  Learn IPhoneand iPad Cocos2d Game Delevopment》第7章(原文中有部分无关紧要的内容未进行翻译)。 对于射击类游戏,使用重力感应进行游戏控制是不可接受的,采用虚拟手柄将会更恰当。出于“不重新发明轮子”的原则,我们将采用开源库SneakyInput。 控制玩家的飞船进行移动只是其中一件事情。我们还需要让背景能够滚动,以造成在某个方向上“前进”的感觉。为此必须自己实现背景滚动。由于CCParallaxNode的限制,它不能无限制地滚动卷轴式背景。 ## 一、高级平行视差滚动 在这个射击游戏中,我们将使用ParallaxBackground节点。同时,我们将使用CCSpriteBatchNode以提高背景图片的渲染速度。 1、创建背景层 下图显示了我用Seashore绘制背景层。   ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-04_572a0b9f373c0.gif)   每个背景层位于Seashore的单独的图层中,每一层可以保存为单独的文件,分别命名为bg0-bg6。 以这种方式创建背景层的原因在于:你既可以把各个层的背景放在一起,也可以分别把每一层存成单独的文件。所有文件大小都是480*320,似乎有点浪费。但不需要把单独把每个文件加到游戏里,只需要把它们融合在一个贴图集里。由于Zwoptex会自动去除每个图片的透明边沿,它会把这些背景层紧紧地放到一起没有丝毫空间的浪费。 把背景分层的原因不仅是便于把每一层放在不同的Z轴。严格讲,bg5.png(位于最下端)和bg6.png(位于最上端)应该是相同的Z坐标,因为它们之间没有交叠,所以我把他们存在分开的文件里。这样Zwoptex会把两者上下之间的空白空间截掉。 此外,把背景分层有利于提高帧率。iOS设备的填充率很低(每1帧能绘制的像素点数量)。由于不同图片之间常存在交叠的部分,iOS设备每1帧经常需要在同1点上绘制多次。比如,最极端的情况,一张全屏图片位于另一张全屏图片之上。你明明只能看到最上面的图片,但设备却不得不两张图片都绘制出来。这种情况叫做overdraw(无效绘制)。把背景分层可以尽量地减少无效绘制。 2、修改背景的绘制 ~~~ #import <Foundation/Foundation.h> #import "cocos2d.h" @interface ParallaxBackground :CCNode { CCSpriteBatchNode* spriteBatch; intnumStripes; CCArray* speedFactors; // 速度系数数组 floatscrollSpeed; } @end ~~~ 我把CCSpriteBatchNode引用保存在成员变量里,因为它在后面会用得比较频繁。采用成员变量访问节点比通过getNodeByTag方式访问要快一点,每1帧都会节约几个时钟周期(但保留几百个成员变量就太夸张了)。   ~~~ #import "ParallaxBackground.h" @implementation ParallaxBackground -(id) init { if ((self = [superinit])) { CGSize screenSize = [[CCDirectorsharedDirector] winSize]; //  把game_art.png加载到贴图缓存 CCTexture2D* gameArtTexture = [[CCTextureCachesharedTextureCache] addImage:@"game-art.png"]; // 初始化CCSpriteBatchNodespritebatch spriteBatch = [CCSpriteBatchNodebatchNodeWithTexture:gameArtTexture]; [selfaddChild:spriteBatch]; numStripes = 7; // 从贴图集中加载7张图片并进行定位 for (int i = 0; i < numStripes; i++) { NSString* frameName = [NSStringstringWithFormat:@"bg%i.png", i]; CCSprite* sprite = [CCSpritespriteWithSpriteFrameName:frameName]; sprite.position = CGPointMake(screenSize.width / 2, screenSize.height / 2); [spriteBatchaddChild:sprite z:i tag:i]; } // 再加7个背景层, 将其翻转并放到下一个屏幕位置的中心for (int i = 0; i < numStripes; i++) { NSString* frameName = [NSStringstringWithFormat:@"bg%i.png", i]; CCSprite* sprite = [CCSpritespriteWithSpriteFrameName:frameName]; //放到下一屏的中心 sprite.position = CGPointMake(screenSize.width / 2 + screenSize.width, screenSize.height / 2); //水平翻转 sprite.flipX =YES; [spriteBatchaddChild:sprite z:i tag:i + numStripes]; } // 初始化速度系数数组,分别定义每一层的滚动速度speedFactors = [[CCArrayalloc] initWithCapacity:numStripes]; [speedFactorsaddObject:[NSNumbernumberWithFloat:0.3f]]; [speedFactorsaddObject:[NSNumbernumberWithFloat:0.5f]]; [speedFactorsaddObject:[NSNumbernumberWithFloat:0.5f]]; [speedFactorsaddObject:[NSNumbernumberWithFloat:0.8f]]; [speedFactorsaddObject:[NSNumbernumberWithFloat:0.8f]]; [speedFactorsaddObject:[NSNumbernumberWithFloat:1.2f]]; [speedFactorsaddObject:[NSNumbernumberWithFloat:1.2f]]; NSAssert([speedFactorscount] == numStripes, @"speedFactors count does notmatch numStripes!"); scrollSpeed = 1.0f; [selfscheduleUpdate]; } returnself; } -(void) dealloc { [speedFactorsrelease]; [superdealloc]; } -(void) update:(ccTime)delta { CCSprite* sprite; CCARRAY_FOREACH([spriteBatchchildren], sprite) { NSNumber* factor = [speedFactorsobjectAtIndex:sprite.zOrder]; CGPoint pos = sprite.position; pos.x -= scrollSpeed * [factor floatValue]; sprite.position = pos; } } @end ~~~ 在GameScene中,我们曾经加载了贴图集game-art.plist: ~~~ CCSpriteFrameCache* frameCache = [CCSpriteFrameCachesharedSpriteFrameCache]; [frameCacheaddSpriteFramesWithFile:@"game-art.plist"]; ~~~ 因此,实际上game-art.png已经加载。当我们在init方法中再次加载game-art.png时(我们需要获得一个CCTexture2D以构造CCSpriteBatchNode),实际上并不会再次加载game-art.png,CCTextureCache会从缓存中返回一个已经加载的CCTexture2D对象。我们没有其他办法,因为cocos2d没有提供一个getTextureByName 的方法。 接下来,初始化了CCSpriteBatchNode对象,并从贴图集中加载了7张背景图。 在update方法中,每一层背景图的x位置每播放一帧,就减去了一点(从右向左移动)。移动的距离由scrollSpeed*一个速度系数(speedFactors数组中相应的一个数值)来计算。 这样,每1层的背景会有不同的速度系数,从而会以不同的速度移动: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-04_572a0b9f51467.gif)   由于各层移动速度不同,所以最终背景的右边沿会呈现出不整齐的现象: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-04_572a0b9f7108d.gif)   3、无限滚动 在 ParallaxBackground 类的init方法中,我们再次添加了7张背景图并进行水平翻转。目的是让每一层背景图片的宽度在水平方向上延伸,翻转的目的则是使拼在一起的时候两张图片的对接边沿能够对齐。 同时,把第2幅图片紧挨着放在第1幅图右边,从而把两张相同但互为镜像的图片拼接在一起。 这是第1幅图的位置摆放: ~~~ sprite.position = CGPointMake(screenSize.width / 2, screenSize.height / 2);   ~~~ 这是第2幅图的位置摆放: ~~~ // 放到下一屏的中心 sprite.position = CGPointMake(screenSize.width / 2 + screenSize.width, screenSize.height / 2); ~~~ 通过比较很容易就得以看出二者的x坐标相差1个屏幕宽度:screenSize(这同时也是图片宽度,我们的图片是严格按照480*320的屏幕尺寸制作的)。 下面我们可以用另外一种方式来摆放图片(更直观),把相应的代码修改为: 第1幅图的摆放: ~~~ sprite.anchorPoint = CGPointMake(0, 0.5f); sprite.position = CGPointMake(0, screenSize.height / 2); ~~~ 第2幅图的摆放: ~~~ sprite.anchorPoint = CGPointMake(0, 0.5f); sprite.position = CGPointMake(screenSize.width, screenSize.height / 2); ~~~ 我们改变了图片的anchorPoint属性。anchorPoint就是一个图形对象“锚点”或“对齐点”,这个属性对于静止不动的对象是没有意义的。但对于可以移动的对象来说,意味着位置移动的参考点。也就是说物体移动后锚点应该和目标点对齐(定点停车?)。如果命令一个物体移动到a点,真实的意思其实是把这个物体的锚点和a点对齐。锚点用一个CGPoint表示,不过这个CGPoint的x和y值都是0-1之间的小数值。一个物体的锚点,如果不改变它的话, 默认 是(0.5f, 0.5f)。这两个浮点数所代表的含义是:该锚点位于物体宽度1/2和高度1/2的地方。即物体(图形)的正中心: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-04_572a0b9f8eccc.gif)   而代码sprite.anchorPoint = CGPointMake(0, 0.5f); 实际上是把图片的锚点移到了图片左中部的位置: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-04_572a0b9fa662e.gif)   这样我们摆放第1张图时候可以从横坐标0开始摆,而不必要计算屏幕宽度。 而摆放第2张图的时候直接从第2屏的起始位置(即1个屏幕宽度)开始摆。 接下来,我们可以修改update的代码,让两幅图交替移动以模拟出背景图无限滚动的效果: ~~~ -(void) update:(ccTime)delta { CCSprite* sprite; CCARRAY_FOREACH([spriteBatchchildren], sprite) { NSNumber* factor = [speedFactorsobjectAtIndex:sprite.zOrder]; CGPoint pos = sprite.position; pos.x -= scrollSpeed * [factor floatValue]; // 当有一副图移出屏幕左边后,把它挪到屏幕右边等待再次滚动—无限滚动 if (pos.x < -screenSize.width) { pos.x+= screenSize.width * 2 - 1; } sprite.position = pos; } } ~~~ 实际上,飞船是不动的,动的是背景,以此模拟出飞船在游戏世界中前进的效果。   4、防止抖动 仔细观察,你会发现画面上有时会出现一条黑色的竖线。这是由于图片之间拼接位置出现凑整的问题。帧与帧之间,由于小数点上的误差,有时会出现1个像素宽度的缝隙。对于商业品质的游戏,应该解决这个小问题。 最简单的办法,让图片之间微微交叠1个像素。 在摆放第2幅图时: ~~~ sprite.position = CGPointMake(screenSize.width-1, screenSize.height / 2); ~~~ 在update方法中: ~~~ // 当有一副图移出屏幕左边后,把它挪到屏幕右边等待再次滚动—无限滚动 if (pos.x < -screenSize.width) { pos.x+= screenSize.width * 2 - 2; } sprite.position = pos; ~~~ 为什么是减2个像素?因为1个像素是上次拼接时“用掉”的(一开始我们在init的时候就拼接过一次)。而在update方法中,已经是第2次拼接了。1次拼接需要1个像素,两次拼接自然要2个像素。   5、重复贴图 在这一章没有其他值得注意的技巧了。你可以让同一个贴图在任意一个空间里重复。只要这个空间够大,你能让这个贴图没完没了地重复。至少成千上万像素或成打的屏幕上能够用一张贴图贴满,而不会给性能和内存带来不良影响。 这个技巧就是使用OpenGL 的GL_REPEAT参数。只不过,要重复的对象只能是边长为2的n次方的正方形。如32*32,128*128。 ~~~ CGRect repeatRect = CGRectMake(-5000, -5000, 5000,5000); CCSprite* sprite = [CCSpritespriteWithFile:@”square.png” rect:repeatRect]; ccTexParams params ={ GL_LINEAR, GL_LINEAR, GL_REPEAT,  GL_REPEAT }; [sprite.texture setTexParameters:&params]; ~~~ 这里,CCSprite必须用一个CGRect构造,这个CGRect描述了要重复贴图的矩形范围。ccTexParams参数是一个GL_REPEAT结构,这个参数用于CCTexture2D的setTexParameters方法。 这将使整个指定的矩形区域被square.png图片铺满(横向平铺,纵向平铺)。当你移动CCSprite时,整个贴图局域也被移动。你可以用这个技巧把最底层的背景删除,然后用一张简单的小图片替代。 ## 二、虚拟手柄 由于iOS设备没有按钮(除了Home键),虚拟手柄(或D-pads)在游戏中就显得很有用。 1、SneakyInput介绍 SneakyInput的作者是Nick Pannuto,示例代码由CJ Hanson提供。这是一个免费的开源项目,它接受自愿捐助:[http://pledgie.com/campaigns/9124](http://pledgie.com/campaigns/9124) 该项目源码托管于github库: [http://github.com/sneakyness/SneakyInput](http://github.com/sneakyness/SneakyInput). 源码下载后,解包,打开该项目,编译运行。你可以在模拟器中看到一个虚拟手柄。 SneakyInput中集成了cocos2d,但可能不是最新版本。如果出现”base SDKmissing”错误,你可以修改Info面板中的base SDK。 2、集成SneakyInput 对于源代码项目,有这样一个问题:当我们需要和其他项目集成时,哪些文件是必须的?每个源码项目都不一样,答案也不尽相同。 但我会告诉你SneakyInput的哪些文件是必须的,包括5个核心的类: SneakyButton 和 SneakyButtonSkinnedBase SneakyJoystick 和 SneakyJoystickSkinnedBase ColoredCircleSprite(可选的) 其他文件不是必须的,但可作为一些参考。 使用Add Existing Files对话框加入上述5个类(5个.m文件,5个.h文件)。   3、射击按钮 首先,我们需要在GameScene的scene方法中加入一个InputLayer(继承自CCLayer): InputLayer* inputLayer = [InputLayernode]; [scene addChild:inputLayer z:1tag:GameSceneLayerTagInput]; 在枚举GameSceneLayerTags中添加GameSceneLayerTagInput定义,用于InputLayer层的tag: ~~~ typedefenum { GameSceneLayerTagGame = 1, GameSceneLayerTagInput, } GameSceneLayerTags; ~~~   然后新建类InputLayer: ~~~ #import <Foundation/Foundation.h> #import "cocos2d.h" // SneakyInputheaders #import "ColoredCircleSprite.h" #import "SneakyButton.h" #import "SneakyButtonSkinnedBase.h" #import "SneakyJoystick.h" #import "SneakyJoystickSkinnedBase.h" #import "SneakyExtensions.h" @interface InputLayer : CCLayer { SneakyButton* fireButton; } @end #import "InputLayer.h" #import "GameScene.h" @interface InputLayer(PrivateMethods) -(void) addFireButton; @end @implementation InputLayer -(id) init { if ((self = [superinit])) { [selfaddFireButton]; [selfscheduleUpdate]; } returnself; } -(void) dealloc { [superdealloc]; } -(void) addFireButton { float buttonRadius = 80; CGSize screenSize = [[CCDirector sharedDirector] winSize]; fireButton = [[[SneakyButton alloc] initWithRect:CGRectZero]autorelease]; fireButton.radius = buttonRadius;  fireButton.position =CGPointMake(screenSize.width - buttonRadius,buttonRadius); [self addChild:fireButton]; } -(void) update:(ccTime)delta { if(fireButton.active) { CCLOG(@"FIRE!!!"); } } @end ~~~ 在头文件中,我们定义了一个Sneakbutton成员变量。然后我们通过addFireButton方法创建发射按钮。 因为SneakyButton的initWithRect方法的CGRect参数其实并没有用到,所以我们可以简单地传递一个CGRectZero给它。实际上SneakyButton使用radius属性代表触摸所能响应的圆形半径,我们通过简单计算(屏幕宽度-按钮半径)把射击按钮紧凑地放到屏幕的右下角。 接下来,[self shceduleUpdate]调用了update方法。 在update方法里,我简单地在Log里输出一句话,以代替射击动作。   4、订制按钮外观 我用了一个特殊的类别(Category),为SneakyButton增加了一个两个特殊的静态初始化方法,以防止你忘记alloc或者autorelease对象。如SneakyExtensions.h和SneakyExtensions.m所示: ~~~ #import "ColoredCircleSprite.h" #import "SneakyButton.h" #import "SneakyButtonSkinnedBase.h" #import "SneakyJoystick.h" #import "SneakyJoystickSkinnedBase.h" @interface SneakyButton(Extension) +(id) button; +(id) buttonWithRect:(CGRect)rect; @end interfaceSneakyButtonSkinnedBase (Extension) +(id) skinnedButton; @end #import "SneakyExtensions.h" @implementation SneakyButton(Extension) +(id) button { return [[[SneakyButtonalloc] initWithRect:CGRectZero] autorelease]; } +(id) buttonWithRect:(CGRect)rect { return [[[SneakyButtonalloc] initWithRect:rect] autorelease]; } @end @implementation SneakyButtonSkinnedBase(Extension) +(id) skinnedButton { return [[[SneakyButtonSkinnedBasealloc] init] autorelease]; } @end ~~~ 我导入了所有.h文件,因为在这个类别中,我打算对每个SneakyInput都进行扩展。 用于SneakyButton的initWithRect方法的CGRect参数其实并没有用到,所以我们可以用button方法来替代SneakyButton的初始化方法: ~~~ fireButton=[SneakyButton button]; ~~~ 现在开始订制SneakyButton的外观。首先制作4张100*100大小的图片,分别表示按钮的4个状态:默认,按下,激活,失效。默认状态即按钮未被按下时的外观,于此相反的是按下状态。激活状态仅发生在切换按钮的时候,此时按钮被激活,或获得焦点。失效状态表示按钮此时是无效的。例如,当武器过热时,你会有几秒钟无法射击,此时应该让按钮失效并让按钮显示失效状态的图片。当然,在这里,我们仅需要使用默认图片和按下图片。 修改InputLayer的addFireButton 方法为: ~~~ -(void) addFireButton { float buttonRadius = 50; CGSize screenSize = [[CCDirectorsharedDirector] winSize];   fireButton = [SneakyButtonbutton]; fireButton.isHoldable = YES; SneakyButtonSkinnedBase* skinFireButton = [SneakyButtonSkinnedBaseskinnedButton]; skinFireButton.position = CGPointMake(screenSize.width - buttonRadius, buttonRadius); skinFireButton.defaultSprite = [CCSpritespriteWithSpriteFrameName:@"button-default.png"]; skinFireButton.pressSprite = [CCSpritespriteWithSpriteFrameName:@"button-pressed.png"]; skinFireButton.button = fireButton; [selfaddChild:skinFireButton]; }   ~~~ 这里设置了isHoldable属性,这意味着当你按下按钮不放时会导致子弹不停地发射。现在,不需要设置radius属性,因为接下来的SneakyButtonSkinnedBase中的图片的大小就决定了radius的值。SneakyButtonSkinedBase的静态初始化方法skinnedButton是我们在Extension这个类别中定义过的。 现在,我们用SneakyButtonSkinnedBase替代了SneakyButton,用设置SneakyButtonSkinnedBase的位置替代了设置SneakyButton的位置。并且设置了SneakyButtonSkinnedBase的状态图片。 注意最后两句代码, SneakyButtonSkinnedBase的button属性持有了SneakyButton对象引用,这样fireButton对象隐式地被加到了InputLayer。   update方法也修改了,这次调用了GameScene的射击方法: ~~~ -(void) update:(ccTime)delta { totalTime += delta; if (fireButton.active && totalTime > nextShotTime) { nextShotTime = totalTime + 0.5f; GameScene* game = [GameScenesharedGameScene]; [game shootBulletFromShip:[game defaultShip]]; } //Allow faster shooting by quickly tapping the fire button. if (fireButton.active == NO) { nextShotTime = 0; } } ~~~ 变量totalTime和nextShortTime限制了子弹射击的速度为2发/秒。如果发射按钮的active状态为NO(意味着它未被按下),nextshortTime变量被设置为0,从而保证你下一次按下发射键时,子弹不再判断时间,直接发射。快速点击发射键导致子弹的射速会更快(超过连续发射)。   5、动作控制 我们需要使用SneakyJoystick来生成一个虚拟摇杆。首先,增加一个SneakyJoystick成员变量:SneakyJoystick* joystick; 增加一个addJoystick方法,这次我们直接使用了SneakyJoystickSkinnedBase,以定制其外观,: ~~~ -(void) addJoystick { float stickRadius = 50;   joystick = [SneakyJoystickjoystickWithRect:CGRectMake(0, 0, stickRadius,stickRadius)]; joystick.autoCenter = YES; joystick.hasDeadzone = YES; joystick.deadRadius = 10; SneakyJoystickSkinnedBase* skinStick = [SneakyJoystickSkinnedBaseskinnedJoystick]; skinStick.position = CGPointMake(stickRadius * 1.5f, stickRadius * 1.5f); skinStick.backgroundSprite = [CCSpritespriteWithSpriteFrameName:@"button-disabled.png"]; skinStick.backgroundSprite.color = ccMAGENTA; skinStick.thumbSprite = [CCSpritespriteWithSpriteFrameName:@"button-disabled.png"]; skinStick.thumbSprite.scale = 0.5f; skinStick.joystick = joystick; [selfaddChild:skinStick]; } ~~~   同样的,我们在extension类别中为SneakyJoystickSkinnedBase增加了新的静态方法skinnedJoystick: ~~~ @implementationSneakyJoystickSkinnedBase (Extension) +(id) skinnedJoystick { return [[[SneakyJoystickSkinnedBasealloc] init] autorelease]; } @end ~~~ SneakyJoystick的初始化方法需要一个CGRect参数,与SneakyButton不同,这里CGRect的确能决定摇杆的半径。autoCenter设置为YES可以使摇杆自动回到中心位置。hasDeadZone和deadRadius属性决定了你能移动的最小半径,在此范围内的移动视作无效。如果hasDeadZone=NO,你几乎不可能让摇杆稳定保持在中心位置。 摇杆与屏幕边缘稍微空出了一些距离,对于游戏而言摇杆的位置和尺寸不是最恰当的,但用来演示足够了。 如果摇杆过于靠近屏幕边缘,手指很容易移出屏幕从而失去对飞船的控制。 我决定让摇杆使用button-disabled.png作为背景图,同时摇杆大小缩放为原来的一半。这里backgroundSprite和thumbSprite使用的图片都是同一张。二者的区别是: 摇杆的手柄(thumbSprite)半径仅为按钮背景(backgroundSprite)半径的一半。button-disabled.png图片是一个灰色的圆形按钮。这样的将导致虚拟摇杆由两个灰色的正圆构成,在一个圆形的中心还有一个一半大小的小圆。 而且,把背景图选取为灰色图片是特意的。因为backgroundSprite的color属性被设置为品红,于是backgroundSprite的灰色图片被着色为品红了!通过把color属性设置为不同的颜色:红色、绿色、黄色,你可以轻易地为backgroundSprite染上不同的颜色! 当然,控制飞船移动的代码是在update方法中:   ~~~ GameScene* game = [GameScenesharedGameScene]; Ship* ship = [game defaultShip]; CGPoint velocity = ccpMult(joystick.velocity, 200); if (velocity.x != 0 && velocity.y != 0) { ship.position = CGPointMake(ship.position.x + velocity.x * delta, ship.position.y + velocity.y * delta); } ~~~ 我们在GameScene中增加了defaultShip方法,以便在这里访问ship对象。摇杆的velocity属性用于改变飞船的位置,但需要根据比例放大,这使得摇柄能在控制上能够有一个放大效果。放大比例是一个经验值,在游戏中感觉可以就行了。 万一出现update方法调用不规律的情况,为确保飞船平滑移动的效果,我们必须利用update方法的delta参数。Delta参数传递了从上次update调用以来到本次调用之间的时间值。另外,飞船可能被移出屏幕区域外——你肯定不希望这样。你可能想把代码直接加在InputLayer中ship位置被改变的地方。这会有一个问题:你是为了防止摇柄把飞船移到屏幕外?还是为了让飞船根本就没有移到屏幕外的能力?无疑,后者更为优雅——这样,你就要覆盖Ship类的setPosition方法了: ~~~ -(void) setPosition:(CGPoint)pos { CGSize screenSize = [[CCDirectorsharedDirector] winSize]; float halfWidth = contentSize_.width * 0.5f; float halfHeight = contentSize_.height * 0.5f;   //防止飞船移出屏幕 if (pos.x < halfWidth) { pos.x = halfWidth; } elseif (pos.x > (screenSize.width - halfWidth)) { pos.x = screenSize.width - halfWidth; } if (pos.y < halfHeight) { pos.y = halfHeight; } elseif (pos.y > (screenSize.height - halfHeight)) { pos.y = screenSize.height - halfHeight; } //一定要调用父类的同名方法 [supersetPosition:pos]; } ~~~   每当飞船的位置发生改变,上面的代码会对飞船的位置进行一个边界检测。如果飞船x,y坐标移出了屏幕外,它将被保持在屏幕边沿以内。 由于position是属性,下面语句会调用setPosition方法: ~~~ ship.position=CGPointMake(200,100); ~~~ 点语法比发送getter/setter消息更简短,当然我们也可以用发送消息的语法: ~~~ [shipsetPosition:CGPointMake(200,100)]; ~~~ 通过这种方法,你可以重写其他基类的方法,以改变游戏对象的行为。例如,如果要限定一个对象只能旋转0-180度,你可以重写setRotation(float)rotation方法在其中加入限制旋转的代码。   6、数字控制 如果你的游戏不适合采用模拟控制,你可以把SneakyJoystick类转换成数字控制,即 D-pad。这需要改动的代码很少: ~~~ joystick=[SneakyJoystick joystickWithRect:CGRectMake(0,0,stickRadius,stickRadius)]; joystick.autoCenter=YES; // 减少控制方向为8方向 joystick.isDPad=YES; joystick.numberOfDirections=8; ~~~   dead zone属性被删除了——在数字控制中他们不再需要了。isDPad属性设置为YES,表明采用数字控制。同时你可以定义方向数。如果你想让D-pads在上下左右4个方向的同时,增加斜角方向(同时按下两个方向将使角色沿斜角移动),你只需要把numberOfDirections设置为8。SneakyJoystick自动把模拟控制的方向转换成8个方向。当然,如果你把方向数设置成6,你会得到一些怪异的结果。   7、GP Joystick SneakyInput不是仅有的解决方案。还有GP Joystick,一个付费的商业产品,不过费用很低: [http://wrensation.com/?p=36](http://wrensation.com/?p=36)   如果你想知道GPJoystick和SneakyInput有什么区别,你可以观看GP Joystick的YouTube视频: [http://www.youtube.com/user/SDKTutor](http://www.youtube.com/user/SDKTutor) 在这里也提供了几个cocos2d的视频教程。   ## 三、结论 这章你学习了背景平行视差滚动效果:背景无限循环滚动(去除抖动),如何将背景拆分成不同的图层以便Zwoptex能去掉透明区域,同时让这些图片保持正确的位置。   接下来是指定屏幕分辨率。假设你想创建一个iPad的版本,除了必需创建1024*768的图片外,你可以使用相同技术。这个工作你可以自己尝试一下。 后半章介绍了SneakyInput,一个开源项目,可以在cocos2d游戏中加入虚拟摇杆和按钮。它并不是最好的,但对大多数游戏来说已将足够,无论如何,总胜过你自己去写虚拟摇杆的代码。 现在,飞船已经能控制了并且不再能飞出屏幕边缘了。通过按下发射按钮,它也能进行射击了。但这个游戏仍然还有许多东西要做。如果没有什么东西给你射击,那么射击游戏就不能成为射击游戏了。下一章继续。          
';

Cocos2d开发系列(五)

最后更新于:2022-04-01 11:39:01

《Learn IPhone andiPad Cocos2d Game Delevopment》第6章(原文中部分无关紧要的内容没有进行翻译)。 ## 一、 CCSpriteBatchNode 在屏幕上贴图时,图形硬件需要经过准备、渲染、清除等步骤。每次贴图都会重复这个过程。如果图形硬件能事先知道有一组拥有相同纹理的Sprite需要渲染,则这个过程会被简化。比如,一组Sprite的准备和清除动作总共只需要执行一次。 下图的例子使用了CCSpriteBacthNode。屏幕上同时有几百颗子弹飞过。如果一次只渲染一颗,那么帧率马上降到85%。使用CCSpriteBatchNode,可以避免这种情况: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-04_572a0b9beb669.gif)   通常我们这样创建一个CCSprite: ~~~ CCSprite*sprite=[CCSprite spriteWithFile:@”bullet.png”]; [selfaddChild:sprite]; ~~~ 而使用CCSpriteBatchNode则需要修改为: ~~~ CCSpriteBatchNode*batch=[CCSpriteBatchNode batchNodeWithFile:@”bullet.png”]; [selfaddChild:batch]; for(inti=0;i<100;i++){ CCSprite* sprite=[CCSpritespriteWithFile:@”bullet.png”]; [batch addChild:bullet]; } ~~~ 注意,CCSpriteBatchNode需要一个图片文件名作为参数,哪怕它根本用不着这个图片(进行显示)。可以把它看做是一个Layer,你可以用它来加入一些CCSprite节点。由于它使用了一个图片文件作为构造参数,所以在后面加入的CCSprite中必须使用相同的文件作为构造参数,否则会导致如下错误: ~~~ SpriteBatches[13879:207]*** Terminating app due to uncaught exception'NSInternalInconsistencyException', reason: 'CCSprite is not using the sametexture id' ~~~ 当采用相同纹理的CCSpite越多,则采用CCSpriteBatchNode的好处越明显。 但这有一个限制,所有的CCSprite节点都会位于同一个Z坐标(深度)上。如果子弹是“击穿”敌人并向后飞,你得使用两个Z轴不同的CCSpriteBatchNode。 另外一个限制是,CCSpriteBatchNode和加入其中的CCSprite必须使用相同的贴图。这一点在使用TextureAtlas时尤其显得重要。一个Texture Atlas可以画多个不同的图片,并且这些图片使用同一个CCSpriteBatchNode,以提高渲染速度。 Z轴的问题可以通过指定CCSpriteBatchNode中单个CCSprite的Z值来解决。如果你所有的图片都放到了一个TextureAtlas(纹理集),则你完全可以只使用一个CCSpriteBatchNode。 把CCSpriteBatchNode看成一个简单的CCLayer,它只接受使用相同图片的CCSprite,这样,你就知道怎么用它了。 在下面代码中,隐藏有一个致命的陷阱: ~~~ -(id)init{     If ((self = [superinitWithFile:@"ship.png"])) { [self scheduleUpdate];     }     return self; } ~~~ 由于-(id)init方法是默认的初始化方法,它会被其他初始化方法比如initWithFile调用。在-(id)init方法中调用了[super initWithFile…]方法,[super initWithFile…]会调用[super init], 该类覆盖了-(id)init方法,于是又会调用-(id)init方法,无限循环。 解决办法是修改方法名,比如修改为-(id)initWithShipImage。 这个教训告诉我们,在默认初始化方法-(id)init中,除了[superinit]之外,永远不要调用其他东西(其他的初始化方法)。如果你必须在初始化方法中调用[super initWith…]方法,你应当把方法名命名为initWith…。 ## 二、示例代码 1、ship类 ~~~ #import <Foundation/Foundation.h> #import "cocos2d.h" @interface Ship : CCSprite { } +(id) ship; @end #import "Ship.h" #import "Bullet.h" #import "GameScene.h" @interface Ship (PrivateMethods) -(id) initWithShipImage; @end @implementation Ship +(id) ship { return [[[selfalloc] initWithShipImage] autorelease]; } -(id) initWithShipImage { if ((self = [superinitWithFile:@"ship.png"])) { [selfscheduleUpdate]; } returnself; } -(void) dealloc { [superdealloc]; } -(void) update:(ccTime)delta { [[GameScenesharedGameScene] shootBulletFromShip:self]; } @end ~~~ ship类很简单,除了update方法。该方法调用了GameScene的shootBulletFromShip方法外([GameSceneshareGameScene]实际上只是获取GameScene 的单实例)。 2、GameScene类 ~~~ #import <Foundation/Foundation.h> #import "cocos2d.h" #import "Ship.h" typedefenum { GameSceneNodeTagBullet = 1, GameSceneNodeTagBulletSpriteBatch, } GameSceneNodeTags; @interface GameScene : CCLayer { intnextInactiveBullet; } +(id) scene; +(GameScene*) sharedGameScene; -(void) shootBulletFromShip:(Ship*)ship; @property (readonly) CCSpriteBatchNode*bulletSpriteBatch; @end #import "GameScene.h" #import "Ship.h" #import "Bullet.h" @interface GameScene(PrivateMethods) -(void) countBullets:(ccTime)delta; @end @implementation GameScene static GameScene*instanceOfGameScene; +(GameScene*) sharedGameScene { NSAssert(instanceOfGameScene != nil, @"GameSceneinstance not yet initialized!"); returninstanceOfGameScene; } +(id) scene { CCScene *scene = [CCScenenode]; GameScene *layer = [GameScenenode]; [scene addChild: layer]; return scene; } -(id) init { if ((self = [superinit])) { instanceOfGameScene = self; CGSize screenSize = [[CCDirectorsharedDirector] winSize]; CCColorLayer* colorLayer = [CCColorLayerlayerWithColor:ccc4(255, 255, 255, 255)]; [selfaddChild:colorLayer z:-1]; CCSprite* background = [CCSpritespriteWithFile:@"background.png"]; background.position = CGPointMake(screenSize.width / 2, screenSize.height / 2); [selfaddChild:background]; Ship* ship = [Shipship]; ship.position = CGPointMake(ship.texture.contentSize.width / 2, screenSize.height / 2); [selfaddChild:ship]; CCSpriteBatchNode* batch = [CCSpriteBatchNodebatchNodeWithFile:@"bullet.png"]; [selfaddChild:batch z:1tag:GameSceneNodeTagBulletSpriteBatch]; for (int i = 0; i < 400; i++) { Bullet* bullet = [Bulletbullet]; bullet.visible =NO; [batch addChild:bullet]; } [selfschedule:@selector(countBullets:) interval:3]; } returnself; } -(void) dealloc { instanceOfGameScene = nil; //don't forget to call "super dealloc" [superdealloc]; } -(void) countBullets:(ccTime)delta { CCLOG(@"Number ofactive Bullets: %i", [self.bulletSpriteBatch.childrencount]); } -(CCSpriteBatchNode*) bulletSpriteBatch { CCNode* node = [selfgetChildByTag:GameSceneNodeTagBulletSpriteBatch]; NSAssert([node isKindOfClass:[CCSpriteBatchNodeclass]], @"not aCCSpriteBatchNode"); return (CCSpriteBatchNode*)node; } -(void) shootBulletFromShip:(Ship*)ship { CCArray* bullets = [self.bulletSpriteBatchchildren]; CCNode* node = [bullets objectAtIndex:nextInactiveBullet]; NSAssert([node isKindOfClass:[Bulletclass]], @"not abullet!"); Bullet* bullet = (Bullet*)node; [bullet shootBulletFromShip:ship]; nextInactiveBullet++; if (nextInactiveBullet >= [bullets count]) { nextInactiveBullet = 0; } } @end ~~~ 现在你应该看到了,在init方法中使用CCSpriteBatchNode加入了400颗子弹(被设置为不可见了)。然后在接下来的shootBulletFromShip方法(在ship的update方法中调用)中,依次调用每一颗子弹的shootBulletFromShip方法。 3、Bullet类 ~~~ #import <Foundation/Foundation.h> #import "cocos2d.h" #import "Ship.h" @interface Bullet : CCSprite { CGPointvelocity; floatoutsideScreen; } @property (readwrite, nonatomic) CGPoint velocity; +(id) bullet; -(void) shootBulletFromShip:(Ship*)ship; @end #import "Bullet.h" @interface Bullet(PrivateMethods) -(id) initWithBulletImage; @end @implementation Bullet @synthesize velocity; +(id) bullet { return [[[selfalloc] initWithBulletImage] autorelease]; } -(id) initWithBulletImage { if ((self = [superinitWithFile:@"bullet.png"])) { } returnself; } -(void) dealloc { [superdealloc]; } // Re-Uses the bullet -(void) shootBulletFromShip:(Ship*)ship { float spread = (CCRANDOM_0_1() - 0.5f) * 0.5f; velocity = CGPointMake(1, spread); outsideScreen = [[CCDirectorsharedDirector] winSize].width; self.position = CGPointMake(ship.position.x + ship.contentSize.width * 0.5f, ship.position.y); self.visible = YES; [selfscheduleUpdate]; } -(void) update:(ccTime)delta { self.position = ccpAdd(self.position, velocity); if (self.position.x > outsideScreen) { self.visible = NO; [selfunscheduleAllSelectors]; } } @end ~~~ shootBulletFromShip方法实现了子弹的射击。Spread变量计算了一个扩散值,使从飞船中射出的子弹有1/2的机率会向上/下两边扩散。Velocity是一个每帧移动的位置偏移量。然后设置子弹的初始位置位于飞船右边。在把子弹可视状态设置为显示后,调度运行update方法(每帧调用一次)。 在update方法中,让子弹移动velocity的偏移量。这种方式,比CCMoveXX方法效率更高一些。而且这里用了一个技巧,当子弹飞出屏幕后,我们并没有立即将Bullet对象清除(为了节省资源),而是子弹设置为不可视缓存起来,方便再次使用以提高程序性能。出于这个原因,我们在GameScene类中设计了一个nextInactiveBullet变量,以此来记录已经使用掉(射出去)的子弹(设置为可视的子弹)。等所有子弹都射出去以后,nextInactiveBullet重置为0。 ## 三、增加角色动画 以下代码为ship对象增加角色动画。Ship对象的角色动画是5张连续的帧图像,以表现飞船尾部不断喷射并变化的火焰。 ~~~ -(id) initWithShipImage { if ((self = [superinitWithFile:@"ship.png"])) { // 把5张图片装入动画帧数组 NSMutableArray* frames = [NSMutableArrayarrayWithCapacity:5]; for (int i = 0; i < 5; i++) { NSString* file = [NSStringstringWithFormat:@"ship-anim%i.png", i]; // 使用贴图缓存构造2D贴图 CCTexture2D* texture = [[CCTextureCachesharedTextureCache] addImage:file]; CGSize texSize = texture.contentSize; CGRect texRect = CGRectMake(0, 0, texSize.width, texSize.height); // 用2D贴图构造动画帧 CCSpriteFrame* frame = [CCSpriteFrameframeWithTexture:texture rect:texRect offset:CGPointZero]; // 把动画帧放入数组 [frames addObject:frame]; } // 用动画帧数组构造动画对象,帧率:0.08秒/帧,标识名:move CCAnimation* anim = [CCAnimationanimationWithName:@"move"delay:0.08fframes:frames]; // 如果你把anim存储到CCSprite,则可以通过名称move来访问CCAnimation //[self addAnimation:anim]; // 构造Action:无限重复 CCAnimate* animate = [CCAnimateactionWithAnimation:anim]; CCRepeatForever* repeat = [CCRepeatForeveractionWithAction:animate]; [selfrunAction:repeat]; [selfscheduleUpdate]; } returnself; } ~~~ 为求简便,上面的代码我们也可以封装为一个新的类别Category。 1、类别AnimationHelper 利用OC中的Category,我们可以扩展CCAnimation类。Category提供了一种不需要修改类的源代码即可为类增加新方法的途径(有点象AOP?),但它不能增加新的成员变量。下面的代码为CCAnimation增加了一个Category,名为Helper(新建Class,名为CCAnimationHelper.h): ~~~ #import <Foundation/Foundation.h> #import "cocos2d.h" @interface CCAnimation (Helper) +(CCAnimation*) animationWithFile:(NSString*)name frameCount:(int)frameCount delay:(float)delay; +(CCAnimation*) animationWithFrame:(NSString*)frame frameCount:(int)frameCount delay:(float)delay; @end #import "CCAnimationHelper.h" @implementation CCAnimation (Helper) // 通过图片文件名创建CCAnimation对象. +(CCAnimation*) animationWithFile:(NSString*)name frameCount:(int)frameCount delay:(float)delay { //把前面的代码移到这里来了 NSMutableArray* frames = [NSMutableArrayarrayWithCapacity:frameCount]; for (int i = 0; i < frameCount;i++) { NSString* file = [NSStringstringWithFormat:@"%@%i.png", name, i]; CCTexture2D* texture = [[CCTextureCachesharedTextureCache] addImage:file]; CGSize texSize = texture.contentSize; CGRect texRect = CGRectMake(0, 0, texSize.width, texSize.height); CCSpriteFrame* frame = [CCSpriteFrameframeWithTexture:texture rect:texRect offset:CGPointZero]; [framesaddObject:frame]; } return [CCAnimationanimationWithName:namedelay:delayframes:frames]; } // 通过sprite frames创建CCAnimation. +(CCAnimation*) animationWithFrame:(NSString*)frame frameCount:(int)frameCount delay:(float)delay { //未实现 returnnil; } @end ~~~ 现住,在Ship类的初始化方法里,可以通过CCAnimation的类别Helper这样简单地创建动画对象了: ~~~ -(id) initWithShipImage { if ((self = [superinitWithFile:@"ship.png"])) { // 使用类别Helper来创建动画对象 CCAnimation* anim = [CCAnimationanimationWithFile:@"ship-anim"frameCount:5delay:0.08f]; // 创建Action:无限循环播放动画 CCAnimate* animate = [CCAnimateactionWithAnimation:anim]; CCRepeatForever* repeat = [CCRepeatForeveractionWithAction:animate]; [selfrunAction:repeat]; [selfscheduleUpdate]; } returnself; } ~~~ ## 四、 Texture Atlas 贴图集(或译作纹理集) 1、定义 贴图集Texture Atlas仅仅是一张大的贴图。通过使用CCSpriteBatchNode,你可以一次性渲染所有的图片。使用TextureAtlas不但节约了内存也提升了性能。 贴图的大小(宽和高)总是2的n次方——例如1024*128或256*512。由于这个规则,贴图尺寸有时候是大于图片的实际尺寸的。例如,图片大小140*600,当加载到内存时,贴图尺寸是256*1024。这显然是一种内存浪费,尤其是你有几个这样的单独的Texture时。 因此有了Texture Atlas的概念。它是一张包含了多个图片的图片,并且它的尺寸已经是对齐的。所谓对齐,即是根据前面提到的那个规则,指它的长和宽都已经是2的n次方。贴图集每一个角色帧(spriteframe)都定义为贴图集中的一部分(一个矩形区域)。这些角色帧的CGrect则定义在单独的一个.plist文件里。这样cocos2d就可以从一张大的贴图集中单独渲染某个指定的角色帧。 2、Zwoptex Zwoptex是一个2D贴图工具,付费版需要$24.95。有一个7天的试用版,下载地址[http://zwoptexapp.com](http://zwoptexapp.com/)。但Zwoptex提供了一个flash版,没有时间限制:[http://zwoptexapp.com/flashwersion](http://zwoptexapp.com/flashwersion),也基本够用(仅仅有一些限制,比如2048*2048贴图限制,角色旋转等)。 如果你不想试用Zwoptex,那么有一个可以替换的工具是TexturePacker: [http://texturepacker.com/](http://texturepacker.com/) 这里以Zwoptex 0.3b7版本为例(这个是免费版)。打开Zwoptex,默认是一个叫做Untitled的空白画布。选择菜单:SpriteSheet ——>Import Sprites,会弹出文件选择对话框,选择你需要的角色帧图片文件,点击import,于是所有的图片会导入到Zwoptex。   选择菜单:Sprite Sheet——>Settings,会弹出布局窗口: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-04_572a0b9c17d8f.gif)   你可以更改设置,包括画布大小,排序规则、行间距、列间距等,目的是用最小的贴图集容纳全部所需的图片。然后点击save去应用。 注意,除非单独为3GS、iPad和iPhone4开发,否则不要使用2048*2048的画布尺寸,因为老的型号最大只支持1024*1024。 改变画布大小时要当心,因为有时候图片会叠在一起——由于空间不足。 建议不要手动更改图片(如移动、旋转),因为这个版本并不支持,它是自动布局的。 Zwoptex会自动截掉图片中透明边沿,所以本来一样大小的图片在Zwoptex中会显得大小不一。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-04_572a0b9c2d3a6.gif)   不用担心,cocos2d会自动计算这些误差并正确显示(不用担心,这些数据都记载在.plist里)。 点击File——>save菜单,编辑结果保存为.zss文件格式(Zwoptex格式)。 点击Sprite Sheet——>Export——>Texture,编辑结果保存为.png格式。 点击Sprite Sheet——>Export——>Cordinates,编辑结果保存为.plist格式。 后两者,正是cocos2d所需要的。 3、Cocos2d中使用贴图集 首先,将Zwoptex生成的.png和.plist文件加入到项目的Resource组中。然后在代码中使用贴图集: ~~~ -(id) initWithShipImage { //用CCSpriteFrameCache加载贴图集,用.plist文件而不是.png文件做参数 CCSpriteFrameCache* frameCache = [CCSpriteFrameCachesharedSpriteFrameCache]; [frameCache addSpriteFramesWithFile:@"ship-and-bullet.plist"];   //从贴图集中加载名为ship.png的sprite,注意ship.png是.plist中定义的key,而不是真正意义的文件名 if ((self = [superinitWithSpriteFrameName:@"ship.png"])) { // 从贴图集中加载sprite帧,注意用.plist中的key值做参数而非文件名 NSMutableArray* frames =[NSMutableArray arrayWithCapacity:5]; for (inti = 0; i < 5; i++) { NSString* file = [NSString stringWithFormat:@"ship-anim%i.png", i]; CCSpriteFrame* frame = [frameCache spriteFrameByName:file]; [frames addObject:frame]; } CCAnimation* anim = [CCAnimationanimationWithName:@"move"delay:0.08fframes:frames]; CCAnimate* animate = [CCAnimateactionWithAnimation:anim]; CCRepeatForever* repeat = [CCRepeatForeveractionWithAction:animate]; [selfrunAction:repeat]; [selfscheduleUpdate]; } returnself; } ~~~ [CCSpriteFrameCachesharedSpriteFrameCache]是一个单例对象,其 addSpriteFramesWithFile 方法用于加载贴图集(需要以.plist文件名作为参数)。对于大文件贴图集(超过512*512),加载过程可能会花费数秒,应当在游戏开始前就加载。 CCSprite的initWithSpriteFrameName方法可以从贴图集中获取贴图,并设置Sprite的显示图片,但它需要以贴图集(.plist)中的帧名(framename,实际上是<frames>中的一个<key>)为参数。 当然,如果要从贴图集中得到一个帧,可以用CCSpriteFrameCache的spriteFrameByName方法。这同样需要用贴图集中的帧名为参数。 如果你加载了多个贴图集,但只要名为ship.png的帧只有1个,那么cocos2d就可以找到正确贴图的。   其他代码没有任何改变。但出现了一个奇怪的现象:飞船的位置莫名其妙地向屏幕中心靠近了一点,尽管不是很明显。这个问题很容易解决,之前Ship的初始化代码是这样的: ~~~ Ship* ship = [Shipship]; ship.position = CGPointMake(ship.texture.contentSize.width / 2, screenSize.height / 2); [selfaddChild:ship];   ~~~ 这个地方需要改变: ~~~ ship.position = CGPointMake(ship.contentSize.width / 2, screenSize.height / 2); ~~~ 问题解决了。导致这个现象的原因是,ship对象的texture的contentSize要比ship对象的contentSize大(Ship的Texture现在用的是贴图集——具体说就是ship-and-bullet.png 这张图,尺寸为256*256,而原来的ship.png才128*64)。 4、修改CCAnimation类别Helper 现在是实现类别Helper中的animationWithFrame方法的时候了: ~~~ +(CCAnimation*)animationWithFrame:(NSString*)frame frameCount:(int)frameCount delay:(float)delay { //构造一个frame数组 NSMutableArray* frames = [NSMutableArray arrayWithCapacity:frameCount]; //通过CCSpriteFrameCache从贴图集中加载frame,并将frame加到数组中 for (inti = 0; i < frameCount; i++) { NSString* file = [NSStringstringWithFormat:@"%@%i.png", frame, i]; CCSpriteFrameCache* frameCache = [CCSpriteFrameCachesharedSpriteFrameCache]; CCSpriteFrame* frame = [frameCache spriteFrameByName:file]; [framesaddObject:frame]; } //用frame数组构建animation对象并返回 return [CCAnimationanimationWithName:frame delay:delayframes:frames]; } ~~~ 现在,可以修改ship类中initWithShipImage方法,把CCAnimation的初始化修改为: ~~~ CCAnimation* anim = [CCAnimationanimationWithFrame:@"ship-anim"frameCount:5delay:0.08f]; ~~~   5、弱水三千,只取一瓢饮 只要你愿意,你可以把所有游戏图片都加到一个贴图集里。用3个1024*1024的贴图集跟用20个更小的贴图集效率更高。 对于程序员而言,应当把代码“分离”成不同的逻辑组件。于此不同,对于贴图集来说,我们的目标就是尽可能多地把图片放到一个贴图集里,尽可能降低内存空间的浪费。 用一个贴图集放入玩家图片,用另外的贴图集放怪物A、B、C的图片——这好像更符合逻辑些,但这仅仅有助于你有大量的图片,而且每次你只是有选择地加载一部分图片的时候。 当你的图片只需要3-4个1024*1024贴图集的时候,你应当把所有图片只放在这些贴图集里进行预加载。这需要12-16MB的内存。程序代码和其他资源如音频则不会占用这么多内存,你可以把这些贴图集都保留在内存,这样哪怕只有128MBRAM的老IOS设备也可以承受。 如果超过这个内存,就应该采取特别的策略了。比如,可以把游戏图片进行分类,并且只在当前地图中加载必要的贴图集。这可以减少地图加载时的延时。 因为cocos2d会自动缓存所有图片,需要一种卸载贴图的机制。绝大部分情况下你可以使用cocos2d提供的: ~~~ [[CCSpriteFrameCachesharedSpriteFrameCache]removeUnusedSpriteFrames]; [[CCTextureCachesharedTextureCache] removeUnusedTextures]; ~~~ 显然应当在某些贴图不再使用的时候调用这些方法。比如转场景完成后。游戏正在进行时就不行了。注意,仅仅在新场景初始化结束后,前面的场景才会被deallocated。意即在一个Scenen的初始化方法中你不能调用removeUnusedxxx方法——除非你在两个scene转换中使用了第5章的LoadingScene类,这样你要扩展LoadingScene使它在加载新场景替时remove所有未使用的贴图。 如果要绝对清除所有内存中的贴图以加载新的贴图,应当使用: ~~~ [CCSpriteFrameCachepurgeSharedSpriteFrameCache]; [CCTextureCachepurgeSharedTextureCache]; ~~~ 6、美工和艺术 图片的编辑你可以使用免费软件Searshore:[http://seashore.sourceforge.net](http://seashore.sourceforge.net/) 对于动画和位图,你可以使用Pixen:[http://opensword.org/Pixen](http://opensword.org/Pixen),这是一个简单易用和强大的动画编辑工具。 如果你想自己制作音乐音效,可以使用Mac自带的GarageBand。插上一只麦克风开始记录一些声音,花几分钟编辑并在任何适当的地方使用这些声音。如果不想自己制作声效,也可以从网上搜索一些免费或便宜的音频,例如[www.soundsnap.com](http://www.soundsnap.com/)或者类似网站。 警告:对于使用源自互联网的音频和图片要尤其小心。你可以下载到一些“免费”的资源用于早期的游戏制作。但简单的“免费”并不意味着你可以再分发这些文件,或者用于商业用途,例如在苹果商店中销售。除非获得作者特别的授权或者这些资源附有一个正式的协议。      
';

Cocos2d开发系列(四)

最后更新于:2022-04-01 11:38:59

《Learn IPhone andiPad Cocos2d Game Delevopment》的第5章。 ## 一、使用多场景 很少有游戏只有一个场景。这个例子是这个样子的:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-04_572a0b97c29b4.gif) 这个Scene中用到了两个Layer,一个Layer位于屏幕上方,标有”Herebe your Game Scores etc“字样的标签,用于模拟游戏菜单。一个Layer位于屏幕下方,一块绿色的草地上有一些随机游动的蜘蛛和怪物,模拟了游戏的场景。 1、加入新场景 一个场景是一个Scene类。加入新场景就是加入更多的Scene类。 有趣的是场景之间的切换。使用[CCDirectorreplaceScene]方法转场时,CCNode有3个方法会被调用:OnEnter、OnExit、onEnterTransitionDidFinish。 覆盖这3个方法时要牢记,始终要调用super的方法,避免程序的异常(比如内存泄露或场景不响应用户动作)。 ~~~ -(void)onEnter { // node的 init方法后调用. // 如果使用CCTransitionScene方法,在转场开始后调用. [superonEnter]; } -(void )onEnterTransitionDidFinish { // onEnter方法后调用. // 如果使用CCTransitionScene方法,在转场结束后调用. [superonEnterTransitionDidFinish]; } -(void)onExit { // node的dealloc 方法前调用. // 如果使用CCTransitionScene方法,在转场结束时调用. [superonExit]; } ~~~ 当场景变化时,有时候需要让某个node干点什么,这时这3个方法就派上用场了。 与在node的init方法和dealloc方法中做同样的事情不同,在onEnter方法执行时,场景已经初始化了;而在onExit方法中,场景的node仍然是存在的。 这样,在进行转场时,你就可以暂停动画或隐藏用户界面元素,一直到转场完成。这些方法调用的先后顺序如下(使用replaceScene 方法): 1. 第2个场景的 scene方法 2. 第2个场景的 init方法 3. 第2个场景的 onEnter方法 4. 转场 5. 第1个场景的 onExit方法 6. 第2个场景的 onEnterTransitionDidFinish方法 7. 第1个场景的 dealloc方法 ## 二、请稍候⋯⋯ 切换场景时,如果场景的加载是一个比较耗时的工作,有必要用一个类似“Loading,please waiting…”的场景来过渡一下。用于在转场时过渡的场景是一个“轻量级”的Scene类,可以显示一些简单的提示内容: ~~~ typedefenum { TargetSceneINVALID = 0, TargetSceneFirstScene, TargetSceneOtherScene, TargetSceneMAX, } TargetScenes; @interface LoadingScene : CCScene { TargetScenes targetScene_; } +(id)sceneWithTargetScene:(TargetScenes)targetScene; -(id)initWithTargetScene:(TargetScenes)targetScene; @end #import "LoadingScene.h" #import "FirstScene.h" #import "OtherScene.h" @interface LoadingScene(PrivateMethods) -(void) update:(ccTime)delta; @end @implementation LoadingScene +(id)sceneWithTargetScene:(TargetScenes)targetScene; { return [[[self alloc]initWithTargetScene:targetScene] autorelease]; } -(id)initWithTargetScene:(TargetScenes)targetScene { if ((self = [super init])) { targetScene_ = targetScene; CCLabel* label = [CCLabellabelWithString:@"Loading ..." fontName:@"Marker Felt" fontSize:64]; CGSize size = [[CCDirectorsharedDirector] winSize]; label.position =CGPointMake(size.width / 2, size.height / 2); [self addChild:label]; [self scheduleUpdate]; } returnself; } -(void) update:(ccTime)delta { [selfunscheduleAllSelectors]; switch (targetScene_) { case TargetSceneFirstScene: [[CCDirector sharedDirector] replaceScene:[FirstScene scene]]; break; case TargetSceneOtherScene: [[CCDirector sharedDirector] replaceScene:[OtherScene scene]]; break; default: // NSStringFromSelector(_cmd) 打印方法名 NSAssert2(nil, @"%@: unsupported TargetScene %i", NSStringFromSelector(_cmd), targetScene_); break; } } -(void) dealloc { CCLOG(@"%@: %@", NSStringFromSelector(_cmd), self); [super dealloc]; } @end ~~~ 首先,定义了一个枚举。这个技巧使LoadingScene能用于多个场景的转场,而不是固定地只能在某个场景的切换时使用。继续扩展这个枚举的成员,使LoadingScene能适用与更多目标Scene的转场。 sceneWithTargetScene方法中返回了一个autorelease的对象。在coco2d自己的类中也是一样的,你要记住在每个静态的初始化方法中使用autorelease。 在方法中,构造了一个CCLabel,然后调用scheduleUpdate方法。scheduleUpdate方法会在下一个时间(约一帧)后调用update方法。在update方法中,我们根据sceneWithTargetScene方法中指定的枚举参数,切换到另一个scene。在这个scene的加载完成之前,LoadingScene会一直显示并且冻结用户的事件响应。 我们不能直接在初始化方法initWithTargetScene中直接切换scene,这会导致程序崩溃。记住,在一个Node还在初始化的时候,千万不要在这个scene上调用CCDirector的replaceScene方法。 LoadingScene的使用很简单,跟一般的scene一样: ~~~ CCScene* newScene = [LoadingScenesceneWithTargetScene:TargetSceneFirstScene]; [[CCDirectorsharedDirector] replaceScene:newScene]; ~~~ ## 三、使用Layer Layer类似Photoshop中层的概念,在一个scene中可以有多个Layer: ~~~ typedefenum { LayerTagGameLayer, LayerTagUILayer, } MultiLayerSceneTags; typedefenum { ActionTagGameLayerMovesBack, ActionTagGameLayerRotates, }MultiLayerSceneActionTags; @classGameLayer; @classUserInterfaceLayer; @interface MultiLayerScene :CCLayer { boolisTouchForUserInterface; } +(MultiLayerScene*) sharedLayer; @property (readonly) GameLayer* gameLayer; @property (readonly) UserInterfaceLayer*uiLayer; +(CGPoint) locationFromTouch:(UITouch*)touch; +(CGPoint) locationFromTouches:(NSSet *)touches; +(id) scene; @end @implementation MultiLayerScene static MultiLayerScene* multiLayerSceneInstance; +(MultiLayerScene*) sharedLayer { NSAssert(multiLayerSceneInstance != nil, @"MultiLayerScenenot available!"); returnmultiLayerSceneInstance; } -(GameLayer*) gameLayer { CCNode* layer = [selfgetChildByTag:LayerTagGameLayer]; NSAssert([layer isKindOfClass:[GameLayerclass]], @"%@: not aGameLayer!", NSStringFromSelector(_cmd)); return (GameLayer*)layer; } -(UserInterfaceLayer*) uiLayer { CCNode* layer = [[MultiLayerScenesharedLayer] getChildByTag:LayerTagUILayer]; NSAssert([layer isKindOfClass:[UserInterfaceLayerclass]], @"%@: not aUserInterfaceLayer!", NSStringFromSelector(_cmd)); return (UserInterfaceLayer*)layer; } +(CGPoint) locationFromTouch:(UITouch*)touch { CGPoint touchLocation = [touchlocationInView: [touch view]]; return [[CCDirectorsharedDirector] convertToGL:touchLocation]; } +(CGPoint) locationFromTouches:(NSSet*)touches { return [selflocationFromTouch:[touches anyObject]]; } +(id) scene { CCScene* scene = [CCScenenode]; MultiLayerScene* layer = [MultiLayerScenenode]; [scene addChild:layer]; return scene; } -(id) init { if ((self = [superinit])) { NSAssert(multiLayerSceneInstance == nil, @"anotherMultiLayerScene is already in use!"); multiLayerSceneInstance = self; GameLayer* gameLayer = [GameLayernode]; [selfaddChild:gameLayerz:1tag:LayerTagGameLayer]; UserInterfaceLayer* uiLayer = [UserInterfaceLayernode]; [selfaddChild:uiLayerz:2tag:LayerTagUILayer]; } returnself; } -(void) dealloc { CCLOG(@"%@: %@", NSStringFromSelector(_cmd), self); [superdealloc]; } @end ~~~ MultiLayerScene 中使用了多个Layer:一个GameLayerh 和一个UserInterfaceLayer 。 MultiLayerScene 使用了静态成员multiLayerSceneInstance 来实现单例。 MultiLayerScene也是一个Layer,其node方法实际上调用的是实例化方法init——在其中,我们加入了两个Layer,分别用两个枚举LayerTagGameLayer 和LayerTagUILayer 来检索,如属性方法gameLayer和uiLayer所示。 uiLayer是一个UserInterfaceLayer,用来和用户交互,在这里实际上是在屏幕上方放置一个菜单,可以把游戏的一些统计数字比如:积分、生命值放在这里: ~~~ typedefenum { UILayerTagFrameSprite, }UserInterfaceLayerTags; @interface UserInterfaceLayer :CCLayer { } -(bool) isTouchForMe:(CGPoint)touchLocation; @end @implementation UserInterfaceLayer -(id) init { if ((self = [superinit])) { CGSize screenSize = [[CCDirectorsharedDirector] winSize]; CCSprite* uiframe = [CCSpritespriteWithFile:@"ui-frame.png"]; uiframe.position = CGPointMake(0, screenSize.height); uiframe.anchorPoint = CGPointMake(0, 1); [selfaddChild:uiframe z:0tag:UILayerTagFrameSprite]; // 用Label模拟UI控件( 这个Label没有什么作用,仅仅是演示). CCLabel* label = [CCLabellabelWithString:@"Here be yourGame Scores etc"fontName:@"Courier"fontSize:22]; label.color = ccBLACK; label.position = CGPointMake(screenSize.width / 2, screenSize.height); label.anchorPoint = CGPointMake(0.5f, 1); [selfaddChild:label]; self.isTouchEnabled = YES; } returnself; } -(void) dealloc { CCLOG(@"%@: %@", NSStringFromSelector(_cmd), self); [superdealloc]; } -(void)registerWithTouchDispatcher { [[CCTouchDispatchersharedDispatcher] addTargetedDelegate:selfpriority:-1swallowsTouches:YES]; } // 判断触摸是否位于有效范围内. -(bool) isTouchForMe:(CGPoint)touchLocation { CCNode* node = [selfgetChildByTag:UILayerTagFrameSprite]; returnCGRectContainsPoint([node boundingBox], touchLocation); } -(BOOL) ccTouchBegan:(UITouch*)touch withEvent:(UIEvent *)event { CGPoint location = [MultiLayerScenelocationFromTouch:touch]; bool isTouchHandled = [selfisTouchForMe:location]; if (isTouchHandled) { // 颜色改变为红色,表示接收到触摸事件. CCNode* node = [selfgetChildByTag:UILayerTagFrameSprite]; NSAssert([node isKindOfClass:[CCSpriteclass]], @"node is not a CCSprite"); ((CCSprite*)node).color = ccRED; // Action:旋转+缩放. CCRotateBy* rotate = [CCRotateByactionWithDuration:4angle:360]; CCScaleTo* scaleDown = [CCScaleToactionWithDuration:2scale:0]; CCScaleTo* scaleUp = [CCScaleToactionWithDuration:2scale:1]; CCSequence* sequence = [CCSequenceactions:scaleDown, scaleUp, nil]; sequence.tag = ActionTagGameLayerRotates; GameLayer* gameLayer = [MultiLayerScenesharedLayer].gameLayer; // 重置GameLayer 属性,以便每次动画都是以相同的状态开始 [gameLayer stopActionByTag:ActionTagGameLayerRotates]; [gameLayer setRotation:0]; [gameLayer setScale:1]; // 运行动画 [gameLayer runAction:rotate]; [gameLayer runAction:sequence]; } return isTouchHandled; } -(void) ccTouchEnded:(UITouch*)touch withEvent:(UIEvent *)event { CCNode* node = [selfgetChildByTag:UILayerTagFrameSprite]; NSAssert([node isKindOfClass:[CCSpriteclass]], @"node is not aCCSprite"); // 色彩复原 ((CCSprite*)node).color = ccWHITE; } @end   ~~~ 为了保证uiLayer总是第一个收到touch事件,我们在 registerWithTouchDispatcher 方法中使用-1的priority。并且用 isTouchForMe 方法检测touch是否处于Layer的范围内。如果在,touchBegan方法返回YES,表示“吃掉”touch事件(即不会传递到下一个Layer处理);否则,返回NO,传递给下一个Layer(GameLayer)处理。 而在GameLayer中,registerWithTouchDispatcher 的priority是0 以下是GameLayer代码: ~~~ @interface GameLayer : CCLayer { CGPointgameLayerPosition; CGPointlastTouchLocation; } @end @interface GameLayer(PrivateMethods) -(void) addRandomThings; @end @implementation GameLayer -(id) init { if ((self = [superinit])) { self.isTouchEnabled = YES; gameLayerPosition = self.position; CGSize screenSize = [[CCDirectorsharedDirector] winSize]; CCSprite* background = [CCSpritespriteWithFile:@"grass.png"]; background.position = CGPointMake(screenSize.width / 2, screenSize.height / 2); [selfaddChild:background]; CCLabel* label = [CCLabellabelWithString:@"GameLayer"fontName:@"MarkerFelt"fontSize:44]; label.color = ccBLACK; label.position = CGPointMake(screenSize.width / 2, screenSize.height / 2); label.anchorPoint = CGPointMake(0.5f, 1); [selfaddChild:label]; [selfaddRandomThings]; self.isTouchEnabled = YES; } returnself; } // 为node加上一个MoveBy的动作(其实就是在围绕一个方框在绕圈) -(void)runRandomMoveSequence:(CCNode*)node { float duration = CCRANDOM_0_1() * 5 + 1; CCMoveBy* move1 = [CCMoveByactionWithDuration:duration position:CGPointMake(-180, 0)]; CCMoveBy* move2 = [CCMoveByactionWithDuration:duration position:CGPointMake(0, -180)]; CCMoveBy* move3 = [CCMoveByactionWithDuration:duration position:CGPointMake(180, 0)]; CCMoveBy* move4 = [CCMoveByactionWithDuration:duration position:CGPointMake(0, 180)]; CCSequence* sequence = [CCSequenceactions:move1, move2, move3,move4, nil]; CCRepeatForever* repeat = [CCRepeatForeveractionWithAction:sequence]; [node runAction:repeat]; } // 模拟一些游戏对象,为每个对象加上一些动作(绕圈). -(void) addRandomThings { CGSize screenSize = [[CCDirectorsharedDirector] winSize]; for (int i = 0; i < 4; i++) { CCSprite* firething = [CCSpritespriteWithFile:@"firething.png"]; firething.position = CGPointMake(CCRANDOM_0_1() * screenSize.width, CCRANDOM_0_1() * screenSize.height); [selfaddChild:firething]; [selfrunRandomMoveSequence:firething]; } for (int i = 0; i < 10; i++) { CCSprite* spider = [CCSpritespriteWithFile:@"spider.png"]; spider.position = CGPointMake(CCRANDOM_0_1() * screenSize.width, CCRANDOM_0_1() * screenSize.height); [selfaddChild:spider]; [selfrunRandomMoveSequence:spider]; } } -(void) dealloc { CCLOG(@"%@: %@", NSStringFromSelector(_cmd), self); //don't forget to call "super dealloc" [superdealloc]; } -(void)registerWithTouchDispatcher { [[CCTouchDispatchersharedDispatcher] addTargetedDelegate:selfpriority:0swallowsTouches:YES]; } -(BOOL) ccTouchBegan:(UITouch*)touch withEvent:(UIEvent *)event { // 记录开始touch时的位置. lastTouchLocation = [MultiLayerScenelocationFromTouch:touch]; //先停止上一次动作,以免对本次拖动产生干扰. [selfstopActionByTag:ActionTagGameLayerMovesBack]; //吃掉所有touche returnYES; } -(void) ccTouchMoved:(UITouch*)touch withEvent:(UIEvent *)event { //记录手指移动的位置 CGPoint currentTouchLocation =[MultiLayerScenelocationFromTouch:touch]; //计算移动的距离 CGPoint moveTo = ccpSub(lastTouchLocation,currentTouchLocation); //上面的计算结果要取反.因为接下来是移动前景,而不是移动背景 moveTo = ccpMult(moveTo,-1); lastTouchLocation =currentTouchLocation; //移动前景——修改Layer的位置,将同时改变Layer所包含的nodeself.position = ccpAdd(self.position, moveTo); } -(void) ccTouchEnded:(UITouch*)touch withEvent:(UIEvent *)event { //最后把Layer的位置复原.Action:移动+渐慢 CCMoveTo* move = [CCMoveToactionWithDuration:1position:gameLayerPosition]; CCEaseIn* ease = [CCEaseInactionWithAction:move rate:0.5f]; ease.tag =ActionTagGameLayerMovesBack; [selfrunAction:ease]; } @end ~~~ 为了让程序运行起来更有趣,GameLayer中加入了一张青草的背景图,以及一些游戏对象,并让这些对象在随机地移动。这部分内容不是我们关注的,我们需要关注的是几个touch方法的处理。 1、ccTouchBegan : 由于GameLayer是最后收到touch事件的Layer,我们不需要检测touch是否在Layer范围(因为传给它的都是别的Layer“吃剩下”的touch)。所以GameLayer的touchBegan方法只是简单的返回YES(“吃掉”所有touch)。 2、ccTouchMoved: 在这里我们计算手指移动的距离,然后让Layer作反向运动。为什么要作“反向”运动?因为我们想制造一种屏幕随着手指划动的感觉,例如: 当手向右划动时,屏幕也要向右运动。当然,iPhone不可能真的向右运动。要想模拟屏幕向右运动,只需让游戏画面向左运动即可。因为当运动物体在向前移动时,如果假设运动物体固定不动,则可以认为是参照物(或背景)在向后运动。 3、ccTouchEnded: 在这里,我们把Layer的位置恢复到原位。   ## 四、其他 这一章还讨论了很多有用的东西,比如“关卡”。是使用Scene还是Layer作为游戏关卡? 作者还建议在设计Sprite时使用聚合而不要使用继承。即Sprite设计为不从CCNode继承,而设计为普通的NSObject子类(在其中聚合了CCNode)。 此外还讨论了CCTargetToucheDelegate、CCProgressTimer、CCParallaxNode、vCCRibbon和CCMotionStreak。 这些东西可以丰富我们的理论知识,但没有必要细读。  
';

Cocos2d开发系列(三)

最后更新于:2022-04-01 11:38:56

正好不知道接下来要怎么写的时候,发现了一本好书:《Learn IPhone and iPad Cocos2d Game Delevopment》。于是直接翻译了第4章的例子。如果你看过这部分内容,可以直接跳过不看了。本章讲如何响应加速器事件。   ## 一、游戏介绍 这个例子是一个叫做DoodleDrop的游戏,是一个重力感应类游戏。玩家操纵角色来躲避从空中坠落的障碍物。游戏界面如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-04_572a0b95e0ffe.gif)   ## 二、设置主场景 1、新建Cocos2dApplication,工程名DoodleDrop。 2、游戏主场景。选File-> new file,选择User Templates -> Cocos2d.0.99.x -> CCNode.class。SubclassOf选择CCLayer。文件名选GameScene。 3、在头文件中声明静态方法+(id) scene; 4、.m文件 ~~~ #import"GameScene.h" @implementationGameScene +(id) scene { CCScene *scene = [CCScene node]; CCLayer* layer = [GameScene node]; [scene addChild:layer]; return scene; } -(id) init { if ((self = [super init])) { CCLOG(@"%@: %@",NSStringFromSelector(_cmd), self); returnself; } } -(void)dealloc { // never forget to call [super dealloc] [superdealloc]; CCLOG(@"%@: %@",NSStringFromSelector(_cmd), self); [super dealloc]; } @end ~~~ 5、删除HelloWorldScene.h和HelloWorldScene.m文件。 6、修改DoodleDropAppDelegate.m,将其中的主场景启动代码修改为GameScene: ~~~ [[CCDirectorsharedDirector] runWithScene: [GameScene scene]]; ~~~ ## 三、游戏角色 1、把玩家角色图片alien.png添加到工程。添加时,选中“Copy items”,同时勾选“add to targets”中的“DoodleDrop”选项。 2、在游戏主场景(GameScene.h)增加变量声明: CCSprite*player; 3、在游戏主场景(GameScene.m)的init方法中加入下列代码: ~~~ self.isAccelerometerEnabled= YES; player =[CCSprite spriteWithFile:@"alien.png"]; [selfaddChild:player z:0 tag:1]; CGSizescreenSize = [[CCDirector sharedDirector] winSize];  float imageHeight = [playertexture].contentSize.height; player.position= CGPointMake(screenSize.width / 2, imageHeight / 2); ~~~ 这样,玩家角色就被放到屏幕底部正中的位置上。注意,player变量未retain。因为addChild会自动retain。 [playertexture].contentSize.height返回的是渲染图的content size。渲染对象(玩家角色图片alient.png)有两个尺寸:contentsize和texture size。前者是图片的实际尺寸,后者是渲染尺寸——iPhone规定渲染尺寸只能是2的n次方。比如图片实际尺寸100*100,那么渲染尺寸则是128*128,因为最接近100的2的n次方为128。 ## 四、使用加速器 1、为了响应加速器事件,你必须在init方法中加上: ~~~ self.isAccelerometerEnabled= YES; ~~~ 同时实现accelerometer方法: ~~~ -(void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration*)acceleration{ CGPoint pos = player.position;  pos.x += acceleration.x * 10; player.position = pos; } ~~~ 跟java和c不同。你不能对player.position.x进行赋值。这种赋值在 c语言中是可以的,但oc中不行。因为player.position实际上是调用[playerposition],这个方法返回一个临时的CGPoint变量。当你想对这个临时的CGPoint的x进行赋值后,这个变量会被被抛弃,所以你的赋值没有任何作用。所以你需要用一个新的CGPoint变量,修改其x值,然后再把这个CGPoint赋值给player.position(即调用[playersetPosition:])。如果你是来自java和c++的程序员,在oc中需要留心这个“不幸的”问题并尽可能的修改编程习惯。 2、运行测试 模拟器不支持重力感应,请在物理设备上运行代码。 ## 五、玩家控制 现住发现用加速器控制有些不灵?反应迟钝,移动也不流畅?为此,我们需要增加一些代码。 首先需要增加变量声明: ~~~ CGPointplayerVelocity; ~~~ 为了便于今后的扩展(假设有一天我们会想上下移动角色),这是一个CGPoint类型,而不是一个float。 然后修改加速器方法: ~~~ -(void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration*)acceleration { // 减速度系数(值越小=转向越快) float deceleration = 0.4f; // 加速度系数 (值越大 = 越敏感) float sensitivity = 6.0f; // 最大速度 float maxVelocity = 100; // 根据加速度计算当前速度  playerVelocity.x = playerVelocity.x * deceleration + acceleration.x* sensitivity; // 限制最大速度为 ±maxVelocity之间  directions if (playerVelocity.x > maxVelocity) { playerVelocity.x= maxVelocity; } else if (playerVelocity.x < - maxVelocity){ playerVelocity.x= - maxVelocity; } } ~~~ 现在,玩家速度由一个一次线性方程决定: V= V₀ * β + V∍ * ε 其中, V为终速 V₀为初速 β    为减速系数 V∍为加速度 ε为加速系数 其中,β 和 ε两个系数(即减速度系数和加速度系数:deceleration和sensitivity变量)是两个经验值,你可以自己调整它以达到理想效果。 然后,需要通过以下方法来改变游戏角色的位置: ~~~ -(void) update:(ccTime)delta{ // 不断改变角色x坐标  CGPointpos = player.position; pos.x += playerVelocity.x; // 防止角色移到屏幕以外  CGSizescreenSize = [[CCDirector sharedDirector] winSize]; float imageWidthHalved = [playertexture].contentSize.width * 0.5f; float leftBorderLimit = imageWidthHalved; float rightBorderLimit = screenSize.width -imageWidthHalved; if (pos.x < leftBorderLimit) { pos.x =leftBorderLimit; playerVelocity= CGPointZero; } else if(pos.x > rightBorderLimit) { pos.x =rightBorderLimit; playerVelocity= CGPointZero; } player.position = pos;  } ~~~ 然后,在init方法中加入: ~~~ [selfscheduleUpdate]; ~~~ 这样,每隔一段时间cocos2d会自动调用update方法。 ## 六、添加障碍物 导入spider.png图片到工程。这是一张蜘蛛的图片,在游戏中我们需要躲避的东西。 首先,增加如下变量声明: ~~~ CCArray*spiders; floatspiderMoveDuration;  int numSpidersMoved; ~~~ 在init方法中,加上一句方法调用语句: ~~~ [selfinitSpiders]; ~~~ 下面是initSpiders方法: ~~~ -(void) { CGSize screenSize = [[CCDirectorsharedDirector] winSize]; // 用一个临时的CCSprider取得图片宽度 CCSprite* tempSpider = [CCSpritespriteWithFile:@"spider.png"]; floatimageWidth = [tempSpider texture].contentSize.width; // 计算出要多少蜘蛛图片可以布满屏幕的宽度 int numSpiders = screenSize.width / imageWidth; // 初始化数组并指定数组大小 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]; [spidersaddObject:spider]; } [self resetSpiders]; } ~~~ tempSpider是一个临时变量,我们仅用于取得图片宽度。我们没有retain他,也不需要release他——他会自动被release。 与此相反,spiders是由我们init的,我们也没有retain(实际上init会自动retain),但我们必须自己release(OC规定,init/copy/new出来的对象,必须手动release,OC的内存管理不会自动release)。因此在dealloc方法中有这么一句: ~~~ [spidersrelease],spiders=nil; ~~~ 同时,我们使用了coco2d提供的一个类似NSMutableArray的CCArray类,该类对数组的操作更快。以下是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; #pragma markAdding Objects - (void)addObject:(id)object; - (void)addObjectsFromArray:(CCArray*)otherArray;  - (void)addObjectsFromNSArray:(NSArray*)otherArray; - (void) insertObject:(id)objectatIndex:(NSUInteger)index; #pragma markRemoving 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; ~~~ resetSpiders 方法如下所示: ~~~ -(void)resetSpiders { CGSize screenSize = [[CCDirectorsharedDirector] winSize]; // 用一个临时的CCSprider取得图片宽度 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); [spiderstopAllActions]; } // 为保险起见,在注册之前先从schedule中反注册(未注册则不动作) [self unschedule:@selector(spidersUpdate:)]; // 注册schedule,每0.7秒执行 [self schedule:@selector(spidersUpdate:)interval:0.7f]; }   -(void) {   // 找出空闲的蜘蛛(未在移动的).   for (int i = 0; i < 10; i++) {     // 从数组中随机抽取一只蜘蛛     int randomSpiderIndex =CCRANDOM_0_1() * [spiders count];     CCSprite* spider = [spidersobjectAtIndex:randomSpiderIndex];     // 若蜘蛛未在移动,让蜘蛛往下掉     if ([spidernumberOfRunningActions] == 0) {       // 控制蜘蛛往下掉       [selfrunSpiderMoveSequence:spider];       // 每次循环仅移动一只蜘蛛       break;     }   } } -(void)runSpiderMoveSequence:(CCSprite*)spider {   // 随时间逐渐加快蜘蛛的速度   numSpidersMoved++;   if (numSpidersMoved % 8 == 0 &&spiderMoveDuration > 2.0f) {     spiderMoveDuration -= 0.1f;   } // 移动的终点 CGPoint belowScreenPosition= CGPointMake(spider.position.x, -[spidertexture].contentSize.height); // 动作:移动  CCMoveTo* move = [CCMoveToactionWithDuration:spiderMoveDuration position:belowScreenPosition]; // 瞬时动作:方法调用 CCCallFuncN*call = [CCCallFuncN actionWithTarget:self selector:@selector(spiderBelowScreen:)]; // 组合动作:移动+方法调用 CCSequence*sequence = [CCSequence actions:move, call, nil]; // 运行组合动作 [spiderrunAction:sequence]; } spiderBelowScreen方法重置蜘蛛的状态,让其回到屏幕上端等待下次坠落。 -(void)spiderBelowScreen:(id)sender { // 断言:sender是否为CCSprite.  NSAssert([sender isKindOfClass:[CCSpriteclass]], @"sender is not a CCSprite!"); CCSprite*spider = (CCSprite*)sender; // 把蜘蛛重新放回屏幕上端 CGPoint pos =spider.position; CGSizescreenSize = [[CCDirector sharedDirector] winSize]; pos.y =screenSize.height + [spider texture].contentSize.height; spider.position = pos; } ~~~ 书中作者提到,出于一个“保守”程序员的习惯,作者使用了 NSAssert语句来测试sender是否是一个CCSprite类。虽然理论上,Sender应当是一个CCSprite,实际上它却有可能根本不是。因为作者曾犯过一个错误:把CCCallFuncN写成了CCCallFunc(二者的区别在于,后者不能传递参数而前者带一个sender参数),导致sender未被作为参数传递到调用方法,即sender=nil。这样的错误也被NSAssert捕获到了,于是作者发现并修改了这个错误。 ## 七、碰撞检测 很简单。在update方法中添加语句: ~~~ [selfcheckForCollision]; ~~~ checkForCollision中包含了碰撞检测的所有逻辑: ~~~ -(void )checkForCollision { // 玩家和蜘蛛的尺寸 floatplayerImageSize = [player texture].contentSize.width; floatspiderImageSize = [[spiders lastObject] texture].contentSize.width; //玩家和蜘蛛的碰撞半径 floatplayerCollisionRadius = playerImageSize * 0.4f; floatspiderCollisionRadius = spiderImageSize * 0.4f; // 发生碰撞的最大距离,如果两个对象间的距离<=此距离可判定为有效碰撞 floatmaxCollisionDistance=playerCollisionRadius +spiderCollisionRadius; intnumSpiders = [spiders count]; //循环检测玩家和每一只蜘蛛间的碰撞距离 for (int i =0; i < numSpiders; i++) {   CCSprite* spider = [spidersobjectAtIndex:i];   // 计算每只蜘蛛和玩家间的距离. ccpDistance及其他非常有用的函数都列在CGPointExtension中   float actualDistance =  ccpDistance(player.position,spider.position);   // 如二者距离小于碰撞最大距离,认为发生碰撞?   if (actualDistance <maxCollisionDistance) {     // 结束游戏.     [self showGameOver];   } } } -(void)showGameOver { // 屏保开启 [self setScreenSaverEnabled:YES]; // 冻结所有对象的动作 CCNode* node; CCARRAY_FOREACH([self children], node){ [nodestopAllActions]; } // 使蜘蛛保持扭动 CCSprite* spider; CCARRAY_FOREACH(spiders, spider){ [selfrunSpiderWiggleSequence:spider]; } // 游戏开始前,关闭加速器的输入 self.isAccelerometerEnabled = NO; // 允许触摸 self.isTouchEnabled = YES; // 取消所有schedule [self unscheduleAllSelectors];   // 显示GameOver文本标签 CGSize screenSize = [[CCDirectorsharedDirector] winSize]; CCLabel* gameOver = [CCLabellabelWithString:@"GAME OVER!" fontName:@"Marker Felt"fontSize:60]; gameOver.position =CGPointMake(screenSize.width / 2, screenSize.height / 3); [self addChild:gameOver z:100 tag:100]; // 动作:色彩渐变 CCTintTo* tint1 = [CCTintToactionWithDuration:2 red:255 green:0 blue:0]; CCTintTo* tint2 = [CCTintToactionWithDuration:2 red:255 green:255 blue:0]; CCTintTo* tint3 = [CCTintToactionWithDuration:2 red:0 green:255 blue:0]; CCTintTo* tint4 = [CCTintToactionWithDuration:2 red:0 green:255 blue:255]; CCTintTo* tint5 = [CCTintToactionWithDuration:2 red:0 green:0 blue:255]; CCTintTo* tint6 = [CCTintToactionWithDuration:2 red:255 green:0 blue:255]; CCSequence* tintSequence = [CCSequenceactions:tint1, tint2, tint3, tint4, tint5, tint6, nil]; CCRepeatForever* repeatTint = [CCRepeatForeveractionWithAction:tintSequence]; [gameOver runAction:repeatTint]; // 动作:转动、颤动 CCRotateTo* rotate1 = [CCRotateToactionWithDuration:2 angle:3]; CCEaseBounceInOut* bounce1 = [CCEaseBounceInOutactionWithAction:rotate1]; CCRotateTo* rotate2 = [CCRotateTo actionWithDuration:2angle:-3]; CCEaseBounceInOut* bounce2 = [CCEaseBounceInOutactionWithAction:rotate2]; CCSequence* rotateSequence = [CCSequenceactions:bounce1, bounce2, nil]; CCRepeatForever* repeatBounce =[CCRepeatForever actionWithAction:rotateSequence]; [gameOver runAction:repeatBounce]; // 动作:跳动 CCJumpBy* jump = [CCJumpBy actionWithDuration:3position:CGPointZero height:screenSize.height / 3 jumps:1]; CCRepeatForever* repeatJump = [CCRepeatForeveractionWithAction:jump]; [gameOver runAction:repeatJump]; // 标签:点击游戏开始 CCLabel* touch = [CCLabellabelWithString:@"tap screen to play again"fontName:@"Arial" fontSize:20]; touch.position = CGPointMake(screenSize.width /2, screenSize.height / 4); [self addChild:touch z:100 tag:101]; // 动作:闪烁 CCBlink* blink = [CCBlink actionWithDuration:10blinks:20]; CCRepeatForever* repeatBlink = [CCRepeatForeveractionWithAction:blink]; [touch runAction:repeatBlink]; } ~~~ 当然,为了使游戏一开始就停顿在GameOver画面,需要在init方法中调用: ~~~ [selfshowGameOver]; ~~~ 只有当用户触摸屏幕后,游戏才会开始。这需要实现方法: ~~~ -(void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [self resetGame]; } resetGame方法负责重置游戏变量并启动游戏。 -(void)resetGame { // 关闭屏保 [self setScreenSaverEnabled:NO]; // 移除GameOver标签和启动游戏标签 [self removeChildByTag:100 cleanup:YES]; [self removeChildByTag:101 cleanup:YES]; // 启动加速器输入,关闭触摸输入 self.isAccelerometerEnabled = YES; self.isTouchEnabled = NO; // 重设蜘蛛数组 [self resetSpiders]; // 注册schedule [self scheduleUpdate]; // 积分 score = 0; totalTime = 0; [scoreLabel setString:@"0"]; } 开启/关闭屏保的方法: -(void)setScreenSaverEnabled:(bool)enabled { UIApplication *thisApp = [UIApplicationsharedApplication]; thisApp.idleTimerDisabled = !enabled; } 使蜘蛛不停扭动的方法如下(实际上是把图像不断的放大缩小): -(void)runSpiderWiggleSequence:(CCSprite*)spider { //动作:放大 CCScaleTo* scaleUp = [CCScaleToactionWithDuration:CCRANDOM_0_1() * 2 + 1 scale:1.05f]; //速度渐变动作:速度由慢至快,再由快至慢 CCEaseBackInOut* easeUp = [CCEaseBackInOutactionWithAction:scaleUp]; //动作:缩小 CCScaleTo* scaleDown = [CCScaleToactionWithDuration:CCRANDOM_0_1() * 2 + 1 scale:0.95f]; //速度渐变动作:速度由慢至快,再由快至慢 CCEaseBackInOut*easeDown = [CCEaseBackInOut actionWithAction:scaleDown]; CCSequence* scaleSequence = [CCSequenceactions:easeUp, easeDown, nil]; CCRepeatForever* repeatScale = [CCRepeatForeveractionWithAction:scaleSequence]; [spider runAction:repeatScale]; }   ~~~ ## 八、CCLabel、CCBitmapFontAtlas 和 Hiero 我们的计分标准很简单,以游戏时间作为游戏分数。 在init方法中加入: ~~~ scoreLabel =[CCLabel labelWithString:@"0" fontName:@"Arial"fontSize:48]; scoreLabel.position= CGPointMake(screenSize.width / 2, screenSize.height); // 调整锚点。 scoreLabel.anchorPoint= CGPointMake(0.5f, 1.0f); // 把label添加到scene,z坐标为-1,则位于所有layer的下方 [selfaddChild:scoreLabel z:-1]; ~~~ 为了将计分牌对其到屏幕上端中心位置,这里使用了“锚点”的概念。 锚点即参考点,和position属性配合使用,用于将物体向其他物体对齐。比如当把一个物体移动到一个位置点时,实际上是把这个物体的“锚点”移动/对齐到另外一个点。锚点由两个float表示,表示的是锚点相对于物体宽/高的比率。比如锚点(0.5f,1.0f)表示该锚点位于该物体宽1/2,高1/1的地方。 修改update方法,在其中加入: ~~~ // 每秒更新一次计分牌 totalTime +=delta;  int currentTime = (int)totalTime; if (score< currentTime) {   score = currentTime;   [scoreLabel setString:[NSStringstringWithFormat:@"%i", score]]; } ~~~ 这里需要说明的是,[CCLabelsetString]方法的效率很低:它需要释放老的texture,分配一个新的texture,并用iOS font的rendering方法重新构造texture。你只需要注释[CCLabel  setString]方法就可以知道,那有多么的糟糕。不使用setString方法时帧率为60帧/秒,而使用该方法的帧率竟然才30帧/秒。 象CCSprite等刷新效率高(只是更费一点内存)的Label类,都是属于CCBitmapFontAtlas类的特例。我们可以通过简单地把CCLabel变量声明从CCLabel更改为CCBitmapFontAtlas,并修改它的构造语句: ~~~ scoreLabel =[CCBitmapFontAtlas bitmapFontAtlasWithString:@"0"fntFile:@"bitmapfont.fnt"]; ~~~ 在游戏中使用bitmapfont是很好的选择,因为操作更快速,同时会有一个缺点:bitmap字体都是大小固定的。如果同样的字体,大小不同,你需要对CCBitmapFontAtlas对象进行缩放。或者为不同尺寸的字体创建单独的bitmap文件,并因此占用更多的内存。 当然需要把bitmapfont.fnt文件和对应的.png文件一起加入到工程的资源目录下。 如果你需要创建自己的bitmap字体,可以用Hiero这个小工具(javaweb application): [http://slick.cokeandcode.com/demos/hiero.jnlp](http://slick.cokeandcode.com/demos/hiero.jnlp) 也可以使用BMFont (windows应用):  [www.angelcode.com/products/bmfont/](http://www.angelcode.com/products/bmfont/) Hiero允许你从TrueTypeFont创建一个.fnt文件,该文件可以直接用于cocos2d的CCBitmapFontAtlas类。 安装Hiero时需要同意一个数字签名。请放心,迄今为止没有迹象表明该签名有任何问题。  Hiero的使用很简单,首先挑选一种TrueType字体,在SampleText 文本框中输入你要用的字符,然后点击File->Save BMFont Files…即可保存为.fnt文件。   ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-04_572a0b960a2f2.gif)   其他的选项是可选的。比如你可以加上渐变和阴影效果,使字体显得更3D。 选择Glyph cache后,你还可以调整生成的.png文件的大小。当然,如果你象我一样只用到了极少的几个字符,只要把页宽/高设为最小值(比如在这里我们设成了256),然后点击ResetCache应用。这样可以创建比较小.png文件同时减少内存占用。对于更复杂的字体,Hiero会创建多个.png文件——记住,每一个.png文件都应当加到工程中。 在这个例子里,我们的字体文件里只放了几个数字。因为png文件被创建为256*256大小,不管你是输入1个字还是再加几个其他的字,都会占用这么多的空间。 注意,如果你使用了在.fnt文件中不存在的字符,那么该字符会被忽略掉,且不会显示在CCBitmapFontAtlas中。 ## 九、加入音频 在工程目录中有一对音频文件: blues.mp3 和 alien-sfx.caf 。 在cocos2d中播放音频的最好也是最初的方法是用 SimpleAudioEngine。然而音频支持并不是cocos2d内置的一部分。它属于CocosDenshion,就像物理引擎一样。因此,你需要import额外的头文件: ~~~ #import "SimpleAudioEngine.h"   ~~~ 然后可以在init方法中象这样来播放音乐/音频: ~~~ [[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"blues.mp3"loop:YES]; [[SimpleAudioEngine sharedEngine] preloadEffect:@"alien-sfx.caf"]; ~~~ 对于背景音乐,我们设置loop参数为YES,这样就会循环播放。 对于音频声效,我们并没有立即播放,而仅仅是加载到内存。然后在条件合适时播放(比如碰撞发生时): ~~~ [[SimpleAudioEngine sharedEngine]playEffect:@"alien-sfx.caf"]; ~~~ 对于音乐,最好使用mp3格式。注意,同一时间内,只能播放1首背景音乐。虽然同时播放多首mp3从技术上是可行的,但物理硬件在同一时间内只能对一首mp3进行解码。在游戏中拒绝任何额外的CPU开销,因此对大部分游戏而言,都不会同时播放多首mp3. 至于声效,我喜欢用CAF格式。如果要进行音频格式的转换,可以使用 SoundConverter: http://dekorte.com/projects/shareware/SoundConverter/ 如果文件大小在500k以内,该软件是免费的,无限制的许可仅仅需要$15。 如果你发现无法播放音频文件或者出现杂音,不要担心。有无数音频软件和音频编码拥有它们特有的文件格式。有些格式无法在iOS设备上播放,然而在其他设备上播放正常。解决办法是打开它们,然后重新保存。或者使用音频转换程序或音频软件。 ## 十、迁移至iPad 如果所有的坐标都采用屏幕坐标,在iPad的大屏上运行游戏将会进行简单缩放而没有任何问题。相反,如果采用了固定坐标,你不得不重新编写游戏代码。   迁移至iPad工程很简单。在Groups&Files面板中选择Target,选择Project->Upgrade CurrentTarget for iPad…,将打开对话框: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-04_572a0b9621b00.gif)   对于这个游戏,选“One Universal application”(即iPhone/iPad通用)。 这样的缺点是两个设备的特性都会被加到target,增加了程序大小。但程序既可在iPhone上运行,也可在iPad上运行。 另一个选择是“Two device-specific application”,你会得到两个独立于设备的app,你需要提交两次。如果用户有两个设备——iPhone和iPad的,那么需要分别购买。   编译运行。程序会自动侦测当前所连接的设备类型并运行对应的版本。如图,选择iPad Simulator 3.2 ,可以查看在iPad模拟器运行游戏的效果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-04_572a0b96362ce.gif)  
';

Cocos2d开发系列(二)

最后更新于:2022-04-01 11:38:54

## 一、安装cocos2d文档集   为了便于今后检索Coco2d的各种类、方法、属性,我们最好将cocos2d的文档安装到Xcode中。作者Blog中曾有一篇译文《使用Doxgen创建Xcode文档集》描述了创建文档集的方法,但时至今日,Xcode已经发展到3.2.5,而Doxgen也升级到1.7.3了,原文中描述的方法也多少有些不适应了。且不知到什么原因,直接在Xcode中编译cocos2d文档集总是不成功,所以这里只能用DoxgenGUI生成cocos2d文档集。 1、安装Doxgen 当前版本为1.7.3,下载地址: [http://www.stack.nl/~dimitri/doxygen/download.html#latestsrc](http://www.stack.nl/~dimitri/doxygen/download.html#latestsrc) dmg格式,下载后把doxgen.app拖到“应用程序”目录(/Applications)即可。 2、运行Doxgen.app Doxgen GUI使用起来很简单,如下图所示: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-04_572a0b924a58f.gif)   Step1: Specifythe working directory… 需要指定一个Doxgen工作目录,随便指定一个即可,实在不行就新建个,Doxgen有时候会放一些临时文件在这里。 为简单起见,我们使用的是Wizard模式,这种方式比较简单。 Project name:填写项目的名称 Projectversion or id:版本号 Project logo:选择一个图标,会显示在文档集中。 Source codedirectory:指定源文件目录,cocos2d的源文件都放在安装目录的cocos2d目录中。当然也可以只指定cocos2d目录,然后勾上Scanrecursively,Doxgen会自己去搜索所有子目录。 Destinationdirectory:文档集生成后存放的路径,这里,我们直接指定为cocos2d目录。 好了,点击Run按钮,进入如下界面,然后点击Rundoxygen就大功告成了: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-04_572a0b9264eef.gif)   打开Xcode,点击Help——>DeveloperDocumentation,打开Document Browser,点击Home按钮,可以看到新多出来的Doc Set: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-04_572a0b9284e66.gif)   当然,你也可以打开文档集存放目录(/cocos2d-iphone-0.99.5/html/)目录进行浏览(因为我们生成的Doc Set是Html格式的): ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-04_572a0b929d81c.gif)   ## 二、理解Cocos2dApplication 新建一个Cocos2dApplication,将Classes目录下的所有源文件删除。因为我们不想使用Cocos2d模板自动生成的代码而准备自己从头实现,这样更方便我们理解Cocos2d应用程序的机制。同时学习在cocos2d动画中Action(动作)的概念和运用。 1、实现 一个CCLayer 新建File,命名为HelloAction,编辑.h和.m文件内容如下: ~~~ #import <Foundation/Foundation.h> #import "cocos2d.h" @interface HelloAction : CCLayer{ } @end #import "HelloAction.h" @implementation HelloAction -(id) init { if( (self=[superinit] )) { // 创建标签 CCLabelTTF* label = [CCLabelTTFlabelWithString:@"Hello Actions"fontName:@"Marker Felt"fontSize:64]; // 通过director得知窗口size CGSize size = [[CCDirectorsharedDirector] winSize]; // 让标签居中 label.position =  ccp( size.width /2 , size.height/2 ); // 向 CCLayer 中加入标签 [selfaddChild: label]; // 定义一个 CCAction:CCScaleBy 动作,3秒内,缩放2.5倍 id action = [CCScaleByactionWithDuration:3.0fscale:2.5f]; // 在标签上应用 CCAction [labelrunAction:action]; // 定义一个 CCSprite:并载入图像 icon.png,设置 CCSprite 的位置 CCSprite *sprite = [CCSpritespriteWithFile:@"Icon.png"]; sprite.position = ccp( 0, 50); // 把 CCSprite 加入到 Layer,并设置其 z 轴为1,z 轴越大则表明该对象越处于上层 // 由于 z 轴默认值为0,因此 CCSprite 将显示在标签的上层 [selfaddChild:sprite z:1]; // 定义 CCAction:CCRotateBy 动作,4秒内旋转720度 // "By" 意味着相对. "To" 意味着绝对. id rotateAction = [CCRotateByactionWithDuration:4angle:180*4]; // 定义 CCAction:CCJumpBy 动作,4秒内蹦4次,位置右下角,高度100. id jumpAction = [CCJumpByactionWithDuration:4position:ccp(size.width,0) height:100jumps:4]; // 定义 CCAtion: CCSpawn 动作,同时执行旋转动作和蹦跳动作 id fordward = [CCSpawnactions:rotateAction, jumpAction, nil]; // 基本上所有的 CCAction 都支持 reverse 方法 // 它将执行相反的动作 id backwards = [fordward reverse]; // 定义 CCAction:CCSequence 动作,它将顺序执行多个动作 id sequence = [CCSequenceactions: fordward, backwards, nil]; // 定义 CCAction:CCRepeat 动作,它可以重复执行指定动作任意次数. // 如果要不停地重复,可以定义 "RepeatForEver" 动作. id repeat = [CCRepeatactionWithAction:sequencetimes:2]; // 命令 CCSprite 执行指定动作,当该层显示在屏幕上时,动作即会自动执行 [spriterunAction:repeat]; } returnself; } - (void) dealloc { [superdealloc]; } @end ~~~ 2、实现应用程序委托 新建File,命名为AppDelegate,编辑.h和.m文件内容如下: ~~~ #import <Foundation/Foundation.h> #import "cocos2d.h" @interface AppDelegate : NSObject<UIAccelerometerDelegate, UIAlertViewDelegate, UITextFieldDelegate,UIApplicationDelegate> {     UIWindow *window; } @property (nonatomic, retain) UIWindow *window; @end #import "AppDelegate.h" #import "HelloAction.h" #import "EAGLView.h" @implementation AppDelegate @synthesize window; - (void) applicationDidFinishLaunching:(UIApplication*)application { //创建主窗口 window = [[UIWindowalloc] initWithFrame:[[UIScreenmainScreen] bounds]]; // 创建EAGLView实例。 EAGLView 派生亍类 UIView, 苹果公司用它来实现 OpenGL 输出支持,以支持OpenGL ES 编程: EAGLView *glView = [EAGLViewviewWithFrame:[windowbounds]    pixelFormat:kEAGLColorFormatRGB565    depthFormat:0 ]; // 并将EAGLView绑定到导演的openGLView属性 [[CCDirectorsharedDirector] setOpenGLView:glView]; // 设置横屏 [[CCDirectorsharedDirector]setDeviceOrientation:CCDeviceOrientationLandscapeLeft]; [windowaddSubview:glView]; // 显示主窗口 [windowmakeKeyAndVisible]; [CCTexture2DsetDefaultAlphaPixelFormat:kCCTexture2DPixelFormat_RGBA8888]; // 创建一个空的场景 CCScene *scene = [CCScenenode]; // 创建层:HelloAction CCLayer *layer = [HelloActionnode]; // 将层加到场景中 [scene addChild:layer]; // 第一个场景:主场景 [[CCDirectorsharedDirector] runWithScene: scene]; } - (void) dealloc { [windowrelease]; [superdealloc]; } ~~~ 3、修改main.h 找到文件中如下语句,并修改为: ~~~ int retVal = UIApplicationMain(argc, argv, nil, @"AppDelegate"); ~~~ 运行程序,可以看到当主场景载入后,“Hello Action”标签渐渐变大,而sprite在屏幕上来回蹦跳并旋转行进: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-04_572a0b92b688c.gif)          
';

Cocos2d开发系列(一)

最后更新于:2022-04-01 11:38:52

Cocos2d是一个比较流行的iphone游戏开发框架,据说在AppStore 上已有超过 100 个游戏是基亍 Cocos2D-iPhone。其中 3 个由此 迚入过 TOP 10 的排名。其中的 StickWars 更是曾排名第一。现在,让我们来开始Cocos2d的学习之旅吧! ## 一、安装下载 Cocos2d下载地址:[http://code.google.com/p/cocos2d-iphone/downloads/list](http://code.google.com/p/cocos2d-iphone/downloads/list) 当前最新版本为1.0。本教程选用的是稳定版本0.99.5。 下载文件为targ.gz格式,将其解压到某个目录下,打开该目录,找到install-templates.sh文件,这个就是Cocos2d的安装脚本。 你可以直接在终端里运行这个脚本进行安装,但如果你的Xcode没有安装在默认目录下,则你需要在命令中添加一个参数。 例如,你的Mac中可能同时存在一个Xcode版本(3.2.5),安装目录为:/Users/$yourname$/Developer,如果你想将Cocos2d安装到这个Xcode(3.2.5),则需要使用用命令: ~~~ ./install-templates.sh  /Users/$yourname$/Developer ~~~ 来进行安装。 安装完毕,你可以打开Xcode,新建Project,你会在UserTemplates目录下看到多了4个Cocos2d Application模板(如果是0.99版本,则只有3个模板): ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-04_572a0b8ff2882.gif)   ## 二、新建cocos2dApplications项目 新建一个cocos2dApplications项目,编译运行效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-04_572a0b9195d9b.gif)   打开HelloWorldScene.h,可以看到如下代码: ~~~ #import "cocos2d.h" // HelloWorld Layer @interface HelloWorld : CCLayer { } // returns a Scenethat contains the HelloWorld as the only child +(id) scene; @end ~~~ 首先,import指令导入了cocos2d.h,在这个.h文件中,已经包含(import)了cocos2d的所有类。因此我们导入了cocos2d.h后,就不需要再一个个导入要用到的cocos2d类了。 其次,HelloWorldScene.h中定义了一个CCLayer子类。CCLayer即所谓“层”的概念。层是一个容器的概念,cocos2d在层上放置多个动画元素,如文字、标签、图片、菜单,此外层接收触摸和加速器事件。 第三,定义了一个静态方法,返回了一个——Scene,这个Scene实际上包含了一个层——HelloWorld,这是一个不错的实例化方法。Coco2d中Scene的概念,类似于电影中“场景”的概念--由于受舞台空间或屏幕空间的限制,一个情节往往只能在固定的空间环境中展现,而电影往往是由许多个情节构成, 随着剧情的发展,场景也需要改变,新的场景需要产生。所以电影就会在一幕幕场景中进行切换 ,这种切换被称为“转场”。 切换到HelloWorldScene.m : ~~~ #import "HelloWorldScene.h" // HelloWorldimplementation @implementation HelloWorld ~~~ 上面导入了HelloWorldScene.h并实现了HelloWorld类。O-C中,文件名不见得需要和类名一致。 ~~~ +(id) scene { …… } ~~~ 这个方法就不介绍了,和前面说过的一样。 ~~~ -(id) init { if( (self=[superinit] )) { CCLabelTTF *label = [CCLabelTTFlabelWithString:@"HelloWorld"fontName:@"MarkerFelt"fontSize:64]; CGSize size = [[CCDirectorsharedDirector] winSize]; label.position =  ccp(size.width /2 , size.height/2 ); [selfaddChild: label]; } returnself; } ~~~ init方法中,创建了一个标签 CCLabelTTF (以前是CCLabel)并让他居中。ccp函数是CGPointMake函数的缩写。 接下来,看一下应用程序委托,如果你有过Cocoa编程的经验,那么会发现这就是一个UIApplicationDelegate,它的主要方法是applicationDidFinishLaunching:(UIApplication*)application :。 ,并将EAGLView绑定到 首先取得Director——导演。导演是cocos2d程序的统筹者和协调者,负责整个应用程序的主窗口的创建,以及每个场景在时间线上的先后顺序。 ~~~ CCDirector *director = [CCDirectorsharedDirector]; ~~~ 然后创建EAGLView实例。EAGLView 派生亍类 UIView, 苹果公司用它来实现 OpenGL 输出支持,以支持OpenGLES 编程: ~~~ EAGLView *glView = [EAGLView viewWithFrame:[windowbounds]    pixelFormat:kEAGLColorFormatRGB565    depthFormat:0 ]; ~~~ 并将EAGLView绑定到导演的openGLView属性: ~~~ [director setOpenGLView:glView]; ~~~ 设置横屏: ~~~ setDeviceOrientation:kCCDeviceOrientationLandscapeLeft]; ~~~ 设置帧间隔(尚未实现): ~~~ [director setAnimationInterval:1.0/60]; ~~~ 设置帧率显示,即屏幕左下角不断变化的数字: ~~~ [director setDisplayFPS:YES]; ~~~ 设置渲染格式位32位RGB: ~~~ [CCTexture2DsetDefaultAlphaPixelFormat:kCCTexture2DPixelFormat_RGBA8888]; ~~~ 移除闪屏,即一开始的那个coco2d Logo标志的屏幕: ~~~ [selfremoveStartupFlicker]; ~~~ 显示HelloWorld文字标签的场景: ~~~ [[CCDirectorsharedDirector] runWithScene: [HelloWorldscene]]; } ~~~
';

前言

最后更新于:2022-04-01 11:38:49

> 原文出处:[Cocos2d游戏开发](http://blog.csdn.net/column/details/cocos2d.html) 作者:[杨宏焱](http://blog.csdn.net/kmyhy) **本系列文章经作者授权在看云整理发布,未经作者允许,请勿转载!** # Cocos2d游戏开发 > Cocos2d是一个比较流行的iphone游戏开发框架,在AppStore上已有超过100个游戏是基于 Cocos2D-iPhone。其中 3 个由此 迚入过 TOP 10 的排名。其中的 StickWars 更是曾排名第一。 现在,让我们来开始Cocos2d的学习之旅吧
';