首页 第3章-基础知识

第3章-基础知识

举报
开通vip

第3章-基础知识 1 译者:杨栋 邮箱:yangdongmy@gmail.com 2 第三章 基础知识 本章将会为你介绍cocos2d游戏引擎的构成要素。你将会在每一个编写的游戏里 用到这些类,所以了解它们是些什么样的类和这些类是如何在一起工作的,将 会帮助你写出更好的游戏。有了这些知识,你会发现使用cocos2d很容易。 配套本章的Xcode项目叫作 “Essentials”。它包含了所有在此讨论的内容, 外加一些额外的例子。源代码附带详尽的注释,所以读起来就像本书的...

第3章-基础知识
1 译者:杨栋 邮箱:yangdongmy@gmail.com 2 第三章 基础知识 本章将会为你介绍cocos2d游戏引擎的构成要素。你将会在每一个编写的游戏里 用到这些类,所以了解它们是些什么样的类和这些类是如何在一起工作的,将 会帮助你写出更好的游戏。有了这些知识,你会发现使用cocos2d很容易。 配套本章的Xcode项目叫作 “Essentials”。它包含了所有在此讨论的内容, 外加一些额外的例子。源代码附带详尽的注释,所以读起来就像本书的附录。 我们将以讨论cocos2d游戏引擎结构作为开始。每个游戏引擎在管理和呈现屏幕 上的游戏对象的方式都是不一样的。所以一开始我们就要了解有哪些元素存在 以及它们之间的关系。 cocos2d的单例 cocos2d很好的利用了单例 设计 领导形象设计圆作业设计ao工艺污水处理厂设计附属工程施工组织设计清扫机器人结构设计 模式。因为此模式经常引起争议,所以我想有必 要在此解释一下单例。原则上,单例是在程序生命周期里只被实例化过一次的 类。为了确保这一点,我们利用类的一个静态方法来生成和访问对象。因此, 你是通过以“shared”开头的方法来访问cocos2d的单例对象的,而不是用 alloc/init或者静态autorelease初始化方法。以下是一些最常用到的cocos2d 单例类和访问它们的方法: CCActionManager* sharedManager = [CCActionManager sharedManager]; CCDirector* sharedDirector = [CCDirector sharedDirector]; CCSpriteFrameCache* sharedCache = [CCSpriteFrameCache sharedSpriteFrameCache]; CCTextureCache* sharedTexCache = [CCTextureCache sharedTextureCache]; CCTouchDispatcher* sharedDispatcher = [CCTouchDispatcher sharedDispatcher]; CDAudioManager* sharedManager = [CDAudioManager sharedManager]; SimpleAudioEngine* sharedEngine = [SimpleAudioEngine sharedEngine]; 单例的好处是它可以在任何时间任何地点被任何类所调用。它接近于全局类的 作用,更像一个全局变量。如果你需要在任何地方都能用到某些数据或者方法, 单例是很好的选择。音频就是个很好的例子:因为任何一个类,不管是玩家, 敌人,菜单按钮,或是过场动画,都可能需要播放声效或者改变背景音乐。因 此,使用单例来播放音频是很好的选择。同样,如果存在全局的游戏状态,比 如说玩家军队的大小和每支部队排的数目,你可以把这些信息存到一个单例中, 把这些信息从一个关卡传到另一个关卡。列表3-1演示了如何实现单例。这些代 码使用了最少的代码实现了MyManager类的单例。SharedManager提供了访问 MyManager单一实例的静态方法。如果实例不存在,一个MyManager的实例将 会被分配和初始化;否则已经存在的实例会被返回。 3 列表3-2:MyManager类实现单例 static MyManager *sharedManager = nil; +(MyManager*) sharedManager { if (sharedManager == nil) { sharedManager = [[MyManager alloc] init]; } return sharedManager; } 不过,单例也有不好的方面。因为单例很容易实现,而且可以在任何地方访问 到,它们可能会被用在不该用的地方。 例如,你可能觉得你的游戏只有一个玩家对象,所以为什么就不能把玩家这个 类变成单例呢?一切看起来都没有什么问 快递公司问题件快递公司问题件货款处理关于圆的周长面积重点题型关于解方程组的题及答案关于南海问题 – 直到你认识到不管什么时候这 个玩家进入下一个关卡,这个玩家不仅带着上一关卡的得分,而且还有上一关 卡的最后一帧动画信息,健康值,和所有已经捡到的物品,并且由于他在离开 上一关卡的时候还在“狂暴”状态下,在新关卡开始的时候,他还处于之前的 状态中。 为了解决这个问题,你可能会在类里加入一个重置某些变量的方法。看起来问 题解决了。但是当你在游戏代码中添加越来越多的功能以后,在转换关卡时, 你需要维护的变量也会越来越多。最糟糕的是,某天你的朋友建议你为iPad版 本增加个双人模式。但是你发现你的玩家类是个单例,在任何时候你只能有一 个玩家对象存在!这可麻烦了:要么你要重写很多代码,或者只能放弃很酷的 双人模式了。 你越依赖于单例,类似的问题就会越多。在创建任何一个单例类之前,你都要 考虑是否真的需要单例,是否需求会在不久的将来改变。 The Director(导演) CCDirector类,简称Director(导演),是cocos2d游戏引擎的核心。如果你回 想一下第二章的HelloWorld应用,你会记得有很多cocos2d的初始化过程包含了 [CCDirector sharedDirector]的调用。Director是一个单例:它保存着 cocos2d的全局配置设定,同时管理着cocos2d的场景。 Director的主要用处如下: 1. 访问和改变场景 2. 访问cocos2d的配置细节 3. 访问视图(OpenGL,UIView,UIWindow) 4. 暂停,恢复和结束游戏 5. 在UIKit和OpenGL之间转换坐标 4 实际上存在四种类型的Director。它们在细节上有所不同。最常用的Director 是CCDisplayLinkDirector,它的内部使用了苹果的CADisplayLink类。它是最 好的选择,但是只有在iOS 3.1以上的版本中才能使用。其次,你可以使用 CCFastDirector。如果你想让Cocoa Touch视图和cocos2d一同工作,你必须转 到CCThreadedFastDirector,因为只有这个Director才能完全支持。 CCThreadedFastDirector不好的一面是:使用它会很耗电。最后的选择是 CCTimerDirector,但这是没有办法的选择,因为它是四种Director里面最慢的。 场景图(The Scene Graph) 有时候又被称为“场景层级”。场景图是由所有目前活跃的cocos2d节点所组成 的一个层级图。除了场景本身,每一个节点只有一个父节点,但是可以有任意 数量的子节点。 当你将节点添加到其它节点中时,你就在构建一个节点场景图。图3-1描绘了一 个虚构的游戏场景图。在最上面,你总是放置场景节点(MyScene),通常跟着的 是一个层节点(MyLayer)。在cocos2d里,层节点的作用是接收触摸和加速计 的输入。 图3-1. 一个简化的由多个不同节点组成的cocos2d场景图。场景图中有一个玩家节点和他的武 器节点,游戏的得分,和游戏中用于暂停和改变游戏选项的菜单。 在CCLayer下一层的是你游戏的组成要素,它们大多数是精灵(sprite)节点。 它们包括用于显示游戏得分的标签节点,用于显示游戏内菜单的菜单和菜单项 目节点,玩家用这些菜单来暂停游戏或者回去主菜单。 在图3-1中你会注意到PlayerSprite节点中有个子节点PlayerWeaponSprite。换 句话说,PlayerWeaponSprite是附加在PlayerSprite上的。如果PlayerSprite 移动,旋转或放大缩小,PlayerWeaponSprite将会跟着做同样的事情而不需要 额外的代码。这就是场景图的强大之处:你对一个节点施加的影响将会影响到 它的所有子节点。但是有时候这也会产生混淆,因为像位置和旋转都是相对于 父节点来说的。 我写了一个叫作“NodeHierarchy”的Xcode样例,你可以在本书提供的源代码 5 里找到。它演示了在一个层级关系里的节点是如何相互影响的。我想实际的例 子比用文字和图片说明要来的更直观和容易理解。 CCNode类的层级. 所有节点都有一个共同的父类:CCNode。它定义了许多除显示节点外的通用的 属性和方法。图3-2展示了继承自CCNode的一些最重要的类。这些类是你最常用 到的。其实即使你只用这些类,你也可以创造出很有意思的游戏。 图3-2. CCNode是cocos2d中最重要的类。所有类都继承自CCNode。CCNode定义了通用的属性和 方法。 CCNode CCNode是所有节点的基类。它是一个抽象类,没有视觉表现。它定义了所有节 点都通用的属性和方法。 使用节点 CCNode类实现了所有添加,获取和删除子节点的方法。以下是一些处理子节点 的方法: 1. 生成一个新的节点 : CCNode* childNode = [CCNode node]; 2. 将新节点添加为子节点: [myNode addChild:childNode z:0 tag:123]; 3. 获取子节点: CCNode* retrievedNode = [myNode getChildByTag:123]; 4. 通过tag删除子节点;cleanup会停止任何运行中的动作: [myNode removeChildByTag:123 cleanup:YES]; 5. 通过节点指针删除节点: [myNode removeChild:retrievedNode]; 6 6. 删除一个节点的所有子节点: [myNode removeAllChildrenWithCleanup:YES]; 7. 从myNode的父节点删除myNode: [myNode removeFromParentAndCleanup:YES]; addChild中的z参数决定了节点的绘制顺序。拥有最小z值的节点会首先被绘制; 拥有最大z值的节点最后一个被绘制。如果多个节点拥有相同的z值,他们的绘 制顺序将由他们的添加顺序来决定。当然,这个规则只适用于像sprites那样有 视觉表现的节点。 tag参数允许你通过getChildByTag方法来获取指定的节点。 注:如果有多个节点拥有相同的tag数值,getChildByTag将把找到的第一个节 点返回。其它节点将不能够再被访问。所以你要确保为你的节点指定独有的tag 数值。 动作(Actions)也有tag。不过,节点和动作的tag不会冲突,所以拥有相同 tag数值的动作和节点可以和平共处。 使用动作(Actions) 节点可以运行动作。我会在以后多讲一些动作相关的知识。现在你只要知道动 作可以让节点移动,旋转和缩放,还可以让节点做一些其它的事情。 1. 以下是一个动作的声明: CCAction* action = [CCBlink actionWithDuration:10 blinks:20]; action.tag = 234; 2. 运行这个动作会让节点闪烁: [myNode runAction:action]; 3. 如果你想在以后使用此动作,你可以用tag获取: CCAction* retrievedAction = [myNode getActionByTag:234]; 4. 你可以用tag停止相关联的动作: [myNode stopActionByTag:234]; 5. 或者你也可以用动作指针停止动作: [myNode stopAction:action]; 6. 你可以停止所有在此节点上运行的动作: [myNode stopAllActions]; 7 预定信息 节点可以预定信息,其实就是Objective-C里面的每隔一段时间调用一次方法。 在很多情况下,你需要节点调用指定的更新方法以处理某些情况,比如说碰撞 测试。以下是一个最简单的,可以在每一帧都被调用的更新方法: -(void) scheduleUpdates { [self scheduleUpdate]; } -(void) update:(ccTime)delta { // 此方法每一帧都会被调用 } 很简单不是吗?你会注意到我们现在的更新方法是固定的,每一帧都会调用上 述方法。delta这个参数表示的是此方法的最后一次调用到现在所经过的时间。 如果你想每一帧都调用相同的更新方法,上述做法很适用。不过有时候你需要 用到更灵活的更新方法。 如果你想运行不同的方法,或者是每秒调用10次更新方法的话,你应该使用以 下代码: -(void) scheduleUpdates { [self schedule:@selector(updateTenTimesPerSecond:) interval:0.1f]; } -(void) updateTenTimesPerSecond:(ccTime)delta { // 此方法将根据时间间隔来调用,每秒10次 } 如果时间间隔(interval)为0的话,你应该使用scheduleUpdate方法。不过, 如果你想之后停止对某个指定更新方法的预定信息的话,上述代码更加合适。 因为scheduleUpdate方法没有停止预定信息的功能。 更新方法的签名和之前是一样的:delta时间是它唯一的参数。但是这次你可以 使用任何名称,而且它会每十分之一秒被调用一次。如果你不想每一帧都判断 是否达到了胜利的条件(有可能判断的过程很复杂),每秒调用10次更新方法 会比每帧都调用要好。或者,你想让代码在10分钟以后调用运行一个动作,你 可以将时间间隔(interval)设置为600。 注:@selector(...)这个语法看起来有点怪。这是Objective-C用来参照指定方 法的方式。这里很重要的一点是最后的那个冒号。它告诉Objective-C去找在此 指定的方法名,并且此方法只有一个参数。如果你忘记在最后加上冒号,程序 还是会继续编译,但是之后会崩溃。在调试控制台(Debugger Console)里, 你会看到这样的错误日志:“unrecognized selector sent to instance …”。 @selector(...)里的冒号数量必须和方法的参数数量和名称相匹配。看一下以 8 下方法: -(void) example:(ccTime)delta sender:(id)sender flag:(bool)aBool 相对应的@selector应该是: @selector(example:sender:flag:) 通过你自己的选择器(selector)或者用@selector(...)关键词的方式来预定 更新方法会有一个很大的问题。默认情况下,如果方法名不存在的话,编译器 并不会报错,而是在方法被调用时直接导致程序崩溃。因为调用是在cocos2d内 部进行的,所以很难发现问题的根源。幸运的是,有一个相关的编译器报警设 置可以使用。图3-3显示“Undeclared Selector”设置已被勾选,示例项目 “Essentials”里的这项设置也已被启用。 图2-3:启用构建设置(Build Setting)中的“Undeclared Selector”设置 接下去我们讨论如何停止对预定方法的调用。 9 以下代码会停止节点的所有选择器,包括那些已经在scheduleUpdate里面设置 了预定的选择器: [self unscheduleAllSelectors]; 以下代码会停止某个指定的选择器(假设选择器名称是updateTenTimesPerSecond): [self unschedule:@selector(updateTenTimesPerSecond:)]; 注:此方法不会停止scheduleUpdate中设置的预定更新方法。 还有一个挺有用的设置和停止选择器预定的方法。很多时候你需要在设置好的 预定方法里面停止调用某个指定的方法,同时因为参数和方法名可能发生变化, 你又不想重复相同的方法名和参数,这时你可以用以下的方法设置(预定的控 制器只会运行一次): -(void) scheduleUpdates { [self schedule:@selector(tenMinutesElapsed:) interval:600]; } -(void) tenMinutesElapsed:(ccTime)delta { // 用_cmd关键词停止当前方法的预定 [self unschedule:_cmd]; } _cmd关键词是当前方法的缩写。上述代码只会让tenMinutesElapsed方法运行一 次。实际上你也可以用_cmd来设置方法调用的预定。假设你需要调用一个方法, 这个方法会使用不同的时间间隔来调用,每次方法被调用以后,时间间隔都会 发生变化。你的代码看起来会是像下面这样: -(void) scheduleUpdates { // 像之前一样预定第一次更新 [self schedule:@selector(irregularUpdate:) interval:1]; } -(void) irregularUpdate:(ccTime)delta { // 首先,停止方法调用的预定 [self unschedule:_cmd]; // 这里我们用随机数来决定下次调用此方法需要经过的时间 float nextUpdate = CCRANDOM_0_1() * 10; // 然后用_cmd来代替选择器,用新的时间间隔来重新预定方法调用 [self schedule:_cmd interval:nextUpdate]; } 用_cmd关键词可以让你避免预定(schedule)或者停止预定(unschedule)错 10 误的方法。从长期来说是很有好处的。 最后一个预定方法调用的问题是安排更新方法的优先次序。请先看一下以下代 码: // 在A节点里 -(void) scheduleUpdates { [self scheduleUpdate]; } // 在B节点里 -(void) scheduleUpdates { [self scheduleUpdateWithPriority:1]; } // 在C节点里 -(void) scheduleUpdates { [self scheduleUpdateWithPriority:-1]; } 这可能需要些时间来消化。所有的节点还是在调用同样的-(void)update: (ccTime) delta方法。但是因为使用了优先级设置,C节点将会被首先运行。然 后是调用A节点,因为默认情况下优先级设定为0。B节点最后一个被调用,因为 它的优先级的数值最大。更新方法的调用次序是从最小的优先级数值到最大的 优先级数值。 你可能想知道什么时候会用到这个优先级功能。坦率地说很少会用到它。不过 按照我过去的经验,在某些极端情况下你可能需要用到这个功能,比如在进行 物理效果模拟之前或者之后,为参与模拟的对象添加力量。在宣布此项功能的 同时也提到了物理效果的更新说明了上述用处。有的时候,通常是在项目后期, 你可能发现了一个很奇怪的bug,这个bug是和时间的选择(timing)有关的, 这迫使你在完成所有的对象自我更新之后,运行玩家对象的更新方法。 直到你有一天需要用到优先级设置这个功能以解决特定的问题,现在你可以忽 略它。 场景和层 CCNode,CCScene和CCLayer这些类是没有视觉表现的。它们是在内部作为场景 图的抽象概念来使用的。CCLayer最典型的应用是把各个节点组织起来,还有接 收触摸输入和加速计输入的信息 - 前提是上述接收功能已被启用。 11 CCScene CCScene对象总是场景图里面的第一个节点。通常CCScene的子节点都是继承自 CCLayer。CCLayer包含了各个游戏对象。因为大多数情况下场景对象本身不包 含任何游戏相关的代码,而且很少被子类化,所以它一般都是在CCLayer对象里 通过+(id)scene这个静态方法来创建的。我已经在第二章谈到过这个方法,但 是我想通过以下代码来刷新一下你的记忆: +(id) scene { CCScene *scene = [CCScene node]; CCLayer* layer = [HelloWorld node]; [scene addChild:layer]; return scene; } 第一个创建场景的地方是在AppDelegate中aplicationDidFinishLaunching方法 结束处。你在那里用Director的runWithScene方法开始运行第一个场景: // 用以下代码运行第一个场景 [[CCDirector sharedDirector] runWithScene:[HelloWorld scene]]; 在其它情况下,用replaceScene方法来替换已有的场景: // 用replaceScene来替换所有以后需要变化的场景 [[CCDirector sharedDirector] replaceScene:[HelloWorld scene]]; 注:你可以在HelloWorld场景里顺利运行这些代码。一个新的HelloWorld实例 会被生成,替换掉原先的HelloWorld实例,相当于刷新场景。但是,不要把 self作为参数传给replaceScene方法以达到刷新场景的目的,那样做会让游戏 卡死! 场景和内存 当你替换一个场景时,新场景被加载进内存,但是旧的场景还没有从内存中释 放。这会让内存使用量在短时间内忽然增大。替换场景的过程很关键,因为很 多时候你会因为系统内存不够而收到内存警告或者导致程序崩溃。如果你在开 发过程中,发现游戏在场景转换过程中占用很多内存的话,你应该尽早和尽量 多的进行测试。 注:在你替换场景的时候,cocos2d会把自己占用的内存清理干净。它会移除所 有的节点,停止所有的动作,并且停止所有选择器的预定。我之所以提到这一 点,是因为我有时候看到开发者会直接调用cocos2d的removeAll方法,那是没 有必要的。你应该相信cocos2d的内存管理能力。 如果你在替换场景的时候使用过渡效果(transitions)的话,这个问题就更明 显了。在过渡的过程中,新的场景首先被生成,然后运行过渡效果,只有在过 渡效果完成以后,旧的场景才会被清理出内存。在创建场景的那个图层中添加 日志可以帮助你更好的了解你的场景。 12 -(id) init { if ((self = [super init])) { CCLOG(@"%@: %@", NSStringFromSelector(_cmd), self); } } -(void) dealloc { CCLOG(@"%@: %@", NSStringFromSelector(_cmd), self); // 总是在dealloc方法里调用[super dealloc] [super dealloc]; } 注意观察这些日志信息。如果你发现在场景转换过程中,dealloc里的日志信息 没有被发送出去的话,你碰到了大麻烦!你的整个场景都在内存泄漏,应该释 放的内存没有得到释放。这样的事情是不大可能由cocos2d本身导致的。大多数 情况下是由于错误的retain或者没有释放节点。 有件事情你永远都不应该尝试,那就是首先把一个节点添加到场景中作为它的 子节点,然后又自己把此节点retain下来。相反,你应该用cocos2d的方式来访 问创建的节点,或者至少是弱引用节点指针,而不是直接retain节点。只要你 让cocos2d来管理节点的内存使用,你就不会遇到麻烦。 推进(Pushing)和弹出(Popping)场景 在讨论转换场景的同时,我想提一下pushScene和popScene这两个来自Director 的方法。有时候它们会有些用处。他们的作用是在不从内存里移除旧场景的情 况下运行新的场景,目的是让转换场景的速度更快。但是这里有个问题:如果 你的场景很简单,同时互相分享内存,那么它们本身的加载速度就很快。而如 果你的场景很复杂,需要消耗很长时间加载,那么它们就会互相争抢宝贵的内 存 - 导致内存使用量迅速上升。 pushScene和popScene最大的问题是可以互相叠加。你可以推进一个场景,同时 运行一个新的场景。然后这个新场景推进另一个场景,而那个场景也会推进又 一个场景。如果你没有管理好场景的推进和弹出,最终你会忘记弹出场景,或 者将同一个场景弹出多次。更加糟糕的是这些场景都共享着同一块内存。 不过在有一个情况下pushScene和popScene很有用:如果你要在很多地方使用一 个通用的场景,比如包含改变音乐和声音音量菜单的“设置场景”。你可以推 进“设置场景”以显示它。“设置场景”的“回去”按钮则会调用popScene让 游戏回到之前的场景。不管你是在主菜单,游戏中,或是其它一些地方打开 “设置场景”,这个方法都能很好的工作。你从此不再需要跟踪“设置场景” 最后一次是在哪里打开的。 13 不过,你还是需要测试“设置场景”在各种情况下的表现,以确定在任何情况 下都有足够的内存可用。理想状态下,“设置场景”本身应该是很简单轻巧的。 用以下代码在任意一个地方显示“设置场景”: [[CCDirector sharedDirector] pushScene:[Settings scene]]; 如果你身处“设置场景”,但又想关闭“设置场景”时,你可以调用popScene。 这样你会回到之前还保留在内存里的场景: [[CCDirector sharedDirector] popScene]; CCTransitionScene 所有过渡效果的类都继承自CCTransitionScene。 注:我先在这里警告一下:在游戏里不是每个过渡效果都很有用,即使它们看 起来很好看。玩家们最关心的是过渡的速度。即使3秒钟他们都会觉得长。我设 置过渡效果的时间不会超过一秒,或者干脆完全不用。 你绝对要避免在转换场景是随机选择过渡效果。玩家们不关心这些。而作为开 发者,你可能对于过渡效果太兴奋了。如果你不清楚该为哪个场景转换使用哪 个过渡效果,那就不要用。换句话说,可以使用并不代表你一定要用。 虽然过渡效果的名称和需要的参数数量很多,但是过渡效果只给场景转换代码 增加了一行代码而已。以下是很流行的淡入淡出过渡效果:它在一秒内过渡到 了白色: // 用我们想要在下一步显示的场景初始化一个过渡场景 CCFadeTransition* tran = [CCFadeTransition transitionWithDuration:1 scene:[HelloWorld scene] withColor:ccWHITE]; // 使用过渡场景对象而不是HelloWorld [[CCDirector sharedDirector] replaceScene:tran]; 你可以把CCTransitionScene与replaceScene和pushScene结合使用,但是你不 能将过渡效果和popScene一起使用。 有很多种过渡效果可以使用,大多是和方向有关的,比如从哪个地方开始过渡 到哪个地方过渡结束。以下是目前可以使用的过渡效果和描述: 1. CCFadeTransition: 淡入淡出到一个指定的颜色,然后回来。 2. CCFadeTRTransition (还有另外三个变化): 瓦片(tiles)反转过来揭示场景。 3. CCJumpZoomTransition: 场景跳动着变小,新场景则跳动着变大。 4. CCMoveInLTransition (还有另外三个变化): 场景移出,同时新的场景从左边,右边,上 方或者下方移入。 5. CCOrientedTransitionScene (还有另外六个变化): 这种过渡效果会将整个场景翻转过来。 6. CCPageTurnTransition: 翻动书页的过渡效果。 7. CCRotoZoomTransition: 当前场景旋转变小,新的场景旋转变大。 8. CCShrinkGrowTransition: 当前场景缩小,新的场景在其之上变大。 14 9. CCSlideInLTransition (还有另外三个变化): 新的场景从左边,右边,上方或者下方滑 入。 10. CCSplitColsTransition (还有另外一个变化): 将当前场景切成竖条,上下移动揭示新场 景。 11. CCTurnOffTilesTransition:将当前场景分成方块,用分成方块的新场景随机的替换当前 场景分出的方块。 CCLayer 有时候在同一个场景里你需要多个CCLayer。你可以参照以下代码生成这样的场 景: +(id) scene { CCScene* scene = [CCScene node]; CCLayer* backgroundLayer = [HelloWorldBackground node]; [scene addChild: backgroundLayer]; CCLayer* layer = [HelloWorld node]; [scene addChild:layer]; CCLayer* userInterfaceLayer = [HelloWorldUserInterface node]; [scene addChild: userInterfaceLayer]; return scene; } 另一个方式是通过创建CCScene的子类,然后在各个场景的init方法中生成 CCLayer层和其它对象。 如果你有一个滚动的背景,背景上有个静止的框围绕着背景(上面可能包含一 些用户界面元素),这种情况下你可能需要在同一个场景中使用多个层。通过 使用两个分开的层,你可以调整背景层的位置来使其移动,同时前景层保持不 动。另外,根据层的z-order属性的不同,同一层的物体要么在另一层物体的前 面或者后面。当然,你也可以不用层而达到相同的效果。不过那样的话就要求 背景上的各个物体要分开移动。这样做非常没有效率。 和场景一样,层没有大小的概念。层是一个组织的概念。比如,如果你对一个 层使用动作,那么所有在这个层上的物体都会受到影响。这意味着你可以让同 一层上的所有物体一起移动,旋转和缩放。通常,如果你想让一组物体执行相 同的动作和行为,层是很好的选择。比如说让所有的物体一起滚动;有时候你 可能想让他们一起旋转,或者将他们重新排列然后覆盖在其它物体上面。如果 所有这些物体是同一个层的子节点,你就可以通过改变层的属性或者在层上执 行动作,来达到影响层上所有子节点的目的。 注:有人建议不要在同一个场景里使用过多的CCLayer对象。这是一个误解。使 用层和使用其它的节点一样,并不会因为使用多个层而降低运行效率。不过, 如果你的层接收触摸或者加速计事件的话就不一样了。因为接收处理外来事件 很耗费资源。所以,你不应该使用很多接收外来事件的层。比较好的处理方式 15 是:只使用一个层来接收和处理事件。如果需要的话,这个层应该通过转发事 件的方式来通知其它节点或类。 接收触摸事件 CCLayer类是用来接收触摸输入的。不过你要首先启用这个功能才可以使用它。 你通过设置isTouchEnabled为YES来让层接收触摸事件: self.isTouchEnabled = YES; 此项设定最好在init方法中设置。你可以在任何时间将其设置为NO或者YES。 一旦启用isTouchEnabled属性,许多与接收触摸输入相关的方法将会开始被调 用。这些事件包括:当新的触摸开始的时候,当手指在触摸屏上移动的时候, 还有在用户手指离开屏幕以后。很少会发生触摸事件被取消的情况,所以你可 以在大多数情况下忽略它,或者使用ccTouchesEnded方法来处理。 1. 当手指首次触摸到屏幕时调用的方法: -(void) ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent*)event 2. 手指在屏幕上移动时调用的方法: -(void) ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent*)event 3. 当手指从屏幕上提起时调用的方法: -(void) ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent*)event 4. 当触摸事件被取消时调用的方法: -(void) ccTouchesCancelled:(NSSet *)touches withEvent:(UIEvent*)event 取消事件的情况很少发生,所以在大多数情况下它的行为和触摸结束时相同。 很多情况下,你可能想知道触摸是在哪里开始的。因为触摸事件由Cocoa Touch API接收,所以触摸的位置必须被转换为OpenGL的坐标。以下是一个用来转换坐 标的方法: -(CGPoint) locationFromTouches:(NSSet *)touches { UITouch *touch = [touches anyObject]; CGPoint touchLocation = [touch locationInView: [touch view]]; return [[CCDirector sharedDirector] convertToGL:touchLocation]; } 上述方法只对单个触摸有效,因为我们使用了[touches anyObject]。为了跟踪 多点触摸的位置,你必须单独跟踪每次触摸。 默认情况下,层接收到的事件和苹果UIResponder类接收到的是一样的。 cocos2d也支持有针对性的触摸处理。和普通处理的区别是:它每次只接收一次 触摸,而UIResponder总是接收到一组触摸。有针对性的触摸事件处理只是简单 的把一组触摸事件分离开来,这样就可以根据游戏的需求提供所需的触摸事件。 更重要的是,有针对性的处理允许你把某些触摸事件从队列里移除。这样的话, 16 如果触摸发生在屏幕某个指定的区域,你会比较容易识别出来;识别出来以后 你就可以把触摸标记为已经处理,并且其它所有的层都不再需要对这个区域再 次做检查。 在你的层中添加以下方法可以启用有针对性的触摸事件处理: -(void) registerWithTouchDispatcher { [[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:INT_MIN+1 swallowsTouches:YES]; } 注:如果你把registerWithTouchDispatcher方法留空,你将不会接收到任何触 摸事件!如果你想保留此方法,而且使用它的默认处理方式,你必须调用 [super registerWithTouchDispatcher]这个方法。 现在,你将使用一套有点不一样的方法来代替默认的触摸输入处理方法。它们 几乎完全一样,除了一点:用 (UITouch *)touch 代替 (NSSet *)touches 作 为方法的第一个参数: -(BOOL) ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {} -(void) ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event {} -(void) ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event {} -(void) ccTouchCancelled:(UITouch *)touch withEvent:(UIEvent *)event {} 这里很重要的一点是:ccTouchBegan返回的是一个布尔值(BOOL)。如果你返 回了YES,那就意味着你不想让当前的触摸事件传导到其它触摸事件处理器。你 实际上是“吞下了”这个触摸事件。 接收加速计事件 和触摸输入一样,加速计必须在启用以后才能接收加速计事件: self.isAccelerometerEnabled = YES; 同样的,层里面要加入一个特定的方法来接收加速计事件: -(void) accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration { CCLOG(@"acceleration: x:%f / y:%f / z:%f", acceleration.x, acceleration.y, acceleration.z); } 你可以通过加速参数来决定任意三个方向的加速度值。 CCSprite CCSprite是最常用到的类。它使用图片把精灵(sprite)显示在屏幕上。生成 精灵最简单的方法是把图片文件加载进CCTexture2D材质里面,然后将它赋给精 灵。你必须把需要用到的图片文件放进Xcode的Resources组中,否则你的应用 17 程序将无法找到指定的图片文件: CCSprite* sprite = [CCSprite spriteWithFile:@”Default.png”]; [self addChild:sprite]; 我想问你个问题:你认为这个精灵会被系统放置在屏幕的哪个地方?可能和你 想的相反,精灵贴图的中心点和精灵的左下角位置是一致的。生成的精灵被放 置在(0,0)点,也就是屏幕的左下角。因为精灵贴图的中心点和精灵的左下 角位置一致,导致贴图只能显示一部份(也就是贴图的右边上半部份)。比如, 假设图片大小是80x30px,你必须将精灵移动到坐标(40,15)才能将精灵贴图 与屏幕的左下角完美对齐,从而看到完整的贴图。 乍看上去这样安排位置很不寻常,不过将贴图的中心点和精灵的左下角位置设 为一致有很大的好处。一旦你开始使用精灵的旋转或缩放属性,精灵的中心点 将会保持在它的位置上。 警告:iOS设备上的文件名是区分大小写的!在模拟器上测试时并不区分大小写。 但是在iOS设备上实际测试时,程序就会因为大小写错误而崩溃。 这个要求导致了很多让开发者头痛的问题,这也是另一个为什么要经常在设备 上做实际测试的原因。为自己确立一个文件命名规则,并且坚持用下去。对我 自己而言,我全部使用小写,词和词之间则用下划线分开。 定位点揭秘 每个节点都有一个定位点,但是只有当此节点拥有贴图时,这个定位点才有用。 默认情况下,anchorPoint属性设置为(0.5,0.5)或者贴图尺寸的一半。它是 一个抽象的因素,一个乘数,而不是一个特定的像素尺寸。 和你想的恰恰相反,定位点和节点的位置没有关系。虽然当你改变anchorPoint 属性的时候,你看到精灵在屏幕上的位置发生了变化。但那是错觉,因为节点 的位置并没有改变;改变的是精灵里贴图的位置! anchorPoint定义的是贴图相对于节点位置的偏移。你可以通过把贴图的宽和高 乘以定位点来得到贴图的偏移值。顺便提一下,有一个只读的 anchorPointPixels属性可以得到贴图的像素偏移值,所以你不需要自己计算。 如果设置anchorPoint为(0,0)的话,你实际上是把贴图的左下角同节点的位 置对齐了。以下代码会把精灵图片完美地同屏幕左下角对齐: CCSprite* sprite = [CCSprite spriteWithFile:@”Default.png”]; sprite.anchorPoint = CGPointMake(0, 0); [self addChild:sprite]; 注:如果你在使用别的游戏引擎时,习惯了把所有精灵定位点都设为0,0的话, 请不要在cocos2d里面这样做。这样做会引起很多麻烦,包括旋转和缩放,父节 点和子节点之间的相对位置,还有距离测试和碰撞测试。你要保证anchorPoint 18 在贴图的中央。相信我。 贴图大小 我要特别提一下贴图大小。目前可用于iOS设备的贴图尺寸必须符合“2的n次方” 规定,所以贴图的宽和高必须是2,4,8,16,32,64,128,256,512,1024。 在第三代设备上可以达到2048像素。贴图不一定是正方形的,所以8x1024像素 的贴图完全没有问题。 在你制作贴图的时候你要考虑到上述尺寸要求,比如在为精灵准备图片时。让 我们马上来看看最坏情况下会发生什么事情:假设你的图片尺寸是260x260,用 的是32位颜色。在内存里,贴图本来只占279KB左右的空间,但是现在却使用了 整整1MB。 这几乎是原尺寸四倍的内存占用,这是因为iOS设备要求任何贴图的尺寸必须符 合“2的n次方”规定。260x260像素的贴图到了iOS设备中以后,系统会自动生 成一张与260x260尺寸最相近的符合“2的n次方”规定的图片(一张512x512像 素的图片),以便于把原贴图放进这个符合规定的“容器”中。而这张512x512 像素的图片占用了1MB的内存空间。 为了解决这个问题,你唯一能够做的是确保任何制作的图片尺寸符合“2的n次 方”规定。260x260像素的图片其实应该做成256x256像素。这样就不会浪费这 么多的内存。如果你有设计师为你工作,你要确保她按照要求制作。 在第六章我会教你如何使用“纹理贴图集”来最大限度的解决这个问题。 CCLabel 当你需要在屏幕上显示文字的时候,CCLabel是最直接的选择。以下代码会生成 一个CCLabel对象用于显示文字: CCLabel* label = [CCLabel labelWithString:@"text" fontName:@"AppleGothic" fontSize:32]; [self addChild:label]; 如果你想知道iOS设备上有哪些TrueType字体可以使用的话,本章提供的 Essentials源代码里提供了一个字体列表。 从生成文字的内部原理来说,TrueType字体被用于CCTexture2D贴图上渲染出文 字。因为每次文字改变都会导致系统重新渲染一遍,所以你不应该经常改变文 字。重建文字标签的贴图非常耗时。 [label setString:@"new text"]; 如果你改变标签上的文字,文字始终是居中的。这是因为定位点的关系。你可 以通过改变anchorPoint属性将文字居左,居右,置顶或者放置在底部。以下代 码通过改变anchorPoint属性来排列对齐标签: // 右对齐 19 label.anchorPoint = CGPointMake(1, 0.5f); // 左对齐 label.anchorPoint = CGPointMake(0, 0.5f); // 置顶放置 label.anchorPoint = CGPointMake(0.5f, 1); // 放置在底部 label.anchorPoint = CGPointMake(0.5f, 0); // 使用实例:将标签放置在屏幕右上角 // 标签文字延展到左下方,并且在屏幕上总是可见 CGSize size = [[CCDirector sharedDirector] winSize]; label.position = CGPointMake(size.width, size.height); label.anchorPoint = CGPointMake(1, 1); 菜单 很快你就会需要一些可以让用户进行操作的按钮,比如进入下一个场景或者将 音乐打开或关闭的按钮。这里你使用CCMenu类来生成菜单。CCMenu只支持 CCMenuItem节点作为它的子节点。 列表3-2的代码展示了如何设置菜单。你可以在Essentials项目的MenuScene类 里找到这些菜单代码。 列表3-2:用文字和图片菜单制作cocos2d菜单 CGSize size = [[CCDirector sharedDirector] winSize]; // 设置CCMenuItemFont的默认属性 [CCMenuItemFont setFontName:@"Helvetica-BoldOblique"]; [CCMenuItemFont setFontSize:26]; // 生成几个文字标签并指定它们的选择器 CCMenuItemFont* item1 = [CCMenuItemFont itemFromString:@"Go Back!" target:self selector:@selector(menuItem1Touched:)]; // 使用已有的精灵生成一个菜单项 CCSprite* normal = [CCSprite spriteWithFile:@"Icon.png"]; normal.color = ccRED; CCSprite* selected = [CCSprite spriteWithFile:@"Icon.png"]; selected.color = ccGREEN; CCMenuItemSprite* item2 = [CCMenuItemSprite itemFromNormalSprite:normal selectedSprite:selected target:self selector:@selector(menuItem2Touched:)]; // 用其它两个菜单项生成一个切换菜单(图片也可以用于切换) [CCMenuItemFont setFontName:@"S
本文档为【第3章-基础知识】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑, 图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
该文档来自用户分享,如有侵权行为请发邮件ishare@vip.sina.com联系网站客服,我们会及时删除。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。
本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。
网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。
下载需要: 免费 已有0 人下载
最新资料
资料动态
专题动态
is_824540
暂无简介~
格式:pdf
大小:1MB
软件:PDF阅读器
页数:0
分类:互联网
上传时间:2012-03-06
浏览量:6