Cocos2d-IPhone-游戏开发秘籍-全-

Cocos2d IPhone 游戏开发秘籍(全)

原文:zh.annas-archive.org/md5/1de83ea1568e6f2b7845d85f314b5452

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Cocos2d for iPhone 是一个强大但易于使用的 iPhone 2D 游戏框架。它是快速的、灵活的、免费的,并且经过 App Store 认证。超过 2500 款 App Store 游戏已经使用它,包括许多畅销游戏。您想将您的 Cocos2d 游戏开发技能提升到下一个层次,并在 Cocos2d 游戏设计中变得更加专业吗?

Cocos2d for iPhone 1 游戏开发食谱将帮助您达到下一个层次。您在这里将找到超过 90 个食谱,解释从单个精灵的绘制到人工智能路径查找和高级网络的一切。强调完整的工作示例。

从第一章 图形 开始,您将了解游戏开发的每一个主要主题。您将在书中找到简单和复杂的食谱。

每个食谱要么是解决常见问题(播放视频文件、加速度计转向)的方法,要么是酷炫的高级技术(3D 渲染、纹理多边形)。

这本食谱将使您能够通过其广泛的工作示例代码快速创建专业质量的 iOS 游戏。

本书涵盖的内容

第一章, 图形 涵盖了广泛的主题。它首先从查看精灵的基本用途开始。然后提供了二维和三维基本绘图、视频播放、粒子效果、缓动动作、纹理填充多边形、调色板交换、光照、视差效果等示例。

第二章, 用户输入 提供了在 iOS 游戏开发中通常使用的不同风格输入的示例。这包括点击、长按、拖动、按钮、方向盘、模拟摇杆、加速度计、捏合缩放和手势。

第三章, 文件和数据 讨论了持久化数据的技术。这些包括 PLIST 文件、JSON 文件、XML 文件、NSUserDefaults、归档对象、SQLite 和 Core Data。

第四章, 物理 涵盖了 Box2D 物理引擎的大量用途。示例包括调试绘图、碰撞响应、不同形状、拖动、物理属性、冲量、力、异步体销毁、关节、车辆、角色移动、子弹、绳索,最后是创建俯视等距游戏引擎。

第五章, 场景和菜单 提供了用户界面实现的示例。它从涉及场景管理的示例开始,然后转向典型的 UI 元素,如标签、菜单、警告对话框和 UIKit 包装。然后它转向更高级的技术,如可拖动的菜单窗口、滚动菜单、滑动菜单、加载屏幕和缩略图。

第六章,音频,涵盖了广泛且难度各异的音频主题。这包括声音、音乐、音频属性、淡入淡出音频、游戏内示例、位置音频、音乐和对话的音量测量、录音、流媒体、播放 iPod 音乐、创建 MIDI 合成器、语音识别和文本到语音。

第七章,人工智能与逻辑,讨论了将智能 AI 角色添加到您的游戏中的技术。这包括处理航点、向移动目标发射弹丸、视线和利用 Boids 的群居行为。路径查找问题在四个独立的菜谱中解决:网格中的 A*路径查找、Box2D 世界、TMX 瓦片地图和侧滚动。最后,本章讨论了添加 Lua 脚本支持、动态加载脚本和使用 Lua 进行对话树。

第八章,技巧、工具和端口,提供了常用工具的示例用法,包括 Cocos2d-iPhone 测试平台、Zwoptex、Tiled、JSONWorldBuilder 和 CocosBuilder。它还讨论了使用 Cocos2d-X 将 Cocos2d 项目移植到 C++,以及使用 Cocos3d 开发 3D iOS 游戏。最后,它讨论了在 Apple App Store 上发布您的应用程序的过程。

您需要这本书什么

本书包含包含完全功能示例代码的项目。您需要以下内容来运行示例代码:

  • 需要运行基于 Intel 的 Macintosh 电脑运行 Snow Leopard(OSX 10.6 或更高版本)。

  • XCode(建议 4.0 或更高版本)。

  • 您必须注册为 iPhone 开发者才能在设备上测试示例项目。它们可以在 iPhone 模拟器中运行,无需上述注册。

这本书面向谁

如果您想将您的 Cocos2d 基础项目提升到下一个层次,那么这本书就是为您准备的。建议您对 Objective-C 和 Cocos2d 有一定的了解。具有一定编程经验的人也可能发现这本书很有用。

习惯用法

在本书中,您将找到许多不同风格的文本,以区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码词如下所示:"在本菜谱中,我们将介绍使用CCSprite、精灵表、CCSpriteFrameCacheCCSpriteBatchNode绘制精灵。"

代码块设置如下:

@implementation Ch1_DrawingSprites
-(CCLayer*) runRecipe {
/*** Draw a sprite using CCSprite ***/
CCSprite *tree1 = [CCSprite spriteWithFile:@"tree.png"];

任何命令行输入或输出都如下所示:

afconvert -f caff -d ima4 mysound.wav

新术语重要词汇以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,将以如下方式显示:"在组与文件下右键单击您的项目。"

注意

警告或重要提示将以这样的框显示。

小贴士

小技巧和技巧将以这样的形式出现。

读者反馈

我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正能从中获得最大收益的标题非常重要。

要发送一般反馈,请简单地将电子邮件发送到<feedback@packtpub.com>,并在邮件主题中提及书籍标题。

如果您需要一本书并且希望我们出版,请通过www.packtpub.com上的建议标题表单或通过电子邮件发送到<suggest@packtpub.com>给我们留言。

如果您在某个主题上具有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在您已经是 Packt 书籍的骄傲拥有者,我们有多个方面可以帮助您从您的购买中获得最大收益。

有关 Cocos2d 的更多信息或任何问题,您可以登录到www.Cocos2dCookbook.com

下载示例代码

您可以从www.PacktPub.com的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.PacktPub.com/support并注册,以便将文件直接通过电子邮件发送给您。

勘误

尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/support,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的现有勘误列表中,在“勘误”部分下。您可以通过从www.packtpub.com/support选择您的标题来查看任何现有勘误。

侵权

互联网上版权材料的侵权是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过<copyright@packtpub.com>与我们联系,并提供疑似侵权材料的链接。

我们感谢您在保护我们的作者和我们为您提供有价值的内容方面的帮助。

问题

如果您在本书的任何方面遇到问题,可以通过<questions@packtpub.com>联系我们,我们将尽力解决。

第一章。图形

在本章中,我们将涵盖以下主题:

  • 简介

  • 绘制精灵

  • 为精灵上色

  • 动画精灵

  • 绘制 OpenGL 原语

  • 播放视频文件

  • 网格、粒子以及运动条纹效果

  • 使用视网膜显示模式

  • 1D 和 2D 缓动动作

  • 渲染和纹理化 3D 立方体

  • 渲染填充纹理的多边形

  • 动画填充纹理的多边形

  • 使用图层交换调色板

  • 使用 CCTexture2DMutable 交换调色板

  • 使用 AWTextureFilter 进行模糊和字体阴影

  • 捕获和使用屏幕截图

  • 使用 CCParallaxNode

  • 使用 glColorMask 进行光照

简介

Cocos2d 首先是一个丰富的图形 API,它允许游戏开发者轻松访问广泛的功能。在本章中,我们将介绍 Cocos2d 的更多高级功能以及您如何使用这些功能来服务于各种不同的目的。我们还将解释尚未成为 Cocos2d 源代码一部分的高级技术。

为了本章节的目的,图形可以被认为是一个总称。我们还将介绍使用动作粒子的高级技术。

绘制精灵

在 2D 游戏开发中最基本的任务是绘制一个精灵。Cocos2d 为用户提供了在这个领域的大量灵活性。在本食谱中,我们将介绍使用 CCSprite、精灵表、CCSpriteFrameCacheCCSpriteBatchNode 绘制精灵,还将介绍米波映射。为了使内容有趣且引人入胜,本书中的许多食谱都将有一个独特的主题。在本食谱中,我们看到的是来自《通过镜子看》的艾丽斯场景。

绘制精灵

小贴士

您可以从您在 www.PacktPub.com 的账户中下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.PacktPub.com/support 并注册以将文件直接通过电子邮件发送给您。

准备工作

请参考项目 RecipeCollection01 以获取本食谱的完整工作代码。

如何实现...

执行以下代码:

@implementation Ch1_DrawingSprites
-(CCLayer*) runRecipe {
/*** Draw a sprite using CCSprite ***/
CCSprite *tree1 = [CCSprite spriteWithFile:@"tree.png"];
//Position the sprite using the tree base as a guide (y anchor point = 0)
[tree1 setPosition:ccp(20,20)];
tree1.anchorPoint = ccp(0.5f,0);
[tree1 setScale:1.5f];
[self addChild:tree1 z:2 tag:TAG_TREE_SPRITE_1];
/*** Load a set of spriteframes from a PLIST file and draw one by name ***/
//Get the sprite frame cache singleton
CCSpriteFrameCache *cache = [CCSpriteFrameCache sharedSpriteFrameCache];
//Load our scene sprites from a spritesheet
[cache addSpriteFramesWithFile:@"alice_scene_sheet.plist"];
//Specify the sprite frame and load it into a CCSprite
CCSprite *alice = [CCSprite spriteWithSpriteFrameName:@"alice.png"];
//Generate Mip Maps for the sprite
[alice.texture generateMipmap];
ccTexParams texParams = { GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE };
[alice.texture setTexParameters:&texParams];
//Set other information.
[alice setPosition:ccp(120,20)];
[alice setScale:0.4f];
alice.anchorPoint = ccp(0.5f,0);
//Add Alice with a zOrder of 2 so she appears in front of other sprites
[self addChild:alice z:2 tag:TAG_ALICE_SPRITE];
//Make Alice grow and shrink.
[alice runAction: [CCRepeatForever actionWithAction:
[CCSequence actions:[CCScaleTo actionWithDuration:4.0f scale:0.7f], [CCScaleTo actionWithDuration:4.0f scale:0.1f], nil] ] ];
/*** Draw a sprite CGImageRef ***/
UIImage *uiImage = [UIImage imageNamed: @"cheshire_cat.png"];
CGImageRef imageRef = [uiImage CGImage];
CCSprite *cat = [CCSprite spriteWithCGImage:imageRef key:@"cheshire_cat.png"];
[cat setPosition:ccp(250,180)];
[cat setScale:0.4f];
[self addChild:cat z:3 tag:TAG_CAT_SPRITE];
/*** Draw a sprite using CCTexture2D ***/
CCTexture2D *texture = [[CCTextureCache sharedTextureCache] addImage:@"tree.png"];
CCSprite *tree2 = [CCSprite spriteWithTexture:texture];
[tree2 setPosition:ccp(300,20)];
tree2.anchorPoint = ccp(0.5f,0);
[tree2 setScale:2.0f];
[self addChild:tree2 z:2 tag:TAG_TREE_SPRITE_2];
/*** Draw a sprite using CCSpriteFrameCache and CCTexture2D ***/
CCSpriteFrame *frame = [CCSpriteFrame frameWithTexture:texture rect:tree2.textureRect];
[[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFrame:frame name:@"tree.png"];
CCSprite *tree3 = [CCSprite spriteWithSpriteFrame:[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"tree.png"]];
[tree3 setPosition:ccp(400,20)];
tree3.anchorPoint = ccp(0.5f,0);
[tree3 setScale:1.25f];
[self addChild:tree3 z:2 tag:TAG_TREE_SPRITE_3];
/*** Draw sprites using CCBatchSpriteNode ***/
//Clouds
CCSpriteBatchNode *cloudBatch = [CCSpriteBatchNode batchNodeWithFile:@"cloud_01.png" capacity:10];
[self addChild:cloudBatch z:1 tag:TAG_CLOUD_BATCH];
for(int x=0; x<10; x++){
CCSprite *s = [CCSprite spriteWithBatchNode:cloudBatch rect:CGRectMake(0,0,64,64)];
[s setOpacity:100];
[cloudBatch addChild:s];
[s setPosition:ccp(arc4random()%500-50, arc4random()%150+200)];
}
//Middleground Grass
int capacity = 10;
CCSpriteBatchNode *grassBatch1 = [CCSpriteBatchNode batchNodeWithFile:@"grass_01.png" capacity:capacity];
[self addChild:grassBatch1 z:1 tag:TAG_GRASS_BATCH_1];
for(int x=0; x<capacity; x++){
CCSprite *s = [CCSprite spriteWithBatchNode:grassBatch1 rect:CGRectMake(0,0,64,64)];
[s setOpacity:255];
[grassBatch1 addChild:s];
[s setPosition:ccp(arc4random()%500-50, arc4random()%20+70)];
}
//Foreground Grass
CCSpriteBatchNode *grassBatch2 = [CCSpriteBatchNode batchNodeWithFile:@"grass_01.png" capacity:10];
[self addChild:grassBatch2 z:3 tag:TAG_GRASS_BATCH_2];
for(int x=0; x<30; x++){
CCSprite *s = [CCSprite spriteWithBatchNode:grassBatch2 rect:CGRectMake(0,0,64,64)];
[s setOpacity:255];
[grassBatch2 addChild:s];
[s setPosition:ccp(arc4random()%500-50, arc4random()%40-10)];
}
/*** Draw colored rectangles using a 1px x 1px white texture ***/
//Draw the sky using blank.png
[self drawColoredSpriteAt:ccp(240,190) withRect:CGRectMake(0,0,480,260) withColor:ccc3(150,200,200) withZ:0];
//Draw the ground using blank.png
[self drawColoredSpriteAt:ccp(240,30) withRect:CGRectMake(0,0,480,60) withColor:ccc3(80,50,25) withZ:0];
return self;
}
-(void) drawColoredSpriteAt:(CGPoint)position withRect:(CGRect)rect withColor:(ccColor3B)color withZ:(float)z {
CCSprite *sprite = [CCSprite spriteWithFile:@"blank.png"];
[sprite setPosition:position];
[sprite setTextureRect:rect];
[sprite setColor:color];
[self addChild:sprite];
//Set Z Order
[self reorderChild:sprite z:z];
}
@end

它是如何工作的...

本食谱将带我们了解绘制精灵的多数常见方法:

  • 从文件创建 CCSprite:

    首先,我们有绘制精灵最简单的方法。这涉及到使用 CCSprite 类的方法,如下所示:

    +(id)spriteWithFile:(NSString*)filename;
    
    

    这是初始化精灵最直接的方法,对于许多情况来说都是足够的。

  • 从文件加载精灵的其他方法:

    之后,我们将看到使用 UIImage/CGImageRefCCTexture2D 和使用 CCTexture2D 对象实例化的 CCSpriteFrame 创建 CCSprite 的示例。CGImageRef 的支持允许您将 Cocos2d 与其他框架和工具集结合。CCTexture2D 是纹理创建的底层机制。

  • 使用 CCSpriteFrameCache 加载精灵表:

    接下来,我们将看到使用精灵最强大的方式,即 CCSpriteFrameCache 类。自 Cocos2d-iPhone v0.99 版本引入,CCSpriteFrameCache 单例是一个所有精灵帧的缓存。使用 spritesheet 及其关联的 PLIST 文件(使用 Zwoptex 创建,稍后会更详细地介绍)我们可以将多个精灵加载到缓存中。从这里,我们可以使用缓存中的精灵创建 CCSprite 对象:

    +(id)spriteWithSpriteFrameName:(NSString*)filename;
    
    
  • Mipmapping:

    Mipmapping 允许你在不产生精灵锯齿的情况下缩放纹理或放大缩小场景。当我们把爱丽丝缩小到很小的尺寸时,锯齿现象不可避免地会出现。开启 Mipmapping 后,Cocos2d 会动态生成低分辨率的纹理,以平滑出较小尺度下的任何像素化。请取消以下行的注释:

    [alice.texture generateMipmap];
    ccTexParams texParams = { GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE };
    [alice.texture setTexParameters:&texParams];
    
    

    现在,你应该能看到爱丽丝变小时像素化的效果。

  • 使用 CCSpriteBatchNode 绘制许多派生精灵:

    CCSpriteBatchNode 类,自 v0.99.5 版本开始添加,提供了一种高效的方式来重复绘制相同的精灵。创建批处理节点的方法如下:

    CCSpriteBatchNode *cloudBatch = [CCSpriteBatchNode batchNodeWithFile:@"cloud_01.png" capacity:10];
    
    

    然后,你可以使用以下代码创建任意数量的精灵:

    CCSprite *s = [CCSprite spriteWithBatchNode:cloudBatch rect:CGRectMake(0,0,64,64)];
    [cloudBatch addChild:s];
    
    

    将容量设置为计划绘制的精灵数量,告诉 Cocos2d 分配这么多空间。这是另一种提高效率的调整,尽管你并不绝对需要这样做。在这三个例子中,我们绘制了 10 个随机放置的云朵和 60 个随机放置的草丛。

  • 绘制彩色矩形:

    最后,我们有一个相当简单但用途多样的技术。通过绘制一个空白 1px x 1px 白色纹理的精灵,然后着色并设置其 textureRect 属性,我们可以创建非常有用的彩色条:

    CCSprite *sprite = [CCSprite spriteWithFile:@"blank.png"];
    [sprite setTextureRect:CGRectMake(0,0,480,320)];
    [sprite setColor:ccc3(255,128,0)];
    
    

    在这个例子中,我们使用了这种技术来创建非常简单的地面和天空背景。

着色精灵

在上一个配方中,我们使用彩色矩形来绘制地面和天空。设置纹理颜色和透明度的能力是简单的工具,如果使用得当,可以创建非常酷的效果。在这个配方中,我们将创建一个电影场景,其中两个武士面对面,手持发光的剑。

着色精灵

准备工作

请参考项目 RecipeCollection01 以获取此配方的完整工作代码。另外,请注意,为了简洁,一些代码已被省略。

如何做到...

执行以下代码:

#import "CCGradientLayer.h
@implementation Ch1_ColoringSprites
-(CCLayer*) runRecipe {
[self initButtons];
//The Fade Scene Sprite
CCSprite *fadeSprite = [CCSprite spriteWithFile:@"blank.png"];
[fadeSprite setOpacity:0];
[fadeSprite setPosition:ccp(240,160)];
[fadeSprite setTextureRect:CGRectMake(0,0,480,320)];
[self addChild:fadeSprite z:3 tag:TAG_FADE_SPRITE];
//Add a gradient below the mountains
//CCGradientDirectionT_B is an enum provided by CCGradientLayer
CCGradientLayer *gradientLayer = [CCGradientLayer layerWithColor: ccc4(61,33,62,255) toColor:ccc4(65,89,54,255) withDirection:CCGradientDirectionT_B width:480 height:100];
[gradientLayer setPosition:ccp(0,50)];
[self addChild:gradientLayer z:0 tag:TAG_GROUND_GRADIENT];
//Add a sinister red glow gradient behind the evil samurai
CCGradientLayer *redGradient = [CCGradientLayer layerWithColor:ccc4(0,0,0,0) toColor:ccc4(255,0,0,100) withDirection:CCGradientDirectionT_B width:200 height:200];
[redGradient setPosition:ccp(280,60)];
[redGradient setRotation:-90];
[self addChild:redGradient z:2 tag:TAG_RED_GRADIENT];
// Make the swords glow
[self glowAt:ccp(230,280) withScale:CGSizeMake(3.0f, 11.0f) withColor:ccc3(0,230,255) withRotation:45.0f withSprite:goodSamurai];
[self glowAt:ccp(70,280) withScale:CGSizeMake(3.0f, 11.0f) withColor:ccc3(255,200,2) withRotation:-45.0f withSprite:evilSamurai];
return self;
}
-(void) initButtons {
[CCMenuItemFont setFontSize:16];
//'Fade To Black' button
CCMenuItemFont* fadeToBlack = [CCMenuItemFont itemFromString:@"FADE TO BLACK" target:self selector:@selector(fadeToBlackCallback:)];
CCMenu *fadeToBlackMenu = [CCMenu menuWithItems:fadeToBlack, nil];
fadeToBlackMenu.position = ccp( 180 , 20 );
[self addChild:fadeToBlackMenu z:4 tag:TAG_FADE_TO_BLACK];
}
/* Fade the scene to black */
-(void) fadeToBlackCallback:(id)sender {
CCSprite *fadeSprite = [self getChildByTag:TAG_FADE_SPRITE];
[fadeSprite stopAllActions];
[fadeSprite setColor:ccc3(0,0,0)];
[fadeSprite setOpacity:0.0f];
[fadeSprite runAction:
[CCSequence actions:[CCFadeIn actionWithDuration:2.0f], [CCFadeOut actionWithDuration:2.0f], nil] ];
}
/* Create a glow effect */
-(void) glowAt:(CGPoint)position withScale:(CGSize)size withColor:(ccColor3B)color withRotation:(float)rotation withSprite:(CCSprite*)sprite {
CCSprite *glowSprite = [CCSprite spriteWithFile:@"fire.png"];
[glowSprite setColor:color];
[glowSprite setPosition:position];
[glowSprite setRotation:rotation];
[glowSprite setBlendFunc: (ccBlendFunc) { GL_ONE, GL_ONE }];
[glowSprite runAction: [CCRepeatForever actionWithAction:
[CCSequence actions:[CCScaleTo actionWithDuration:0.9f scaleX:size.width scaleY:size.height], [CCScaleTo actionWithDuration:0.9f scaleX:size.width*0.75f scaleY:size.height*0.75f], nil] ] ];
[glowSprite runAction: [CCRepeatForever actionWithAction:
[CCSequence actions:[CCFadeTo actionWithDuration:0.9f opacity:150], [CCFadeTo actionWithDuration:0.9f opacity:255], nil] ] ];
[sprite addChild:glowSprite];
}
@end

它是如何工作的...

这个配方展示了多种基于颜色的技术。

  • 设置精灵颜色:

    最简单的颜色使用方法涉及使用以下方法设置精灵的颜色:

    -(void) setColor:(ccColor3B)color;
    
    

    设置精灵颜色有效地减少了你可以显示的颜色,但它允许在绘制中具有一定的程序灵活性。在这个配方中,我们使用 setColor 来做很多事情,包括绘制蓝色天空、黄色太阳、黑色“戏剧性电影条”等等。

    ccColor3B 是一个包含三个 GLubyte 变量的 C 结构体。使用以下辅助宏来创建 ccColor3B 结构体:

    ccColor3B ccc3(const GLubyte r, const GLubyte g, const GLubyte b);
    
    

    Cocos2d 还指定了一些预定义的颜色作为常量。这些包括以下内容:

    ccWHITE, ccYELLOW, ccBLUE, ccGREEN, ccRED,
    ccMAGENTA, ccBLACK, ccORANGE, ccGRAY
    
    
  • 渐变到颜色:

    要将场景渐变到特定颜色,我们使用在上一个菜谱中提到的 blank.png 技术。我们首先绘制一个与屏幕大小相同的精灵,然后将精灵着色为我们想要渐变到的颜色,最后在精灵上运行一个 CCFadeIn 动作以渐变到该颜色:

    [fadeSprite setColor:ccc3(255,255,255)];
    [fadeSprite setOpacity:0.0f];
    [fadeSprite runAction: [CCFadeIn actionWithDuration:2.0f] ];
    
    
  • 使用 CCGradientLayer:

    使用 CCGradientLayer 类,我们可以通过编程创建渐变。为了使背景中的山逐渐淡入到两个武士站立的地面上,我们使用这种方法创建了一个渐变:

    CCGradientLayer *gradientLayer = [CCGradientLayer layerWithColor:ccc4(61,33,62,255) toColor:ccc4(65,89,54,255) withDirection:CCGradientDirectionT_B width:480 height:100];
    [gradientLayer setPosition:ccp(0,50)];
    [self addChild:gradientLayer z:0 tag:TAG_GROUND_GRADIENT];
    
    

    由于CCGradientLayer允许你控制不透明度和颜色,它有很多用途。正如你所见,在邪恶武士的背后还有一个邪恶的红色光芒。

  • 制作发光的精灵:

    为了让演示中的剑发光,我们使用微妙的颜色调整、加法混合和渐变缩放动作。首先,我们加载 Cocos2d 提供的fire.png精灵。通过独立改变其 X 和 Y 缩放比例,我们可以使它变细或变粗。一旦你得到了所需的缩放比例(在这个演示中我们使用 x:y 3:11,因为剑非常细),你可以不断缩放和淡入淡出精灵,以给效果增添一些活力。你还需要将混合函数设置为{ GL_ONE, GL_ONE }以实现加法混合。最后,将这个效果精灵添加到实际精灵上,使其看起来像在发光。

    CCSprite *glowSprite = [CCSprite spriteWithFile:@"fire.png"];
    [glowSprite setColor:color];
    [glowSprite setPosition:position];
    [glowSprite setRotation:rotation];
    [glowSprite setBlendFunc: (ccBlendFunc) { GL_ONE, GL_ONE }];
    [glowSprite runAction: [CCRepeatForever actionWithAction:
    [CCSequence actions:[CCScaleTo actionWithDuration:0.9f scaleX:size.width scaleY:size.height], [CCScaleTo actionWithDuration:0.9f scaleX:size.width*0.75f scaleY:size.height*0.75f], nil] ] ];
    [glowSprite runAction: [CCRepeatForever actionWithAction:
    [CCSequence actions:[CCFadeTo actionWithDuration:0.9f opacity:150], [CCFadeTo actionWithDuration:0.9f opacity:255], nil] ] ];
    [sprite addChild:glowSprite];
    
    

动画精灵

现在是时候给我们的精灵添加一些动画了。关于动画,应该强调的是,它的复杂程度取决于你如何实现。在这个菜谱中,我们将使用非常简单的动画来创建一个引人入胜的效果。我们将创建一个场景,其中蝙蝠在看起来令人毛骨悚然的城堡周围飞翔。我还添加了一个基于之前菜谱中制作剑发光的技术的基础上的酷炫闪电效果。

动画精灵

准备工作

请参考项目RecipeCollection01以获取此菜谱的完整工作代码。另外,请注意,为了简洁,一些代码已被省略。

如何实现...

执行以下代码:

//SimpleAnimObject.h
@interface SimpleAnimObject : CCSprite {
int animationType;
CGPoint velocity;
}
@interface Ch1_AnimatingSprites {
NSMutableArray *bats;
CCAnimation *batFlyUp;
CCAnimation *batGlideDown;
CCSprite *lightningBolt;
CCSprite *lightningGlow;
int lightningRemoveCount;
}
-(CCLayer*) runRecipe {
//Add our PLIST to the SpriteFrameCache
[[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@"simple_bat.plist"];
//Add a lightning bolt
lightningBolt = [CCSprite spriteWithFile:@"lightning_bolt.png"];
[lightningBolt setPosition:ccp(240,160)];
[lightningBolt setOpacity:64];
[lightningBolt retain];
//Add a sprite to make it light up other areas.
lightningGlow = [CCSprite spriteWithFile:@"lightning_glow.png"];
[lightningGlow setColor:ccc3(255,255,0)];
[lightningGlow setPosition:ccp(240,160)];
[lightningGlow setOpacity:100];
[lightningGlow setBlendFunc: (ccBlendFunc) { GL_ONE, GL_ONE }];
[lightningBolt addChild:lightningGlow];
//Set a counter for lightning duration randomization
lightningRemoveCount = 0;
//Bats Array Initialization
bats = [[NSMutableArray alloc] init];
//Add bats using a batch node.
CCSpriteBatchNode *batch1 = [CCSpriteBatchNode batchNodeWithFile:@"simple_bat.png" capacity:10];
[self addChild:batch1 z:2 tag:TAG_BATS];
//Make them start flying up.
for(int x=0; x<10; x++){
//Create SimpleAnimObject of bat
SimpleAnimObject *bat = [SimpleAnimObject spriteWithBatchNode:batch1 rect:CGRectMake(0,0,48,48)];
[batch1 addChild:bat];
[bat setPosition:ccp(arc4random()%400+40, arc4random()%150+150)];
//Make the bat fly up. Get the animation delay (flappingSpeed).
float flappingSpeed = [self makeBatFlyUp:bat];
//Base y velocity on flappingSpeed.
bat.velocity = ccp((arc4random()%1000)/500 + 0.2f, 0.1f/flappingSpeed);
//Add a pointer to this bat object to the NSMutableArray
[bats addObject:[NSValue valueWithPointer:bat]];
[bat retain];
//Set the bat's direction based on x velocity.
if(bat.velocity.x > 0){
bat.flipX = YES;
}
}
//Schedule physics updates
[self schedule:@selector(step:)];
return self;
}
-(float)makeBatFlyUp:(SimpleAnimObject*)bat {
CCSpriteFrameCache * cache = [CCSpriteFrameCache sharedSpriteFrameCache];
//Randomize animation speed.
float delay = (float)(arc4random()%5+5)/80;
CCAnimation *animation = [[CCAnimation alloc] initWithName:@"simply_bat_fly" delay:delay];
//Randomize animation frame order.
int num = arc4random()%4+1;
for(int i=1; i<=4; i+=1){
[animation addFrame:[cache spriteFrameByName:[NSString stringWithFormat:@"simple_bat_0%i.png",num]]];
num++;
if(num > 4){ num = 1; }
}
//Stop any running animations and apply this one.
[bat stopAllActions];
[bat runAction:[CCRepeatForever actionWithAction: [CCAnimate actionWithAnimation:animation]]];
//Keep track of which animation is running.
bat.animationType = BAT_FLYING_UP;
return delay; //We return how fast the bat is flapping.
}
-(void)makeBatGlideDown:(SimpleAnimObject*)bat {
CCSpriteFrameCache * cache = [CCSpriteFrameCache sharedSpriteFrameCache];
//Apply a simple single frame gliding animation.
CCAnimation *animation = [[CCAnimation alloc] initWithName:@"simple_bat_glide" delay:100.0f];
[animation addFrame:[cache spriteFrameByName:@"simple_bat_01.png"]];
//Stop any running animations and apply this one.
[bat stopAllActions];
[bat runAction:[CCRepeatForever actionWithAction: [CCAnimate actionWithAnimation:animation]]];
//Keep track of which animation is running.
bat.animationType = BAT_GLIDING_DOWN;
}
-(void)step:(ccTime)delta {
CGSize s = [[CCDirector sharedDirector] winSize];
for(id key in bats){
//Get SimpleAnimObject out of NSArray of NSValue objects.
SimpleAnimObject *bat = [key pointerValue];
//Make sure bats don't fly off the screen
if(bat.position.x > s.width){
bat.velocity = ccp(-bat.velocity.x, bat.velocity.y);
bat.flipX = NO;
}else if(bat.position.x < 0){
bat.velocity = ccp(-bat.velocity.x, bat.velocity.y);
bat.flipX = YES;
}else if(bat.position.y > s.height){
bat.velocity = ccp(bat.velocity.x, -bat.velocity.y);
[self makeBatGlideDown:bat];
}else if(bat.position.y < 0){
bat.velocity = ccp(bat.velocity.x, -bat.velocity.y);
[self makeBatFlyUp:bat];
}
//Randomly make them fly back up
if(arc4random()%100 == 7){
if(bat.animationType == BAT_GLIDING_DOWN){ [self makeBatFlyUp:bat]; bat.velocity = ccp(bat.velocity.x, -bat.velocity.y); }
else if(bat.animationType == BAT_FLYING_UP){ [self makeBatGlideDown:bat]; bat.velocity = ccp(bat.velocity.x, -bat.velocity.y); }
}
//Update bat position based on direction
bat.position = ccp(bat.position.x + bat.velocity.x, bat.position.y + bat.velocity.y);
}
//Randomly make lightning strike
if(arc4random()%70 == 7){
if(lightningRemoveCount < 0){
[self addChild:lightningBolt z:1 tag:TAG_LIGHTNING_BOLT];
lightningRemoveCount = arc4random()%5+5;
}
}
//Count down
lightningRemoveCount -= 1;
//Clean up any old lightning bolts
if(lightningRemoveCount == 0){
[self removeChildByTag:TAG_LIGHTNING_BOLT cleanup:NO];
}
}
@end

工作原理...

这个菜谱展示了如何通过使用SimpleAnimObject来结构动画类:

  • 动画对象类结构:

    在切换动画时,通常需要跟踪动画对象的状态。在我们的例子中,我们使用SimpleAnimObject,它保持一个任意的animationType变量。我们还维护一个速度变量,它具有与动画帧延迟成反比的 Y 标量值:

    @interface SimpleAnimObject : CCSprite {
    int animationType;
    CGPoint velocity;
    }
    
    

    根据你想要动画系统有多深入,你应该维护更多信息,例如,例如,指向正在运行的CCAnimation实例的指针、帧信息以及物理体。

还有更多...

随着你对 Cocos2d 游戏开发的参与越来越深入,你将越来越倾向于使用 异步操作 来实现游戏逻辑和人工智能。这些操作由 CCAction 类派生,可用于从使用 CCMoveBy 移动 CCNode 到使用 CCAnimate 动画 CCSprite 的各种操作。当执行操作时,后台会维护一个异步计时机制。第一次编写游戏程序的开发者往往过度依赖这个特性。当运行多个操作时,这种技术所需的额外开销会迅速增加。在下面的示例中,我们使用了一个简单的整数计时器,允许我们调节屏幕上闪电持续的时间:

//Randomly make lightning strike
if(arc4random()%70 == 7){
if(lightningRemoveCount < 0){
[self addChild:lightningBolt z:1 tag:TAG_LIGHTNING_BOLT];
lightningRemoveCount = arc4random()%5+5;
}
}
//Count down
lightningRemoveCount -= 1;
//Clean up any old lightning bolts
if(lightningRemoveCount == 0){
[self removeChildByTag:TAG_LIGHTNING_BOLT cleanup:NO];
}

同步计时器,如前面代码片段中所示,通常但并非总是比异步操作更可取。随着你的游戏规模和范围的增长,请记住这一点。

绘制 OpenGL 原始图形

有时在 2D 游戏开发中,我们需要使用传统的 OpenGL 原始图形。通过这些,我们可以制作迷你地图、抬头显示和像子弹追踪和闪电爆炸这样的特殊效果等。在下面的场景中,我使用 Cocos2d 提供的所有原始绘图函数以及我调整并添加的一个函数创建了一个简单的图形。

绘制 OpenGL 原始图形

准备工作

请参考项目 RecipeCollection01 以获取此菜谱的完整工作代码。

如何实现...

执行以下代码:

/* Create a solid circle */
void ccDrawSolidCircle( CGPoint center, float r, float a, NSUInteger segs, BOOL drawLineToCenter)
{
//Check to see if we need to draw a line to the center
int additionalSegment = 1;
if (drawLineToCenter)
additionalSegment++;
const float coef = 2.0f * (float)M_PI/segs;
GLfloat *vertices = calloc( sizeof(GLfloat)*2*(segs+2), 1);
if( ! vertices )
return;
//Calculate line segments
for(NSUInteger i=0;i<=segs;i++)
{
float rads = i*coef;
GLfloat j = r * cosf(rads + a) + center.x;
GLfloat k = r * sinf(rads + a) + center.y;
vertices[i*2] = j * CC_CONTENT_SCALE_FACTOR();
vertices[i*2+1] =k * CC_CONTENT_SCALE_FACTOR();
}
vertices[(segs+1)*2] = center.x * CC_CONTENT_SCALE_FACTOR();
vertices[(segs+1)*2+1] = center.y * CC_CONTENT_SCALE_FACTOR();
//Draw our solid polygon
glDisable(GL_TEXTURE_2D);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
glDisableClientState(GL_COLOR_ARRAY);
glVertexPointer(2, GL_FLOAT, 0, vertices);
glDrawArrays(GL_TRIANGLE_FAN, 0, segs+additionalSegment);
glEnableClientState(GL_COLOR_ARRAY);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glEnable(GL_TEXTURE_2D);
//Free up memory
free( vertices );
}
@implementation ShapeLayer
-(void) draw {
//Set line width.
glLineWidth(4.0f);
//Set point size
glPointSize(16);
//Enable line smoothing
glEnable(GL_LINE_SMOOTH);
//Draw a blue quadratic bezier curve
glColor4ub(0, 0, 255, 255);
ccDrawQuadBezier(ccp(100,0), ccp(240,70), ccp(380,0), 10);
//Draw a hollow purple circle
glColor4ub(255, 0, 255, 255);
ccDrawCircle(ccp(240,160), 125.0f, 0.0f, 100, NO);
//Draw a solid red lines
glColor4ub(255, 0, 0, 255);
ccDrawLine(ccp(170,220), ccp(220,190));
ccDrawLine(ccp(260,190), ccp(310,220));
//Draw a green point
glColor4ub(0, 255, 0, 255);
ccDrawPoint(ccp(200,180));
ccDrawPoint(ccp(280,180));
//Draw a turquoise solid circle
glColor4ub(0, 128, 255, 50);
ccDrawSolidCircle(ccp(200,180), 25.0f, 0.0f, 20, NO);
ccDrawSolidCircle(ccp(280,180), 25.0f, 0.0f, 20, NO);
//Draw a brown hollow circle
glColor4ub(64,32, 0, 255);
ccDrawCircle(ccp(200,180), 25.0f, 0.0f, 100, NO);
ccDrawCircle(ccp(280,180), 25.0f, 0.0f, 100, NO);
//Draw brown lines
glColor4ub(64,32, 0, 255);
ccDrawLine(ccp(225,180), ccp(255,180));
ccDrawLine(ccp(305,180), ccp(370,160));
ccDrawLine(ccp(175,180), ccp(110,160));
//Draw an orange polygon
glColor4ub(255, 128, 0, 255);
CGPoint vertices[5]={ ccp(230,150),ccp(240,160),ccp(250,150),ccp(245,140),ccp(235,140) };
ccDrawPoly(vertices, 5, YES);
//Draw a yellow cubic bezier curve
glColor4ub(255, 255, 0, 255);
ccDrawCubicBezier(ccp(170,90), ccp(220,150), ccp(260,50), ccp(320,100), 10);
//Restore original values
glLineWidth(1);
glDisable(GL_LINE_SMOOTH);
glColor4ub(255,255,255,255);
glPointSize(1);
}
@end
-(CCLayer*) runRecipe {
ShapeLayer *layer = [[ShapeLayer alloc] init];
[layer setPosition:ccp(0,0)];
[self addChild:layer z:2 tag:0];
return self;
}

它是如何工作的...

此菜谱展示了如何使用每个原始绘图函数:

  • 覆盖 draw 方法:

    为了使用 OpenGL 绘图例程,我们必须覆盖 CCNode 的以下方法:

    -(void) draw;
    
    

    CCNode.h 中所述,覆盖此方法使我们能够控制底层的 OpenGL 绘图例程。以下 OpenGL 语句是隐含的:

    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_COLOR_ARRAY);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
    glEnable(GL_TEXTURE_2D);
    
    

    为了覆盖此方法,我们创建了一个名为 ShapeLayer 的类,它继承自 CCLayer,因此也继承自 CCNode。一旦附加到场景,这个覆盖的绘图方法将每周期调用一次。

  • 原始绘图函数:

    Cocos2d 中提供了以下原始绘图函数:

    void ccDrawPoint( CGPoint point );
    void ccDrawPoints( const CGPoint *points, NSUInteger numberOfPoints );
    void ccDrawLine( CGPoint origin, CGPoint destination );
    void ccDrawPoly( const CGPoint *vertices, NSUInteger numOfVertices, BOOL closePolygon );
    void ccDrawCircle( CGPoint center, float radius, float angle, NSUInteger segments, BOOL drawLineToCenter);
    void ccDrawQuadBezier(CGPoint origin, CGPoint control, CGPoint destination, NSUInteger segments);
    void ccDrawCubicBezier(CGPoint origin, CGPoint control1, CGPoint control2, CGPoint destination, NSUInteger segments);
    
    

    在所有这些之上,我们对 ccDrawCircle 进行了调整,以创建 ccDrawSolidCircle,如下所示:

    void ccDrawSolidCircle( CGPoint center, float r, float a, NSUInteger segs, BOOL drawLineToCenter);
    
    

    由于我们正在控制每一帧的 OpenGL 渲染调用,因此当在实时迷你地图中使用时,这种技术效果很好。我们将在后面的菜谱中探讨这一点。

还有更多...

如果你打算大量使用原始绘图,你可能想考虑使用 顶点缓冲对象 OpenGL 扩展。使用 GL 函数 glGenBuffersglBindBufferglBufferData,你可以将顶点和其他信息放入视频内存而不是系统内存。这可能会根据情况大幅提高性能。有关更多信息,请查看 Apple 开发者文档中 OpenGL ES 编程指南 for iOS处理顶点数据最佳实践 部分,位于 developer.apple.com/library/ios/#documentation/3DDrawing/ Conceptual/OpenGLES_ProgrammingGuide/TechniquesforWorkingwithVertexData/TechniquesforWorkingwithVertexData.html

播放视频文件

过场场景是一个自视频游戏早期就存在的概念。过场场景通常穿插在游戏玩法段落之间或在游戏加载时显示。对于更复杂的过场场景,使用全动态视频通常是有利的。在这个食谱中,我们将看到如何将视频插入到我们的游戏中。

播放视频文件

准备工作

请参考项目 RecipeCollection01 以获取此食谱的完整工作代码。

如何操作...

这个食谱需要额外的步骤将 MediaPlayer iOS 框架链接到我们的项目中:

  1. 组与文件下右键点击你的项目。

  2. 点击 添加 | 已存在的框架

  3. iOS SDK 下选择 MediaPlayer.framework

请记住,RecipeCollection01 已经链接了这个库。

现在,执行以下代码:

#import <MediaPlayer/MediaPlayer.h>
@interface Ch1_PlayingVideoFiles {
MPMoviePlayerController *moviePlayer;
}
@implementation Ch1_PlayingVideoFiles
-(CCLayer*) runRecipe {
//Load our video file
NSURL *url = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"example_vid" ofType:@"mov"]];
//Create a MPMoviePlayerController object
moviePlayer = [[MPMoviePlayerController alloc] initWithContentURL:url];
//Register to receive a notification when the movie has finished playing.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(moviePlayBackDidFinish:)
name:MPMoviePlayerPlaybackDidFinishNotification
object:moviePlayer];
//Set the movie's control style and whether or not it should automatically play.
if ([moviePlayer respondsToSelector:@selector(setFullscreen:animated:)]) {
//Use the new 3.2 style API.
moviePlayer.controlStyle = MPMovieControlStyleNone;
moviePlayer.shouldAutoplay = YES;
CGSize winSize = [[CCDirector sharedDirector] winSize];
moviePlayer.view.frame = CGRectMake(45, 50, winSize.width-90, winSize.height-100);
[[[CCDirector sharedDirector] openGLView] addSubview:moviePlayer.view];
} else {
//Use the old 2.0 style API.
moviePlayer.movieControlMode = MPMovieControlModeHidden;
[self playMovie];
}
return self;
}
-(void)moviePlayBackDidFinish:(NSNotification*)notification {
//If playback is finished we stop the movie.
[self stopMovie];
}
-(void)playMovie {
//We do not play the movie if it is already playing.
MPMoviePlaybackState state = moviePlayer.playbackState;
if(state == MPMoviePlaybackStatePlaying) {
NSLog(@"Movie is already playing.");
return;
}
[moviePlayer play];
}
-(void)stopMovie {
//We do not stop the movie if it is already stopped.
MPMoviePlaybackState state = moviePlayer.playbackState;
if(state == MPMoviePlaybackStateStopped) {
NSLog(@"Movie is already stopped.");
return;
}
//Since playback has finished we remove the observer.
[[NSNotificationCenter defaultCenter] removeObserver:self
name:MPMoviePlayerPlaybackDidFinishNotification
object:moviePlayer];
//If the moviePlayer.view was added to the openGL view, it needs to be removed.
if ([moviePlayer respondsToSelector:@selector(setFullscreen:animated:)]) {
[moviePlayer.view removeFromSuperview];
}
}
-(void)cleanRecipe {
[super cleanRecipe];
[self stopMovie];
[moviePlayer release];
}
@end

它是如何工作的...

这个食谱展示了如何加载、播放和停止一个电影。

  • 使用 MPMoviePlayerController:

    这个食谱只是关于电影播放的冰山一角。电影也可以在全屏模式、纵向模式和多种其他选项下播放。请参考官方 Apple 文档以自定义和/或添加此技术。

  • UsingObjective-C 观察者:

    在进行 Cocos2d 编程时,观察者模式并不常用,但它是一个强大的机制,并且是推荐的方式来知道你的视频何时播放完毕。你可以通过参考官方的 Objective-C 文档来了解更多关于观察者的信息。

  • 电影文件格式:

    根据 Apple 文档,建议你使用 H.264/MPEG-4 对视频进行压缩,AAC 音频,以及以下文件格式之一:MOVMP4MPV3GP

    还建议你的电影大小不超过 640x480,运行速度不超过 30 FPS

    用于食谱中的电影是由 Apple 的 iMovie 软件创建和编码的。

    如需更多信息,请查阅官方 Apple iOS SDK 文档。

网格、粒子以及运动条纹效果

Cocos2d 配备了各种易于使用的特殊效果。在这里,我们只简要介绍所有效果,因为它们相当直接,在其他文本中也有很好的介绍。

网格、粒子和动态条纹效果

准备工作

请参考项目RecipeCollection01以获取此菜谱的完整工作代码。

如何做...

要在游戏中正确显示网格效果,你首先需要将EAGLView pixelFormat设置为kEAGLColorFormatRGBA8(默认设置为kEAGLColorFormatRGB565)。

通过进入你的项目文件中的${PROJECT_NAME}AppDelegate.m文件并更改以下代码来实现:

EAGLView *glView = [EAGLView viewWithFrame:[window bounds]
pixelFormat: kEAGLColorFormatRGB565
depthFormat:0
];

改成这样:

EAGLView *glView = [EAGLView viewWithFrame:[window bounds]
pixelFormat: kEAGLColorFormatRGBA8
depthFormat:0
];

然后,执行以下代码:

//Custom particle effect
@implementation ParticleWaterfall
-(id)init {
return [self initWithTotalParticles:400];
}
-(id)initWithTotalParticles:(int)p {
if(self != [super initWithTotalParticles: p])
return nil;
//Angle
angle = 270;
angleVar = 12;
//Emitter position
self.position = ccp(160, 60);
posVar = ccp(16, 4);
//Life of particles
life = 2;
lifeVar = 0.25f;
//Speed of particles
self.speed = 100;
self.speedVar = 20;
self.gravity = ccp(self.gravity.x, -5);
//Size of particles
startSize = 35.0f;
endSize = 100.0f;
//Color of particles
startColor = ccc4(0.4f, 0.4f, 1.0f, 0.6f);
startColorVar = ccc4(0,0,0,0);
endColor = ccc4(0.5f, 0.5f, 0.5f, 0);
endColorVar = ccc4(0,0,0,0);
//Additive
self.blendAdditive = NO;
return self;
}
@end
@interface Ch1_GridParticleMotionEffects
{
//Variables for motion streak effect
CCSprite *rocket;
CCMotionStreak *streak;
CGPoint rocketDirection;
}
@implementation Ch1_GridParticleMotionEffects
-(CCLayer*) runRecipe {
CGSize s = [[CCDirector sharedDirector] winSize];
/*** Grid effect demo ***/
//Create a CCSprite
CCSprite *sprite = [CCSprite spriteWithFile:@"colorable_sprite.png"];
[sprite setPosition:ccp(240,160)];
[self addChild:sprite z:1 tag:TAG_SPRITE];
//Create a grid effect
CCAction *gridEffect = [CCShaky3D actionWithRange:5 shakeZ:YES grid:ccg(15,10) duration:10];
//Run the effect
[sprite runAction:gridEffect];
/*** Particle effect demo ***/
//Create a simple fire particle effect
CCNode *fireEffect = [CCParticleFire node];
[self addChild:fireEffect z:1 tag:TAG_FIRE_EFFECT];
//Create a waterfall particle effect
CCNode *waterfallEffect = [ParticleWaterfall node];
[self addChild:waterfallEffect z:1 tag:TAG_WATERFALL_EFFECT];
/*** Motion streak demo ***/
//Set the rocket initially in a random direction.
rocketDirection = ccp(arc4random()%4+1,arc4random()%4+1);
//Add the rocket sprite.
rocket = [CCSprite spriteWithFile:@"rocket.png"];
[rocket setPosition:ccp(s.width/2, s.height/2)];
[rocket setScale:0.5f];
[self addChild:rocket];
//Create the streak object and add it to the scene.
streak = [CCMotionStreak streakWithFade:1 minSeg:1 image:@"streak.png" width:32 length:32 color:ccc4(255,255,255,255)];
[self addChild:streak];
streak.position = ccp(s.width/2, s.height/2);
[self schedule:@selector(step:)];
return self;
}
-(void)step:(ccTime)delta {
CGSize s = [[CCDirector sharedDirector] winSize];
//Make rocket bounce off walls
if(rocket.position.x > s.width || rocket.position.x < 0){
rocketDirection = ccp(-rocketDirection.x, rocketDirection.y);
}
else if(rocket.position.y > s.height || rocket.position.y < 0){
rocketDirection = ccp(rocketDirection.x, -rocketDirection.y);
}
//Slowly turn the rocket
rocketDirection = ccp(rocketDirection.x, rocketDirection.y+0.05f);
//Update rocket position based on direction
rocket.position = ccp(rocket.position.x + rocketDirection.x, rocket.position.y + rocketDirection.y);
[streak setPosition:rocket.position];
//Set the rocket's rotation
[rocket setRotation: radiansToDegrees(vectorToRadians(rocketDirection))];
}
@end

它是如何工作的...

在这个菜谱中,我们看到了许多东西。为了简洁起见,我在书中只包含了一个网格效果和两个粒子效果。每个库存网格和粒子效果都可以在RecipeCollection01中查看,还有一些自定义粒子效果,如WaterfallWaterSplash

  • 自定义粒子:

    Cocos2d 粒子有许多变量,通常最有利的是通过子类化内置粒子来帮助你创建自己的。以下是一些内置的 Cocos2d 粒子:

    CCParticleExplosion, CCParticleFire, CCParticleFireworks, CCParticleFlower, CCParticleGalaxy, CCParticleMeteor, CCParticleRain, CCParticleSmoke, CCParticleSnow, CCParticleSpiral, CCParticleSun
    
    
  • 使用 CCMotionStreak:

    动态条纹是向 CCNode 添加动态元素的好方法。这些通常可以与粒子结合,产生很好的效果。

    在创建动态条纹时,需要注意的一点是,当纹理弯曲回自身时,其纹理需要看起来很好。垂直纹理带有透明渐变边缘通常看起来最好。

使用 Retina 显示模式

iPhone 4 和 iPad 都支持 Apple 的Retina 显示模式。在 iPhone 4 上,这使分辨率加倍达到960x640

Cocos2d 的制作者在将此功能集成到框架中时非常用心。通过简单的开关即可开启 Retina 显示。然而,让游戏在高清和标清模式下运行相似可能有些棘手。幸运的是,他们也考虑到了这一点。在这个菜谱中,我们将启用 Retina 显示,并显示一个高分辨率图像,如下面的截图所示:

使用 Retina 显示模式

准备工作

要正确查看 Retina 显示,你需要一个 Retina 显示设备。在模拟器中,你需要执行以下操作以切换到iPhone Retina模拟:

  1. 打开 iOS 模拟器。

  2. 在文件菜单中点击硬件 | 设备 | iPhone (Retina)

你当然也可以使用真实的 iPhone 4 或 iPad 设备。

如何做...

首先,你必须在应用程序中启用Retina 显示。进入${PROJECT_NAME}AppDelegate.m取消注释以下行:

if( ! [director enableRetinaDisplay:YES] )
CCLOG(@"Retina Display Not supported");

现在将开启支持 Retina 显示的设备上的 Retina 显示,并关闭不支持 Retina 显示的设备上的 Retina 显示。

现在,执行以下代码:

-(CCLayer*) runRecipe {
//Switch to Retina mode to see the difference
CCSprite *sprite = [CCSprite spriteWithFile:@"cocos2d_beginner.png"];
[sprite setPosition:ccp(240,160)];
[sprite setScale:1.0f];
[self addChild:sprite];
return self;
}

它是如何工作的...

  • 一物两用:

    正如你所见,我们创建的精灵现在非常大且细节丰富。如果你关闭 Retina Display 或在一个不支持它的设备上运行,你会看到一个更小、更模糊的精灵。这是因为 Retina Display 会选择每个精灵的更高分辨率版本(如果有的话)。我们使用-hd后缀指定更高分辨率的版本。因此,在 Retina Display 模式下,Cocos2d 会自动显示cocos2d_beginner-hd.png而不是cocos2d_beginner.png

  • 位置、尺寸等:

    据说 Cocos2d 会相应地转换所有坐标位置、尺寸比率和任何其他内容。你唯一需要改变的是添加高分辨率图像。

    建议在使用此功能时注意以下几点。低级别的 OpenGL 黑客技术通常不会显示成你想要的样子。对此保持警惕,并在考虑支持两种模式之前,确保在 Retina Display 模式下测试任何复杂的技术。

  • Retina Display 的缺点:

    Retina Display 的主要缺点仅仅是它占用的磁盘空间量。包括所有 HD 图像将使你的所有艺术资产占用的空间增加一倍以上。此外,更高分辨率的图像在运行时也会占用更多内存。

  • Retina Display 的优点。

    另一方面,苹果不断增加应用大小限制和设备内存。随着新硬件的推出和制作桌面应用的能力,对于 AAA 游戏来说,提高分辨率是必须的。

1D 和 2D 缓动动作

缓动动作允许你使用多种公式微调游戏中使用的动作。它们可以应用于任何动作:移动、缩放、淡入淡出等。具体到移动,可以应用一个小调整,允许在 X 轴和 Y 轴上独立进行缓动。这可以用来创建许多酷炫的效果。

1D 和 2D 缓动动作

准备工作

请参考项目RecipeCollection01以获取此菜谱的完整工作代码。

如何操作...

执行以下代码:

@interface CCMoveByCustom : CCMoveBy
{}
-(void) update: (ccTime) t;
@end
@implementation CCMoveByCustom
-(void) update: (ccTime) t {
//Here we neglect to change something with a zero delta.
if(delta.x == 0){
[target_ setPosition:ccp( [(CCNode*)target_ position].x, (startPosition.y + delta.y*t ) )];
}else if(delta.y == 0){
[target_ setPosition:ccp( (startPosition.x + delta.x*t ), [(CCNode*)target_ position].y )];
}else{
[target_ setPosition:ccp( (startPosition.x + delta.x*t ), (startPosition.y + delta.y * t ) )];
}
}
@end
@implementation Ch1_EasingActions
-(CCLayer*) runRecipe {
/*** 1D Movement Ease Action ***/
//Create the basic action to move by a certain X and Y vector
CCActionInterval *action1D = [CCMoveBy actionWithDuration:2 position:ccp(200,200)];
//Create a sprite to move
CCSprite *spriteEase1D = [CCSprite spriteWithFile:@"colorable_sprite.png"];
[spriteEase1D setPosition:ccp(150,50)];
[self addChild:spriteEase1D z:1 tag:TAG_SPRITE_EASE_1D];
//Create an 'eased' movement action with a CCEase class
CCActionInterval *easeAction1D = [CCEaseInOut actionWithAction:action1D rate:2];
//Run the action
[spriteEase1D runAction:easeAction1D];
/*** 2D Movement Ease Action ***/
//Create two movement actions, one in each dimension
CCActionInterval *action2DX = [CCMoveByCustom actionWithDuration:2 position:ccp(200,0)];
CCActionInterval *action2DY = [CCMoveByCustom actionWithDuration:2 position:ccp(0,200)];
//Create a sprite to move
CCSprite *spriteEase2D = [CCSprite spriteWithFile:@"colorable_sprite.png"];
[spriteEase2D setPosition:ccp(150,50)];
[self addChild:spriteEase2D z:1 tag:TAG_SPRITE_EASE_2D];
//Create two 'eased' movement actions, one on each dimension
CCActionInterval *easeAction2DX = [CCEaseSineIn actionWithAction:action2DX];
CCActionInterval *easeAction2DY = [CCEaseBounceIn actionWithAction:action2DY];
//Run both actions
[spriteEase2D runAction:easeAction2DX];
[spriteEase2D runAction:easeAction2DY];
return self;
}
@end

它是如何工作的...

执行此代码后,你应该看到其中一个角色沿着直线向目的地移动,而另一个角色则以看似无序但经过计算的方式移动。

  • 2D 缓动动作的使用:

    在今年夏天创建 iOS 游戏GoldenAgeBaseball时,我使用了CCMoveByCustom来模拟不同的投球。一个滑块向下移动并远离,一个切球只远离,一个沉球只向下。这种投球风格的变体对于投球/击球游戏机制的开发至关重要。

    总体而言,缓动动作给你的游戏带来了一种精致和专业的感觉。无论是平滑相机移动还是模拟棒球投球,缓动动作都是一项优秀的工具,可以帮助你将游戏调整到完美。

渲染和纹理化 3D 形状

虽然听起来很奇怪,但在 2D 游戏中,有时你只是想添加一些简单的3D 图形。无论你是创建酷炫的 2D/3D 混合游戏还是简单的带有 2D 用户界面的 3D 游戏,3D 图形都不是一件容易产生的事情。第三维的复杂性常常与 2D 编程范式冲突。

为了简单起见,这个配方将向您展示如何创建一个简单的彩色立方体和一个简单的纹理立方体。即使在制作 2D 游戏时,简单几何形状的用途也很多。然而,包括着色器3D 模型在内的更多示例超出了本书的范围。

渲染和纹理化 3D 形状

准备工作

请参考项目RecipeCollection01以获取此配方的完整工作代码。

如何操作...

执行以下代码:

#import "Vector3D.h"
@interface Cube3D : CCSprite
{
Vector3D *translation3D;
Vector3D *rotation3DAxis;
GLfloat rotation3DAngle;
bool drawTextured;
}
@property (readwrite, assign) Vector3D *translation3D;
@property (readwrite, assign) Vector3D *rotation3DAxis;
@property (readwrite, assign) GLfloat rotation3DAngle;
@property (readwrite, assign) bool drawTextured;
-(void) draw;
@end
@implementation Cube3D
@synthesize translation3D,rotation3DAxis,rotation3DAngle,drawTextured;
-(void) draw {
//Vertices for each side of the cube
const GLfloat frontVertices[]={ -0.5f,-0.5f,0.5f, 0.5f,-0.5f,0.5f, -0.5f,0.5f,0.5f, 0.5f,0.5f,0.5f};
const GLfloat backVertices[] = { -0.5f,-0.5f,-0.5f, -0.5f,0.5f,-0.5f, 0.5f,-0.5f,-0.5f, 0.5f,0.5f,-0.5f };
const GLfloat leftVertices[] = { -0.5f,-0.5f,0.5f, -0.5f,0.5f,0.5f, -0.5f,-0.5f,-0.5f, -0.5f,0.5f,-0.5f };
const GLfloat rightVertices[] = { 0.5f,-0.5f,-0.5f, 0.5f,0.5f,-0.5f, 0.5f,-0.5f,0.5f, 0.5f,0.5f,0.5f };
const GLfloat topVertices[] = { -0.5f,0.5f,0.5f, 0.5f,0.5f,0.5f, -0.5f,0.5f,-0.5f, 0.5f,0.5f,-0.5f };
const GLfloat bottomVertices[] = {-0.5f,-0.5f,0.5f,-0.5f,-0.5f,-0.5f,0.5f,-0.5f,0.5f, 0.5f,-0.5f,-0.5f };
//Coordinates for our texture to map it to a cube side
const GLfloat textureCoordinates[] = { 0,0, 1,0, 0,1, 1,1,};
//We enable back face culling to properly set the depth buffer
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
//We are not using GL_COLOR_ARRAY
glDisableClientState(GL_COLOR_ARRAY);
//We disable GL_TEXTURE_COORD_ARRAY if not using a texture
if(!drawTextured){
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
}
//Replace the current matrix with the identity matrix
glLoadIdentity();
//Translate and rotate
glTranslatef(translation3D.x, translation3D.y, translation3D.z);
glRotatef(rotation3DAngle, rotation3DAxis.x, rotation3DAxis.y, rotation3DAxis.z);
//Bind our texture if neccessary
if(drawTextured){
glBindTexture(GL_TEXTURE_2D, texture_.name);
}
//Here we define our vertices, set our textures or colors and finally draw the cube sides
glVertexPointer(3, GL_FLOAT, 0, frontVertices);
if(drawTextured){ glTexCoordPointer(2, GL_FLOAT, 0, textureCoordinates); }
else{ glColor4f(1.0f, 0.0f, 0.0f, 1.0f); }
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glVertexPointer(3, GL_FLOAT, 0, backVertices);
if(drawTextured){ glTexCoordPointer(2, GL_FLOAT, 0, textureCoordinates); }
else{ glColor4f(1.0f, 1.0f, 0.0f, 1.0f); }
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glVertexPointer(3, GL_FLOAT, 0, leftVertices);
if(drawTextured){ glTexCoordPointer(2, GL_FLOAT, 0, textureCoordinates); }
else{ glColor4f(1.0f, 0.0f, 1.0f, 1.0f); }
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glVertexPointer(3, GL_FLOAT, 0, rightVertices);
if(drawTextured){ glTexCoordPointer(2, GL_FLOAT, 0, textureCoordinates); }
else{ glColor4f(0.0f, 1.0f, 1.0f, 1.0f); }
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glVertexPointer(3, GL_FLOAT, 0, topVertices);
if(drawTextured){ glTexCoordPointer(2, GL_FLOAT, 0, textureCoordinates); }
else{ glColor4f(0.0f, 1.0f, 0.0f, 1.0f); }
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glVertexPointer(3, GL_FLOAT, 0, bottomVertices);
if(drawTextured){ glTexCoordPointer(2, GL_FLOAT, 0, textureCoordinates); }
else{ glColor4f(0.0f, 0.0f, 1.0f, 1.0f); }
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
//We re-enable the default render state
glEnableClientState(GL_COLOR_ARRAY);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glDisable(GL_CULL_FACE);
glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
}
@end
@interface Ch1_3DCubes {
Cube3D *cube3d1;
Cube3D *cube3d2;
}
@implementation Ch1_3DCubes
-(CCLayer*) runRecipe {
//Load a textured cube and set initial variables
cube3d1 = [Cube3D spriteWithFile:@"crate.jpg"];
cube3d1.translation3D = [Vector3D x:2.0f y:0.0f z:-4.0f];
cube3d1.rotation3DAxis = [Vector3D x:2.0f y:2.0f z:4.0f];
cube3d1.rotation3DAngle = 0.0f;
cube3d1.drawTextured = YES;
[self addChild:cube3d1 z:3 tag:0];
//Load a colored cube and set initial variables
cube3d2 = [Cube3D spriteWithFile:@"blank.png"];
cube3d2.translation3D = [Vector3D x:-2.0f y:0.0f z:-4.0f];
cube3d2.rotation3DAxis = [Vector3D x:2.0f y:2.0f z:4.0f];
cube3d2.rotation3DAngle = 0.0f;
cube3d2.drawTextured = NO;
[self addChild:cube3d2 z:1 tag:1];
//Schedule cube rotation
[self schedule:@selector(step:)];
return self;
}
-(void) step:(ccTime)delta {
cube3d1.rotation3DAngle += 0.5f;
cube3d2.rotation3DAngle -= 0.5f;
}
@end

工作原理...

我们在这里看到的是 OpenGL ES 立方体渲染的快速入门课程,带有 Cocos2d 的变体。就像我们绘制 OpenGL 原语一样,这里我们创建另一个CCNode并重写其绘制方法以创建更复杂的 OpenGL 几何形状。

  • 纹理化:

    我们利用CCSprite方法将纹理加载到内存中,以便我们可以将该纹理绑定用于 3D 绘制。这个过程相当直接。

  • 深度测试、尺寸和转换:

    多亏了 Cocos2d 内置的深度测试,立方体将根据 Z 属性正确排序。translation3D.z值影响立方体的实际大小,而其translation3D.xtranslation3D.y值影响它在屏幕上的位置,与translation3D.z成比例。

还有更多...

有关 3D 图形的更多信息,请参考第八章中的配方使用 Cocos3d提示、工具和端口

渲染纹理填充的多边形

当创建大型关卡的游戏时,很容易遇到内存限制。大地图也包含诸如草地、树木、山脉等物体的重复绘制。这个配方将向您展示如何高效地渲染一个用重复纹理填充的多边形。这些可以绘制成任何大小,同时仍然只占用很少的内存和 CPU 时间。

渲染纹理填充的多边形

准备工作

请参考项目RecipeCollection01以获取此配方的完整工作代码。

如何操作...

执行以下代码:

#import "Vector3D.h"
//Included for CPP polygon triangulation
#import "triangulate.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
@implementation TexturedPolygon
@synthesize vertices, triangles;
+(id) createWithFile:(NSString*)file withVertices:(NSArray*)verts {
/*** Create a TexturedPolygon with vertices only. ***/
/*** Perform polygon trianglulation to get triangles. ***/
//Initialization
TexturedPolygon *tp = [TexturedPolygon spriteWithFile:file];
tp.vertices = [[NSMutableArray alloc] init];
tp.triangles = [[NSMutableArray alloc] init];
//Polygon Triangulation
Vector2dVector a;
for(int i=0; i<[verts count];i+=1){
//Add polygon vertices
[tp.vertices addObject:[verts objectAtIndex:i]];
//Add polygon vertices to triangulation container
CGPoint vert = [[verts objectAtIndex:i] CGPointValue];
a.push_back( Vector2d(vert.x, vert.y) );
}
//Run triangulation algorithm
Vector2dVector result;
Triangulate::Process(a,result);
//Gather all triangles from result container
int tcount = result.size()/3;
for (int i=0; i<tcount; i++) {
const Vector2d &p1 = result[i*3+0];
const Vector2d &p2 = result[i*3+1];
const Vector2d &p3 = result[i*3+2];
//Add triangle index
[tp.triangles addObject: [tp getTriangleIndicesFromPoint1:ccp(p1.GetX(),p1.GetY()) point2:ccp(p2.GetX(),p2.GetY()) point3:ccp(p3.GetX(), p3.GetY())] ];
}
//Set texture coordinate information
[tp setCoordInfo];
return tp;
}
+(id) createWithFile:(NSString*)file withVertices:(NSArray*)verts withTriangles:(NSArray*)tris {
/*** Create a TexturedPolygon with vertices and triangles given. ***/
//Initialization
TexturedPolygon *tp = [TexturedPolygon spriteWithFile:file];
tp.vertices = [[NSMutableArray alloc] init];
tp.triangles = [[NSMutableArray alloc] init];
//Set polygon vertices
for(int i=0; i<[verts count];i+=1){
[tp.vertices addObject:[verts objectAtIndex:i]];
}
//Set triangle indices
for(int i=0; i<[tris count];i+=1){
[tp.triangles addObject:[tris objectAtIndex:i]];
}
//Set texture coordinate information
[tp setCoordInfo];
return tp;
}
-(Vector3D*) getTriangleIndicesFromPoint1:(CGPoint)p1 point2:(CGPoint)p2 point3:(CGPoint)p3 {
/*** Convert three polygon vertices to triangle indices ***/
Vector3D* indices = [Vector3D x:-1 y:-1 z:-1];
for(int i=0; i< [vertices count]; i++){
CGPoint vert = [[vertices objectAtIndex:i] CGPointValue];
if(p1.x == vert.x and p1.y == vert.y){
indices.x = i;
}else if(p2.x == vert.x and p2.y == vert.y){
indices.y = i;
}else if(p3.x == vert.x and p3.y == vert.y){
indices.z = i;
}
}
return indices;
}
-(void) addAnimFrameWithFile:(NSString*)file toArray:(NSMutableArray*)arr {
/*** For textured polygon animation ***/
ccTexParams params = {GL_NEAREST,GL_NEAREST_MIPMAP_NEAREST,GL_REPEAT,GL_REPEAT};
CCTexture2D *frameTexture = [[CCTextureCache sharedTextureCache] addImage:file];
[frameTexture setTexParameters:&params];
CCSpriteFrame *frame = [CCSpriteFrame frameWithTexture:frameTexture rect:self.textureRect];
[[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFrame:frameTexture name:file];
[arr addObject:frame];
}
-(void) setCoordInfo {
/*** Set texture coordinates for each vertex ***/
if(coords){ free(coords); }
coords = (ccV2F_T2F*)malloc(sizeof(ccV2F_T2F)*[vertices count]);
for(int i=0;i<[vertices count];i++) {
coords[i].vertices.x = [[vertices objectAtIndex:i] CGPointValue].x;
coords[i].vertices.y = [[vertices objectAtIndex:i] CGPointValue].y;
float atlasWidth = texture_.pixelsWide;
float atlasHeight = texture_.pixelsHigh;
coords[i].texCoords.u = (coords[i].vertices.x + rect_.origin.x)/ atlasWidth;
coords[i].texCoords.v = (contentSize_.height - coords[i].vertices.y + rect_.origin.y)/ atlasHeight ;
}
}
-(void) dealloc
{
//Release texture coordinates if necessary
if(coords) free(coords);
[super dealloc];
}
-(void) draw
{
/*** This is where the magic happens. Texture and draw all triangles. ***/
glDisableClientState(GL_COLOR_ARRAY);
glColor4ub( color_.r, color_.g, color_.b, quad_.bl.colors.a);
BOOL newBlend = NO;
if( blendFunc_.src != CC_BLEND_SRC || blendFunc_.dst != CC_BLEND_DST ) {
newBlend = YES;
glBlendFunc( blendFunc_.src, blendFunc_.dst );
}
glBindTexture(GL_TEXTURE_2D, texture_.name);
unsigned int offset = (unsigned int)coords;
unsigned int diff = offsetof( ccV2F_T2F, vertices);
glVertexPointer(2, GL_FLOAT, sizeof(ccV2F_T2F), (void*) (offset + diff));
diff = offsetof( ccV2F_T2F, texCoords);
glTexCoordPointer(2, GL_FLOAT, sizeof(ccV2F_T2F), (void*) (offset + diff));
for(int i=0;i<[triangles count];i++){
Vector3D *tri = [triangles objectAtIndex:i];
short indices[] = {tri.x, tri.y, tri.z};
glDrawElements(GL_TRIANGLE_STRIP, 3, GL_UNSIGNED_SHORT, indices);
}
if(newBlend) { glBlendFunc(CC_BLEND_SRC, CC_BLEND_DST); }
glColor4ub( 255, 255, 255, 255);
glEnableClientState(GL_COLOR_ARRAY);
}
@end
@implementation Ch1_RenderTexturedPolygon
-(CCLayer*) runRecipe {
CGSize s = [[CCDirector sharedDirector] winSize];
//Set polygon vertices
CGPoint vertexArr[] = { ccp(248,340), ccp(200,226), ccp(62,202), ccp(156,120), ccp(134,2), ccp(250,64), ccp(360,0), ccp(338,128), ccp(434,200), ccp(306,230) };
int numVerts = 10;
NSMutableArray *vertices = [[NSMutableArray alloc] init];
//Add vertices to array
for(int i=0; i<numVerts; i++){
[vertices addObject:[NSValue valueWithCGPoint:vertexArr[i]]];
}
//Note: Your texture size MUST be a product of 2 for this to work.
//Set texture parameters to repeat
ccTexParams params = {GL_NEAREST,GL_NEAREST_MIPMAP_NEAREST,GL_REPEAT,GL_REPEAT};
//Create textured polygon
TexturedPolygon *texturedPoly = [TexturedPolygon createWithFile:@"bricks.jpg" withVertices:vertices];
[texturedPoly.texture setTexParameters:&params];
texturedPoly.position = ccp(128,128);
//Add textured polygon to scene
[self addChild:texturedPoly z:1 tag:0];
return self;
}
@end

工作原理...

TexturedPolygon接受一组给定的顶点,并使用多边形三角化算法找到多边形内包含的所有三角形。然后使用 OpenGL 三角形带对这些三角形进行纹理化和绘制。

  • 三角化:

    三角化,根据多边形的不同,可能是一个复杂的过程。这通常在地图加载时执行。对于非常复杂的多边形,在关卡创建期间执行多边形三角化并存储与多边形顶点一起的三角形索引可能是有益的。这可以加快关卡加载时间。

  • 用途:

    纹理多边形有许多用途,包括静态地图纹理和背景纹理。

  • 性能:

    使用这种技术,你可以高效地绘制几乎任何大小的多边形。空间需求取决于使用的每个纹理的大小,而不是每个多边形的大小。为了节省空间,修改 TexturedPolygon 以重用预先初始化的纹理。

  • 注意事项:

    这种技术有几个注意事项。使用的纹理必须是正方形,每边的尺寸必须是 2n(16x16、32x32、64x64 等)。此外,纹理只能是单个文件,不能是精灵帧。

更多...

这个食谱可能是你第一次尝试将 Objective-CC++ 代码结合使用。这通常被称为 Objective-C++。有关更多信息,请参阅苹果官方开发者文档 使用 C++ 与 Objective-C,链接为 developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/ObjectiveC/Articles/ocCPlusPlus.html

动画纹理填充多边形

TexturedPolygon 也可以轻松动画化。这对于动画人群、海浪、沸腾的熔岩坑等非常有用。在示例中,我们看到一片动画的麦田。

动画纹理填充多边形

准备工作

请参考项目 RecipeCollection01 以获取此食谱的完整工作代码。

如何操作...

执行以下代码:

#import "Vector3D.h"
#import "TexturedPolygon.h"
@implementation Ch1_AnimateTexturedPolygon
-(CCLayer*) runRecipe {
CGSize s = [[CCDirector sharedDirector] winSize];
ccTexParams params = {GL_NEAREST, GL_NEAREST_MIPMAP_NEAREST, GL_REPEAT,GL_REPEAT};
//Create grass animated textured polygon
CGPoint grassVertexArr[] = { ccp(0,0), ccp(480,0), ccp(480,320), ccp(0,320) };
int grassNumVerts = 4;
NSMutableArray *grassVertices = [[NSMutableArray alloc] init];
for(int i=0; i<grassNumVerts; i++){
[grassVertices addObject:[NSValue valueWithCGPoint:ccp(grassVertexArr[i].x*1, grassVertexArr[i].y*1)]];
}
TexturedPolygon *grassPoly = [TexturedPolygon createWithFile:@"grass_tile_01.png" withVertices:grassVertices];
[grassPoly.texture setTexParameters:&params];
grassPoly.position = ccp(32,32);
[self addChild:grassPoly z:1 tag:1];
//Create swaying grass animation
NSMutableArray *grassAnimFrames = [NSMutableArray array];
//This is a two part animation with 'back' and 'forth' frames
for(int i=0; i<=6; i++){
[grassPoly addAnimFrameWithFile:[NSString stringWithFormat:@"grass_tile_0%d.png",i] toArray:grassAnimFrames];
}
for(int i=5; i>0; i--){
[grassPoly addAnimFrameWithFile:[NSString stringWithFormat:@"grass_tile_0%d.png",i] toArray:grassAnimFrames];
}
CCAnimation *grassAnimation = [[CCAnimation alloc] initWithName:@"grass_tile_anim" delay:0.1f];
for(int i=0; i<[grassAnimFrames count]; i++){
[grassAnimation addFrame:[grassAnimFrames objectAtIndex:i]];
}
CCActionInterval *grassAnimate = [CCSequence actions: [CCAnimate actionWithAnimation:grassAnimation restoreOriginalFrame:NO],
[CCDelayTime actionWithDuration:0.0f], nil];
CCActionInterval *grassRepeatAnimation = [CCRepeatForever actionWithAction:grassAnimate];
[grassPoly runAction:grassRepeatAnimation];
return self;
}
@end

工作原理...

通过使用 CCAnimation 动态更改纹理,我们可以创建非常简单的平铺动画。这个操作的唯一额外成本是为动画的每一帧分配的额外空间。

使用图层交换调色板

任何游戏开发者工具箱中的关键工具之一是能够交换颜色调色板。从 NES 上的 The Legend of ZeldaXbox 上的 Halo,调色板交换是一种简单而有效的视觉提示,可以扩展有限的美术资源。

在以下示例中,你将学习如何使用图层进行调色板交换。我们在这个例子中使用了一个动画棒球运动员。

使用图层交换调色板

准备工作

请参考项目 RecipeCollection01 以获取此食谱的完整工作代码。

对于这个食谱,你需要一个图像处理程序。我推荐免费且易于使用的 GIMP

如何操作...

我们将要做的第一件事是绘制精灵和可着色区域:

  1. 用所有可动态着色的区域留白的方式绘制你的纹理。在你的图像编辑程序中,你的纹理应该看起来像以下这样:如何操作...

  2. 创建一个新图层,并将特定区域着色为白色。在这个例子中,我们正在将他的制服(腿和衬衫)着色为白色:如何操作...

  3. 隐藏其他图层,并将仅包含白色的单独图层保存为单独的纹理。

  4. 对任何其他单独着色的部分重复此操作。

  5. 一旦我们有了纹理,我们就可以编写一些代码:

    @implementation Ch1_PaletteSwapping
    -(CCLayer*) runRecipe {
    //Create a nice looking background
    CCSprite *bg = [CCSprite spriteWithFile:@"baseball_bg_02.png"];
    [bg setPosition:ccp(240,160)];
    bg.opacity = 100;
    [self addChild:bg z:0 tag:0];
    /*** Animate 4 different fielders with different color combinations ***/
    //Set color arrays
    ccColor3B colors1[] = {
    ccc3(255,217,161), ccc3(225,225,225), ccc3(0,0,150), ccc3(255,255,255) };
    ccColor3B colors2[] = {
    ccc3(140,100,46), ccc3(150,150,150), ccc3(255,0,0), ccc3(255,255,255) };
    ccColor3B colors3[] = {
    ccc3(255,217,161), ccc3(115,170,115), ccc3(115,170,115), ccc3(255,255,255) };
    ccColor3B colors4[] = {
    ccc3(140,100,46), ccc3(50,50,50), ccc3(255,255,0), ccc3(255,255,255) };
    //Animate fielders with colors
    [self animateFielderWithColors:colors1 withPosition:ccp(150,70)];
    [self animateFielderWithColors:colors2 withPosition:ccp(150,200)];
    [self animateFielderWithColors:colors3 withPosition:ccp(300,200)];
    [self animateFielderWithColors:colors4 withPosition:ccp(300,70)];
    return self;
    }
    -(void) animateFielderWithColors:(ccColor3B[])colors withPosition:(CGPoint)pos {
    //The names of our layers
    NSString *layers[] = { @"skin", @"uniform", @"trim", @"black_lines" };
    //Number of layers
    int numLayers = 4;
    for(int i=0; i<numLayers; i+=1){
    NSString *layerName = layers[i];
    ccColor3B color = colors[i];
    //We need each plist, the first frame name and finally a name for the animation
    NSString *plistName = [NSString stringWithFormat:@"fielder_run_%@.plist", layerName];
    NSString *firstFrameName = [NSString stringWithFormat:@"fielder_run_%@_01.png", layerName];
    NSString *animationName = [NSString stringWithFormat:@"fielder_run_%@", layerName];
    //Add plist frames to the SpriteFrameCache
    [[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:plistName];
    //Get the first sprite frame
    CCSpriteFrame *firstFrame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:firstFrameName];
    //Create our sprite
    CCSprite *sprite = [CCSprite spriteWithSpriteFrame:firstFrame];
    //Set color and position
    sprite.position = pos;
    sprite.color = color;
    //Create the animation and add frames
    CCAnimation *animation = [[CCAnimation alloc] initWithName:animationName delay:0.15f];
    for(int i=1; i<=8; i+=1){
    CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[NSString stringWithFormat:@"fielder_run_%@_0%i.png",layerName,i]];
    [animation addFrame:frame];
    }
    //Run the repeating animation
    [sprite runAction:[CCRepeatForever actionWithAction: [CCAnimate actionWithAnimation:animation]]];
    //Finally, add the sprite
    [self addChild:sprite];
    }
    }
    @end
    
    

工作原理...

通过在主层(黑色轮廓)下方绘制可交换层,我们掩盖了着色中的任何不精确之处。对于像前一部分中显示的没有使用厚黑色轮廓的绘图这样的艺术作品,这种方法稍微困难一些。

  • 效率—磁盘空间:

    在磁盘上保持你的 iOS 应用程序大小不超过一定范围总是一个好主意。这种技术在磁盘空间上相当容易,因为可交换的纹理由于简单的 PNG 压缩只占用很少的空间。

  • 效率—内存使用:

    不幸的是,内存中纹理的大小由其像素大小决定。因此,如果你正在交换大型动画纹理,可能会遇到内存消耗问题。调色板交换纹理的内存消耗等于正常内存大小乘以要交换的调色板数量。

  • 效率—CPU:

    当动画化调色板交换的纹理时,动画例程使用的 CPU 时间也将乘以可交换层的数量。这通常并不重要,因为动画本身占用的 CPU 时间非常少。

使用 CCTexture2DMutable 交换调色板

另一种调色板交换的方法涉及 哨兵颜色 和逐像素修改纹理的能力。这种方法可以帮助回收一些额外的磁盘和内存空间,但它通常需要大量的 CPU 时间。当与抗锯齿或混合纹理一起使用时,这种方法比之前的技术更混乱。

使用 CCTexture2DMutable 交换调色板

准备工作

请参阅 project RecipeCollection01 以获取此配方的完整工作代码。同时注意,包含的库 CCTexture2DMutable 并未包含在书中。

对于这个配方,你需要一个图像处理程序。再次推荐免费且易于使用的 GIMP

如何做到这一点...

我们首先要做的是绘制一个包含由哨兵颜色定义的可着色区域的精灵。哨兵颜色通常是易于识别的基本颜色,并且可以程序化地替换。在这种情况下,我们将使用红色、蓝色和绿色:

如何做到这一点...

在使用此技术时,最好尽可能避免抗锯齿和混合。根据你的纹理调整着色算法的容差可能很棘手。

现在,执行以下代码:

#import "CCTexture2DMutable.h"
@implementation Ch1_MutablePaletteSwapping
-(CCLayer*) runRecipe {
//Create a nice looking background
CCSprite *bg = [CCSprite spriteWithFile:@"baseball_bg_01.png"];
[bg setPosition:ccp(240,160)];
bg.opacity = 100;
[self addChild:bg z:0 tag:0];
/*** Animate 4 different fielders with different color combinations ***/
//Set color arrays
ccColor4B colors1[] = { ccc4(255,217,161,255), ccc4(225,225,225,255), ccc4(0,0,150,255) };
ccColor4B colors2[] = { ccc4(140,100,46,255), ccc4(150,150,150,255), ccc4(255,0,0,255) };
ccColor4B colors3[] = { ccc4(255,217,161,255), ccc4(115,170,115,255), ccc4(115,170,115,255) };
ccColor4B colors4[] = { ccc4(140,100,46,255), ccc4(50,50,50,255), ccc4(255,255,0,255) };
//Create texture copy to use as an immutable guide.
CCTexture2DMutable* textureCopy = [[[CCTexture2DMutable alloc] initWithImage:[UIImage imageNamed:@"fielder_run_sentinel_colors.png"]] autorelease];
//Create our sprites using mutable textures.
CCSprite *sprite1 = [CCSprite spriteWithTexture:[[[CCTexture2DMutable alloc] initWithImage:[UIImage imageNamed:@"fielder_run_sentinel_colors.png"]] autorelease]];
CCSprite *sprite2 = [CCSprite spriteWithTexture:[[[CCTexture2DMutable alloc] initWithImage:[UIImage imageNamed:@"fielder_run_sentinel_colors.png"]] autorelease]];
CCSprite *sprite3 = [CCSprite spriteWithTexture:[[[CCTexture2DMutable alloc] initWithImage:[UIImage imageNamed:@"fielder_run_sentinel_colors.png"]] autorelease]];
CCSprite *sprite4 = [CCSprite spriteWithTexture:[[[CCTexture2DMutable alloc] initWithImage:[UIImage imageNamed:@"fielder_run_sentinel_colors.png"]] autorelease]];
//Set sprite positions
[sprite1 setPosition:ccp(125,75)];
[sprite2 setPosition:ccp(125,225)];
[sprite3 setPosition:ccp(325,75)];
[sprite4 setPosition:ccp(325,225)];
//Swap colors in each sprite mutable texture and apply the changes.
[self swapColor:ccc4(0,0,255,255) withColor:colors1[0] inTexture:sprite1.texture withCopy:textureCopy];
[self swapColor:ccc4(0,255,0,255) withColor:colors1[1] inTexture:sprite1.texture withCopy:textureCopy];
[self swapColor:ccc4(255,0,0,255) withColor:colors1[2] inTexture:sprite1.texture withCopy:textureCopy];
[sprite1.texture apply];
/* CODE OMITTED */
//Finally, add the sprites to the scene.
[self addChild:sprite1 z:0 tag:0];
[self addChild:sprite2 z:0 tag:1];
[self addChild:sprite3 z:0 tag:2];
[self addChild:sprite4 z:0 tag:3];
return self;
}
-(void) swapColor:(ccColor4B)color1 withColor:(ccColor4B)color2 inTexture:(CCTexture2DMutable*)texture withCopy:(CCTexture2DMutable*)copy {
//Look through the texture, find all pixels of the specified color and change them.
//We use a tolerance of 200 here.
for(int x=0; x<texture.pixelsWide; x++){
for(int y=0; y<texture.pixelsHigh; y++){
if( [self isColor:[copy pixelAt:ccp(x,y)] equalTo:color1 withTolerance:200] ){
[texture setPixelAt:ccp(x,y) rgba:color2];
}
}
}
}
-(bool) isColor:(ccColor4B)color1 equalTo:(ccColor4B)color2 withTolerance:(int)tolerance {
//If the colors are equal within a tolerance we change them.
bool equal = YES;
if( abs(color1.r - color2.r) + abs(color1.g - color2.g) +
abs(color1.b - color2.b) + abs(color1.a - color2.a) > tolerance ){
equal = NO;
}
return equal;
}
@end

它是如何工作的...

无论好坏,这种技术与 Adobe Photoshop 和类似绘图程序中的颜色选择和替换工作方式相同。使用 CCTexture2DMutable 可能是一个缓慢的过程,并且这种方法仅推荐用于需要像素完美图形或对空间/内存要求非常严格的游戏。

使用 AWTextureFilter 进行模糊和字体阴影

通过利用 CCTexture2DMutable 类,AWTextureFilter 类可以用来创建一些酷炫的效果。这些包括 高斯模糊、选择性的高斯模糊以及如下场景中所示动态生成的字体阴影:

使用 AWTextureFilter 进行模糊和字体阴影

准备工作

请参考项目 RecipeCollection01 以获取此菜谱的完整工作代码。

如何操作...

执行以下代码:

#import "CCTexture2DMutable.h"
#import "AWTextureFilter.h"
@implementation Ch1_UsingAWTextureFilter
-(CCLayer*) runRecipe {
CGSize winSize = [[CCDirector sharedDirector] winSize];
//Pixel Format RGBA8888 is required for blur effects
[CCTexture2D setDefaultAlphaPixelFormat:kCCTexture2DPixelFormat_RGBA8888];
/*** Display a blurred texture ***/
//Create the blur mutable texture
CCTexture2DMutable *mutableBlurTexture = [[[CCTexture2DMutable alloc] initWithImage:[UIImage imageNamed:@"cocos2d_beginner.png"]] autorelease];
//Apply blur to the mutable texture
[AWTextureFilter blur:mutableBlurTexture radius:3];
//Create a sprite to show the blur
CCSprite *blurSprite = [CCSprite spriteWithTexture:mutableBlurTexture];
[blurSprite setPosition:ccp(winSize.width/2+blur.contentSize.width/2+1, winSize.height/2)];
//Add sprite to the scene
[self addChild:blurSprite z:0 tag:0];
/*** Display a selectively blurred texture ***/
//Create the mutable texture to selectively blur
CCTexture2DMutable *mutableSelectiveBlurTexture = [[[CCTexture2DMutable alloc] initWithImage:[UIImage imageNamed:@"cocos2d_beginner.png"]] autorelease];
//Apply selective blur to the mutable texture
[AWTextureFilter blur:mutableSelectiveBlurTexture radius:8 rect:CGRectMake(240-200, (winSize.height-160)-75, 150, 150)];
//Create a sprite to show the selective blur
CCSprite *selectiveBlurSprite = [CCSprite spriteWithTexture:mutableSelectiveBlurTexture];
[selectiveBlurSprite setPosition:ccp(winSize.width/2, winSize.height/2)];
//Add sprite to the scene
[self addChild:selectiveBlurSprite z:0 tag:1];
/*** Display dynamic font shadow effect ***/
//Create a background so we can see the shadow
CCLayerColor *background = [CCLayerColor layerWithColor:ccc4(200, 100, 100, 255) width:300 height:50];
[background setIsRelativeAnchorPoint:YES];
[background setAnchorPoint:ccp(0.5f, 0.5f)];
[background setPosition:ccp(winSize.width/2, winSize.height/2)];
//Create a sprite for the font label
CCSprite* labelSprite = [CCSprite node];
[labelSprite setPosition:ccp(winSize.width/2, winSize.height/2)];
//Create a sprite for the shadow
CCSprite* shadowSprite = [CCSprite node];
[shadowSprite setPosition:ccp(winSize.width/2+1, winSize.height/2+1)];
//Color it black
[shadowSprite setColor:ccBLACK];
//Add sprites to a node and the node to the scene
CCNode* node = [[CCNode alloc] init];
[node addChild:background z:-1];
[node addChild:shadowSprite z:0];
[node addChild:labelSprite z:1];
[self addChild:node z:-1 tag:2];
//Create a mutable texture with a string
CCTexture2DMutable *shadowTexture = [[[CCTexture2DMutable alloc] initWithString:@"Shadowed Text" fontName:@"Arial" fontSize:28] autorelease];
//Copy the mutable texture as non mutable texture
CCTexture2D *labelTexture = [[shadowTexture copyMutable:NO] autorelease];
//Set the label texture
[labelSprite setTexture:labelTexture];
[labelSprite setTextureRect:CGRectMake(0, 0, shadowTexture.contentSize.width, shadowTexture.contentSize.height)];
//Apply blur to the shadow texture
[AWTextureFilter blur:shadowTexture radius:4];
//Set the shadow texture
[shadowSprite setTexture:shadowTexture];
[shadowSprite setTextureRect:CGRectMake(0, 0, shadowTexture.contentSize.width, shadowTexture.contentSize.height)];
return self;
}

它是如何工作的...

AWTextureFilter 使用 CCTexture2DMutable 来实现令人印象深刻的高斯模糊效果。这是复杂像素操作的一个例子。

字体阴影:

CCTexture2DMutable 继承自 CCTexture2D。这允许我们使用以下方法:

- (id) initWithString:(NSString*)string fontName:(NSString*)name fontSize:(CGFloat)size;

这创建了一个标签纹理,然后我们可以通过创建一个偏移、变暗、模糊并最终绘制在原始标签纹理后面的类似纹理来创建模糊的字体阴影效果。

还有更多...

这里还有一些关于使用此模糊技术的其他建议:

  • 将截图作为暂停菜单的背景进行模糊(参见本章下一节,捕获和使用截图

  • 结合颜色效果以产生酷炫的光晕效果

  • 根据揭示类谜题和知识竞赛游戏的模糊半径进行增加或减少

捕获和使用截图

如上一次菜谱中所述,可以在游戏中捕获并使用截图来创建酷炫的效果,如暂停菜单的模糊背景。例如,在已发布的应用程序中,可以查看 2K Sports NHL 2K11 的暂停菜单。

捕获和使用截图

准备工作

请参考项目 RecipeCollection01 以获取此菜谱的完整工作代码。同时请注意,包含的 Screenshot 库在书中并未包含。

如何操作...

执行以下代码:

#import "Screenshot.h"
@implementation Ch1_TakingScreenshots
-(CCLayer*) runRecipe {
CCSprite* sprite = [CCSprite spriteWithTexture:[Screenshot takeAsTexture2D]];
[sprite setPosition:ccp(240,160)];
[sprite setScale:0.75f];
[self addChild:sprite z:0 tag:0];
return self;
}

它是如何工作的...

包含的 Screenshot 库使用了一些超出本书范围的复杂 iOS 技术。该库包含在 RecipeCollection01 中。你可以在那里查看它。

简而言之,Screenshot 会捕获当前屏幕上的内容并将其推入 CCTexture2D 以供你操作。

还有更多...

实时截图可用于各种用途,如下所示:

  • 在比赛或关卡结束时,截图游戏中的精彩瞬间和高潮

  • 考虑到你可以分析玩家当前正在看到的确切内容,你可以“打破第四面墙”(想想 Metal Gear Solid 中的 Psycho Mantis,在 Sony PlayStation 上)

  • 类似于在 Nintendo 64 上的 Pokemon Snap 的游戏内用户控制摄像头

使用 CCParallaxNode

Parallaxing 是 2D 侧滚动视频游戏的基础。一个有能力的开发者如果不在 2D 侧滚动游戏中包含一个漂亮的透视背景,那将是失职的。Cocos2d 通过 CCParallaxNode 使透视变得简单。

使用 CCParallaxNode

准备工作

请参考项目 RecipeCollection01 以获取此菜谱的完整工作代码。

如何操作...

执行以下代码:

@implementation Ch1_UsingCCParallaxNode
-(CCLayer*) runRecipe {
//Create four parallax sprites, one for each layer
CCSprite* parallaxLayer01 = [CCSprite spriteWithFile:@"parallax_layer_01.png"];
CCSprite* parallaxLayer02 = [CCSprite spriteWithFile:@"parallax_layer_02.png"];
CCSprite* parallaxLayer03 = [CCSprite spriteWithFile:@"parallax_layer_03.png"];
CCSprite* parallaxLayer04 = [CCSprite spriteWithFile:@"parallax_layer_04.png"];
//Create a parallax node and add all four sprites
CCParallaxNode* parallaxNode = [CCParallaxNode node];
[parallaxNode setPosition:ccp(0,0)];
[parallaxNode addChild:parallaxLayer01 z:1 parallaxRatio:ccp(0, 0) positionOffset:ccp(240,200)];
[parallaxNode addChild:parallaxLayer02 z:2 parallaxRatio:ccp(1, 0) positionOffset:ccp(240,100)];
[parallaxNode addChild:parallaxLayer03 z:3 parallaxRatio:ccp(2, 0) positionOffset:ccp(240,100)];
[parallaxNode addChild:parallaxLayer04 z:4 parallaxRatio:ccp(3, 0) positionOffset:ccp(240,20)];
[self addChild:parallaxNode z:0 tag:1];
//Move the node to the left then the right
//This creates the effect that we are moving to the right then the left
CCMoveBy* moveRight = [CCMoveBy actionWithDuration:5.0f position:ccp(-80, 0)];
CCMoveBy* moveLeft = [CCMoveBy actionWithDuration:2.5f position:ccp(80, 0)];
CCSequence* sequence = [CCSequence actions:moveRight, moveLeft, nil];
CCRepeatForever* repeat = [CCRepeatForever actionWithAction:sequence];
[parallaxNode runAction:repeat];
return self;
}
@end

它是如何工作的...

Cocos2d 使得创建看起来专业的滚动背景变得非常容易。CCParallaxNode将视差的概念分解为其关键组件。在下面的示例中,我们将四个精灵附加到CCParallaxNode的一个实例上。请注意,您可以将任何CCNode附加到CCParallaxNode。然后我们设置parallaxRatioparallaxOffset以创建所需的效果。

  • 视差比率:

    这个比率决定了游戏坐标如何影响这个特定视差层的坐标。ccp(2,0)的比率意味着精灵在 X 轴上滚动速度是两倍,而在 Y 轴上则完全不滚动。更高的(更快的)比率通常绘制得离相机更近。

  • 位置偏移:

    每个子节点的位置偏移量代表当其父节点(CCParallaxNode)位于原点或ccp(0,0)时,它将被绘制的位置。一旦主CCParallaxNode实例移动,子节点将以适当的比率移动。

还有更多...

有多种方法可以循环视差背景。一种方法是在每一步检查parallaxNode的位置,并根据视差节点 X 位置除以屏幕大小的整数值调整所有子节点的位置偏移量:

parallaxNodeChildXOffset = baseXOffset + ((int) (self.position.x / winSize.width)) * winSize.width;

这实际上在parallaxNode移动一个完整屏幕宽度后重置了子节点的位置。

使用glColorMask进行光照

光照是大多数 3D 视频游戏的基本组成部分。2D 游戏本身并不自然地适合光照效果,但通过正确的技术,我们可以创建一个 2D 体验,其中光照扮演着至关重要的角色。这为我们的 2D 场景增添了悬念。

在这个菜谱中,我们看到一位僧人手持灯笼穿过一个黑暗的山洞。僧人的灯笼发出圆形的光,照亮了场景中的黑暗部分。随着僧人穿过山洞,一群蝙蝠变得可见。

使用 glColorMask 进行光照

准备工作

请参考项目RecipeCollection01以获取此菜谱的完整工作代码。此外,请注意,用于创建“飞蝙蝠”效果的代码已被省略,因为那已经在之前的菜谱中介绍过了。

如何操作...

执行以下代码:

@interface Ch1_ColorMaskLighting : Recipe
{
SimpleAnimObject *burnSprite;
SimpleAnimObject *lightSprite;
SimpleAnimObject *monkSprite;
CCRenderTexture *darknessLayer;
NSMutableArray *bats;
CCAnimation *batFlyUp;
CCAnimation *batGlideDown;
}
@end
@implementation Ch1_ColorMaskLighting
-(CCLayer*) runRecipe {
//Add our PLISTs to the SpriteFrameCache singleton
CCSpriteFrameCache * cache = [CCSpriteFrameCache sharedSpriteFrameCache];
[cache addSpriteFramesWithFile:@"simple_bat.plist"];
[cache addSpriteFramesWithFile:@"monk_lantern.plist"];
//Add cave background
CCSprite *caveBg = [CCSprite spriteWithFile:@"cave.png"];
[caveBg setPosition:ccp(240,160)];
[self addChild: caveBg z:0 tag:TAG_CAVE_BG];
//Set up the burn sprite that will "knock out" parts of the darkness layer depending on the alpha value of the pixels in the image.
burnSprite = [SimpleAnimObject spriteWithFile:@"fire.png"];
burnSprite.position = ccp(50,50);
burnSprite.scale = 10.0f;
[burnSprite setBlendFunc: (ccBlendFunc) { GL_ZERO, GL_ONE_MINUS_SRC_ALPHA }];
[burnSprite retain];
burnSprite.velocity = ccp(1,0);
//Add a 'light' sprite which additively blends onto the scene. This represents the cone of light created by the monk's candle.
lightSprite = [SimpleAnimObject spriteWithFile:@"fire.png"];
lightSprite.position = ccp(50,50);
lightSprite.scale = 10.0f;
[lightSprite setColor:ccc3(100,100,50)];
[lightSprite setBlendFunc: (ccBlendFunc) { GL_ONE, GL_ONE }];
lightSprite.velocity = ccp(1,0);
[self addChild:lightSprite z:4 tag:TAG_LIGHT_SPRITE];
//Add the monk
monkSprite = [[SimpleAnimObject alloc] init];
monkSprite.position = ccp(50,50);
monkSprite.velocity = ccp(1,0);
[self addChild:monkSprite z:1 tag:TAG_MONK];
//Animate the monk to simulate walking.
CCAnimation *animation = [[CCAnimation alloc] initWithName:@"monk_lantern_walk" delay:0.1f];
for(int i=1; i<=5; i+=1){
[animation addFrame:[cache spriteFrameByName:[NSString stringWithFormat:@"monk_lantern_0%i.png",i]]];
}
for(int i=4; i>=2; i-=1){
[animation addFrame:[cache spriteFrameByName:[NSString stringWithFormat:@"monk_lantern_0%i.png",i]]];
}
[monkSprite runAction:[CCRepeatForever actionWithAction: [CCAnimate actionWithAnimation:animation]]];
//Add the 'darkness' layer. This simulates darkness in the cave.
darknessLayer = [CCRenderTexture renderTextureWithWidth:480 height:320];
darknessLayer.position = ccp(240,160);
[self addChild:darknessLayer z:0 tag:TAG_DARKNESS_LAYER];
//Schedule physics updates
[self schedule:@selector(step:)];
return self;
}
-(void)step:(ccTime)delta {
CGSize s = [[CCDirector sharedDirector] winSize];
//Clear the darkness layer for redrawing. Here we clear it to BLACK with 90% opacity.
[darknessLayer clear:0.0f g:0.0f b:0.0f a:0.9f];
//Begin the darkness layer drawing routine. This transforms to the proper location, among other things.
[darknessLayer begin];
//Limit drawing to the alpha channel.
glColorMask(0.0f, 0.0f, 0.0f, 1.0f);
//Draw the burn sprite only on the alpha channel.
[burnSprite visit];
//Reset glColorMask to allow drawing of colors.
glColorMask(1.0f, 1.0f, 1.0f, 1.0f);
//Finish transformation.
[darknessLayer end];
//Make the monk walk back and forth.
if(monkSprite.position.x > 480){
monkSprite.flipX = YES;
burnSprite.velocity = ccp(-1,0);
lightSprite.velocity = ccp(-1,0);
monkSprite.velocity = ccp(-1,0);
}else if(monkSprite.position.x < 0){
monkSprite.flipX = NO;
burnSprite.velocity = ccp(1,0);
lightSprite.velocity = ccp(1,0);
monkSprite.velocity = ccp(1,0);
}
//Update our SimpleAnimObjects
[burnSprite update:delta];
[lightSprite update:delta];
[monkSprite update:delta];
}
@end

它是如何工作的...

Cocos2d 仅暴露了足够的 OpenGL 绘制逻辑,使得复杂的渲染顺序操作看起来很容易。为了实现这种效果,我们使用CCRenderTexture。首先,我们使用以下调用清除屏幕:

[darknessLayer clear:0.0f g:0.0f b:0.0f a:0.9f];

我们随后通过glColorMask调用仅限制绘制到alpha 通道。这实际上告诉 OpenGL 根据我们渲染的内容来修改图形缓冲区的透明度(只有透明度,而不是颜色)。因此,我们渲染fire.png纹理来模拟 2D 光并在圆形中扩散。

最后,我们在上面添加另一个fire.png纹理以模拟光亮和颜色。

节点darknessLayer仅在屏幕的视图中绘制,而burnSpritelightSprite则在灯笼的位置绘制。

还有更多...

使用类似的技术,可以创造出各种形状、大小和颜色的灯光。这包括像火炬这样的动画灯光、像汽车前灯这样的形状灯光,或者像爆炸时明亮的闪光这样的短暂快速灯光效果。

最重要的是,这种效果让我们能够用可能或可能不存在于游戏世界阴影中的事物来逗弄玩家。

第二章. 用户输入

在本章中,我们将介绍以下内容:

  • 点击、长按和拖动输入

  • 深度测试输入

  • 创建按钮

  • 创建方向盘

  • 创建模拟摇杆

  • 使用加速度计进行转向

  • 使用加速度计进行 3D 旋转

  • 捏合缩放

  • 执行手势

简介

没有用户输入,视频游戏仅仅是一个技术演示。iOS 触摸设备允许对用户输入进行无限定制的自定义。在本章中,我们将介绍使用 触摸屏加速度计 的最常见输入方法。

点击、长按和拖动输入

点击、长按拖动 是最常用的输入技术。它们构成了用户界面输入以及与游戏对象交互的基本构建块。在本菜谱中,我们通过继承 CCSprite 来创建一个可以处理触摸事件并保持一些自定义状态信息的精灵。这,加上一些逻辑,使我们能够触摸、长按和拖动这个精灵。

点击、长按和拖动输入

准备工作

请参考项目 RecipeCollection01 以获取此菜谱的完整工作代码。

如何实现...

执行以下代码:

//ColorTouchSprite.h
enum { TS_NONE, TS_TAP, TS_HOLD, TS_DRAG };
@interface ColorTouchSprite : CCSprite
{
@public
float holdTime; //How long have we held down on this?
int touchedState; //Current touched state
bool isTouched; //Are we touching this currently?
float lastMoved; //How long has it been since we moved this?
CGPoint lastTouchedPoint; //Where did we last touch?
const float releaseThreshold = 1.0f; //How long before we recognize a release
const float holdThreshold = 0.2f; //How long before a tap turns into a hold
const float lastMovedThreshold = 0.5f; //How long before we consider you to be 'not moving'
const int dragThreshold = 3; //We have a drag threshold of 3 pixels.
}
@end
@implementation ColorTouchSprite
@synthesize touchedState;
-(id) init {
holdTime = 0; lastMoved = 0; touchedState = TS_NONE;
isTouched = NO; lastTouchedPoint = ccp(0,0);
[self schedule:@selector(step:)];
return [super init];
}
-(void) step:(ccTime)dt {
//We use holdTime to determine the difference between a tap and a hold
if(isTouched){
holdTime += dt; lastMoved += dt;
}else{
holdTime += dt;
if(holdTime > releaseThreshold){
touchedState = TS_NONE;
}
}
//If you are holding and you haven't moved in a while change the state
if(holdTime > holdThreshold && isTouched && lastMoved > lastMovedThreshold){
touchedState = TS_HOLD;
}
}
/* Used to determine whether or not we touched this object */
- (CGRect) rect {
float scaleMod = 1.0f;
float w = [self contentSize].width * [self scale] * scaleMod;
float h = [self contentSize].height * [self scale] * scaleMod;
CGPoint point = CGPointMake([self position].x - (w/2), [self position].y - (h/2));
return CGRectMake(point.x, point.y, w, h);
}
/* Process touches */
-(void) ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView: [touch view]];
point = [[CCDirector sharedDirector] convertToGL: point];
isTouched = YES; holdTime = 0; touchedState = TS_NONE;
lastTouchedPoint = point;
}
-(void) ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
if(!isTouched){ return; }
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView: [touch view]];
point = [[CCDirector sharedDirector] convertToGL: point];
if(touchedState == TS_DRAG || distanceBetweenPoints(lastTouchedPoint, point) > dragThreshold){
touchedState = TS_DRAG;
self.position = point;
lastMoved = 0;
}
lastTouchedPoint = point;
}
-(void) ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if(!isTouched){ return; }
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView: [touch view]];
point = [[CCDirector sharedDirector] convertToGL: point];
//A short hold time after a touch ended means a tap.
if(holdTime < 10){
touchedState = TS_TAP;
}
holdTime = 0;
isTouched = NO;
lastTouchedPoint = point;
}
@end
#import "Helpers.h"
@implementation Ch2_TapHoldDragInput
-(CCLayer*) runRecipe {
self.isTouchEnabled = YES;
//Our message sprite
message = [CCLabelBMFont labelWithString:@"Tap, hold or drag the square." fntFile:@"eurostile_30.fnt"];
message.position = ccp(240,260);
message.scale = 0.75f;
[self addChild:message];
//Init the ColorTouchSprite
colorTouchSprite = [ColorTouchSprite spriteWithFile:@"blank.png"];
colorTouchSprite.position = ccp(240,160);
[colorTouchSprite setTextureRect:CGRectMake(0,0,100,100)];
[self addChild:colorTouchSprite];
[self schedule:@selector(step)];
return self;
}
/* Process touch events */
-(void) ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView: [touch view]];
point = [[CCDirector sharedDirector] convertToGL: point];
//Helper function 'pointIsInRect' is defined in Helpers.h
if(pointIsInRect(point, [colorTouchSprite rect])){
[colorTouchSprite ccTouchesBegan:touches withEvent:event];
}
}
-(void) ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
/* CODE OMITTED */
}
-(void) ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
/* CODE OMITTED */
}
@end

工作原理...

首先,我们通过继承 CCSprite 创建 ColorTouchSprite 类。在这里,我们维护状态变量,以便我们能够区分点击、长按和拖动。我们还指定了一个 (CGRect)rect 方法。这个方法用于确定精灵是否被触摸。主菜谱层使用以下三个方法将触摸事件信息传递给这个精灵:

-(void) ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
-(void) ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
-(void) ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;

这些方法相当直接。每次我们触摸层时,我们调用 ccTouchesBegan。当我们移动时,我们调用 ccTouchesMoved。最后,当我们抬起手指时,我们调用 ccTouchesEnded。每个方法都执行 pointIsInRect 检查,然后调用精灵上的相应触摸方法。最后,精灵运行一些简单的逻辑来确定状态,并允许拖动精灵。

还有更多...

之前使用的技术并非捕获输入的唯一方式。Cocos2d 还提供了 CCTouchDispatcher 类。使用这个类,你可以实现 CCTargetedTouchDelegate 协议中的方法,并将代理对象分配给自动处理你的触摸输入。

相关内容...

有关此方法的更多信息,请参阅官方 Cocos2d 文档和 Cocos2d 论坛。

深度测试输入

手动处理输入,如前一个菜谱所示,给我们提供了在高级别管理可触摸对象的机会。使用按 Z 轴顺序排序的精灵数组,我们可以“吞没输入”,这样背景精灵就不会受到影响。

深度测试输入

准备工作

请参考项目 RecipeCollection01 以获取此菜谱的完整工作代码。

如何实现...

执行以下代码:

#import "ColorTouchSprite.h"
@implementation Ch2_DepthTestingInput
-(CCLayer*) runRecipe {
//Init the ColorTouchSprites
[self initSprites];
return self;
}
-(void) initSprites {
sprites = [[NSMutableArray alloc] init];
//We add 10 randomly colored sprites
for(int x=0; x<10; x++){
CCSprite *sprite = [ColorTouchSprite spriteWithFile:@"blank.png"];
/* CODE OMITTED */
}
}
/* Process touch events */
-(void) ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView: [touch view]];
point = [[CCDirector sharedDirector] convertToGL: point];
//Process input for all sprites
for(id sprite in sprites){
if(pointIsInRect(point, [sprite rect])){
//Swallow the input
[sprite ccTouchesBegan:touches withEvent:event];
return;
}
}
}
-(void) ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
/* CODE OMITTED */
//Process input for all sprites
for(id sprite in sprites){
if(pointIsInRect(point, [sprite rect])){
[sprite ccTouchesMoved:touches withEvent:event];
}
}
}
-(void) ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
/* CODE OMITTED */
//Process input for all sprites
for(id sprite in sprites){
//End all input when you lift up your finger
[sprite ccTouchesEnded:touches withEvent:event];
}
}
@end

工作原理...

我们的精灵数组有一个节点顺序,这直接对应于它们的Z顺序。因此,遍历这些精灵会进行隐式的深度测试。当一个精灵触摸开始时,我们吞下输入,只允许触摸那个精灵。

  • 注意事项:

    这种技术的唯一注意事项是,输入深度测试与精灵数组顺序相关联。任何对精灵Z顺序的修改都需要对数组中的节点进行重新排序

创建按钮

按钮以某种形式被用于大多数游戏中。使用 Cocos2d 实现一个简单的按钮解决方案很容易,但创建一个支持多指同时触摸的按钮则更困难。在这个菜谱中,我们将实现一个简单但有效的解决方案来解决这个问题。

创建按钮

准备工作

请参阅项目RecipeCollection01以获取此菜谱的完整工作代码。

如何实现...

执行以下代码:

//TouchableSprite.h
@interface TouchableSprite : CCSprite
{
@public
bool pressed; //Is this sprite pressed
NSUInteger touchHash; //Used to identify individual touches
}
@end
@implementation TouchableSprite
- (bool)checkTouchWithPoint:(CGPoint)point {
if(pointIsInRect(point, [self rect])){
return YES;
}else{
return NO;
}
}
- (CGRect) rect {
//We set our scale mod to make sprite easier to press.
//This also lets us press 2 sprites with 1 touch if they are sufficiently close.
float scaleMod = 1.5f;
float w = [self contentSize].width * [self scale] * scaleMod;
float h = [self contentSize].height * [self scale] * scaleMod;
CGPoint point = CGPointMake([self position].x - (w/2), [self position].y - (h/2));
return CGRectMake(point.x, point.y, w, h);
}
- (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView: [touch view]];
point = [[CCDirector sharedDirector] convertToGL: point];
//We use circle collision for our buttons
if(pointIsInCircle(point, self.position, self.rect.size.width/2)){
touchHash = [touch hash];
[self processTouch:point];
}
}
- (void)ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
/* CODE OMITTED */
if(pointIsInCircle(point, self.position, self.rect.size.width/2)){
if(touchHash == [touch hash]){ //If we moved on this sprite
[self processTouch:point];
}else if(!pressed){ //If a new touch moves onto this sprite
touchHash = [touch hash];
[self processTouch:point];
}
}else if(touchHash == [touch hash]){ //If we moved off of this sprite
[self processRelease];
}
}
- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
/* CODE OMITTED */
if(touchHash == [touch hash]){ //If the touch which pressed this sprite ended we release
[self processRelease];
}
}
- (void)processTouch:(CGPoint)point {
pressed = YES;
}
buttonscreating- (void)processRelease {
pressed = NO;
}
@end
//GameButton.h
@interface GameButton : TouchableSprite {
@public
NSString* upSpriteFrame;
NSString* downSpriteFrame;
NSString* name;
}
@end
@implementation GameButton
- (void)processTouch:(CGPoint)point {
CCSpriteFrameCache *cache = [CCSpriteFrameCache sharedSpriteFrameCache];
[self setDisplayFrame:[cache spriteFrameByName:downSpriteFrame]];
pressed = true;
[self setColor:ccc3(255,200,200)];
}
- (void)processRelease {
CCSpriteFrameCache *cache = [CCSpriteFrameCache sharedSpriteFrameCache];
[self setDisplayFrame:[cache spriteFrameByName:upSpriteFrame]];
pressed = false;
[self setColor:ccc3(255,255,255)];
}
@end
@implementation Ch2_Buttons
-(CCLayer*) runRecipe {
//Init buttons data structure
buttons = [[NSMutableArray alloc] init];
//Create buttons
CCSpriteFrameCache *cache = [CCSpriteFrameCache sharedSpriteFrameCache];
[cache addSpriteFramesWithFile:@"dpad_buttons.plist"];
[self createButtonWithPosition:ccp(350,50) withUpFrame:@"b_button_up.png" withDownFrame:@"b_button_down.png" withName:@"B"];
/* CODE OMITTED */
//Schedule step method
[self schedule:@selector(step)];
return self;
}
/* Display pressed buttons */
-(void) step {
[message setString:@"Buttons pressed:"];
for(GameButton *b in buttons){
if(b.pressed){
[message setString:[NSString stringWithFormat:@"%@ %@",message.string,b.name]];
}
}
}
/* Button creation shortcut method */
-(void) createButtonWithPosition:(CGPoint)position withUpFrame:(NSString*)upFrame withDownFrame:(NSString*)downFrame withName:(NSString*)name {
CCSpriteFrameCache *cache = [CCSpriteFrameCache sharedSpriteFrameCache];
GameButton *button = [[GameButton alloc] init];
button.position = position;
[button setUpSpriteFrame:upFrame];
[button setDownSpriteFrame:downFrame];
[button setDisplayFrame:[cache spriteFrameByName:[button upSpriteFrame]]];
button.name = name;
[self addChild:button];
[buttons addObject:button];
}
-(void) ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView: [touch view]];
point = [[CCDirector sharedDirector] convertToGL: point];
//Pass all touchesBegan events to GameButton instances
for(GameButton *b in buttons){
[b ccTouchesBegan:touches withEvent:event];
}
}
-(void) ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
/* CODE OMITTED */
}
-(void) ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
/* CODE OMITTED */
}
@end

它是如何工作的...

这个菜谱使用几个不同的类,所有这些类都从CCSprite派生,来创建逼真的按钮。这些按钮都可以独立触摸。

  • 独立触摸多个按钮:

    要使多按钮触摸工作,我们首先在我们的AppDelegate文件中的主UIWindow上调用以下方法:

    [window setMultipleTouchEnabled:YES];
    
    

    然后,我们的TouchableSprite类使用唯一标识通过ccTouches方法的每个UITouch对象的哈希变量。这样我们就可以跟踪每个独特的触摸。触摸甚至可以用来同时触摸两个按钮。

  • 用一个触摸点触摸两个按钮:

    我们的(CGRect)rect方法使用scaleMod1.5f。这,加上使用pointInCircle进行触摸检测,允许我们用一个放置得当的触摸点同时按下两个按钮。这对许多游戏至关重要。例如,原始超级马里奥兄弟要求用户按下 B 按钮来跑步,同时按下 A 按钮来跳跃。这种技术允许类似地使用 Y 和 A 按钮。

创建方向垫

另一种视频游戏输入的基本形式是方向垫。在这个菜谱中,你将看到如何创建一个令人信服的 3D 方向垫,你将看到如何在游戏场景中正确处理方向垫信息。

创建方向垫

准备工作

请参阅项目RecipeCollection01以获取此菜谱的完整工作代码。此外,请注意,为了简洁起见,一些代码已被省略。

如何实现...

执行以下代码:

#import "TouchableSprite.h"
@interface DPad : TouchableSprite {
@public
CGPoint pressedVector;
int direction;
}
@end
@implementation DPad
-(id)init {
self = [super init];
if (self != nil) {
pressedVector = ccp(0,0);
direction = DPAD_NO_DIRECTION;
CCSpriteFrameCache *cache = [CCSpriteFrameCache sharedSpriteFrameCache];
[cache addSpriteFramesWithFile:@"dpad_buttons.plist"];
//Set the sprite display frame
[self setDisplayFrame:[cache spriteFrameByName:@"d_pad_normal.png"]];
}
return self;
}
/* Process DPad touch */
- (void)processTouch:(CGPoint)point {
CCSpriteFrameCache *cache = [CCSpriteFrameCache sharedSpriteFrameCache];
//Set a color visual cue if pressed
[self setColor:ccc3(255,200,200)];
pressed = true;
CGPoint center = CGPointMake( self.rect.origin.x+self.rect.size.width/2, self.rect.origin.y+self.rect.size.height/2 );
//Process center dead zone
if(distanceBetweenPoints(point, center) < self.rect.size.width/10){
[self setDisplayFrame:[cache spriteFrameByName:@"d_pad_normal.png"]];
self.rotation = 0;
pressedVector = ccp(0,0);
direction = DPAD_NO_DIRECTION;
return;
}
//Process direction
float radians = vectorToRadians( CGPointMake(point.x-center.x, point.y-center.y) );
float degrees = radiansToDegrees(radians) + 90;
float sin45 = 0.7071067812f;
if(degrees >= 337.5 || degrees < 22.5){
[self setDisplayFrame:[cache spriteFrameByName:@"d_pad_horizontal.png"]];
self.rotation = 180; pressedVector = ccp(-1,0); direction = DPAD_LEFT;
}else if(degrees >= 22.5 && degrees < 67.5){
[self setDisplayFrame:[cache spriteFrameByName:@"d_pad_diagonal.png"]];
self.rotation = -90; pressedVector = ccp(-sin45,sin45); direction = DPAD_UP_LEFT;
}/* CODE OMITTED */
}
/* Process DPad release */
- (void)processRelease {
[self setColor:ccc3(255,255,255)];
CCSpriteFrameCache *cache = [CCSpriteFrameCache sharedSpriteFrameCache];
[self setDisplayFrame:[cache spriteFrameByName:@"d_pad_normal.png"]];
self.rotation = 0;
pressed = false;
pressedVector = ccp(0,0);
direction = DPAD_NO_DIRECTION;
}
@end
@implementation Ch2_DPad
-(CCLayer*) runRecipe {
//Add gunman sprites
CCSpriteFrameCache *cache = [CCSpriteFrameCache sharedSpriteFrameCache];
[cache addSpriteFramesWithFile:@"gunman.plist"];
//Initialize gunman
gunman = [SimpleAnimObject spriteWithSpriteFrame:[cache spriteFrameByName:@"gunman_stand_down.png"]];
gunman.position = ccp(240,160);
[self addChild:gunman];
gunmanDirection = DPAD_DOWN;
//Initialize DPad
[cache addSpriteFramesWithFile:@"dpad_buttons.plist"];
dPad = [[DPad alloc] init];
dPad.position = ccp(100,100);
[self addChild:dPad];
[self schedule:@selector(step:)];
return self;
}
-(void) step:(ccTime)delta {
//We reset the animation if the gunman changes direction
if(dPad.direction != DPAD_NO_DIRECTION){
if(gunmanDirection != dPad.direction){
resetAnimation = YES;
gunmanDirection = dPad.direction;
}
}
if(gunman.velocity.x != dPad.pressedVector.x*2 || gunman.velocity.y != dPad.pressedVector.y*2){
gunman.velocity = ccp(dPad.pressedVector.x*2, dPad.pressedVector.y*2);
resetAnimation = YES;
}
//Update gunman position
[gunman update:delta];
//Re-animate if necessary
if(resetAnimation){
[self animateGunman];
}
directional padcreating}
-(void) animateGunman {
CCSpriteFrameCache *cache = [CCSpriteFrameCache sharedSpriteFrameCache];
/* Animate our gunman */
CCAnimation *animation = [[CCAnimation alloc] initWithName:@"gunman_anim" delay:0.15f];
NSString *direction;
bool flipX = NO;
bool moving = YES;
if(gunman.velocity.x == 0 && gunman.velocity.y == 0){ moving = NO; }
if(gunmanDirection == DPAD_LEFT){ direction = @"right"; flipX = YES; }
else if(gunmanDirection == DPAD_UP_LEFT){ direction = @"up_right"; flipX = YES; }
/* CODE OMITTED */
//Our simple running loop
if(moving){
[animation addFrame:[cache spriteFrameByName:[NSString stringWithFormat:@"gunman_run_%@_01.png",direction]]];
/* CODE OMITTED */
}
directional padcreatinggunman.flipX = flipX;
[gunman runAction:[CCRepeatForever actionWithAction: [CCAnimate actionWithAnimation:animation]]];
}
/* Process touches */
-(void) ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView: [touch view]];
point = [[CCDirector sharedDirector] convertToGL: point];
[dPad ccTouchesBegan:touches withEvent:event];
}
-(void) ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
/* CODE OMITTED */
}
-(void) ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
/* CODE OMITTED */
}
@end

它是如何工作的...

这个菜谱使用一些简单的技巧来制作令人信服的方向垫效果。首先,我们必须看看DPad类。

  • DPad类:

    DPad类通过首先从 DPad 图像的中心创建一个 2D 向量到触摸点的位置来确定触摸方向。然后它将图像分成八个方向切片。每个方向对应于不同的精灵帧。当一切组合在一起时,我们得到一个看起来很棒的伪 3D 效果。

  • 处理 DPad 状态和 pressedVector

    DPad 类维护一个方向枚举和一个方向向量。这使我们能够确定我们的“枪手”精灵应该面向哪个方向,以及我们应该如何设置他的 velocity 变量以引发移动。

  • DPad 死区:

    我们的 DPad 在中间大约有 10% 的 死区。这使得控制感觉对用户来说更加自然。我们这样做是因为,在真实的方向板上,直接按下中间会导致没有移动。

创建模拟摇杆

通过在上一个菜谱的基础上构建,我们可以创建一个更复杂的虚拟 模拟摇杆。这种输入方法测量向量的大小以及方向。我们还为模拟摇杆创建了一个酷炫的视觉效果。

创建模拟摇杆

准备工作

请参阅项目 RecipeCollection01 以获取此菜谱的完整工作代码。

如何实现...

执行以下代码:

#import "TouchableSprite.h"
//AnalogStick.h
@interface AnalogStick : TouchableSprite {
@public
CGPoint _pressedVector; //Internal _pressedVector with no outer dead zone
CCSprite *nub;
CCSprite *bar;
int direction;
}
@property (readonly) CGPoint pressedVector; //External pressedVector with a dead zone
@end
@implementation AnalogStick
-(id)init {
self = [super init];
if (self != nil) {
self.scale = 0.5f;
_pressedVector = ccp(0,0);
CCSpriteFrameCache *cache = [CCSpriteFrameCache sharedSpriteFrameCache];
[cache addSpriteFramesWithFile:@"analog_stick.plist"];
//Set the sprite display frame
[self setDisplayFrame:[cache spriteFrameByName:@"analog_pad.png"]];
//Init the bar, set position and display frame
bar = [[CCSprite alloc] init];
[bar setDisplayFrame:[cache spriteFrameByName:@"analog_bar.png"]];
[self repositionBarWithPoint:self.position];
[self addChild:bar];
//Init the nub, set position and display frame
nub = [[CCSprite alloc] init];
[self repositionNub];
[nub setDisplayFrame:[cache spriteFrameByName:@"analog_nub.png"]];
[self addChild:nub];
}
return self;
}
analog stickcreating, steps-(void)dealloc {
[nub release];
[bar release];
[super dealloc];
}
/* Process analog stick touch */
-(void)processTouch:(CGPoint)point {
self.pressed = YES;
[self setColor:ccc3(255,200,200)]; [nub setColor:ccc3(255,200,200)]; [bar setColor:ccc3(255,200,200)];
CGPoint center = CGPointMake( self.rect.origin.x+self.rect.size.width/2, self.rect.origin.y+self.rect.size.height/2 );
_pressedVector = CGPointMake((point.x-center.x)/(self.rect.size.width/2), (point.y-center.y)/(self.rect.size.height/2));
[self repositionNub];
[self repositionBarWithPoint:point];
[self resetDirection];
}
/* Process analog stick release */
-(void)processRelease {
[self setColor:ccc3(255,255,255)]; [nub setColor:ccc3(255,255,255)]; [bar setColor:ccc3(255,255,255)];
self.pressed = NO;
_pressedVector = ccp(0,0);
[self repositionNub];
[self repositionBarWithPoint:self.position];
}
/* Reposition the nub according to the pressedVector */
-(void)repositionNub {
float width = ([self contentSize].width);
float height = ([self contentSize].height);
nub.position = ccp(_pressedVector.x*(width/2)+width/2,
_pressedVector.y*(height/2)+height/2);
}
analog stickcreating, steps/* Reposition the bar according to a pressed point */
-(void)repositionBarWithPoint:(CGPoint)point {
float width = ([self contentSize].width);
float height = ([self contentSize].height);
//Rotation
float radians = vectorToRadians( _pressedVector );
float degrees = radiansToDegrees(radians);
bar.rotation = degrees;
//Set the display frame of the bar
CCSpriteFrameCache *cache = [CCSpriteFrameCache sharedSpriteFrameCache];
[bar setDisplayFrame:[cache spriteFrameByName:@"analog_bar.png"]];
//Calculate bar position
float distFromCenter = distanceBetweenPoints(point, self.position);
float sizeMod = distFromCenter / [self contentSize].width;
float oldHeight = bar.textureRect.size.height;
float newHeight = oldHeight * sizeMod * 5;
//Custom fixes
if(newHeight < 100){ newHeight = 100.0f; }
if(distFromCenter < 3){ newHeight = 0.0f; }
bar.textureRect = CGRectMake(bar.textureRect.origin.x,bar.textureRect.origin.y+ (oldHeight-newHeight),
bar.textureRect.size.width,newHeight );
bar.anchorPoint = ccp(0.5f,0);
CGPoint directionVector = radiansToVector(radians-PI/2);
bar.position = ccp(width/2 + directionVector.x*width/4, height/2 + directionVector.y*height/4);
}
/* Reset the direction based on the pressedVector */
-(void) resetDirection {
if(_pressedVector.x == 0 && _pressedVector.y == 0){
direction = AS_NO_DIRECTION;
return;
}
analog stickcreating, stepsfloat radians = vectorToRadians(_pressedVector);
float degrees = radiansToDegrees(radians) + 90;
if(degrees >= 337.5 || degrees < 22.5){
direction = AS_LEFT;
}else if(degrees >= 22.5 && degrees < 67.5){
direction = AS_UP_LEFT;
}/* CODE OMITTED */
}
-(float) magnitude {
float m = sqrt( pow(_pressedVector.x,2) + pow(_pressedVector.y,2) );
//25% end deadzone to make it easier to hold highest magnitude
m += 0.25f;
if(m > 1.0f){ m = 1.0f; }
return m;
}
-(CGPoint) pressedVector {
float m = sqrt( pow(_pressedVector.x,2) + pow(_pressedVector.y,2) );
m += 0.25f;
CGPoint pv = ccp(_pressedVector.x*1.25f, _pressedVector.y*1.25f);
//25% end deadzone to make it easier to hold highest magnitude
if(m > 1){
float radians = vectorToRadians(_pressedVector);
pv = radiansToVector(radians + PI/2);
}
return pv;
}
@end
@implementation Ch2_AnalogStick
-(CCLayer*) runRecipe {
self.isTouchEnabled = YES;
CCSpriteFrameCache *cache = [CCSpriteFrameCache sharedSpriteFrameCache];
[cache addSpriteFramesWithFile:@"gunman.plist"];
//Initialize gunman
gunman = [SimpleAnimObject spriteWithSpriteFrame:[cache spriteFrameByName:@"gunman_stand_down.png"]];
gunman.position = ccp(240,160);
[self addChild:gunman];
gunman.velocity = ccp(0,0);
gunmanDirection = AS_DOWN;
//Initialize analog stick
[cache addSpriteFramesWithFile:@"analog_stick.plist"];
analogStick = [[AnalogStick alloc] init];
analogStick.position = ccp(100,100);
[self addChild:analogStick];
[self schedule:@selector(step:)];
//This sets off a chain reaction.
[self animateGunman];
return self;
}
-(void) step:(ccTime)delta {
//Set gunman velocity and animate if necessary
if(analogStick.direction != AS_NO_DIRECTION){
if(analogStick.direction != gunmanDirection){
[gunman stopAllActions];
gunmanDirection = analogStick.direction;
[self animateGunman];
}
}
gunman.velocity = ccp(analogStick.pressedVector.x*4, analogStick.pressedVector.y*4);
[gunman update:delta];
}
-(void) animateGunman {
CCSpriteFrameCache *cache = [CCSpriteFrameCache sharedSpriteFrameCache];
float speed = [analogStick magnitude];
//Animation delay is inverse speed
float delay = 0.075f/speed;
if(delay > 0.5f){ delay = 0.5f; }
CCAnimation *animation = [[CCAnimation alloc] initWithName:@"gunman_anim" delay:delay];
NSString *direction;
bool flipX = NO;
bool moving = YES;
if(gunman.velocity.x == 0 && gunman.velocity.y == 0){ moving = NO; }
if(gunmanDirection == AS_LEFT){ direction = @"right"; flipX = YES; }
else if(gunmanDirection == AS_UP_LEFT){ direction = @"up_right"; flipX = YES; }
/* CODE OMITTED */
//Our simple animation loop
if(moving){
[animation addFrame:[cache spriteFrameByName:[NSString stringWithFormat:@"gunman_run_%@_01.png",direction]]];
/* CODE OMITTED */
}
gunman.flipX = flipX;
//animateGunman calls itself indefinitely
[gunman runAction:[CCSequence actions: [CCAnimate actionWithAnimation:animation],
[CCCallFunc actionWithTarget:self selector:@selector(animateGunman)], nil ]];
}
/* Process touches */
-(void) ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView: [touch view]];
point = [[CCDirector sharedDirector] convertToGL: point];
[analogStick ccTouchesBegan:touches withEvent:event];
}
-(void) ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
/* CODE OMITTED */
}
-(void) ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
/* CODE OMITTED */
}
@end

它是如何工作的...

使用 AnalogStick 类为用户提供更精确的控制。

  • AnalogStick 类:

    DPad 类一样,AnalogStick 类确定方向。与 DPad 不同,它还使用以下行确定大小

    CGPoint center = CGPointMake( self.rect.origin.x+self.rect.size.width/2, self.rect.origin.y+self.rect.size.height/2 );
    _pressedVector = CGPointMake((point.x-center.x)/(self.rect.size.width/2), (point.y-center.y)/(self.rect.size.height/2));
    
    

    这个触摸位置也决定了“小突起”和“杆”的位置和方向。不深入细节,这创建了一个很好的模拟摇杆视觉效果。像上一个菜谱中的 DPad 类一样,我们的 AnalogStick 类也包括一个死区。

  • AnalogStick 死区:

    这次,死区涉及到使可触摸区域的 25% 外围达到向量的最大值。为了实现这一点,我们存储一个内部 _pressedVector 变量,并给 readonly 访问权限的 pressedVector 属性。它指向一个执行正确计算的方法。我们提供这个区域的原因是让用户能够舒适地让“枪手”以最高速度奔跑。

使用加速度计进行转向

iOS 应用程序还有另一种形式的输入:加速度计。它测量 iOS 设备在 X, YZ 平面上的方向。设备方向是一个动态(如果稍微延迟)的输入机制,具有多种用途。其中一种用途是赛车视频游戏中的转向。

使用加速度计进行转向

准备工作

请参阅项目 RecipeCollection01 以获取此菜谱的完整工作代码。

如何实现...

执行以下代码:

@implementation Ch2_AccelerometerSteering
-(CCLayer*) runRecipe {
//Enable the accelerometer and set its updateInterval
self.isAccelerometerEnabled = YES;
[[UIAccelerometer sharedAccelerometer] setUpdateInterval:(1.0 / 60)];
//Init car background
CCSprite *bg = [CCSprite spriteWithFile:@"car_dash.jpg"];
bg.position = ccp(240,160);
bg.opacity = 200;
[self addChild:bg z:0];
//Init steeringWheel sprite
steeringWheel = [CCSprite spriteWithFile:@"car_steering_wheel.png"];
steeringWheel.position = ccp(230,170);
[self addChild:steeringWheel z:1];
return self;
}
/* Handle accelerometer input */
- (void)accelerometer:(UIAccelerometer*)accelerometer didAccelerate:(UIAcceleration*)acceleration{
//Set steeringWheel rotation based on Y plane rotation
steeringWheel.rotation = -acceleration.y * 180;
}
@end

它是如何工作的...

当你直接看着屏幕时,左右旋转你的 iPhone 将导致看到方向盘旋转。包含在 UIAcceleration 变量中的 3D 向量在 iOS 设备旋转 90 度时上升或下降 1。因此,通过将这个旋转乘以 180 度,我们将 45 度的倾斜等同于方向盘的 90 度转向。

  • 加速度计延迟:

    在机械上,加速度计与触摸屏相比有轻微的延迟。这使得它在需要绝对瞬间控制的某些游戏类型中的应用变得不切实际。

使用加速度计进行 3D 旋转

同时使用多个加速度计值可以允许用户在空间中操纵 3D 对象的旋转。这在 iOS 游戏的《超级猴子球》系列中得到了很好的应用。

使用加速度计进行 3D 旋转

准备工作

请参考项目 RecipeCollection01 以获取此菜谱的完整工作代码。请注意,此示例中省略了 IphoneCube 类代码,因为它与第一章中的 3D 立方体代码类似,

如何操作...

执行以下代码:

#import "IphoneCube.h"
@implementation Ch2_AccelerometerRotation
-(CCLayer*) runRecipe {
//Enable the accelerometer and set its updateInterval
self.isAccelerometerEnabled = YES;
[[UIAccelerometer sharedAccelerometer] setUpdateInterval:(1.0 / 60)];
//Init our textured box
iphoneCube = [[IphoneCube alloc] init];
iphoneCube.translation3D = [Vector3D x:0.0f y:0.0f z:-2.0f];
iphoneCube.rotation3DAxis = [Vector3D x:0.0f y:0.0f z:(PI/2 - 0.075f)];
[self addChild:iphoneCube z:3 tag:0];
return self;
}
/* Handle accelerometer input */
- (void)accelerometer:(UIAccelerometer*)accelerometer didAccelerate:(UIAcceleration*)acceleration{
//Set x and y box orientation
iphoneCube.rotation3DAxis.x = -acceleration.x * 180;
iphoneCube.rotation3DAxis.y = -acceleration.y * 180;
}
@end

工作原理...

XY平面上旋转您的设备将导致屏幕上的虚拟 iPhone 旋转。我们将acceleration变量乘以 180,再次将我们的对象旋转得比设备本身多一倍。IphoneCube变量rotation3DAxis使用glRotatef在 3D 空间中旋转纹理盒。

捏合缩放

苹果的触摸设备普及了使用两个手指进行缩放和缩小的操作,这种方法仍然是任何广泛可用的触摸屏设备上缩放的最流行方式。在本菜谱中,我们将看到如何通过捏合来缩放场景。

捏合缩放

准备工作

请参考项目 RecipeCollection01 以获取此菜谱的完整工作代码。另外请注意,前一个屏幕截图所示的箭头效果在以下代码中已被省略。

如何操作...

执行以下代码:

#import "IphoneCube.h"
@implementation Ch2_PinchZooming
-(CCLayer*) runRecipe {
//Enable touching
self.isTouchEnabled = YES;
//Set initial variables
arrowsIn = NO;
cameraZoom = 1.0f;
lastMultiTouchZoomDistance = 0.0f;
//Init background
bg = [CCSprite spriteWithFile:@"dracula_castle.jpg"];
bg.position = ccp(240,160);
[self addChild:bg];
//Set initial zoom
[self setCameraZoom:1];
return self;
}
/* Check for HUD input */
-(bool) hudPressedWithPoint:(CGPoint)point {
//There is no HUD.
return NO;
}
-(void) setCameraZoom:(float)zoom {
cameraZoom = zoom;
bg.scale = cameraZoom;
}
/* Check touches */
-(void) ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView: [touch view]];
point = [[CCDirector sharedDirector] convertToGL: point];
//If HUD has not been touched we reset lastMultiTouchZoomDistance
if(![self hudPressedWithPoint:point]){
lastMultiTouchZoomDistance = 0.0f;
}
}
-(void) ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
CGSize s = [[CCDirector sharedDirector] winSize];
//Check for only 2 touches
if(touches.count == 2){
NSArray *twoTouch = [touches allObjects];
//Get both touches
UITouch *tOne = [twoTouch objectAtIndex:0];
UITouch *tTwo = [twoTouch objectAtIndex:1];
CGPoint firstTouch = [tOne locationInView:[tOne view]];
CGPoint secondTouch = [tTwo locationInView:[tTwo view]];
//If HUD hasn't been touched we use this distance and last distance to calculate zooming
if(![self hudPressedWithPoint:firstTouch] && ![self hudPressedWithPoint:secondTouch]){
CGFloat currentDistance = distanceBetweenPoints(firstTouch, secondTouch);
if(lastMultiTouchZoomDistance == 0){
lastMultiTouchZoomDistance = currentDistance;
}else{
float difference = currentDistance - lastMultiTouchZoomDistance;
float newZoom = (cameraZoom + (difference*cameraZoom/s.height));
if(newZoom < 1.0f){ newZoom = 1.0f; }
if(newZoom > 4.0f){ newZoom = 4.0f; }
[self setCameraZoom:newZoom];
lastMultiTouchZoomDistance = currentDistance;
}
}
}
}
-(void) ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView: [touch view]];
point = [[CCDirector sharedDirector] convertToGL: point];
//If HUD has not been touched we reset lastMultiTouchZoomDistance
if(![self hudPressedWithPoint:point]){
lastMultiTouchZoomDistance = 0.0f;
}
}
@end

工作原理...

此菜谱处理两个独立的触摸并确定它们各自的距离。它保持这个变量以确定触摸是否变得更近或更远。然后,使用这个距离来计算新的缩放级别。以下代码行执行此操作,同时保持缩放效果平滑:

float newZoom = (cameraZoom + (difference*cameraZoom/s.height));

这实现了预期的效果。

  • 处理多个同时触摸:

    如您所见,处理多个触摸与处理单个触摸类似。touches变量包含那个特定时刻的每个触摸。如果两个触摸一起移动,这个方法可以轻松地处理它们。

更多...

如果你想增加这项技术,尝试实现类似 iPhoto 的图像平移功能。这种平移/缩放组合已成为所有文档和图像查看器的标准,并且是许多 iOS 游戏的自然用户界面增强。

执行手势

手势可以作为功能输入快捷键。简单的手势,如滑动和滚动,已内置到许多苹果 UI 工具中。一些游戏,特别是《恶魔城:悲叹之晨》(DS)和《大野狼》(PS2、Wii),将手势用作核心游戏玩法机制。在本菜谱中,我们将实现一个简单且坦白说是粗略的手势系统。

执行手势

准备工作

请参阅项目RecipeCollection01以获取此菜谱的完整工作代码。此外,请注意,为了简洁,省略了GestureLineGestureShapeLayer类。GestureLine仅包含两个CGPoint结构。GestureShapeLayer绘制一个圆或一组线。

如何做...

执行以下代码:

#import "GestureLine.h"
#import "GestureShapeLayer.h"
@implementation Ch2_Gestures
-(CCLayer*) runRecipe {
//Init message
message = [CCLabelBMFont labelWithString:@"Draw a rectangle, triangle, circle or line" fntFile:@"eurostile_30.fnt"];
message.position = ccp(200,270);
message.scale = 0.65f;
[message setColor:ccc3(255,0,0)];
[self addChild:message z:3];
//Allow touching
self.isTouchEnabled = YES;
//Set font size
[CCMenuItemFont setFontSize:20];
//Add our breadcrumbs node
[self addBreadcrumbs];
//Init GestureShapeLayer
gestureShapeLayer = [[GestureShapeLayer alloc] init];
gestureShapeLayer.position = ccp(0,0);
[self addChild:gestureShapeLayer z:1];
return self;
}
/* Process touches */
-(void) ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
/* CODE OMITTED */
//Start a new gesture
[self newGestureWithPoint:point];
}
-(void) ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
/* CODE OMITTED */
//Add a point to our current gesture
[self addGesturePoint:point override:NO];
}
-(void) ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
/* CODE OMITTED */
//Finish our gesture
[self finishGestureWithPoint:point];
}
-(void) newGestureWithPoint:(CGPoint)point {
[self resetMessage];
//Init gesture variables
gestureShapeLayer.points = [[NSMutableArray alloc] init];
gestureShapeLayer.lines = [[NSMutableArray alloc] init];
firstPoint = point;
lastPoint = point;
vertex = point;
[gestureShapeLayer.points addObject:[NSValue valueWithCGPoint:point]];
gestureShapeLayer.drawCircle = NO;
gestureShapeLayer.drawLines = NO;
}
-(void) addGesturePoint:(CGPoint)point override:(bool)override {
//Set our angle change tolerance to 40 degrees. If it changes more than this we consider this a 'line'
float angleDiffTolerance = 40.0f;
//Check the old angle versus the new one
CGPoint vect = ccp(point.x-lastPoint.x, point.y-lastPoint.y);
float newAngle = radiansToDegrees( vectorToRadians(vect) );
//Add a line if the angle changed significantly
if(gestureShapeLayer.points.count > 1){
float angleDiff = angleDifference(newAngle, angle);
if(override || (angleDiff > angleDiffTolerance && distanceBetweenPoints(vertex, point) > 15.0f)){
[gestureShapeLayer.lines addObject:[GestureLine point1:vertex point2:point]];
vertex = point;
}
}
//Update values
angle = newAngle;
lastPoint = point;
[gestureShapeLayer.points addObject:[NSValue valueWithCGPoint:point]];
}
-(void) finishGestureWithPoint:(CGPoint)point {
[self addGesturePoint:point override:YES];
gestureShapeLayer.drawCircle = NO;
gestureShapeLayer.drawLines = NO;
//To finish gestures which require the end to be close to the beginning point we supply this distance tolerance
float lastPointTolerance = 100.0f;
//Rectangles, triangles and circles
if(distanceBetweenPoints(firstPoint, lastPoint) <= lastPointTolerance){
if(gestureShapeLayer.lines.count == 4){ //4 lines
[message setString:@"Rectangle"];
gestureShapeLayer.drawLines = YES;
}else if(gestureShapeLayer.lines.count == 3){ //3 lines
[message setString:@"Triangle"];
gestureShapeLayer.drawLines = YES;
}else if(gestureShapeLayer.lines.count <= 1){ //0 or 1 lines
[message setString:@"Circle"];
[gestureShapeLayer setCircleRectFromPoints];
gestureShapeLayer.drawCircle = YES;
}else{
[self resetMessage];
gestureShapeLayer.lines = [[NSMutableArray alloc] init];
}
}else{ //Lines and angles
if(gestureShapeLayer.lines.count == 1){ //1 line
[message setString:@"Line"];
gestureShapeLayer.drawLines = YES;
}else if(gestureShapeLayer.lines.count == 2){ //2 lines
[message setString:@"Angle"];
gestureShapeLayer.drawLines = YES;
}else{
[self resetMessage];
gestureShapeLayer.lines = [[NSMutableArray alloc] init];
}
}
}
@end

它是如何工作的...

这个手势系统跟踪用户输入的每个单独的点。每对点创建一个2D 向量。当当前向量的角度与上一个向量足够不同时,我们认为这是用户正在绘制的形状的新顶点。然后我们取这个顶点和上一个顶点,创建一条线。通过存储每个点和线,我们可以确定用户试图绘制的内容。

还有更多...

实现的这个系统还有很多不足之处。然而,它为更复杂和功能更强大的系统提供了概念基础。从某种角度看一系列的点,我们可以看到模式的出现。在这个例子中,我们寻找角度差异很大的连续向量来确定绘制的线条。其他如曲线、方向和点距离等因素可以导致更复杂形状的识别。

第三章. 文件和数据

在本章中,我们将涵盖以下要点:

  • 读取 PLIST 数据文件

  • 读取 JSON 数据文件

  • 读取 XML 数据文件

  • 使用 NSUserDefaults 保存简单数据

  • 将对象存档到存档文件中

  • 修改嵌套元数据

  • 将数据保存到 PLIST 文件中

  • 将数据保存到 SQLite 数据库中

  • 使用 Core Data 保存数据

简介

简单和复杂游戏都会处理和持久化数据。这包括高分、玩家资料和保存的游戏会话等。在本章中,我们将使用多种不同的技术来读取和写入数据。

读取 PLIST 数据文件

此菜谱以及随后的两个菜谱展示了如何将简单数据读取和解析到 Cocos2d 场景中。在这里,我们读取一个 PLIST 文件来创建一个描绘沙漠和几株仙人掌的场景。

读取 PLIST 数据文件

准备工作

请参考项目 RecipeCollection01 以获取此菜谱的完整工作代码。

如何操作...

执行以下代码:

#import <Foundation/Foundation.h>
/* This returns the full absolute path to a specified file in the bundle */
NSString* getActualPath( NSString* file )
{
NSArray* path = [file componentsSeparatedByString: @"."];
NSString* actualPath = [[NSBundle mainBundle] pathForResource: [path objectAtIndex: 0] ofType: [path objectAtIndex: 1]];
return actualPath;
}
@implementation Ch2_ReadingPlistFiles
-(CCLayer*) runRecipe {
//Initialize a read-only dictionary from our file
NSString *fileName = @"scene1.plist";
NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:getActualPath(fileName)];
//Process this dictionary
[self processMap:dict];
return self;
}
-(void) processMap:(NSDictionary*)dict {
//Loop through all dictionary nodes to process individual types
NSArray *nodes = [dict objectForKey:@"nodes"];
for (id node in nodes) {
if([[node objectForKey:@"type"] isEqualToString:@"spriteFile"]){
[self processSpriteFile:node];
}else if([[node objectForKey:@"type"] isEqualToString:@"texturedPolygon"]){
[self processTexturedPolygon:node];
}
}
}
/* Process the 'spriteFile' type */
-(void) processSpriteFile:(NSDictionary*)nodeDict {
//Init the sprite
NSString *file = [nodeDict objectForKey:@"file"];
CCSprite *sprite = [CCSprite spriteWithFile:file];
//Set sprite position
NSDictionary *posDict = [nodeDict objectForKey:@"position"];
sprite.position = ccp([[posDict objectForKey:@"x"] floatValue], [[posDict objectForKey:@"y"] floatValue]);
//Each numeric value is an NSString or NSNumber that must be cast into a float
sprite.scale = [[nodeDict objectForKey:@"scale"] floatValue];
//Set the anchor point so objects are positioned from the bottom-up
sprite.anchorPoint = ccp(0.5,0);
//We set the sprite Z according to its Y to produce an isometric perspective
float z = [self getZFromY:[[posDict objectForKey:@"y"] floatValue]];
if([nodeDict objectForKey:@"z"]){
z = [[nodeDict objectForKey:@"z"] floatValue];
}
//Finally, add the sprite
[self addChild:sprite z:z];
}
/* Process the 'texturedPolygon' type */
-(void) processTexturedPolygon:(NSDictionary*)nodeDict {
//Process vertices
NSMutableArray *vertices = [[[NSMutableArray alloc] init] autorelease];
NSArray *vertexData = [nodeDict objectForKey:@"vertices"];
for(id vData in vertexData){
float x = [[vData objectForKey:@"x"] floatValue];
float y = [[vData objectForKey:@"y"] floatValue];
[vertices addObject:[NSValue valueWithCGPoint:ccp(x,y)]];
}
//Init our textured polygon
NSString *file = [nodeDict objectForKey:@"file"];
ccTexParams params = {GL_NEAREST,GL_NEAREST_MIPMAP_NEAREST,GL_REPEAT,GL_REPEAT};
TexturedPolygon *texturedPoly = [TexturedPolygon createWithFile:file withVertices:vertices];
[texturedPoly.texture setTexParameters:&params];
[texturedPoly retain];
//Set position
NSDictionary *posDict = [nodeDict objectForKey:@"position"];
texturedPoly.position = ccp([[posDict objectForKey:@"x"] floatValue], [[posDict objectForKey:@"y"] floatValue]);
//Add the texturedPolygon behind any sprites
[self addChild:texturedPoly z:0];
}
/* Our simple method used to order sprites by depth */
-(float) getZFromY:(float)y {
return 320-y;
}
@end

它是如何工作的...

从 PLIST 文件中加载数据是一种在程序内存中创建复杂数据结构的无缝方式。在这里,我们加载 scene1.plist,它包含一个字典数组。这相当于一个 NSArrayNSDictionary 值数组。在每一个字典中,我们有一个键为 'type' 的字符串值。这告诉应用程序它在查看什么类型的节点。PLIST 数据格式可以容纳无限深的数组和字典组合,最终包含包括布尔值、数据、日期、数字和字符串在内的原始数据类型。每一个都可以轻松转换为 NSNumberNSDataNSDateNSString。以下是我们的 PLIST 文件看起来像:

如何工作...

PLIST 文件仅是一个使用特定约定解析的 XML 文件。前面的图是 XML 数据的图形表示。您将在后面的示例中看到,数组和字典的组合是存储数据的标准。它们最终包含包括布尔值、数据、日期、数字和字符串在内的原始数据类型。每一个都可以轻松转换为 NSNumberNSDataNSDateNSString。以下是我们的 PLIST 文件看起来像:

  • 使用 getActualPath:

    getActualPath: 方法提供了一个快速获取包资源完整文件路径的途径。这允许需要精确路径的类在文件系统中操作文件。

  • 等距场景:

    如您所见,我们的场景有一些深度和阴影。这种技术是等距投影的模拟。这是一个没有消失点的模拟 3D 空间。它被用于无数 2D 游戏,并将成为本书中更多菜谱的主要特性。

读取 JSON 数据文件

JSON 代表 JavaScript 对象表示法。它是一种非常轻量且易于消费的数据打包方式。多亏了 CJSONDeserializer 库,读取 JSON 文件就像读取 PLIST 文件一样简单。在下面的场景中,我们看到一个有猫和几棵树的草地:

读取 JSON 数据文件

准备工作

请参考项目 RecipeCollection01 以获取此菜谱的完整工作代码。

如何操作...

执行以下代码:

#import "ActualPath.h"
@implementation Ch2_ReadingJsonFiles
-(CCLayer*) runRecipe {
//Initialize a read-only dictionary from our file
NSString *fileName = @"scene2.json";
NSString *jsonString = [[[NSString alloc] initWithContentsOfFile:getActualPath(fileName) encoding:NSUTF8StringEncoding error:nil] autorelease];
NSData *jsonData = [jsonString dataUsingEncoding:NSUTF32BigEndianStringEncoding];
NSDictionary *dict = [[CJSONDeserializer deserializer] deserializeAsDictionary:jsonData error:nil];
//Process this dictionary
[self processMap:dict];
return self;
}
-(void) processMap:(NSDictionary*)dict {
NSArray *nodes = [dict objectForKey:@"nodes"];
for (id node in nodes) {
if([[node objectForKey:@"type"] isEqualToString:@"spriteFile"]){
[self processSpriteFile:node];
}else if([[node objectForKey:@"type"] isEqualToString:@"texturedPolygon"]){
[self processTexturedPolygon:node];
}
}
}
/* Process the 'spriteFile' type */
-(void) processSpriteFile:(NSDictionary*)nodeDict {
/* CODE OMITTED */
}
/* Process the 'texturedPolygon' type */
-(void) processTexturedPolygon:(NSDictionary*)nodeDict {
/* CODE OMITTED */
}
/* Our simple method used to order sprites by depth */
-(float) getZFromY:(float)y {
return 320-y;
}
@end

它是如何工作的...

将 JSON 加载到只读 NSDictionary 中相当直接。以下是我们的 JSON 文件,其中省略了一些行:

{ "nodes":
[ { "type":"spriteFile", "file":"tree.png", "position":{"x":250,"y":50}, "scale":0.9 },
{ "type":"spriteFile", "file":"tree_shadow.png", "position":{"x":195,"y":51}, "scale":0.9, "z":-100 },
{ "type":"spriteFile", "file":"cheshire_cat.png", "position":{"x":120,"y":70}, "scale":0.3 },
{ "type":"spriteFile", "file":"actor_shadow.png", "position":{"x":120,"y":65}, "scale":1.75, "z":-100 },
{ "type":"texturedPolygon", "file":"grass_texture.png", "position":{"x":16,"y":16},
"vertices":[{"x":0,"y":0},{"x":480,"y":0},{"x":480,"y":320},{"x":0,"y":320}] },
{ "type":"rectangle", "position":{"x":0,"y":0}, "size":{"x":480,"y":320}, "meta": [{"type":"boundary"}] }
]
}

如你所见,JSON 格式非常简洁。一眼看去,它比 XML 更容易理解。

读取 XML 数据文件

最后,我们有了大家最喜欢的数据格式:基本的未加工 XML。对于这个菜谱,我们将使用 Google 的 GDataXML 库来读取和解析一个简单的 XML 文档。在下面的场景中,我们看到有岩石地形和一些巨石和杂草:

读取 XML 数据文件

准备工作

请参考项目 RecipeCollection01 以获取此菜谱的完整工作代码。

如何操作...

我们需要做的第一件事是集成 Google 的 GData XML 工具:

  1. 从这里下载并解压 gdata-objectivec-clientcode.google.com/p/gdata-objectivec-client/downloads/list

  2. Source\XMLSupport 文件夹中找到 GDataXMLNode.hGDataXMLNode.m 文件,并将它们添加到你的项目中。

  3. 在你的 Project Navigator 中点击你的 Project

  4. 在此右侧,点击你的目标

  5. 前往 Build Settings 选项卡。

  6. 找到 Search Paths\Header Search Paths 设置。如何操作...

  7. /usr/include/libxml2 添加到列表中。如何操作...

  8. 找到 Linking\Other Linker Flags 部分。如何操作...

  9. -lxml2 添加到列表中。如何操作...

  10. GDataXMLNode.h 导入到你的代码中。如果它能编译并运行,那么你就已成功集成了 GDataXML。

现在,执行以下代码:

#import "GDataXMLNode.h"
@implementation Ch2_ReadingXmlFiles
-(CCLayer*) runRecipe {
//Read our file in as an NSData object
NSString *fileName = @"scene3.xml";
NSString *xmlString = [[[NSString alloc] initWithContentsOfFile:getActualPath(fileName) encoding:NSUTF8StringEncoding error:nil] autorelease];
NSData *xmlData = [xmlString dataUsingEncoding:NSUTF32BigEndianStringEncoding];
//Initialize a new GDataXMLDocument with our data
GDataXMLDocument *doc = [[[GDataXMLDocument alloc] initWithData:xmlData options:0 error:nil] autorelease];
//Process that document
[self processMap:doc];
return self;
}
-(void) processMap:(GDataXMLDocument*)doc {
//Find all elements of 'node' type
NSArray *nodes = [doc.rootElement elementsForName:@"node"];
//Loop through each element
for (GDataXMLElement *node in nodes) {
//Find the first (and assumed only) element with the name 'type' in this node
NSString *type = [[[node elementsForName:@"type"] objectAtIndex:0] stringValue];
//Process specific node types
if([type isEqualToString:@"spriteFile"]){
[self processSpriteFile:node];
}else if([type isEqualToString:@"texturedPolygon"]){
[self processTexturedPolygon:node];
}
}
}
/* Process the 'spriteFile' type */
-(void) processSpriteFile:(GDataXMLElement*)node {
//Init the sprite
NSString *file = [[[node elementsForName:@"file"] objectAtIndex:0] stringValue];
CCSprite *sprite = [CCSprite spriteWithFile:file];
//Set sprite position
GDataXMLElement *posElement = [[node elementsForName:@"position"] objectAtIndex:0];
sprite.position = ccp( [[[[posElement elementsForName:@"x"] objectAtIndex:0] stringValue] floatValue],
[[[[posElement elementsForName:@"y"] objectAtIndex:0] stringValue] floatValue]);
//Each element is considered a string first
sprite.scale = [[[[node elementsForName:@"scale"] objectAtIndex:0] stringValue] floatValue];
//Set the anchor point
sprite.anchorPoint = ccp(0.5,0);
//We set the sprite Z according to its Y to produce an isometric perspective
float z = [self getZFromY:sprite.position.y];
if([node elementsForName:@"z"].count > 0){
z = [[[[node elementsForName:@"z"] objectAtIndex:0] stringValue] floatValue];
}
//Finally, add the sprite
[self addChild:sprite z:z];
}
/* Process the 'texturedPolygon' type */
-(void) processTexturedPolygon:(GDataXMLElement*)node {
//Process vertices
NSMutableArray *vertices = [[[NSMutableArray alloc] init] autorelease];
NSArray *vertexData = [[[node elementsForName:@"vertices"] objectAtIndex:0] elementsForName:@"vertex"];
for(id vData in vertexData){
GDataXMLElement *vertexElement = (GDataXMLElement*)vData;
float x = [[[[vertexElement elementsForName:@"x"] objectAtIndex:0] stringValue] floatValue];
float y = [[[[vertexElement elementsForName:@"y"] objectAtIndex:0] stringValue] floatValue];
[vertices addObject:[NSValue valueWithCGPoint:ccp(x,y)]];
}
//Init our textured polygon
NSString *file = [[[node elementsForName:@"file"] objectAtIndex:0] stringValue];
ccTexParams params = {GL_NEAREST,GL_NEAREST_MIPMAP_NEAREST,GL_REPEAT,GL_REPEAT};
TexturedPolygon *texturedPoly = [TexturedPolygon createWithFile:file withVertices:vertices];
[texturedPoly.texture setTexParameters:&params];
//Set position
GDataXMLElement *posElement = [[node elementsForName:@"position"] objectAtIndex:0];
texturedPoly.position = ccp( [[[[posElement elementsForName:@"x"] objectAtIndex:0] stringValue] floatValue],
[[[[posElement elementsForName:@"y"] objectAtIndex:0] stringValue] floatValue]);
//Add the texturedPolygon behind any sprites
[self addChild:texturedPoly z:0];
}
/* Our simple method used to order sprites by depth */
-(float) getZFromY:(float)y {
return 320-y;
}
@end

它是如何工作的...

读取和处理 XML 文件与处理 PLIST 和 JSON 文件没有太大区别。在这种情况下,我们使用 GDataXMLDocumentGDataXMLElement 类。后者实现了 (NSString*)stringValue 方法,可以从其他值中解析出来。以下是我们在处理的 XML 文档的摘录:

<?xml version="1.0" encoding="ISO-8859-1"?>
<nodes>
<node>
<type>spriteFile</type>
<file>boulder.png</file>
<position><x>250</x><y>50</y></position>
<scale>0.9</scale>
</node>
<node>
<type>texturedPolygon</type>
<file>cracked_earth_texture.png</file>
<position> <x>32</x><y>32</y> </position>
<vertices>
<vertex> <x>0</x><y>0</y> </vertex>
<vertex> <x>480</x><y>0</y> </vertex>
<vertex> <x>480</x><y>320</y> </vertex>
<vertex> <x>0</x><y>320</y> </vertex>
</vertices>
</node>
</nodes>

纯 XML 比 PLIST 文件更难阅读,并且比 JSON 包含更多的标记语言冗余。然而,XML 允许使用 JSON 中缺失的两个功能:属性命名空间。属性可以用来提供有关 元素 的额外信息。命名空间可以用来帮助减少元素之间的歧义。

相关信息...

更多关于 XML 规范的信息可以在:www.w3.org/TR/xml/ 找到

使用 NSUserDefaults 保存简单数据

为了持久化用户设置和其他一些小数据,iOS 框架提供了 NSUserDefaults 类。在这个例子中,我们正在保存我们游戏的默认难度级别。

使用 NSUserDefaults 保存简单数据

准备工作

请参考项目 RecipeCollection01 以获取此菜谱的完整工作代码。

如何操作...

执行以下代码:

@implementation Ch2_SavingSimpleData
-(CCLayer*) runRecipe {
//Set font size
[CCMenuItemFont setFontSize:30];
//Add main label
CCLabelBMFont *chooseDifficultyLabel = [CCLabelBMFont labelWithString:@"CHOOSE DIFFICULTY:" fntFile:@"eurostile_30.fnt"];
chooseDifficultyLabel.position = ccp(240,250);
chooseDifficultyLabel.scale = 0.5f;
[self addChild:chooseDifficultyLabel z:1];
//Add difficulty choices
easyMIF = [CCMenuItemFont itemFromString:@"Easy" target:self selector:@selector(chooseEasy)];
/* CODE OMITTED */
mainMenu = [CCMenu menuWithItems:easyMIF, mediumMIF, hardMIF, insaneMIF, nil];
[mainMenu alignItemsVertically];
mainMenu.position = ccp(240,140);
[self addChild:mainMenu z:1];
//Load any previously chosen difficulty
[self loadDifficulty];
return self;
}
-(void) loadDifficulty {
//If a difficulty is set we use that, otherwise we choose Medium
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if([defaults stringForKey:@"simple_data_difficulty"]){
difficulty = [defaults stringForKey:@"simple_data_difficulty"];
[self setDifficultyFromValue];
}else{
[self chooseMedium];
}
}
-(void) saveDifficulty {
//Save our difficulty
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:difficulty forKey:@"simple_data_difficulty"];
[defaults synchronize];
}
-(void) setDifficultyFromValue {
//More menu color management
[self resetMenuColors];
if([difficulty isEqualToString:@"Easy"]){
[easyMIF setColor:ccc3(255,0,0)];
}else if([difficulty isEqualToString:@"Medium"]){
[mediumMIF setColor:ccc3(255,0,0)];
}/* CODE OMITTED */
[self saveDifficulty];
}
/* Shortcut callback methods */
-(void) chooseEasy {
difficulty = @"Easy";
[self setDifficultyFromValue];
}
/* CODE OMITTED */
@end

它是如何工作的...

NSUserDefaults 类使用与 PLIST 类似的格式。它可以接受包括 NSString, NSData, NSNumber 以及其他类型的对象。它也可以接受数组和字典。

  • 加载数据:

    当加载 NSUserDefaults 数据时,以下这些关键行需要记住:

    if([defaults stringForKey:@"simple_data_difficulty"]){
    difficulty = [defaults stringForKey:@"simple_data_difficulty"];
    }
    
    

    if 语句简单地检查该条目是否已经存在。

  • 保存数据:

    保存数据的过程也非常直接:

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    [defaults setObject:difficulty forKey:@"simple_data_difficulty"];
    [defaults synchronize];
    
    

参见...

在这个菜谱中,我们简要地看了菜单自定义。我们将在 第四章 中详细讨论这个主题。

将对象归档到归档文件中

NSKeyedArchiverNSKeyedUnarchiver 允许我们以非常 面向对象 的方式持久化数据。通过遵守 NSCoding 协议,我们可以告诉归档器如何打包和解包我们任何类。在这个菜谱中,我们将打包一个具有许多龙与地下城风格属性的角色的数据。

将对象归档到归档文件中

准备工作

请参考项目 RecipeCollection01 以获取此菜谱的完整工作代码。同时请注意,为了简洁,以下代码中省略了一些部分。

如何做到...

执行以下代码:

//SimpleCharacter.h
@interface SimpleCharacter : NSObject <NSCoding> {
NSString *charColor; NSString *charClass;
int strength; int dexterity; int constitution;
int intelligence; int wisdom; int charisma;
}
@property (readwrite, assign) NSString *charColor;
@property (readwrite, assign) NSString *charClass;
@property (readwrite, assign) int strength;
/* CODE OMITTED */
@end
@implementation SimpleCharacter
@synthesize charColor, charClass, strength, dexterity, constitution, intelligence, wisdom, charisma;
/* This merely adds this character with the proper color to a CCNode */
-(void) addCharacterToNode:(CCNode *)node atPosition:(CGPoint)position {
ccColor3B color;
if([charColor isEqualToString:@"Red"]){
color = ccc3(255,0,0);
}else if([charColor isEqualToString:@"Blue"]){
color = ccc3(0,0,255);
}/* CODE OMITTED */
CCSpriteFrameCache *cache = [CCSpriteFrameCache sharedSpriteFrameCache];
[cache addSpriteFramesWithFile:@"dnd_characters.plist"];
CCSprite *drawing = [CCSprite spriteWithSpriteFrame:[cache spriteFrameByName:[NSString stringWithFormat:@"dnd_%@_drawing.png",charClass]]];
CCSprite *colors = [CCSprite spriteWithSpriteFrame:[cache spriteFrameByName:[NSString stringWithFormat:@"dnd_%@_colors.png",charClass]]];
drawing.position = position; colors.position = position;
drawing.scale = 1.5f; colors.scale = 1.5f;
colors.color = color;
[node addChild:colors z:0 tag:0];
[node addChild:drawing z:1 tag:1];
}
/* This method determines how data is encoded into an NSCoder object */
- (void) encodeWithCoder: (NSCoder *)coder {
[coder encodeObject:charColor];
[coder encodeObject:charClass];
[coder encodeObject:[NSNumber numberWithInt:strength]];
/* CODE OMITTED */
}
/* This method determines how data is read out from an NSCode object */
-(id) initWithCoder: (NSCoder *) coder {
[super init];
charColor = [[coder decodeObject] retain];
charClass = [[coder decodeObject] retain];
strength = [[coder decodeObject] intValue];
/* CODE OMITTED */
return self;
}
/* Initialization */
-(id) init {
self = [super init];
if (self) {
charColor = @"Red"; charClass = @"Wizard";
strength = 10; dexterity = 10; constitution = 10;
intelligence = 10; wisdom = 10; charisma = 10;
}
return self;
}
/* All objects must be released here */
- (void) dealloc {
[charColor release]; [charClass release]; [super dealloc];
}
@end
@implementation Ch2_ArchivingObjects
-(CCLayer*) runRecipe {
//Load our character
[self loadCharacter];
return self;
}
-(void) loadCharacter {
//Our archive file name
NSString *fileName = @"dnd_character.archive";
//We get our file path
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:fileName];
if(![[NSFileManager defaultManager] fileExistsAtPath:filePath]){
//If file doesn't exist in document directory create a new default character and save it
character = [[SimpleCharacter alloc] init];
[NSKeyedArchiver archiveRootObject:character toFile:filePath];
}else{
//If it does we load it
character = [[NSKeyedUnarchiver unarchiveObjectWithFile:filePath] retain];
}
//Add character and reload HUD
[character addCharacterToNode:self atPosition:ccp(300,180)];
[self loadHUD];
}
-(void) saveCharacter {
//Our archive file name
NSString *fileName = @"dnd_character.archive";
//We get our file path
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:fileName];
//Save character
[NSKeyedArchiver archiveRootObject:character toFile:filePath];
}
-(void) deleteData {
//Our archive file name
NSString *fileName = @"dnd_character.archive";
//We get our file path
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:fileName];
//Delete our file
[[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
//Set removal message
[message setString:@"Data deleted!"];
//Remove character node and load a new default character
[self removeCharacter];
[self loadCharacter];
}
@end

它是如何工作的...

正确地 归档 一个对象需要几个步骤。

  • 遵守 NSCoding 协议:

    NSCoding 协议要求我们实现以下两个方法:

    - (void) encodeWithCoder: (NSCoder *)coder;
    -(id) initWithCoder: (NSCoder *) coder;
    
    

    一个方法将数据打包到 NSCoder 对象中,另一个方法从它解包数据。

  • 使用 Documents 目录:

    您的应用程序对 iOS 框架指定的区域有写访问权限。这些包括 DocumentsLibrary 目录。在本例和未来的示例中,我们可能会从应用程序包内的多个位置读取,但我们通常只会写入 Documents 目录。与磁盘上的其他区域不同,在此处保存的文件在应用程序升级时将得到保留。通常,我们会在以下代码中定位 Documents 目录内的文件:

    NSString *fileName = @"my.file";
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString *filePath = [documentsDirectory stringByAppendingPathComponent:fileName];
    
    

    这将返回绝对文件路径。

  • 使用 NSFileManager:

    我们还使用 NSFileManager 来确定文件是否存在,并在必要时删除文件。

    if([[NSFileManager defaultManager] fileExistsAtPath:filePath]){
    [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
    }
    
    
  • 使用 NSKeyedArchiverNSKeyedUnarchiver:

    最后,有了这些工具在手,我们可以使用 NSKeyedArchiverNSKeyedUnarchiver 类来归档和解档对象:

    //Archive
    character = [[SimpleCharacter alloc] init];
    [NSKeyedArchiver archiveRootObject:character toFile:filePath];
    //Un-archive
    character = [[NSKeyedUnarchiver unarchiveObjectWithFile:filePath] retain];
    
    

修改嵌套元数据

数据文件中的数据通常被加载到一个不可变的、嵌套的数组和大字典结构中。这个不可变结构使得数据不可编辑。在这个菜谱中,我们将读取一个嵌套的 JSON 数据结构,然后使用可变数据结构递归地重新创建数据,以便允许编辑数据。

修改嵌套元数据

准备工作

请参考项目RecipeCollection01以获取此菜谱的完整工作代码。

如何做...

执行以下代码:

#import "GameHelper.h"
//Implementation
@implementation Ch3_MutatingNestedMetadata
-(CCLayer*) runRecipe {
[super runRecipe];
//Load JSON data
NSString *fileName = @"data_to_mutate.json";
NSString *jsonString = [[[NSString alloc] initWithContentsOfFile:getActualPath(fileName) encoding:NSUTF8StringEncoding error:nil] autorelease];
NSData *jsonData = [jsonString dataUsingEncoding:NSUTF32BigEndianStringEncoding];
NSDictionary *dict = [[CJSONDeserializer deserializer] deserializeAsDictionary:jsonData error:nil];
//Create deep mutable copy
dictMutable = [GameHelper makeRecMutableCopy:dict];
[dictMutable retain];
//Show JSON data
[self showJsonData:dictMutable];
//Add randomize button
[CCMenuItemFont setFontSize:30];
CCMenuItemFont *randomizeItem = [CCMenuItemFont itemFromString:@"Randomize Data" target:self selector:@selector(randomizeData)];
CCMenu *menu = [CCMenu menuWithItems:randomizeItem, nil];
menu.position = ccp(240,140);
[self addChild:menu z:1];
return self;
}
-(void) showJsonData:(NSDictionary*)dict {
[self showMessage:@""];
//Loop through all dictionary nodes to process individual types
NSMutableDictionary *nodes = [dict objectForKey:@"people"];
for (NSMutableDictionary* node in nodes) {
float height = [[node objectForKey:@"height"] floatValue];
float weight = [[node objectForKey:@"weight"] floatValue];
NSString *name = [node objectForKey:@"name"];
[self appendMessage:[NSString stringWithFormat:@"%@: %din %dlbs", name, (int)height, (int)weight]];
}
nested metadatamutating}
-(void) randomizeData {
//Randomize some data in 'dictMutable'
NSMutableArray *nodes = [dictMutable objectForKey:@"people"];
for (NSMutableDictionary* node in nodes) {
[node setObject:[NSNumber numberWithFloat:(float)(arc4random()%48)+30.0f] forKey:@"height"];
[node setObject:[NSNumber numberWithFloat:(float)(arc4random()%100)+100.0f] forKey:@"weight"];
}
[self showJsonData:dictMutable];
}
@end

它是如何工作的...

就像在前一节中,读取 JSON 数据文件,我们首先从 JSON 文件中读取数据。数据由一个嵌套数组、字典和字符串的NSDictionary对象组成。为了创建一个“深层”可变副本,我们在GameHelper类中调用以下方法:

+(NSMutableDictionary*) makeRecMutableCopy:(NSDictionary*)dict;

此方法假设根节点是一个字典。

  • makeRecMutableCopy方法:

    在这个菜谱中,我们省略了此方法的恐怖细节。简单地说,它递归地遍历嵌套结构,将NSDictionary对象转换为NSMutableDictionary对象,将NSArray对象转换为NSMutableArray对象。

  • 修改数据树:

    一旦我们有一个嵌套的可变结构,我们就可以访问它并根据需要修改元素。点击随机化数据按钮以随机更改结构中的某些数据。

将数据保存到 PLIST 文件中

对于接下来的三个菜谱,我们有三个需要持久化其最高分的小游戏。在这个菜谱中,我们看到了一个打地鼠游戏,它将使用 PLIST 文件来维护一个最高分列表。

将数据保存到 PLIST 文件中

准备工作

请参考项目RecipeCollection01以获取此菜谱的完整工作代码。为了简洁起见,以下代码中省略了所有游戏逻辑。

如何做...

执行以下代码:

#import "ActualPath.h"
@implementation Ch2_SavingDataPlist
-(CCLayer*) runRecipe {
[self loadHiScores];
return self;
}
-(void) loadHiScores {
//Our template and file names
NSString *templateName = @"whackamole_template.plist";
NSString *fileName = @"whackamole.plist";
//Our dictionary
NSMutableDictionary *fileDict;
//We get our file path
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:fileName];
if(![[NSFileManager defaultManager] fileExistsAtPath:filePath]){
//If file doesn't exist in document directory create a new one from the template
fileDict = [NSMutableDictionary dictionaryWithContentsOfFile:getActualPath(templateName)];
}else{
//If it does we load it in the dict
fileDict = [NSMutableDictionary dictionaryWithContentsOfFile:filePath];
}
//Load hi scores into our dictionary
hiScores = [fileDict objectForKey:@"hiscores"];
//Set the 'hiScore' variable (the highest score)
for(id score in hiScores){
int scoreNum = [[score objectForKey:@"score"] intValue];
if(hiScore < scoreNum){
hiScore = scoreNum;
}
}
//Write dict to file
[fileDict writeToFile:filePath atomically:YES];
}
-(void) addHiScore {
//Our template and file names
NSString *templateName = @"whackamole_template.plist";
NSString *fileName = @"whackamole.plist";
//Our dictionary
NSMutableDictionary *fileDict;
//We get our file path
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:fileName];
if(![[NSFileManager defaultManager] fileExistsAtPath:filePath]){
//If file doesn't exist in document directory create a new one from the template
fileDict = [NSMutableDictionary dictionaryWithContentsOfFile:getActualPath(templateName)];
}else{
//If it does we load it in the dict
fileDict = [NSMutableDictionary dictionaryWithContentsOfFile:filePath];
}
//Load hi scores into our dictionary
hiScores = [fileDict objectForKey:@"hiscores"];
//Add hi score
bool scoreRecorded = NO;
//Add score if player's name already exists
for(id score in hiScores){
NSMutableDictionary *scoreDict = (NSMutableDictionary*)score;
if([[scoreDict objectForKey:@"name"] isEqualToString:currentPlayerName]){
if([[scoreDict objectForKey:@"score"] intValue] < currentScore){
[scoreDict setValue:[NSNumber numberWithInt:currentScore] forKey:@"score"];
}
scoreRecorded = YES;
}
}
//Add new score if player's name doesn't exist
if(!scoreRecorded){
NSMutableDictionary *newScore = [[NSMutableDictionary alloc] init];
[newScore setObject:currentPlayerName forKey:@"name"];
[newScore setObject:[NSNumber numberWithInt:currentScore] forKey:@"score"];
[hiScores addObject:newScore];
}
//Write dict to file
[fileDict writeToFile:filePath atomically:YES];
}
-(void) deleteHiScores {
//Our file name
NSString *fileName = @"whackamole.plist";
//We get our file path
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:fileName];
//Delete our file
[[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
[message setString:@"Hi scores deleted!"];
hiScore = 0;
[self loadHiScores];
}
@end

它是如何工作的...

我们首先做的事情是检查Documents目录中的whackamole.plist文件,并将该数据加载到fileDict字典中。如果我们找不到该文件,我们将从Resources/Data文件夹中打开whackamole_template.plist。它看起来像这样:

它是如何工作的...

无论我们加载哪个文件,加载行看起来都像这样:

fileDict = [NSMutableDictionary dictionaryWithContentsOfFile:filePath];

在修改fileDict字典后,我们将它保存到Documents目录下的whackamole.plist中:

[fileDict writeToFile:filePath atomically:YES];

考虑到所有因素,这是一个非常简单的方式来持久化数据。

将数据保存到 SQLite 数据库中

我们第二个游戏是一个“飞碟射击”游戏,飞碟被射入空中,目标是尽可能在规定时间内射下尽可能多的飞碟。对于这个游戏,我们将使用 SQLite 数据库来持久化最高分数据。我们将使用FMDB Objective-C SQLite 包装器来访问代码中的 SQLite 数据库,并使用Firefox 插件 SQLite Manager来创建初始数据库文件。

将数据保存到 SQLite 数据库中

准备工作

请参考项目RecipeCollection01以获取此菜谱的完整工作代码。为了简洁起见,以下代码中省略了所有游戏逻辑。

如何做...

要使用 SQLite,我们首先需要做一些事情:

  1. 首先,我们需要添加libsqlite3.0.dylib框架。你可以通过右键单击你的项目,然后转到添加 > 已存在的框架,然后在iOS 4.x SDK下选择libsqlite3.0.dylib

  2. 接下来,我们需要将 FMDB Objective-C SQLite 包装器添加到我们的项目中。FMDB 可以从这里下载:github.com/ccgus/fmdb

  3. 从这里下载并安装 Firefox 插件 SQLite Manager:addons.mozilla.org/en-US/firefox/addon/sqlite-manager/

现在我们需要创建一个新的 SQLite 数据库作为我们的默认数据库模板。进入 SQLite Manager 并转到 数据库 > 新数据库。对于我们的项目,我们创建了 skeetshooter_template.sqlite,其中包含一个名为 hiscores 的表,该表具有 namescore 字段:

如何操作...

一旦我们有了数据库模板,执行以下代码:

#import "ActualPath.h"
#import <Foundation/Foundation.h>
#import "FMDatabase.h"
@implementation Ch2_SavingDataSQLite
-(CCLayer*) runRecipe {
[self loadHiScores];
return self;
}
-(void) dealloc {
//Release our database
[db close]; [db release]; [super dealloc];
}
-(NSArray *) createDictionariesArrayFromFMResultSet:(FMResultSet *)rs fields:(NSString *)fields {
//Parse field string into an array
NSArray * listFields = [fields componentsSeparatedByString:@","];
//Create an array of dictionaries from each field
NSMutableArray * items = [NSMutableArray arrayWithCapacity:1];
while ([rs next]) {
NSMutableDictionary * item = [NSMutableDictionary dictionaryWithCapacity:1];
for (int i = 0; i < [listFields count]; i++) {
NSString * key = [listFields objectAtIndex:i];
NSString * value = [rs stringForColumn: key];
if (value == NULL) value = @"";
[item setObject:value forKey:key];
}
[items addObject:item];
}
[rs close];
return items;
}
-(void) writeNewScore:(int)score forName:(NSString*)name {
//Find the hi score with this name
NSString *selectQuery = [NSString stringWithFormat:@"SELECT * FROM hiscores WHERE name = '%@'", name];
FMResultSet *rs = [db executeQuery:selectQuery];
//What is the score? Is there a score at all?
int storedScore = -1;
while([rs next]){
storedScore = [[rs stringForColumn:@"score"] intValue];
}
[rs close];
if(storedScore == -1){
//Name doesn't exist, add it
NSString *insertQuery = [NSString stringWithFormat:@"INSERT INTO hiscores (name, score) VALUES ('%@','%i')", name, score];
rs = [db executeQuery:insertQuery];
while([rs next]){};
[rs close];
}else if(score > storedScore){
//Write new score for existing name
NSString *updateQuery = [NSString stringWithFormat:@"UPDATE hiscores SET score='%i' WHERE name='%@'", score, name];
rs = [db executeQuery:updateQuery];
while([rs next]){};
[rs close];
}
}
-(void) loadHiScores {
//Our file and template names
NSString *fileName = @"skeetshooter.sqlite";
NSString *templateName = @"skeetshooter_template.sqlite";
//We get our file path
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:fileName];
//If file doesn't exist in document directory create a new one from the template
if(![[NSFileManager defaultManager] fileExistsAtPath:filePath]){
[[NSFileManager defaultManager] copyItemAtPath:getActualPath(templateName)
toPath:[NSString stringWithFormat:@"%@/%@", documentsDirectory, fileName] error:nil];
}
//Initialize the database
if(!db){
db = [FMDatabase databaseWithPath:filePath];
[db setLogsErrors:YES];
[db setTraceExecution:YES];
[db retain];
if(![db open]){ NSLog(@"Could not open db.");
}else{ NSLog(@"DB opened successfully.");}
}
//Select all hi scores
FMResultSet *rs = [db executeQuery:@"select * from hiscores"];
//Load them into an array of dictionaries
hiScores = [[NSMutableArray alloc] init];
hiScores = [self createDictionariesArrayFromFMResultSet:rs fields:@"name,score"];
//Set hi score
for(id score in hiScores){
int scoreNum = [[score objectForKey:@"score"] intValue];
if(hiScore < scoreNum){
hiScore = scoreNum;
}
}
}
-(void) addHiScore {
//Add hi score to db
[self writeNewScore:currentScore forName:currentPlayerName];
//Reset dictionary
FMResultSet *rs = [db executeQuery:@"SELECT * FROM hiscores"];
hiScores = [self createDictionariesArrayFromFMResultSet:rs fields:@"name,score"];
}
-(void) deleteHiScores {
//Our file name
NSString *fileName = @"skeetshooter.sqlite";
//We get our file path
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:fileName];
//Delete our file
[[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
[message setString:@"Hi scores deleted!"];
hiScore = 0;
//Close and release our db pointer
[db close]; [db release]; db = nil;
//Load new blank hi scores
[self loadHiScores];
}
@end

它是如何工作的...

将数据保存到 SQLite 数据库与使用 PLIST 文件略有不同。读取和写入 SQLite 数据库的主要方式是通过以下方法:

- (FMResultSet *)executeQuery:(NSString*)sql;

这将返回一个 FMResultSet 对象,然后可以遍历:

FMResultSet *rs = [db executeQuery:@"SELECT * FROM mytable"];
while([rs next]){
somevalue = [[rs stringForColumn:@"myfield"] intValue];
}
[rs close];

确保迭代你创建的每个 FMResultSet 对象,并调用 [rs close],否则 SQLite 将会抛出错误。

  • 查询你的 SQLite 数据库:

    使用这个简单但强大的界面,你可以执行在 SQLite 数据库上通常允许的任何类型的查询。在这个菜谱中,我们有一个包含两个值的表。请随意尝试更复杂的数据模型。

使用 Core Data 保存数据

我们的第三个游戏是一个记忆卡牌游戏,你必须翻过一组组卡片。翻到两张匹配的卡片,你得到一分。翻到两张不匹配的卡片,你得到一次失误。三次失误,你就出局了。对于这个游戏,我们使用苹果的 Core Data 模式来持久化高分数据。

使用 Core Data 保存数据

准备工作

请参考项目 RecipeCollection01 以获取此菜谱的完整工作代码。为了简洁起见,以下代码中省略了所有游戏逻辑。

如何操作...

设置基于 Core Data 的菜谱需要多个步骤。

首先,我们需要添加 CoreData 框架。你可以通过在项目上右键单击并选择 添加 > 已存在框架 来完成此操作,然后在 iOS 4.x SDK 下选择 CoreData.framework

  1. 在你的资源中创建一个名为 Data Model 的新文件夹。在这个文件夹内,创建一个名为 Hiscore 的类,它继承自 ManagedObject:

    //Hiscore.h
    #import <CoreData/CoreData.h>
    @interface Hiscore : NSManagedObject
    {}
    @property (nonatomic, retain) NSString * name;
    @property (nonatomic, retain) NSNumber * score;
    @end
    //Hiscore.m
    #import "Hiscore.h"
    @implementation Hiscore
    @dynamic name;
    @dynamic score;
    @end
    
    
  2. 添加此类后,右键单击项目并选择 添加 > 新文件。在 iOS资源 下选择并创建一个新的 数据模型如何操作...

  3. 将此数据模型命名为 hiscore,并在提示时将现有的 Hiscore 类添加到该数据模型中。如何操作...

  4. 在 XCode 中单击 hiscores.xcdatamodel。单击 Hiscore 实体,然后向其中添加两个新属性,这两个属性对应于 Hiscore 类中的两个属性:

如何操作...

现在,我们已经准备好开始编写代码。执行以下操作:

#import <UIKit/UIKit.h>
#import "Hiscore.h"
@interface Ch2_SavingDataCoreData : SimpleTimedGameRecipe <NSFetchedResultsControllerDelegate>
{
NSManagedObjectModel *managedObjectModel;
NSManagedObjectContext *managedObjectContext;
NSPersistentStoreCoordinator *persistentStoreCoordinator;
}
@property (nonatomic, retain, readonly) NSManagedObjectModel *managedObjectModel;
@property (nonatomic, retain, readonly) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, retain, readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator;
@end
@implementation Ch2_SavingDataCoreData
-(CCLayer*) runRecipe {
[self loadHiScores];
return self;
}
/* Returns the managed object context for the application.
If the context doesn't already exist, it is created and bound to the persistent store coordinator for the application. */
- (NSManagedObjectContext *) managedObjectContext {
//Return the managedObjectContext if it already exists
if (managedObjectContext != nil) {
return managedObjectContext;
}
//Init the managedObjectContext
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
managedObjectContext = [[NSManagedObjectContext alloc] init];
[managedObjectContext setPersistentStoreCoordinator: coordinator];
}
return managedObjectContext;
}
/* Returns the managed object model for the application.
If the model doesn't already exist, it is created by merging all of the models found in the application bundle. */
- (NSManagedObjectModel *)managedObjectModel {
//Return the managedObjectModel if it already exists
if (managedObjectModel != nil) {
return managedObjectModel;
}
//Init the managedObjectModel
managedObjectModel = [[NSManagedObjectModel mergedModelFromBundles:nil] retain];
return managedObjectModel;
}
/* Returns the persistent store coordinator for the application.
If the coordinator doesn't already exist, it is created and the application's store added to it. */
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
//Return the persistentStoreCoordinator if it already exists
if (persistentStoreCoordinator != nil) {
return persistentStoreCoordinator;
}
//Our file name
NSString *fileName = @"memory.sqlite";
//We get our file path
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:fileName];
NSURL *filePathURL = [NSURL fileURLWithPath:filePath];
//Init the persistentStoreCoordinator
persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
[persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:filePathURL options:nil error:nil];
return persistentStoreCoordinator;
}
-(void) loadHiScores {
//Initialization
managedObjectContext = self.managedObjectContext;
//Attempt to create SQLite database
NSEntityDescription *entity;
@try{
//Define our table/entity to use
entity = [NSEntityDescription entityForName:@"Hiscore" inManagedObjectContext:managedObjectContext];
}@catch (NSException *exception){
NSLog(@"Caught %@: %@", [exception name], [exception reason]);
//Copy SQLite template because creation failed
NSString *fileName = @"memory.sqlite";
NSString *templateName = @"memory_template.sqlite";
//File paths
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:fileName];
if(![[NSFileManager defaultManager] fileExistsAtPath:filePath]){
//If file doesn't exist in document directory create a new one from the template
[[NSFileManager defaultManager] copyItemAtPath:getActualPath(templateName)
toPath:[NSString stringWithFormat:@"%@/%@", documentsDirectory, fileName] error:nil];
}
//Finally define our table/entity to use
entity = [NSEntityDescription entityForName:@"Hiscore" inManagedObjectContext:managedObjectContext];
}
//Set up the fetch request
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entity];
//Define how we will sort the records with a descriptor
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"score" ascending:NO];
NSArray *sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
[request setSortDescriptors:sortDescriptors];
[sortDescriptor release];
//Init hiScores
hiScores = [[managedObjectContext executeFetchRequest:request error:nil] mutableCopy];
//Add an intial score if necessary
if(hiScores.count < 1){
currentScore = 0;
currentPlayerName = @"Player1";
[self addHiScore];
hiScores = [[managedObjectContext executeFetchRequest:request error:nil] mutableCopy];
}
//Set the hi score
Hiscore *highest = [hiScores objectAtIndex:0];
hiScore = [highest.score intValue];
}
-(void) addHiScore {
bool hasScore = NO;
//Add score if player's name already exists
for(id score in hiScores){
Hiscore *hiscore = (Hiscore*)score;
if([hiscore.name isEqualToString:currentPlayerName]){
hasScore = YES;
if(currentScore > [hiscore.score intValue]){
hiscore.score = [NSNumber numberWithInt:currentScore];
}
}
}
//Add new score if player's name doesn't exist
if(!hasScore){
Hiscore *hiscoreObj = (Hiscore *)[NSEntityDescription insertNewObjectForEntityForName:@"Hiscore" inManagedObjectContext:managedObjectContext];
[hiscoreObj setName:currentPlayerName];
[hiscoreObj setScore:[NSNumber numberWithInt:currentScore]];
[hiScores addObject:hiscoreObj];
}
//Save managedObjectContext
[managedObjectContext save:nil];
}
-(void) deleteHiScores {
//Delete all Hi Score objects
NSFetchRequest * allHiScores = [[NSFetchRequest alloc] init];
[allHiScores setEntity:[NSEntityDescription entityForName:@"Hiscore" inManagedObjectContext:managedObjectContext]];
[allHiScores setIncludesPropertyValues:NO]; //only fetch the managedObjectID
NSArray * hs = [managedObjectContext executeFetchRequest:allHiScores error:nil];
[allHiScores release];
for (NSManagedObject *h in hs) {
[managedObjectContext deleteObject:h];
}
//Our file name
NSString *fileName = @"memory.sqlite";
//We get our file path
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:fileName];
//Delete our file
[[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
[message setString:@"Hi scores deleted!"];
hiScore = 0;
[hiScores removeAllObjects];
[hiScores release];
hiScores = nil;
//Finally, load clean hi scores
[self loadHiScores];
}
@end

它是如何工作的...

经过一些复杂的初始化后,我们可以使用以下代码来操作一个简单的Hiscore对象数组:

NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entity];
//Define how we will sort the records with a descriptor
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"score" ascending:NO];
NSArray *sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
[request setSortDescriptors:sortDescriptors];
[sortDescriptor release];
//Load hiScores
hiScores = [[managedObjectContext executeFetchRequest:request error:nil] mutableCopy];

修改这些对象中的任何一个后,我们可以使用以下行将它们全部保存:

[managedObjectContext save:nil];

可以使用以下代码添加新条目:

Hiscore *hiscoreObj = (Hiscore *)[NSEntityDescription insertNewObjectForEntityForName:@"Hiscore" inManagedObjectContext:managedObjectContext];
[hiscoreObj setName:currentPlayerName];
[hiscoreObj setScore:[NSNumber numberWithInt:currentScore]];
[hiScores addObject:hiscoreObj];

Core Data 的更复杂的使用或解释超出了本书的范围。本例仅作为使用 Core Data 的工作介绍。

还有更多...

在 XCode 中正确配置 Core Data 有时可能有些棘手。

  • SQLite 数据库创建错误:

    根据情况,Core Data 可能无法从您创建的模型中创建 SQLite 数据库。要解决这个问题,请确保您的xcdatamodel文件和您的NSManagedObject类在磁盘上以及在同一 XCode 组中位于相同的文件夹。如果这不能解决问题,请从项目中删除这些文件,但不要从磁盘上删除,然后重新添加它们。如果这还不行,您还可以手动创建一个 SQLite 数据库。这需要您遵循 Core Data 约定。为了帮助这个过程,我包括了memory_template.sqlite文件。此文件具有正确的 Core Data 数据库结构。如果它无法创建 SQLite 数据库,食谱也将使用此文件。

  • 处理错误:

    在许多示例中,我们指定了error:nil。要实际处理这些错误,请执行以下操作:

    NSError *error = nil;
    if (![hiscore.managedObjectContext save:&error]) {
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    exit(-1);
    }
    
    

使用 Core Data 有时可能显得有些过度。然而,具有复杂底层数据结构的应用程序

第四章:物理学

在本章中,我们将涵盖以下内容:

  • Box2D 设置和调试绘图

  • 创建碰撞响应例程

  • 使用不同形状

  • 拖动和碰撞过滤

  • 操作物理属性

  • 应用冲量

  • 应用力

  • 异步销毁身体

  • 使用关节

  • 创建车辆

  • 角色移动

  • 模拟子弹

  • 模拟和渲染绳子

  • 创建俯视等距游戏引擎

简介

多年来,物理引擎在视频游戏中被用来增加屏幕上动作的真实感。在许多游戏中,物理在游戏玩法中起着至关重要的作用。Cocos2d 随带两个流行的 2D 物理引擎:Box2DChipmunk。在本章中,我们将使用 Box2D 作为首选引擎来解释游戏中物理的常见用法。这里的大多数菜谱都可以轻松修改以使用 Chipmunk 或任何其他类似物理引擎。

Box2D 设置和调试绘图

在我们的第一个物理菜谱中,我们将探索创建 Box2D 项目和设置 Box2D 世界的基础知识。示例创建了一个场景,允许用户创建逼真的 2D 块。

Box2D 设置和调试绘图

准备工作

请参考项目 RecipeCollection02 以获取此菜谱的完整工作代码。

如何操作...

我们需要做的第一件事是使用内置的 Box2D 项目模板创建一个 Box2D 项目:

  1. 前往文件 | 新建项目

  2. 用户模板下点击Cocos2d

  3. 现在,右键点击Cocos2d Box2d 应用程序如何操作...

  4. 点击选择,命名你的项目,然后点击保存

现在,执行以下代码:

#import "Box2D.h"
#import "GLES-Render.h"
//32 pixels = 1 meter
#define PTM_RATIO 32
@implementation Ch4_BasicSetup
-(CCLayer*) runRecipe {
[super runRecipe];
/* Box2D Initialization */
//Set gravity
b2Vec2 gravity;
gravity.Set(0.0f, -10.0f);
//Initialize world
bool doSleep = YES;
world = new b2World(gravity, doSleep);
world->SetContinuousPhysics(YES);
//Initialize debug drawing
m_debugDraw = new GLESDebugDraw( PTM_RATIO );
world->SetDebugDraw(m_debugDraw);
uint32 flags = 0;
flags += b2DebugDraw::e_shapeBit;
m_debugDraw->SetFlags(flags);
//Create level boundaries
[self addLevelBoundaries];
//Add batch node for block creation
CCSpriteBatchNode *batch = [CCSpriteBatchNode batchNodeWithFile:@"blocks.png" capacity:150];
[self addChild:batch z:0 tag:0];
//Add a new block
CGSize screenSize = [CCDirector sharedDirector].winSize;
[self addNewSpriteWithCoords:ccp(screenSize.width/2, screenSize.height/2)];
//Schedule step method
Box2D setupsteps[self schedule:@selector(step:)];
return self;
}
/* Adds a polygonal box around the screen */
-(void) addLevelBoundaries {
CGSize screenSize = [CCDirector sharedDirector].winSize;
//Create the body
b2BodyDef groundBodyDef;
groundBodyDef.position.Set(0, 0);
b2Body *body = world->CreateBody(&groundBodyDef);
//Create a polygon shape
b2PolygonShape groundBox;
//Add four fixtures each with a single edge
groundBox.SetAsEdge(b2Vec2(0,0), b2Vec2(screenSize.width/PTM_RATIO,0));
body->CreateFixture(&groundBox,0);
groundBox.SetAsEdge(b2Vec2(0,screenSize.height/PTM_RATIO), b2Vec2(screenSize.width/PTM_RATIO,screenSize.height/PTM_RATIO));
body->CreateFixture(&groundBox,0);
groundBox.SetAsEdge(b2Vec2(0,screenSize.height/PTM_RATIO), b2Vec2(0,0));
body->CreateFixture(&groundBox,0);
groundBox.SetAsEdge(b2Vec2(screenSize.width/PTM_RATIO,screenSize.height/PTM_RATIO), b2Vec2(screenSize.width/PTM_RATIO,0));
body->CreateFixture(&groundBox,0);
}
/* Adds a textured block */
-(void) addNewSpriteWithCoords:(CGPoint)p {
CCSpriteBatchNode *batch = (CCSpriteBatchNode*) [self getChildByTag:0];
//Add randomly textured block
int idx = (CCRANDOM_0_1() > .5 ? 0:1);
int idy = (CCRANDOM_0_1() > .5 ? 0:1);
CCSprite *sprite = [CCSprite spriteWithBatchNode:batch rect:CGRectMake(32 * idx,32 * idy,32,32)];
[batch addChild:sprite];
sprite.position = ccp( p.x, p.y);
//Define body definition and create body
b2BodyDef bodyDef;
bodyDef.type = b2_dynamicBody;
bodyDef.position.Set(p.x/PTM_RATIO, p.y/PTM_RATIO);
bodyDef.userData = sprite;
b2Body *body = world->CreateBody(&bodyDef);
//Define another box shape for our dynamic body.
b2PolygonShape dynamicBox;
dynamicBox.SetAsBox(.5f, .5f);//These are mid points for our 1m box
//Define the dynamic body fixture.
b2FixtureDef fixtureDef;
fixtureDef.shape = &dynamicBox;
fixtureDef.density = 1.0f;
fixtureDef.friction = 0.3f;
body->CreateFixture(&fixtureDef);
}
/* Draw debug data */
-(void) draw {
//Disable textures
glDisable(GL_TEXTURE_2D);
glDisableClientState(GL_COLOR_ARRAY);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
//Draw debug data
world->DrawDebugData();
Box2D setupsteps//Re-enable textures
glEnable(GL_TEXTURE_2D);
glEnableClientState(GL_COLOR_ARRAY);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
}
/* Update graphical positions using physical positions */
-(void) step: (ccTime) dt {
//Set velocity and position iterations
int32 velocityIterations = 8;
int32 positionIterations = 3;
//Steo the Box2D world
world->Step(dt, velocityIterations, positionIterations);
//Update sprite position and rotation to fit physical bodies
for (b2Body* b = world->GetBodyList(); b; b = b->GetNext()) {
if (b->GetUserData() != NULL) {
CCSprite *obj = (CCSprite*)b->GetUserData();
obj.position = CGPointMake( b->GetPosition().x * PTM_RATIO, b->GetPosition().y * PTM_RATIO);
obj.rotation = -1 * CC_RADIANS_TO_DEGREES(b->GetAngle());
}
}
}
/* Tap to add a block */
- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
for( UITouch *touch in touches ) {
CGPoint location = [touch locationInView: [touch view]];
location = [[CCDirector sharedDirector] convertToGL: location];
[self addNewSpriteWithCoords: location];
}
}
@end

它是如何工作的...

Box2D 示例项目是理解物理系统外观的简单方法。

  • 初始化:

    b2World 对象初始化时,我们设置了一些事情,包括重力、对象休眠和连续物理。休眠允许静止的身体占用更少的系统资源。重力通常在 Y 方向上设置为负数,但可以使用以下方法在 b2World 上随时重置:

    void SetGravity(const b2Vec2& gravity);
    
    

    除了存储对主 b2World 实例的指针外,我们通常还存储对 GLESDebugDraw 实例的指针。

  • 调试绘图:

    调试绘图由 GLESDebugDraw 类处理,该类在 GLES-Render.h 中定义。调试绘图包括在屏幕上绘制五个不同的元素。这些包括形状关节连接、AABB(轴对齐边界框)、广相对和质心位。

  • 视觉到物理绘图比例:

    我们将常量PTM_RATIO定义为 32,以便在物理世界和视觉世界之间进行一致的转换。PTM代表像素到米。Box2D 以米为单位测量身体,并构建和优化以与 0.1 到 10.0 米大小的身体一起工作。将此比例设置为 32 是屏幕上形状出现 3.2 到 320 像素之间的常见约定。除了优化之外,Box2D 身体大小没有上限或下限。

  • 关卡边界:

    在这个和许多未来的示例中,我们添加了一个大致覆盖整个屏幕的水平边界。这是通过创建一个具有四个固定体b2Body对象来处理的。每个固定体都有一个定义单个边缘b2Polygon形状。创建边缘通常涉及以下步骤:

    b2BodyDef bodyDef;
    bodyDef.position.Set(0, 0);
    b2Body *body = world->CreateBody(&bodyDef);
    b2PolygonShape poly;
    poly.SetAsEdge(b2Vec2(0,0), b2Vec2(480/PTM_RATIO,0));
    body->CreateFixture(&poly,0);
    
    

    由于这些边缘没有相应的视觉组件(它们是不可见的),我们不需要设置bodyDef.userData指针。

  • 创建方块:

    块的创建方式与创建关卡边界的方式大致相同。我们不是调用SetAsEdge,而是调用SetAsBox来创建一个矩形多边形。然后我们设置固定体densityfriction属性。我们还设置bodyDef.userData以指向我们创建的CCSprite。这连接了视觉和物理,并允许我们的step:方法根据需要重新定位精灵。

  • 安排世界步长:

    最后,我们安排我们的step方法。在这个方法中,我们使用以下代码运行一个离散的b2World步长:

    int32 velocityIterations = 8;
    int32 positionIterations = 3;
    world->Step(dt, velocityIterations, positionIterations);
    
    

    Box2D 的world Step方法将物理引擎向前推进一步。Box2D 约束求解器在两个阶段运行:速度阶段和位置阶段。这些确定物体的移动速度以及它们在游戏世界中的位置。将这些变量设置得更高会导致更精确的模拟,但会牺牲速度。Box2D 手册中建议将velocityIterations设置为 8,将positionIterations设置为 3。使用 dt 变量同步应用程序的逻辑时间与物理时间。如果游戏步长花费了过多的时间,物理系统将快速前进以补偿。这被称为可变时间步长。另一种选择是将时间步长设置为 1/60 秒的固定时间步长。除了物理步长之外,我们还将所有CCSprites根据它们各自的b2Body位置和旋转进行重新定位和重新定向:

    for (b2Body* b = world->GetBodyList(); b; b = b->GetNext()) {
    if (b->GetUserData() != NULL) {
    CCSprite *obj = (CCSprite*)b->GetUserData();
    obj.position = CGPointMake( b->GetPosition().x * PTM_RATIO, b->GetPosition().y * PTM_RATIO);
    obj.rotation = -1 * CC_RADIANS_TO_DEGREES(b->GetAngle());
    }
    }
    
    

    将这些代码片段放在一起,可以将物理世界与视觉世界同步。

创建碰撞响应例程

为了高效且有序地使用 Box2D,我们必须创建几个包装类来封装特定的功能。在这个菜谱中,我们将使用这些类将碰撞响应例程添加到之前菜谱中的简单下落方块演示中。

创建碰撞响应例程

准备工作

请参考项目 RecipeCollection02 以获取此菜谱的完整工作代码。另外,请注意,为了简洁,一些代码已被省略。

如何实现...

执行以下代码:

/* GameObject.h */
@interface GameObject : CCNode {
@public
GameArea2D *gameArea; b2Body *body; b2BodyDef *bodyDef;
b2FixtureDef *fixtureDef; b2PolygonShape *polygonShape;
b2CircleShape *circleShape; CCSprite *sprite;
int typeTag; bool markedForDestruction;
}
/* GameSensor.h */
@interface GameSensor : GameObject {}
@property (readonly) int type;
@end
/* GameMisc.h */
@interface GameMisc : GameObject {
@public
float life;
}
@property (readonly) int type;
@property (readwrite, assign) float life;
@end
collision response routinescreating/* BasicContactListener.h */
class basicContactListener : public b2ContactListener
{
public:
void BeginContact(b2Contact* contact);
};
void basicContactListener::BeginContact(b2Contact* contact)
{
b2Body *bodyA = contact->GetFixtureA()->GetBody();
b2Body *bodyB = contact->GetFixtureB()->GetBody();
//Handle collision using your custom routine
if(bodyA and bodyB){
GameObject *objA = (GameObject*)bodyA->GetUserData();
GameObject *objB = (GameObject*)bodyB->GetUserData();
GameArea2D *gameArea = (GameArea2D*)objA.gameArea;
[gameArea handleCollisionWithObjA:objA withObjB:objB];
}
}
/* GameArea2D.h */
@implementation GameArea2D
-(CCLayer*) runRecipe {
/* CODE OMITTED */
//Add contact filter and contact listener
world->SetContactListener(new basicContactListener);
/* CODE OMITTED */
//Add button to hide/show debug drawing
CCMenuItemFont* swapDebugDrawMIF = [CCMenuItemFont itemFromString:@"Debug Draw" target:self selector:@selector(swapDebugDraw)];
CCMenu *swapDebugDrawMenu = [CCMenu menuWithItems:swapDebugDrawMIF, nil];
swapDebugDrawMenu.position = ccp( 260 , 20 );
[self addChild:swapDebugDrawMenu z:5];
//Schedule our every tick method call
[self schedule:@selector(step:)];
return self;
}
/* This is called from 'basicContactListener'. It will need to be overridden. */
-(void) handleCollisionWithObjA:(GameObject*)objA withObjB:(GameObject*)objB {
/** ABSTRACT **/
}
/* Destroy the world upon exit */
- (void) dealloc {
delete world; world = NULL;
delete m_debugDraw;
[super dealloc];
}
/* Debug information is drawn over everything */
-(void) initDebugDraw {
DebugDrawNode * ddn = [DebugDrawNode createWithWorld:world];
[ddn setPosition:ccp(0,0)];
[gameNode addChild:ddn z:100000];
}
/* When we show debug draw we add a number of flags to show specific information */
-(void) showDebugDraw {
debugDraw = YES;
uint32 flags = 0;
flags += b2DebugDraw::e_shapeBit;
flags += b2DebugDraw::e_jointBit;
flags += b2DebugDraw::e_aabbBit;
flags += b2DebugDraw::e_pairBit;
flags += b2DebugDraw::e_centerOfMassBit;
m_debugDraw->SetFlags(flags);
collision response routinescreating}
@end
@implementation Ch4_CollisionResponse
-(CCLayer*) runRecipe {
/* CODE OMITTED */
//Create circular GameSensor object
GameSensor *gameObjSensor = [[GameSensor alloc] init];
gameObjSensor.gameArea = self;
//Create the body definition
gameObjSensor.bodyDef->type = b2_staticBody;
gameObjSensor.bodyDef->position.Set(240/PTM_RATIO,160/PTM_RATIO);
gameObjSensor.bodyDef->userData = gameObjSensor;
//Create the body
gameObjSensor.body = world->CreateBody(gameObjSensor.bodyDef);
//Create the shape and fixture
gameObjSensor.circleShape = new b2CircleShape();
gameObjSensor.circleShape->m_radius = 1.0f;
//Create the fixture definition
gameObjSensor.fixtureDef->shape = gameObjSensor.circleShape;
gameObjSensor.fixtureDef->isSensor = YES;
//Create the fixture
gameObjSensor.body->CreateFixture(gameObjSensor.fixtureDef);
//Create level boundaries
[self addLevelBoundaries];
//Add block batch sprite
CCSpriteBatchNode *batch = [CCSpriteBatchNode batchNodeWithFile:@"blocks.png" capacity:150];
[gameNode addChild:batch z:0 tag:0];
return self;
collision response routinescreating}
/* Our base collision handling routine */
-(void) handleCollisionWithObjA:(GameObject*)objA withObjB:(GameObject*)objB {
//SENSOR to MISC collision
if(objA.type == GO_TYPE_SENSOR && objB.type == GO_TYPE_MISC){
[self handleCollisionWithSensor:(GameSensor*)objA withMisc:(GameMisc*)objB];
}else if(objA.type == GO_TYPE_MISC && objB.type == GO_TYPE_SENSOR){
[self handleCollisionWithSensor:(GameSensor*)objB withMisc:(GameMisc*)objA];
}
//MISC to MISC collision
else if(objA.type == GO_TYPE_MISC && objB.type == GO_TYPE_MISC){
[self handleCollisionWithMisc:(GameMisc*)objA withMisc:(GameMisc*)objB];
}
}
/* Handling collision between specific types of objects */
-(void) handleCollisionWithSensor:(GameSensor*)sensor withMisc:(GameMisc*)misc {
[message setString:@"Box collided with sensor"];
[self runAction:[CCSequence actions:[CCDelayTime actionWithDuration:0.5f],
[CCCallFunc actionWithTarget:self selector:@selector(resetMessage)], nil]];
}
-(void) handleCollisionWithMisc:(GameMisc*)a withMisc:(GameMisc*)b {
[message setString:@"Box collided with another box"];
[self runAction:[CCSequence actions:[CCDelayTime actionWithDuration:0.5f],
[CCCallFunc actionWithTarget:self selector:@selector(resetMessage)], nil]];
}
/* Adding a new block */
-(void) addNewObjectWithCoords:(CGPoint)p {
/* CODE OMITTED */
}
- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
for( UITouch *touch in touches ) {
CGPoint location = [touch locationInView: [touch view]];
location = [[CCDirector sharedDirector] convertToGL: location];
[self addNewObjectWithCoords: location];
}
}
@end

它是如何工作的...

这个菜谱为本章的其余部分奠定了基础。在这里,我们看到与之前相同的块创建菜谱,但现在当块相互碰撞或与 传感器 碰撞时,会在屏幕上打印一条消息。

  • GameObject:

    GameObject 类封装了 Box2D 数据结构,以帮助简化 Box2D 对象创建的过程。它还包括一个指向其父 GameArea 对象的指针以及我们稍后会用到的其他信息。GameObject 被设计为一个抽象基类,应该为特定用途进行扩展。

  • 传感器:

    附着到 b2Body 上的 固定体 可以设置为 '传感器模式'。这允许碰撞响应例程在没有身体在物理世界中实际存在的情况下运行。不会发生物理碰撞响应。我们已经在 GameSensor 类中封装了这个功能。可以通过检查其类型属性来区分此类对象的其他对象。

  • GameMisc:

    GameMisc 类作为一个典型的 GameObject 扩展的例子存在。在 GameMisc 中添加的唯一功能是我们将在后面的菜谱中使用的生命变量。

  • GameArea2D:

    GameArea2D 类是动作发生的地方。在这里,我们封装了之前菜谱中概述的大多数功能。除此之外,我们还有一个 DebugDrawNode 实例和一个名为 gameNodeCCNode 实例。这允许我们将调试信息和游戏信息分别从主场景中绘制出来。随着菜谱变得更加复杂,这个特性将非常有用。

  • 接触监听器:

    b2ContactListener 通常被重写以允许自定义碰撞响应处理。我们扩展 b2ContactListener 来创建 basicContentListener 类。有四种方法可以扩展以在多个不同的间隔检测碰撞:

    void BeginContact(b2Contact* contact);
    void EndContact(b2Contact* contact);
    void PreSolve(b2Contact* contact, const b2Manifold* oldManifold);
    void PostSolve(b2Contact* contact, const b2ContactImpulse* impulse);
    
    

    BeginContactEndContact 方法相当直观。前者在两个固定体开始接触时被调用,后者在它们停止接触时被调用。PreSolvePostSolve 方法在接触求解器例程运行前后被调用。我们将在后面的菜谱中使用这个功能。对于这个菜谱,我们只关心 BeginContact。在这个方法中,我们从 body->GetUserData() 中检索两个 GameObject 实例,并将它们传递给相应 GameArea 实例中的下一个方法:

    -(void) handleCollisionWithObjA:(GameObject*)objA withObjB:(GameObject*)objB;
    
    

    那个方法检查对象类型,并最终在屏幕上显示不同的消息。

更多...

在这个例子中,方块正在与一个静态传感器发生碰撞。传感器不会移动,因为它的身体type属性被设置为b2_staticBody。静态物体永远不会移动,并且它们不会相互碰撞。每个方块都有其type属性设置为b2_dynamicBody。动态物体可以自由移动,并且与所有其他物体发生碰撞。

使用不同的形状

Box2D 身体的主要属性是其形状。Box2D 使用两个类,b2PolygonShapeb2CircleShape,来表示任何可能的形状。在这个菜谱中,我们将创建许多不同的形状。

使用不同的形状

准备工作

请参阅项目RecipeCollection02以获取此菜谱的完整工作代码。

如何做到这一点...

执行以下代码:

@implementation Ch4_DifferentShapes
/* Here add an object randomly chosen from a rectangle, square, circle, convex polygon and multi-fixture concave polygon. */
-(void) addNewObjectWithCoords:(CGPoint)p
{
//Initialize the object
GameMisc *obj = [[GameMisc alloc] init];
obj.gameArea = self;
obj.bodyDef->type = b2_dynamicBody;
obj.bodyDef->position.Set(p.x/PTM_RATIO, p.y/PTM_RATIO);
obj.bodyDef->userData = obj;
obj.body = world->CreateBody(obj.bodyDef);
obj.fixtureDef->density = 1.0f;
obj.fixtureDef->friction = 0.3f;
obj.fixtureDef->restitution = 0.2f;
//Pick a random shape, size and texture
int num = arc4random()%5;
if(num == 0){
/* Create square object */
/* CODE OMITTED */
//Create shape, add to fixture def and finally create the fixture
obj.polygonShape = new b2PolygonShape();
obj.polygonShape->SetAsBox(shapeSize/PTM_RATIO, shapeSize/PTM_RATIO);
obj.fixtureDef->shape = obj.polygonShape;
obj.body->CreateFixture(obj.fixtureDef);
}else if(num == 1){
/* Create circle object */
/* CODE OMITTED */
//Create shape, add to fixture def and finally create the fixture
obj.circleShape = new b2CircleShape();
obj.circleShape->m_radius = shapeSize/PTM_RATIO;
obj.fixtureDef->shape = obj.circleShape;
obj.fixtureDef->restitution = 0.9f;
obj.body->CreateFixture(obj.fixtureDef);
}else if(num == 2){
/* Create rectangle object */
/* CODE OMITTED */
//Create shape, add to fixture def and finally create the fixture
obj.polygonShape = new b2PolygonShape();
obj.polygonShape->SetAsBox(shapeSize.x/PTM_RATIO, shapeSize.y/PTM_RATIO);
obj.fixtureDef->shape = obj.polygonShape;
obj.body->CreateFixture(obj.fixtureDef);
}else if(num == 3){
/* Create convex polygon object */
/* CODE OMITTED */
shapes, Box2Dcreating//Create shape, add to fixture def and finally create the fixture
obj.polygonShape = new b2PolygonShape();
obj.polygonShape->Set(vertices, numVerts);
obj.fixtureDef->shape = obj.polygonShape;
obj.body->CreateFixture(obj.fixtureDef);
}else if(num == 4){
/* Create concave multi-fixture polygon */
/* CODE OMITTED */
//Create two opposite rectangles
for(int i=0; i<2; i++){
CGPoint shapeSize;
if(i == 0){ shapeSize = ccp(2.0f, 0.4f);
}else{ shapeSize = ccp(0.4f, 2.0f); }
CGPoint vertexArr[] = { ccp(0,0), ccp(shapeSize.x,0), ccp(shapeSize.x,shapeSize.y), ccp(0,shapeSize.y) };
int32 numVerts = 4;
b2Vec2 vertices[4];
NSMutableArray *vertexArray = [[[NSMutableArray alloc] init] autorelease];
shapes, Box2Dcreating//Set vertices
for(int i=0; i<numVerts; i++){
vertices[i].Set(vertexArr[i].x, vertexArr[i].y);
[vertexArray addObject:[NSValue valueWithCGPoint:ccp(vertexArr[i].x *PTM_RATIO, vertexArr[i].y*PTM_RATIO)]];
}
//Create textured polygon
ccTexParams params = {GL_NEAREST,GL_NEAREST_MIPMAP_NEAREST, GL_REPEAT,GL_REPEAT};
CCSprite *sprite = [TexturedPolygon createWithFile:@"box2.png" withVertices:vertexArray];
[sprite.texture setTexParameters:&params];
[sprite setPosition:ccp(0,0)];
[sprite setColor:color];
[obj.sprite addChild:sprite];
//Create shape, set shape and create fixture
obj.polygonShape = new b2PolygonShape();
obj.polygonShape->Set(vertices, numVerts);
obj.fixtureDef->shape = obj.polygonShape;
obj.body->CreateFixture(obj.fixtureDef);
}
}
//Set a random color
[obj.sprite setColor:ccc3(arc4random()%255, arc4random()%255, arc4random()%255)];
}
- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
for( UITouch *touch in touches ) {
CGPoint location = [touch locationInView: [touch view]];
location = [[CCDirector sharedDirector] convertToGL: location];
[self addNewObjectWithCoords: location];
}
}
@end

它是如何工作的...

在这个菜谱中,我们随机创建具有五种不同形状的物体:正方形、圆形、矩形、一个奇特的凸多边形和一个简单的凹多边形。

  • 矩形:

    矩形是通过与前面两个菜谱中相同的b2PolygonShape方法SetAsBox创建的。在这个例子中,我们有一个简单的纹理正方形以及一个矩形柱图像。

  • 圆形:

    在 Box2D 中,圆形是一个特殊情况,并且它们在b2CircleShape类中有一个特殊的类。初始化后,我们只需设置圆形形状的m_radius变量。在这个例子中,我们还给圆形形状的对象赋予了一个高的restitution值,使它们能够弹跳。我们将在另一个菜谱中更深入地介绍这一点。

  • 凸形多边形:

    单个多边形必须是凸形。这意味着多边形内部的所有角度都小于 180 度。在这个例子中,我们创建了一个具有 8 个顶点的奇特形状的凸多边形。我们使用TexturedPolygon来准确绘制这个多边形。有关TexturedPolygon类的更多信息,请参阅第一章,图形。

  • 凹形多边形:

    凹形多边形可以通过创建多个凸多边形并将它们通过多个固定件链接到单个物体上来表示。在这个例子中,我们通过在同一个物体上创建两个固定件将两个简单的凸多边形链接在一起。我们反转宽度和高度值以创建一个简单的 L 形物体。使用这种技术,你可以创建任意复杂的形状。

  • GameObject的扩展性:

    GameObject类主要是为单个固定件身体设计的。它包含一个CCSprite对象,一个b2FixtureDef等。然而,正如你在凹形多边形示例中看到的,你可以创建多个CCSprite对象并将它们链接到主要的GameObject精灵上。你还可以在GameObject实例内重用 Box2D 对象指针,以便轻松创建多个固定件和形状。

拖动和碰撞过滤

在之前的菜谱中,我们处理了用户输入,允许用户拖动一个物体。在这个例子中,我们看到一个装满水果的碗,这些水果可以在屏幕上拖动。一块水果不会与其他水果发生碰撞。

拖动和碰撞过滤

准备工作

请参考项目 RecipeCollection02 以获取此菜谱的完整工作代码。

如何操作...

执行以下代码:

enum { //Collision bits for filtering
CB_GROUND = 1<<0,
CB_FRUIT = 1<<2,
CB_BOWL = 1<<4
};
@implementation Ch4_DraggingAndFiltering
-(CCLayer*) runRecipe {
[super runRecipe];
[message setString:@"Pick up the fruit."];
//Create level boundaries
[self addLevelBoundaries];
dragging featureusing//Add fruit bowl
[self addFruitBasket];
//Initialization of any variables
fruitGrabbed = NO;
return self;
}
/* Add basket and fruit objects */
-(void) addFruitBasket {
/* Add the basket */
/* CODE OMITTED */
//Add physical parts
b2BodyDef bowlBodyDef;
bowlBodyDef.position.Set(0, 0);
bowlBodyDef.type = b2_staticBody;
b2Body *body = world->CreateBody(&bowlBodyDef);
b2PolygonShape bowlShape;
b2FixtureDef bowlFixtureDef;
bowlFixtureDef.restitution = 0.5f;
bowlFixtureDef.filter.categoryBits = CB_BOWL;
bowlFixtureDef.filter.maskBits = CB_FRUIT;
//Rim left
bowlShape.SetAsEdge(b2Vec2(120.0f/PTM_RATIO,120.0f/PTM_RATIO), b2Vec2(180.0f/PTM_RATIO,0.0f/PTM_RATIO));
bowlFixtureDef.shape = &bowlShape;
body->CreateFixture(&bowlFixtureDef);
/* CODE OMITTED */
dragging featureusing/* Add fruit */
fruitObjects = [[[NSMutableArray alloc] init] autorelease];
[self addFruit:@"fruit_banana.png" position:ccp(210,200) shapeType:@"rect"];
[self addFruit:@"fruit_apple.png" position:ccp(230,200) shapeType:@"circle"];
[self addFruit:@"fruit_grapes.png" position:ccp(250,200) shapeType:@"rect"];
[self addFruit:@"fruit_orange.png" position:ccp(270,200) shapeType:@"circle"];
}
/* Add a fruit object with circle physical properties */
-(void) addFruit:(NSString*)spriteFrame position:(CGPoint)p shapeType:(NSString*)s {
//Create GameMisc object
GameMisc *fruit = [[GameMisc alloc] init];
fruit.gameArea = self;
//Define body def and create body
fruit.bodyDef->type = b2_dynamicBody;
fruit.bodyDef->position.Set(p.x/PTM_RATIO, p.y/PTM_RATIO);
fruit.bodyDef->userData = fruit;
fruit.body = world->CreateBody(fruit.bodyDef);
//Create fixture def
fruit.fixtureDef->density = 1.0f;
fruit.fixtureDef->friction = 0.3f;
fruit.fixtureDef->restitution = 0.4f;
fruit.fixtureDef->filter.categoryBits = CB_FRUIT;
fruit.fixtureDef->filter.maskBits = CB_GROUND | CB_BOWL; //Fruit does not collide with other fruit
//Create sprite
fruit.sprite = [CCSprite spriteWithSpriteFrameName:spriteFrame];
fruit.sprite.position = ccp(p.x,p.y);
if([s isEqualToString:@"circle"]){
/* Set fixture shape and sprite scale */
float textureSize = 160;
float shapeSize = 40;
dragging featureusingfruit.sprite.scale = shapeSize / textureSize * 2;
[gameNode addChild:fruit.sprite z:2];
fruit.circleShape = new b2CircleShape();
fruit.circleShape->m_radius = shapeSize/PTM_RATIO;
fruit.fixtureDef->shape = fruit.circleShape;
}else if([s isEqualToString:@"rect"]){
/* Set fixture shape and sprite scale */
CGPoint textureSize = ccp(300,100);
CGPoint shapeSize = ccp(60,20);
fruit.sprite.scaleX = shapeSize.x / textureSize.x * 2;
fruit.sprite.scaleY = shapeSize.y / textureSize.y * 2;
[gameNode addChild:fruit.sprite z:2];
fruit.polygonShape = new b2PolygonShape();
fruit.polygonShape->SetAsBox(shapeSize.x/PTM_RATIO, shapeSize.y/PTM_RATIO);
fruit.fixtureDef->shape = fruit.polygonShape;
}
//Finally create the fixture
fruit.body->CreateFixture(fruit.fixtureDef);
//Add object to container
[fruitObjects addObject:fruit];
grabbedFruit = fruit;
}
-(void) ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView: [touch view]];
point = [[CCDirector sharedDirector] convertToGL: point];
/* Grab the nearest fruit */
//We first grab a fruit.
dragging featureusing//Then, if another fruit is closer we grab that until we finally have the closest one.
float grabbedDistance = distanceBetweenPoints(point, ccp(grabbedFruit.body->GetPosition().x*PTM_RATIO, grabbedFruit.body->GetPosition().y*PTM_RATIO));
for(int i=0; i<fruitObjects.count; i++){
GameMisc *fruit = [fruitObjects objectAtIndex:i];
float thisDistance = distanceBetweenPoints(ccp(fruit.body->GetPosition().x*PTM_RATIO, fruit.body->GetPosition().y*PTM_RATIO), point);
if(thisDistance < grabbedDistance){
grabbedFruit = fruit;
grabbedDistance = thisDistance;
}
}
//Set the fruit to 'grabbed'
fruitGrabbed = YES;
//Immediately move the fruit
[self ccTouchesMoved:touches withEvent:event];
}
-(void) ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView: [touch view]];
point = [[CCDirector sharedDirector] convertToGL: point];
/* Reposition the grabbed fruit */
grabbedFruit.body->SetTransform(b2Vec2(point.x/PTM_RATIO, point.y/PTM_RATIO), grabbedFruit.body->GetAngle());
b2Vec2 moveDistance = b2Vec2( (point.x/PTM_RATIO - grabbedFruit.sprite.position.x/PTM_RATIO), (point.y/PTM_RATIO - grabbedFruit.sprite.position.y/PTM_RATIO) );
lastFruitVelocity = b2Vec2(moveDistance.x*20, moveDistance.y*20);
}
-(void) ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
/* Release the fruit */
fruitGrabbed = NO;
dragging featureusinggrabbedFruit.body->SetLinearVelocity(lastFruitVelocity);
}
-(void) step: (ccTime) dt {
[super step:dt];
/* Suspend the fruit in mid-air while it is grabbed */
if(fruitGrabbed){
grabbedFruit.body->SetLinearVelocity(b2Vec2_zero);
}
}
@end

它是如何工作的...

在这个例子中,我们创建了一个逼真的“抓取”效果。我们通过使用 SetTransform 方法重新定位最近的 Box2D 物体来实现这一点:

grabbedFruit.body->SetTransform(b2Vec2(point.x/PTM_RATIO, point.y/PTM_RATIO), grabbedFruit.body->GetAngle());

我们然后存储物体之前移动的距离,以确定最终速度,并允许用户松手时物体被“抛出”。我们使用 SetLinearVelocity 方法应用这个速度:

grabbedFruit.body->SetLinearVelocity(lastFruitVelocity);

当用户在屏幕上有手指时,为了使水果悬浮在空中,我们将物体速度设置为 b2Vec2_zero,此时它被抓住。

  • 碰撞过滤:

    在这个例子中,我们不允许水果与其他水果碰撞,以便它们可以整齐地放在碗中。我们通过设置水果的固定件上的 filter 属性来实现这一点。具体来说,我们设置了 categoryBitsmaskBits

    enum {
    CB_GROUND = 1<<0,
    CB_FRUIT = 1<<2,
    CB_BOWL = 1<<4
    };
    fruit.fixtureDef->filter.categoryBits = CB_FRUIT;
    fruit.fixtureDef->filter.maskBits = CB_GROUND | CB_BOWL;
    
    

    categoryBits 变量表示这是什么类型的物体。maskBits 变量表示它应该与哪些类型的物体碰撞。这两个属性都使用 布尔逻辑 来指定物体应该如何交互。例如,| 表示“或”。因此,我们说 CB_FRUIT 类别可以与 CB_GROUNDCB_BOWL 类别碰撞。或者,可以使用 过滤器组 来设置过滤器。此外,请注意,如果您没有指定对象的固定件的 filter 变量,则它将不会与设置了过滤器的对象碰撞。有关过滤器的更多信息,请参阅 Box2D 手册:www.box2d.org/manual.html

操纵物理特性

Box2D 允许用户在物体上设置物理特性以创建各种效果。在这个例子中,我们看到一块冰块将箱子推下斜坡。我们还看到了一些弹跳球。

操纵物理特性

准备工作

请参考项目 RecipeCollection02 以获取此菜谱的完整工作代码。

如何操作...

执行以下代码:

@implementation Ch4_PhysicalProperties
-(CCLayer*) runRecipe {
[super runRecipe];
[message setString:@"Friction and restitution"];
//Variable initialization
movableObjects = [[[NSMutableArray alloc] init] autorelease];
objectGrabbed = NO;
//Create level boundaries
[self addLevelBoundaries];
/* Add a crate, a block of ice, bouncing balls and a ledge */
//Crate with 0.4f friction
[self addBlockWithSpriteFile:@"crate2.png" friction:0.4f textureSize:64.0f shapeSize:20.0f position:ccp(130,250)];
//Ice block with 0.0f friction
[self addBlockWithSpriteFile:@"ice_block.png" friction:0.0f textureSize:70.0f shapeSize:20.0f position:ccp(10,250)];
//Ball with size 5.0f and restitution 0.9f
[self addBallWithShapeSize:5.0f restitution:0.9f position:ccp(450,200) color:ccc3(255,0,0)];
//Ball with size 10.0f and restitution 0.8f
[self addBallWithShapeSize:10.0f restitution:0.8f position:ccp(400,200) color:ccc3(255,128,0)];
//Ball with size 15.0f and restitution 0.7f
[self addBallWithShapeSize:15.0f restitution:0.7f position:ccp(350,200) color:ccc3(255,255,0)];
//Ball with size 20.0f and restitution 0.6f
[self addBallWithShapeSize:20.0f restitution:0.6f position:ccp(300,200) color:ccc3(0,255,0)];
//Add brick ledge
[self addLedge];
return self;
}
/* Add a block with a certain texture, size, position and friction */
-(void) addBlockWithSpriteFile:(NSString*)file friction:(float)friction textureSize:(float)textureSize shapeSize:(float)shapeSize position:(CGPoint)p {
/* CODE OMITTED */
}
/* Add a ball with a certain size, position, color and restitution */
-(void) addBallWithShapeSize:(float)shapeSize restitution:(float)restitution position:(CGPoint)p color:(ccColor3B)color {
/* CODE OMITTED */
}
/* Add a brick textured ledge polygon to show the blocks sliding down */
-(void) addLedge {
GameMisc *obj = [[GameMisc alloc] init];
obj.gameArea = self;
physical propertiesmanipulatingobj.bodyDef->position.Set(0,100/PTM_RATIO);
obj.body = world->CreateBody(obj.bodyDef);
obj.fixtureDef->density = 1.0f;
obj.fixtureDef->friction = 0.3f;
obj.fixtureDef->restitution = 0.2f;
float polygonSize = 4;
CGPoint vertexArr[] = { ccp(0,0.8f), ccp(2,0.5f), ccp(2,0.7f), ccp(0,1) };
int32 numVerts = 4;
b2Vec2 vertices[4];
NSMutableArray *vertexArray = [[[NSMutableArray alloc] init] autorelease];
for(int i=0; i<numVerts; i++){
vertices[i].Set(vertexArr[i].x*polygonSize, vertexArr[i].y*polygonSize);
[vertexArray addObject:[NSValue valueWithCGPoint:ccp(vertexArr[i].x*PTM_RATIO*polygonSize,
vertexArr[i].y*PTM_RATIO*polygonSize)]];
}
ccTexParams params = {GL_NEAREST,GL_NEAREST_MIPMAP_NEAREST,GL_REPEAT,GL_REPEAT};
obj.sprite = [TexturedPolygon createWithFile:@"bricks2.png" withVertices:vertexArray];
[obj.sprite.texture setTexParameters:&params];
[obj.sprite setPosition:ccp(0,100)];
[gameNode addChild:obj.sprite z:1];
obj.polygonShape = new b2PolygonShape();
obj.polygonShape->Set(vertices, numVerts);
obj.fixtureDef->shape = obj.polygonShape;
obj.body->CreateFixture(obj.fixtureDef);
}
@end

它是如何工作的...

在这个例子中,我们看到多个具有不同物理特性的物体。

  • 密度:

    物体的密度决定了移动物体所需的力的大小。两个大小不同但密度相同的物体将具有不同的质量。较大的物体自然需要更大的力来移动。

  • 摩擦:

    摩擦决定了物体相对于另一个物体移动的难易程度。物理爱好者可能会指出静摩擦和动摩擦之间的区别。Box2D 将它们合并为一个变量,同时假设静摩擦和动摩擦的比例是恒定的。在我们的例子中,冰块完全没有摩擦。这意味着它可以在任何表面上滑动。这允许冰块慢慢地将箱子推下斜坡,直到它最终落在弹跳球上。

  • 弹性系数:

    术语恢复系数与弹性可以互换。这衡量了物体的“弹性”。具有恢复系数为 1.0f 的物体理论上会永远弹跳。在我们的例子中,我们看到四个具有不同恢复系数的球。这导致它们以不同的速率弹跳。为了看到这些差异的实际效果,请迅速抓住冰块,在它推动箱子越过边缘之前。

施加冲量

在 Box2D 中,可以使用冲量来移动物体。在这个菜谱中,我们将使用冲量来准确地将篮球射入篮球网。

施加冲量

准备工作

请参考项目 RecipeCollection02 以获取此菜谱的完整工作代码。

如何做到这一点...

执行以下代码:

enum { //Object type tags
TYPE_OBJ_BASKETBALL = 0,
TYPE_OBJ_SHOOTER = 1,
TYPE_OBJ_NET_SENSOR = 2
};
@implementation Ch4_Impulses
-(CCLayer*) runRecipe {
[super runRecipe];
[message setString:@"Shoot the ball in the hoop."];
//Create level boundaries
[self addLevelBoundaries];
//Add level background
CCSprite *bg = [CCSprite spriteWithFile:@"bball_bg.png"];
bg.position = ccp(240,160);
[gameNode addChild:bg z:0];
//Add basketball
[self addBasketball];
//Add basketball net
[self addBasketballNet];
//Add shooter
[self addShooter];
return self;
impulsesapplying}
/* Add a basketball net with a sensor */
-(void) addBasketballNet {
/* CODE OMITTED */
//Add net sensor
GameSensor *gameObjSensor = [[GameSensor alloc] init];
gameObjSensor.typeTag = TYPE_OBJ_NET_SENSOR;
gameObjSensor.gameArea = self;
gameObjSensor.bodyDef->type = b2_staticBody;
gameObjSensor.bodyDef->position.Set(0,0);
gameObjSensor.bodyDef->userData = gameObjSensor;
gameObjSensor.body = world->CreateBody(gameObjSensor.bodyDef);
gameObjSensor.polygonShape = new b2PolygonShape();
gameObjSensor.polygonShape->SetAsEdge(b2Vec2(370.0f/PTM_RATIO,200.0f/PTM_RATIO), b2Vec2(380.0f/PTM_RATIO,200.0f/PTM_RATIO));
gameObjSensor.fixtureDef->shape = gameObjSensor.polygonShape;
gameObjSensor.fixtureDef->isSensor = YES;
gameObjSensor.body->CreateFixture(gameObjSensor.fixtureDef);
}
/* Add a basketball */
-(void) addBasketball {
/* CODE OMITTED */
}
/* Add a shooter with reverse karate chop action! */
-(void) addShooter {
/* CODE OMITTED */
impulsesapplying}
-(void) ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView: [touch view]];
point = [[CCDirector sharedDirector] convertToGL: point];
/* Apply an impulse when the user touches the screen */
CGPoint vect = ccp(point.x - basketball.body->GetPosition().x*PTM_RATIO, point.y - basketball.body->GetPosition().y*PTM_RATIO);
basketball.body->ApplyLinearImpulse(b2Vec2(vect.x/20, vect.y/20) , basketball.body->GetPosition() );
}
/* Main collision handling routine */
-(void) handleCollisionWithObjA:(GameObject*)objA withObjB:(GameObject*)objB {
//SENSOR to MISC collision
if(objA.type == GO_TYPE_SENSOR && objB.type == GO_TYPE_MISC){
[self handleCollisionWithSensor:(GameSensor*)objA withMisc:(GameMisc*)objB];
}else if(objA.type == GO_TYPE_MISC && objB.type == GO_TYPE_SENSOR){
[self handleCollisionWithSensor:(GameSensor*)objB withMisc:(GameMisc*)objA];
}
}
/* SENSOR to MISC collision */
-(void) handleCollisionWithSensor:(GameSensor*)sensor withMisc:(GameMisc*)misc {
if(misc.typeTag == TYPE_OBJ_BASKETBALL && sensor.typeTag == TYPE_OBJ_NET_SENSOR){
//Animate the net when the shooter makes a basket
/* CODE OMITTED */
}else if(misc.typeTag == TYPE_OBJ_BASKETBALL && sensor.typeTag == TYPE_OBJ_SHOOTER){
//Animate the shooter's arm and apply an impulse when he touches the ball */
impulsesapplying/* CODE OMITTED */
basketball.body->SetLinearVelocity(b2Vec2(0,0));
basketball.body->ApplyLinearImpulse(b2Vec2(3.5f, 7) , basketball.body->GetPosition() );
}
}
@end

它是如何工作的...

在这个场景中,我们看到一名篮球运动员将篮球射入篮筐。当他接触到篮球时,他将篮球射入篮筐。

  • 冲量:

    当篮球触碰到篮球运动员时,我们重置篮球的速度,然后施加一个精确的冲量以准确地将篮球射入篮筐:

    basketball.body->SetLinearVelocity(b2Vec2(0,0));
    basketball.body->ApplyLinearImpulse(b2Vec2(3.5f, 7) , basketball.body->GetPosition() );
    
    

    应用冲量,立即改变物体的动量。而不是在一段时间内施加力,冲量施加瞬时力以立即改变物体的方向。如果需要,冲量还会唤醒一个休眠的物体。

  • GameObject 类型标签:

    GameObject 类的实例迄今为止已被扩展类指定的 type 属性所识别。为了进行更细粒度的对象识别,你可以使用 typeTag 枚举。这允许我们标记对象以执行多项任务。在这个例子中,我们使用 typeTag 在碰撞响应期间正确地动画化篮球运动员以及篮球网。

施加力

与冲量不同,力必须在一段时间内施加才能在物理世界中显著移动一个物体。在这个菜谱中,我们看到我们太阳系的模拟。

施加力

准备工作

请参考项目 RecipeCollection02 以获取此菜谱的完整工作代码。

如何做到这一点...

执行以下代码:

@implementation Ch4_Forces
-(CCLayer*) runRecipe {
[super runRecipe];
//Set our gravity to 0
world->SetGravity(b2Vec2(0,0));
//Level background
CCSprite *bg = [CCSprite spriteWithFile:@"solar_system_bg.png"];
bg.position = ccp(240,160);
[gameNode addChild:bg z:0];
//Add Planets
planets = [[[NSMutableDictionary alloc] init] autorelease];
[[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@"solar_system.plist"];
forcesapplying[self addPlanetWithSpriteFrameName:@"sun.png" position:ccp(240,160)];
[self addPlanetWithSpriteFrameName:@"mercury.png" position:ccp(210,160)];
[self addPlanetWithSpriteFrameName:@"venus.png" position:ccp(195,160)];
[self addPlanetWithSpriteFrameName:@"earth.png" position:ccp(170,160)];
[self addPlanetWithSpriteFrameName:@"mars.png" position:ccp(150,160)];
[self addPlanetWithSpriteFrameName:@"jupiter.png" position:ccp(120,160)];
[self addPlanetWithSpriteFrameName:@"saturn.png" position:ccp(90,160)];
[self addPlanetWithSpriteFrameName:@"uranus.png" position:ccp(60,160)];
[self addPlanetWithSpriteFrameName:@"neptune.png" position:ccp(30,160)];
//Apply initial impulses to planets
[[planets objectForKey:@"mercury.png"] body]->ApplyLinearImpulse(b2Vec2(0,0.075f), [[planets objectForKey:@"mercury.png"] body]->GetPosition());
[[planets objectForKey:@"venus.png"] body]->ApplyLinearImpulse(b2Vec2(0,0.25f), [[planets objectForKey:@"venus.png"] body]->GetPosition());
[[planets objectForKey:@"earth.png"] body]->ApplyLinearImpulse(b2Vec2(0,0.45f), [[planets objectForKey:@"earth.png"] body]->GetPosition());
[[planets objectForKey:@"mars.png"] body]->ApplyLinearImpulse(b2Vec2(0,0.175f), [[planets objectForKey:@"mars.png"] body]->GetPosition());
[[planets objectForKey:@"jupiter.png"] body]->ApplyLinearImpulse(b2Vec2(0,1.3f), [[planets objectForKey:@"jupiter.png"] body]->GetPosition());
[[planets objectForKey:@"saturn.png"] body]->ApplyLinearImpulse(b2Vec2(0,4.5f), [[planets objectForKey:@"saturn.png"] body]->GetPosition());
[[planets objectForKey:@"uranus.png"] body]->ApplyLinearImpulse(b2Vec2(0,0.6f), [[planets objectForKey:@"uranus.png"] body]->GetPosition());
[[planets objectForKey:@"neptune.png"] body]->ApplyLinearImpulse(b2Vec2(0,0.8f), [[planets objectForKey:@"neptune.png"] body]->GetPosition());
//Fast forward about 16 seconds to create realistic orbits from the start
for(int i=0; i<1000; i++){
[self step:0.016666667f];
forcesapplying}
return self;
}
/* Every tick applies a force on each planet according to how large it is and how far it is from the sun. This simulates heavenly rotation. */
-(void) step:(ccTime)dt {
[super step:dt];
GameMisc *sun = [planets objectForKey:@"sun.png"];
for(id key in planets){
GameMisc *planet = [planets objectForKey:key];
if(![key isEqualToString:@"sun.png"]){
CGPoint vect = ccp(sun.body->GetPosition().x - planet.body->GetPosition().x, sun.body->GetPosition().y - planet.body->GetPosition().y);
float planetSize = pow([planet.sprite contentSize].width,2);
float dist = distanceBetweenPoints(ccp(sun.body->GetPosition().x, sun.body->GetPosition().y),
ccp(planet.body->GetPosition().x, planet.body->GetPosition().y));
float mod = dist/planetSize*2000;
planet.body->ApplyForce(b2Vec2(vect.x/mod, vect.y/mod) , planet.body->GetPosition() );
}
}
}
/* Add a planet with a spriteFrame and a position. We determine the shape size from the texture size. */
-(void) addPlanetWithSpriteFrameName:(NSString*)frameName position:(CGPoint)p {
/* CODE OMITTED */
}
@end

它是如何工作的...

在这个场景中,我们看到八个行星围绕太阳运行。它们的运行速度大致相同。

  • 力:

    每个行星都受到一个指向太阳方向的恒定力的作用:

    planet.body->ApplyForce(b2Vec2(vect.x/mod, vect.y/mod) , planet.body->GetPosition() );
    
    

    这,加上由冲量施加的初始动量,形成了一个围绕太阳的轨道。施加的力考虑了行星的大小和距离太阳的距离,其方式与真实重力相似。

  • 扭矩:

    当施加力或冲量时,你必须指定力或冲量作用在物体上的点。如果这不是物体的质心,那么物体还会受到扭矩的作用。这将改变物体的角速度并使其旋转。

  • 重力:

    正如你在本例中看到的,将重力设置为 b2Vec2(0.0f, 0.0f) 会创建一个从上到下的物理模拟。

参见...

将重力设置为 b2Vec2(0.0f, 0.0f) 创建一个上下模拟。我们将在后面的菜谱中使用这项技术,包括本章后面找到的 创建上下等距游戏引擎

异步刚体销毁

到目前为止,我们已经学习了如何创建刚体,如何重新定位它们,以及如何施加力和冲量来在屏幕上移动它们。在这个例子中,我们将看到如何在物理模拟期间销毁一个刚体。这是一个非常棘手的过程,如果不小心,可能会导致错误和游戏崩溃。

异步刚体销毁

准备工作

请参阅项目 RecipeCollection02 以获取此菜谱的完整工作代码。同时请注意,为了简洁,以下代码中省略了一些内容。

如何实现...

执行以下代码:

@interface GameObjectCallback : NSObject {
@public
GameObject *gameObject;
NSString *callback;
}
@end
@interface QueuedAction : NSObject {
@public
GameObject* gameObject;
CCAction* action;
}
@end
@interface GameArea2D : Recipe {
NSMutableArray *bodiesToDestroy;
NSMutableArray *postDestructionCallbacks;
NSMutableArray *bodiesToCreate;
NSMutableArray *queuedActions;
}
@implementation GameArea2D
-(void) step: (ccTime) dt {
//Process body destruction/creation
[self destroyBodies];
[self createBodies];
[self runQueuedActions];
}
asynchronous body destructionsteps/* Mark a body for destruction */
-(void) markBodyForDestruction:(GameObject*)obj {
[bodiesToDestroy addObject:[NSValue valueWithPointer:obj]];
}
/* Destroy queued bodies */
-(void) destroyBodies {
for(NSValue *value in bodiesToDestroy){
GameObject *obj = (GameObject*)[value pointerValue];
if(obj && obj.body && !obj.markedForDestruction){
obj.body->SetTransform(b2Vec2(0,0),0);
world->DestroyBody(obj.body);
obj.markedForDestruction = YES;
}
}
[bodiesToDestroy removeAllObjects];
//Call all game object callbacks
for(NSValue *value in postDestructionCallbacks){
GameObjectCallback *goc = (GameObjectCallback*)value;
[goc.gameObject runAction:[CCCallFunc actionWithTarget:goc.gameObject selector:NSSelectorFromString(goc.callback)]];
}
[postDestructionCallbacks removeAllObjects];
}
/* Mark a body for creation */
-(void) markBodyForCreation:(GameObject*)obj {
[bodiesToCreate addObject:[NSValue valueWithPointer:obj]];
}
asynchronous body destructionsteps/* Create all queued bodies */
-(void) createBodies {
for(NSValue *value in bodiesToCreate){
GameObject *obj = (GameObject*)[value pointerValue];
obj.body = world->CreateBody(obj.bodyDef);
obj.body->CreateFixture(obj.fixtureDef);
}
[bodiesToCreate removeAllObjects];
}
/* Run any queued actions after creation/destruction */
-(void) runQueuedActions {
for(NSValue *value in queuedActions){
QueuedAction *qa = (QueuedAction*)[value pointerValue];
GameObject *gameObject = (GameObject*)qa.gameObject;
CCAction *action = (CCAction*)qa.action;
[gameObject runAction:action];
}
[queuedActions removeAllObjects];
}
@end
@implementation Ch4_AsyncBodyDestruction
-(CCLayer*) runRecipe {
[super runRecipe];
[message setString:@"Tap to throw a grenade."];
//Create level boundaries
[self addLevelBoundaries];
asynchronous body destructionsteps//Add gunman
[self addGunman];
//Initialize explosion animation
[[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@"explosion5.plist"];
//Initialize grenade container
grenades = [[[NSMutableArray alloc] init] autorelease];
return self;
}
-(void) step:(ccTime)delta {
[super step:delta];
//Grenade life cycle
for(id obj in grenades){
GameMisc *grenade = (GameMisc*)obj;
grenade.life -= delta;
//If a grenade is out of life we mark it for destruction, do cleanup and finally animate an explosion
if(grenade.life < 0){
[self markBodyForDestruction:grenade];
[grenades removeObject:obj];
[self explosionAt:grenade.sprite.position];
[gameNode removeChild:grenade.sprite cleanup:NO];
}
}
//Explosion life cycle
for(id obj in explosions){
GameMisc *explosion = (GameMisc*)explosion;
explosion.life -= delta;
if(explosion.life < 0){
[explosions removeObject:explosion];
[gameNode removeChild:explosion.sprite cleanup:YES];
}
}
}
asynchronous body destructionsteps/* Callback for throwing the arm. This involves animating the arm and creating a grenade */
-(void) throwGrenade {
CCSpriteFrameCache *cache = [CCSpriteFrameCache sharedSpriteFrameCache];
//Animate the arm
CCAnimation *animation = [[CCAnimation alloc] initWithName:@"gunmanStandRightArmEmpty" delay:1.0f];
[animation addFrame:[cache spriteFrameByName:@"gunman_stand_right_arm_empty.png"]];
[gunmanArm runAction:[CCRepeatForever actionWithAction:[CCAnimate actionWithAnimation:animation]]];
//Create and launch a grenade
GameMisc *grenade = [[GameMisc alloc] init];
grenade.life = 5.0f;
grenade.gameArea = self;
CGPoint grenadePosition = ccp(65,150);
grenade.bodyDef->type = b2_dynamicBody;
grenade.bodyDef->position.Set(grenadePosition.x/PTM_RATIO, grenadePosition.y/PTM_RATIO);
grenade.body = world->CreateBody(grenade.bodyDef);
grenade.body->SetTransform(b2Vec2(grenadePosition.x/PTM_RATIO, grenadePosition.y/PTM_RATIO),PI/2);
CGPoint textureSize = ccp(16,16);
CGPoint shapeSize = ccp(7,7);
grenade.sprite = [CCSprite spriteWithSpriteFrameName:@"gunman_grenade.png"];
grenade.sprite.position = ccp(grenadePosition.x,grenadePosition.y);
grenade.sprite.scaleX = shapeSize.x / textureSize.x * 2;
grenade.sprite.scaleY = shapeSize.y / textureSize.y * 2;
[gameNode addChild:grenade.sprite z:1];
grenade.circleShape = new b2CircleShape();
grenade.circleShape->m_radius = shapeSize.x/PTM_RATIO;
grenade.fixtureDef->shape = grenade.circleShape;
grenade.body->CreateFixture(grenade.fixtureDef);
[grenades addObject:grenade];
grenade.body->ApplyLinearImpulse(b2Vec2(1.0f,2.0f) , grenade.body->GetPosition() );
grenade.body->SetAngularVelocity(PI);
}
@end

它是如何工作的...

在这个菜谱中,我们有能力投掷在五秒后爆炸的手榴弹。爆炸会发射出附近区域的其他任何物体。

  • 销毁 b2Body

    Box2D 不允许在 world->Step(dt, velocityIterations, positionIterations) 调用期间销毁刚体。因此,碰撞响应例程和定时回调不能同步启动刚体的销毁。为了解决这个问题,我们创建了一个简单的异步系统,可以排队等待销毁和创建刚体。此系统使用以下方法:

    -(void) markBodyForDestruction:(GameObject*)obj;
    -(void) destroyBodies;
    -(void) markBodyForCreation:(GameObject*)obj;
    -(void) createBodies;
    -(void) runQueuedActions;
    
    

    每个物理步骤完成后都会创建和销毁刚体。

  • GameObjectCallbackQueuedAction

    GameObjectCallbackQueuedAction 辅助类允许我们在对象创建/删除后排队方法回调和 CCAction 实例以供使用。这有助于在游戏运行时保持操作逻辑的顺序。

  • GameObject 生命:

    GameMisc 类中,我们添加了一个 life 值。在这个菜谱中,我们将使用它。每个手榴弹的 life 会逐渐减少直到爆炸。创建的爆炸也是一个 GameMisc 对象,具有与其动画持续时间相对应的固定 life。生命值也可以用于演员和可破坏物体。

使用关节

我们尚未调查 Box2D 的最后一个主要功能是 关节。关节允许我们将对象连接起来,创建像滑轮、杠杆和简单电机这样的简单机械。在这个菜谱中,我们将学习如何使用关节创建一个简单的跷跷板。

使用关节

准备工作

请参阅项目 RecipeCollection02 以获取此菜谱的完整工作代码。同时请注意,为了简洁,以下代码中省略了一些内容。

如何实现...

执行以下代码:

@implementation Ch4_Joints
-(CCLayer*) runRecipe {
[super runRecipe];
[message setString:@"Drop the weight on the see-saw"];
//Initialization
movableObjects = [[[NSMutableArray alloc] init] autorelease];
objectGrabbed = NO;
//Create level boundaries
[self addLevelBoundaries];
//Add objects
[self addSeeSaw];
[self addBoxWithPosition:ccp(130,120) file:@"crate2.png" density:1.0f];
[self addBoxWithPosition:ccp(160,120) file:@"crate2.png" density:1.0f];
[self addBoxWithPosition:ccp(145,150) file:@"crate2.png" density:1.0f];
[self addBoxWithPosition:ccp(270,100) file:@"weight.png" density:15.0f];
return self;
}
/* Create a complex see-saw object */
-(void) addSeeSaw {
/* The triangle is the static base of the see-saw */
CGPoint trianglePosition = ccp(240,50);
GameMisc *triangle = [[GameMisc alloc] init];
triangle.gameArea = self;
triangle.bodyDef->type = b2_staticBody;
triangle.bodyDef->position.Set(trianglePosition.x/PTM_RATIO, trianglePosition.y/PTM_RATIO);
triangle.body = world->CreateBody(triangle.bodyDef);
jointsusing//Our triangle polygon
float polygonSize = 2.0f;
CGPoint vertexArr[] = { ccp(0,0), ccp(1,0), ccp(0.5f,1) };
int32 numVerts = 3;
b2Vec2 vertices[3];
NSMutableArray *vertexArray = [[[NSMutableArray alloc] init] autorelease];
for(int i=0; i<numVerts; i++){
vertices[i].Set(vertexArr[i].x*polygonSize, vertexArr[i].y*polygonSize);
[vertexArray addObject:[NSValue valueWithCGPoint:ccp(vertexArr[i].x*PTM_RATIO*polygonSize,
vertexArr[i].y*PTM_RATIO*polygonSize)]];
}
ccTexParams params = {GL_NEAREST,GL_NEAREST_MIPMAP_NEAREST,GL_REPEAT,GL_REPEAT};
triangle.sprite = [TexturedPolygon createWithFile:@"box.png" withVertices:vertexArray];
[triangle.sprite.texture setTexParameters:&params];
[triangle.sprite setPosition:ccp(trianglePosition.x,trianglePosition.y)];
[gameNode addChild:triangle.sprite z:1];
triangle.polygonShape = new b2PolygonShape();
triangle.polygonShape->Set(vertices, numVerts);
triangle.fixtureDef->shape = triangle.polygonShape;
triangle.body->CreateFixture(triangle.fixtureDef);
/* The plank is the dynamic part of the see-saw */
CGPoint plankPosition = ccp(270,80);
GameMisc *plank = [[GameMisc alloc] init];
plank.gameArea = self;
plank.bodyDef->type = b2_dynamicBody;
plank.bodyDef->position.Set(plankPosition.x/PTM_RATIO, plankPosition.y/PTM_RATIO);
plank.body = world->CreateBody(plank.bodyDef);
jointsusingplank.body->SetTransform(b2Vec2(plankPosition.x/PTM_RATIO, plankPosition.y/PTM_RATIO),PI/2);
CGPoint textureSize = ccp(54,215);
CGPoint shapeSize = ccp(12,180);
plank.sprite = [CCSprite spriteWithFile:@"column2.png"];
plank.sprite.position = ccp(plankPosition.x,plankPosition.y);
plank.sprite.scaleX = shapeSize.x / textureSize.x * 2;
plank.sprite.scaleY = shapeSize.y / textureSize.y * 2;
[gameNode addChild:plank.sprite z:1];
plank.polygonShape = new b2PolygonShape();
plank.polygonShape->SetAsBox(shapeSize.x/PTM_RATIO, shapeSize.y/PTM_RATIO);
plank.fixtureDef->shape = plank.polygonShape;
plank.body->CreateFixture(plank.fixtureDef);
/* We initialize a revolute joint linking the plank to the triangle */
b2RevoluteJointDef rjd;
b2RevoluteJoint* joint;
rjd.Initialize(plank.body, triangle.body, b2Vec2(trianglePosition.x/PTM_RATIO + polygonSize/2, trianglePosition.y/PTM_RATIO + polygonSize/2));
joint = (b2RevoluteJoint*)world->CreateJoint(&rjd);
}

它是如何工作的...

通过将重物放在一边,我们可以将轻质箱子从另一边抛向空中。这是通过一个简单的关节实现的。

  • 关节类型:

    所有关节连接两个 Box2D 物体。每个关节都由一个从b2Joint派生的类表示。这些包括b2PulleyJoint, b2WeldJoint, b2RopeJoint等。Box2D 所有关节类型的全面概述超出了本书的范围。请参阅 Box2D 测试床中的每个关节的示例代码以及 Box2D 手册页面www.box2d.org/manual.html

  • 旋转关节:

    在这个例子中,我们使用b2RevoluteJoint来迫使两个物体共享一个共同的锚点:

    b2RevoluteJointDef rjd;
    b2RevoluteJoint* joint;
    rjd.Initialize(plank.body, triangle.body, b2Vec2(trianglePosition.x/PTM_RATIO + polygonSize/2, trianglePosition.y/PTM_RATIO + polygonSize/2));
    joint = (b2RevoluteJoint*)world->CreateJoint(&rjd);
    
    

    通过将动态的plank物体固定到静态的triangle物体上,在这个例子中,我们已经限制了plank在 X 轴和 Y 轴上的运动。现在它不能移动,它只能旋转。这产生了真实的跷跷板效果。

创建车辆

结合两个或多个关节可以产生一些有趣的效果。在这个例子中,我们将创建一个可以在关卡中驾驶的汽车。

创建车辆

准备工作

请参阅项目RecipeCollection02以获取此菜谱的完整工作代码。此外,请注意,为了简洁,以下代码中省略了一些内容。

如何做...

执行以下代码:

@implementation Ch4_Vehicles
-(CCLayer*) runRecipe {
[super runRecipe];
[message setString:@"Press and hold to drive car."];
//Initialization
pressedLeft = NO;
pressedRight = NO;
//Create level
[self createLevel];
//Add taxi
[self addTaxi];
return self;
}
-(void) createLevel {
/* Create a sine wave road for our car */
b2BodyDef groundBodyDef;
groundBodyDef.position.Set(0, 0);
b2Body *body = world->CreateBody(&groundBodyDef);
vehiclecreatingb2PolygonShape groundBox;
b2FixtureDef groundFixtureDef;
groundFixtureDef.restitution = 0.0f;
groundFixtureDef.friction = 10.0f; //The road has a lot of friction
groundFixtureDef.filter.categoryBits = CB_GROUND;
groundFixtureDef.filter.maskBits = CB_CAR | CB_WHEEL;
groundBox.SetAsEdge(b2Vec2(-960/PTM_RATIO,0), b2Vec2(-960/PTM_RATIO,200/PTM_RATIO));
groundFixtureDef.shape = &groundBox;
body->CreateFixture(&groundFixtureDef);
groundBox.SetAsEdge(b2Vec2(960/PTM_RATIO,0), b2Vec2(960/PTM_RATIO,200/PTM_RATIO));
groundFixtureDef.shape = &groundBox;
body->CreateFixture(&groundFixtureDef);
float32 x1; float32 y1;
for(int u = -1; u < 2; u++){
//Add Edge Shapes
x1 = -15.0f;
y1 = 2.0f * cosf(x1 / 10.0f * b2_pi);
for (int32 i = 0; i < 60; ++i)
{
float32 x2 = x1 + 0.5f;
float32 y2 = 2.0f * cosf(x2 / 10.0f * b2_pi);
b2PolygonShape shape;
shape.SetAsEdge(b2Vec2(x1 + u*960/PTM_RATIO, y1), b2Vec2(x2 + u*960/PTM_RATIO, y2));
body->CreateFixture(&shape, 0.0f);
vehiclecreatingx1 = x2;
y1 = y2;
}
//Add corresponding graphics
CCSprite *bg = [CCSprite spriteWithFile:@"road_bg.png"];
bg.position = ccp(u*960,70);
[gameNode addChild:bg z:0];
CCSprite *fg = [CCSprite spriteWithFile:@"road_fg.png"];
fg.position = ccp(u*960,70);
[gameNode addChild:fg z:2];
}
/* Add two bricks walls so you can't drive off the course */
[self addBrickWallSpriteAtPosition:ccp(970,60)];
[self addBrickWallSpriteAtPosition:ccp(-970,60)];
}
-(void) addTaxi {
// NOTE: In b2Settings.h we increased the b2_maxPolygonVertices definition:
// #define b2_maxPolygonVertices 16
[[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@"taxi.plist"];
CGPoint taxiPosition = ccp(-960,80);
float taxiScale = 0.2f;
taxi = [[GameMisc alloc] init];
taxi.gameArea = self;
taxi.bodyDef->type = b2_dynamicBody;
taxi.bodyDef->position.Set(taxiPosition.x/PTM_RATIO, taxiPosition.y/PTM_RATIO);
taxi.body = world->CreateBody(taxi.bodyDef);
taxi.fixtureDef->filter.categoryBits = CB_CAR;
taxi.fixtureDef->filter.maskBits = CB_GROUND;
taxi.fixtureDef->density = 0.5f;
taxi.fixtureDef->friction = 0.25f;
taxi.fixtureDef->restitution = 0.0f;
//Polygon
/* CODE OMITTED */
vehiclecreating//Wheels
CGPoint wheelPosition[] = { ccp(taxiPosition.x + 16, taxiPosition.y), ccp(taxiPosition.x + 43, taxiPosition.y) };
for(int i=0; i<2; i++){
GameMisc *wheel = [[GameMisc alloc] init];
if(i == 0){
wheel1 = wheel;
}else{
wheel2 = wheel;
}
wheel.gameArea = self;
wheel.bodyDef->type = b2_dynamicBody;
wheel.bodyDef->position.Set(wheelPosition[i].x/PTM_RATIO, wheelPosition[i].y/PTM_RATIO);
wheel.body = world->CreateBody(wheel.bodyDef);
wheel.body->SetTransform(b2Vec2(wheelPosition[i].x/PTM_RATIO, wheelPosition[i].y/PTM_RATIO),PI/2);
wheel.fixtureDef->filter.categoryBits = CB_WHEEL;
wheel.fixtureDef->filter.maskBits = CB_GROUND;
wheel.fixtureDef->density = 10.0f;
wheel.fixtureDef->friction = 10.0f;
wheel.fixtureDef->restitution = 0.0f;
CGPoint textureSize = ccp(52,51);
CGPoint shapeSize = ccp(9,9);
wheel.sprite = [CCSprite spriteWithSpriteFrameName:@"taxi_wheel.png"];
wheel.sprite.position = ccp(wheelPosition[i].x,wheelPosition[i].y);
wheel.sprite.scaleX = shapeSize.x / textureSize.x * 2;
wheel.sprite.scaleY = shapeSize.y / textureSize.y * 2;
[gameNode addChild:wheel.sprite z:1];
wheel.circleShape = new b2CircleShape();
wheel.circleShape->m_radius = shapeSize.x/PTM_RATIO;
wheel.fixtureDef->shape = wheel.circleShape;
wheel.body->CreateFixture(wheel.fixtureDef);
wheel.body->SetAngularDamping(1.0f);
//Add Joint to connect wheel to the taxi
vehiclecreatingb2RevoluteJointDef rjd;
b2RevoluteJoint* joint;
rjd.Initialize(wheel.body, taxi.body, b2Vec2(wheelPosition[i].x/PTM_RATIO, wheelPosition[i].y/PTM_RATIO));
joint = (b2RevoluteJoint*)world->CreateJoint(&rjd);
}
}
-(void) step: (ccTime) dt {
[super step:dt];
gameNode.position = ccp(-taxi.sprite.position.x + 240, -taxi.sprite.position.y + 160);
//Front wheel drive
//We apply some counter-torque to steady the car
if(pressedRight){
wheel2->body->ApplyTorque(-20.0f);
taxi->body->ApplyTorque(5.0f);
}else if(pressedLeft){
wheel1->body->ApplyTorque(20.0f);
taxi->body->ApplyTorque(-5.0f);
}
}
@end

它是如何工作的...

通过按屏幕的任意一侧,我们可以看到汽车向前或向后行驶,直到不可避免地撞到关卡两端的一堵砖墙。

  • 汽车:

    在 Box2D 中创建一个简单的汽车,你所要做的就是使用旋转关节将两个圆圈连接到一个多边形上。每个圆圈,或称为“车轮”,具有高密度和摩擦力,有助于它在道路上拉动汽车。它还具有低恢复力,以限制弹跳。当放置在不平的表面上时,汽车将向前或向后滚动。此外,为了简化,汽车的底盘实际上是一个凸多边形。

  • 驾驶汽车:

    要驾驶汽车,我们在前轮上施加扭矩,同时在汽车本身上施加一些反扭矩:

    wheel2->body->ApplyTorque(-20.0f);
    taxi->body->ApplyTorque(5.0f);
    
    

    反扭矩的作用方式与尾翼保持汽车平衡的方式相同。

  • 创建弯曲的道路:

    这个菜谱中的道路是 Box2D 中曲面形状的一个很好的例子。我们使用许多小的边缘固定件来构建一个高多边形曲线。

  • 摄像头:

    在这个菜谱中,我们最终使用了gameNode。通过重新定位此节点,我们有效地将摄像头与HUD分开单独定位:

    gameNode.position = ccp(-taxi.sprite.position.x + 240, -taxi.sprite.position.y + 160);
    
    

    我们将在另一个菜谱中深入讨论摄像头的使用。

更多...

在这个例子中,车辆远非完美。尝试使用旋转关节将车轮从车底伸出,并添加一些减震。

  • b2_maxPolygonVertices:

    由于我们的汽车有超过八个顶点,我们必须覆盖b2_maxPolygonVertices的定义。这位于文件b2Settings.h中。新的定义看起来像这样:

    #define b2_maxPolygonVertices 16
    
    
  • 这允许我们定义具有多达 16 个顶点的多边形。

角色移动

在一个关卡中移动一个角色可能比你想象的要复杂。在本菜谱中,我们将介绍 2D 侧滚动角色移动的基础。

角色移动

准备工作

请参阅项目 RecipeCollection02 以获取此菜谱的完整工作代码。此外,请注意,以下代码中省略了一些内容以简化。

如何操作...

执行以下代码:

@implementation SideScrollerRecipe
-(void) step:(ccTime)delta {
[super step:delta];
//Apply gunman running direction
if(dPad.direction == DPAD_LEFT || dPad.direction == DPAD_UP_LEFT || dPad.direction == DPAD_DOWN_LEFT){
gunmanDirection = DPAD_LEFT;
gunman.body->ApplyForce(b2Vec2(-35.0f,0), gunman.body->GetPosition());
((CCSprite*)[gunman.sprite getChildByTag:0]).flipX = YES;
}else if(dPad.direction == DPAD_RIGHT || dPad.direction == DPAD_UP_RIGHT || dPad.direction == DPAD_DOWN_RIGHT){
gunmanDirection = DPAD_RIGHT;
gunman.body->ApplyForce(b2Vec2(35.0f,0), gunman.body->GetPosition());
((CCSprite*)[gunman.sprite getChildByTag:0]).flipX = NO;
}
//Decrement jump counter
jumpCounter -= delta;
//Did the gunman just hit the ground?
if(!onGround){
if((gunman.body->GetLinearVelocity().y - lastYVelocity) > 2 && lastYVelocity < -2){
gunman.body->SetLinearVelocity(b2Vec2(gunman.body->GetLinearVelocity().x,0));
onGround = YES;
}else if(gunman.body->GetLinearVelocity().y == 0 && lastYVelocity == 0){
gunman.body->SetLinearVelocity(b2Vec2(gunman.body->GetLinearVelocity().x,0));
onGround = YES;
}
}
//Did he just fall off the ground without jumping?
if(onGround){
if(gunman.body->GetLinearVelocity().y < -2.0f && lastYVelocity < -2.0f && (gunman.body->GetLinearVelocity().y < lastYVelocity)){
onGround = NO;
charactermoving}
}
//Store last velocity
lastYVelocity = gunman.body->GetLinearVelocity().y;
//Keep him upright on the ground
if(onGround){
gunman.body->SetTransform(gunman.body->GetPosition(),0);
}
//Animate gunman if his speed changed significantly
float speed = gunman.body->GetLinearVelocity().x;
if(speed < 0){ speed *= -1; }
if(speed > lastXSpeed*2){
[[gunman.sprite getChildByTag:0] stopAllActions];
[self animateGunman];
}
//Keep the gunman in the level
b2Vec2 gunmanPos = gunman.body->GetPosition();
if(gunmanPos.x > 530/PTM_RATIO || gunmanPos.x < (-50/PTM_RATIO) || gunmanPos.y < -100/PTM_RATIO){
gunman.body->SetTransform(b2Vec2(2,10), gunman.body->GetAngle());
}
//Process input for the A button
for(id b in buttons){
GameButton *button = (GameButton*)b;
if(button.pressed && [button.name isEqualToString:@"A"]){
[self processJump];
}else{
jumpCounter = -10.0f;
}
}
}
/* Initialize gunman */
-(void) initGunman {
gunman = [[GameMisc alloc] init];
/* CODE OMITTED */
gunman.body->SetLinearDamping(2.0f);
}
/* Process jump */
-(void) processJump {
if(onGround && jumpCounter < 0){
//Start a jump. Starting requires you to not be moving on the Y.
jumpCounter = 0.4f;
gunman.body->ApplyLinearImpulse(b2Vec2(0,20.0f), gunman.body->GetPosition());
onGround = NO;
}else if(jumpCounter > 0){
//Continue a jump
gunman.body->ApplyForce(b2Vec2(0,65.0f), gunman.body->GetPosition());
}
}

它是如何工作的...

在本菜谱中,我们可以让“枪手”在关卡中奔跑和跳跃。这里使用的动画程序是基于之前菜谱中使用的。

  • 向左和向右移动:

    使用方向垫,我们可以将枪手移动到左边或右边。这涉及到在 X 轴上对物体施加力:

    gunman.body->ApplyForce(b2Vec2(35.0f,0), gunman.body->GetPosition());
    
    

    随后,枪手的动画速度基于他的 X 轴移动速度。

  • 阻尼:

    为了在空中和地面上减慢枪手的速度,我们在身体上设置一个 线性阻尼 值:

    gunman.body->SetLinearDamping(2.0f);
    
    

    这会逐渐减少枪手在所有方向上的速度。这有两个作用:一方面产生空气阻力,另一方面在他没有积极奔跑时减慢他的速度。

  • 跳跃:

    要创建一个舒适的马里奥式跳跃,我们需要应用一些技巧并存储几个变量。跳跃只应该在枪手站在一个物体上时发生。用户应该能够按住跳跃按钮进行更高跳跃,即直到某个点。为了实现所有这些,我们使用以下变量:

    float lastYVelocity;
    float jumpCounter;
    bool onGround;
    
    

    变量 lastYVelocity 用于确定枪手是否最近触地,或者他只是从地面上跑开(而不是从地面上跳开)。Y 速度的微妙变化可以告诉我们这些信息。变量 jumpCounter 用于限制跳跃高度。计数器会不断递减。重置到最初跳跃时的时间是枪手向上推力的最大时间。这个时间可以修改,以允许某些角色跳得更高或更低。跳跃首先是一个初始冲量,然后是一个持续的向上推力。当用户松开跳跃按钮时,我们重置 jumpCounter,枪手开始下落。

模拟子弹

子弹和其他快速移动的物体是许多电子游戏的基本组成部分。在本菜谱中,我们将看到如何正确实现子弹物理。

模拟子弹

准备工作

请参阅项目 RecipeCollection02 以获取此菜谱的完整工作代码。此外,请注意,以下代码中省略了一些内容以简化。

如何操作...

执行以下代码:

@implementation Ch4_Bullets
/* Fire the gun */
-(void) fireGun {
//Fire 10 bullets per second
if(fireCount > 0){
return;
}
fireCount = 0.2f;
CCSpriteFrameCache *cache = [CCSpriteFrameCache sharedSpriteFrameCache];
//Fire bullet in the correct direction
float gunAngle = -gunman.body->GetAngle() + PI/2;
if(gunmanDirection == DPAD_LEFT){ gunAngle += PI; }
CGPoint bulletVector = ccp( sin(gunAngle), cos(gunAngle) );
//Create bullet and shell casing
bulletssimulatingfor(int i=0; i<2; i++){
//Create bullet or casing object
//NOTE: It might be more efficient to re-use a group of bullet objects instead of creating new bullets each time
GameMisc *bullet = [[GameMisc alloc] init];
bullet.gameArea = self;
bullet.typeTag = TYPE_OBJ_BULLET;
if(i == 1){
bullet.typeTag = TYPE_OBJ_SHELL;
}
bullet.life = 2.0f;
if(i == 1){
bullet.life = 5.0f;
}
//Calculate bullet/casing position as being slightly ahead of the gunman
CGPoint bulletPosition = ccp( gunman.sprite.position.x + bulletVector.x*10, gunman.sprite.position.y + bulletVector.y*10 );
if(i == 1){
bulletPosition = ccp( gunman.sprite.position.x, gunman.sprite.position.y );
}
//Create body using body definition
bullet.bodyDef->type = b2_dynamicBody;
if(i == 0){
bullet.bodyDef->bullet = YES;
}
bullet.bodyDef->position.Set(bulletPosition.x/PTM_RATIO, bulletPosition.y/PTM_RATIO);
bullet.body = world->CreateBody(bullet.bodyDef);
//Set the angle of the bullet/casing in the direction of the firing gun
bullet.body->SetTransform(bullet.body->GetPosition(), gunAngle);
CGPoint textureSize = ccp(17,17);
CGPoint shapeSize = ccp(2,2);
//Create the bullet sprite
bulletssimulatingbullet.sprite = [CCSprite spriteWithFile:@"bullet.png"];
bullet.sprite.position = ccp(bulletPosition.x,bulletPosition.y);
bullet.sprite.scaleX = shapeSize.x / textureSize.x * 2.25f;
bullet.sprite.scaleY = shapeSize.y / textureSize.y * 2.25f;
//If this is a shell casing make it a golden color
if(i == 1){ bullet.sprite.color = ccc3(255,200,0); }
//Add object
[gameNode addChild:bullet.sprite z:1];
//Set bullet shape
bullet.polygonShape = new b2PolygonShape();
bullet.polygonShape->SetAsBox(shapeSize.x/PTM_RATIO/2, shapeSize.y/PTM_RATIO);
bullet.fixtureDef->shape = bullet.polygonShape;
//Create fixture and configure collision
bullet.fixtureDef->density = 20.0f;
bullet.fixtureDef->friction = 1.0f;
bullet.fixtureDef->restitution = 0.0f;
if(i == 0){
bullet.fixtureDef->filter.categoryBits = CB_BULLET;
bullet.fixtureDef->filter.maskBits = CB_OTHER;
}else{
bullet.fixtureDef->filter.categoryBits = CB_SHELL;
bullet.fixtureDef->filter.maskBits = CB_OTHER | CB_SHELL;
}
bullet.body->CreateFixture(bullet.fixtureDef);
//Add this bullet to our container
[bullets addObject:bullet];
//If this is a bullet, fire it. If its a shell, eject it.
if(i == 0){
//Fire the bullet by applying an impulse
bullet.body->ApplyLinearImpulse(b2Vec2(bulletVector.x*50, bulletVector.y*50), bullet.body->GetPosition());
}else{
//Eject the shell
float radians = vectorToRadians(bulletVector);
radians += 1.85f * PI;
CGPoint shellVector = radiansToVector(radians);
if(shellVector.x > 0){ shellVector.y *= -1; }
bullet.body->ApplyLinearImpulse(b2Vec2(shellVector.x, shellVector.y), bullet.body->GetPosition());
}
}
}
-(void) handleCollisionWithMisc:(GameMisc*)a withMisc:(GameMisc*)b {
//If a bullet touches something we set life to 0 and process the impact on that object
if(a.typeTag == TYPE_OBJ_BULLET && b.typeTag == TYPE_OBJ_BOX && a.life > 0){
a.life = 0;
[self bulletImpactAt:a.sprite.position onObject:b];
[message setString:@"Bullet hit"];
}else if(b.typeTag == TYPE_OBJ_BULLET && a.typeTag == TYPE_OBJ_BOX && b.life > 0){
b.life = 0;
[self bulletImpactAt:b.sprite.position onObject:a];
[message setString:@"Bullet hit"];
}
//Reset our message
[self runAction:[CCSequence actions:[CCDelayTime actionWithDuration:5.0f],
[CCCallFunc actionWithTarget:self selector:@selector(resetMessage)], nil]];
}
/* Process the bullet impact */
-(void) bulletImpactAt:(CGPoint)p onObject:(GameMisc*)obj {
//Here we use some trigonometry to determine exactly where the bullet impacted on the box.
float dist = distanceBetweenPoints(p, obj.sprite.position); //Hypotenuse
float xDist = obj.sprite.position.x - p.x; //Opposite side
float yDist = obj.sprite.position.y - p.y; //Adjacent side
bulletssimulatingfloat xAngle = asin(xDist/dist);
float yAngle = acos(yDist/dist);
float objSize = [obj.sprite contentSize].width/2 * obj.sprite.scale;
float newXDist = xDist - sin(xAngle) * objSize;
float newYDist = yDist - cos(yAngle) * objSize;
p = ccp( p.x + newXDist, p.y + newYDist );
//Animate bullet impact
float delay = 0.035f;
float duration = 8 * delay;
GameMisc *blastmark = [[GameMisc alloc] init];
blastmark.sprite = [CCSprite spriteWithSpriteFrameName:@"blastmark_0000.png"];
blastmark.life = duration;
blastmark.sprite.position = p;
blastmark.sprite.scale = 0.2f;
blastmark.sprite.opacity = 100;
CCSpriteFrameCache *cache = [CCSpriteFrameCache sharedSpriteFrameCache];
CCAnimation *animation = [[CCAnimation alloc] initWithName:@"blastmark" delay:delay];
for(int i=0; i<8; i+=1){
[animation addFrame:[cache spriteFrameByName:[NSString stringWithFormat:@"blastmark_000%i.png",i]]];
}
[blastmark.sprite stopAllActions];
[blastmark.sprite runAction:
[CCSpawn actions:
[CCFadeOut actionWithDuration:duration],
[CCAnimate actionWithAnimation:animation],
nil
]
];
[gameNode addChild:blastmark.sprite z:5];
[explosions addObject:blastmark];
//Decrement the box life
obj.life -= 1.0f;
}
@end

它是如何工作的...

按下 B 按钮会触发枪口闪光,发射子弹对象,并弹出已使用的弹壳。枪口闪光只是一个动画,但子弹和弹壳是物理对象。

  • 设置子弹标志:

    在菜谱中,我们将子弹身体的标志设置为识别它为一个快速移动的投射物:

    bullet.bodyDef->bullet = YES;
    
    

    设置此标志允许子弹正确地与其他动态物体发生碰撞。当两个动态物体发生碰撞时,Box2D 仅在每个离散物理步骤中执行碰撞检测。这意味着在每个循环中,所有动态物理物体都有离散的位置。正因为如此,当一个物体移动得足够快时,它有可能穿过它应该与之碰撞的物体。将这个快速移动的物体指定为子弹,允许 Box2D 执行连续碰撞检测,使该物体能够以任何速度与其他动态物体发生碰撞。

  • 动画子弹撞击:

    在我们的示例中,我们使用了一些三角学来确定子弹落在二维盒形物体的外围。对于更复杂的形状,你可以从 Box2D 求解器中检索接触法线。这将有助于确定两个物体确切碰撞的位置。有关 接触法线 的更多信息,请参阅 Box2D 文档。

模拟和渲染线索

Box2D 库最近新增了 b2RopeJoint 功能。在本教程中,我们将了解如何实现这一功能在物理和视觉上的应用。

模拟和渲染线索

准备工作

请参考项目 RecipeCollection02 以获取本教程的完整工作代码。同时请注意,为了简洁,以下代码中省略了一些内容。

如何实现...

执行以下代码:

#import "VRope.h"
@implementation Ch4_Rope
-(CCLayer*) runRecipe {
[super runRecipe];
[message setString:@"Press B to fire a rope."];
//Initialization
onRope = NO;
ropeUseTimer = 0;
//Move gunman to left
gunman.body->SetTransform(b2Vec2(2,10), gunman.body->GetAngle());
//Create buttons
[self createButtonWithPosition:ccp(340,75) withUpFrame:@"b_button_up.png" withDownFrame:@"b_button_down.png" withName:@"B"];
[self createButtonWithPosition:ccp(420,75) withUpFrame:@"a_button_up.png" withDownFrame:@"a_button_down.png" withName:@"A"];
//Create ground
/* CODE OMITTED */
//Add invisible rope anchor
[self addRopeAnchor];
ropesimulatingreturn self;
}
-(void) step:(ccTime)delta {
[super step:delta];
//Process button input
for(id b in buttons){
GameButton *button = (GameButton*)b;
if(button.pressed && [button.name isEqualToString:@"B"]){
if(!onRope){
[self useRope];
}else{
[self releaseRope];
}
}
if(button.pressed && [button.name isEqualToString:@"A"]){
if(onRope){
[self releaseRope];
}else{
[self processJump];
}
}else if(!button.pressed && [button.name isEqualToString:@"A"]){
jumpCounter = -10.0f;
}
}
//Update all ropes
for(id v in vRopes){
VRope *rope = (VRope*)v;
[rope update:delta];
[rope updateSprites];
}
//Decrement our use timer
ropesimulatingropeUseTimer -= delta;
}
-(void) addRopeAnchor {
//Add rope anchor body
b2BodyDef anchorBodyDef;
anchorBodyDef.position.Set(240/PTM_RATIO,350/PTM_RATIO); //center body on screen
anchorBody = world->CreateBody(&anchorBodyDef);
//Add rope spritesheet to layer
ropeSpriteSheet = [CCSpriteBatchNode batchNodeWithFile:@"rope.png" ];
[self addChild:ropeSpriteSheet];
//Init array that will hold references to all our ropes
vRopes = [[[NSMutableArray alloc] init] autorelease];
}
-(void) useRope {
if(ropeUseTimer > 0){
return;
}else{
ropeUseTimer = 0.2f;
}
//The rope joint goes from the anchor to the gunman
b2RopeJointDef jd;
jd.bodyA = anchorBody;
jd.bodyB = gunman.body;
jd.localAnchorA = b2Vec2(0,0);
jd.localAnchorB = b2Vec2(0,0);
jd.maxLength= (gunman.body->GetPosition() - anchorBody->GetPosition()).Length();
ropesimulating//Create VRope with two b2bodies and pointer to spritesheet
VRope *newRope = [[VRope alloc] init:anchorBody body2:gunman.body spriteSheet:ropeSpriteSheet];
//Create joint
newRope.joint = world->CreateJoint(&jd);
[vRopes addObject:newRope];
//Keep track of 'onRope' state
onRope = !onRope;
}
-(void) releaseRope {
if(ropeUseTimer > 0){
return;
}else{
ropeUseTimer = 0.2f;
}
//Jump off the rope
[self processJump];
//Destroy the rope
for(id v in vRopes){
VRope *rope = (VRope*)v;
world->DestroyJoint(rope.joint);
[rope removeSprites];
[rope release];
}
[vRopes removeAllObjects];
//Keep track of 'onRope' state
onRope = !onRope;
}
@end

工作原理...

按下 B 键将线索射入中间缺口上方的关卡。这允许枪手跨越缺口。

  • 使用线索关节:

    线索关节的初始化方式与其他关节类似。它连接两个物体在两个特定的局部点:

    b2RopeJointDef jd;
    jd.bodyA = anchorBody;
    jd.bodyB = gunman.body;
    jd.localAnchorA = b2Vec2(0,0);
    jd.localAnchorB = b2Vec2(0,0);
    
    

    然后我们设置最大线索长度并创建关节:

    jd.maxLength= (gunman.body->GetPosition() - anchorBody->GetPosition()).Length();
    newRope.joint = world->CreateJoint(&jd);
    
    

    这允许用户在锚点周围的圆形弧线上摆动。

  • 使用 VRope

    VRope 类允许我们可视化线索。VRope 的一个实例存储了两个连接的物体和连接关节,然后在每一帧中创建线索的真实描绘:

    //Update all ropes
    for(id v in vRopes){
    VRope *rope = (VRope*)v;
    [rope update:delta];
    [rope updateSprites];
    }
    
    

    从原始的 Pitfall 街机游戏到较新的 Worms 游戏,线索在游戏中已经使用了多年。它们以比仅仅跑步和跳跃更动态的方式将玩家与世界连接起来。

创建俯视等距游戏引擎

通过对 Box2D 进行一些修改,我们可以将二维世界转变为 2.5D 世界。在本教程中,我们将看到 2.5D 沙盒 的实际应用。

创建俯视等距游戏引擎

准备工作

请参考项目 RecipeCollection02 以获取本教程的完整工作代码。同时请注意,为了简洁,本教程中省略了大量代码。

如何实现...

执行以下代码:

@interface GameIsoObject : GameObject {
@public
float yModifier; //This is typically half the height of the object. It allows us to change the sprite y.
float actualImageSize; //This is the actual size of the image (48x48, 96x96, etc)
float inGameSize; //This is how large the object in the game is.
float zModifier; //Changes the depth testing for this object.
CCSprite *spriteShadow;
Vector3D *bounceCoefficient; //x, y, z, lower is bouncier for Z
Vector3D *rollCoefficient;
}
@end
/* IsometricContactListener.h */
class isometricContactListener : public b2ContactListener
{
public:
void BeginContact(b2Contact* contact);
void EndContact(b2Contact* contact);
void PreSolve(b2Contact* contact, const b2Manifold* oldManifold);
};
top-down isometric game enginecreatingvoid isometricContactListener::BeginContact(b2Contact* contact)
{
b2Body *bodyA = contact->GetFixtureA()->GetBody();
b2Body *bodyB = contact->GetFixtureB()->GetBody();
if(bodyA and bodyB){
float lowerZSize;
if(bodyA->GetZPosition() < bodyB->GetZPosition()){ lowerZSize = bodyA->GetZSize(); }
else{ lowerZSize = bodyB->GetZSize(); }
//Check for Z Miss and disable collision if neccessary
if( absoluteValue(bodyA->GetZPosition() - bodyB->GetZPosition()) > lowerZSize ) { //If distance is greater than the height of the bottom one
contact->SetEnabled(false);
if(bodyA->GetHandleZMiss() || bodyB->GetHandleZMiss()){
GameObject *gameObjectA = (GameObject*)bodyA->GetUserData();
GameObject *gameObjectB = (GameObject*)bodyB->GetUserData();
[gameObjectA->gameArea handleZMissWithObjA:gameObjectA withObjB:gameObjectB];
bodyA->SetHandleZMiss(false);
bodyB->SetHandleZMiss(false);
}
//If no Z Miss handle collision
}else {
GameObject *gameObjectA = (GameObject*)bodyA->GetUserData();
GameObject *gameObjectB = (GameObject*)bodyB->GetUserData();
[gameObjectA->gameArea handleCollisionWithObjA:gameObjectA withObjB:gameObjectB];
top-down isometric game enginecreating}
}
}
/* END IsometricContactListener.h */
@implementation Ch4_TopDownIsometric
-(CCLayer*) runRecipe {
[super runRecipe];
//Iso debug drawing
m_debugDraw = new IsoGLESDebugDraw( PTM_RATIO, PERSPECTIVE_RATIO, gameAreaSize );
world->SetDebugDraw(m_debugDraw);
//Special isometric gravity, contact filter and contact listener
world->SetGravity(b2Vec2(0,0));
world->SetContactListener(new isometricContactListener);
return self;
}
/* We override all physical calculations here */
-(void) step: (ccTime) delta {
//Update Physics
int32 velocityIterations = 8;
int32 positionIterations = 3;
world->Step(delta, velocityIterations, positionIterations);
float deltaMod = delta/0.01666666667f;
for (b2Body* b = world->GetBodyList(); b; b = b->GetNext()) {
//Z Miss handling allows us to know when an object passes over or under another object
b->SetHandleZMiss(YES);
if (b->GetUserData() != NULL) {
//Synchronize the sprites position and rotation with the corresponding body
top-down isometric game enginecreatingGameIsoObject *gameObject = (GameIsoObject*)b->GetUserData();
if(gameObject.sprite) {
if(gameObject.bodyDef->type == b2_dynamicBody){
//Process Z velocity and position
gameObject.body->SetZVelocity( gameObject.body->GetZVelocity() - GRAVITY*deltaMod );
gameObject.body->SetZPosition( gameObject.body->GetZPosition() + gameObject.body->GetZVelocity()*deltaMod );
//Process object bouncing and rolling
if(gameObject.body->GetZPosition() < (-0.01f)){
gameObject.body->SetZPosition(0.01f);
gameObject.body->SetZVelocity( gameObject.body->GetZVelocity() * -1 );
b2Vec2 worldVector = gameObject.body->GetLinearVelocityFromLocalPoint(b2Vec2(0,0));
if(absoluteValue(gameObject.body->GetZVelocity()) > 1.0f){
[self handleCollisionWithGroundWithObj:gameObject];
gameObject.body->ApplyLinearImpulse( b2Vec2( gameObject.bounceCoefficient.x*worldVector.x*-1, gameObject.bounceCoefficient.y*worldVector.y*-1 ), gameObject.body->GetPosition() );
gameObject.body->SetZVelocity( gameObject.body->GetZVelocity() * (1-gameObject.bounceCoefficient.z) );
}else{
gameObject.body->ApplyLinearImpulse( b2Vec2( gameObject.rollCoefficient.x*worldVector.x*-1, gameObject.rollCoefficient.y*worldVector.y*-1 ), gameObject.body->GetPosition() );
gameObject.body->SetZVelocity( gameObject.body->GetZVelocity() * (1-gameObject.rollCoefficient.z) );
}
}
//Change sprite positions based on body positions
gameObject.sprite.position = CGPointMake( convertPositionX(gameAreaSize, b->GetPosition().x * PTM_RATIO), convertPositionY(gameAreaSize, b->GetPosition().y * PTM_RATIO * PERSPECTIVE_RATIO) + gameObject.yModifier + gameObject.body->GetZPosition() * zHeightModifier * PERSPECTIVE_RATIO);
gameObject.spriteShadow.position = CGPointMake( convertPositionX(gameAreaSize, b->GetPosition().x * PTM_RATIO), convertPositionY(gameAreaSize, b->GetPosition().y * PTM_RATIO * PERSPECTIVE_RATIO));
//Modify sprite scale based on Z (height)
[gameObject.sprite setScale:( gameObject.body->GetZPosition()*scaleHeightMultiplier + gameObject->inGameSize/gameObject->actualImageSize )];
gameObject.spriteShadow.scale = gameObject.body->GetZPosition()/100;
if(gameObject.spriteShadow.scale > 1){ gameObject.spriteShadow.scale = 1; }
//Sprite depth testing based on Y (depth)
[self setZOrderByBodyPosition:gameObject];
}else if(gameObject.bodyDef->type == b2_staticBody){
//Static bodies are only positioned and depth tested
gameObject.sprite.position = CGPointMake( convertPositionX(gameAreaSize, b->GetPosition().x * PTM_RATIO), convertPositionY(gameAreaSize, b->GetPosition().y * PTM_RATIO * PERSPECTIVE_RATIO) + gameObject.yModifier + gameObject.body->GetZPosition() * zHeightModifier * PERSPECTIVE_RATIO);
[self setZOrderByBodyPosition:gameObject];
gameObject.spriteShadow.position = CGPointMake( convertPositionX(gameAreaSize, b->GetPosition().x * PTM_RATIO), convertPositionY(gameAreaSize, b->GetPosition().y * PTM_RATIO * PERSPECTIVE_RATIO));
}
}
}
}
//Process body creation/destruction
[self destroyBodies];
[self createBodies];
[self runQueuedActions];
//Follow gunman with camera
gameNode.position = ccp((-gunman.spriteShadow.position.x)*cameraZoom + 240, (-gunman.spriteShadow.position.y)*cameraZoom + 160);
}
top-down isometric game enginecreating/* Fire a bouncy ball */
-(void) fireBall {
if(fireCount < 0){
GameIsoObject *ball = [self addBallAtPoint:ccp(gunman.body->GetPosition().x*PTM_RATIO + lastPressedVector.x*20.0f, gunman.body->GetPosition().y*PTM_RATIO*PERSPECTIVE_RATIO + lastPressedVector.y*20.0f)];
ball.body->ApplyLinearImpulse(b2Vec2(lastPressedVector.x*1.75f, lastPressedVector.y*1.75f), ball.body->GetPosition());
ball.body->SetZVelocity( gunman.body->GetZVelocity()*5.0f + 10.0f );
ball.body->SetZPosition( gunman.body->GetZPosition() + 40.0f);
fireCount = 10;
}else{
fireCount--;
}
}
/* Process a jump */
-(void) processJump {
//You can only jump if you are standing or running. You also need to be on the ground.
if(gunman.body->GetZPosition() > 1.0f){
return;
}
//Make him jump
[[gunman.sprite getChildByTag:0] stopAllActions];
gunman.body->SetZVelocity(7.5f);
}
/* Convert a body position to a world position */
-(CGPoint) getWorldPosition:(GameIsoObject*)g {
return CGPointMake(g.body->GetPosition().x * PTM_RATIO, g.body->GetPosition().y * PTM_RATIO * PERSPECTIVE_RATIO);
}
/* A camera bound limiting routine */
- (bool) checkCameraBoundsWithFailPosition:(CGPoint*)failPosition {
CGSize screenSize = [CCDirector sharedDirector].winSize;
bool passed = true;
top-down isometric game enginecreatingfloat fsx = (gameAreaSize.x/2)*cameraZoom;
float fsy = (gameAreaSize.y/2)*cameraZoom;
float ssx = screenSize.width;
float ssy = screenSize.height;
if( [gameNode position].y < -(fsy - ssy) ) {
(*failPosition).y = -(fsy - ssy);
passed = false;
}else if( [gameNode position].y > fsy) {
(*failPosition).y = fsy;
passed = false;
}else{ //Passed
(*failPosition).y = [gameNode position].y;
}
if( [gameNode position].x < -(fsx - ssx) ) {
(*failPosition).x = -(fsx - ssx);
passed = false;
}else if( [gameNode position].x > fsx) {
(*failPosition).x = fsx;
passed = false;
}else { //Passed
(*failPosition).x = [gameNode position].x;
}
return passed;
}
/* Depth testing */
-(void) setZOrderByBodyPosition:(GameIsoObject*)g {
float fixedPositionY = gameAreaSize.y - (g.body->GetPosition().y * PTM_RATIO * PERSPECTIVE_RATIO) + g.zModifier;
[g.sprite.parent reorderChild:g.sprite z:fixedPositionY];
}
/* Add a tree object */
-(void) addTreeAtPoint:(CGPoint)treePosition {
GameIsoObject *tree = [[GameIsoObject alloc] init];
/* CODE OMITTED */
}
/* Add a ball with a random size at a position */
-(GameIsoObject*) addBallAtPoint:(CGPoint)ballPosition {
GameIsoObject *ball = [[GameIsoObject alloc] init];
//Bounce and roll coefficients determine how high the ball boucnes and how fast the ball rolls
ball.bounceCoefficient = [Vector3D x:0.05f y:0.05f z:0.1f*scaleMod];
top-down isometric game enginecreatingball.rollCoefficient = [Vector3D x:0.0005f y:0.0005f z:0.5f];
/* CODE OMITTED */
return ball;
}
@end

工作原理...

在本教程中,我们控制枪手在一个伪 3D 世界中奔跑。按下 B 键使他向空中发射彩色弹球。按下 A 键使他跳跃。与之前的教程类似,用户可以捏合来缩放。

  • Box2D 修改:

    为了创建一个相对逼真的 3D 效果,我们需要在b2Body类内部存储更多的数据。通过在 RecipeCollection02 项目中搜索字符串"Isometric Additions",你将找到四组对b2Body类的扩展。这些更改添加了以下变量:

    float32 m_zPosition;
    float32 m_zSize;
    float32 m_zVelocity;
    bool m_handleZMiss;
    
    

    位置、大小和速度变量使我们能够在 Z 平面上进行一些基本的物理计算。m_handleZMiss变量告诉我们,当一个对象在 Z 平面上经过另一个对象上方或下方时,是否向回调方法发送消息。

  • GameIsoObject类:

    这个新类添加了一些我们可以用在我们的引擎中的新变量。特别是,yModifierzModifier是简单的值,分别用于帮助精灵定位和深度测试。变量actualImageSizeinGameSize有助于确定基线图像缩放。

  • isometricContactListener类:

    在这里,我们使用我们的接触监听器来检查第三维或'Z'维的碰撞。如果两个物体在 X 和 Y 轴上相撞,但在 Z 轴上错过,那么我们禁用物理碰撞响应:

    contact->SetEnabled(false);
    
    

    我们还在相应的 gameArea 实例上调用以下函数:

    -(void) handleZMissWithObjA:(GameObject*)objA withObjB:(GameObject*)objB;
    
    

    这很有用,例如,当你想确定某人是否跳过了栅栏,或者在一个体育视频游戏中球是否越过了墙壁。

  • Z 平面的物理:

    这个配方覆盖了正常的step例程以使用自己的自定义例程。在这里,我们处理 Z 物理。这涉及到假设一个任意、静态的地面在 Z=0。物体受到一个GRAVITY常数的作用,该常数随着每一步时间的流逝而减少 Z 速度。这确保了 Z 轴上的物理与 Box2D 物理保持同步。

  • 弹跳和滚动:

    每个GameIsoObject都有一个bounceCoefficientrollCoefficient。这些Vector3D实例决定了所有三个平面上物体的恢复力和摩擦力:

    ball.bounceCoefficient = [Vector3D x:0.05f y:0.05f z:0.1f*scaleMod];
    ball.rollCoefficient = [Vector3D x:0.0005f y:0.0005f z:0.5f];
    
    

    这与 Box2D 阻尼、恢复力和摩擦变量相结合,允许进行大量的自定义。

  • PERSPECTIVE_RATIO和深度测试:

    这个等距设置假设一个透视比为0.5f。这意味着,对于 Y 轴上的每1.0f距离,我们在屏幕上只能看到0.5f的距离。用更简单的话说,你可以认为这相当于摄像机以 45 度角向下看地面。这作为我们强制透视的数学参考。因此,当我们从物理转换到视觉(或反之亦然)时,我们在 X 和 Y 轴上使用PTM_RATIO,在 Y 轴上只使用PERSPECTIVE_RATIO。在我们的步进例程中,我们还应用了深度测试。为此,我们只需使用 Y 轴上的物体位置来确定物体深度。所有精灵都会持续重新排序。

  • Z 轴上的阴影和图像缩放:

    为了进一步增强等距视角的视觉效果,我们使用 spriteShadow 变量在每一个物体下添加阴影。这个阴影始终保持在高度 Z=0 的位置。当物体向上移动时,它会增加大小。此外,高海拔的物体也会稍微放大。这就是为什么我们需要为我们的 GameIsoObject 精灵设置一个基线缩放。当这两个效果结合在一起时,它们会给人更强的 3D 空间印象。此外,正如您从树木和墙壁中看到的,阴影可以精细绘制(树木)或程序生成(墙壁)。

  • 相机限制和缩放:

    在以下方法中,我们限制相机在gameArea边缘之外的移动:

    - (bool) checkCameraBoundsWithFailPosition:(CGPoint*)failPosition;
    
    

    然后将此方法与捏合缩放结合,使用户能够放大和缩小以进一步查看游戏区域。如果您计划使用此游戏中的缩放功能,我强烈建议使用米派映射来平滑缩小的纹理。

更多内容...

之前提供的 Z 平面的物理模拟非常简单。例如,此示例不允许地形高度变化或 Z 轴上的正确物理碰撞响应。不幸的是,更复杂的模拟超出了本书的范围。

  • 带有深度的侧滚动:

    这种技术可以很容易地修改,以创建侧滚动等距场景,而不是俯视等距场景。这方面的例子包括世嘉 Genesis 游戏机的《街头霸王》和《NBA Jam》。游戏角度将被缩短到0.25f或更低。此外,自定义的 Z 轴应表示深度而不是高度,因为大多数物理交互都发生在宽度方向上。

第五章. 场景和菜单

在本章中,我们将涵盖以下主题:

  • 切换场景

  • 场景之间的转换

  • 使用 CCLayerMultiplex

  • 使用 CCLabel

  • 使用 CCMenu

  • 创建带阴影的菜单标签

  • UIKit 警告对话框

  • 包装 UIKit

  • 创建可拖拽的菜单窗口

  • 创建水平可滚动菜单

  • 创建垂直滑动菜单网格

  • 带指示器的加载屏幕创建

  • 创建缩略图

简介

所有游戏都有辅助的图形用户界面(GUI)需求,如菜单和游戏中的抬头显示(HUD)。在本章中,我们将解释创建这些元素的技术以及如何将它们整合到场景的基本结构中。

切换场景

场景是基本的高级CCNode对象。所有其他节点都被视为场景的子节点。一次只能运行一个场景。场景使用堆栈数据结构进行管理。在本菜谱中,我们将看到如何推入弹出场景到堆栈上。

切换场景

准备工作

请参考项目 RecipeCollection02 以获取此菜谱的完整工作代码。

如何做到这一点...

执行以下代码:

// TreeSceneMenu
// The node for our binary tree of scenes
@interface TreeSceneMenu : CCLayer {
NSString *name;
}
+(id) sceneWithString:(NSString*)str;
-(id) initWithString:(NSString*)str;
-(void) goToScene1:(id)sender;
-(void) goToScene2:(id)sender;
-(void) back:(id)sender;
@end
@implementation TreeSceneMenu
+(id) sceneWithString:(NSString*)str {
//Initialize our scene
CCScene *s = [CCScene node];
TreeSceneMenu *node = [[TreeSceneMenu alloc] initWithString:str];
[s addChild:node z:0 tag:0];
return s;
}
-(id) initWithString:(NSString*)str {
if( (self=[super init] )) {
//Set scene name
name = [NSString stringWithFormat:@"%@",str];
[name retain];
/* CODE OMITTED */
//Buttons to push new scenes onto the stack
CCMenuItemFont *scene1Item = [CCMenuItemFont itemFromString:[NSString stringWithFormat:@"Scene %@.1",name] target:self selector:@selector(goToScene1:)];
CCMenuItemFont *scene2Item = [CCMenuItemFont itemFromString:[NSString stringWithFormat:@"Scene %@.2",name] target:self selector:@selector(goToScene2:)];
//If we are at the root we "Quit" instead of going "Back"
NSString *backStr = @"Back";
if([str isEqualToString:@"1"]){
backStr = @"Quit";
}
CCMenuItemFont *backItem = [CCMenuItemFont itemFromString:backStr target:self selector:@selector(back:)];
//Add menu items
CCMenu *menu = [CCMenu menuWithItems: scene1Item, scene2Item, backItem, nil];
[menu alignItemsVertically];
[self addChild:menu];
}
return self;
}
//Push scene 1
-(void) goToScene1:(id)sender {
[[CCDirector sharedDirector] pushScene:[TreeSceneMenu sceneWithString:[NSString stringWithFormat:@"%@.1",name]]];
}
//Push scene 2
-(void) goToScene2:(id)sender {
[[CCDirector sharedDirector] pushScene:[TreeSceneMenu sceneWithString:[NSString stringWithFormat:@"%@.2",name]]];
}
//Pop scene
-(void) back:(id)sender {
[[CCDirector sharedDirector] popScene];
}
@end
//Our Base Recipe
@interface Ch5_SwitchingScenes : Recipe{}
-(CCLayer*) runRecipe;
-(void) goToScene1:(id)sender;
@end
@implementation Ch5_SwitchingScenes
-(CCLayer*) runRecipe {
[super runRecipe];
//Go to our initial scene
CCMenuItemFont *goToScene1 = [CCMenuItemFont itemFromString:@"Go To Scene 1" target:self selector:@selector(goToScene1:)];
CCMenu *menu = [CCMenu menuWithItems: goToScene1, nil];
[menu alignItemsVertically];
[self addChild:menu];
return self;
}
scenesswitching//Push initial scene
-(void) goToScene1:(id)sender {
[[CCDirector sharedDirector] pushScene:[TreeSceneMenu sceneWithString:@"1"]];
}
@end

它是如何工作的...

在 Cocos2d 中,常见的做法是在创建简单场景时对CCLayer进行子类化。这允许我们仅使用一个类将我们的单个CCSceneCCLayer耦合。这个类继承自CCLayer,但它有一个返回自身包装在CCScene中的类方法:

+(id) sceneWithString:(NSString*)str {
//Initialize our scene
CCScene *s = [CCScene node];
TreeSceneMenu *node = [[TreeSceneMenu alloc] initWithString:str];
[s addChild:node z:0 tag:0];
return s;
}

在这个例子中,我们的类名为TreeSceneMenu。通过按下其两个按钮之一,可以将另一个场景推入堆栈,并带有适当的子字符串名称。这创建了一个可能的场景组合的二叉树。弹出根场景将把你带回主菜谱选择场景。

  • 层与场景:

    层/场景的区别主要是一个形式上的区别。场景将游戏的最基本部分分离出来,并被视为这样。例如,在场景之间切换时可以使用多种过渡效果(见下一个菜谱)。另一方面,层被设计为是唯一直接添加到场景中的节点。层是所有动作发生的地方。其他节点附着在其上,并实现TouchEventsDelegate协议以处理输入。场景和层之间的唯一区别是,当场景被推入堆栈时,场景需要更多的内存和处理器的开销。因此,在任何时候堆栈上的场景越少,越好。

场景之间的转换

如前一个菜谱中提到的,场景是根 CCNodes,一次只能运行一个。在场景之间切换时,我们可以应用过渡效果,使场景变化更加明确和时尚。在这个菜谱中,你可以演示所有内置的场景过渡效果。

场景转换

准备工作

请参考项目 RecipeCollection02 以获取此菜谱的完整工作代码。

如何做到这一点...

执行以下代码:

@implementation TransSceneMenu
+(id) sceneWithString:(NSString*)str withCurrentTransition:(int)ct {
//Create scene
CCScene *s = [CCScene node];
TransSceneMenu *node = [[TransSceneMenu alloc] initWithString:str withCurrentTransition:ct];
[s addChild:node z:0 tag:0];
return s;
}
-(id) initWithString:(NSString*)str withCurrentTransition:(int)ct {
if( (self=[super init] )) {
name = str;
currentTransition = ct;
/* CODE OMITTED */
}
return self;
}
-(void) prevScene:(id)sender {
currentTransition--;
if(currentTransition < 0){
currentTransition = numTransitionTypes-1;
}
[self loadNewScene];
}
-(void) nextScene:(id)sender {
currentTransition++;
if(currentTransition >= numTransitionTypes){
currentTransition = 0;
}
[self loadNewScene];
}
-(void) randomScene:(id)sender {
currentTransition = arc4random()%numTransitionTypes;
[self loadNewScene];
}
-(void) loadNewScene {
[[CCDirector sharedDirector] popScene];
NSString *className = [NSString stringWithFormat:@"%@",transitionTypes[currentTransition]];
Class clazz = NSClassFromString (className);
[[CCDirector sharedDirector] pushScene: [clazz transitionWithDuration:1.2f scene:[TransSceneMenu sceneWithString:className withCurrentTransition:currentTransition]]];
}
-(void) quit:(id)sender {
[[CCDirector sharedDirector] popScene];
}
@end
//Our Base Recipe
@interface Ch5_SceneTransitions : Recipe{}
-(CCLayer*) runRecipe;
-(void) viewTransitions:(id)sender;
@end
@implementation Ch5_SceneTransitions
-(CCLayer*) runRecipe {
[super runRecipe];
CCMenuItemFont *viewTransitions = [CCMenuItemFont itemFromString:@"View Transitions" target:self selector:@selector(viewTransitions:)];
CCMenu *menu = [CCMenu menuWithItems: viewTransitions, nil];
[menu alignItemsVertically];
[self addChild:menu];
return self;
}
-(void) viewTransitions:(id)sender {
[[CCDirector sharedDirector] pushScene:[TransSceneMenu sceneWithString:@"" withCurrentTransition:0]];
}
@end

它是如何工作的...

当使用常规的 CCScene 实例化 CCTransitionScene 类并立即将其推入场景栈时,它将创建一个过渡效果。这是通过以下行完成的:

[[CCDirector sharedDirector] pushScene: [CCTransitionFade transitionWithDuration:1.2f scene:[MyScene scene] withColor:ccWHITE]];

在这个例子中,我们在加载 MyScene 场景时使用了一个“淡入白色”的过渡效果。以下是内置的 Cocos2d 过渡类列表:

CCTransitionFadeTR, CCTransitionJumpZoom, CCTransitionMoveInL, CCTransitionSplitCols, CCTransitionSceneOriented, CCTransitionPageTurn, CCTransitionRadialCCW, CCTransitionFade, CCTransitionRotoZoom, CCTransitionShrinkGrow, CCTransitionSlideInL,以及 CCTransitionTurnOffTiles

还有更多...

除了使用过渡效果推送场景外,你还可以通过向 CCDirector 类添加以下方法来使用过渡效果弹出场景:

//CCDirector.h
- (void) popSceneWithTransition: (Class)transitionClass duration:(ccTime)t;
//CCDirector.m
-(void) popSceneWithTransition: (Class)transitionClass duration:(ccTime)t {
NSAssert( runningScene_ != nil, @"A running Scene is needed");
[scenesStack_ removeLastObject];
NSUInteger c = [scenesStack_ count];
if( c == 0 ) {
[self end];
} else {
CCScene* scene = [transitionClass transitionWithDuration:t scene:[scenesStack_ objectAtIndex:c-1]];
[scenesStack_ replaceObjectAtIndex:c-1 withObject:scene];
nextScene_ = scene;
}
}

这将以一个漂亮的过渡效果弹出场景。

使用 CCLayerMultiplex

CCLayerMultiplex 类提供了在多个层之间无缝切换的功能。在这个例子中,我们有三层相似的层分配给一个复用层。每个层显示用于切换到其他任何层的按钮。

使用 CCLayerMultiplex

准备中

请参考项目 RecipeCollection02 以获取此菜谱的完整工作代码。

如何操作...

执行以下代码:

@interface MultiplexLayerMenu : CCLayer {}
+(id) layerWithLayerNumber:(int)layerNumber;
-(id) initWithLayerNumber:(int)layerNumber;
-(void) goToLayer:(id)sender;
@end
@implementation MultiplexLayerMenu
+(id) layerWithLayerNumber:(int)layerNumber {
return [[[MultiplexLayerMenu alloc] initWithLayerNumber:layerNumber] autorelease];
}
CCLayerMultiplexusing-(id) initWithLayerNumber:(int)layerNumber {
if( (self=[super init] )) {
//Random background color
CCSprite *bg = [CCSprite spriteWithFile:@"blank.png"];
bg.position = ccp(240,160);
[bg setTextureRect:CGRectMake(0,0,480,320)];
[bg setColor:ccc3(arc4random()%150,arc4random()%150,arc4random()%150)];
[self addChild:bg];
//Layer number as message
CCLabelBMFont *message = [CCLabelBMFont labelWithString:[NSString stringWithFormat:@"Layer %i",layerNumber+1] fntFile:@"eurostile_30.fnt"];
message.position = ccp(160,270);
message.scale = 0.75f;
[message setColor:ccc3(255,255,255)];
[self addChild:message z:10];
//Buttons to go to different layers
CCMenuItemFont *goToLayer1 = [CCMenuItemFont itemFromString:@"Go To Layer 1" target:self selector:@selector(goToLayer:)];
CCMenuItemFont *goToLayer2 = [CCMenuItemFont itemFromString:@"Go To Layer 2" target:self selector:@selector(goToLayer:)];
CCMenuItemFont *goToLayer3 = [CCMenuItemFont itemFromString:@"Go To Layer 3" target:self selector:@selector(goToLayer:)];
goToLayer1.tag = 0; goToLayer2.tag = 1; goToLayer3.tag = 2;
//Add menu items
CCMenu *menu = [CCMenu menuWithItems: goToLayer1, goToLayer2, goToLayer3, nil];
[menu alignItemsVertically];
[self addChild:menu];
}
return self;
}
//Switch to a different layer
-(void) goToLayer:(id)sender {
CCMenuItemFont *item = (CCMenuItemFont*)sender;
[(CCLayerMultiplex*)parent_ switchTo:item.tag];
}
@end
@interface Ch5_UsingCCLayerMultiplex : Recipe{}
-(CCLayer*) runRecipe;
@end
@implementation Ch5_UsingCCLayerMultiplex
-(CCLayer*) runRecipe {
[super runRecipe];
//Create our multiplex layer with three MultiplexLayerMenu objects
CCLayerMultiplex *layer = [CCLayerMultiplex layerWithLayers: [MultiplexLayerMenu layerWithLayerNumber:0], [MultiplexLayerMenu layerWithLayerNumber:1],
[MultiplexLayerMenu layerWithLayerNumber:2], nil];
[self addChild: layer z:0];
return self;
}
@end

它是如何工作的...

这种技术提供了一种不同于场景切换之间的控制流风格的替代方式。它允许实例化多个层,并动态地激活和挂起这些层。通过以下类方法创建复用层:

+(id) layerWithLayers: (CCLayer*) layer, ... NS_REQUIRES_NIL_TERMINATION;

有许多方法可以利用这项技术。它提供了一个平铺的替代方案,用于分层堆叠场景。

使用 CCLabel

在整本书中,我们一直在使用多种不同的标签类型。在这个菜谱中,我们将简要解释三个常用的标签类:CCLabelAtlas, CCLabelBMFont,CCLabelTTF

使用 CCLabel

准备中

请参考项目 RecipeCollection02 以获取此菜谱的完整工作代码。

如何操作...

执行以下代码:

@implementation Ch5_UsingCCLabel
-(CCLayer*) runRecipe {
[super runRecipe];
//CCLabelAtlas for fixed-width bitmap fonts
CCLabelAtlas *labelAtlas = [CCLabelAtlas labelWithString:@"Atlas Label Test" charMapFile:@"tuffy_bold_italic-charmap.png" itemWidth:48 itemHeight:65 startCharMap:' '];
[self addChild:labelAtlas z:0];
labelAtlas.anchorPoint = ccp(0.5f,0.5f);
labelAtlas.scale = 0.5f;
labelAtlas.position = ccp(240,220);
[labelAtlas setColor:ccc3(0,255,0)];
[labelAtlas runAction:[CCRepeatForever actionWithAction: [CCSequence actions: [CCScaleTo actionWithDuration:1.0f scale:0.5f], [CCScaleTo actionWithDuration:1.0f scale:0.25f], nil]]];
//CCLabelBMFont for variable-width bitmap fonts using FNT files
CCLabelBMFont *labelBMFont = [CCLabelBMFont labelWithString:@"Bitmap Label Test" fntFile:@"eurostile_30.fnt"];
[self addChild:labelBMFont z:0];
labelBMFont.position = ccp(240,160);
for(id c in labelBMFont.children){
CCSprite *child = (CCSprite*)c;
[child setColor:ccc3(arc4random()%255,arc4random()%255,arc4random()%255)];
[child runAction:[CCRepeatForever actionWithAction:
[CCSequence actions: [CCScaleTo actionWithDuration:arc4random()%2+1 scale:1.75f], [CCScaleTo actionWithDuration:arc4random()%2+1 scale:0.75f], nil]
]];
}
//CCLabelTTF for true-type fonts
CCLabelTTF *labelTTF = [CCLabelTTF labelWithString:@"True-Type Label Test" fontName:@"arial_narrow.otf" fontSize:32];
[self addChild:labelTTF z:0];
labelTTF.position = ccp(240,100);
[labelTTF runAction:[CCRepeatForever actionWithAction: [CCSequence actions: [CCScaleTo actionWithDuration:2.0f scale:1.5f], [CCScaleTo actionWithDuration:2.0f scale:0.5f], nil]]];
[labelTTF setColor:ccc3(0,0,255)];
return self;
}
@end

它是如何工作的...

每种标签类型都有其优缺点。

  • CCLabelAtlas:

    在屏幕上绘制文本的最简单方法是使用 CCLabelAtlas 类。这允许你绘制固定宽度的位图字体。这是一个低技术解决方案,本质上是通过标准 ASCII 值顺序索引纹理文件。提供的唯一元信息是字符大小和地图中的第一个字符。

  • CCLabelBMFont:

    CCLabelBMFont 类具有位图字体绘制的速度优势,以及许多其他功能。它使用 FNT 文件格式来存储非固定宽度位图字体。这些字体可以使用包括 Hiero 在内的多个编辑器创建,Hiero 可以在以下网址找到:www.n4te.com/hiero/hiero.jnlpCCLabelBMFont 将每个字符视为 CCSprite 子节点。这允许我们单独操作它们。

  • CCLabelTTF:

    最后,CCLabelTTF 类允许绘制 TrueType 字体。这允许使用内置系统字体以及其他您指定的 TrueType 字体。必须注意,TrueType 字体的渲染速度较慢,应仅用于静态文本。对于将频繁更新的文本(如得分显示)应使用位图字体。

使用 CCMenu

Cocos2d 提供的菜单工具使得创建简单菜单的过程变得非常简单。在这个例子中,我们将看到如何创建简单菜单,调整菜单对齐,启用/禁用菜单项,等等。

使用 CCMenu

准备工作

请参考项目 RecipeCollection02 以获取此菜谱的完整工作代码。

如何操作...

执行以下代码:

@implementation OptionsMenu
+(id) scene {
//Create a scene
CCScene *s = [CCScene node];
OptionsMenu *node = [OptionsMenu node];
[s addChild:node z:0 tag:0];
return s;
}
-(id) init {
if( (self=[super init] )) {
/* CODE OMITTED */
//Disabled title label for Sound option
CCMenuItemFont *title1 = [CCMenuItemFont itemFromString:@"Sound"];
[title1 setIsEnabled:NO];
title1.color = ccc3(0,0,0);
//Toggleable item for Sound option
CCMenuItemToggle *item1 = [CCMenuItemToggle itemWithTarget:self selector:@selector(soundToggle:) items:
[CCMenuItemFont itemFromString: @"On"], [CCMenuItemFont itemFromString: @"Off"], nil];
CCMenuusing//Disabled title label for Difficulty option
CCMenuItemFont *title2 = [CCMenuItemFont itemFromString:@"Difficulty"];
[title2 setIsEnabled:NO];
title2.color = ccc3(0,0,0);
//Toggleable item for Difficulty option
CCMenuItemToggle *item2 = [CCMenuItemToggle itemWithTarget:self selector:@selector(difficultyToggle:) items:
[CCMenuItemFont itemFromString: @"Easy"], [CCMenuItemFont itemFromString: @"Medium"],
[CCMenuItemFont itemFromString: @"Hard"], [CCMenuItemFont itemFromString: @"Insane"], nil];
//Back button
CCMenuItemFont *back = [CCMenuItemFont itemFromString:@"Back" target:self selector:@selector(back:)];
//Finally, create our menu
CCMenu *menu = [CCMenu menuWithItems:
title1, title2,
item1, item2,
back, nil]; // 5 items.
//Align items in columns
[menu alignItemsInColumns:
[NSNumber numberWithUnsignedInt:2],
[NSNumber numberWithUnsignedInt:2],
[NSNumber numberWithUnsignedInt:1],
nil
];
[self addChild:menu];
}
return self;
}
-(void) back:(id)sender {
[[CCDirector sharedDirector] popScene];
}
//Use the 'selectedIndex' variable to identify the touched item
-(void) soundToggle: (id) sender {
CCMenuItem *item = (CCMenuItem*)sender;
[message setString:[NSString stringWithFormat:@"Selected Sound Index:%d", [item selectedIndex]]];
}
-(void) difficultyToggle: (id) sender {
CCMenuItem *item = (CCMenuItem*)sender;
[message setString:[NSString stringWithFormat:@"Selected Difficulty Index:%d", [item selectedIndex]]];
}
@end
CCMenuusing@implementation Ch5_UsingCCMenu
-(CCLayer*) runRecipe {
[super runRecipe];
//Set font size/name
[CCMenuItemFont setFontSize:30];
[CCMenuItemFont setFontName:@"Marker Felt"];
//Image Button
CCMenuItemSprite *imageButton = [CCMenuItemSprite itemFromNormalSprite:[CCSprite spriteWithFile:@"button_unselected.png"]
selectedSprite:[CCSprite spriteWithFile:@"button_selected.png"] disabledSprite:[CCSprite spriteWithFile:@"button_disabled.png"]
target:self selector:@selector(buttonTouched:)];
//Enable Options Label
CCLabelBMFont *enableOptionsLabel = [CCLabelBMFont labelWithString:@"Enable Options" fntFile:@"eurostile_30.fnt"];
CCMenuItemLabel *enableOptions = [CCMenuItemLabel itemWithLabel:enableOptionsLabel target:self selector:@selector(enableOptions:)];
//Options Label
optionsItem = [CCMenuItemFont itemFromString:@"Options" target:self selector:@selector(options:)];
optionsItem.isEnabled = NO;
//Re-Align Label
CCMenuItemFont *reAlign = [CCMenuItemFont itemFromString:@"Re-Align" target:self selector:@selector(reAlign:)];
//Add menu items
menu = [CCMenu menuWithItems: imageButton, enableOptions, optionsItem, reAlign, nil];
[menu alignItemsVertically];
[self addChild:menu];
return self;
}
-(void) buttonTouched:(id)sender {
[message setString:@"Button touched!"];
}
-(void) options:(id)sender {
[[CCDirector sharedDirector] pushScene:[OptionsMenu scene]];
}
-(void) enableOptions:(id)sender {
optionsItem.isEnabled = !optionsItem.isEnabled;
}
//Randomly re-align our menu
-(void) reAlign:(id)sender {
int n = arc4random()%6;
if(n == 0){
[menu alignItemsVertically];
}else if(n == 1){
[menu alignItemsHorizontally];
}else if(n == 2){
[menu alignItemsHorizontallyWithPadding:arc4random()%30];
}else if(n == 3){
[menu alignItemsVerticallyWithPadding:arc4random()%30];
}else if(n == 4){
[menu alignItemsInColumns: [NSNumber numberWithUnsignedInt:2], [NSNumber numberWithUnsignedInt:2], nil];
}else if(n == 5){
[menu alignItemsInRows: [NSNumber numberWithUnsignedInt:2], [NSNumber numberWithUnsignedInt:2], nil];
}
}
@end

工作原理...

CCMenu 类充当一个容器,其中包含可配置的 CCMenuItem 对象列表:

  • CCMenuItemFont:

    CCMenuItemFont 类是一个辅助类,旨在通过类方法快速创建使用 TrueType 字体的 CCMenuItemLabel 对象。它提供了作为创建菜单项标签过程快捷方式的类方法。字体名称和大小通过类方法设置:

    [CCMenuItemFont setFontSize:30];
    [CCMenuItemFont setFontName:@"Marker Felt"];
    
    

    然后通过类方法创建一个 CCMenuItemFont 对象:

    CCMenuItemFont *reAlign = [CCMenuItemFont itemFromString:@"Re-Align" target:self selector:@selector(reAlign:)];
    
    

    这个类在整本书中用于创建菜单项标签。

  • CCMenuItemLabel:

    为了对菜单项标签有更多控制,您可以直接使用 CCMenuItemLabel

    CCLabelBMFont *enableOptionsLabel = [CCLabelBMFont labelWithString:@"Enable Options" fntFile:@"eurostile_30.fnt"];
    CCMenuItemLabel *enableOptions = [CCMenuItemLabel itemWithLabel:enableOptionsLabel target:self selector:@selector(enableOptions:)];
    
    

    这允许您使用 CCLabelBMFontCCLabelAtlas 添加位图字体。

  • CCMenuItemSprite:

    CCMenuItemSprite 类创建一个可触摸的按钮作为菜单项,而不是文本标签:

    CCMenuItemSprite *imageButton = [CCMenuItemSprite itemFromNormalSprite:[CCSprite spriteWithFile:@"button_unselected.png"] selectedSprite:[CCSprite spriteWithFile:@"button_selected.png"] disabledSprite:[CCSprite spriteWithFile:@"button_disabled.png"] target:self selector:@selector(buttonTouched:)];
    
    

    建议使用两个或三个精灵来创建引人注目的按钮效果。

  • CCMenuItemToggle: 可切换的菜单项包含一个要迭代的菜单项列表:

    CCMenuItemToggle *item1 = [CCMenuItemToggle itemWithTarget:self selector:@selector(soundToggle:) items: [CCMenuItemFont itemFromString: @"On"], [CCMenuItemFont itemFromString: @"Off"], nil];
    
    

    可以通过菜单项上的 selectedIndex 属性识别当前状态。这通常在项的回调方法中处理。

  • 自动对齐菜单项:

    通过使用以下方法,可以水平或垂直对齐 CCMenu 对象的项:

    -(void) alignItemsVertically;
    -(void) alignItemsVerticallyWithPadding:(float) padding;
    -(void) alignItemsHorizontally;
    -(void) alignItemsHorizontallyWithPadding: (float) padding;
    
    

    菜单也可以按列或行对齐:

    [menu alignItemsInColumns: [NSNumber numberWithUnsignedInt:2], [NSNumber numberWithUnsignedInt:2], nil];
    [menu alignItemsInRows: [NSNumber numberWithUnsignedInt:2], [NSNumber numberWithUnsignedInt:2], nil];
    
    

    NSNumber 对象的列表总数必须等于附加到菜单的菜单项数量,以便正确处理对齐。

  • 手动对齐菜单项:

    菜单项也可以像任何其他 CCNode 对象一样使用 position 属性手动定位。

  • 启用/禁用菜单项:

    所有菜单项都可以被禁用,以便它们忽略触摸。标签可以将它们的 disabledColor 属性设置为指示这一点,而 CCMenuItemSprite 的实例有一个特定的精灵来指示这一点。

创建阴影菜单标签

常常在背景中有一个动态的色彩漩涡,标签有时在屏幕上难以识别。为了解决这个问题,我们可以创建带有暗阴影的标签。在这个菜谱中,我们将创建几个这样的标签。

创建阴影菜单标签

准备工作

请参阅项目 RecipeCollection02 以获取此菜谱的完整工作代码。

如何做到这一点...

执行以下代码:

#import "ShadowLabel.h"
@implementation Ch5_ShadowedLabels
-(CCLayer*) runRecipe {
[super runRecipe];
/* Draw four different shadowed labels using 4 different fonts */
[CCMenuItemFont setFontSize:47];
[CCMenuItemFont setFontName:@"Georgia"];
[self label:@"Label 1" at:ccp(-120,50) color:ccc3(0,50,255) activeColor:ccc3(0,200,255) selector:@selector(labelTouched:) tag:1];
[CCMenuItemFont setFontSize:40];
[CCMenuItemFont setFontName:@"Marker Felt"];
[self label:@"Label 2" at:ccp(120,50) color:ccc3(255,128,0) activeColor:ccc3(255,255,0) selector:@selector(labelTouched:) tag:2];
[CCMenuItemFont setFontSize:45];
[CCMenuItemFont setFontName:@"Arial"];
[self label:@"Label 3" at:ccp(-120,-50) color:ccc3(0,128,0) activeColor:ccc3(0,255,0) selector:@selector(labelTouched:) tag:3];
[CCMenuItemFont setFontSize:50];
[CCMenuItemFont setFontName:@"Courier New"];
[self label:@"Label 4" at:ccp(120,-50) color:ccc3(255,0,0) activeColor:ccc3(255,255,0) selector:@selector(labelTouched:) tag:4];
return self;
}
//Label creation helper method
-(void) label:(NSString*)s at:(CGPoint)p color:(ccColor3B)col activeColor:(ccColor3B)activeCol selector:(SEL)sel tag:(int)tag {
ShadowLabel *label = [ShadowLabel labelFromString:s target:self selector:sel];
label.position = p;
label.color = col;
label.activeColor = activeCol;
label.tag = tag;
CCMenu *menu = [CCMenu menuWithItems: label.shadow, label, nil];
[self addChild:menu];
}
//Label touch callback
-(void) labelTouched:(id)sender {
ShadowLabel *label = (ShadowLabel*)sender;
[self showMessage:[NSString stringWithFormat:@"Pressed label %d",label.tag]];
}
@end

它是如何工作的...

ShadowLabel 类创建了一个位于其父对象后面并稍微偏向一侧的子 CCMenuItemLabel 对象。方法被覆盖,因此两个标签是同步的。

  • 注意事项:

    这种方法的唯一缺点是,使用此类的菜单无法自动对齐,因为“阴影”标签也必须作为菜单项添加。

更多内容...

在这个例子中,我们为每个 ShadowLabel 设置了 tag 属性,以便在回调期间正确识别。这与我们过去使用的相同的 tag 属性;只是它被重新用于这个角色。

  • 在字体编辑器中添加阴影:

    作为之前使用技术的替代方案,可以使用字体编辑器将阴影添加到 TrueType 字体中。这里的权衡是代码更少,渲染时间更快。但是,您必须首先花时间在编辑器中将阴影添加到字体中。

UIKit 警告对话框

在接下来的两个菜谱中,我们将尝试将 UIKit 元素集成到 Cocos2d 游戏中的黑色艺术。在这个例子中,我们看到一个带有选择和关联回调方法的 UIKit 警告 对话框。

UIKit 警告对话框

准备工作

请参阅项目 RecipeCollection02 以获取此菜谱的完整工作代码。

如何做到这一点...

执行以下代码:

@interface Ch5_UIKitAlerts : Recipe <UIAlertViewDelegate>{}
-(CCLayer*) runRecipe;
-(void)showPieAlert;
-(void)alertView:(UIAlertView*)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex;
@end
@implementation Ch5_UIKitAlerts
-(CCLayer*) runRecipe {
[super runRecipe];
[self showPieAlert];
return self;
}
//Shows a UIAlertView
-(void)showPieAlert {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Do You Like Pie?" message:@"" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Yes",@"No",nil];
[alert show];
[alert release];
UIKit alert dialogsabout}
//AlertView callback
-(void)alertView:(UIAlertView *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex {
if(buttonIndex == 0) {
[self showMessage:@"You remain tight lipped on\nthe 'pie' question."];
}else if(buttonIndex == 1){
[self showMessage:@"Ah yes, another lover of pie."];
}else if(buttonIndex == 2){
[self showMessage:@"You don't like pie?\nWhat's wrong with you?"];
}
}
@end

它是如何工作的...

显示一个警告信息相对直接。我们创建一个包含一些基本信息 UIAlertView 对象,然后调用 show 方法。这会启动我们的警告。

  • 使用 UIAlertViewDelegate

    UIAlertViewDelegate 协议规定我们处理以下方法:

    -(void)alertView:(UIAlertView *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex;
    
    

    这允许我们通过检查返回的 buttonIndex 变量来处理警告响应。

包装 UIKit

其他 UIKit 类提供了一系列经过时间考验的 UI 功能。Cocos2d 需要使用 UIKit 包装器将 UIKit 对象转换为 CCNode 对象,以便正确操作。在这个例子中,我们将包装两个不同的类并在屏幕上操作它们。

包装 UIKit

准备工作

请参阅项目 RecipeCollection02 以获取此菜谱的完整工作代码。

如何做到这一点...

执行以下代码:

#import "CCUIViewWrapper.h"
@implementation Ch5_WrappingUIKit
-(CCLayer*) runRecipe {
[super runRecipe];
[self addSpinningButton];
[self addScrollView];
return self;
}
-(void) addSpinningButton {
//Label
CCLabelBMFont *label = [CCLabelBMFont labelWithString:@"UIButton" fntFile:@"eurostile_30.fnt"];
label.position = ccp(350,220);
label.scale = 0.75f;
[label setColor:ccc3(255,255,255)];
[self addChild:label z:10];
//Our UIButton example
UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[button addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchDown];
[button setTitle:@"Touch Me!" forState:UIControlStateNormal];
button.frame = CGRectMake(0.0, 0.0, 120.0, 40.0);
//Wrap the UIButton using CCUIViewWrapper
CCUIViewWrapper *wrapper = [CCUIViewWrapper wrapperForUIView:button];
[self addChild:wrapper];
wrapper.position = ccp(90,140);
[wrapper runAction:[CCRepeatForever actionWithAction:[CCRotateBy actionWithDuration:5.0f angle:360]]];
}
-(void) addScrollView {
//Label
CCLabelBMFont *label = [CCLabelBMFont labelWithString:@"UIScrollView" fntFile:@"eurostile_30.fnt"];
label.position = ccp(100,220);
label.scale = 0.75f;
[label setColor:ccc3(255,255,255)];
[self addChild:label z:10];
//Create a simple UIScrollView with colored UIViews
CGPoint viewSize = ccp(200.0f,100.0f);
CGPoint nodeSize = ccp(200.0f,50.0f);
int nodeCount = 10;
//Init scrollview
UIScrollView *scrollview = [[UIScrollView alloc] initWithFrame: CGRectMake(0, 0, viewSize.x, viewSize.y)];
//Add nodes
for (int i = 0; i <nodeCount; i++){
CGFloat y = i * nodeSize.y;
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, y, nodeSize.x, nodeSize.y)];
view.backgroundColor = [UIColor colorWithRed:(CGFloat)random()/(CGFloat)RAND_MAX green:(CGFloat)random()/(CGFloat)RAND_MAX blue:(CGFloat)random()/(CGFloat)RAND_MAX alpha:1.0];
[scrollview addSubview:view];
[view release];
}
scrollview.contentSize = CGSizeMake(viewSize.x, viewSize.y * nodeCount/2);
//Wrap the UIScrollView object using CCUIViewWrapper
CCUIViewWrapper *wrapper = [CCUIViewWrapper wrapperForUIView:scrollview];
[self addChild:wrapper];
wrapper.rotation = -90;
wrapper.position = ccp(50,400);
}
-(void) buttonTapped:(id)sender {
[self showMessage:@"Button tapped"];
}
@end

它是如何工作的...

CCUIViewWrapper 在其主创建类方法中接受任何 UIView 对象:

CCUIViewWrapper *wrapper = [CCUIViewWrapper wrapperForUIView:button];

这个对象可以被像正常的 CCSprite 对象一样操作。

  • UIButton:

    使用 UIKit 类的优点有很多。UIButton 类允许创建带有文本的巧妙按钮。

  • UIScrollView:

    在我们的另一个示例中,我们创建了一个更复杂的 UIScrollView 对象。尽管语法比使用内置的 Cocos2d 类要混乱一些,但这个 UIKit 视图提供的流畅功能很难复制。

  • 混合动作:

    如果你滚动 UIScrollView 对象,你会看到右侧的 UIButton 停止旋转。一些 UIKit 动作比异步 Cocos2d 动作具有优先级。

  • 自动旋转和 UIKit 包装器:

    这个包装器的一个限制是它目前不能与 Cocos2d 自动旋转一起工作。如果你在使用这个配方时旋转设备,你会看到屏幕上的元素并没有随着屏幕一起旋转。建议你在 GameConfig.h 文件中使用以下行:

    #define GAME_AUTOROTATION kGameAutorotationNone
    
    

    这将禁用自动旋转。

  • UIKit 的力量:

    最好将此包装器作为实验 UIKit 类的起点。Cocos2d 和 UIKit 并不总是相处融洽,但能够利用像 UIKit 这样的强大 UI 库可以帮助创建更复杂的菜单,而无需编写和测试自己的 UI 代码。

创建可拖拽的菜单窗口

Cocos2d 通常被认为是一个游戏开发库,在这本书的大部分内容中都被这样对待。然而,Cocos2d 是任何 2D 应用程序的强大解决方案。话虽如此,可拖拽窗口是许多应用程序中的常见元素。在这个示例中,我们将创建可移动、可折叠的菜单窗口。

创建可拖拽的菜单窗口

准备工作

请参考项目 RecipeCollection02 以获取此配方的完整工作代码。

如何做到这一点...

执行以下代码:

#import "GameMenuWindow.h"
@implementation Ch5_MenuWindows
-(CCLayer*) runRecipe {
[super runRecipe];
//Initialization
windows = [[[NSMutableArray alloc] init] autorelease];
CCNode *windowContainer = [[CCNode alloc] init];
/* Create three menu windows with randomized positions */
GameMenuWindow *window1 = [GameMenuWindow windowWithTitle:@"Window 1" size:CGSizeMake(arc4random()%200+120,arc4random()%100+50)];
window1.position = ccp(arc4random()%100+150,arc4random()%140+100);
[windowContainer addChild:window1 z:1];
[windows addObject:window1];
/* CODE OMITTED */
//Sort our window array by zOrder
//This allows ordered touching
NSSortDescriptor *sorter = [[NSSortDescriptor alloc] initWithKey:@"self.zOrder" ascending:NO];
[windows sortUsingDescriptors:[NSArray arrayWithObject:sorter]];
//Add window container node
[self addChild:windowContainer];
return self;
}
-(void) ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView: [touch view]];
point = [[CCDirector sharedDirector] convertToGL: point];
//Sort our window array before we process a touch
NSSortDescriptor *sorter = [[NSSortDescriptor alloc] initWithKey:@"self.zOrder" ascending:NO];
[windows sortUsingDescriptors:[NSArray arrayWithObject:sorter]];
//Grab the window by touching the top bar. Otherwise, merely bring the window to the front
for(GameMenuWindow* w in windows){
if(pointIsInRect(point, [w titleBarRect])){
[w ccTouchesBegan:touches withEvent:event];
return;
}else if(pointIsInRect(point, [w rect])){
[w bringToFront];
return;
}
}
}
-(void) ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
/* CODE OMITTED */
//If we touched a window them we can drag it
for(GameMenuWindow* w in windows){
if(w.isTouched){
[w ccTouchesMoved:touches withEvent:event];
}
}
}
-(void) ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
/* CODE OMITTED */
//End a touch if neccessary
for(GameMenuWindow* w in windows){
if(w.isTouched){
[w ccTouchesEnded:touches withEvent:event];
}
}
}
@end

它是如何工作的...

之前显示的窗口可以通过触摸标题栏然后拖动来移动。按下加减符号可以展开或折叠窗口内容。

  • 创建标题栏:

    每个窗口的标题栏和其他部分都是使用彩色 blank.png 精灵技术创建的。标题栏中使用的标签锚定在左侧,以实现文本左对齐。

  • 向窗口添加内容:

    可以将节点添加到 content 精灵中,以添加窗口内容。这可以包括文本、图像、动态内容等等。请注意,在这个示例中,添加到 content 精灵中的节点不会被裁剪,并且可以根据节点位置出现在窗口之外。

  • 排序窗口:

    在我们能够正确地与窗口交互之前,它们必须按照它们的 zOrder 属性进行排序:

    NSSortDescriptor *sorter = [[NSSortDescriptor alloc] initWithKey:@"self.zOrder" ascending:NO];
    [windows sortUsingDescriptors:[NSArray arrayWithObject:sorter]];
    
    

    NSSortDescriptor 类允许您根据排序对象的公共属性对 NSArray 容器进行排序。我们指定键 "self.zOrder"。这将根据 Orderz 属性重新排序数组。现在,当我们遍历数组寻找被触摸的窗口时,我们找到的第一个窗口将是出现在顶部的窗口。

创建一个水平可滚动菜单

Cocos2d 提供了相当平凡的 CCMenuItemToggle 类来遍历多个 CCMenuItem 选择。在这个例子中,我们将使用模仿 iPod Touch 专辑艺术随机播放视觉技术的 LoopingMenu 类来增加一些趣味。

创建带有指示器的加载屏幕

准备工作

请参考项目 RecipeCollection02 以获取此菜谱的完整工作代码。

如何操作...

执行以下代码:

#import "LoopingMenu.h"
@implementation Ch5_HorizScrollMenu
-(CCLayer*) runRecipe {
[super runRecipe];
message.position = ccp(70,270);
/* Create 5 default sprites and 'selected' sprites */
CCSprite *book1 = [CCSprite spriteWithFile:@"book1.jpg"];
CCSprite *book2 = [CCSprite spriteWithFile:@"book2.jpg"];
/* CODE OMITTED */
CCSprite *book1_selected = [CCSprite spriteWithFile:@"book1.jpg"]; book1_selected.color = ccc3(128,128,180); [book1_selected setBlendFunc: (ccBlendFunc) { GL_ONE, GL_ONE }];
CCSprite *book2_selected = [CCSprite spriteWithFile:@"book2.jpg"]; book2_selected.color = ccc3(128,128,180); [book2_selected setBlendFunc: (ccBlendFunc) { GL_ONE, GL_ONE }];
/* CODE OMITTED */
/* Create CCMenuItemSprites */
CCMenuItemSprite* item1 = [CCMenuItemSprite itemFromNormalSprite:book1 selectedSprite:book1_selected target:self selector:@selector(bookClicked:)];
item1.tag = 1;
CCMenuItemSprite* item2 = [CCMenuItemSprite itemFromNormalSprite:book2 selectedSprite:book2_selected target:self selector:@selector(bookClicked:)];
item2.tag = 2;
/* CODE OMITTED */
//Initialize LoopingMenu and add menu items
LoopingMenu *menu = [LoopingMenu menuWithItems:item1, item2, item3, item4, item5, nil];
menu.position = ccp(240, 150);
[menu alignItemsHorizontallyWithPadding:0];
//Add LoopingMenu to scene
[self addChild:menu];
return self;
}
//Book clicked callback
-(void) bookClicked:(id)sender {
CCMenuItemSprite *sprite = (CCMenuItemSprite*)sender;
[self showMessage:[NSString stringWithFormat:@"Book clicked: %d", sprite.tag]];
}
@end

它是如何工作的...

LoopingMenu 类继承自 CCMenu 类。它使用相同的基本创建方法:

LoopingMenu *menu = [LoopingMenu menuWithItems:item1, item2, item3, item4, item5, nil];

这通过使用提供的 CCMenuItem 对象创建一个无限滚动的菜单。在这种情况下,我们使用 CCMenuItemSprite 对象。

  • 注意事项:

    这种技术的缺点是 CCMenuItem 对象会不断缩放。因此,在将菜单项添加到 LoopingMenu 实例之前,不能对菜单项进行缩放。必须使用完整图像或标签的大小。

创建一个垂直滑动菜单网格

有时,你希望在屏幕上同时显示大量菜单选项。在这个例子中,我们看到 SlidingMenuGrid 类在行动。

创建垂直滑动菜单网格

准备工作

请参考项目 RecipeCollection02 以获取此菜谱的完整工作代码。

如何操作...

执行以下代码:

#import "SlidingMenuGrid.h"
@implementation Ch5_VertSlidingMenuGrid
-(CCLayer*) runRecipe {
[super runRecipe];
message.position = ccp(200,270);
[self showMessage:@"Tap a button or slide the menu grid up or down."];
//Init item array
NSMutableArray* allItems = [[[NSMutableArray alloc] init] autorelease];
/* Create 45 CCMenuItemSprite objects with tags, callback methods and randomized colors */
for (int i = 1; i <= 45; ++i) {
CCSprite* normalSprite = [CCSprite spriteWithFile:@"sliding_menu_button_0.png"];
CCSprite* selectedSprite = [CCSprite spriteWithFile:@"sliding_menu_button_1.png"];
ccColor3B color = [self randomColor];
normalSprite.color = color;
selectedSprite.color = color;
CCMenuItemSprite* item = [CCMenuItemSprite itemFromNormalSprite:normalSprite selectedSprite:selectedSprite target:self selector:@selector(buttonClicked:)];
item.tag = i;
//Add each item to array
[allItems addObject:item];
}
//Init SlidingMenuGrid object with array and some other information
SlidingMenuGrid* menuGrid = [SlidingMenuGrid menuWithArray:allItems cols:5 rows:3 position:ccp(70.f,220.f) padding:ccp(90.f,80.f) verticalPages:true];
[self addChild:menuGrid z:1];
return self;
}
//Button clicked callback
-(void) buttonClicked:(id)sender {
CCMenuItemSprite *sprite = (CCMenuItemSprite*)sender;
[self showMessage:[NSString stringWithFormat:@"Button clicked: %d", sprite.tag]];
}
//Random base color method
-(ccColor3B) randomColor {
/* CODE OMITTED */
}
@end

它是如何工作的...

主要受超受欢迎的 iOS 游戏 Angry Birds 的启发,SlidingMenuGrid 类接受一个 CCMenuItem 对象数组,并将它们按指定的行和列排列:

SlidingMenuGrid* menuGrid = [SlidingMenuGrid menuWithArray:allItems cols:5 rows:3 position:ccp(70.f,220.f) padding:ccp(90.f,80.f) verticalPages:true];

根据数量和它们在屏幕上的布局,菜单项被分成页面。

  • 从一个页面切换到另一个页面:

    垂直滑动菜单将从一个页面切换到另一个页面。

  • 调整 SlidingMenuGrid:

    如果你检查 SlidingMenuGrid.hSlidingMenuGrid.mm,你可以看到许多变量,这些变量决定了菜单网格的行为,包括需要多少距离来“翻页”菜单以及翻页动画速度。

创建带有指示器的加载屏幕

大型关卡的游戏往往需要较长的加载时间。如果关卡元素可以异步加载,那么我们可以给用户一些令人放心的反馈,表明游戏仍在加载,并未崩溃。

创建带有指示器的加载屏幕

准备工作

请参考项目 RecipeCollection02 以获取此菜谱的完整工作代码。

如何操作...

执行以下代码:

/* The actual 'game' class where we display the textures we loaded asynchronously */
@implementation GameScene
+(id) sceneWithLevel:(NSString*)str {
//Create our scene
CCScene *s = [CCScene node];
GameScene *node = [[GameScene alloc] initWithLevel:str];
[s addChild:node z:0 tag:0];
return s;
}
-(id) initWithLevel:(NSString*)str {
if( (self=[super init] )) {
//Load our level
[self loadLevel:str];
/* CODE OMITTED */
//Create a label to indicate that this is the loaded level
CCLabelBMFont *label = [CCLabelBMFont labelWithString:@"The Loaded Level:" fntFile:@"eurostile_30.fnt"];
/* CODE OMITTED */
//Quit button
CCMenuItemFont *quitItem = [CCMenuItemFont itemFromString:@"Quit" target:self selector:@selector(quit:)];
CCMenu *menu = [CCMenu menuWithItems: quitItem, nil];
menu.position = ccp(430,300);
[self addChild:menu z:10];
}
return self;
}
//Quit callback
-(void) quit:(id)sender {
[[CCDirector sharedDirector] popScene];
//Clear all loaded textures (including ones from other recipes)
[[CCTextureCache sharedTextureCache] removeAllTextures];
}
//Load level file and process sprites
-(void) loadLevel:(NSString*)str {
NSString *jsonString = [[NSString alloc] initWithContentsOfFile:getActualPath(str) encoding:NSUTF8StringEncoding error:nil];
NSData *jsonData = [jsonString dataUsingEncoding:NSUTF32BigEndianStringEncoding];
NSDictionary *dict = [[CJSONDeserializer deserializer] deserializeAsDictionary:jsonData error:nil];
NSArray *nodes = [dict objectForKey:@"nodes"];
for (id node in nodes) {
if([[node objectForKey:@"type"] isEqualToString:@"spriteFile"]){
[self processSpriteFile:node];
}
}
}
-(void) processSpriteFile:(NSDictionary*)node {
//Init the sprite
NSString *file = [node objectForKey:@"file"];
CCSprite *sprite = [CCSprite spriteWithFile:file];
//Set sprite position
sprite.position = ccp(arc4random()%480, arc4random()%200);
//Each numeric value is an NSString or NSNumber that must be cast into a float
sprite.scale = [[node objectForKey:@"scale"] floatValue];
//Set the anchor point so objects are positioned from the bottom-up
sprite.anchorPoint = ccp(0.5,0);
//Finally, add the sprite
[self addChild:sprite z:2];
}
@end
@implementation LoadingScene
+(id) sceneWithLevel:(NSString*)str {
//Create our scene
CCScene *s = [CCScene node];
LoadingScene *node = [[LoadingScene alloc] initWithLevel:str];
[s addChild:node z:0 tag:0];
return s;
}
-(id) initWithLevel:(NSString*)str {
if( (self=[super init] )) {
//Set levelStr
levelStr = str;
[levelStr retain];
/* CODE OMITTED */
//Set the initial loading message
loadingMessage = [CCLabelBMFont labelWithString:@"Loading, Please Wait...0%" fntFile:@"eurostile_30.fnt"];
/* CODE OMITTED */
//Create an initial '0%' loading bar
loadingBar = [CCSprite spriteWithFile:@"blank.png"];
loadingBar.color = ccc3(255,0,0);
[loadingBar setTextureRect:CGRectMake(0,0,10,25)];
loadingBar.position = ccp(50,50);
loadingBar.anchorPoint = ccp(0,0);
[self addChild:loadingBar z:10];
//Start level pre-load
[self preloadLevel];
}
return self;
}
//Asynchronously load all required textures
-(void) preloadLevel {
nodesLoaded = 0;
NSString *jsonString = [[NSString alloc] initWithContentsOfFile:getActualPath(levelStr) encoding:NSUTF8StringEncoding error:nil];
NSData *jsonData = [jsonString dataUsingEncoding:NSUTF32BigEndianStringEncoding];
NSDictionary *dict = [[CJSONDeserializer deserializer] deserializeAsDictionary:jsonData error:nil];
NSArray *nodes = [dict objectForKey:@"nodes"];
nodesToLoad = [nodes count];
for (id node in nodes) {
if([[node objectForKey:@"type"] isEqualToString:@"spriteFile"]){
[self preloadSpriteFile:node];
}
}
}
//Asynchronously load a texture and call the specified callback when finished
-(void) preloadSpriteFile:(NSDictionary*)node {
NSString *file = [node objectForKey:@"file"];
[[CCTextureCache sharedTextureCache] addImageAsync:file target:self selector:@selector(nodeLoaded:)];
}
//The loading callback
//This increments nodesLoaded and reloads the indicators accordingly
-(void) nodeLoaded:(id)sender {
nodesLoaded++;
float percentComplete = 100.0f * (nodesLoaded / nodesToLoad);
[loadingMessage setString:[NSString stringWithFormat:@"Loading, Please Wait...%d%@", (int)percentComplete, @"%"]];
//When we are 100% complete we run the game
if(percentComplete >= 100.0f){
[self runAction:[CCSequence actions: [CCDelayTime actionWithDuration:0.25f], [CCCallFunc actionWithTarget:self selector:@selector(runGame:)], nil]];
}
//Grow the loading bar
[loadingBar setTextureRect:CGRectMake(0,0,percentComplete*4,25)];
}
//First pop this scene then load the game scene
-(void) runGame:(id)sender {
[[CCDirector sharedDirector] popScene];
[[CCDirector sharedDirector] pushScene:[GameScene sceneWithLevel:@"level1.json"]];
}
@end
@implementation Ch5_LoadingScreen
-(CCLayer*) runRecipe {
[super runRecipe];
//The load level button
CCMenuItemFont *loadLevelItem = [CCMenuItemFont itemFromString:@"Load Level" target:self selector:@selector(loadLevel:)];
CCMenu *menu = [CCMenu menuWithItems: loadLevelItem, nil];
menu.position = ccp(240,160);
[self addChild:menu];
return self;
}
//Callback to load the level
-(void) loadLevel:(id)sender {
[[CCDirector sharedDirector] pushScene:[LoadingScene sceneWithLevel:@"level1.json"]];
}
@end

它是如何工作的...

此菜谱读取 JSON 文件并加载指定的图像。有关从 JSON 文件加载数据的更多信息,请参阅位于 第三章、文件和数据 的菜谱 Reading JSON data files。在这里,总共加载了 10 张图像,总大小约为 6 兆字节。加载时间取决于应用程序运行的设备(模拟器、iPhone 4、iPad 等)。

  • 异步纹理加载:

    我们可以创建这个加载屏幕,因为我们有使用以下方法调用来异步加载纹理的能力:

    [[CCTextureCache sharedTextureCache] addImageAsync:file target:self selector:@selector(nodeLoaded:)];
    
    

    每当 nodeLoaded 回调触发时,我们增加一个计数器以跟踪已加载的文件。尽管这忽略了正在加载的文件大小的变化,但这给我们提供了一个粗略的估计,了解我们在加载过程中的进度。以条形图的形式图形化显示,给用户一些基本的视觉反馈,而不需要过多细节。

  • 切换到游戏场景:

    一旦所有图像异步加载完成,我们就弹出加载场景并切换到主游戏场景。JSON 文件名被传递过去,以便进行第二次遍历,实际上在屏幕上显示图像。因为这些图像已经预先加载,所以场景立即显示。

  • 卸载纹理:

    当我们完成游戏后,我们卸载所有已加载的纹理:

    [[CCTextureCache sharedTextureCache] removeAllTextures];
    
    

    这将卸载所有已加载的纹理,包括在应用程序的任何其他地方加载的纹理。也可以通过在要删除的纹理上调用 release 方法,然后调用 sharedTextureCache 单例的 removeUnusedTextures 方法来手动删除纹理。请注意,removeUnusedTextures 也会删除添加到 CCTextureCache 的纹理。可能更安全的是使用以下方法之一逐个删除纹理:

    -(void) removeTexture: (CCTexture2D*) tex;
    -(void) removeTextureForKey: (NSString*) textureKeyName;
    
    

创建最小地图

一个引人入胜且信息丰富的游戏内 HUD 是大多数游戏的关键部分。特别是移动游戏,由于屏幕空间有限,通常会将用户输入与抬头显示信息结合起来。在这个例子中,我们将创建一个 最小地图 来帮助玩家导航上一章中的等距游戏演示中的地形。

创建最小地图

准备工作

请参阅项目 RecipeCollection02 以获取此菜谱的完整工作代码。

如何操作...

执行以下代码:

#import "Minimap.h"
@implementation Ch5_Minimap
-(CCLayer*) runRecipe {
//Initialize the Minimap object
minimap = [[[Minimap alloc] init] autorelease];
minimap.position = ccp(300,140);
[self addChild:minimap z:10];
//Run our top-down isometric game recipe
[super runRecipe];
//Add trees as static objects
for(id t in trees){
GameObject *tree = (GameObject*)t;
[minimap addStaticObject:ccp(tree.body->GetPosition().x, tree.body->GetPosition().y)];
}
return self;
}
-(void) step:(ccTime)delta {
[super step:delta];
//Set the actor position
[minimap setActor: ccp(gunman.body->GetPosition().x, gunman.body->GetPosition().y)];
//Set individual projectile positions
for(id b in balls){
GameObject *ball = (GameObject*)b;
[minimap setProjectile:ccp(ball.body->GetPosition().x, ball.body->GetPosition().y) withKey:[NSString stringWithFormat:@"%d", ball.tag]];
}
}
//We override this method to automatically add walls to the minimap
-(void) addBrickWallFrom:(CGPoint)p1 to:(CGPoint)p2 height:(float)height {
//Convert wall vertex positions to the properly scaled Box2D coordinates
CGPoint vert1 = ccp(p1.x/PTM_RATIO,p1.y/PTM_RATIO/PERSPECTIVE_RATIO);
CGPoint vert2 = ccp(p2.x/PTM_RATIO,p2.y/PTM_RATIO/PERSPECTIVE_RATIO);
//Add both wall vertices
[minimap addWallWithVertex1:vert1 withVertex2:vert2];
[super addBrickWallFrom:p1 to:p2 height:height];
}
@end

它是如何工作的...

本例中所示的最小地图基本上是 Box2D 世界的一个简单图形表示,类似于 Box2D 调试绘图例程。在这种情况下,我们调整了 OpenGL 绘图代码以适应我们的需求。

  • 最小地图的泛化:

    Minimap 类将物理元素泛化成多种类型,然后使用特定的颜色绘制这些类型。这包括墙壁、投射物、静态物体和中心演员。

  • 墙:

    墙的顶点存储在两个 NSMutableArray 对象中。这些对象被设计为初始设置后不再更新。

  • 投射物:

    弹道坐标存储在一个单独的 NSMutableDictionary 对象中。这使得我们可以通过使用字典中的 tag 属性作为键来重复更新所有弹道。这意味着弹道处于持续运动的状态。

  • 静态对象:

    本例中的静态对象是树木。它们代表地图上的静态点,不会移动。它们用与墙壁相同的颜色绘制。大小信息没有存储,因为假设静态对象不是很大。

  • 中心角色:

    最后,中心角色的位置以一个大的蓝色点表示。

  • 自定义 Minimap 类:

    Minimap 类代表一个通用的模板,可以针对任何游戏类型或情况进行自定义。通过更改 scale 属性、位置信息和绘制程序,Minimap 可以

第六章。音频

在本章中,我们将涵盖以下主题:

  • 播放声音和音乐

  • 修改音频属性

  • 淡入淡出声音和音乐

  • 在游戏中使用音频

  • 在游戏中使用定位音频

  • 计量背景音乐

  • 为动画计量对话

  • 录音音频

  • 流式音频

  • 使用 iPod 音乐库

  • 创建 MIDI 合成器

  • 语音识别和语音合成

简介

根据你制作的游戏类型,添加音频可能从简单到令人畏惧的任务。在本章中,我们将将声音和音乐集成到游戏示例中。我们还将使用诸如计量、录音、语音识别等高级音频技术。

播放声音和音乐

大多数游戏使用各种音效,最多只有几条不同的背景音乐轨道。"CocosDenshion"是 Cocos2d 内置的音频库。它提供包括SimpleAudioEngine API 在内的多项功能。在本食谱中,我们将使用此 API 来播放声音和音乐。

播放声音和音乐

准备中

请参阅项目RecipeCollection02以获取本食谱的完整工作代码。

如何做...

执行以下代码:

#import "SimpleAudioEngine.h"
@implementation Ch6_SoundsAndMusic
-(CCLayer*) runRecipe {
//Initialize the audio engine
sae = [SimpleAudioEngine sharedEngine];
//Background music is stopped on resign and resumed on become active
[[CDAudioManager sharedManager] setResignBehavior:kAMRBStopPlay autoHandle:YES];
//Initialize source container
soundSources = [[NSMutableDictionary alloc] init];
//Add the sounds
[self loadSoundEffect:@"crazy_chimp.caf"];
[self loadSoundEffect:@"rapid_gunfire.caf"];
[self loadSoundEffect:@"howie_scream.caf"];
[self loadSoundEffect:@"air_horn.caf"];
[self loadSoundEffect:@"slide_whistle.caf"];
//Add the background music
[self loadBackgroundMusic:@"hiphop_boss_man_by_p0ss.mp3"];
//Add menu items
CCMenuItemSprite *musicItem = [self menuItemFromSpriteFile:@"music_note.png" tag:0];
CCMenuItemSprite *chimpItem = [self menuItemFromSpriteFile:@"you_stupid_monkey.png" tag:1];
CCMenuItemSprite *gunItem = [self menuItemFromSpriteFile:@"tommy_gun.png" tag:2];
CCMenuItemSprite *screamItem = [self menuItemFromSpriteFile:@"yaaargh.png" tag:3];
CCMenuItemSprite *airHornItem = [self menuItemFromSpriteFile:@"air_horn.png" tag:4];
CCMenuItemSprite *slideWhistleItem = [self menuItemFromSpriteFile:@"slide_whistle.png" tag:5];
//Create our menu
CCMenu *menu = [CCMenu menuWithItems: musicItem, chimpItem, gunItem, screamItem, airHornItem, slideWhistleItem, nil];
[menu alignItemsInColumns: [NSNumber numberWithUnsignedInt:3], [NSNumber numberWithUnsignedInt:3], nil];
menu.position = ccp(240,140);
[self addChild:menu];
return self;
}
//Play sound callback
-(void) playSoundNumber:(id)sender {
CCMenuItem *item = (CCMenuItem*)sender;
int number = item.tag;
if(number == 0){
[self playBackgroundMusic:@"hiphop_boss_man_by_p0ss.mp3"];
}else if(number == 1){
[self playSoundFile:@"crazy_chimp.caf"];
}else if(number == 2){
[self playSoundFile:@"rapid_gunfire.caf"];
}else if(number == 3){
[self playSoundFile:@"howie_scream.caf"];
}else if(number == 4){
[self playSoundFile:@"air_horn.caf"];
}else if(number == 5){
[self playSoundFile:@"slide_whistle.caf"];
}
}
-(void) loadBackgroundMusic:(NSString*)fn {
//Pre-load background music
[sae preloadBackgroundMusic:fn];
}
-(void) playBackgroundMusic:(NSString*)fn {
if (![sae isBackgroundMusicPlaying]) {
//Play background music
[sae playBackgroundMusic:fn];
}else{
//Stop music if its currently playing
[sae stopBackgroundMusic];
}
}
-(CDSoundSource*) loadSoundEffect:(NSString*)fn {
//Pre-load sound
[sae preloadEffect:fn];
//Init sound
CDSoundSource *sound = [[sae soundSourceForFile:fn] retain];
//Add sound to container
[soundSources setObject:sound forKey:fn];
return sound;
}
-(void) playSoundFile:(NSString*)fn {
//Get sound
CDSoundSource *sound = [soundSources objectForKey:fn];
//Play sound
[sound play];
}
-(void) cleanRecipe {
//Stop background music
[sae stopBackgroundMusic];
for(id s in soundSources){
//Release source
CDSoundSource *source = [soundSources objectForKey:s];
[source release];
}
[soundSources release];
//End engine
[SimpleAudioEngine end];
sae = nil;
[super cleanRecipe];
}
@end

它是如何工作的...

SimpleAudioEngine类为用户提供了一个非常简单的接口来进行基本的音频播放。[SimpleAudioEngine sharedEngine]单例仅仅是 CocosDenshion 提供的[CDAudioManager sharedManager]单例的一个简化包装。

  • 初始化:

    不需要初始化SimpleAudioEngine。在本食谱中,我们只是维护对[SimpleAudioEngine sharedEngine]的指针以缩短一些代码。我们使用以下行设置辞职行为。这会改变应用挂起或被其他方式中断时的音频行为。

    [[CDAudioManager sharedManager] setResignBehavior:kAMRBStopPlay autoHandle:YES];
    
    

    这将覆盖由CDAudioManager实现的UIApplicationDelegate下指定的applicationWillResignActive方法。其他辞职类型在CDAudioManager.h中定义。这个方法会在辞职(在 iOS 设备上按下主页按钮)时停止背景音乐,并在应用变为活动状态时播放背景音乐。

  • 播放音效:

    我们将要播放的每个音效都是CDSoundSource的一个实例。"加载"音效涉及使用SimpleAudioEngine预先加载它。不预先加载就播放声音会导致延迟和降低音质。要预先加载声音,请使用以下行:

    [sae preloadEffect:@"crazy_chimp.caf"];
    
    

    初始化CDSoundSource对象:

    CDSoundSource *sound = [[sae soundSourceForFile:fn] retain];
    
    

    最后,保持对该对象的引用:

    NSMutableDictionary *soundSources = [[[NSMutableDictionary alloc] init] autorelease];
    [soundSources setObject:sound forKey:fn];
    
    

    要播放声音,我们只需获取引用并调用播放方法:

    CDSoundSource *sound = [soundSources objectForKey:fn];
    [sound play];
    
    

    SimpleAudioEngine隐藏了此过程的复杂方面。

  • 播放背景音乐:

    播放背景音乐与播放音效类似,但一次只能播放一首背景音乐:

    [sae preloadBackgroundMusic: @"hiphop_boss_man_by_p0ss.mp3"];
    [sae playBackgroundMusic:@"hiphop_boss_man_by_p0ss.mp3"];
    
    

    如果需要,你可以获取实际背景音乐CDLongAudioSource对象的引用:

    CDLongAudioSource *bgm = [CDAudioManager sharedManager].backgroundMusic;
    
    

    此类优化用于较长的音频片段,如音乐和旁白。

更多...

实时音频解码和播放,尤其是在移动设备上,需要使用特定的音频格式。

  • CDSoundSource 格式:

    声音效果的推荐编码为未压缩音频文件的 16 位单声道 Wave 和在 CAF 容器中的 IMA4,用于有损音频文件。

  • 转换为 IMA4 音频文件:

    在基于 Unix 的系统上,您可以使用 afconvert 工具将多种格式转换为 IMA4:

afconvert -f caff -d ima4 mysound.wav

  • CDLongAudioSource 格式:

    对于音乐和其他长音频的播放,任何由 Apple 的 AVAudioPlayer 支持的格式都可以使用。通常使用的格式是 MP3

  • 内存大小:

    虽然压缩音频可以减少磁盘空间需求,但所有声音效果都以 16 位未压缩 PCM 格式存储在内存中。因此,压缩声音效果不会减少您应用程序的内存占用。这对于更大、声音更密集的游戏来说是一个因素。

修改音频属性

CocosDenshion 提供了更改音频源 音调、增益声像 属性的功能。音调是频率,增益是音量,声像是左右扬声器之间调整音量的方式。在本例中,我们将创建一个音乐弯曲乐器来显示这些属性被动态修改。

修改音频属性

准备工作

请参考项目 RecipeCollection02 以获取此菜谱的完整工作代码。

如何操作...

执行以下代码:

#import "SimpleAudioEngine.h"
@implementation Ch6_AudioProperties
-(CCLayer*) runRecipe {
//Enable accelerometer support
self.isAccelerometerEnabled = YES;
[[UIAccelerometer sharedAccelerometer] setUpdateInterval:(1.0 / 60)];
//Add background
CCSprite *bg = [CCSprite spriteWithFile:@"synth_tone_sheet.png"];
bg.position = ccp(240,160);
[self addChild:bg];
//Initialize the audio engine
sae = [SimpleAudioEngine sharedEngine];
//Background music is stopped on resign and resumed on becoming active
[[CDAudioManager sharedManager] setResignBehavior:kAMRBStopPlay autoHandle:YES];
//Initialize note container
notes = [[NSMutableDictionary alloc] init];
noteSprites = [[NSMutableDictionary alloc] init];
//Preload tone
[sae preloadEffect:@"synth_tone_mono.caf"];
return self;
}
-(void) ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
//Process multiple touches
for(int i=0; i<[[touches allObjects] count]; i++){
UITouch *touch = [[touches allObjects] objectAtIndex:i];
CGPoint point = [touch locationInView: [touch view]];
point = [[CCDirector sharedDirector] convertToGL: point];
//Use [touch hash] as a key for this sound source
NSString *key = [NSString stringWithFormat:@"%d",[touch hash]];
if([notes objectForKey:key]){
CDSoundSource *sound = [notes objectForKey:key];
[sound release];
[notes removeObjectForKey:key];
CCSprite *sprite = [noteSprites objectForKey:key];
[self removeChild:sprite cleanup:YES];
[noteSprites removeObjectForKey:key];
}
//Play our sound with custom pitch and gain
CDSoundSource *sound = [[sae soundSourceForFile:@"synth_tone_mono.caf"] retain];
[sound play];
sound.looping = YES;
[notes setObject:sound forKey:key];
sound.pitch = point.x/240.0f;
sound.gain = point.y/320.0f;
//Show music note where you touched
CCSprite *sprite = [CCSprite spriteWithFile:@"music_note.png"];
sprite.position = point;
[noteSprites setObject:sprite forKey:key];
sprite.scale = (point.y/320.0f)/2 + 0.25f;
[self addChild:sprite];
}
}
-(void) ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
//Adjust sound sources and music note positions
for(int i=0; i<[[touches allObjects] count]; i++){
UITouch *touch = [[touches allObjects] objectAtIndex:i];
CGPoint point = [touch locationInView: [touch view]];
point = [[CCDirector sharedDirector] convertToGL: point];
NSString *key = [NSString stringWithFormat:@"%d",[touch hash]];
if([notes objectForKey:key]){
CDSoundSource *sound = [notes objectForKey:key];
sound.pitch = point.x/240.0f;
sound.gain = point.y/320.0f;
CCSprite *sprite = [noteSprites objectForKey:key];
sprite.position = point;
sprite.scale = (point.y/320.0f)/2 + 0.25f;
}
}
}
-(void) ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
//Stop sounds and remove sprites
for(int i=0; i<[[touches allObjects] count]; i++){
UITouch *touch = [[touches allObjects] objectAtIndex:i];
CGPoint point = [touch locationInView: [touch view]];
point = [[CCDirector sharedDirector] convertToGL: point];
NSString *key = [NSString stringWithFormat:@"%d",[touch hash]];
if([notes objectForKey:key]){
//Stop and remove sound source
CDSoundSource *sound = [notes objectForKey:key];
[sound stop];
[sound release];
[notes removeObjectForKey:key];
//Remove sprite
CCSprite *sprite = [noteSprites objectForKey:key];
[self removeChild:sprite cleanup:YES];
[noteSprites removeObjectForKey:key];
}
}
}
//Adjust sound pan by turning the device sideways
- (void) accelerometer:(UIAccelerometer*)accelerometer didAccelerate:(UIAcceleration*)acceleration{
for(id s in notes){
CDSoundSource *sound = [notes objectForKey:s];
sound.pan = -acceleration.y; //"Turn" left to pan to the left speaker
}
}
@end

如何工作...

此菜谱所需的第一件事是一个短、恒定、中音范围的合成音。这是在 GarageBand 中创建的,然后使用 Audacity 进行修改。当您触摸屏幕时,音符就会播放:

CDSoundSource *sound = [[sae soundSourceForFile:@"synth_tone_mono.caf"] retain];
[sound play];
sound.looping = YES;

现在我们需要根据触摸的 X 位置设置 pitch 在 0 到 2 之间:

sound.pitch = point.x/240.0f;

我们根据触摸的 Y 位置设置 gain 属性在 0 到 1 之间:

sound.gain = point.y/320.0f;

摇动设备将设置 pan 属性。向左倾斜以在您的左耳听到更多音调,向右倾斜以在您的右耳听到更多音调:

for(id s in notes){
CDSoundSource *sound = [notes objectForKey:s];
sound.pan = -acceleration.y; //"Turn" left to pan to the left speaker
}

如您所见(并可听到),在播放声音的同时,可以实时修改音频属性,以创建真正酷炫的效果。

更多...

在此菜谱中使用的 @"synth_tone_mono.caf" 文件被特别编码为 单声道 音效。这是因为 声像 属性只能在单声道音效上设置。

淡化声音和音乐

从 Cocos2d 动作中汲取灵感,CocosDenshion 提供了一些用于淡化声音和音乐的类。这些是 CDLongAudioSourceFaderCDXPropertyModifierAction。在本例中,我们将看到如何淡化/取消淡化所有声音、单个声音和音乐,以及如何交叉淡化两个音乐源。

淡化声音和音乐

准备工作

请参考项目 RecipeCollection02 以获取此菜谱的完整工作代码。

如何操作...

执行以下代码:

#import "SimpleAudioEngine.h"
#import "CDXPropertyModifierAction.h"
@implementation Ch6_FadingSoundsAndMusic
-(CCLayer*) runRecipe {
//Initialize the audio engine
sae = [SimpleAudioEngine sharedEngine];
//Background music is stopped on resign and resumed on becoming active
[[CDAudioManager sharedManager] setResignBehavior:kAMRBStopPlay autoHandle:YES];
//Initialize source container
soundSources = [[NSMutableDictionary alloc] init];
musicSources = [[NSMutableDictionary alloc] init];
//Add music
[self loadMusic:@"hiphop_boss_man_by_p0ss.mp3"];
[self loadMusic:@"menu_music_by_mrpoly.mp3"];
//Add sounds
[self loadSoundEffect:@"gunman_pain.caf"];
[self loadSoundEffect:@"synth_tone.caf"];
//Add menu items
/* CODE OMITTED */
return self;
}
//Play music callback
-(void) playMusicNumber:(id)sender {
CCMenuItem *item = (CCMenuItem*)sender;
int number = item.tag;
if(number == 0){
[self fadeOutPlayingMusic];
[self fadeInMusicFile:@"hiphop_boss_man_by_p0ss.mp3"];
}else if(number == 1){
[self fadeOutPlayingMusic];
[self fadeInMusicFile:@"menu_music_by_mrpoly.mp3"];
}
}
//Fade out any music sources currently playing
-(void) fadeOutPlayingMusic {
for(id m in musicSources){
//Release source
CDLongAudioSource *source = [musicSources objectForKey:m];
if(source.isPlaying){
//Create fader
CDLongAudioSourceFader* fader = [[CDLongAudioSourceFader alloc] init:source interpolationType:kIT_Exponential startVal:source.volume endVal:0.0f];
[fader setStopTargetWhenComplete:NO];
//Create a property modifier action to wrap the fader
CDXPropertyModifierAction* fadeAction = [CDXPropertyModifierAction actionWithDuration:3.0f modifier:fader];
[fader release];//Action will retain
CCCallFuncN* stopAction = [CCCallFuncN actionWithTarget:source selector:@selector(stop)];
[[CCActionManager sharedManager] addAction:[CCSequence actions:fadeAction, stopAction, nil] target:source paused:NO];
}
}
}
//Fade in a specific music file
-(void) fadeInMusicFile:(NSString*)fn {
//Stop music if its playing and return
CDLongAudioSource *source = [musicSources objectForKey:fn];
if(source.isPlaying){
[source stop];
return;
}
//Set volume to zero and play
source.volume = 0.0f;
[source play];
//Create fader
CDLongAudioSourceFader* fader = [[CDLongAudioSourceFader alloc] init:source interpolationType:kIT_Exponential startVal:source.volume endVal:1.0f];
[fader setStopTargetWhenComplete:NO];
//Create a property modifier action to wrap the fader
CDXPropertyModifierAction* fadeAction = [CDXPropertyModifierAction actionWithDuration:1.5f modifier:fader];
[fader release];//Action will retain
[[CCActionManager sharedManager] addAction:[CCSequence actions:fadeAction, nil] target:source paused:NO];
}
-(void) fadeUpAllSfx:(id)sender {
//Fade up all sound effects
[CDXPropertyModifierAction fadeSoundEffects:2.0f finalVolume:1.0f curveType:kIT_Linear shouldStop:NO];
}
-(void) fadeDownAllSfx:(id)sender {
//Fade down all sound effects
[CDXPropertyModifierAction fadeSoundEffects:2.0f finalVolume:0.0f curveType:kIT_Linear shouldStop:NO];
}
-(void) fadeUpSfxNumber:(id)sender {
//Fade up a specific sound effect
CCMenuItem *item = (CCMenuItem*)sender;
int number = item.tag;
CDSoundSource *source;
if(number == 0){
source = [soundSources objectForKey:@"gunman_pain.caf"];
}else if(number == 1){
source = [soundSources objectForKey:@"synth_tone.caf"];
}
source.gain = 0.0f;
[CDXPropertyModifierAction fadeSoundEffect:2.0f finalVolume:1.0f curveType:kIT_Linear shouldStop:NO effect:source];
}
-(void) fadeDownSfxNumber:(id)sender {
//Fade down a specific sound effect
CCMenuItem *item = (CCMenuItem*)sender;
int number = item.tag;
CDSoundSource *source;
if(number == 0){
source = [soundSources objectForKey:@"gunman_pain.caf"];
}else if(number == 1){
source = [soundSources objectForKey:@"synth_tone.caf"];
}
source.gain = 1.0f;
[CDXPropertyModifierAction fadeSoundEffect:2.0f finalVolume:0.0f curveType:kIT_Linear shouldStop:NO effect:source];
}
@end

如何工作...

在这个菜谱中,我们直接使用 CDLongAudioSource 而不是使用 SimpleAudioEngine 提供的 backgroundMusic 源。这允许我们在给定时间内播放多个长音频源。

  • 交叉淡入长音频源:

    交叉淡入涉及同时淡入一个源和淡出一个源。首先,我们初始化一个 CDLongAudioSourceFader 对象来指定淡出值和插值类型:

    CDLongAudioSourceFader* fader = [[CDLongAudioSourceFader alloc] init:source interpolationType:kIT_Linear startVal:source.volume endVal:0.0f];
    [fader setStopTargetWhenComplete:NO];
    
    

    在这种情况下,我们希望从源当前音量开始线性淡出。然后我们创建一个具有指定持续时间的 CDXPropertyModifierAction 对象。我们还在这一点上释放了淡出对象:

    CDXPropertyModifierAction* fadeAction = [CDXPropertyModifierAction actionWithDuration:3.0f modifier:fader];
    [fader release];
    
    

    在淡出音轨后,我们希望停止其播放。为此,我们创建一个 CCCallFuncN 动作:

    CCCallFuncN* stopAction = [CCCallFuncN actionWithTarget:source selector:@selector(stop)];
    
    

    最后,我们按顺序运行这些操作:

    [[CCActionManager sharedManager] addAction:[CCSequence actions:fadeAction, stopAction, nil] target:source paused:NO];
    
    

    执行此操作,以及其他轨道的“淡入”操作,将创建所需的交叉淡入效果。

  • 淡出单个音效:

    CDXPropertyModifierAction 类提供了一个方便的方法来淡出单个音效:

    [CDXPropertyModifierAction fadeSoundEffect:2.0f finalVolume:0.0f curveType:kIT_Linear shouldStop:YES effect:source];
    
    

    在上一个例子中,我们淡出指定的音源 2 秒,然后在我们完成时停止音源播放。

  • 淡出所有音效:

    所有当前正在播放的音效也可以使用以下方便方法淡出:

    [CDXPropertyModifierAction fadeSoundEffects:2.0f finalVolume:0.0f curveType:kIT_Linear shouldStop:YES];
    
    

    这将对所有正在播放的音效应用相同的“淡出”效果。

在游戏中使用音频

虽然 SimpleAudioEngine 可能很简单,但它足够高效,可以用于任何类型的游戏。在这个菜谱中,我们将向第四章中提到的 子弹 演示(ch04.html "第四章。物理")添加音效和音乐,

在游戏中使用音频

准备工作

请参考项目 RecipeCollection02 以获取此菜谱的完整工作代码。

如何操作...

执行以下代码:

#import "Ch4_Bullets.h"
#import "SimpleAudioEngine.h"
@interface Ch6_AudioInGame : Ch4_Bullets
/* CODE OMITTED */
@end
@implementation Ch6_AudioInGame
-(CCLayer*) runRecipe {
[super runRecipe];
//Initialize the audio engine
sae = [SimpleAudioEngine sharedEngine];
//Background music is stopped on resign and resumed on becoming active
[[CDAudioManager sharedManager] setResignBehavior:kAMRBStopPlay autoHandle:YES];
//Initialize source container
soundSources = [[NSMutableDictionary alloc] init];
//Add the sounds
[self loadSoundEffect:@"bullet_fire_no_shell.caf" gain:1.0f];
[self loadSoundEffect:@"bullet_casing_tink.caf" gain:0.25f];
[self loadSoundEffect:@"gunman_jump.caf" gain:1.5f];
[self loadSoundEffect:@"box_break.wav" gain:1.5f];
//Add the background music
[self loadBackgroundMusic:@"hiphop_boss_man_by_p0ss.mp3"];
sae.backgroundMusicVolume = 0.5f;
[self playBackgroundMusic:@"hiphop_boss_man_by_p0ss.mp3"];
return self;
}
//Jump sound override
-(void) processJump {
if(onGround && jumpCounter < 0){
[self playSoundFile:@"gunman_jump.caf"];
}
[super processJump];
}
//Fire gun sound override
-(void) fireGun {
if(fireCount <= 0){
[self playSoundFile:@"bullet_fire_no_shell.caf"];
}
[super fireGun];
}
//Box explosion sound override
-(void) boxExplosionAt:(CGPoint)p withRotation:(float)rot {
[self playSoundFile:@"box_break.wav"];
[super boxExplosionAt:p withRotation:rot];
}
//Bullet casing sound override
-(void) handleCollisionWithMisc:(GameMisc*)a withMisc:(GameMisc*)b {
if(a.typeTag == TYPE_OBJ_SHELL || b.typeTag == TYPE_OBJ_SHELL){
[self playSoundFile:@"bullet_casing_tink.caf"];
}
[super handleCollisionWithMisc:a withMisc:b];
}
@end

它是如何工作的...

使用本章中描述的技术,我们可以通过添加音效和音乐来给我们的射击盒演示增添一些活力。

  • 声音缓冲区:

    默认情况下,CDAudioManager 为每个音源分配一个声音缓冲区。因此,每次我们播放 @"bullet_fire_no_shell.caf" 音效时,如果它已经在播放过程中,我们实际上会停止该音效的播放。这对于大多数游戏中的音效使用情况是足够的。

还有更多...

寻找适合你游戏使用的音效可能是一个既有趣又繁琐的过程。尽管存在大量的免费音效,但通常很难找到适合特定情况的正确音效。或者,一个麦克风和一些音频生成和操作软件可以大有帮助。例如,效果 @"bullet_casing_tink.caf" 是通过使用 GarageBand 播放钢琴的最高音符创建的。另一个程序,sfxr,可以用来生成简单的 8 位风格音效。Cocoa 版本,cfxr,可以在此处下载:thirdcog.eu/apps/cfxr

在游戏中使用位置音频

为了增加我们在游戏中使用的声音的真实感,我们可以根据游戏中的因素修改音频属性。在这个例子中,我们使用源距离可听范围对象大小来确定增益音调声像。我们将通过向第四章中的TopDownIsometric演示添加声音来演示这一点,

游戏中使用位置音频

准备工作

请参阅项目 RecipeCollection02 以获取此菜谱的完整工作代码。

如何做到这一点...

执行以下代码:

#import "Ch4_TopDownIsometric.h"
#import "SimpleAudioEngine.h"
enum {
CGROUP_NON_INTERRUPTIBLE = 0
};
@interface Ch6_PositionalAudio : Ch4_TopDownIsometric
/* CODE OMITTED */
@end
@implementation Ch6_PositionalAudio
-(CCLayer*) runRecipe {
//Run our top-down isometric game recipe
[super runRecipe];
//Initialize max audible range
audibleRange = 20.0f;
//Initialize the audio engine
sae = [SimpleAudioEngine sharedEngine];
//Background music is stopped on resign and resumed on becoming active
[[CDAudioManager sharedManager] setResignBehavior:kAMRBStopPlay autoHandle:YES];
//Preload the sounds
[sae preloadEffect:@"forest_birds_ambience.caf"];
[sae preloadEffect:@"kick_ball_bounce.caf"];
[sae preloadEffect:@"gunman_jump.caf"];
[sae preloadEffect:@"bullet_fire_no_shell.caf"];
//Non-interruptible ball source group
[[CDAudioManager sharedManager].soundEngine setSourceGroupNonInterruptible:CGROUP_NON_INTERRUPTIBLE isNonInterruptible:YES];
//Add the sounds
ballSource = [[sae soundSourceForFile:@"kick_ball_bounce.caf"] retain];
forestBirdsSource = [[sae soundSourceForFile:@"forest_birds_ambience.caf"] retain];
gunmanJumpSource = [[sae soundSourceForFile:@"gunman_jump.caf"] retain];
fireBallSource = [[sae soundSourceForFile:@"bullet_fire_no_shell.caf"] retain];
//Start playing forest bird source
forestBirdsSource.gain = 0.0f;
forestBirdsSource.looping = YES;
[forestBirdsSource play];
//Customize fire ball sound
fireBallSource.pitch = 2.0f;
fireBallSource.gain = 0.5f;
return self;
}
-(void) step:(ccTime)delta {
[super step:delta];
//Play forest bird source with gain based on distance from gunman
float distance = 10000.0f;
for(int i=0; i<[trees count]; i++){
GameObject *tree = [trees objectAtIndex:i];
float thisDistance = distanceBetweenPoints(ccp(tree.body->GetPosition().x,tree.body->GetPosition().y),
ccp(gunman.body->GetPosition().x, gunman.body->GetPosition().y));
if(thisDistance < distance){ distance = thisDistance; }
}
//If closest tree is outside of audible range we set gain to 0.0f
if(distance < audibleRange){
forestBirdsSource.gain = (audibleRange-distance)/audibleRange;
}else{
forestBirdsSource.gain = 0.0f;
}
}
//Fire ball sound override
-(void) fireBall {
if(fireCount < 0){
[fireBallSource play];
}
[super fireBall];
}
//Jump sound override
-(void) processJump {
if(gunman.body->GetZPosition() <= 1.0f){
[gunmanJumpSource play];
}
[super processJump];
}
-(void) handleCollisionWithGroundWithObj:(GameObject*)gameObject {
[super handleCollisionWithGroundWithObj:gameObject];
//Play ball bounce sound with gain based on distance from gunman
if(gameObject.typeTag == TYPE_OBJ_BALL){
float distance = distanceBetweenPoints(ccp(gameObject.body->GetPosition().x, gameObject.body->GetPosition().y), ccp(gunman.body->GetPosition().x, gunman.body->GetPosition().y));
if(distance < audibleRange){
float gain = (audibleRange-distance)/audibleRange;
float pan = (gameObject.body->GetPosition().x - gunman.body->GetPosition().x)/distance;
float pitch = ((((GameIsoObject*)gameObject).inGameSize / 10.0f) * -1) + 2;
if(distance < audibleRange){
[self playBallSoundWithGain:gain pan:pan pitch:pitch];
}
}
}
}
-(void) playBallSoundWithGain:(float)gain pan:(float)pan pitch:(float)pitch {
//Play the sound using the non-interruptible source group
[[CDAudioManager sharedManager].soundEngine playSound:ballSource.soundId sourceGroupId:CGROUP_NON_INTERRUPTIBLE pitch:pitch pan:pan gain:gain loop:NO];
}
@end

它是如何工作的...

创建逼真的声音环境涉及以创造性的方式改变音频属性。

  • 森林氛围:

    对于这个菜谱,我们有一个 30 秒循环的森林氛围片段,代替背景音乐播放。我们根据玩家与最近树木的距离确定这个声音源的增益属性:

    if(distance < audibleRange){
    forestBirdsSource.gain = (audibleRange-distance)/audibleRange;
    }else{
    forestBirdsSource.gain = 0.0f;
    }
    
    

    如果所有树木都在可听范围内,则将增益设置为零。

  • 球弹跳声音:

    为了创建一个引人入胜的球弹跳声音效果,我们修改了所有三个音频属性。增益属性由距离决定:

    float gain = (audibleRange-distance)/audibleRange;
    
    

    声像属性由 X 平面的距离决定:

    float pan = (gameObject.body->GetPosition().x - gunman.body->GetPosition().x)/distance;
    
    

    最后,音调属性由球的大小决定:

    float pitch = ((((GameIsoObject*)gameObject).inGameSize / 10.0f) * -1) + 2;
    
    

    这些修改共同创造了一系列独特的声音。这增加了听觉体验的深度。

  • 使用多个声音缓冲区:

    由于我们同时有多个球启动弹跳声音效果,单个缓冲区将不再足够。我们现在需要同一个声音多次播放。为了实现这一点,我们使用一个特殊的源组。源组只是将声音分组在一起以操纵它们如何播放的一种方式。例如,你可能希望两个声音源共享一个缓冲区。在这种情况下,我们指定源组为不可中断:

    enum { CGROUP_NON_INTERRUPTIBLE = 0 };
    [[CDAudioManager sharedManager].soundEngine setSourceGroupNonInterruptible:CGROUP_NON_INTERRUPTIBLE isNonInterruptible:YES];
    
    

    现在,使用此源组播放的所有声音都将获得一个开放缓冲区。要指定在播放声音时使用源组,我们使用以下行:

    [[CDAudioManager sharedManager].soundEngine playSound:ballSource.soundId sourceGroupId:CGROUP_NON_INTERRUPTIBLE pitch:pitch pan:pan gain:gain loop:NO];
    
    

    现在,可以听到多个具有不同音频属性的球弹跳声音效果相互重叠。

  • 缓冲区最大数量:

    可用的最大声音缓冲区数量和缓冲区增量在CDConfig.h中指定:

    #define CD_BUFFERS_START 64
    #define CD_BUFFERS_INCREMENT 16
    
    

    在默认情况下,在 64 个缓冲区填满后,再分配 16 个。这些可以根据具有特定音频要求的应用程序进行自定义。

测量背景音乐

CDAudioManager类封装了AVAudioPlayer类。使用这个类我们可以访问更底层的音频功能。在这个菜谱中,我们将动态读取当前播放的背景音乐的平均电平峰值电平。我们可以使用这些信息来同步或提示动画。

测量背景音乐

准备工作

请参阅项目 RecipeCollection02 以获取此菜谱的完整工作代码。

如何做到这一点...

执行以下代码:

#import "SimpleAudioEngine.h"
@implementation Ch6_MeteringMusic
-(CCLayer*) runRecipe {
//Initialize the audio engine
sae = [SimpleAudioEngine sharedEngine];
//Background music is stopped on resign and resumed on becoming active
[[CDAudioManager sharedManager] setResignBehavior:kAMRBStopPlay autoHandle:YES];
//Set peak and average power initially
peakPower = 0;
avgPower = 0;
//Init speaker sprites (speakerBase, speakerLarge and speakerSmall)
/* CODE OMITTED */
//Init meter sprites (avgMeter and peakMeter)
/* CODE OMITTED */
//Add the background music
[sae preloadBackgroundMusic:@"technogeek_by_mrpoly.mp3"];
[sae playBackgroundMusic:@"technogeek_by_mrpoly.mp3"];
//Enable metering
[CDAudioManager sharedManager].backgroundMusic.audioSourcePlayer.meteringEnabled = YES;
//Schedule step method
[self schedule:@selector(step:)];
return self;
}
-(void) step:(ccTime)delta {
[self setPeakAndAveragePower];
[self animateMeterAndSpeaker];
}
-(void) setPeakAndAveragePower {
//Update meters
[[CDAudioManager sharedManager].backgroundMusic.audioSourcePlayer updateMeters];
//Get channels
int channels = [CDAudioManager sharedManager].backgroundMusic.audioSourcePlayer.numberOfChannels;
//Average all the channels
float peakPowerNow = 0;
float avgPowerNow = 0;
for(int i=0; i<channels; i++){
float peak = [[CDAudioManager sharedManager].backgroundMusic.audioSourcePlayer peakPowerForChannel:i];
float avg = [[CDAudioManager sharedManager].backgroundMusic.audioSourcePlayer averagePowerForChannel:i];
peakPowerNow += peak/channels;
avgPowerNow += avg/channels;
}
//Change from a DB level to a 0 to 1 ratio
float adjustedPeak = pow(10, (0.05 * peakPowerNow));
float adjustedAvg = pow(10, (0.05 * avgPowerNow));
//Average it out for smoothing
peakPower = (peakPower + adjustedPeak)/2;
avgPower = (avgPower + adjustedAvg)/2;
}
-(void) animateMeterAndSpeaker {
//Average meter
[avgMeter setTextureRect:CGRectMake(0,0,10,avgPower*500.0f)];
//Peak meter
peakMeter.position = ccp(100,20+peakPower*500.0f);
//Animate speaker
speakerLarge.scale = powf(avgPower,0.4f)*2;
speakerSmall.scale = powf(avgPower,0.4f)*2;
}
@end

它是如何工作的...

访问动态计测功能需要在 CDLongAudioSource 对象内部使用 audioSourcePlayer 引用,在本例中,backgroundMusic。在我们开始之前,我们启用计测:

[CDAudioManager sharedManager].backgroundMusic.audioSourcePlayer.meteringEnabled = YES;

现在,我们每周期收集所有播放通道的平均峰值分贝水平。我们平均这些数字:

[[CDAudioManager sharedManager].backgroundMusic.audioSourcePlayer updateMeters];
int channels = [CDAudioManager sharedManager].backgroundMusic.audioSourcePlayer.numberOfChannels;
for(int i=0; i<channels; i++){
float peak = [[CDAudioManager sharedManager].backgroundMusic.audioSourcePlayer peakPowerForChannel:i];
float avg = [[CDAudioManager sharedManager].backgroundMusic.audioSourcePlayer averagePowerForChannel:i];
peakPowerNow += peak/channels;
avgPowerNow += avg/channels;
}

之后,我们将平均和峰值分贝水平转换为 0 到 1 之间的比率。这使得数字更容易应用于动画。

//Change from a DB level to a 0 to 1 ratio
float adjustedPeak = pow(10, (0.05 * peakPowerNow));
float adjustedAvg = pow(10, (0.05 * avgPowerNow));
//Average it out for smoothing
peakPower = (peakPower + adjustedPeak)/2;
avgPower = (avgPower + adjustedAvg)/2;

在设置新的 peakPoweravgPower 变量时,我们也分割差异。这使音量变化更加平滑。

动画计测对话框

LongAudioSource 对象可以是任何类型的音频,而不仅仅是音乐。在这个配方中,我们将使用计测技术来动画化社交的参议员 Beauregard Claghorn 的嘴巴。

动画计测对话框

准备工作

请参阅项目 RecipeCollection02 以获取此配方的完整工作代码。

如何做...

执行以下代码:

#import "SimpleAudioEngine.h"
@implementation Ch6_MeteringDialogue
-(CCLayer*) runRecipe {
/* CODE OMITTED */
//Add the sounds
[self loadLongAudioSource:@"claghorn_a_joke_son.caf"];
[self loadLongAudioSource:@"claghorn_carolina.caf"];
/* CODE OMITTED */
//Add the background music
[self loadBackgroundMusic:@"dixie_1916.mp3"];
/* CODE OMITTED */
//Play background music
[self playBackgroundMusic:@"dixie_1916.mp3"];
//Have Claghorn introduce himself
[self playLongAudioSource:@"claghorn_howdy.caf"];
}
-(void) step:(ccTime)delta {
/* CODE OMITTED */
[self setPeakAndAveragePower];
[self animateClaghorn];
}
-(void) setPeakAndAveragePower {
//Find our playing audio source
CDLongAudioSource *audioSource = nil;
for(id s in soundSources){
CDLongAudioSource *source = [soundSources objectForKey:s];
if(source.isPlaying){
audioSource = source;
break;
}
}
//Update meters
[audioSource.audioSourcePlayer updateMeters];
/* CODE OMITTED */
}
-(void) animateClaghorn {
/* Custom mouth animation */
float level = avgPower;
//Make sure he's actually speaking
if(level == 0){
claghornEyebrows.position = ccp(240,120);
claghornMouth.position = ccp(240,120);
lastAudioLevel = level;
return;
}
//Level bounds
if(level <= 0){ level = 0.01f; }
if(level >= 1){ level = 0.99f; }
//Exaggerate level ebb and flow
if(level < lastAudioLevel){
//Closing mouth
lastAudioLevel = level;
level = powf(level,1.5f);
}else{
//Opening mouth
lastAudioLevel = level;
level = powf(level,0.75f);
}
//If mouth is almost closed, close mouth
if(level < 0.1f){ level = 0.01f; }
//Blink if level > 0.8f
if(level > 0.8f && !isBlinking){
[self blink];
[self runAction:[CCSequence actions:[CCDelayTime actionWithDuration:0.5f],
[CCCallFunc actionWithTarget:self selector:@selector(unblink)], nil]];
}
//Raise eyebrows if level > 0.6f
if(level > 0.6f){
claghornEyebrows.position = ccp(240,120 + level*5.0f);
}else{
claghornEyebrows.position = ccp(240,120);
}
//Set mouth position
claghornMouth.position = ccp(240,120 - level*19.0f);
}
-(CDLongAudioSource*) loadLongAudioSource:(NSString*)fn {
//Init source
CDLongAudioSource *source = [[CDLongAudioSource alloc] init];
source.backgroundMusic = NO;
[source load:fn];
//Enable metering
source.audioSourcePlayer.meteringEnabled = YES;
//Add sound to container
[soundSources setObject:source forKey:fn];
return source;
}
-(void) playLongAudioSource:(NSString*)fn {
//Get sound
CDLongAudioSource *audioSource = [soundSources objectForKey:fn];
bool aSourceIsPlaying = NO;
for(id s in soundSources){
CDLongAudioSource *source = [soundSources objectForKey:s];
if(source.isPlaying){
[source stop];
[source rewind];
aSourceIsPlaying = YES;
break;
}
}
//Play sound
if(!aSourceIsPlaying){
//Play sound
[audioSource play];
[self runAction: [CCSequence actions: [CCDelayTime actionWithDuration:[audioSource.audioSourcePlayer duration]],
[CCCallFunc actionWithTarget:audioSource selector:@selector(stop)],
[CCCallFunc actionWithTarget:audioSource selector:@selector(rewind)], nil]];
}
}
@end

它是如何工作的...

与之前的配方类似,我们使用从 setPeakAndAveragePower 收集的信息来运行动画。与之前的配方不同,我们有多个 CDLongAudioSource 对象可供选择。在这里,我们找到当前正在播放的源,并用于计测:

CDLongAudioSource *audioSource = nil;
for(id s in soundSources){
CDLongAudioSource *source = [soundSources objectForKey:s];
if(source.isPlaying){
audioSource = source;
break;
}
}
[audioSource.audioSourcePlayer updateMeters];

在计算 avgPower 之后,我们然后夸大该数字的峰值和谷值,以帮助模拟 Claghorn 嘴巴的快速张合:

if(level < lastAudioLevel){
lastAudioLevel = level;
level = powf(level,1.5f);
}else{
lastAudioLevel = level;
level = powf(level,0.75f);
}

此外,我们还动画化眨眼、眼球移动和眉毛。将这些动画与计测链接起来,可以创建一个很好的嘴巴运动效果。

流式音频

在第一章,图形中,我们使用了 MPMoviePlayerController 类来播放全动作视频。在这个配方中,我们将使用类似的技术来创建流式音频播放器。

流式音频

准备工作

请参阅项目 RecipeCollection02 以获取此配方的完整工作代码。

如何做...

MediaPlayer 框架链接到您的项目中。现在,执行以下代码:

#import <MediaPlayer/MediaPlayer.h>
#import "AppDelegate.h"
@implementation Ch6_StreamingAudio
-(CCLayer*) runRecipe {
//Create music player buttons
[[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@"music_player.plist"];
CCMenuItemSprite *prevItem = [self menuItemFromSpriteFile:@"music_player_prev.png" target:self selector:@selector(previousSong:)];
/* CODE OMITTED */
//Create menu
/* CODE OMITTED */
//Initial variable values
sourceIndex = 0;
isPlaying = NO;
//Streaming sources
streamingSources = [[NSMutableArray alloc] init];
[streamingSources addObject:@"http://shoutmedia.abc.net.au:10326"];
[streamingSources addObject:@"http://audioplayer.wunderground.com/drgruver/Philadelphia.mp3.m3u"];
[streamingSources addObject:@"http://s8.mediastreaming.it:7050/"];
[streamingSources addObject:@"http://www.radioparadise.com/musiclinks/rp_64aac.m3u"];
[streamingSources addObject:@"http://streaming.wrek.org:8000/wrek_HD-2.m3u"];
//Init movie playing (music streamer in this case)
moviePlayer = [[MPMoviePlayerController alloc] init];
moviePlayer.movieSourceType = MPMovieSourceTypeStreaming;
moviePlayer.view.hidden = YES;
((AppDelegate*)[UIApplication sharedApplication].delegate).window addSubview:moviePlayer.view];
//Set initial stream source
[self setStreamSource];
return self;
}
//Next callback
- (void) nextSong:(id)sender {
[self setIsPlaying];
sourceIndex++;
if(sourceIndex > [streamingSources count]-1){
sourceIndex = 0;
}
[self setStreamSource];
}
//Previous callback
- (void) previousSong:(id)sender {
[self setIsPlaying];
sourceIndex--;
if(sourceIndex < 0){
sourceIndex = [streamingSources count]-1;
}
[self setStreamSource];
}
-(void) setIsPlaying {
if(moviePlayer.playbackState == MPMoviePlaybackStatePlaying){
isPlaying = YES;
}
}
-(void) setStreamSource {
[moviePlayer stop];
moviePlayer.contentURL = [NSURL URLWithString:[streamingSources objectAtIndex:sourceIndex]];
if(isPlaying){
[self playMusic:nil];
}
}
@end

它是如何工作的...

这个配方的工作方式与我们之前在第一章中看到的类似,即图形。一个关键的区别是,在这里我们指定了 mediaSourceType,并且我们还隐藏了播放器视图:

moviePlayer.movieSourceType = MPMovieSourceTypeStreaming;
moviePlayer.view.hidden = YES;

这设置了播放器进行音频流。

  • 达到 AppDelegate:

    Cocos2d 中,AppDelegate 类是实现 UIApplicationDelegate 协议的最高级类。该协议指定了主 UIApplication 单例指向的委托。这个委托处理重要应用程序事件。为了将我们的 moviePlayer 对象添加到我们的视图中,我们通过 UIApplication 单例访问这个委托:

    ((AppDelegate*)[UIApplication sharedApplication].delegate).window addSubview:moviePlayer.view];
    
    

    如您所见,这涉及到将 delegate 属性转换为 AppDelegate* 类型。

  • 切换电台:

    更改流源涉及停止播放并更改contentURL属性:

    [moviePlayer stop];
    moviePlayer.contentURL = [NSURL URLWithString:[streamingSources objectAtIndex:sourceIndex]];
    if(isPlaying){
    [self playMusic:nil];
    }
    
    

    这样用户可以在保持播放的同时无缝切换频道。

  • 实时流格式:

    本食谱中使用的流源示例使用苹果的HTTP 实时流协议。这允许通过HTTP进行优雅的实时流传输,且无需太多麻烦。您可以在此处了解更多关于此协议的信息:developer.apple.com/resources/http-streaming/

  • 流式传输文件:

    单个文件,使用如MP3的格式,也可以使用此技术通过简单的HTTP服务器进行流式传输。

  • 流式传输视频:

    通过将此食谱与第一章“图形”中的视频播放食谱相结合,您还可以进行视频流式传输。兼容格式和其他要求在上文提到的网站上详细说明。作为一个好的经验法则,任何可以使用 iOS 设备内置Safari网络浏览器播放的文件类型或 URL 通常也可以使用MPMoviePlayerController播放。

还有更多...

此前,我们提到了UIApplication单例。单例是在运行时实例化的顶级全局对象。Cocos2d 在很大程度上采用了单例模式。按照惯例,通过以“shared”一词开头的类方法([类sharedClass])访问的任何对象都是单例。您可以使用包含在SynthesizeSingleton.h文件中的创建自己的自定义单例对象。有关更多信息,请参阅Cocos2d 食谱网站的“更多食谱”部分,网址为cocos2dcookbook.com/more_recipes

录音音频

大多数 iOS 设备的一个显著特性是能够录音。在这个食谱中,我们将使用麦克风录音并将音频保存到磁盘上的临时位置,使用AVAudioRecorder类。然后我们将使用CDSoundEngine类以修改后的音高播放它。

录音音频

准备工作

请参考项目RecipeCollection02以获取本食谱的完整工作代码。

如何做到这一点...

CoreAudioAVFoundation框架链接到您的项目中。现在,执行以下代码:

#import <AVFoundation/AVFoundation.h>
#import <CoreAudio/CoreAudioTypes.h>
#import "CocosDenshion.h"
@interface Ch6_RecordingAudio : Recipe <AVAudioRecorderDelegate>
{ /* CODE OMITTED */}
@implementation Ch6_RecordingAudio
-(CCLayer*) runRecipe {
//Set initial pitch and recorded temp file object
pitch = 1.0f;
recordedTmpFile = nil;
//Init audio session
[self initAudioSession];
/* CODE OMITTED */
return self;
}
-(void) initAudioSession {
//Our AVAudioSession singleton pointer
AVAudioSession * audioSession = [AVAudioSession sharedInstance];
//Set up the audioSession for playback and record.
[audioSession setCategory:AVAudioSessionCategoryPlayAndRecord error:nil];
//Activate the session
[audioSession setActive:YES error:nil];
//Init CDSoundEngine
soundEngine = [[CDSoundEngine alloc] init];
//Define source groups
NSArray *defs = [NSArray arrayWithObjects: [NSNumber numberWithInt:1],nil];
[soundEngine defineSourceGroups:defs];
}
-(void) recordAudio {
//Set settings dictionary: IMA4 format, 44100 sample rate, 2 channels
NSMutableDictionary* recordSetting = [[[NSMutableDictionary alloc] init] autorelease];
[recordSetting setValue :[NSNumber numberWithInt:kAudioFormatAppleIMA4] forKey:AVFormatIDKey];
[recordSetting setValue:[NSNumber numberWithFloat:44100.0] forKey:AVSampleRateKey];
[recordSetting setValue:[NSNumber numberWithInt: 2] forKey:AVNumberOfChannelsKey];
//Set recording temp file location on disk
recordedTmpFile = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent: [NSString stringWithString: @"recording.caf"]]];
//Init AVAudioRecorder with location and settings
recorder = [[AVAudioRecorder alloc] initWithURL:recordedTmpFile settings:recordSetting error:nil];
//Set delegate and start recording
[recorder setDelegate:self];
[recorder prepareToRecord];
[recorder record];
}
-(void) playAudio {
//Override the audio to go back to the speaker
UInt32 audioRouteOverride = kAudioSessionOverrideAudioRoute_Speaker;
AudioSessionSetProperty(kAudioSessionProperty_OverrideAudioRoute, sizeof (audioRouteOverride),&audioRouteOverride);
//Get the file path to the recorded audio
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent: [NSString stringWithString: @"recording.caf"]];
//Play our recorded audio
[soundEngine loadBuffer:0 filePath: filePath];
[soundEngine playSound:0 sourceGroupId:0 pitch:pitch pan:0.0f gain:10.0f loop: NO];
}
-(void) stopRecordingAudio {
//Stop recording
[recorder stop];
}
- (void) unloadAudioSession {
//Remove temp file
NSFileManager * fm = [NSFileManager defaultManager];
if(recordedTmpFile){ [fm removeItemAtURL:recordedTmpFile error:nil]; }
//Release recorder
[recorder dealloc];
recorder = nil;
//Release sound engine
[soundEngine release];
//Deactivate audio session
AVAudioSession * audioSession = [AVAudioSession sharedInstance];
[audioSession setActive:NO error:nil];
}
@end

它是如何工作的...

录音和播放音频将向我们介绍一些新的类和概念。

  • 初始化音频会话:

    因为我们想录音,所以我们必须设置一个特定的音频会话。音频会话是一种配置我们将要使用的音频输入和输出设置的途径。AVAudioSession单例封装了这一功能。首先,我们需要设置会话类别以允许录音和播放:

    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:nil];
    
    

    然后我们需要激活会话:

    [[AVAudioSession sharedInstance] setActive:YES error:nil];
    
    

    CocosDenshion 通常做这些事情,但在这个食谱中,我们需要对音频系统有更细粒度的控制。

  • 初始化 CDSoundEngine:

    在这里,我们还初始化了一个CDSoundEngine对象:

    soundEngine = [[CDSoundEngine alloc] init];
    NSArray *defs = [NSArray arrayWithObjects: [NSNumber numberWithInt:1],nil];
    [soundEngine defineSourceGroups:defs];
    
    

    我们将使用它来播放我们录制的音频。

  • 录制音频:

    配方的核心,录制音频,需要几个步骤。首先,我们使用音频录制格式和我们要存储录制的音频的位置初始化AVAudioRecorder对象:

    NSMutableDictionary* recordSetting = [[[NSMutableDictionary alloc] init] autorelease];
    [recordSetting setValue :[NSNumber numberWithInt:kAudioFormatAppleIMA4] forKey:AVFormatIDKey];
    [recordSetting setValue:[NSNumber numberWithFloat:44100.0] forKey:AVSampleRateKey];
    [recordSetting setValue:[NSNumber numberWithInt: 2] forKey:AVNumberOfChannelsKey];
    recordedTmpFile = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent: [NSString stringWithString: @"recording.caf"]]];
    recorder = [[AVAudioRecorder alloc] initWithURL:recordedTmpFile settings:recordSetting error:nil];
    
    

    我们指定我们的代理对象:

    [recorder setDelegate:self];
    
    

    最后,我们开始录音:

    [recorder prepareToRecord];
    [recorder record];
    
    

    录制将持续到我们调用停止例程:

    [recorder stop];
    
    

    记录将不会停止,直到记录器收到stop消息或代理收到错误。例如,磁盘可能已满。

  • AVAudioRecorderDelegate协议:

    通过指定我们的Ch6_RecordingAudio类遵循AVAudioRecorderDelegate协议,我们同意处理包括错误在内的一系列方法调用。如果我们未能这样做,这些错误将被抛出。在这个例子中,为了简洁起见,我们跳过了这一步,但在一个专业的应用程序中,建议您处理AVAudioRecorder类可能想要传递的任何消息。

  • 播放我们录制的音频:

    一旦录制的音频存储在磁盘上,我们就可以播放它。在 iPhone 上,当音频会话类别为AVAudioSessionCategoryPlayAndRecord时,扬声器输出会被重定向到耳机扬声器。因此,在我们能够正确播放录制的音频之前,我们必须将播放重定向到扬声器:

    UInt32 audioRouteOverride = kAudioSessionOverrideAudioRoute_Speaker;
    AudioSessionSetProperty(kAudioSessionProperty_OverrideAudioRoute, sizeof (audioRouteOverride),&audioRouteOverride);
    
    

    现在,使用CDSoundEngine,我们可以将录制的音频加载到缓冲区并播放它:

    NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent: [NSString stringWithString: @"recording.caf"]];
    [soundEngine loadBuffer:0 filePath: filePath];
    [soundEngine playSound:0 sourceGroupId:0 pitch:pitch pan:0.0f gain:10.0f loop:NO];
    
    

    在上述行中,可以修改音调、平衡增益属性。在这个例子中,您可以修改音调。尝试录制您的声音,然后改变音调。

使用 iPod 音乐库

有时用户可能希望将他们个人收藏中的音乐曲目添加到游戏的背景中。在这个例子中,我们将创建一个简单的音乐播放器,可以从设备上的iPod 音乐库中加载歌曲、专辑和播放列表。

使用 iPod 音乐库

准备工作

请参阅项目RecipeCollection02以获取此配方的完整工作代码。

如何实现...

MediaPlayer框架链接到您的项目中。现在,执行以下代码:

#import <MediaPlayer/MediaPlayer.h>
#import "AppDelegate.h"
@interface Ch6_iPodLibrary : Recipe <MPMediaPickerControllerDelegate>
{ /* CODE OMITTED */ }
@implementation Ch6_iPodLibrary
-(CCLayer*) runRecipe {
//Device detection
NSString *model = [[UIDevice currentDevice] model];
//Show a blank recipe if we use the simulator
if([model isEqualToString:@"iPhone Simulator"]){
message.position = ccp(240,250);
[self showMessage:@"This recipe is not compatible with the Simulator. \nPlease connect a device."];
return self;
}
/* CODE OMITTED */
//Init music player
musicPlayer = [MPMusicPlayerController iPodMusicPlayer];
[musicPlayer setRepeatMode:MPMusicRepeatModeAll];
//Initial sync of display with music player state
[self handleNowPlayingItemChanged:nil];
//Register for music player notifications
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self selector:@selector(handleNowPlayingItemChanged:)
name:MPMusicPlayerControllerNowPlayingItemDidChangeNotification object:musicPlayer];
[musicPlayer beginGeneratingPlaybackNotifications];
return self;
}
- (void) handleNowPlayingItemChanged:(id)notification {
//Get the current playing item
MPMediaItem *currentItem = musicPlayer.nowPlayingItem;
//Set labels
if([currentItem valueForProperty:MPMediaItemPropertyTitle]){
[songLabel setString: [NSString stringWithFormat:@"%@",[currentItem valueForProperty:MPMediaItemPropertyTitle]]];
[artistLabel setString: [NSString stringWithFormat:@"%@",[currentItem valueForProperty:MPMediaItemPropertyArtist]]];
[albumLabel setString: [NSString stringWithFormat:@"%@",[currentItem valueForProperty:MPMediaItemPropertyAlbumTitle]]];
}
//Get album artwork
MPMediaItemArtwork *artwork = [currentItem valueForProperty:MPMediaItemPropertyArtwork];
UIImage *artworkImage = nil;
if(artwork) { artworkImage = [artwork imageWithSize:CGSizeMake(100,100)]; }
//Remove current album art if necessary
if(albumArt){
[self removeChild:albumArt cleanup:YES];
albumArt = nil;
}
//Set album art
if(artworkImage){
CCTexture2D *texture = [[[CCTexture2D alloc] initWithImage:artworkImage] autorelease];
albumArt = [CCSprite spriteWithTexture:texture];
[self addChild:albumArt z:1];
albumArt.position = ccp(240,120);
albumArt.scale = 0.25f;
}
}
//Play callback
-(void)playMusic:(id)sender { [musicPlayer play]; }
//Pause callback
-(void)pauseMusic:(id)sender{ [musicPlayer pause]; }
//Stop callback
-(void)stopMusic:(id)sender{ [musicPlayer stop]; }
//Next callback
- (void)nextSong:(id)sender { [musicPlayer skipToNextItem]; }
//Previous callback
- (void)previousSong:(id)sender {
//After 3.5 seconds hitting previous merely rewinds the song
static NSTimeInterval skipToBeginningOfSongIfElapsedTimeLongerThan = 3.5;
NSTimeInterval playbackTime = musicPlayer.currentPlaybackTime;
if (playbackTime <= skipToBeginningOfSongIfElapsedTimeLongerThan) {
//Previous song
[musicPlayer skipToPreviousItem];
} else {
//Rewind to beginning of current song
[musicPlayer skipToBeginning];
}
}
//Add music callback
- (void)openMediaPicker:(id)sender {
//Unit music MPMediaPickerController
MPMediaPickerController *mediaPicker = [[MPMediaPickerController alloc] initWithMediaTypes:MPMediaTypeMusic];
mediaPicker.delegate = self;
mediaPicker.allowsPickingMultipleItems = YES;
//Present picker as a modal view
((AppDelegate*)[UIApplication sharedApplication].delegate).viewController presentModalViewController:mediaPicker animated:YES];
[mediaPicker release];
}
- (void)mediaPicker: (MPMediaPickerController *)mediaPicker didPickMediaItems:(MPMediaItemCollection *)mediaItemCollection {
//Dismiss the picker
((AppDelegate*)[UIApplication sharedApplication].delegate).viewController dismissModalViewControllerAnimated:YES];
//Assign the selected item(s) to the music player and start playback.
[musicPlayer stop];
[musicPlayer setQueueWithItemCollection:mediaItemCollection];
[musicPlayer play];
}
- (void)mediaPickerDidCancel:(MPMediaPickerController *)mediaPicker {
//User chose no items, dismiss the picker
((AppDelegate*)[UIApplication sharedApplication].delegate).viewController dismissModalViewControllerAnimated:YES];
}
-(void) cleanRecipe {
//Stop player
[musicPlayer stop];
//Stop music player notifications
[[NSNotificationCenter defaultCenter] removeObserver:self name:MPMusicPlayerControllerNowPlayingItemDidChangeNotification object:musicPlayer];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:MPMusicPlayerControllerPlaybackStateDidChangeNotification object:musicPlayer];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:MPMusicPlayerControllerVolumeDidChangeNotification object:musicPlayer];
[musicPlayer endGeneratingPlaybackNotifications];
//Release player
[musicPlayer release];
musicPlayer = nil;
[super cleanRecipe];
}
@end

它是如何工作的...

按下绿色按钮将打开标准 iPod 媒体选择器。有些游戏选择创建自己的选择器以更好地匹配他们的用户界面。在这个例子中,我们为了简单起见选择了默认媒体选择器。

  • 初始化MPMusicPlayerController

    首先,我们创建我们的MPMusicPlayerController对象:

    musicPlayer = [MPMusicPlayerController iPodMusicPlayer];
    [musicPlayer setRepeatMode:MPMusicRepeatModeAll];
    
    

    我们将播放器设置为MPMusicRepeatModeAll,这样我们的nextSongpreviousSong方法就可以循环播放歌曲。

  • 获取现在播放音频信息:

    每当现在播放项目发生变化时,我们希望得到通知,以便我们可以获取媒体信息。为此,我们将我们的配方对象设置为musicPlayer对象的观察者,并允许接收此类通知:

    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
    [notificationCenter addObserver:self selector:@selector(handleNowPlayingItemChanged:)
    name:MPMusicPlayerControllerNowPlayingItemDidChangeNotification object:musicPlayer];
    [musicPlayer beginGeneratingPlaybackNotifications];
    
    

    作为观察者,我们的类将通过调用 handleNowPlayingItemChanged 方法得到通知。在这里,我们检查当前播放的 MPMediaItem 对象,以获取包括歌曲标题、艺术家姓名、专辑名称和专辑封面在内的信息:

    MPMediaItem *currentItem = musicPlayer.nowPlayingItem;
    if([currentItem valueForProperty:MPMediaItemPropertyTitle]){
    [songLabel setString: [NSString stringWithFormat:@"%@",[currentItem valueForProperty:MPMediaItemPropertyTitle]]];
    [artistLabel setString: [NSString stringWithFormat:@"%@",[currentItem valueForProperty:MPMediaItemPropertyArtist]]];
    [albumLabel setString: [NSString stringWithFormat:@"%@",[currentItem valueForProperty:MPMediaItemPropertyAlbumTitle]]];
    }
    MPMediaItemArtwork *artwork = [currentItem valueForProperty:MPMediaItemPropertyArtwork];
    UIImage *artworkImage = nil;
    if(artwork) { artworkImage = [artwork imageWithSize:CGSizeMake(100,100)]; }
    
    

    然后,我们将创建的 UIImage 对象放入场景中,使用第一章中描述的技术。

  • 使用 MPMediaPickerController:

    当用户触摸绿色按钮时,我们初始化一个 MPMediaPickerController 对象并指定其代理:

    MPMediaPickerController *mediaPicker = [[MPMediaPickerController alloc] initWithMediaTypes:MPMediaTypeMusic];
    mediaPicker.delegate = self;
    mediaPicker.allowsPickingMultipleItems = YES;
    
    

    然后,我们将选择器添加到屏幕上,以“模式视图”的形式展示它。这让我们可以动画化选择器滑入屏幕:

    ((AppDelegate*)[UIApplication sharedApplication].delegate).viewController presentModalViewController:mediaPicker animated:YES];
    
    

    当选择器打开时,用户可以从歌曲、播放列表、专辑等中进行选择。

  • MPMediaPickerControllerDelegate:

    遵循此代理,我们实现了以下方法:

    -(void) mediaPicker:(MPMediaPickerController *)mediaPicker didPickMediaItems:(MPMediaItemCollection *)mediaItemCollection;
    -(void) mediaPickerDidCancel:(MPMediaPickerController *)mediaPicker;
    
    

    这些对应于至少选择一个项目和选择零个项目的情况。在选择一个或多个项目后,我们关闭模式视图,将项目添加到我们的播放器中,然后播放第一个项目:

    ((AppDelegate*)[UIApplication sharedApplication].delegate).viewController dismissModalViewControllerAnimated:YES];
    [musicPlayer stop];
    [musicPlayer setQueueWithItemCollection:mediaItemCollection];
    [musicPlayer play];
    
    

    如果用户没有选择任何项目,我们简单地关闭模式视图控制器。

还有更多...

MPMusicPlayerController 类实际上正在访问你当前使用的设备的 iPod 功能。让你的应用程序访问外部资源,增加了我们需要考虑的几个额外条件:

  • 确定当前设备类型:

    如您从前面的代码中看到的那样,或者如果您在模拟器中尝试运行此配方,我们将在非真实设备上完全禁用此配方。我们这样做是因为模拟器上没有安装 iPod 音乐播放器应用程序,这会导致抛出错误。确定设备型号很简单:

    NSString *model = [[UIDevice currentDevice] model];
    
    

    这条字符串会告诉你你的应用程序正在运行什么型号。在我们的例子中,我们检查字符串 @"iPhone Simulator"。

  • UIApplicationDelegate 协议:

    使用 iPod 资源的一个副作用是,在挂起我们的应用程序后,音乐将继续播放。虽然你可以切换到 iPod 应用本身来停止播放的音乐,但我们希望在挂起我们的应用程序时停止它,并在将应用程序恢复时继续播放。在 AppDelegate.mm 中,我们的应用程序实现了由 UIApplicationDelegate 协议指定的某些方法:

    - (void)applicationWillResignActive:(UIApplication *)application;
    - (void)applicationDidBecomeActive:(UIApplication *)application;
    
    

    通常,Cocos2d 只在这里调用 pause 和 resume。我们将添加代码在挂起 iPod 音乐播放器时暂停它,在激活时播放它:

    - (void)applicationWillResignActive:(UIApplication *)application {
    [[CCDirector sharedDirector] pause];
    //Pause the music player if its playing
    if(![[[UIDevice currentDevice] model] isEqualToString:@"iPhone Simulator"]){
    MPMusicPlayerController *musicPlayer = [MPMusicPlayerController iPodMusicPlayer];
    if(musicPlayer.playbackState == MPMusicPlaybackStatePlaying){
    [musicPlayer pause];
    }
    }
    }
    - (void)applicationDidBecomeActive:(UIApplication *)application {
    [[CCDirector sharedDirector] resume];
    //Play the music play if its paused
    if(![[[UIDevice currentDevice] model] isEqualToString:@"iPhone Simulator"]){
    MPMusicPlayerController *musicPlayer = [MPMusicPlayerController iPodMusicPlayer];
    if(musicPlayer.playbackState == MPMusicPlaybackStatePaused){
    [musicPlayer play];
    }
    }
    }
    
    

    根据需要,可以在这里添加其他代码。

创建 MIDI 合成器

随着 iOS 4.0 的发布,iPhone、iPad 和 iPod Touch 现在可以利用强大的 MIDI 协议。对于允许用户生成自己的声音和音乐的游戏,或者对于想要酷炫复古声音但内存占用不大的游戏,MIDI 合成是完成这项工作的工具。在这个配方中,我们将使用出色的 MobileSynth 库创建 MIDI 合成器。

创建 MIDI 合成器

准备工作

请参考项目RecipeCollection02以获取此菜谱的完整工作代码。

如何做...

AudioToolbox框架链接到你的项目中。现在,执行以下代码:

#import "MIDISampleGenerator.h"
static const int kWhiteKeyNumbers[] = { 0, 2, 4, 5, 7, 9, 11 };
static const int kWhiteKeyCount = sizeof(kWhiteKeyNumbers) / sizeof(int);
static const int kBlackKey1Numbers[] = { 1, 3 };
static const int kBlackKey1Count = sizeof(kBlackKey1Numbers) / sizeof(int);
static const int kBlackKey2Numbers[] = { 6, 8, 10 };
static const int kBlackKey2Count = sizeof(kBlackKey2Numbers) / sizeof(int);
@implementation Ch6_MIDISynthesization
-(CCLayer*) runRecipe {
//Init sample generator
sampleGenerator = [[MIDISampleGenerator alloc] init];
//Init keyboard
[self initKeyboard];
return self;
}
-(void) initKeyboard {
/* CODE OMITTED */
}
-(void) randomize:(id)sender {
//Randomize values including Modulation, Oscillation, Filter, etc
[sampleGenerator randomize];
}
-(bool) keyPressed:(CCSprite*)key withHash:(NSString*)hashKey {
//Set darker key color
[key setColor:ccc3(255,100,100)];
//Play note
[sampleGenerator noteOn:key.tag];
//Keep track of touch
[keyTouches setObject:[NSNumber numberWithInt:key.tag] forKey:hashKey];
return YES;
}
-(bool) keyReleased:(int)note remove:(bool)remove {
/* CODE OMITTED */
if(keyReleased){
//Stop playing note
[sampleGenerator noteOff:note];
//Remove tracking
if(remove){ [keyTouches removeObjectForKey:[NSNumber numberWithInt:note]]; }
}
return keyReleased;
}
@end

它是如何工作的...

这个菜谱让你能在虚拟键盘上播放两个八度的合成声音。

  • MIDISampleGenerator类:

    MIDISampleGenerator类是专门为这个菜谱创建的,以便模糊使用 MobileSynth 的一些更粗糙的细节。MobileSynth 库提供了一系列令人眼花缭乱的音效合成选项来生成声音。其中包括,仅举几个例子,调制、振荡、滤波、琶音以及一些与音量相关的效果。随机化按钮随机化这些效果中的一系列,以便用户能够快速轻松地了解合成化可能性的范围。

  • 扩展合成器:

    想象一下,合成器可以扩展到将一首歌(一系列定时音符)记录到数据文件中,然后像自动钢琴一样将其输入合成器。这可以作为生成大量复古风格的电子游戏音乐的一种简单解决方案,而这些音乐不会占用太多空间(想想《Mega Man》)。同样的情况也适用于音效。

更多...

关于MobileSynth的更多信息,您可以访问他们的网站:code.google.com/p/mobilesynth/

语音识别和文本到语音

在机器学习、量子计算和 3D 打印的宿命组合孕育出统治全人类的暴政人工智能生命体之前,我们需要满足于半智能设备,这些设备是我们手动编程的。这个谜团中的重要部分是语言处理。在这个菜谱中,我们将使用OpenEars库让我们的 iOS 设备说话并识别一些基本的英语对话。

语音识别和文本到语音

准备工作

由于使用OpenEars所需的库大小,这个菜谱有自己的项目。请参考项目Ch6_SpeechRecognition以获取此菜谱的完整工作代码。

如何做...

OpenEars的安装过程很复杂。其中之一是它需要配置四个其他库:flite、pocketsphinx、sphinxbasewince。OpenEars 库本身是一个嵌入的 XCode 项目,该项目与你的项目静态链接。

建议您首先查看Ch6_SpeechRecognition项目。从那里,您可以仔细遵循www.politepix.com/openears/上列出的步骤来设置和配置示例项目。

在遵循“入门”和“为 OpenEars 配置你的应用”页面上的步骤之后,你可以转到“在应用中使用 OpenEars”页面。在这里,你将被告知创建一个语料库文件。这是一个包含我们希望 OpenEars 识别的所有单词和短语的文件。我们的语料库文件看起来像这样:

HELLO, HAL. DO YOU READ ME, HAL?
OPEN THE POD BAY DOORS, HAL.
WHAT'S THE PROBLEM?
WHAT ARE YOU TALKING ABOUT, HAL?
I DON'T KNOW WHAT YOU'RE TALKING ABOUT, HAL.
WHERE THE HELL'D YOU GET THAT IDEA, HAL?
ALRIGHT, HAL. I'LL GO IN THROUGH THE EMERGENCY AIRLOCK.
HAL, I WON'T ARGUE WITH YOU ANYMORE. OPEN THE DOORS.

然后,我们将此文件上传到卡内基梅隆大学提供的Sphinx知识库创建工具,网址为:www.speech.cs.cmu.edu/tools/lmtool-new.html。该工具将为您生成多个文件。将.lm文件重命名为.languagemodel文件。同时下载.dic文件。按照以下说明将这些文件添加到您的项目中:www.politepix.com/openears/yourapp/

现在,我们已经准备好开始编码了。我们主要的代码片段将给用户一些自信的 HAL 9000 响应:

#import "cocos2d.h"
#import "AudioSessionManager.h"
#import "OpenEarsEventsObserver.h"
#import "PocketsphinxController.h"
#import "FliteController.h"
@interface MainLayer : CCLayer <OpenEarsEventsObserverDelegate>
{ /* CODE OMITTED */ }
@end
@implementation MainLayer
-(id) init
{
if( (self=[super init])) {
/* CODE OMITTED */
//Init AudioSessionManager and start session
audioSessionManager = [[AudioSessionManager alloc] init];
[audioSessionManager startAudioSession];
//Init pocketsphinx, flite and OpenEars
pocketsphinxController = [[PocketsphinxController alloc] init];
fliteController = [[FliteController alloc] init];
openEarsEventsObserver = [[OpenEarsEventsObserver alloc] init];
//Text to speech
[self say:@"Welcome to OpenEars."];
[self runAction:[CCSequence actions:[CCDelayTime actionWithDuration:4.0f],
[CCCallFunc actionWithTarget:self selector:@selector(welcomeMessage)], nil]];
//Start the Pocketsphinx continuous listening loop.
[pocketsphinxController startListening];
//Set this is an OpenEars observer delegate
[openEarsEventsObserver setDelegate:self];
/* CODE OMITTED */
}
return self;
}
-(void) welcomeMessage {
//Greet the user with a message about his pitiful human brain
[self say:@"Hello Dave. I've just picked up a fault in your brain. \nIt's going to go 100% failure in 72 hours. \nWould you like me to open the pod bay doors?"];
}
-(void) saySomething {
//Respond with a random response
int num = arc4random()%5;
if(num == 0){
[self say:@"This mission is too important for me to allow you to \njeopardize it Dave."];
}
/* CODE OMITTED */
}
-(void) say:(NSString*)str { /* CODE OMITTED */
//Have flite speak the message (text to speech)
[fliteController say:str];
}
-(void) suspendRecognition { /*CODE OMITTED */
//Suspend recognition
[pocketsphinxController suspendRecognition];
}
-(void) resumeRecognition { /* CODE OMITTED */
//Suspend recognition
[pocketsphinxController resumeRecognition];
}
-(void) stopListening { /* CODE OMITTED */
//Stop listening
[pocketsphinxController stopListening];
}
-(void) startListening { /* CODE OMITTED */
//Start listening
[pocketsphinxController startListening];
}
//Delivers the text of speech that Pocketsphinx heard and analyzed, along with its accuracy score and utterance ID.
- (void) pocketsphinxDidReceiveHypothesis:(NSString *)hypothesis recognitionScore:(NSString *)recognitionScore utteranceID:(NSString *)utteranceID {
//Display information
[self showMessage:[NSString stringWithFormat:@"The received hypothesis is %@ with a score of %@ and an ID of %@", hypothesis, recognitionScore, utteranceID]]; //Log it.
//Tell the user what we heard
[self say:[NSString stringWithFormat:@"You said %@",hypothesis]]; //React to it by telling our FliteController to say the heard phrase.
//Respond with a witty retort
[self runAction:[CCSequence actions:[CCDelayTime actionWithDuration:4.0f],
[CCCallFunc actionWithTarget:self selector:@selector(saySomething)], nil]];
}
@end

它是如何工作的...

尝试将语料库文件中的几行话输入到演示应用中。在安静的房间里,结果可以非常准确。

  • 实例化音频会话、控制器和观察者:

    在我们能够做任何事情之前,我们需要实例化音频会话以及提供文本到语音和语音识别功能的三个控制器:

    audioSessionManager = [[AudioSessionManager alloc] init];
    [audioSessionManager startAudioSession];
    pocketsphinxController = [[PocketsphinxController alloc] init];
    fliteController = [[FliteController alloc] init];
    openEarsEventsObserver = [[OpenEarsEventsObserver alloc] init];
    
    

    PocketsphinxController对象提供了语音识别 API。FliteController对象提供了文本到语音 API。最后,OpenEarsEventsObserver提供了一个协议,代表两个控制器进行回调。

  • 使用 FliteController:

    FliteController API 非常简单直接。只需调用 say 方法,Flite 将通过默认音频通道生成计算机语音。

  • 使用 PocketsphinxController:

    PocketsphinxController API 允许您使用以下四个方法来管理 Pocketsphinx 何时监听以及何时积极尝试识别语音:

    [pocketsphinxController suspendRecognition];
    [pocketsphinxController resumeRecognition];
    [pocketsphinxController stopListening];
    [pocketsphinxController startListening];
    
    

    这种基本级别的控制让您能够管理何时使用处理器时间实际尝试识别语音模式。

  • OpenEarsEventsObserverDelegate协议:

    此协议负责代表PocketsphinxControllerFliteController调用多个方法。需要特别注意的重要方法是主 Pocketsphinx 语音识别假设方法。这将告诉你 Pocketsphinx 听到了什么,并且还会给它一个置信度分数:

    - (void) pocketsphinxDidReceiveHypothesis:(NSString *)hypothesis recognitionScore:(NSString *)recognitionScore utteranceID:(NSString *)utteranceID;
    
    

    当 Pocketsphinx 正在监听并尝试识别时,此方法将由任何离散声音触发。然而,您可以使用recognitionScore来过滤掉背景噪音和其他

第七章。AI 和逻辑

在本章中,我们将涵盖以下内容:

  • 处理 AI 路标

  • 向移动目标发射弹丸

  • AI 视线

  • 使用 Boids 进行 AI 群集

  • 在网格上进行 A*路径查找

  • 在 Box2D 世界中进行 A*路径查找

  • 在 TMX 瓦片地图上进行 A*路径查找

  • 在横版滚动游戏中进行 A*路径查找

  • 运行 Lua 脚本

  • 动态加载 Lua 脚本

  • 使用 Lua 进行对话树

简介

所有模拟智能行为的游戏都使用一种形式的人工智能(AI)。根据不同的游戏玩法需求,使用不同的技术来模拟行为。在本章中,我们将实现其中的一些技术。

处理 AI 路标

最基本的 AI 过程之一涉及在物理环境中移动 AI 演员。为此,我们将创建一个队列路标。每个路标代表我们希望演员移动到的下一个位置。

处理 AI 路标

准备工作

请参阅项目RecipeCollection03以获取此菜谱的完整工作代码。

如何做到这一点...

执行以下代码:

#import "GameWaypoint.h"
@interface GameActor : GameObject {
NSMutableArray *waypoints;
float runSpeed;
}
@end
@implementation GameActor
-(void) processWaypoints {
bool removeFirstWaypoint = NO;
//The actor's position onscreen
CGPoint worldPosition = CGPointMake(self.body->GetPosition().x * PTM_RATIO, self.body->GetPosition().y * PTM_RATIO);
//Process waypoints
for(GameWaypoint *wp in waypoints){
float distanceToNextPoint = [GameHelper distanceP1:worldPosition toP2:CGPointMake(wp.position.x, wp.position.y)];
//If we didn't make progress to the next point, increment timesBlocked
if(distanceToNextPoint >= wp.lastDistance){
timesBlocked++;
//Drop this waypoint if we failed to move a number of times
if(timesBlocked > TIMES_BLOCKED_FAIL){
distanceToNextPoint = 0.0f;
}
}else{
//If we are just starting toward this point we run our pre-callback
wp.lastDistance = distanceToNextPoint;
[wp processPreCallback];
}
//If we are close enough to the waypoint we move onto the next one
if(distanceToNextPoint <= WAYPOINT_DIST_THRESHOLD){
removeFirstWaypoint = YES;
[self stopRunning];
//Run post callback
[wp processPostCallback];
}else{
//Keep running toward the waypoint
float speedMod = wp.speedMod;
//Slow down close to the waypoint
if(distanceToNextPoint < [self runSpeed]/PTM_RATIO){
speedMod = (distanceToNextPoint)/([self runSpeed]/PTM_RATIO);
}
[self runWithVector:ccp(wp.position.x - worldPosition.x, wp.position.y - worldPosition.y) withSpeedMod:speedMod withConstrain:NO ];
break;
}
}
if(removeFirstWaypoint){
[waypoints removeObjectAtIndex:0];
timesBlocked = 0;
}
}
@end
@implementation Ch7_Waypoints
-(CCLayer*) runRecipe {
//Add polygons
[self addRandomPolygons:10];
//Create Actor
[self addActor];
/* CODE OMITTED */
return self;
}
-(void) step: (ccTime) dt {
[super step:dt];
//Process actor waypoints
[actor processWaypoints];
//Turn actor toward next waypoint
if(actor.waypoints.count > 0){
CGPoint movementVector = ccp(actor.body->GetLinearVelocity().x, actor.body->GetLinearVelocity().y);
actor.body->SetTransform(actor.body->GetPosition(), -1 * [GameHelper vectorToRadians:movementVector] + PI_CONSTANT/2);
}
}
/* Draw all waypoint lines */
-(void) drawLayer {
glColor4ub(255,255,0,32);
CGPoint actorPosition = ccp(actor.body->GetPosition().x*PTM_RATIO, actor.body->GetPosition().y*PTM_RATIO);
if(actor.waypoints.count == 1){
GameWaypoint *gw = (GameWaypoint*)[actor.waypoints objectAtIndex:0];
ccDrawLine(actorPosition, gw.position);
}else if(actor.waypoints.count > 1){
for(int i=0; i<actor.waypoints.count-1; i++){
GameWaypoint *gw = (GameWaypoint*)[actor.waypoints objectAtIndex:i];
GameWaypoint *gwNext = (GameWaypoint*)[actor.waypoints objectAtIndex:i+1];
if(i == 0){
//From actor to first waypoint
ccDrawLine(actorPosition, gw.position);
ccDrawLine(gw.position, gwNext.position);
}else{
//From this waypoint to next one
ccDrawLine(gw.position, gwNext.position);
}
}
}
glColor4ub(255,255,255,255);
}
/* Add a new waypoint when you touch the screen */
-(void) tapWithPoint:(CGPoint)point {
ObjectCallback *goc1 = [ObjectCallback createWithObject:self withCallback:@"movingToWaypoint"];
ObjectCallback *goc2 = [ObjectCallback createWithObject:self withCallback:@"reachedWaypoint"];
GameWaypoint *wp = [GameWaypoint createWithPosition:[self convertTouchCoord:point] withSpeedMod:1.0f];
wp.preCallback = goc1;
wp.postCallback = goc2;
[actor addWaypoint:wp];
}
@end

它是如何工作的...

路标处理涉及在每一步将演员移动到下一个路标。如果演员停止向下一个点前进,则该点将被丢弃。

  • GameWaypoint:

    GameWaypoint类包含多个变量,其中最重要的是路标的位置以及演员应该以多快的速度移动到该点。

  • 处理路标:

    GameActor类中存储了一个GameWaypoint对象列表。在每一帧中都会调用processWaypoints方法。这会将演员移动到下一个路标。此过程的伪代码如下:

    for all waypoints
    if we didn't make progress, increment timesBlocked
    if we have reached this waypoint we remove it and move to the next
    else keep running toward this waypoint and break the loop
    
    

    使用这种基本逻辑,我们在 2D 空间中将演员移动到每个后续路标。

  • ObjectCallback:

    包含了额外的功能,允许在演员到达特定路标前后进行方法回调。这些使用ObjectCallback类,该类简单地使用以下行在现有类上调用方法:

    [preCallback.obj performSelector:NSSelectorFromString(preCallback.callback)];
    
    

    这使我们能够结合逻辑和 AI 角色移动。

  • 使用凸包算法创建随机多边形:

    为了用随机生成的多边形填充我们的物理世界,我们使用单调链凸包算法生成我们的多边形顶点:

    NSMutableArray *convexPolygon = [GameHelper convexHull:points];
    
    

    此方法接受一组随机生成的点,并返回围绕这些点的顶点数组。尽管我们使用这个算法的原因相当简单,但该算法还有许多其他应用,从简单的 AI 到高级计算机视觉。有关此算法的更多信息,请参阅此处:en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain

向移动目标发射弹丸

为了使人工智能演员能够与环境进行真实交互,它们必须进行人类玩家自然做出的计算。一种常见的交互涉及向移动目标发射投射物。

向移动目标发射投射物

准备工作

请参阅项目RecipeCollection03以获取此菜谱的完整工作代码。

如何做...

执行以下代码:

@implementation Ch7_ProjectileAiming
-(void) step: (ccTime) dt {
[super step:dt];
/* CODE OMITTED */
//Firing projectiles
fireCount += dt;
if(fireCount > 1.0f){
fireCount = 0;
[self fireMissiles];
}
}
/* Each enemy fires a missile object */
-(void) fireMissiles {
for(int i=0; i<enemies.count; i++){
GameActor *enemy = [enemies objectAtIndex:i];
//Create missile
GameMisc *missile = [[GameMisc alloc] init];
missile.gameArea = self;
missile.tag = GO_TAG_MISSILE;
missile.bodyDef->type = b2_dynamicBody;
missile.bodyDef->position.Set( enemy.body->GetPosition().x, enemy.body->GetPosition().y );
missile.bodyDef->userData = missile;
missile.body = world->CreateBody(missile.bodyDef);
missile.polygonShape = new b2PolygonShape();
missile.polygonShape->SetAsBox(0.5f, 0.2f);
missile.fixtureDef->density = 10.0f;
missile.fixtureDef->shape = missile.polygonShape;
missile.fixtureDef->filter.categoryBits = CB_MISSILE;
missile.fixtureDef->filter.maskBits = CB_EVERYTHING & ~CB_MISSILE & ~CB_ENEMY;
missile.body->CreateFixture(missile.fixtureDef);
//Calculate intercept trajectory
Vector3D *point = [self interceptSrc:missile dst:actor projSpeed:20.0f];
if(point){
//Align missile
CGPoint pointToFireAt = CGPointMake(point.x, point.y);
CGPoint directionVector = CGPointMake(pointToFireAt.x - missile.body->GetPosition().x, pointToFireAt.y - missile.body->GetPosition().y);
float radians = [GameHelper vectorToRadians:directionVector];
missile.body->SetTransform(missile.body->GetPosition(), -1 * radians + PI_CONSTANT/2);
//Fire missile
CGPoint normalVector = [GameHelper radiansToVector:radians];
missile.body->SetLinearVelocity( b2Vec2(normalVector.x*20.0f, normalVector.y*20.0f) );
}
[missiles addObject:missile];
}
}
/* Find the intercept angle given projectile speed and a moving target */
-(Vector3D*) interceptSrc:(GameObject*)src dst:(GameObject*)dst projSpeed:(float)projSpeed {
float tx = dst.body->GetPosition().x - src.body->GetPosition().x;
float ty = dst.body->GetPosition().y - src.body->GetPosition().y;
float tvx = dst.body->GetLinearVelocity().x;
float tvy = dst.body->GetLinearVelocity().y;
//Get quadratic equation components
float a = tvx*tvx + tvy*tvy - projSpeed*projSpeed;
float b = 2 * (tvx * tx + tvy * ty);
float c = tx*tx + ty*ty;
//Solve quadratic equation
Vector3D *ts = [GameHelper quadraticA:a B:b C:c];
//Find the smallest positive solution
Vector3D *solution = nil;
if(ts){
float t0 = ts.x;
float t1 = ts.y;
float t = MIN(t0,t1);
if(t < 0){ t = MAX(t0,t1); }
if(t > 0){
float x = dst.body->GetPosition().x + dst.body->GetLinearVelocity().x*t;
float y = dst.body->GetPosition().y + dst.body->GetLinearVelocity().y*t;
solution = [Vector3D x:x y:y z:0];
}
}
return solution;
}
@end

它是如何工作的...

在这个菜谱中,我们创建了三个向玩家发射投射物的敌对演员。每个投射物都以足够的速度和方向发射,即使玩家在移动也能击中玩家。

  • 计算拦截轨迹:

    如果我们给出了演员和敌人的位置、速度以及我们可以发射投射物的速度,我们可以通过为玩家和投射物创建一个距离随时间变化的方程来计算'拦截角度'。然后我们使用二次方程来找到这些线相交的时间。公式如下:

    x = (-b +/- sqrt(b2 - 4ac)) / 2a
    
    

    为了得到 a、b 和 c 变量,我们进行以下操作,其中txty是位置向量的分量,tvxtvy是速度向量的分量:

    float a = tvx*tvx + tvy*tvy - projSpeed*projSpeed;
    float b = 2 * (tvx * tx + tvy * ty);
    float c = tx*tx + ty*ty;
    
    

    然后我们使用我们的GameHelper二次方程法:

    Vector3D *ts = [GameHelper quadraticA:a B:b C:c];
    
    

    二次方法返回一个Vector3D对象,以便在NSObject内部方便地存储两个浮点原语。如果对象为 nil,则公式的判别式为<= 0.0f。否则,我们取最小的非零解。我们使用这个来最终计算发射解决方案:

    float x = dst.body->GetPosition().x + dst.body->GetLinearVelocity().x*t;
    float y = dst.body->GetPosition().y + dst.body->GetLinearVelocity().y*t;
    solution = [Vector3D x:x y:y z:0];
    
    

    投射物可以以最初指定的速度向这个方向发射。如果移动目标保持航向,投射物将与之相撞。

  • 使用布尔代数进行 Box2D 过滤:

    就像在第四章“物理”中一样,这里我们使用类别位和掩码位来防止某些物理对象类型相撞。在这个菜谱中,我们通过使用'一切位'(0xFFFF)以及一些更高级的布尔逻辑来扩展我们对这种技术的使用:

    missile.fixtureDef->filter.categoryBits = CB_MISSILE;
    missile.fixtureDef->filter.maskBits = CB_EVERYTHING & ~CB_MISSILE & ~CB_ENEMY;
    
    

    这可以防止导弹与发射它们的敌人相撞,以及彼此相撞。

人工智能视线

人类使用五种不同的感官与环境互动。其中之一,视觉,是计算机科学的一个分支,称为计算机视觉。在这个例子中,我们使用射线投射在 Box2D 环境中实现基本的视觉测试,以查看玩家和敌对人工智能演员之间是否有其他对象。

人工智能视线

准备工作

请参阅项目RecipeCollection03以获取此菜谱的完整工作代码。

如何做...

执行以下代码:

class RayCastAnyCallback : public b2RayCastCallback
{
public:
RayCastAnyCallback()
{
m_hit = false;
}
float32 ReportFixture( b2Fixture* fixture, const b2Vec2& point,
const b2Vec2& normal, float32 fraction)
{
b2Body* body = fixture->GetBody();
void* userData = body->GetUserData();
if (userData)
{
int32 index = *(int32*)userData;
if (index == 0)
{
// filter
return -1.0f;
}
}
m_hit = true;
m_point = point;
m_normal = normal;
m_fraction = fraction;
m_fixture = fixture;
return 0.0f;
}
bool m_hit;
b2Vec2 m_point;
b2Vec2 m_normal;
float32 m_fraction;
b2Fixture *m_fixture;
};
@implementation Ch7_LineOfSight
-(void) step: (ccTime) dt {
[super step:dt];
/* CODE OMITTED */
//Make the enemies follow the actor
[self followActorWithEnemies];
}
-(void) followActorWithEnemies {
//If enemies can see the actor they follow
for(int i=0; i<enemies.count; i++){
//Align enemies
GameActor *enemy = [enemies objectAtIndex:i];
CGPoint directionVector = CGPointMake(actor.body->GetPosition().x - enemy.body->GetPosition().x, actor.body->GetPosition().y - enemy.body->GetPosition().y);
float radians = [GameHelper vectorToRadians:directionVector];
enemy.body->SetTransform(enemy.body->GetPosition(), -1 * radians + PI_CONSTANT/2);
RayCastClosestCallback callback;
world->RayCast(&callback, enemy.body->GetPosition(), actor.body->GetPosition());
//Did the raycast hit anything?
enemy.tag = 0; //Assume it didn't
//Note that in this case we are using the 'tag' property for something other than its intended purpose.
if(callback.m_hit){
//Is the closest point the actor?
if(callback.m_fixture->GetBody() == actor.body){
//If so, follow the actor
b2Vec2 normal = b2Vec2( callback.m_normal.x * -5.0f, callback.m_normal.y * -5.0f);
enemy.body->ApplyForce(normal, actor.body->GetPosition());
enemy.tag = 1; //Set seeing flag to true
}
}
}
}
/* Draw each enemy 'sight line' if they can see you */
-(void) drawLayer {
for(int i=0; i<enemies.count; i++){
GameActor *enemy = [enemies objectAtIndex:i];
if(enemy.tag == 1){
glColor4ub(255,255,0,32);
CGPoint actorPosition = ccp(actor.body->GetPosition().x*PTM_RATIO, actor.body->GetPosition().y*PTM_RATIO);
CGPoint enemyPosition = ccp(enemy.body->GetPosition().x*PTM_RATIO, enemy.body->GetPosition().y*PTM_RATIO);
ccDrawLine(actorPosition, enemyPosition);
glColor4ub(255,255,255,255);
}
}
}
@end

它是如何工作的...

当可以在敌对演员和玩家之间画一条直线而不穿过关卡几何时,我们认为玩家对敌人来说是可见的,然后敌人开始跟随玩家。

  • 使用RayCastClosest:

    我们使用以下方法对 Box2D 世界执行射线测试:

    RayCastClosestCallback callback;
    world->RayCast(&callback, enemy.body->GetPosition(), actor.body->GetPosition());
    
    

    RayCastClosestCallback 封装了类 b2RayCastCallback。当我们调用 RayCast 方法并传入这个类的实例时,我们就能判断我们的射线是否触碰到一个 Box2D 几何体。它还维护了一个指向它首次接触到的几何体的指针。这是离我们的源点最近的几何体。

  • 过滤掉第一个找到的几何体:

    由于射线投射通常涉及从一个几何体向另一个几何体投射,所以第一个找到的几何体会被过滤掉。

  • RayCast.h:

    我们的 RayCast.h 文件还包含类 RayCastAnyCallbackRayCastMultipleCallbackany 类在射线上找到任何几何体,而 multiple 类维护一个几何体列表。

还有更多...

射线投射有许多用途。一个简单的例子是掩体机制。当敌人感到脆弱时,它可以找到玩家无法看穿的最近的几何体。另一个用途涉及到使用射线投射返回的 法线 点。这是与指定几何体碰撞的确切点。这可以用来创建激光武器或瞬间子弹冲击。

使用 Boids 的 AI 鸟群

通过在游戏中放置更多的敌人,我们开始需要一些基于群体的 AI。在视频游戏和电影中广泛使用的一个流行算法是 Boids 算法。它模拟 鸟群 行为。在这个菜谱中,我们将创建大量聚集在一起并追逐玩家的敌人。

使用 Boids 的 AI 鸟群

准备工作

请参考项目 RecipeCollection03 以获取此菜谱的完整工作代码。

如何做到这一点...

执行以下代码:

@interface Ch7_AIFlocking : Ch7_LineOfSight {}
/* CODE OMITTED */
@end
@implementation Ch7_AIFlocking
-(void) step:(ccTime)delta {
[super step:delta];
//Process the 'boids' flocking algorithm
[self processBoids];
}
/* Make the flock of 'boids' follow the actor */
-(void) followActorWithEnemies {
//All enemies constantly follow the actor
for(int i=0; i<enemies.count; i++){
//Align enemies
GameActor *enemy = [enemies objectAtIndex:i];
CGPoint directionVector = CGPointMake(actor.body->GetPosition().x - enemy.body->GetPosition().x, actor.body->GetPosition().y - enemy.body->GetPosition().y);
float radians = [GameHelper vectorToRadians:directionVector];
enemy.body->SetTransform(enemy.body->GetPosition(), -1 * radians + PI_CONSTANT/2);
b2Vec2 normal = actor.body->GetPosition() - enemy.body->GetPosition();
CGPoint vector = ccp(normal.x, normal.y);
CGPoint normalVector = [GameHelper radiansToVector:[GameHelper vectorToRadians:vector]];
//If so, follow the actor
b2Vec2 v = enemy.body->GetLinearVelocity();
enemy.body->SetLinearVelocity(b2Vec2(v.x + normalVector.x*0.2f, v.y + normalVector.y*0.2f));
}
}
/* Process boids algorithm */
-(void) processBoids {
for(int i=0; i<enemies.count; i++){
GameActor *b = [enemies objectAtIndex:i];
b2Vec2 v1 = b2Vec2(0,0);
b2Vec2 v2 = b2Vec2(0,0);
b2Vec2 v3 = b2Vec2(0,0);
v1 = [self boidRule1:b];
v2 = [self boidRule2:b];
v3 = [self boidRule3:b];
b2Vec2 v = b.body->GetLinearVelocity();
b2Vec2 newV = v+v1+v2+v3;
/* Limit velocity */
float vLimit = 7.5f;
b2Vec2 absV = b2Vec2([GameHelper absoluteValue:newV.x], [GameHelper absoluteValue:newV.y]);
if(absV.x > vLimit || absV.y > vLimit){
float ratio;
if(absV.x > absV.y){
ratio = vLimit / absV.x;
}else{
ratio = vLimit / absV.y;
}
newV = b2Vec2( newV.x*ratio, newV.y*ratio );
}
b.body->SetLinearVelocity(newV);
}
}
/* Clump the Boids together */
-(b2Vec2) boidRule1:(GameActor*)bJ {
//The variable 'pcJ' represents the center point of the flock
b2Vec2 pcJ = b2Vec2(0,0);
float N = enemies.count;
//Add up all positions
for(int i=0; i<enemies.count; i++){
GameActor *b = [enemies objectAtIndex:i];
if(b != bJ){
pcJ += b.body->GetPosition();
}
}
//Average them out
pcJ = b2Vec2(pcJ.x/(N-1), pcJ.y/(N-1));
//Return 1/100 of the velocity required to move to this point
return b2Vec2( (pcJ.x - bJ.body->GetPosition().x)/100.0f, (pcJ.y - bJ.body->GetPosition().y)/100.0f );
}
/* Keep the Boids apart from each other */
-(b2Vec2) boidRule2:(GameActor*)bJ {
//Set optimal distance boids should keep between themselves (padding)
float padding = 1.5f;
//The variable 'c' represents the velocity required to move away from any other boids in this one's personal space
b2Vec2 c = b2Vec2(0,0);
//If an ememy is too close we add velocity required to move away from it
for(int i=0; i<enemies.count; i++){
GameActor *b = [enemies objectAtIndex:i];
if(b != bJ){
CGPoint bPos = ccp(b.body->GetPosition().x, b.body->GetPosition().y);
CGPoint bJPos = ccp(bJ.body->GetPosition().x, bJ.body->GetPosition().y);
if([GameHelper distanceP1:bPos toP2:bJPos] < padding){
c = c - (b.body->GetPosition() - bJ.body->GetPosition());
}
}
}
return c;
}
/* Match up all Boid velocities */
-(b2Vec2) boidRule3:(GameActor*)bJ {
//The variable 'pvJ' represents the total velocity of all the boids combined
b2Vec2 pvJ = b2Vec2(0,0);
//Get the total velocity
for(int i=0; i<enemies.count; i++){
GameActor *b = [enemies objectAtIndex:i];
if(b != bJ){
pvJ += b.body->GetLinearVelocity();
}
}
//Get this boid's velocity
b2Vec2 v = bJ.body->GetLinearVelocity();
//Return the difference averaged out over the flock then divided by 30
return b2Vec2((pvJ.x - v.x)/30.0f/enemies.count, (pvJ.y - v.y)/30.0f/enemies.count);
}
@end

它是如何工作的...

Boids 算法使用一些简单概念来创建逼真的角色鸟群。它使用三个规则,在每个帧上作用于每个角色的速度。这些规则调整它们的行为,以保持鸟群而不过度影响其他力量。

  • Boids 规则 1—保持 鸟群在一起:

    为了使角色鸟群保持在一起,我们首先通过平均所有角色的位置来获取鸟群的重心。然后我们调整角色的速度,使每个角色向鸟群中心移动 1%。

  • Boids 规则 2—给予 角色一些个人空间:

    为了防止鸟群过于聚集,我们检查每个角色。如果某个角色在某个空间阈值内还有其他角色,那么我们就将这个角色移动到离其他角色这个距离。当这个操作应用于所有角色时,就会达到一个良好的平衡。

  • Boids 规则 3—匹配 所有角色的速度:

    最后,所有角色应该以大致相同的速度一起移动。在规则 1 中,我们平均了所有角色的位置。在这个规则中,我们平均了所有角色的速度,然后给每个角色加上该速度的分数(1/30)。这确保了所有角色的均匀移动速度。

  • 将角色移动到玩家身边:

    将敌人角色移动到玩家身边涉及找到指向玩家的归一化向量,然后将该向量与一定大小的向量相加到角色的线性速度上:

    enemy.body->SetLinearVelocity(b2Vec2(v.x + normalVector.x*0.2f, v.y + normalVector.y*0.2f));
    
    

    我们还限制了每个角色的速度,以防止它们快速冲向一个方向:

    float vLimit = 7.5f;
    b2Vec2 absV = b2Vec2([GameHelper absoluteValue:newV.x], [GameHelper absoluteValue:newV.y]);
    if(absV.x > vLimit || absV.y > vLimit){
    float ratio;
    if(absV.x > absV.y){
    ratio = vLimit / absV.x;
    }else{
    ratio = vLimit / absV.y;
    }
    newV = b2Vec2( newV.x*ratio, newV.y*ratio );
    }
    b.body->SetLinearVelocity(newV);
    
    

    当所有这些方法结合在一起时,我们得到一个逼真的群聚效果,适用于鸟群、蜜蜂,甚至僵尸。

网格上的 A* 寻路

一个经典视频游戏问题就是寻路问题。在游戏过程中,智能角色通常需要绕过障碍物进行导航。A* 搜索算法(也称为A 星)通常用于通过有效地遍历构建的节点图来解决寻路问题。在这个菜谱中,我们将演示基于网格的 A* 寻路。

网格上的 A* 寻路

准备工作

请参阅项目 RecipeCollection03 以获取此菜谱的完整工作代码。

如何操作...

执行以下代码:

/* AStarNode */
@interface AStarNode : NSObject
{
CGPoint position; //The node's position on our map
NSMutableArray *neighbors; //An array of neighbor AStarNode objects
bool active; //Is this node active?
float costMultiplier; //Use this to multiply the normal cost to reach this node.
}
@end
@implementation AStarNode
/* Cost to node heuristic */
-(float) costToNode:(AStarNode*)node {
CGPoint src = ccp(self.position.x, self.position.y);
CGPoint dst = ccp(node.position.x, node.position.y);
float cost = [GameHelper distanceP1:src toP2:dst] * node.costMultiplier;
return cost;
}
@end
/* AStarPathNode */
@interface AStarPathNode : NSObject
{
AStarNode *node; //The actual node this "path" node points to
AStarPathNode *previous; //The previous node on our path
float cost; //The cumulative cost of reaching this node
}
@end
@implementation AStarPathNode
/* Our implementation of the A* search algorithm */
+(NSMutableArray*) findPathFrom:(AStarNode*)fromNode to:(AStarNode*)toNode {
NSMutableArray *foundPath = [[NSMutableArray alloc] init];
if(fromNode.position.x == toNode.position.x && fromNode.position.y == toNode.position.y){
return nil;
}
NSMutableArray *openList = [[[NSMutableArray alloc] init] autorelease];
NSMutableArray *closedList = [[[NSMutableArray alloc] init] autorelease];
AStarPathNode *currentNode = nil;
AStarPathNode *aNode = nil;
AStarPathNode *startNode = [AStarPathNode createWithAStarNode:fromNode];
AStarPathNode *endNode = [AStarPathNode createWithAStarNode:toNode];
[openList addObject:startNode];
while(openList.count > 0){
currentNode = [AStarPathNode lowestCostNodeInArray:openList];
if( currentNode.node.position.x == endNode.node.position.x &&
currentNode.node.position.y == endNode.node.position.y){
//Path Found!
aNode = currentNode;
while(aNode.previous != nil){
//Mark path
[foundPath addObject:[NSValue valueWithCGPoint: CGPointMake(aNode.node.position.x, aNode.node.position.y)]];
aNode = aNode.previous;
}
[foundPath addObject:[NSValue valueWithCGPoint: CGPointMake(aNode.node.position.x, aNode.node.position.y)]];
return foundPath;
}else{
//Still searching
[closedList addObject:currentNode];
[openList removeObject:currentNode];
for(int i=0; i<currentNode.node.neighbors.count; i++){
AStarPathNode *aNode = [AStarPathNode createWithAStarNode:[currentNode.node.neighbors objectAtIndex:i]];
aNode.cost = currentNode.cost + [currentNode.node costToNode:aNode.node] + [aNode.node costToNode:endNode.node];
aNode.previous = currentNode;
if(aNode.node.active && ![AStarPathNode isPathNode:aNode inList:openList] && ![AStarPathNode isPathNode:aNode inList:closedList]){
[openList addObject:aNode];
}
}
}
}
//No Path Found
return nil;
}
@end
/* Ch7_GridPathfinding */
@implementation Ch7_GridPathfinding
-(CCLayer*) runRecipe {
//Initial variables
gridSize = ccp(25,15);
nodeSpace = 16.0f;
touchedNode = ccp(0,0);
startCoord = ccp(2,2);
endCoord = ccp(gridSize.x-3, gridSize.y-3);
foundPath = [[NSMutableArray alloc] init];
//Create 2D array (grid)
grid = [[NSMutableArray alloc] initWithCapacity:((int)gridSize.x)];
for(int x=0; x<gridSize.x; x++){
[grid addObject:[[NSMutableArray alloc] initWithCapacity:((int)gridSize.y)]];
}
//Create AStar nodes and place them in the grid
for(int x=0; x<gridSize.x; x++){
for(int y=0; y<gridSize.y; y++){
//Add a node
AStarNode *node = [[AStarNode alloc] init];
node.position = ccp(x*nodeSpace + nodeSpace/2, y*nodeSpace + nodeSpace/2);
[[grid objectAtIndex:x] addObject:node];
}
}
//Add neighbor nodes
for(int x=0; x<gridSize.x; x++){
for(int y=0; y<gridSize.y; y++){
//Add a node
AStarNode *node = [[grid objectAtIndex:x] objectAtIndex:y];
//Add self as neighbor to neighboring nodes
[self addNeighbor:node toGridNodeX:x-1 Y:y-1]; //Top-Left
[self addNeighbor:node toGridNodeX:x-1 Y:y]; //Left
[self addNeighbor:node toGridNodeX:x-1 Y:y+1]; //Bottom-Left
[self addNeighbor:node toGridNodeX:x Y:y-1]; //Top
[self addNeighbor:node toGridNodeX:x Y:y+1]; //Bottom
[self addNeighbor:node toGridNodeX:x+1 Y:y-1]; //Top-Right
[self addNeighbor:node toGridNodeX:x+1 Y:y]; //Right
[self addNeighbor:node toGridNodeX:x+1 Y:y+1]; //Bottom-Right
}
}
/* CODE OMITTED */
return self;
}
/* Find a path from the startNode to the endNode */
-(void) findPath:(id)sender {
AStarNode *startNode = [[grid objectAtIndex:(int)startCoord.x] objectAtIndex:(int)startCoord.y];
AStarNode *endNode = [[grid objectAtIndex:(int)endCoord.x] objectAtIndex:endCoord.y];
if(foundPath){
[foundPath removeAllObjects];
[foundPath release];
}
foundPath = nil;
//Run the pathfinding algorithm
foundPath = [AStarPathNode findPathFrom:startNode to:endNode];
if(!foundPath){
[self showMessage:@"No Path Found"];
}else{
[self showMessage:@"Found Path"];
}
}
/* Helper method for adding neighbor nodes */
-(void) addNeighbor:(AStarNode*)node toGridNodeX:(int)x Y:(int)y {
if(x >= 0 && y >= 0 && x < gridSize.x && y < gridSize.y){
AStarNode *neighbor = [[grid objectAtIndex:x] objectAtIndex:y];
[node.neighbors addObject:neighbor];
}
}
@end

它是如何工作的...

A* 算法使用启发式方法在节点图上执行最佳优先搜索。首先,我们必须创建这个节点图。

  • AStarNode:

    节点图由一组节点组成,每个节点代表一个现实世界的位置。这被封装在 AStarNode 类中。

  • 存储节点:

    对于这个菜谱,我们将我们的节点存储在一个嵌套的 2D NSArray 结构中。这不是 A* 算法所必需的,而仅仅是一个用于存储节点的约定。有了这个结构,我们可以快速识别到点的最近 AStarNode。我们还可以以简单、逻辑的方式将节点相互连接。

  • 连接节点:

    通过每个节点维护其相邻节点列表的方式将节点相互连接。在这个网格设置中,每个节点以八个不同方向链接到八个其他节点。为了移除对角线移动,节点将只在四个方向上连接而不是八个。

  • 创建“墙壁”:

    在这个菜谱中,较暗色的“墙壁”代表不可导航的区域。这些节点简单地设置为 active = NO。当运行 A* 算法时,它们会被跳过。

  • AStarPathNode:

    用于查找和存储最佳路径的数据结构是一个由 AStarPathNode 对象组成的链表。此类存储一个节点、前一个节点以及从该节点到目标的总估计成本。

  • 寻找路径:

    一旦我们的节点创建并连接起来,我们就调用以下方法来寻找路径:

    +(NSMutableArray*) findPathFrom:(AStarNode*)fromNode to:(AStarNode*)toNode;
    
    

    这执行了对最佳路径的贪婪最佳优先搜索。使用的贪婪启发式是简单的“鸟飞”距离到目标的绝对值:

    /* Cost to node heuristic */
    -(float) costToNode:(AStarNode*)node {
    CGPoint src = ccp(self.position.x, self.position.y);
    CGPoint dst = ccp(node.position.x, node.position.y);
    float cost = [GameHelper distanceP1:src toP2:dst] * node.costMultiplier;
    return cost;
    }
    
    

    根据与节点图构建相关的假设以及它与移动成本的关系,可以使用其他启发式方法。例如,一个小的改进可能是使用节点到目标的斜边距离而不是绝对 2D 空间距离。如果我们禁用对角线移动,那么我们就会想使用曼哈顿启发式方法来估计网格上的移动成本。

更多内容...

AStarNode类包含一个costModifier变量。这可以用来增加对这个特定节点的相对成本。一个增加移动成本的节点可以代表像泥地或浅水这样的崎岖地形。其他 AI 概念也可以混合到路径查找算法中。例如,一个特定的区域或节点组可能比其他区域或节点组更危险。AI 演员在确定路径时必须权衡速度与危险。

在 Box2D 世界中进行 A*路径查找

A*算法的真正乐趣在于将其应用于更复杂的场景。在这个菜谱中,我们将将上一个菜谱的基于网格的技术应用于充满随机生成多边形的 Box2D 世界。

在 Box2D 世界中的 A*路径查找

准备中

请参考项目 RecipeCollection03 以获取此菜谱的完整工作代码。

如何操作...

执行以下代码:

@interface Ch7_Box2DPathfinding : GameArea2D
{
NSMutableArray *grid;
float nodeSpace; //The space between each node, increase this to increase A* efficiency at the cost of accuracy
int gridSizeX;
int gridSizeY;
}
@end
@implementation Ch7_Box2DPathfinding
-(CCLayer*) runRecipe {
//Initial variables
nodeSpace = 32.0f;
actorRadius = nodeSpace/PTM_RATIO/3;
/* CODE OMITTED */
//Remove neighbors from positive TestPoint and RayCast tests
for(int x=0; x<gridSizeX; x++){
for(int y=0; y<gridSizeY; y++){
//Add a node
AStarNode *node = [[grid objectAtIndex:x] objectAtIndex:y];
//If a node itself is colliding with an object we cut off all connections
for (b2Body* b = world->GetBodyList(); b; b = b->GetNext()){
if (b->GetUserData() != NULL) {
GameObject *obj = (GameObject*)b->GetUserData();
if(obj->polygonShape){
b2Vec2 nodePosition = b2Vec2(node.position.x/PTM_RATIO, node.position.y/PTM_RATIO);
//Test this node point against this polygon
if(obj->polygonShape->TestPoint(b->GetTransform(), nodePosition)){
for(int i=0; i<node.neighbors.count; i++){
//Remove connections
AStarNode *neighbor = [node.neighbors objectAtIndex:i];
[node.neighbors removeObject:neighbor];
[neighbor.neighbors removeObject:node];
}
}
}
}
}
//Test all node to neighbor connections using a RayCast test
for(int i=0; i<node.neighbors.count; i++){
AStarNode *neighbor = [node.neighbors objectAtIndex:i];
//Do a RayCast from the node to the neighbor.
//If there is something in the way, remove the link
b2Vec2 nodeP = b2Vec2(node.position.x/PTM_RATIO, node.position.y/PTM_RATIO);
b2Vec2 neighborP = b2Vec2(neighbor.position.x/PTM_RATIO, neighbor.position.y/PTM_RATIO);
//Do 4 tests (based on actor size)
for(float x = -actorRadius; x <= actorRadius; x+= actorRadius*2){
for(float y = -actorRadius; y <= actorRadius; y+= actorRadius*2){
RayCastAnyCallback callback;
world->RayCast(&callback, b2Vec2(nodeP.x+x,nodeP.y+y), b2Vec2(neighborP.x+x,neighborP.y+y));
if(callback.m_hit){
//Remove connections
[node.neighbors removeObject:neighbor];
[neighbor.neighbors removeObject:node];
break; break;
}
}
}
}
}
}
return self;
}
/* Find a path and add it (as a set of waypoints) when we tap the screen */
-(void) tapWithPoint:(CGPoint)point {
//Convert touch coordinate to physical coordinate
CGPoint endPoint = [self convertTouchCoord:point];
if(endPoint.x < 0 || endPoint.y < 0 || endPoint.x >= gameAreaSize.x*PTM_RATIO || endPoint.y >= gameAreaSize.y*PTM_RATIO){
return;
}
//Actor position
CGPoint actorPosition = ccp(actor.body->GetPosition().x*PTM_RATIO, actor.body->GetPosition().y*PTM_RATIO);
//We use the last waypoint position if applicable
if(actor.waypoints.count > 0){
actorPosition = [[actor.waypoints objectAtIndex:actor.waypoints.count-1] position];
}
//Starting node
AStarNode *startNode = [[grid objectAtIndex:(int)(actorPosition.x/nodeSpace)] objectAtIndex:(int)(actorPosition.y/nodeSpace)];
//Make sure the start node is actually properly connected
if(startNode.neighbors.count == 0){
bool found = NO; float n = 1;
while(!found){
//Search the nodes around this point for a properly connected starting node
for(float x = -n; x<= n; x+= n){
for(float y = -n; y<= n; y+= n){
if(x == 0 && y == 0){ continue; }
float xIndex = ((int)(actorPosition.x/nodeSpace))+x;
float yIndex = ((int)(actorPosition.y/nodeSpace))+y;
if(xIndex >= 0 && yIndex >= 0 && xIndex < gridSizeX && yIndex < gridSizeY){
AStarNode *node = [[grid objectAtIndex:xIndex] objectAtIndex:yIndex];
if(node.neighbors.count > 0){
startNode = node;
found = YES;
break; break;
}
}
}
}
n += 1;
}
}
//End node
AStarNode *endNode = [[grid objectAtIndex:(int)(endPoint.x/nodeSpace)] objectAtIndex:(int)(endPoint.y/nodeSpace)];
//Run the pathfinding algorithm
NSMutableArray *foundPath = [AStarPathNode findPathFrom:startNode to:endNode];
if(!foundPath){
[self showMessage:@"No Path Found"];
}else{
[self showMessage:@"Found Path"];
//Add found path as a waypoint set to the actor
for(int i=foundPath.count-1; i>=0; i--){
CGPoint pathPoint = [[foundPath objectAtIndex:i] CGPointValue];
[actor addWaypoint:[GameWaypoint createWithPosition:pathPoint withSpeedMod:1.0f]];
}
}
}
@end

它是如何工作的...

就像在上一个菜谱中一样,我们首先为我们的AStarNode对象创建一个 2D 嵌套NSArray容器。在将所有节点连接起来之后,我们需要调整图,以准确反映 Box2D 世界中的 2D 几何形状。

  • 删除相邻节点:

    为了正确表示 2D 世界几何形状,我们需要删除指向这些静态固定装置的节点图的边缘。为此,我们首先找到所有位于形状内部的节点。这涉及到使用以下方法:

    obj->polygonShape->TestPoint(b->GetTransform(), nodePosition);
    
    
  • 这将返回该点是否位于形状内。如果是,我们切断与此节点的所有连接。除了这个之外,我们还对每个相邻连接进行射线投射测试:

    world->RayCast(&callback, b2Vec2(nodeP.x+x,nodeP.y+y), b2Vec2(neighborP.x+x,neighborP.y+y));
    
    
  • 每个连接都会进行四次测试。我们这样做是为了近似演员的圆形形状。如果这个射线投射击中了一个固定装置,我们会移除这个连接。

还有更多...

这种技术在小型关卡中效果很好。然而,为大型关卡生成节点图可能是一个非常耗时的过程。

  • 加快加载时间:

    为了减少地图加载时间,相邻节点的删除应由关卡编辑器完成,并且相邻连接应与节点和几何形状一起存储在地图文件中。我们在应用程序的运行时这样做,以给你一个过程的概念,而不必创建一个实现此技术的 Cocos2d 关卡编辑器。

在 TMX 瓦片地图上的 A*路径查找

如果你已经跳到了第八章,技巧、工具和端口,你会看到一个菜谱,展示如何使用 Tiled 应用程序与 TMX 瓦片工具集。在这个菜谱中,我们创建了一个 2.5D 冒险游戏。为了看到我们的基于网格的路径查找技术在行动中的效果,我们重载了第八章,技巧、工具和端口的菜谱,名为使用 Tiled 创建关卡

在 TMX 瓦片地图上的 A*路径查找

准备中

请参考项目 RecipeCollection03 以获取此菜谱的完整工作代码。

如何操作...

执行以下代码:

@interface Ch7_TileMapPathfinding : Ch8_TMXTilemap
{
NSMutableArray *grid;
float actorRadius;
}
@end
@implementation Ch7_TileMapPathfinding
-(CCLayer*) runRecipe {
//Shorter variable names
float mw = tileMap.mapSize.width;
float mh = tileMap.mapSize.height;
float tw = tileMap.tileSize.width;
float th = tileMap.tileSize.height;
/* CODE OMITTED */
//Create active and inactive nodes determined by the "Collidable" TMX layer
CCTMXLayer *collidableLayer = [tileMap layerNamed:@"Collidable"];
for(int x=0; x<mw; x++){
for(int y=0; y<mh; y++){
//Add a node
AStarNode *node = [[AStarNode alloc] init];
node.position = ccp(x*tw + tw/2, y*th + th/2);
if([collidableLayer tileAt:ccp(x,y)]){ node.active = NO; }
[[grid objectAtIndex:x] addObject:node];
}
}
/* CODE OMITTED */
return self;
}
@end

它是如何工作的...

在这个菜谱中,我们可以看到我们的算法通过一些漂亮的动画 AI 角色动作变得生动起来。我们只需将nodeSpace替换为tileMap.tileSize.width/height,并将gridSizeX/Y替换为tileMap.mapSize.width/height。现在我们的角色可以在森林和其他障碍物周围移动。

在横版游戏中进行 A*路径查找

A算法是一种通用的节点图遍历程序,可以应用于许多抽象问题。在 2D 横版游戏中,空间以复杂、非线性的方式遍历。期望演员在平台上奔跑并从平台跳到平台。通过一些额外的数学计算,我们可以将我们的 A技术定制为解决这个问题。

横版游戏中的 A*路径查找

准备工作

下面的代码为了简洁而进行了大量编辑。请参考项目RecipeCollection03以获取此菜谱的完整工作代码。

如何做到这一点...

执行以下代码:

/* SSAStarNode */
@implementation SSAStarNode
-(float) costToNeighbor:(SSNeighborNode*)nn {
SSAStarNode *node = nn.node;
//Here we use jumping/running to determine cost. We could also possibly use a heuristic.
CGPoint src = ccp(self.position.x/PTM_RATIO, self.position.y/PTM_RATIO);
CGPoint dst = ccp(node.position.x/PTM_RATIO, node.position.y/PTM_RATIO);
float cost;
if(node.body == self.body){
//Compute simple distance
float runTime = ([GameHelper distanceP1:src toP2:dst]) / actor.runSpeed;
cost = runTime * node.costMultiplier;
}else{
//Compute a jump
float y = dst.y - src.y;
if(y == 0){ y = 0.00001f; } //Prevent divide by zero
CGPoint launchVector = nn.launchVector;
float gravity = actor.body->GetWorld()->GetGravity().y;
Vector3D *at = [GameHelper quadraticA:gravity*0.5f B:launchVector.y C:y*-1];
float airTime;
if(at.x > at.y){
airTime = at.x;
}else{
airTime = at.y;
}
cost = airTime * node.costMultiplier;
}
return cost;
}
@end
/* SSGameActor */
@implementation SSGameActor
+(Vector3D*) canJumpFrom:(CGPoint)src to:(CGPoint)dst radius:(float)radius world:(b2World*)world maxSpeed:(CGPoint)maxSpeed {
float x = dst.x - src.x;
float y = dst.y - src.y;
if(y == 0){ y = 0.00001f; } //Prevent divide by zero
bool foundJumpSolution = NO;
bool triedAngles = NO;
CGPoint launchVector;
float jumpHeightMod = 0.5f;
while(!triedAngles){
//Gravity
float gravity = world->GetGravity().y;
if(gravity == 0){ gravity = 0.00001f; } //Prevent divide by zero
launchVector = [SSGameActor getLaunchVector:CGPointMake(x,y) jumpHeightMod:jumpHeightMod gravity:gravity];
bool hitObject = NO;
bool movingTooFast = NO;
/* Make sure jump doesn't hit an object */
Vector3D *at = [GameHelper quadraticA:gravity*0.5f B:launchVector.y C:y*-1];
float airTime;
if(at.x > at.y){ airTime = at.x; }else{ airTime = at.y; }
//Do a ray test sequence (from 0.1 to 0.9 of airTime)
for(float t=airTime/10; t<airTime-airTime/10; t+= airTime/10){
if(hitObject){ break; }
float t1 = t + airTime/10;
float x1 = launchVector.x * t + src.x;
float y1 = launchVector.y * t + (0.5f) * gravity * pow(t,2) + src.y;
float x2 = launchVector.x * t1 + src.x;
float y2 = launchVector.y * t1 + (0.5f) * gravity * pow(t1,2) + src.y;
//Point Test
/* CODE OMITTED */
//RayCast Test
/* CODE OMITTED */
}
//Make sure the launchVector is not too fast for this actor
if(!hitObject){
if([GameHelper absoluteValue:launchVector.x] > maxSpeed.x || [GameHelper absoluteValue:launchVector.y] > maxSpeed.y){
movingTooFast = YES;
}
}
if(hitObject || movingTooFast){
//This jump failed, try another
if(jumpHeightMod <= 0.5f && jumpHeightMod >= 0.2f){ //First, try 0.5f to 0.1f
jumpHeightMod -= 0.1f;
}else if(jumpHeightMod > 0.5f && jumpHeightMod < 1.0f){ //Then try 0.6f to 1.0f
jumpHeightMod += 0.1f;
}else if(jumpHeightMod < 0.2f){
jumpHeightMod = 0.6f;
}else if(jumpHeightMod >= 1.0f){
//FAIL
triedAngles = YES;
}
}else{
//SUCCESS
foundJumpSolution = YES;
triedAngles = YES;
}
}
if(foundJumpSolution){
return [Vector3D x:launchVector.x y:launchVector.y z:0];
}else{
return nil;
}
}
+(CGPoint) getLaunchVector:(CGPoint)vect jumpHeightMod:(float)jumpHeightMod gravity:(float)gravity {
//Gravity
if(gravity == 0){ gravity = 0.00001f; } //Prevent divide by zero
//The angle between the points
float directionAngle = [GameHelper vectorToRadians:ccp(vect.x, vect.y)];
//Jump height is a percentage of X distance, usually 0.5f
float apexX;
if(vect.y > 0){ apexX = vect.x - (vect.x*0.5f*pow([GameHelper absoluteValue:sinf(directionAngle)],0.5f/jumpHeightMod));
}else{ apexX = vect.x*0.5f*pow([GameHelper absoluteValue:sinf(directionAngle)],0.5f/jumpHeightMod); }
float apexY;
if(vect.y > 0){ apexY = vect.y + [GameHelper absoluteValue:vect.x*jumpHeightMod]*[GameHelper absoluteValue:sinf(directionAngle)];
}else{ apexY = [GameHelper absoluteValue:vect.x*jumpHeightMod]*[GameHelper absoluteValue:sinf(directionAngle)]; }
//Get launch vector
float vectY = sqrtf(2*(-1)*gravity*apexY);
float vectX = (apexX*(-1)*gravity) / vectY;
return CGPointMake(vectX, vectY);
}
@end
/* Ch7_SideScrollingPathfinding */
@implementation Ch7_SideScrollingPathfinding
-(CCLayer*) runRecipe {
/* CODE OMITTED */
//Distance between nodes that the actor can run between
float nodeRunDistInterval = 100.0f;
//How far to search for nodes the actor can jump to
float maxJumpSearchDist = 500.0f;
//Add some nodes to the bottom of the level
for(float x=20.0f; x<=gameAreaSize.x*PTM_RATIO-20.0f; x+=nodeRunDistInterval){
//Add node
/* CODE OMITTED */
}
//Link those nodes together as 'run neighbors'
for(int i=0; i<nodes.count-1; i++){
SSAStarNode *n1 = (SSAStarNode*)[nodes objectAtIndex:i];
SSAStarNode *n2 = (SSAStarNode*)[nodes objectAtIndex:i+1];
[self linkRunNeighbor:n1 with:n2];
}
/* Add nodes to all level platforms */
for(b2Body *b = world->GetBodyList(); b; b = b->GetNext()){
if (b->GetUserData() != NULL) {
GameObject *obj = (GameObject*)b->GetUserData();
if(obj.tag == GO_TAG_WALL && obj->polygonShape){
//Nodes on this body only
NSMutableArray *nodesThisBody = [[[NSMutableArray alloc] init] autorelease];
//Process each polygon vertex
for(int i=0; i<obj->polygonShape->m_vertexCount; i++){
b2Vec2 vertex = obj->polygonShape->m_vertices[i];
//All nodes are 1 unit above their corresponding platform
b2Vec2 nodePosition = b2Vec2(vertex.x + b->GetPosition().x,vertex.y + b->GetPosition().y+1.0f);
//Move nodes inward to lessen chance of missing a jump
if(obj->polygonShape->m_centroid.x < vertex.x){
nodePosition = b2Vec2(nodePosition.x-0.5f, nodePosition.y);
}else{
nodePosition = b2Vec2(nodePosition.x+0.5f, nodePosition.y);
}
//If this node position is not inside the polygon we create an SSAStarNode
if(!obj->polygonShape->TestPoint(b->GetTransform(), nodePosition)){
//Add node
/* CODE OMITTED */
}
}
//Add in-between nodes (for running)
bool done = NO;
while(!done){
if(nodesThisBody.count == 0){ break; }
done = YES;
for(int i=0; i<nodesThisBody.count-1; i++){
SSAStarNode *n1 = (SSAStarNode*)[nodesThisBody objectAtIndex:i];
SSAStarNode *n2 = (SSAStarNode*)[nodesThisBody objectAtIndex:i+1];
if([GameHelper absoluteValue:n1.position.y-n2.position.y] > 0.1f){
//These are not side by side
continue;
}
if( [GameHelper distanceP1:n1.position toP2:n2.position] > nodeRunDistInterval ){
CGPoint midPoint = [GameHelper midPointP1:n1.position p2:n2.position];
b2Vec2 mp = b2Vec2(midPoint.x/PTM_RATIO, midPoint.y/PTM_RATIO);
//If node is not in the polygon, add it
if(!obj->polygonShape->TestPoint(b->GetTransform(), mp)){
//Add node
/* CODE OMITTED */
break;
}
}
}
}
//Link all of the neighboring nodes on this body
for(int i=0; i<nodesThisBody.count-1; i++){
if(nodesThisBody.count == 0){ break; }
SSAStarNode *n1 = (SSAStarNode*)[nodesThisBody objectAtIndex:i];
SSAStarNode *n2 = (SSAStarNode*)[nodesThisBody objectAtIndex:i+1];
if([GameHelper absoluteValue:n1.position.y-n2.position.y] > 0.1f){
//These are not side by side
continue;
}
//Two-way link
[self linkRunNeighbor:n1 with:n2];
}
}
}
}
//Neighbor all other nodes (for jumping)
for(int i=0; i<nodes.count; i++){
for(int j=0; j<nodes.count; j++){
if(i==j){ continue; }
SSAStarNode *n1 = (SSAStarNode*)[nodes objectAtIndex:i];
SSAStarNode *n2 = (SSAStarNode*)[nodes objectAtIndex:j];
if(n1.body == n2.body){ continue; }
if( [GameHelper distanceP1:n1.position toP2:n2.position] <= maxJumpSearchDist ){
CGPoint src = ccp(n1.position.x/PTM_RATIO, n1.position.y/PTM_RATIO);
CGPoint dst = ccp(n2.position.x/PTM_RATIO, n2.position.y/PTM_RATIO);
//Calculate our jump "launch" vector
Vector3D *launchVector3D = [SSGameActor canJumpFrom:src to:dst radius:actor.circleShape->m_radius*1.5f world:world maxSpeed:actor.maxSpeed];
if(launchVector3D){
//Only neighbor up if a jump can be made
//1-way link
if(![n1 containsNeighborForNode:n2]){
//Add neighbor
/* CODE OMITTED */
}
}
}
}
}
return self;
}
@end

它是如何工作的...

首先,我们必须创建我们的节点图。在俯视的 2D 环境中,我们只需使用网格,然后剔除任何碰撞的边。在横版环境中,我们需要有不同的思考方式。在这样的环境中,演员必须始终站在关卡几何形状的顶部。除此之外,他们还有两种不同的方式穿越关卡:奔跑跳跃

  • 在平台上奔跑:

    为了让演员能够在平台上奔跑,我们需要在每个平台上创建一串 A*节点。从这些节点中的一个开始,演员可以轻松地移动到同一身体上的其他节点。

  • 从平台跳到平台:

    要到达另一个平台,演员必须跳到那里。这增加了复杂性。我们需要执行将演员安全地发射到另一个平台所需的计算。我们还需要检查演员跳跃轨迹中的几何形状,并根据需要调整跳跃的角度。在我们可以实现这个功能之前,我们需要将其封装在我们创建的新类中,这些新类是我们当前类的子类。

  • SSAStarNode:

    SSAStarNode包含对 b2Body 对象的引用以及SSGameActor对象。当演员在这个节点上休息时,身体对象代表演员所在的身体。由于需要演员的大小、跳跃速度和奔跑速度等信息来执行一些上述计算,因此保留了演员的引用。

  • SSGameWaypoint:

    我们的新航点类包含一个moveType枚举,用于指定演员是否应该向航点的位置奔跑跳跃。它还包含一个launchVector,用于指定如果需要跳跃,所需的冲量向量。

  • SSGameActor:

    新的演员类有一个maxSpeed变量,它决定了演员可以跑多快以及可以跳多高。此类还封装了我们新修改的processWaypoints方法以及其他几个方法:

    -(void) runToWaypoint:(SSGameWaypoint*)wp speedMod:(float)speedMod constrain:(bool)constrain;
    -(void) jumpToWaypoint:(SSGameWaypoint*)wp;
    +(Vector3D*) canJumpFrom:(CGPoint)src to:(CGPoint)dst radius:(float)radius world:(b2World*)world maxSpeed:(CGPoint)maxSpeed;
    +(CGPoint) getLaunchVector:(CGPoint)vect jumpHeightMod:(float)jumpHeightMod gravity:(float)gravity;
    
    

    runToWaypoint方法简单地设置演员的速度,使其向左或向右跑。jumpToWaypoint方法使用航点的launchVector发射演员。canJumpFrom方法确定是否可以跳到某个特定点。这涉及到对 Box2D 世界的几何形状进行多次跳跃角度测试。每个跳跃抛物线被分成 10 个直线段,这些直线段被射向地图几何形状进行测试。这足以满足我们的碰撞检测需求。最后,getLaunchVector方法,它被canJumpFrom方法使用,根据着陆点和相对于 X 跳跃宽度的 Y 跳跃高度确定演员的launchVector

  • SSAStarPathNode:

    这个类与AStarPathNode类似。它包含一个SSGameWaypoint指针以方便使用。在更新的findPathFrom方法中,我们创建这个航点并设置其launchVector。这确保了launchVector只在加载时计算。

  • SSNeighborNode:

    由于我们现在有两种从节点到节点移动的方法,我们需要一种更复杂的方式来存储图边信息。而不是简单地存储指向相邻节点的指针,这个类封装了该节点以及moveTypecostlaunchVector

  • 创建我们的节点图—运行节点:

    首先,我们必须创建我们的关键节点。这些就像动画中的关键帧。它们位于每个平台顶部的每个顶点略微上方。然后我们在这些节点之间添加节点。最后,这些“运行节点”通过设置moveType变量为MOVE_TYPE_RUN相互连接。

  • 创建我们的节点图—跳跃节点:

    一旦在每个平台上设置了运行节点,我们就创建跳跃节点。这涉及到在每个节点周围搜索,确定演员是否可以从该节点跳到找到的节点,然后最终创建相邻链接。我们逐个创建这些链接,因为向上跳跃到达节点与向下跳跃非常不同。

  • 调整侧滚路径查找:

    这种技术需要大量调整才能正常工作。例如,用于确定是否到达航点的 X 和 Y 距离阈值差异很大。另一个调整涉及到,如果航点被阻挡,整个航点集必须被丢弃。也许另一个版本可以通过在错过节点前添加新路径来挽救节点路径的第二部分。

运行 Lua 脚本

许多商业游戏都使用一种脚本语言来隔离和抽象它们的游戏逻辑。其中最受欢迎的是Lua。在本教程中,我们将 Lua 集成到我们的项目中。

运行 Lua 脚本

准备工作

请参阅项目RecipeCollection03以获取此教程的完整工作代码。

如何操作...

可以通过几个简单的步骤将 Lua 添加到您的项目中:

  1. 在导航器中突出显示你的项目。在窗口中间的底部,点击 Add Target:如何操作...

  2. 将目标命名为 "Lua"。这将在你的项目文件夹中创建一个新文件夹,紧挨着你的主要目标文件夹。它还应创建一个新的组。

  3. www.lua.org 下载 Lua 源代码并将其复制到该文件夹。

  4. 右键点击 Lua 组并选择 Add Files to "Your如何操作...

  5. 导航到 src 目录并添加该目录下所有文件,除了 lua.c, luac.c, Makefileprint.c。同时,确保取消选中 Copy items into destination groups folder 并在 Add to Targets 部分只选择 Lua

  6. 到目前为止,你应该能够无错误地构建 Lua 目标。

  7. 在中间面板中,点击你的项目的主要目标。展开 Link Binary With Libraries。点击左下角的 + 符号并将 libLua.a 库添加到列表中:如何操作...

  8. 收起此内容并展开 Target Dependencies。将目标 Lua 添加为此目标的依赖项:如何操作...

  9. 就这样。Lua 现已集成。清理并构建你的项目以确保其正确集成。

执行以下代码:

#import "mcLua.hpp"
@interface Ch7_LuaScripting : Recipe
{
class mcLuaManager * lua_;
mcLuaScript * sc;
}
@end
//Callback pointer
Ch7_LuaScripting *lsRecipe = nil;
//Static append message C function
static int lsAppendMessage(lua_State * l)
{
//Pass lua string into append message method
[lsRecipe appendMessage:[NSString stringWithUTF8String:lua_tostring(l,1)]];
return 0;
}
@implementation Ch7_LuaScripting
-(CCLayer*) runRecipe {
//Set callback pointer
lsRecipe = self;
//Lua initialization
lua_ = new mcLuaManager;
//Lua function wrapper library
static const luaL_reg scriptLib[] =
{
{"appendMessage", lsAppendMessage },
{NULL, NULL}
};
lua_->LuaOpenLibrary("scene",scriptLib);
//Open Lua script
sc = lua_->CreateScript();
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"show_messages.lua" ofType:@""];
sc->LoadFile([filePath UTF8String]);
//Set initial update method counter
lua_->Update(0);
//Schedule step method
[self schedule: @selector(step:)];
//Resume button
CCMenuItemFont *resumeItem = [CCMenuItemFont itemFromString:@"Resume Script" target:self selector:@selector(resumeScript:)];
CCMenu *menu = [CCMenu menuWithItems:resumeItem, nil];
[self addChild:menu];
return self;
}
-(void) step:(ccTime)delta {
//Update Lua script runner
lua_->Update(delta);
}
/* Resume script callback */
-(void) resumeScript:(id)sender {
sc->YieldResume();
}
@end

它是如何工作的...

这些和随后的食谱使用 Robert Grzesek 的 mcLua API 来简化 Lua 脚本的加载和执行。它还允许多个脚本的并发执行。

  • mcLuaManager 类:

    mcLuaManager 类是管理所有运行脚本的顶级类:

    class mcLuaManager lua_ = new mcLuaManager;
    
    

    它负责运行脚本以及它们的创建和销毁。

  • 加载并启动脚本:

    加载 Lua 脚本是一个相当直接的过程:

    mcLuaScript *sc = lua_->CreateScript();
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"show_messages.lua" ofType:@""];
    sc->LoadFile([filePath UTF8String]);
    
    

    一旦文件加载,我们调用 mcLuaManager 类的 Update 方法:

    lua_->Update(0);
    
    

    这将启动已加载的脚本并运行。

  • 静态函数库:

    在 Lua 脚本中,可以根据分配给对象标识符的函数库调用全局方法。mcLua API 默认将一些函数分配给脚本对象。这些包括 waitSeconds, waitFramespause。它们可以在 Lua 脚本内部调用:

    script.waitSeconds(1);
    
    

    在这个例子中,我们还创建了一个名为 appendMessage 的回调方法并将其分配给名为 scene 的对象。为此,我们首先创建一个静态 C 函数:

    static int lsAppendMessage(lua_State * l)
    {
    [lsRecipe appendMessage:[NSString stringWithUTF8String:lua_tostring(l,1)]];
    return 0;
    }
    
    

    当食谱加载时,我们创建一个 Lua 脚本库链接器数组并将其使用 mcLua API 提供的 LuaOpenLibrary 加载到内存中:

    lua_ = new mcLuaManager;
    static const luaL_reg scriptLib[] =
    {
    {"appendMessage", lsAppendMessage },
    {NULL, NULL}
    };
    lua_->LuaOpenLibrary("scene", scriptLib);
    
    

    一旦加载,我们就可以从 Lua 脚本本身调用该函数:

    scene.appendMessage("This is a Lua script.");
    
    

    最后,如果脚本被暂停(从脚本内部或外部)它可以从外部脚本恢复:

    sc->YieldResume();
    
    

    一起,这个库允许你的 Lua 脚本与你的应用程序交互。

动态加载 Lua 脚本

类似 Lua 这样的脚本语言的强大之处在于,脚本可以在 运行时 加载和重新加载。这意味着您可以在不重新编译 Objective-C++ 代码的情况下测试您的游戏逻辑。在这个菜谱中,我们将从本地 Web 服务器 加载远程脚本。

动态加载 Lua 脚本

准备工作

请参考项目 RecipeCollection03 以获取此菜谱的完整工作代码。

如何操作...

执行以下代码:

#import "mcLua.hpp"
#import "Reachability.h"
@interface Ch7_DynamicScriptLoading : Recipe
{
class mcLuaManager * lua_;
}
@end
//Callback pointer
Ch7_DynamicScriptLoading *dslRecipe = nil;
//Static append message C function
static int dslAppendMessage(lua_State * l)
{
//Pass lua string into append message method
[dslRecipe appendMessage:[NSString stringWithUTF8String:lua_tostring(l,1)]];
return 0;
}
@implementation Ch7_DynamicScriptLoading
-(CCLayer*) runRecipe {
//Superclass initialization
[super runRecipe];
//Set callback pointer
dslRecipe = self;
//Lua initialization
lua_ = new mcLuaManager;
//Lua function wrapper library
static const luaL_reg scriptLib[] =
{
{"appendMessage", dslAppendMessage },
{NULL, NULL}
};
lua_->LuaOpenLibrary("scene",scriptLib);
//Load Lua script
[self loadScript];
//Set initial update method counter
lua_->Update(0);
//Schedule step method
[self schedule: @selector(step:)];
//Reload script button
CCMenuItemFont *reloadItem = [CCMenuItemFont itemFromString:@"Reload Script" target:self selector:@selector(loadScript)];
CCMenu *menu = [CCMenu menuWithItems:reloadItem, nil];
[self addChild:menu];
return self;
}
-(void) step:(ccTime)delta {
//Update Lua script runner
lua_->Update(delta);
}
-(void) loadScript{
//Reset message
[self resetMessage];
//Make sure localhost is reachable
Reachability* reachability = [Reachability reachabilityWithHostName:@"localhost"];
NetworkStatus remoteHostStatus = [reachability currentReachabilityStatus];
if(remoteHostStatus == NotReachable) {
[self showMessage:@"Script not reachable."];
}else{
[self appendMessage:@"Loading script from http://localhost/ch7_remote_script.lua"];
//Load script via NSURL
mcLuaScript *sc = lua_->CreateScript();
NSString *remoteScriptString = [NSString stringWithContentsOfURL:[NSURL URLWithString:@"http://localhost/ch7_remote_script.lua"]
encoding:NSUTF8StringEncoding error:nil];
sc->LoadString([remoteScriptString UTF8String]);
}
}
@end

它是如何工作的...

加载远程脚本最简单的方法是使用本地 Web 服务器。这样我们就可以绕过 NSBundle 和相对受限的 iOS 文件系统。Mac OSX 预装了内置的 Apache HTTP Web 服务器。可以通过转到 系统偏好设置 | 互联网和无线 | 共享 | Web 共享:来启用和配置它。

  • 加载远程脚本:

    我们不是从文件系统中读取我们的脚本,而是使用 NSURL 通过 HTTP 加载它:

    mcLuaScript *sc = lua_->CreateScript();
    NSString *remoteScriptString = [NSString stringWithContentsOfURL:[NSURL URLWithString:@"http://localhost/ch7_remote_script.lua"] encoding:NSUTF8StringEncoding error:nil];
    sc->LoadString([remoteScriptString UTF8String]);
    
    

    在您的本地 Web 服务器上编辑文件并点击重新加载按钮只是重新加载脚本。这是一种简单但有效的方法,可以快速开发和测试游戏逻辑。

  • Reachability:

    在这个例子中,我们使用苹果的 Reachability 库来帮助我们确定是否可以通过网络访问脚本。没有这个工具,我们的 stringWithContentsOfURL 方法会抛出错误。

使用 Lua 创建对话树

Lua 允许程序员编写针对通用 接口 的代码,并稍后关注 实现。这种将游戏逻辑与游戏音频/视觉元素展示的细节分开,是任何游戏引擎的重要部分。在这个菜谱中,我们将使用 Lua 创建一个小型的基于故事的游戏。

使用 Lua 创建对话树

准备工作

请参考项目 RecipeCollection03 以获取此菜谱的完整工作代码。

如何操作...

执行以下代码:

//Static C functions
static int ldtLogic(lua_State * l) {
int num = [ldtRecipe logic:[NSString stringWithUTF8String:lua_tostring(l,1)]];
lua_pushnumber(l,num);
return 1;
}
static int ldtPresentOptions(lua_State * l) {
[ldtRecipe presentOptions];
ldtRecipe.sc->YieldPause();
return (lua_yield(l, 0));
}
@implementation Ch7_LuaDecisionTree
/* Logic callback */
-(int) logic:(NSString*)str {
int num = 0;
if([str isEqualToString:@"Put guns down"]){
gunsDown = YES;
}else if([str isEqualToString:@"Are guns down?"]){
if(gunsDown){
num = 1;
}else{
num = 0;
}
}else if([str isEqualToString:@"You win"]){
[self showMessage:@"You WIN!!"];
}
return num;
}
/* Present options callback */
-(void) presentOptions {
text = @"";
[textLabel setString:text];
optionsNode.visible = YES;
}
/* Select option callback */
-(void) selectOption:(id)sender {
/* CODE OMITTED */
//Resume the script
sc->YieldResume();
}
@end
/* decision_tree.lua */
function start()
scene.desc("You are deep undercover with the mafia.");
scene.anim("Open door");
scene.anim("Enter officer");
scene.anim("Louie looks away");
scene.dialog("Officer: Alright Big Louie. This is a raid. You're under arrest for the murder of Frankie Boy Caruso.");
scene.anim("Pull guns");
scene.dialog("Big Louie: Murder? What's a murder?");
scene.dialog("Officer: Don't play dumb with me.");
scene.dialog("Big Louie: A one-man raid? You must have a death wish.");
scene.anim("Louie looks at you");
scene.dialog("Big Louie: What do YOU think we should do with him?");
scene.dialogOption("You can't take him out now. There are too many witnesses.");
scene.dialogOption("I say take him out. He's here alone.");
scene.dialogOption("He's a cop. We'll have a mess on our hands if we take him down.");
scene.presentOptions();
if scene.getResponse() == 1 then
tooManyWitnesses();
elseif scene.getResponse() == 2 then
hereAlone();
elseif scene.getResponse() == 3 then
bigMess();
end
end
function tooManyWitnesses()
scene.dialog("Big Louie: Whaddya mean too many witnesses? These are all my men...");
scene.anim("Louie scowls");
scene.dialog("Big Louie: ...and YOU! Blast him boys.");
script.waitSeconds(1);
scene.anim("Gun pointed at you");
scene.desc("You are dead.");
end
function bigMess()
scene.dialog("Big Louie: I don't like it, but, you're right.");
scene.dialog("Big Louie: Men, you can put your guns down.");
scene.logic("Put guns down");
scene.anim("Put guns down");
scene.desc("Louie's men lower their weapons");
scene.anim("Louie looks away");
scene.dialog("Big Louie: Cop, looks like you have a new lease on life.");
scene.actionOption("Pull your gun on Big Louie");
scene.actionOption("Pull your gun on Big Louie's men");
scene.presentOptions();
if scene.getResponse() == 1 then
pullGunOnLouie();
elseif scene.getResponse() == 2 then
pullGunOnMen();
end
end
function hereAlone()
scene.anim("Officer shocked");
script.waitSeconds(1);
scene.dialog("Officer: Jerry! What the hell are you doing?!");
scene.anim("Louie scowls");
scene.dialog("Big Louie: Jerry? You lying scumbag! We trusted you...blast him boys.");
script.waitSeconds(1);
scene.anim("Gun pointed at you");
scene.desc("You are dead.");
end
function pullGunOnLouie()
scene.anim("Pull gun on Louie");
scene.anim("Louie scowl");
scene.dialog("Big Louie: This guy's a Fed! Blast him!");
script.waitSeconds(1);
scene.anim("Gun pointed at you");
scene.desc("You are dead.");
end
function pullGunOnMen()
gunsDown = scene.logic("Are guns down?");
scene.anim("Pull gun on men");
if gunsDown == 1 then
scene.anim("Louie scowls");
scene.dialog("Big Louie: You played me for a fool!");
scene.anim("Louie looks away");
scene.dialog("Officer: You're under arrest Big Louie.");
scene.dialog("Big Louie: I'll be back on the streets in twenty-four hours!");
scene.dialog("Officer: We'll try to make it twelve.");
scene.anim("Louie looks at you");
scene.logic("You win");
scene.desc("You win!");
else
scene.anim("Louie scowls");
scene.dialog("Big Louie: This guy's a Fed! Blast him!");
script.waitSeconds(1);
scene.anim("Gun pointed at you");
scene.desc("You are dead.");
end
end
start();

它是如何工作的...

对于这个菜谱,我们创建了一些回调函数来处理动画、对话框和逻辑。其中一些,如选项提示,在脚本恢复之前等待用户响应。其他函数,如 ldtLogic,有返回值。

  • 使用 Lua 传递和返回变量:

    Lua 的变量传递机制相当简单。函数传递一个 lua_State 指针。然后可以从这个指针指向的堆栈中检索数据:

    lua_State * l;
    NSString *str = [NSString stringWithUTF8String:lua_tostring(l,1)];
    
    

    Lua 支持同时返回多个变量。要从我们的回调函数中返回一个变量,我们必须首先将变量推入堆栈:

    int num = 7;
    lua_pushnumber(l,num);
    
    

    然后,我们指定要返回的变量数量作为一个整数:

    return 1;
    
    

    所有基本的 C 类型都可以使用 Lua 传递和返回。

  • 本地 Lua 函数:

    在我们的脚本文件中,我们使用了多个本地 Lua 函数。除了最初的 start() 函数调用外,所有内容都被封装在函数中。本地函数也支持返回多个

第八章.技巧、工具和端口

本章将涵盖以下主题:

  • 简介

  • 访问 Cocos2d-iPhone 测试平台

  • 使用 Zwoptex 打包纹理

  • 使用 Tiled 创建关卡

  • 使用 JSONWorldBuilder 创建关卡

  • 使用 CocosBuilder 创建场景

  • 使用 Cocos2d-X

  • 使用 Cocos3d

  • 发布您的应用

简介

在本章中,我们将通过介绍一些常用的工具来增强 Cocos2d 游戏开发来结束本章。我们还将介绍 Cocos2d 衍生项目,并指导您在苹果的 App Store 上发布应用的流程。

访问 Cocos2d-iPhone 测试平台

Cocos2d-iPhone 测试平台 是一系列示例的集合,旨在测试错误、展示功能,并通过有用的示例指导程序员。在本食谱中,我们将介绍这个非常有用的工具。

访问 Cocos2d-iPhone 测试平台

准备工作

首先,我们必须从 www.cocos2d-iphone.org/download 下载 Cocos2d 源代码。解压缩主源代码包后,双击 cocos2d.xcworkspace 文件。

如何操作...

在 XCode 的 方案选择 菜单中,您现在可以选择运行测试平台的哪个部分。这些测试包括一切——绘图、物理、声音、网络等等。

它是如何工作的...

cocos2d-ios 目标的 tests 文件夹中,您可以找到每个单独测试的源文件。在这里,您可以玩转这些演示,以获得自己游戏的灵感。

Box2D 测试平台

由于 Cocos2d-iPhone 框架的范围,许多其他测试平台都包含在 Cocos2d-iPhone 测试平台中。通过构建 Box2dTestBed 方案,我们可以运行针对 Cocos2d 定制的官方 Box2D 测试平台版本。

使用 Zwoptex 打包纹理

手动创建精灵表可以是一个繁琐的过程。为了解决这个问题,我们使用 Zwoptex 纹理打包器 将单个精灵打包到尽可能小的区域。在本食谱中,我们将详细介绍这个过程。

使用 Zwoptex 打包纹理

准备工作

首先,我们必须下载并安装 Zwoptex 应用程序。访问 zwoptexapp.com/ 并点击 下载 链接。将应用程序拖到您的 Applications 文件夹。

如何操作...

一旦启动 Zwoptex,点击 文件 | 新建。您应该看到一个空白画布。以下是一些画布的属性:

  • 画布大小:

    如您在 Zwoptex 中所见,画布的宽度和高度只能达到 2048 像素。此外,它们只能是 2 的幂。在 iPhone 3G 及更早的设备上,未压缩的纹理最大尺寸为 1024x1024。在支持 OpenGL ES 2.0 的新设备上,从 iPhone 3GS 开始,未压缩的纹理可以达到 2048x2048 像素。纹理仅在 2 的幂次下加载到内存中。考虑到所有这些因素,Zwoptex 限制了您可以使用画布的大小。

  • 导入精灵:

    精灵通过简单地从 Finder 中将单个文件拖放到画布上导入到画布中。它们将重叠在一起。它们也将用红色框勾勒出来。

  • 应用布局:

    在布局标题下调整任何设置后,我们可以点击应用按钮。这将重新排列精灵以适应画布大小。如果所有精灵都无法适应,一些精灵仍然会有红色的框包围着。这表示有重叠。

  • 发布:

    一旦我们的精灵在图纸上正确排列,并且我们的 Zwoptex 文件已保存,我们就可以发布我们的图集。这将创建一个 PLIST 文件和一个 PNG 文件。通过将这些文件添加到我们的 XCode Cocos2d 项目中,我们现在可以在我们的应用程序中使用它们。

它是如何工作的...

Zwoptex 使用偏移和其他定位技巧,旨在尽可能多地挤压精灵到一个图集中。

参见...

Zwoptex 的一个流行替代品是TexturePacker。可以从www.texturepacker.com/下载。

使用 Tiled 创建层级

游戏开发者工具箱中最重要的武器之一是层级编辑器。在这个菜谱中,我们将使用Tiled层级编辑器创建一个层级。然后我们将使用这个层级创建一个简单的俯视世界。

使用 Tiled 创建层级

准备工作

请参阅项目RecipeCollection03以获取此菜谱的完整工作代码。

如何操作...

执行以下代码:

//Interface
@interface Ch8_Tiled : GameArea2D {
CCTMXTiledMap *tileMap;
}
@end
//Implementation
@implementation Ch8_Tiled
-(CCLayer*) runRecipe {
//Load TMX tilemap file
tileMap = [CCTMXTiledMap tiledMapWithTMXFile:@"tilemap.tmx"];
//Set game area size based on tilemap size
[self setGameAreaSize];
//Superclass initialization and message
[super runRecipe];
[self showMessage:@"Use the DPad to move the actor around."];
//Add tile map
[gameNode addChild:tileMap z:0];
/* Re-order layers according to their Y value. This creates isometric depth. */
//Our layers
CCTMXLayer *collidableLayer = [tileMap layerNamed:@"Collidable"];
CCTMXLayer *ground = [tileMap layerNamed:@"Ground"];
CCTMXLayer *wall = [tileMap layerNamed:@"Wall"];
//Gather all the layers into a container
float mw = tileMap.mapSize.width; float mh = tileMap.mapSize.height;
float tw = tileMap.tileSize.width; float th = tileMap.tileSize.height;
NSMutableDictionary *layersToReorder = [[[NSMutableDictionary alloc] init] autorelease];
for( CCTMXLayer* child in [tileMap children] ) {
//Skip tiles marked "Collidable", "Ground" and "Wall"
if(child == ground){ continue; }
else if(child == wall){ continue; }
else if(child == collidableLayer){ continue; }
//Gather all the layers
for(float x=0; x<mw; x+=1){
for(float y=mh-1; y>=0; y-=1){
CCSprite *childTile = [child tileAt:ccp(x,y)];
CCSprite *collideTile = [collidableLayer tileAt:ccp(x,y)];
if(childTile && collideTile){
[layersToReorder setObject:[NSNumber numberWithFloat:y] forKey:[child layerName]];
x=mw; y=-1;
}
}
}
}
//Re-order gathered layers
for(id key in layersToReorder){
NSString *str = (NSString*)key;
[tileMap reorderChild:[tileMap layerNamed:str] z:[[layersToReorder objectForKey:key] floatValue]];
}
//Set the ground to z=0
[tileMap reorderChild:ground z:0];
//Add Box2D boxes to represent all layers marked "Collidable"
for(float x=0; x<mw; x+=1){
for(float y=0; y<mh; y+=1){
if([collidableLayer tileAt:ccp(x,y)]){
[self addBoxAtPoint:ccp(x*tw, mh*th - y*th) size:ccp(tw/2,th/2)];
}
}
}
//Remove the "Collidable" layer art as it's only an indicator for the level editor
[tileMap removeChild:collidableLayer cleanup:YES];
return self;
}
-(void) step: (ccTime) dt {
[super step:dt];
/* CODE OMITTED */
//Re-order the actor
float mh = tileMap.mapSize.height;
float th = tileMap.tileSize.height;
CGPoint p = [actor.sprite position];
float z = -(p.y/th) + mh;
[tileMap reorderChild:actor.sprite z:z ];
}
-(void) setGameAreaSize {
//Set gameAreaSize based on tileMap size
gameAreaSize = ccp((tileMap.mapSize.width * tileMap.tileSize.width)/PTM_RATIO,(tileMap.mapSize.height * tileMap.tileSize.height)/PTM_RATIO); //Box2d units
}
-(void) addActor {
//Get spawn point from tile object named "SpawnPoint"
if(!spawnPoint){
CCTMXObjectGroup *objects = [tileMap objectGroupNamed:@"Objects"];
NSAssert(objects != nil, @"'Objects' object group not found");
NSMutableDictionary *sp = [objects objectNamed:@"SpawnPoint"];
NSAssert(sp != nil, @"SpawnPoint object not found");
int x = [[sp valueForKey:@"x"] intValue];
int y = [[sp valueForKey:@"y"] intValue];
spawnPoint = [Vector3D x:x y:y z:0];
}
//Add actor
/* CODE OMITTED */
[tileMap addChild:actor.sprite z:[[tileMap layerNamed:@"0"] vertexZ]];
}
@end

它是如何工作的...

此菜谱加载了一个使用Tiled应用程序创建的 TMX tilemap。然后它使用tilemap中的信息创建一个 2.5D 游戏世界。这是按照以下步骤完成的:

  1. 安装 Tiled:

    首先,我们必须下载并安装Tiled应用程序。访问www.mapeditor.org/并点击Tiled Qt 0.7.0 for Mac OS X链接。将应用程序拖到您的Applications文件夹中。

  2. 创建新层级:

    打开 Tiled 并点击文件 | 新建以创建一个新的层级。

  3. 选择视角:

    如您从网站上的Tiled截图中所见,可以选择两种视角类型。当您第一次点击文件 | 新建Tiled菜单中,您将不得不在正交等距视角之间做出选择。每种视角都创建不同的视觉风格和世界对象布局。在我们的例子中,我们选择了正交,因为它更直接一些。

  4. 地图大小:

    地图大小以瓦片为单位测量。对于我们的地图,我们选择了 50x50 的大小。

  5. 瓦片大小:

    Tiled中,瓦片大小可以是可变的。在这个例子中,我们选择了默认大小 32x32 像素。这意味着我们可以保持我们的艺术资产小巧。

  6. 创建tileset

    Tiled 中,主要使用的资源是 tileset。这是一个根据在 Tiled 中选择的瓦片大小创建的精灵表。要创建此精灵表,打开 Zwoptex,创建一个新文件,并将 Padding 设置为 0px。然后,将 32x32 像素的图像拖放到精灵表上。此 PNG 文件是瓦片集文件。与使用相应的 PLIST 文件来管理精灵信息不同,Tiled 简单地使用位置信息来匹配精灵和瓦片。对于这个配方,我们的 tileset 看起来如下:

    如何工作...

    因此,在将其加载到级别之前,请确保您的瓦片集是正确的。最后,点击 地图 | 新瓦片集。命名您的瓦片集并指定您的 PNG 精灵表。

  7. 瓦片层:

    Tiled 支持创建多个重叠层。通常,这些层用于将图形元素重叠放置。除此之外,层还可以指定信息。在我们的级别中,红色区域可以被认为是“可碰撞的”。这将通过程序处理。

    对象层:

    除了瓦片之外,对象也可以放置以指示非瓦片数据,如物品位置。在我们的级别中,我们放置了一个“生成点”对象来指示玩家应该生成的位置。有关此用法的示例,请参阅位于 RecipeCollection03, CCTMXTiledMapResources/Tilemaps 文件夹中的 tilemap.tmx 文件。

    • 一旦我们的地图完成,我们将运行以下代码片段来将 tilemap 资源加载到游戏中:

      CCTMXTiledMap *tileMap = [CCTMXTiledMap tiledMapWithTMXFile:@"tilemap.tmx"];
      [gameNode addChild:tileMap z:0];
      
      

      CCTMXTiledMap 对象包含对每个瓦片的引用,作为一个 CCSprite 对象,以及一些结构来组织这些文件。

    • CCTMXTileLayer

      要访问瓦片层,我们使用以下代码:

      CCTMXLayer *collidableLayer = [tileMap layerNamed:@"Collidable"];
      
      

      要遍历所有 CCTMXLayer 对象,我们也可以使用以下行:

      for( CCTMXLayer* child in [tileMap children] ) {
      //Do Something
      }
      
      
    • 访问瓦片的精灵涉及到调用 tileAt 方法:

      float x = 0; float y = 0;
      CCSprite *tileSprite = [collidableLayer tileAt:ccp(x,y)];
      
      

    当调用 tileAt 方法时,这些精灵会懒加载创建。更多信息,请参考 Cocos2d-iPhone API 参考页面中的 CCTMXTiledMap

  8. 重新排序瓦片:

    在我们的示例中,为了创建正确的透视错觉,我们将遍历所有瓦片精灵并重新排序它们。这仅仅涉及到根据每个瓦片的 Y 位置在 tileMap 上调用 reorderChild 方法。

  9. 添加 Box2D 几何形状:

    要创建物理级别几何形状,我们处理“可碰撞”层并为每个找到的瓦片创建适当大小的盒子对象:

    for(float x=0; x<mw; x+=1){
    for(float y=0; y<mh; y+=1){
    if([collidableLayer tileAt:ccp(x,y)]){
    [self addBoxAtPoint:ccp(x*tw, mh*th - y*th) size:ccp(tw/2,th/2)];
    }
    }
    }
    
    

    使用这个特殊的 collidableLayer 允许级别艺术与我们要使其可碰撞的位置完全对齐。这产生了这样的错觉:每棵树的底部是可碰撞的,而树枝不是。

  10. 处理级别对象:

    级别对象使用 CCTMXObjectGroup 类进行处理:

    CCTMXObjectGroup *objects = [tileMap objectGroupNamed:@"Objects"];
    
    
  11. 接下来,我们处理我们的 SpawnPoint 对象:

    NSMutableDictionary *sp = [objects objectNamed:@"SpawnPoint"];
    int x = [[sp valueForKey:@"x"] intValue];
    int y = [[sp valueForKey:@"y"] intValue];
    spawnPoint = [Vector3D x:x y:y z:0];
    
    

    我们现在可以在地图上的这个位置生成玩家。

参见...

更多关于使用 Tiled 的信息,请参考位于 github.com/bjorn/tiled/wikiTiled 维基。

使用 JSONWorldBuilder 创建层级

使用瓦片创建游戏关卡是一种适用于许多游戏的技巧。然而,在本菜谱中,我们将使用JSONWorldBuilder关卡编辑器以更非线性的方式创建一个关卡。

使用 JSONWorldBuilder 创建关卡

准备工作

请参考项目RecipeCollection03以获取此菜谱的完整工作代码。

如何操作...

执行以下代码:

#import "ActualPath.h"
#import "CJSONDeserializer.h"
//Interface
@interface Ch8_JSONWorldBuilder : GameArea2D
{
NSDictionary *mapData;
CGPoint canvasSize;
NSMutableArray *lineVerticesA;
NSMutableArray *lineVerticesB;
NSMutableArray *points;
}
@end
//Implementation
@implementation Ch8_JSONWorldBuilder
-(CCLayer*) runRecipe {
//Load our map file
[self loadMap:@"world.json"];
return self;
}
/* Called after the map has been loaded into a container but before assets have been loaded */
-(void) finishInit {
//Superclass initialization and message
[super runRecipe];
/* CODE OMITTED */
//Init line/point containers
lineVerticesA = [[NSMutableArray alloc] init];
lineVerticesB = [[NSMutableArray alloc] init];
points = [[NSMutableArray alloc] init];
}
/* Our load map method */
-(void) loadMap:(NSString*)mapStr {
/* CODE OMITTED */
//Add all sprite frames for listed plist files
NSArray *plistFiles = [mapData objectForKey:@"plistFiles"];
for (id plistFile in plistFiles) {
[[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:plistFile];
}
//List of PNG files is also available
NSArray *pngFiles = [mapData objectForKey:@"pngFiles"];
//Pre process data
[self preProcessMapData];
//Process map nodes
NSDictionary *mapNodes = [mapData objectForKey:@"mapNodes"];
for (id mapNodeKey in mapNodes) {
NSDictionary *mapNode = [mapNodes objectForKey:mapNodeKey];
NSString *nodeType = [mapNode objectForKey:@"type"];
//Process node types
if([nodeType isEqualToString:@"sprite"]){
[self processSprite:mapNode];
}else if([nodeType isEqualToString:@"tiledSprite"]){
[self processTiledSprite:mapNode];
}else if([nodeType isEqualToString:@"line"]){
[self processLine:mapNode];
}else if([nodeType isEqualToString:@"point"]){
[self processPoint:mapNode];
}
}
}
-(void) preProcessMapData {
//Set canvasSize and gameAreaSize from map file
canvasSize = ccp( [[mapData objectForKey:@"canvasWidth"] floatValue], [[mapData objectForKey:@"canvasHeight"] floatValue] );
gameAreaSize = ccp( canvasSize.x/PTM_RATIO, canvasSize.y/PTM_RATIO );
//Finish map initialization
[self finishInit];
}
/* Process a sprite node. This represents a single sprite onscreen */
-(void) processSprite:(NSDictionary*)mapNode {
//Get node information
NSString *texture = [mapNode objectForKey:@"selectedSpriteY"];
float originX = [[mapNode objectForKey:@"originX"] floatValue];
/* CODE OMITTED */
//Get metadata
NSDictionary *metaPairs = [mapNode objectForKey:@"meta"];
for (id metaKey in metaPairs) {
NSString* metaValue = [metaPairs objectForKey:metaKey];
//Check for key "tag"
if([metaKey isEqualToString:@"tag"]){
tag = ((int)[metaValue dataUsingEncoding:NSUTF8StringEncoding]);
}
}
/* CODE OMITTED */
//Finally, add the sprite
[gameNode addChild:sprite z:zIndex-24995 tag:tag];
}
/* Process a tiled sprite. */
-(void) processTiledSprite:(NSDictionary*)mapNode {
//Get node information
NSString *texture = [mapNode objectForKey:@"selectedSpriteY"];
NSMutableDictionary *frames = [[[NSMutableDictionary alloc] init] autorelease];
float originX = [[mapNode objectForKey:@"originX"] floatValue];
/* CODE OMITTED */
//Get metadata
NSDictionary *metaPairs = [mapNode objectForKey:@"meta"];
for (id metaKey in metaPairs) {
NSString* metaValue = [metaPairs objectForKey:metaKey];
//Check for key "tag" or key "frame" (for animation)
if([metaKey isEqualToString:@"tag"]){
tag = ((int)[metaValue dataUsingEncoding:NSUTF8StringEncoding]);
}else if ([metaKey rangeOfString:@"frame"].location != NSNotFound){
[frames setObject:metaValue forKey:metaKey];
}
}
//Get any masks to be applied to this tiled sprite
NSArray *masks = [mapNode objectForKey:@"masks"];
//OpenGL texture parameters
ccTexParams params = {GL_NEAREST,GL_NEAREST_MIPMAP_NEAREST,GL_REPEAT,GL_REPEAT};
//If a mask exists, apply it
if([masks count] > 0){
/* CODE OMITTED */
//Create TexturedPolygon object
TexturedPolygon *tp = [TexturedPolygon createWithFile:texture withVertices:vertices withTriangles:triangles];
[tp.texture setTexParameters:&params];
//Set position
float x = originX - (canvasSize.x/2);
float y = canvasSize.y - originY - (canvasSize.y/2);
tp.position = ccp( x, y-height );
/* CODE OMITTED */
//Finally, add the node
[gameNode addChild:tp z:zIndex-24995];
}else if([frames count] > 0){
/* If we have a non-masked tiled animated sprite */
/* CODE OMITTED */
}else{
//Use a regular Sprite
CCSprite *sprite = [CCSprite spriteWithFile:texture rect:CGRectMake(0,0,width,height)];
[sprite.texture setTexParameters:&params];
//Set position
float x = originX - (canvasSize.x/2);
float y = canvasSize.y - originY - (canvasSize.y/2);
sprite.position = ccp( x+width/2, y-height/2 );
//Add the node
[gameNode addChild:sprite z:zIndex-24999];
}
}
/* Process a line */
-(void) processLine:(NSDictionary*)mapNode{
//Get line information
NSArray *drawnLines = [mapNode objectForKey:@"drawnLines"];
/* CODE OMITTED */
//Add information to our line containers
[lineVerticesA addObject:[NSValue valueWithCGPoint:ccp(fromX, canvasSize.y-fromY)]];
[lineVerticesB addObject:[NSValue valueWithCGPoint:ccp(toX, canvasSize.y-toY)]];
}
/* Process a point */
-(void) processPoint:(NSDictionary*)mapNode{
//Get point information
float originX = [[mapNode objectForKey:@"originX"] floatValue];
float originY = [[mapNode objectForKey:@"originY"] floatValue];
originY = canvasSize.y - originY;
//If metadata is appropriate, add point to container
/* CODE OMITTED */
}
-(void) cleanRecipe {
[lineVerticesA release];
[lineVerticesB release];
[points release];
[super cleanRecipe];
}
@end

如何工作...

此菜谱加载由 JSONWorldBuilder 创建的 JSON 关卡文件:

  1. 安装 JSONWorldBuilder:

    首先,我们必须下载并安装 JSONWorldBuilder 应用程序。JSONWorldBuilder 的源代码可以在http://github.com/n8dogg/JSONWorldBuilder找到。要下载最新的build版本,请点击下载,下载源代码压缩包,解压它,最后查看 build 文件夹。在这里,你会找到一个包含最新 JSONWorldBuilder 应用程序的压缩包。将应用程序拖到你的Applications文件夹中。

  2. 创建新关卡:

    自动打开 JSONWorldBuilder 会创建一个新关卡。要清除你目前正在工作的关卡,请选择文件 | 新地图

  3. 指定资源文件夹:

    JSONWorldBuilder 设计用于与 PNG 图像文件以及 PNG/PLIST 组合一起工作。当你指定资源文件夹时,我们告诉编辑器这些资源所在的位置。点击资源 | 指定资源文件夹。这将弹出一个提示。一旦指定了一个文件夹,精灵窗口将填充精灵。点击精灵图集名称以隐藏/显示单个精灵:

    如何工作...

    点击单个精灵将选择它用于编辑器。

  4. 精灵印章:

    精灵窗口中选择一个精灵后,点击左侧菜单中的精灵印章工具。现在,通过点击画布,你可以将选定的精灵重复地印在画布上。

  5. 精灵选择器:

    要移动你刚刚印章的精灵,点击精灵选择器工具。现在,点击并拖动画布上的精灵以移动它。

  6. 绘制瓦片精灵:

    要绘制瓦片精灵,点击绘制瓦片精灵工具。现在点击并拖动鼠标以创建一个带有选定纹理的矩形瓦片区域。在编辑器中,你可以使用精灵图集或单个图像文件作为瓦片纹理。然而,请注意,我们一直在使用的TexturedPolygon类需要单个图像文件才能正常工作。

  7. 绘制遮罩:

    现在,我们在画布上有一个瓦片精灵,我们可以对其进行遮罩。这意味着我们将它切割成形状。使用精灵工具选择你放置在画布上的瓦片精灵。现在选择绘制遮罩工具。在瓦片精灵上或其周围单击一次以开始遮罩创建过程。这涉及到通过连续的鼠标点击创建多边形,最后回到你最初点击的位置以完成多边形。

  8. 线条、点和多边形:

    可以使用线条、点和多边形将空间信息添加到您的地图中。这些分别使用 创建线条、创建点创建多边形 工具创建。请注意,您需要点击并拖动来创建线条。

  9. 形状选择器:

    形状选择器 工具将允许您在画布上选择和重新定位形状。

  10. 移动相机、缩放和画布调整大小:

    移动相机 工具允许我们平移相机的位置。在右侧的 Nav 窗口中,您可以点击 + 或 - 按钮来缩放相机。您还可以调整画布大小。

  11. 地图对象窗口:

    地图对象 窗口中,您可以指定诸如对象位置和瓦片对象大小等信息。在这里,您还可以以键/值字典的形式添加元标签,在 X 和/或 Y 轴上翻转图像,以及在 Z 轴上重新排列对象。

  12. 加载我们的地图:

    一旦我们创建了地图,我们就可以继续在 Cocos2d 中加载它。我们使用 CJSONDeserializer 遍历我们的地图文件并处理数据。在处理一些初始信息后,loadMap 方法根据地图节点类型调用四个不同的方法:

    -(void) processSprite:(NSDictionary*)mapNode;
    -(void) processTiledSprite:(NSDictionary*)mapNode;
    -(void) processLine:(NSDictionary*)mapNode;
    -(void) processPoint:(NSDictionary*)mapNode;
    
    

    每个方法处理适当的节点,并将精灵信息附加到 gameNode 对象或绘图信息附加到 drawLayer。元数据也在这类方法中处理,尽管它也可以在 loadMap 方法中处理。

使用 CocosBuilder 创建场景

并非只有级别可以使用 所见即所得 编辑器构建。在这个配方中,我们将使用 CocosBuilder 创建一个简单的菜单场景。

使用 CocosBuilder 创建场景

准备工作

请参阅项目 RecipeCollection03 以获取此配方的完整工作代码。

如何操作...

执行以下代码:

#import "CCBReader.h"
//Implementation
@implementation Ch8_CocosBuilder
-(CCLayer*) runRecipe {
//Add button to push CocosBuilder scene
[CCMenuItemFont setFontSize:32];
CCMenuItemFont *pushItem = [CCMenuItemFont itemFromString:@"Push CocosBuilder Scene" target:self selector:@selector(pushScene)];
CCMenu *pushMenu = [CCMenu menuWithItems:pushItem, nil];
pushMenu.position = ccp(240,160);
[self addChild:pushMenu];
return self;
}
/* Push scene callback */
-(void) pushScene {
CCScene* scene = [CCBReader sceneWithNodeGraphFromFile:@"scene.ccb" owner:self];
[[CCDirector sharedDirector] pushScene:scene];
}
/* Callback called from CocosBuilder scene */
-(void) back {
[[CCDirector sharedDirector] popScene];
}
@end

它是如何工作的...

此配方加载 CocosBuilder CCB 场景及其相关资源:

  1. 安装 CocosBuilder:

    首先,我们必须下载并安装 CocosBuilder 应用程序。转到 http://cocosbuilder.com/?page_id=11 并点击 下载 CocosBuilder 应用程序 链接。将应用程序拖到您的 Applications 文件夹中。

  2. 开始:

    CocosBuilder 允许我们创建从 CCNode 派生的对象分层布局。可用的节点类型包括 CCLayer, CCSprite, CCMenuCCParticleSystem。这些节点的文件资源应与 CCB 文件本身位于同一文件夹中。因此,在创建新的 CCB 文件之前,我们必须创建一个文件夹并填充我们想要使用的资源。完成后,点击 文件 | 新建。选择 CCNode 作为 根对象类型。将此文件保存到您创建的文件夹中。如果您以后想添加更多资源,只需将它们复制到文件夹中,然后点击 对象 | 重新加载资源

  3. 添加对象:

    要将对象作为根 CCNode 对象的子对象添加,请单击根节点,然后单击 对象 | 添加对象为子对象。然后单击您想要添加的对象类型。

  4. 添加 CCSprite 对象:

    在我们的示例中,我们添加了三个CCSprite对象。精灵对象总是添加而没有相应的纹理文件。在添加黑色精灵后,选择你的精灵文件/图集和对应的精灵名称,在CCSprite标题下的右侧。

  5. 添加带有回调的CCMenuItemImage

    我们还添加了一个CCMenu对象和一个子CCMenuItemImage。在CCMenuItemImage对象上,我们指定了一个回调。当"所有者"对象返回时,将调用该方法。

  6. 加载我们的场景:

    在 Cocos2d 中,我们使用以下行来加载场景文件,设置场景的"所有者",并最终推送场景:

    CCScene* scene = [CCBReader sceneWithNodeGraphFromFile:@"scene.ccb" owner:self];
    [[CCDirector sharedDirector] pushScene:scene];
    
    

    在场景中点击返回按钮会调用我们配方文件中的返回方法,然后场景被弹出。使用这些工具,你可以快速搭建游戏菜单和其他独立场景。

使用 Cocos2d-X

Cocos2d 不仅限于 iOS 开发。Cocos2d-X是 Cocos2d-iPhone 的C++移植版。使用 Cocos2d-X,我们可以为包括 Mac、PC、Linux、Android 等多个平台开发游戏。在本教程中,我们将安装 Cocos2d-X XCode 模板,创建一个简单的 Cocos2d-X 应用程序,并介绍 Cocos2d-X 测试平台。

使用 Cocos2d-X

准备工作

请参考项目Ch8_Cocos2d-X以获取本教程的完整工作代码。

如何操作...

执行以下代码:

#include "HelloWorldScene.h"
#include "SimpleAudioEngine.h"
using namespace cocos2d;
using namespace CocosDenshion;
CCScene* HelloWorld::scene()
{
//'scene' is an autorelease object
CCScene *scene = CCScene::node();
//'layer' is an autorelease object
HelloWorld *layer = HelloWorld::node();
//Add layer as a child to scene
scene->addChild(layer);
return scene;
}
// on "init" you need to initialize your instance
bool HelloWorld::init()
{
//Super initialization
if ( !CCLayer::init() )
{
return false;
}
//Add a menu item with "X" image, which is clicked to quit the program. You may modify it.
//Add a "close" icon to exit the progress. it's an autorelease object
CCMenuItemImage *pCloseItem = CCMenuItemImage::itemFromNormalImage("CloseNormal.png", "CloseSelected.png", this, menu_selector(HelloWorld::menuCloseCallback) );
pCloseItem->setPosition( ccp(CCDirector::sharedDirector()->getWinSize().width - 20, 20) );
//Create menu, it's an autorelease object
CCMenu* pMenu = CCMenu::menuWithItems(pCloseItem, NULL);
pMenu->setPosition( CCPointZero );
this->addChild(pMenu, 1);
//Add a label shows "Hello World"
// create and initialize a label
CCLabelTTF* pLabel = CCLabelTTF::labelWithString("Hello World", "Thonburi", 34);
//Ask director the window size
CCSize size = CCDirector::sharedDirector()->getWinSize();
//Position the label on the center of the screen
pLabel->setPosition( ccp(size.width / 2, size.height - 20) );
//Add the label as a child to this layer
this->addChild(pLabel, 1);
//Add "HelloWorld" splash screen"
CCSprite* pSprite = CCSprite::spriteWithFile("HelloWorld.png");
//Position the sprite on the center of the screen
pSprite->setPosition( ccp(size.width/2, size.height/2) );
//Add the sprite as a child to this layer
this->addChild(pSprite, 0);
return true;
}
void HelloWorld::menuCloseCallback(CCObject* pSender)
{
CCDirector::sharedDirector()->end();
#if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS)
exit(0);
#endif
}

它是如何工作的...

本教程展示了 Cocos2d-X 的基本用法示例,操作如下:

  1. 安装 Cocos2d-X XCode 模板:

    首先,我们必须下载最新的 Cocos2d-X 版本并安装 XCode 模板。访问www.cocos2d-x.org/projects/cocos2d-x/wiki/Download并下载最新的源代码包。解压包后,你会找到用于创建多个开发环境项目的工具。要安装 XCode 模板,打开终端,导航到 Cocos2d-X 文件夹,并运行以下命令:

    sudo sh install-templates-xcode.sh
    
    

    这将安装模板。

  2. 创建 Cocos2d-X 项目:

    要使用新安装的 XCode 模板创建项目,请点击文件 | 新建 | 新建项目。在 iOS 下,你应该能看到cocos2d-x。在此之下,有一些用于 Box2D、Chipmunk 和 Lua 集成的模板。选择其中一个。

  3. 使用 Cocos2d-X:

    如前述代码所示,Cocos2d-X是将 Cocos2d-iPhone 完整移植到 C++的版本。有关Cocos2d-X的更多信息,请参阅位于http://www.cocos2d-x.org/embedded/cocos2d-x/classes.htmlCocos2d-X | Doxygen文档。

  4. Cocos2d-X 测试平台:

    在测试文件夹中,你可以找到针对不同操作系统的多个测试项目。在test.ios文件夹中打开项目test.xcodeproj。这是 Cocos2d 测试平台的彻底移植,增加了简单的菜单系统,使得在示例之间导航更加容易。

使用 Cocos3d

Cocos2d 是一个如此通用的框架,它甚至已经被移植并扩展为一个名为 Cocos3d3D 游戏引擎。在这个配方中,我们将安装 Cocos3d XCode 模板,创建一个示例 Cocos3d 应用程序,并介绍 Cocos3d 的 demo 混合项目

使用 Cocos3d

准备工作

请参考 Ch8_Cocos3d 项目以获取本配方的完整工作代码。

如何操作...

执行以下代码:

#import "Ch8_Cocos3dWorld.h"
#import "CC3PODResourceNode.h"
#import "CC3ActionInterval.h"
#import "CC3MeshNode.h"
#import "CC3Camera.h"
#import "CC3Light.h"
@implementation Ch8_Cocos3dWorld
-(void) dealloc {
[super dealloc];
}
-(void) initializeWorld {
//Create the camera, place it back a bit, and add it to the world
CC3Camera* cam = [CC3Camera nodeWithName: @"Camera"];
cam.location = cc3v( 0.0, 0.0, 6.0 );
[self addChild: cam];
//Create a light, place it back and to the left at a specific position (not just directional lighting), and add it to the world
CC3Light* lamp = [CC3Light nodeWithName: @"Lamp"];
lamp.location = cc3v( -2.0, 0.0, 0.0 );
lamp.isDirectionalOnly = NO;
[cam addChild: lamp];
//This is the simplest way to load a POD resource file and add the nodes to the CC3World, if no customized resource subclass is needed.
[self addContentFromPODResourceFile: @"hello-world.pod"];
//Create OpenGL ES buffers for the vertex arrays to keep things fast and efficient, and to save memory, release the vertex data in main memory because it is now redundant.
[self createGLBuffers];
[self releaseRedundantData];
//That's it! The model world is now constructed and is good to go.
//But to add some dynamism, we'll animate the 'hello, world' message using a couple of cocos2d actions...
//Fetch the 'hello, world' 3D text object that was loaded from the POD file and start it rotating
CC3MeshNode* helloTxt = (CC3MeshNode*)[self getNodeNamed: @"Hello"];
CCActionInterval* partialRot = [CC3RotateBy actionWithDuration: 1.0 rotateBy: cc3v(0.0, 30.0, 0.0)];
[helloTxt runAction: [CCRepeatForever actionWithAction: partialRot]];
//To make things a bit more appealing, set up a repeating up/down cycle to change the color of the text from the original red to blue, and back again.
GLfloat tintTime = 8.0f;
ccColor3B startColor = helloTxt.color;
ccColor3B endColor = { 50, 0, 200 };
CCActionInterval* tintDown = [CCTintTo actionWithDuration: tintTime red: endColor.r green: endColor.g blue: endColor.b];
CCActionInterval* tintUp = [CCTintTo actionWithDuration: tintTime red: startColor.r green: startColor.g blue: startColor.b];
CCActionInterval* tintCycle = [CCSequence actionOne: tintDown two: tintUp];
[helloTxt runAction: [CCRepeatForever actionWithAction: tintCycle]];
}
/* This template method is invoked periodically whenever the 3D nodes are to be updated. */
-(void) updateBeforeTransform: (CC3NodeUpdatingVisitor*) visitor {}
/* This template method is invoked periodically whenever the 3D nodes are to be updated. */
-(void) updateAfterTransform: (CC3NodeUpdatingVisitor*) visitor {}
@end

它是如何工作的...

这个配方展示了 Cocos3d 的基本应用示例。

  1. 安装 Cocos3d XCode 模板:

    首先,我们必须下载最新的 Cocos3d 版本并安装 XCode 模板。访问 http://brenwill.com/cocos3d/。在右侧,你应该能看到最新的 Cocos3d 源代码包。下载并解压该包。要安装 XCode 模板,打开终端,导航到最近解压的 Cocos3d 文件夹,并运行以下命令:

    sudo sh install-cocos3d.sh
    
    

    这将安装模板。

  2. 创建 Cocos3d 项目:

    要使用新安装的 XCode 模板创建项目,请点击 文件 | 新建 | 新建项目。在 iOS 下,你应该能看到 cocos3d。在此选择 cocos3d 应用程序

  3. 使用 Cocos3d:

    在我们创建的简单示例中,我们看到一个读取 "hello, world" 的 3D 字体渲染。这个 3D 模型是一个在 Maya 或 3DS Max 中创建的 PowerVR POD 文件,并使用 PVRGeoPOD 导出。有关 Cocos3d 的更多信息,请参阅位于(截至本文撰写时)http://brenwill.com/docs/cocos3d/0.6.0-sp/api/ 的 Cocos3d 文档。

  4. CC3DemoMashUp:

    在源代码包中,你可以找到 XCode 工作空间文件 cocos3d.xcworkspace。打开它,你会找到 CC3DemoMashUp 目标。这个混合体包含了许多使用网格模型、摄像机、灯光、凹凸贴图、动画等高级示例。

发布你的应用

当你最终完成创建你的应用后,就是时候在 Apple 的 App Store 上发布它了。在这个配方中,我们将介绍这个过程。

发布你的应用

准备工作

本配方的范围相当广泛。因此,我们将提供一个大致的过程概述以及支持性文档,以帮助您将应用发布到 App Store。这些步骤主要基于 iOS 配置文件门户分发 部分的指南。您需要一个有效的 iOS 开发者账号 来访问此指南。另一个优秀的指南可以在以下位置找到(需要有效的 iOS 开发者账号):adcdownload.apple.com/ios/ios_developer_program_user_guide/ ios_developer_program_user_guide__standard_program_v2.7__final_9110.pdf

如何操作...

一旦您的应用完成,就是时候开始为 App Store 准备您的应用了:

  1. iOS 配置文件门户:

    iOS 配置文件门户是创建和管理开发证书和配置文件的地方,通常与 XCode 一起使用。您可以通过访问http://developer.apple.com/ios/manage/overview/index.action或通过访问http://developer.apple.com/devcenter/ios/index.action并点击 iOS 配置文件门户来找到该门户。一旦您在那里,请转到门户的App IDs部分。

  2. 创建显式的 App ID:

    到目前为止,您可能一直使用带有星号作为 ID 后缀的App ID将应用程序配置到设备上。此通配符允许轻松地将任何应用程序发布到已注册的设备。然而,如果我们想将我们的应用程序发布到 App Store 并启用如Push Notification等功能,我们需要创建一个显式的 App ID。点击新建 App ID,并在App ID 后缀下输入一个正确的反向域名样式字符串,而不是输入一个*。例如,这将是com.domainname.appname。这是您的包标识符。完成此操作后,请转到 iOS 配置文件门户的分发部分。

  3. 获取您的分发证书:

    这是你必须开始仔细遵循屏幕上指示的地方。点击获取您的 iOS 分发证书以显示第一组指示。在这里,您将被告知使用 Mac 上的钥匙串访问应用程序生成一个证书签名请求。然后,此请求将在线提交。一旦批准,您就可以下载并安装您的分发证书。

  4. 创建分发配置文件:

    接下来,我们必须创建一个新的“配置文件”,专门用于在 App Store 上的分发。这可以在 iOS 配置文件门户的配置选项卡下处理。请记住,此配置文件不会允许您直接将应用推送到设备。为此,请查看Ad Hoc 分发

  5. 为分发构建您的应用程序:

    一旦您的分发配置文件安装完毕,您必须在 XCode 中创建一个新的构建配置以进行分发。此配置指定了必要的证书和配置文件以及之前创建的特定包标识符。此步骤还将引导您设置 Entitlements.plist 文件。最后,您构建应用程序并将其压缩以进行传输。

  6. 在 iTunes Connect 中添加您的应用:

    如我们之前所讨论的,iTunes Connect是一套工具,帮助开发者发布他们的应用程序并管理应用程序信息。您可以通过访问itunesconnect.apple.com/来登录 iTunes Connect。一旦您登录到 iTunes Connect,您需要点击管理您的应用程序。然后,在左上角点击添加新应用以开始添加新的应用程序。按照屏幕上的说明操作。这包括上传应用程序图标、图片和描述。完成之后,返回到管理您的应用程序页面,选择您刚刚创建的应用程序。最后,点击准备上传二进制文件并填写任何必要的信息。现在,您的应用程序应该显示为等待上传状态。

  7. 使用应用加载器:

    现在是时候使用应用加载器上传您的应用程序了。应用加载器是一个独立的 Mac 应用程序,用于处理应用程序的上传。您可以从itunesconnect.apple.com/apploader/ApplicationLoader_1.3.dmg下载应用加载器。一旦您安装了应用加载器,打开它并开始上传我们之前打包的应用程序。有关使用应用加载器的更多信息,请参阅此处提供的文档:itunesconnect.apple.com/docs/UsingApplicationLoader.pdf

  8. App Store 审核流程:

    一旦您的应用程序上传完毕,它将被放入队列中等待苹果的审核。这个过程可能需要一周以上,所以请耐心等待。一旦您的应用程序获得批准,您将看到一个小的绿色指示灯,应用程序状态将显示为准备销售。给苹果 24-48 小时的时间,您的应用程序应该就会出现在 App Store 上!有关 App Store 审查指南的更多信息,请参阅此页面:developer.apple.com/appstore/guidelines.html

posted @ 2025-09-28 09:13  绝不原创的飞龙  阅读(30)  评论(0)    收藏  举报