zoukankan      html  css  js  c++  java
  • 游戏编程从哪里开始呢

    转自Jamie Gotch:Fieldrunners底层技术

    从头创建一个游戏可能相当困难。你早先做出的很多决策都可能会对最终产品产生巨大影响。

    因此,在具体编写代码之前先创建大致的行动计划非常重要。为了创建 这个计划,需要明确要

    创建什么类型的游戏并确定自身需求。

    1.确定需求

             游戏形状各异,大小不同。特别是休闲型游戏,它的需求跨度更广。例如,牌类游戏通常

        不需要以很高的帧速率绘制屏幕,也不需要同时播放多种音效。首先了解应用需求,从长期来

        讲可以节省大量时间,也可以为你免去很多麻烦。下面这些问题可以帮助你确定游戏的具体需

        求。

            ■你的游戏非常依赖于3D 图形吗?它是基于sprite 还是使用简单的2D 形状?

            ■你的游戏需要很多同时在屏幕上不断移动的图形元素吗?或者,是不是同时只有一个或

              两个移动的图形,而其余的图形元素大多数情况下都只是原地不动?

            ■你的游戏同时使用多种音效 (比如,爆炸、开火、尖叫)吗?或者,只是每隔几秒才播

              放一次声音?

     2.你的具体需要可以指示图形和声音编程将使用的技术。

            (1)OpenGL ES 与Quartz

            很多人开始开发游戏时问我最多的一个问题就是,到底使用OpenGL 还是Quartz。

            ■Quartz 更易于使用,因为它的大多数概念都来自高中几何。与之不同,OpenGL 编程使

              用了一些更复杂的概念,如矩阵和向量。

            ■Quartz 对于不需要以高帧速率重绘屏幕和只需要2D 图形的应用非常适用。OpenGL 设

              计时则特别考虑了3D 图形。iPhone GPU 支持堪与Sony PS2 和PSP 媲美的高速图形。

            ■Quartz 使用的电池电量要少于OpenGL,因为OpenGL 更多地使用了iPhone GPU.

            OpenAL、AVFoundation 与CoreAudio

            (2)类似地,谈到声音和音乐播放时,也有很多选择,包括OpenAL、AVFoundation 和

          CoreAudio 框架。

            ■AVFoundation使用更简单。只需加载你的声音文件,告诉播放器播放、暂停或停止播

              放声音。可以创建多个AVAudioPlayer 实例,在每个实例上加载一个不同的声音,并指

              示各个实例播放相应的声音。另一方面,这个框架不能保证播放声音的同步性。

            ■CoreAudio 为你提供了更多控制,允许以很细的粒度同步声音播放的时间,不过它的

              API 很复杂。

         ■OpenAL 要求使用矩阵和向量在3D 空间中指定声音位置,这类似于用OpenGL 指定图

           形的位置和组成。这使它成为三者之中最复杂的一个API。

    3.学习Object-c

      我唯一的建议是不要放弃,要不断地去查找教程、图书和视频课程。对于之前你没有涉及

    的资源,可以从中学到的内容绝对是难以预料的。我还建议你不要只考虑iPhone 资源,还可以

    看看Mac OS X 编程资源 (如Mark Dalrymple 和Scott Knaster 所著的《Objective-C 基础教程》),

    还有Cocoadevcentral.com 网站。它们的编程环境都是Xcode,而且使用相同的语言和设计模式。

    Mac OS X 编程资源有一个好处,Mac OS X 编程团队已经有10 年的历史,有更多经过实践检验

    的提示和技巧可供学习。

      从众多资源学习Objective-C 编程、Cocoa 框架和Xcode 时,你还会意识到Objective-C 上

    内存管理方法的重要性。下面就来研究这个问题:

         研究内存管理方法

        iPhone 上内存管理的重要性再强调也不为过。写这本书时,iPhone 和iPod Touch 有128

    MB 的内存。不过,大多数内存都被操作系统和后台处理所占用,如查看邮件和与Exchange 同

    步日程表。在iPhone 上,后台处理还包括SMS 消息和来电通知。即使是最好情况下,你的游

    戏也只有70 MB 的内存可用。如果大量使用iPhone 来浏览和发送邮件,可用内存可能更少。

       很多去年转向iPhone 和Objective-C 编程的开发人员有过Java 和C# 经验,或者使用过动

    态语言 (如Ruby 或PHP )。这些环境都内置有垃圾回收,将丑陋的内存管理细节对开发人员隐

    藏。这通常来说是件好事。

        另一方面,Objective-C 仍要求开发人员手动地处理内存管理,像C 和C++ 编程中一样。不过

    最新ios sdk5.0以上都可以使用ARC机制,可以让系统帮你你管理内存。

        iPhone 上的内存管理是开发人员遇到的最常见问题之一。即使老练的C 和C++ 程序员也可能被

    Objective-C 完成内存管理的做法搞糊涂。

        Objective-C 使用一种称为持有计数 (retain  counts)的内存管理方法。第一次创建一个对象
    时,会有一个名为持有计数的属性,该属性初始时设置为1。在这个对象的生命期中,每次将
    这个对象赋值一个数据结构时(如一个集合),持有计数就会增1。还有一些情况下,你需自行
    增加持有计数,如把对象传递到代码的其他部分时。

        使用完所持有的对象时,必须记住将其释放,否则就会存在所谓的内存泄漏(memory leak )。

    Objective-C 在持有计数设置为0 之前不会回收这个对象使用的内存。如果没有适当地 管理持有计数,

    就会耗尽iPhone 上的可用内存,而操作系统会在最不合适的时候将你的游戏退出。

        理解自动释放

      在Objective-C 中利用自动释放 (autorelease)池可以更容易地管理内存。使用自动释放池 时,

    会把标为自动释放的对象增加到这个池中。以后释放这个池时,其中的所有对象也会被释 放。如果没有使用自动释放池

    ,则要由开发人员负责在不再需要这些对象时手动地释放各个对象。可以参考图1 的图示。

    说明 在iPhone OS 上 由于应用在一个内存受限的环境中执行 如果方法或代码块中会创建很多对象,

    那么不鼓励这些方法和代码块使用自动释放池,要在可能的时机明确地释放对象 另外 使用Application Kit 开发应用时

    不需要建立自动释放池 因为Application Kit 会在应用事件周期内自动建立一个自动释放池。

      除了学习Objective-C 和iPhone 上的内存管理很有难度,异常也让我很费解。我发现了一
    个很方便的技巧,它非常有助于调试Objective-C 代码中的异常。

      出现异常时中断

        这个方便的技巧就是设置一个断点捕获异常。Objective-C 通过调用一个Objective-C 运行
    时函数 (名为obj_exception_throw)抛出异常。通过在这个函数上设置一个断点,允许你
    检查发生崩溃时的调用栈来跟踪崩溃。这是一个很好的帮手!不必再费劲地猜测EXC_BAD_
    ACCESS 错误的原因。调试工具会显示直到出现异常那一刻的整个调用栈,你可以直接跳到导
    致崩溃的那一行问题代码。
        要设置断点,打开Breakpoints 编辑器,创建一个新的断点,名为objc_exception_ throw,如图2 所示。
      

    4. A* 算法

        A* 算法非常强大,用途也很广。它主要用于角色在一个布满障碍的地图上导航,不过也可

    以用作其他用途。A* 在Fieldrunners 的实现中就有用到。

        很多书已经介绍了A* 算法的复杂细节,所以本章不再过多深入。本章将简单介绍这个算

    法,帮助你了解可以在什么时候以及在哪里使用这个算法。

        从高层看A* 算法

        A* 算法会在避开障碍的同时寻找两个节点之间的最短路径。尽管A* 算法不要求节点必须

    采用某种特定的布置,但是要了解它是如何工作的,最好将节点按一种网格图形摆放。请看图3

    的例子。白方格表示可走的方块,有颜色的方块表示不同类型的障碍。

      地图网格上的一些节点表示可以移动游戏单元的位置。在一个驾驶游戏中,这些位置可能 是道路、人行道

    或小巷。其他节点是障碍或者地图中此时此刻不能到达的部分。在一个驾驶游 戏中,另外的这些节点可能是大楼、公园周围的篱笆,或者一条河。

          地图网格只是用户在屏幕上看到的图形的一种表示,它不是屏幕上的图形。这些地图网格 节点可以是城堡高墙、陡峭的悬崖,或者游戏单元根本无法游过的深水。它们也可能只是场景, 只是用来在地图上简单“显示”或者用来填充地图。不过在游戏网格中,它们在Objective-C 或  C++ 中会存储为对象,在用C 编写的游戏中存储为表示对象类型的整数, 或者如果开发人员确 实很执着,也可能存储为一个C 函数指针查找表中的一个索引,从而知道如何在屏幕上绘制适 当的图形。按模型—视图—控制器设计模式的说法,游戏网格就是游戏的模型,屏幕上的图形则 是视图。

      如何确定游戏单元不能经过哪些节点,这取决于你开发的是何种类型的游戏。例如,在一 个战争模拟游戏中,有些节点可以让某些单元经过但禁止其他类型的单元经过 (也就是说,直 升机可以到处移动,但坦克不能跨越深湖或翻越陡峭的悬崖)。需要在设计各个级别时创建地图 网格,然后把级别数据结合到游戏代码中加以改进。

          A* 技术实现


          A* 算法作用于游戏网格,网格上的每个方块表示空间中的一个具体位置。讨论A* 时,网 格方块通常称为节点。A* 维护两个单独的列表来跟踪寻找到达目标的路径过程中已经检查过的节点和尚未检查的节点。这两个列表分别称为封闭列表 (Closed list )和开放列表 (Open list )。

         另外,在一些实现中还有一个可选的移动列表 (Moves list ),算法在这个列表中存储从起 始节点到目标节点的所有节点。这个移动列表在游戏单元轮流移动的游戏中很常见。不过,在 游戏单元“实时”移动的游戏中,如驾驶游戏中的对手AI,游戏AI 只是提前看几步。这种AI 在很短的周期内运行A*,找到合适的位置就移动到该位置。在这样的情况下,移动列表就没有 必要了。

         算法第一次执行时,它会清空封闭列表并在游戏网格上移动,从起始节点开始,一次一个 元素地逐步移向目标,为各节点及其相邻节点指定一个加权的目标分数。这个加权的目标分数 就是一个数,用于确定一个特定的节点是否比它周围的其他节点更接近目标。很多算法变种就 针对于如何计算这个加权目标分数,另外一些变种则主要强调优化网格的初始扫描。

         一旦计算出节点的目标分数,算法将起始节点放入开放列表。每次迭代中,算法会把有最佳目标分数的节点从开放列表删除。这个节点成为当前节点,并得到检查。如果当前节点指示 目标位置,则停止处理,因为你已经到达目标。如果当前节点不表示目标位置,则检查当前节点的相邻节点。如果在封闭列表中没有找到任何相邻节点,则确定它是否离目标更近。如果确
    实离目标更近,就把它增加到开放列表。不过,如果相邻节点在封闭列表中,则将其忽略。所有相邻节点都得到检查后,当前节点放入封闭列表,指示它已经得到完全检查。它还被增加到 可选的移动列表。如果在找到目标之前开放列表就已经为空,这说明根本没有路径能够到达目标。请参考代码清单1,其中给出了这个算法的一个例子。

        代码清单1 A* 算法伪代码

        清空封闭列表
        计算加权目标分数
        将开始节点放入开放列表

        While 开放列表非空
         当前节点= 开放列表中与目标最接近的节点
         If 当前节点与目标相同
           将当前节点增加到移动列表
           完成。退出循环
         End if

         If 当前节点上方的节点不在封闭列表中
           If 它不是一个障碍,而且离目标更近
              如果尚未在开放列表中,则将其增加到开放列表
           Else
              将它增加到封闭列表
           End if
         End if
         If 当前节点下方的节点不在封闭列表中
           If 它不是一个障碍,而且离目标更近
              如果尚未在开放列表中,则将其增加到开放列表
           Else
              将它增加到封闭列表

        End if
             End if
             If 当前节点左边的节点不在封闭列表中
               If 它不是一个障碍,而且离目标更近
                  如果尚未在开放列表中,则将其增加到开放列表
               Else
                  将它增加到封闭列表
               End if
             End if
             If 当前节点右边的节点不在封闭列表中
               If 它不是一个障碍,而且离目标更近
                  如果尚未在开放列表中,则将其增加到开放列表
               Else
                  将它增加到封闭列表
               End if
             End if
             
             从开放列表删除当前节点
             将当前节点增加到封闭列表
             将当前节点增加到移动列表


            End while循环
      一旦到达循环末尾,移动列表将包含算法认为指向目标的路径上的所有节点。如果移动列表不包含目标节点,说明到达目标只有一个部分解。如果移动列表只包含起始节点,说明没有任何解。

        A* 是一个用途很广的算法。尽管它主要在游戏编程中用于AI 寻路,不过也可以修改后用在不需要寻路的游戏中,如需要扫描游戏面板查找得分条件的游戏。俄罗斯方块游戏就是这样 一类游戏。下一节,本书首席作者PJ Cabrera 讨论了这样一个游戏的一种实现。


        Puyo :
           本节中将讨论一个简单的游戏实现。我要讨论的是Puyo 游戏,这是一个俄罗斯方块游戏。 尽管看起来很简单,但你会发现,这个游戏实现起来很复杂。这个实现使用了一个简化的A* 算法来寻找和跟踪同色方块的匹配。 游戏的对象很简单。随机着色的方块成对出现在屏幕上方并逐渐落至底部。这些方块会一 直下落,直至碰上一个不可移动的对象 (如另一个方块或游戏区底部)。玩家要控制这对方块, 必须按一定策略定位和旋转它,来清除占据游戏区的其他方块。为达到这个目的,玩家必须让 4 个或更多同色方块连结起来。只有水平或垂直相邻的方块才能连结在一起,对角线相邻的方块不算数。作为连结的部分的各个方块会从游戏区删除。所有失去下层“地基”的方块开始下落,直到找到另一个可以放置的表面。

       在最简单的Puyo 实现中,没有获胜游戏条件。你只是不断地玩,直至游戏区被方块占满, 屏幕上方再没有空间可以放置方块为止。此时游戏结束。参考图4,这里给出了实际运行这个游戏的一个切屏图。
      

      图4 实际运行的Puyo 游戏。在这个切屏图中相邻的一组 (4 个红色)方块正在消失

        5.基本代码设计

        Puyo 游戏的这个实现使用了一些简单的数据结构。游戏面板是一个包含Puyo 对象的简单 的C 数组。为力求简单,共有13 行,每行有10 个Puyo,不过这个游戏代码完全可以处理两倍于此甚至更多的Puyo,而不会出现丢帧。

        Puyo 是我创建的一个Objective-C 类的实例,这个类名为Puyo,它有多个属性:游戏面板 上的位置、Puyo 的颜色、Puyo 是否卡在另一个Puyo 上或者位于游戏面板底部,以及Puyo 是否已经由扫描同色相邻Puyo 组的代码标志为消失。

        扫描游戏面板寻找相邻Puyo 组的算法是A* 算法的一个简化版本。这里并不是查找从点A 到点B 的一条路径,这个A* 算法会在水平方向查看游戏面板的每一行,并在垂直方向查看每一列。发现同色的相邻Puyo 组时,A* 算法会把它们放在一个集合中,这就是A* 算法的开放列表。一旦开放列表中有4 个或更多同色的Puyo,则把这些Puyo 增加到另一个列表,对应于 A* 中的移动列表。
         代码剖析

             这个游戏使用Objective-C 实现,这里使用了一个面向iPhone 的游戏库,名为Cocos2D。 这个易学的游戏库使你可以忽略技术细节重点关注游戏的底层逻辑。不过,需要了解技术细节 时你必能有所收获。

             代码已经在本书的网站上提供,所以我不打算逐行讲述。这里只介绍最重要的代码。这个
    游戏有关于Puyo,所以先从Pugo类开始。

             Puyo 类

             Puyo类是屏幕上下落方块的代码表示。代码清单2 显示了Puyo.h 文件中Puyo类的公共接口。

       代码清单2 Puyo 类的公共接口

             #import "cocos2d.h"

             @interface Puyo : Sprite {
                     int boardX, boardY;
                     int puyoType;
                     BOOL stuck;
                     BOOL disappearing;
             }

             @property int boardX;
             @property int boardY;
             @property int puyoType;
             @property BOOL stuck;
             @property BOOL disappearing;

             + (Puyo *) newPuyo;
             - (void) moveUp;
             - (void) moveDown;
             - (void) moveLeft;
             - (void) moveRight;

             @end

             // 根据游戏面板坐标
             // 定义屏幕上puyo位置的宏
             #define COMPUTE_X(x) (abs(x) * 32)
             #define COMPUTE_Y(y) abs(480 - (abs(y) * 32))
             #define COMPUTE_X_Y(x,y) ccp( COMPUTE_X(x), COMPUTE_Y(y) )

      Puyo类继承自Cocos2D 类Sprite,简单地来讲,这说明你可以加载一个图形并在屏幕上 的任意位置显示它,可以对图形应用透明度,还可以应用Cocos2D 为我们提供的其他特效。我 扩展了Sprite 类,增加了“基本代码设计”一节中讨论的属性:游戏面板上的列和行位置、一 个Puyo 类型整数 (用来确定是否有其他Puyo 实例有相同颜色),Puyo 是否卡住以及是否正在消失。我还增加了一个名为newPuyo 的构造方法和4 个实例方法,这些实例方法可以一步完成 Puyo 在屏幕上的移动并更新游戏面板坐标。

      在Puyo.h 的最后,我创建了一些C 宏将游戏面板行和列坐标转换为屏幕位置,它们将由类 实现使用。

         下面就来看Puyo类的实现,如代码清单3 所示。这个代码在文件Puyo.m 中。

      代码清单3 Puyo.m类的实现

        #import "Puyo.h"

        @interface Puyo (private)

        - (void) initializeDefaultValues;
        - (void) redrawPositionOnBoard;

        @end

        @implementation Puyo

        @synthesize puyoType;
        @synthesize stuck;
        @synthesize boardX;
        @synthesize boardY;
        @synthesize disappearing;

        在代码清单最上面导入了头文件来定义接口。导入Puyo接口定义之后,紧接着为Puyo 类 定义了一个类别,这里命名为private。在这里放置了我希望Puyo类中保持私有的所有内部 方法定义。在C++ 中可以使用private关键字,不过Objective-C 没有私有方法。在类的.m 文件最上面定义一个类别是Objective-C 中完成方法隐藏的一个常用模式。把它命名为private
    则是有经验的Objective-C 开发人员流传下来的一个传统。

        private类别定义结束后就开始了Puyo类的实现。这里要生成类公共接口中定义的所有属性。生成属性意味着编译器会在后台生成魔法代码来设置和检索这些属性的值,而且还允许你使用简化语法定义或检索这些属性的值。处理类的实现时,这种简化语法有利于更好地编写 代码。在这个游戏逻辑代码中(关于Puyo 的处理)我会展示这一点。

        下面是newPuyo构造方法的实现,如代码清单4 所示。

      代码清单4 Puyo类的实现细节:newPuyo构造方法

      + (Puyo *) newPuyo {
                 NSString *filename = nil, *color = nil;
                 Puyo *temp = nil;

                 int puyoType = random() % 4;

                 switch (puyoType) {
                     case 0:
                         color = @"blue";
                         break;
                     case 1:
                         color = @"red";
                         break;
                     case 2:
                         color = @"yellow";
                         break;
                     case 3:
                         color = @"green";
                         break;
                     default:
                         color = nil;
                         break;
                 }

         if (color) {
                     filename =
                          [[NSString alloc]
                             initWithFormat:@"block_%@.pvr", color];
                     temp = [self spriteWithFile:filename];
                      [filename release];

                      [temp initializeDefaultValues];
                      [temp setPuyoType: puyoType];
                 }
                 return temp;
             }

      创建Puyo实例的代码绘制一个介于0 和3 之间的随机数,取决于结果来加载相应的图形。
    Puyo 方块是32 像素宽、32 像素高的图像。包括绿色、黄色、蓝色和红色Puyo 的图像。

      如果查看获取Puyo 颜色相应文件名的那一行代码,你可能会注意到,这里加载了一个扩展名为.pvr 的奇怪文件。这些是PVR              (PowerVR )文件,而PVR 文件是特别针对iPhone 和iPod Touch 的GPU 进行了优化的一种文件类型。这些文件已经压缩,与其他类型的图像文件相比, 在iPhone 的内存中所占空间不到其四分之一。

      我会尽可能使用这种文件,因为GPU 会与iPhone 平台中的系统分享最多24  MB 的内存。这说明,如果没有对图形使用GPU,你的游戏可能最多会少24  MB 的可用内存。(例如,基本 UIKit 应用不存在这个问题)。通过使用PVR 文件让图形尽可能少占用GPU  RAM,意味着可 以为游戏的其他部分留出更多可用内存。PVR 文件唯一的缺点是它们必须基于正方形图像来创 建,图形大小必须是2 的幂。这些Puyo 图形都是32 ×32 像素,所以对这个游戏可以创建PVR文件。


      说实在的,这个游戏任何时刻只会一次加载4 个小Puyo 图形和一个320 ×480 像素的背景图形,根本没有达到GPU 的内存上限。不过,我希望展示和解释PVR 文件,从而鼓励开发人员使用这种类型的文件。创建PVR 文件有很多方法:可以使用PowerVR 的仅面向Windows 的 GUI 工具,或者Apple 随iPhone SDK 提供的converttool 命令行工具 (参见本书第4 章)。

        下面继续分析Puyo类实现。代码清单5 显示了两个私有方法:initializeDefault- Values 和redrawPositionOnBoard。

      代码清单5 Puyo类中initializeDefaultValues 和redrawPositionOnBoard 方法的实现

        - (void) initializeDefaultValues {
            [self setTransformAnchor: ccp(0,0)];
            [self setPosition: ccp(0,0)];
            [self setOpacity: 255];
            [self setStuck: NO];
            [self setDisappearing: NO];
            [self setBoardX: 0];
            [self setBoardY: 0];
        }

        - (void) redrawPositionOnBoard {
            [self setPosition: COMPUTE_X_Y(boardX, boardY)];
        }

      方法initializeDefaultValues用于将Puyo 中的所有属性设置为一个合适的默认值。 第一个属性是Sprite 的变换锚点。在Cocos2D 中,变换锚点是Cocos2D 用来在屏幕上旋转对象的一组坐标。在Sprites 中,默认的变换锚点在图像中心,我把它设置到图像的左下角,因为这样可以更容易地根据游戏面板坐标计算屏幕位置。

        位置属性是指屏幕上的位置,而不是游戏面板上的位置。不透明度 (opacity )与透明度 (transparency )正相反。不透明度值为255 表示Puyo 完全不透明,也称为不透明(opaque)。其他属性的含义应该是显而易见的。

         在前面的代码中,你可能已经注意到这里有一种称为ccp 的函数用来设置变换锚点和位置属性。  ccp实际上是Cocos2D 定义的一个方便的宏,表示“Cocos 点”(Cocos Point )。具体来讲,变换锚点和位置属性是一个C 结构,其中有一个对应X坐标的元素,还有一个对应 Y 坐标的元素。利用ccp宏,使得创建这种结构的实例相当简洁。这类似于Core Graphics 宏CGPointMake。

      方法redrawPositionOnBoard    对应游戏面板 X和Y 的坐标来设置Puyo 的屏幕位置。

        接下来是Puyo类实现的最后一部分。代码清单6 显示了根据游戏面板位置移动Puyo 所用 方法的实现。

       代码清单6 在游戏面板上移动Puyo 的方法的实现

             - (void) moveRight {
                 boardX += 1;
                  [self redrawPositionOnBoard];
             }

             - (void) moveLeft {
                 boardX -= 1;
                  [self redrawPositionOnBoard];
             }

             - (void) moveDown {
                 boardY += 1;
                  [self redrawPositionOnBoard];
             }

             - (void) moveUp {
                 boardY -= 1;
                  [self redrawPositionOnBoard];
             }

      这些方法只是将相应游戏面板坐标减1 或加1,并在其新的屏幕位置上重绘Puyo。Puyo类实现到此结束。

      接下来,分析启动游戏的代码:PuyoCloneAppDelegate类。

             PuyoCloneAppDelegate 类
        PuyoCloneAppDelegate可以实现很多方法,执行你的iPhone 应用期间会由Cocoa Touch  调用这些方法。这些方法可以让你知道代码已经完成启动、内存过低,以及iPhone OS 需要中 断你的程序 (因为接到一个电话或SMS 短信)。下面来看这些方法的实现,如代码清单7 所示。

         代码清单7 PuyoCloneAppDelegate类的实现细节

      -(void) applicationWillResignActive:(UIApplication *)application
             {
                  [[Director sharedDirector] pause];

                 UIAlertView *alert = [[UIAlertView alloc]
                     initWithTitle:@"Resuming Game"
                     message:@"Click OK to resume the game"
                     delegate:self
                     cancelButtonTitle:@"OK"
                     otherButtonTitles: nil];
                  [alert show];
                  [alert release];
             }

             - (void)alertView:(UIAlertView *)alertView
                 clickedButtonAtIndex:(NSInteger)buttonIndex
             {
                  [[Director sharedDirector] resume];
             }
         

      用户运行应用时如果接到一个电话、SMS 短信或者其他任意通知,就会调用applicationWillResignActive:方法。可以使用这个方法暂停游戏,保存玩家的进度。不过,在这 个简化的例子中,并未保存游戏状态。相反,这里要求Cocos2D Director 暂停游戏,并创建一个 UIAlertView让用户继续游戏。用户点击警告中的OK 按钮时,alertView:clickedButtonAtIndex:方法会得到调用。这个方法将恢复游戏。

      AppDelegate 协议定义了一个方法applicationDidFinishLaunching:,iPhone OS 完成应用的加载时会调用这个方法。正是从这里游戏真正开始。代码清单8 显示了applicationDidFinishLaunching:方法的实现。
      

      代码清单8 PuyoCloneAppDelegate类中applicationDidFinishLaunching:方法的实现

        - (void)applicationDidFinishLaunching:(UIApplication *)application
        {
            // 初始化窗口
            window =  [[UIWindow alloc] initWithFrame:  [[UIScreen mainScreen] bounds]];
            [window setUserInteractionEnabled:YES];
            //[window setMultipleTouchEnabled:YES];

            // 初始化Cocos2D Director
            //[Director useFastDirector];
            //[[Director sharedDirector] setLandscape: YES];
            [[Director sharedDirector] setDisplayFPS:YES];

            // 将窗口关联到Cocos2D Director,并使之可见。
            [[Director sharedDirector] attachInWindow:window];
            [window makeKeyAndVisible];

            // 指定随机数生成器种子。
            struct timeval tv;
            gettimeofday( &tv, 0 );
            srandom( tv.tv_usec + tv.tv_sec );

            // 运行GameScene启动游戏。
            [[Director sharedDirector] runWithScene: [GameScene node]];
        }

        这个方法首先创建UIWindow实例,它将显示游戏。窗口设置为包括整个屏幕,并响应用户交互,也称为触控事件。这里已经把启用多指触控的代码注释掉,不过实验这个游戏代码时你可能希望去掉注释来启用多指触控。
      接下来,初始化Cocos2D Director 。Cocos2D Director 是一个单例类,用于显示游戏屏幕。 这里已将请求使Cocos2D FastDirector 的调用注释掉,因为这个游戏并不需要它。FastDirector会大量地运行Cocos2D 显示循环,挤占更多iPhone 处理能力而影响执行Cocos2D 游戏的性能。

      类似地,我还将横向显示模式的设置注释掉,因为这是一个纵向模式的游戏。我在屏幕左下角启用了FPS     (Frames Per  Second,每秒帧速率)的显示。可以把它注释掉或者将设置改为NO,之后将不再显示FPS 数。

      //将window 关联到Cocos2D Director,并使之可见
            [[Director sharedDirector] attachInWindow:window];
            [window makeKeyAndVisible];

      
       一旦初始化了Cocos2D Director,它将Director关联到这个方法开始部分创建的UIWindow实例。这就告诉Cocos2D Director 使用这个窗口来完成显示。然后设置窗口“关联并可见”。Cocoa Touch 以这种方式让窗口接受输入并显示其内容。
       // 指定随机数生成器种子
            struct timeval tv;
            gettimeofday( &tv, 0 );
            srandom( tv.tv_usec + tv.tv_sec );

      置窗口可见之后,要为标准C 库随机数生成器函数srandom指定种子,这里使用了由当前时间得出的一个数作为种子,当前时间通过标准C 库函数gettimeofday得到。这说明,任意两次游戏中生成的Puyo 序列都不会重复,除非你在一天中的同一时刻 (精确到毫秒)运行这两个游戏。这样完全可以保证我需要的随机性。

              //运行GameScene 来启动游戏
            [[Director sharedDirector] runWithScene: [GameScene node]];

          最后,实例化GameScene类,并要求Cocos2D Director 运行这个实例。现在游戏真正开始。下面来分析GameScene

    类。

      GameScene 类

          场景是一个Cocos2D 抽象,用来包含任意特定时刻屏幕上的不同元素。一个Cocos2D 游戏由一个或多个Scene类实例组成。启动游戏时,设置Cocos2D 并让它运行你的第一个场景。有时,游戏逻辑确定当前场景结束,它会告诉Cocos2D 运行另一个场景。

          不过,“场景”这个名字会让人有些困惑。Cocos2D 场景并不表示仅限于创建电影式的RPG 或图形冒险类游戏。场景可以很简单,可能只包含背景图形和一些正在下落的Puyo。可以认为场景实例只是一个容器,其中包含游戏特定部分中显示在屏幕上的所有内容。游戏从一个部分“转移”到另一个部分时(比如,执行主游戏逻辑,返回到主菜单,或者显示高分),就要 实例化不同的场景并让Cocos2D 运行这些场景。
      

      在这个游戏中只有一个场景,即GameScene类。我的Puyo 游戏确实相当简单。运行这个游戏时,会直接进入主游戏逻辑。这里没有主菜单,没有高分,也没有帮助屏幕。这只是作为一个代码示例,可以让你从中学习游戏开发,而不是作为待售的完备游戏产品。最后的细节将作为练习留给读者来完成,这里只强调游戏逻辑。

      如果查看GameScene.h 文件,会看到GameScene类继承自Cocos2D  Scene类,而且没有指定任何新的属性或方法。Cocos2D  Scene实例和GameScene实例之间的具体区别在于init 和dealloc方法的实现。下面来分析GameScene类的实现细节,见代码清单9,这里给出了

      文件GameScene.m 。

      代码清单9 GameScene类的实现细节

        #import "GameScene.h"
        #import "GameLogicLayer.h"

        @implementation GameScene
        - (id) init {
            self = [super init];
            if (self != nil) {
                Sprite *bg = [Sprite spriteWithFile:@"background.png"];
                 [bg setPosition:ccp(160, 240)];
                 [self addChild:bg z:0];

                Layer *layer = [GameLogicLayer node];
                 [self addChild:layer z:1];
            }
            return self;
         }
      

      - (void) dealloc {

             [self removeAllChildrenWithCleanup:YES];
             [super dealloc];
         }

        @end

       GameScene类导入了GameScene头文件,之后又导入了GameLogicLayer头文件。后面 将会讨论GameLogicLayer,解释完GameScene后再来考虑它。

           在init方法中,GameScene 创建了一个Sprite 对象,它会加载背景图像。然后, GameScene设置这个Sprite 的屏幕位置为屏幕中心。最后,它将这个背景sprite 增加到场景。 然后,再实例化GameLogicLayer类,把它增加到场景。

      注意, Scene  类的  addChild  方法有一个名为z 的参数。这表示增加到场景的不同元素的相对深度。 z参数是可选的,不过使用这个参数时,有最高z 值的元素会显示在 z值较低的元素之上,这称为深度排序,在这里,实际上表示GameLogicLayer类所显示的元素会显示在背景图像之上。

      在dealloc方法中,要删除增加到场景的所有对象,表明你希望Cocos2D 释放对象来完成“清理”,然后必须调用超类 (也就是派生GameScene类的Cocos2D Scene类)的dealloc方法。

      GameLogicLayer 类

          顾名思义,GameLogicLayer类就是编写所有游戏逻辑的地方。GameLogicLayer实例检测触控并进行解释,然后建立一个运行游戏逻辑的方法,这个方法每秒调用60 次。游戏中发生的一切都是这两个动作的直接结果。下面来分析文件GameLogicLayer.h 中的GameLogicLayer接口,如代码清单10 所示。

      代码清单10 GameLogicLayer类的接口

             #import "Puyo.h"

             @interface GameLogicLayer : Layer {
                enum touchTypes {
                    kNone,
                    kDropPuyos,
                    kPuyoFlip,
                    kMoveLeft,
                    kMoveRight
                } touchType;

             #define kLastColumn 9
             #define kLastRow 12

                //游戏面板宽是一个Puyo宽的10倍,
                //高是13行Puyo 的高
                Puyo *board[kLastColumn + 1][kLastRow + 1];
                Puyo *puyo1, *puyo2;
                int frameCount;
                int moveCycleRatio;

       这个代码首先导入Puyo类头文件,因为下面将针对这个类中的类型声明一些变量。然后, 创建一个枚举类型来定义游戏逻辑所知道的不同类型触控事件。

       触控类型枚举之后,紧接着声明一个Puyo 的C 数组。这个数组有13 行,每行10 个Puyo。这个Puyo 数组是屏幕上显示内容的一个代理。这就是游戏模型—视图—控制器模式中的模型。

           C 预处理器宏kLastColumn和kLastRow在整个游戏逻辑中用于确保Puyo 的移动限制在Puyo 数组范围以内,依循代理的说法就是限制在屏幕以内。

        声明Puyo 数组之后,这里声明了两个Puyo,分别名为puyo1 和puyo2,其中包含从屏幕顶部下落逐渐落到游戏面板上的两个Puyo。之所以声明两个变量,这是因为有时一个Puyo 会卡在一个垂直Puyo 组上,而另一个Puyo 仍继续下落。需要在游戏逻辑中分别考虑这两个Puyo。

            这里还声明了两个一同使用的变量frameCount和moveCycleRatio。对于每一帧frameCount会增1。MoveCycleRatio是一个方块落到下一行所需的帧数。后面分析代码实现时你会对它有所了解。正是因为这个原因存在这两个变量,如下面的代码所示。

             int difficulty;
             int score;
       
          Label *scoreLabel;
          Label *difficultyLabel;

       
             后面两个变量分别是难度和得分。游戏逻辑发现彼此相邻的4 个或更多同色Puyo 构成一组时,每个Puyo 会分别使得分增加10。每得到500 分时难度会增加,通过递减moveCycleRatio来加快Puyo 下落的速度。这个代码还声明了两个Cocos2D 标签。标签是一个显示文本或数字的图形。在这里,它们分别显示得分和难度。这些标签会在每次修改得分和难度时更新。

          enum puyoOrientations {
              kPuyo2RightOfPuyo1,
              kPuyo2BelowPuyo1,
              kPuyo2LeftOfPuyo1,
              kPuyo2AbovePuyo1
           } puyoOrientation;

         这个代码声明另一个枚举,其中包含“翻转”时两个下落Puyo 可能的不同方向。默认方向将第二个Puyo(puyo2)置于第一个Puyo(puyo1)右边。其他方向分别将puyo2  置于puyo1 的下方、左边和上方。Puyo 下落时只要被用户轻击就会翻转,条件是没有任何障碍使得puyo2 翻转后会留在某处。如果存在一个障碍,则会忽略轻击。

            NSMutableSet *currentGrouping;
            NSMutableSet *groupings;
         }

          - (void) updateBoard:(ccTime)dt;
          @end
      
       最后,代码声明了两个NSMutableSet,它们是Cocoa 提供的集合类。代码使用这些类来检查同色的相邻Puyo 组。在这个实现中,这些集合大致等价于A* 中的开放列表和移动列表。

           这部分代码还定义了这个类中的唯一一个公共方法updateBoard,这个方法将被设置为每秒调用60 次。既然已经分析了GameLogicLayer类的接口,下面来分析它的实现,从代码清单11 开始。

       
      代码清单11 GameLogicLayer类的实现

         #import "GameLogicLayer.h"

          // 这里列出私有方法。
          @interface GameLogicLayer (private)

        - (void) startGame;
        - (void) clearBoard;
        - (void) tryToCreateNewPuyos;
        - (void) createNewPuyos;
        - (void) gameOver;

        - (void) processTaps;
        - (void) movePuyosAndDisappearGroupings;

        - (void) findPuyoGroupings;
        - (void) computeScore;
        - (void) determineDifficultyIncrease;
        - (void) updateInfoDisplays;

        - (void) detectStrayGrouping;
        - (void) checkForPuyoGroupings:(Puyo *)p1;

        - (void) movePuyoLeft:(Puyo *)puyo;
        - (void) movePuyoRight:(Puyo *)puyo;
        - (void) movePuyoDown:(Puyo *)puyo;
        - (void) movePuyoUp:(Puyo *)puyo;
        @end

         与前面介绍的其他类一样,首先导入类头文件并声明一些私有方法。在这里有很多私有方 法,但我并不会全部介绍,而只讨论游戏逻辑的主要部分:处理屏幕上的轻击、在游戏面板上移动Puyo,以及查找同色的相邻Puyo 组。

             Cocos2D 中屏幕轻击的处理通过声明一个名为ccTouchesEnded:withEvent: 的方法来完成。手指从屏幕上抬起时就会调用这个方法,所以它非常适用于捕获轻击。这个游戏不支持拖动、同时轻击或多指拖动。代码清单12 显示了ccTouchesEnded:withEvent: 的实现。

        代码清单12 ccTouchesEnded:withEvent:方法的实现

             - (BOOL)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {

                 UITouch *touch = [touches anyObject];
                 CGPoint point = [touch locationInView: [touch view]];
                 point.y = 480 - point.y;

                 if ( !puyo1.stuck && !puyo2.stuck &&
                      ( abs((int)puyo1.position.x - (int)point.x) < 32 &&
                     abs((int)puyo1.position.y - (int)point.y) < 32 ) ||
                      ( abs((int)puyo2.position.x - (int)point.x) < 32 &&
                     abs((int)puyo2.position.y - (int)point.y) < 32 ) )
                  {

                     touchType = kPuyoFlip;

                  }

                 else if ((int)point.y < 68) touchType = kDropPuyos;

                 else if ((int)point.x < 116) touchType = kMoveLeft;

                 else if ((int)point.x > 204) touchType = kMoveRight;

                 return kEventHandled;
              }
                                                           
          这个代码捕获触控,将它转换为屏幕上的一个点。在Cocos2D 中,屏幕上Y 坐标的正方向
    是从下到上。不过触控由 UIKit  提供,而 UIKit 的Y坐标正方向则是从上到下。因此,需要修改触控Y 坐标使之与屏幕坐标的方式一致。

          然后,代码检查屏幕轻击是否在puyo1或puyo2 的32 像素以内。如果确实如此,则把变量touchType设置为kPuyoFlip。

      然后,代码继续检查用户是否触摸屏幕底部最后68 像素内的区域。如果是这样,代码将touchType设置为kDropPuyos。其余的代码检查用户是否触摸了屏幕的最左和最右区域。这些区域都是116 像素宽,从屏幕左边界和右边界开始。代码根据所触摸的区域设置touchType变量。所以取决于用户触摸了屏幕的哪个位置,touchType变量会被设置不同的值。这里有一个底部区域,左区域和右区域,还有一个特殊的区域,即两个Puyo 的32 像素以内的区域。updateBoard方法调用processTaps方法 (每秒60 次)来处理touchType变量。updateBoard方法的实现如代码清单13 所示。
      代码清单13      updateBoard: 方法的实现

        - (void) updateBoard:(ccTime)dt {
            frameCount++;
            [self processTaps];
            [self disappearPuyos];
            if (frameCount % moveCycleRatio == 0) {
                [self movePuyosDown];
                [self findPuyoGroupings];
                [self computeScore];
                [self determineDifficultyIncrease];
                [self updateInfoDisplays];
            }
        }

       updateBoard:方法每秒执行60 次,并调用多个不同方法,这些方法构成了游戏逻辑。之所以采用这种方式编写代码,是为了让游戏逻辑的不同部分短小并且可管理。如果由一个庞大的方法完成所有游戏逻辑,游戏将更难调试也更难移植。

          updateBoard:方法调用的第一个方法是processTaps方法 (它用于处理touchType变量)。代码清单14 显示processTaps方法的第一部分。我将把这个方法分为多个部分来介绍。
      代码清单14 processTaps 方法的实现

        - (void) processTaps {
            if (touchType == kPuyoFlip) {
                // 重置触控类型
                touchType = kNone;

                if (!puyo1.stuck && !puyo2.stuck &&
                   abs(puyo1.boardX - puyo2.boardX) <= 1 &&
                   abs(puyo1.boardY - puyo2.boardY) <= 1)
                {
          switch (puyoOrientation) {
                         case kPuyo2RightOfPuyo1:
                             if ( (puyo1.boardY + 1) <= kLastRow &&
                                 nil == board[puyo1.boardX][puyo1.boardY + 1])
                              {
                                  [self movePuyoDown:puyo2];
                                  [self movePuyoLeft:puyo2];
                                 puyoOrientation = kPuyo2BelowPuyo1;
                             }
                             break;
                         case kPuyo2BelowPuyo1:
                             if ( (puyo1.boardX - 1) >= 0 &&
                                 nil == board[puyo1.boardX - 1][puyo1.boardY])
                              {
                                  [self movePuyoLeft:puyo2];
                                  [self movePuyoUp:puyo2];
                                 puyoOrientation = kPuyo2LeftOfPuyo1;
                             }
                             break;
                         case kPuyo2LeftOfPuyo1:
                             if ( (puyo1.boardY - 1) >= 0 &&
                             nil == board[puyo1.boardX][puyo1.boardY - 1])
                         {
                              [self movePuyoUp:puyo2];
                              [self movePuyoRight:puyo2];
                             puyoOrientation = kPuyo2AbovePuyo1;
                         }
                         break;

           case kPuyo2AbovePuyo1:
                         if ( (puyo1.boardX + 1) <= kLastColumn &&
                             nil == board[puyo1.boardX + 1][puyo1.boardY])
                         {
                              [self movePuyoRight:puyo2];
                              [self movePuyoDown:puyo2];
                             puyoOrientation = kPuyo2RightOfPuyo1;
                         }
                         break;
                 }

             }

          processTaps方法的工作基于检查之前由ccTouchesEnded:withEvent:方法设置的touchType变量。ProcessTaps 的第一部分处理Puyo 翻转。代码立即重置touchType变量(这样你就不会忘记),并准备处理下一个触控事件。然后,确保两个下落的Puyo 彼此相邻,而且没有“卡住”。这个代码还检查了puyo2 翻转后将移动到哪个位置。如果这个位置不是空的, 如果puyo1 或puyo2 中任意一个被卡住,或者两个Puyo 不是彼此相邻,代码就不会执行翻转。然后,代码根据puyoOrientation变量的当前值将puyo2移动到它的新位置,并更新这个变量使之与新方向一致。

            图5 到图9 显示了几秒时间内一对Puyo 翻转4 次的游戏切屏图。

      下面来分析让Puyo 向左移的代码,如代码清单15 所示。

          代码清单15 processTaps方法的实现 (续)

                } else if (touchType == kMoveLeft) {
                     //重置触控类型
                    touchType = kNone;

                     // 确定最左puyo,使之先移动
                     Puyo *p1, *p2;
                     if (puyo1.boardX < puyo2.boardX) {
                         p1 = puyo1; p2 = puyo2;
                     } else {
                         p1 = puyo2; p2 = puyo1;
                     }
                if (p1.boardX > 0 && !p1.stuck) {
                     if (nil == board[p1.boardX - 1][p1.boardY]) {
                          [self movePuyoLeft: p1];
                     }
                }

                if (p2.boardX > 0 && !p2.stuck) {
                     if (nil == board[p2.boardX - 1][p2.boardY]) {
                          [self movePuyoLeft: p2];
                     }
                }

       将Puyo 左移、右移或下移有些难度。在这里,要把它们向左移,首先需要确定两个Puyo中哪一个在游戏面板最左边。然后,检查这个Puyo 左边是否有障碍。还需要考虑到地图的左边界,以及这个Puyo 是否已经卡住。两个Puyo 都要做这些检查 (见代码清单16)。

       代码清单16 processTaps方法的实现 (续)
         } else if (touchType == kMoveRight) {
             //重置触控类型
            touchType = kNone;

             // 确定最右puyo,使之先移动
             Puyo *p1, *p2;
             if (puyo1.boardX > puyo2.boardX) {
                p1 = puyo1; p2 = puyo2;
             } else {
                p1 = puyo2; p2 = puyo1;
             }

             if (p1.boardX < kLastColumn && !p1.stuck) {
                 if (nil == board[p1.boardX + 1][p1.boardY]) {
                     [self movePuyoRight: p1];
                 }
             }

             if (p2.boardX < kLastColumn && !p2.stuck) {
                 if (nil == board[p2.boardX + 1][p2.boardY]) {
                     [self movePuyoRight: p2];
                 }
             }

       右移也采用类似的方式完成:确定哪一个是最右Puyo,考虑右边界,检查这个Puyo 右边是否有障碍,检查这个Puyo 是否被卡住而不能移动。同样地,两个Puyo 都要做这些检查 (见代码清单17)。

       
    代码清单17 processTaps方法的实现 (续)

         } else if (touchType == kDropPuyos) {
                 //重置触控类型
                touchType = kNone;

                 // 确定最下puyo,使之先移动
                 Puyo *p1, *p2;
                 if (puyo1.boardY > puyo2.boardY) {
                    p1 = puyo1; p2 = puyo2;
                 } else {
                    p1 = puyo2; p2 = puyo1;
                 }

                 if (!p1.stuck) {
                    while (p1.boardY != kLastRow &&
                        nil == board[p1.boardX][p1.boardY + 1]) {
                              [self movePuyoDown: p1];
                         }
                     }

                     if (!p2.stuck) {
                         while (p2.boardY != kLastRow &&
                             nil == board[p2.boardX][p2.boardY + 1])
                         {
                              [self movePuyoDown: p2];
                         }
                     }
                 } // End of if (touchType . . .
             }

        终于到了processTaps方法的最后部分。在这一部分,要在一步中让Puyo 在游戏面板上下落得尽可能远。要确定哪一个Puyo 比较靠下。如果这个Puyo 没有被卡住,则下移直到它到达一个障碍,或者达到游戏面板底边。要对两个Puyo 都做这些检查。

             在图10 到图13 中,可以看到一对下落中的Puyo。在这个过程中,其中一个Puyo 被卡住,而另一个继续响应轻击。因为其中一个Puyo 被卡住,所以翻转轻击被忽略。

      

      

      你应该还记得updateBoard: 方法,调用processTaps 之后,它会调用disappearPuyos方法。这个方法通过改变Puyo 的不透明度让所有标记为消失的Puyo 真正消失。代码清 单18 显示了这个方法。
        

          代码清单18 disappearPuyos方法的实现

         - (void) disappearPuyos {
             Puyo *puyo = nil;

             for (int x = 0; x <= kLastColumn; x++) {
                 for (int y = 0; y <= kLastRow; y++) {

                    puyo = board[x][y];

                     if (nil != puyo && puyo.disappearing) {
                         if (5 < puyo.opacity) {

                            puyo.opacity -= 5;

                         } else {

                             [self removeChild:puyo cleanup:YES];
             puyo = nil;
                                 board[x][y] = nil;

                             } // End of if (5 < puyo.opacity)

                         } // End of if (nil != puyo && puyo.disappearing)

                     } // End of for y loop.
                 } // End of for x loop.

                 if ( puyo1.stuck && puyo2.stuck ) {
                      [self tryToCreateNewPuyos];
                 }
             }
          disappearPuyos方法通过一个循环从左向右、从上向下扫描游戏面板。首先,它利用游戏逻辑的其他部分检查游戏面板上的Puyo 是否标记为消失,降低其不透明度 (使之更加透明)。如果发现一个不透明度低于5 的Puyo,则将它从游戏面板和屏幕删除。

             disappearPuyos方法还要检查puyo1 和puyo2是否被卡住。这个条件指示代码需要创建新的Puyo (见代码清单19)。查看下载代码中tryToCreateNewPuyos方法的实现来了解有关细节。

          代码清单19 movePuyosDown方法的实现

             - (void) movePuyosDown {
                 Puyo *puyo = nil;

                 for (int x = kLastColumn; x >= 0; x--) {
                     for (int y = kLastRow; y >= 0; y--) {
                         puyo = board[x][y];

                         // puyo “可见”吗(也就是说,没有消失吧)?
                         if (nil != puyo && !puyo.disappearing) {

                             // 这个puyo能下落到下一个单元格吗?
                             if ( kLastRow != y && (nil == board[x][y + 1]) ) {

                                 // Channel Bob Barker: 继续向下!
                                  [self movePuyoDown:puyo];
                                 puyo.stuck = NO;

                             } else {
                                 // 这个puyo不能再下落了;它被卡住了
                                 puyo.stuck = YES;
                             }

                         } // End of if (nil != puyo && !puyo.disappearing)

                     } // End of for y loop.
                 } // End of for x loop.
        if (kLastRow == puyo1.boardY) {
                puyo1.stuck = YES;
             }
            if (kLastRow == puyo2.boardY) {
                puyo2.stuck = YES;
             }
         }
      
      与disappearPuyos方法不同,movePuyosDown方法通过一个循环从下向上扫描游戏面板。它的工作如下:

        ■检查Puyo 是否非消失状态,以及Puyo 下面是否有一个障碍;

        ■如果Puyo 为非消失状态,而且下面没有障碍,则将Puyo 下移一个方块;

        ■如果有一个障碍,将这个Puyo 标记为卡住;

        ■这个方法还要检查puyo1或puyo2是否位于游戏面板的最后一行,如果确实如此,则标记为卡住。

          GameLogicLayer类中我想讨论的最后一个代码是寻找同色相邻Puyo 组的有关逻辑。这个逻辑由多个方法完成,不过由updateBoard:方法启动。updateBoard:方法调用findPuyoGroupings方法,它先水平扫描再垂直扫描游戏面板。组的检测和收集与从左向右扫描还是上下扫描无关。所以,检测和收集逻辑都在checkForPuyoGroupings:方法中,如代码 单20 所示。


       代码清单20 checkForPuyoGroupings:方法的实现

        - (void) checkForPuyoGroupings:(Puyo *)p1 {

            if (nil != p1 && p1.stuck && !p1.disappearing) {

                if ([currentGrouping count] > 0) {

                    Puyo *p2 = [currentGrouping anyObject];
                    if ( p2.puyoType == p1.puyoType ) {

                         [currentGrouping addObject:p1];

                    } else {

                        if ([currentGrouping count] > 3) {

                             [groupings addObject:currentGrouping];

                        }

                         [currentGrouping release];
                        currentGrouping = nil;

                        currentGrouping = [[NSMutableSet alloc] init];

           [currentGrouping addObject:p1];

                       } // End of if ( p2.puyoType == p1.puyoType )

                    } else {

                        [currentGrouping addObject:p1];

                    } // End of if ([currentGrouping count] > 0)

                } else {

                    if ([currentGrouping count] > 3) {

                        [groupings addObject:currentGrouping];

                    }

                    [currentGrouping release];
                    currentGrouping = nil;

                    currentGrouping = [[NSMutableSet alloc] init];

                } // End of if (nil != p1)
             }

          checkForPuyoGroupings:是一个很复杂的方法,有一些复杂的规则。下面对这个方法

         的工作做一个分解。

            ■首先,检查当前查看的Puyo 是否卡住,而且未消失状态 (正消失的Puyo 已经是某个组

               的一部分,并且已经在前面的某一步中统计过)。

            ■如果Puyo 被卡住,而且非消失状态,代码会检查以前运行这个代码时是否已经见过同
               色的Puyo。(要记住,这个代码在findPuyoGroupings方法中的一个循环中运行)。

            ■如果这个Puyo 是最近见到的第一个Puyo,将其自动增加到当前集合。

            ■如果有更多同色Puyo,将当前Puyo 增加到该集合。

            ■如果没有同色Puyo,检查集合,查看是否已经有至少4 个同色的Puyo。

            ■如果确实如此,为计分将这个集合置于所有组集合中。

            ■最终,释放这个集合,当前Puyo 增加到一个新集合。

            ■如果Puyo 没有卡住,正在消失,或者所检查的当前面板块为空,代码会检查当前集合,

               查看是否至少有4 个同色的Puyo。

            ■如果是这样,为计分将这个集合置于所有组集合中。

            ■最后,释放这个集合并分配一个新的空集合。

         代码分解到此为止。一定要下载完整的源代码来全面分析。下载的代码中会有更多注释。

    我从代码清单删除了原先的注释,因为它们会暴露代码的秘密。如果有这些注释,根本不需要

    我再做任何解释!

    留给读者的练习

        在这样一个Puyo 游戏基础上,还可以做一些改进。下面给出一些可以尝试的想法。

        ■增加一个主菜单:创建另一个Scene类,使用Cocos2D Menu 和MenuItem实例使游戏

          启动时显示一个菜单。让其中一个MenuItem在轻击时启动GameScene。

        ■增加一些声音或背景音乐:目前,这个游戏是完全无声的。

        ■用一种更刺激的方式增加难度:像现在这样,每增加500 分时代码才会增加难度。能不

          能在玩家每次完全清空游戏面板时就增加难度呢?

        ■用户如果同时得到两个组,就为他提供奖励积分。可以为这些特殊的奖励增加一些音

          效。

    小结

        本章,你已经了解了很多基础技术,但还要记住以下要点。

        ■考虑游戏的需求来确定开发游戏需要使用哪些技术。iPhone 平台为你提供了很多可供选

          择的技术,有些技术可能相对来讲更适合于某些游戏设计。相对于较大的控制台游戏,

          休闲型游戏通常使用较为简单的技术。

        ■一定要注意内存管理。iPhone 的内存有限,如果不小心,操作系统就会将你的游戏退

          出,以便留出内存来完成后台进程的工作。这可能发生在任意时刻,有可能破坏你的游

          戏。

        ■A* 是一个用途相当广泛的算法。通常用于游戏AI,指导游戏单元在布满障碍的地图上

          移动。很多游戏 (从驾驶模拟到实时的策略战争游戏)都非常依赖A* 算法。如果你确

          实想要从事游戏开发,这是一个非常值得学习的算法。

        ■Puyo 看上去是一个简单的游戏,不过需要同时跟踪很多方面,游戏逻辑可能很快变得

          相当复杂。要想擅长开发游戏,最好的办法就是找准方向,动手实践!尝试修改这里提

          供的游戏代码,为其增加一些特性,或者改变游戏逻辑,在俄罗斯方块游戏中加入你自

          己的创意吧。

        iPhone 平台让人非常兴奋,而开发游戏需要学习大量技术。在你开始探险之前,希望本章

    能让你有所收获。祝你好运,希望在App Store 见到你的作品!


        

     


          
         

       

  • 相关阅读:
    淘宝网-六属性
    软件架构之黑板模式
    06需求工程-软件建模与分析阅读笔记之六
    05需求工程-软件建模与分析阅读笔记之五
    04需求工程-软件建模与分析阅读笔记之四
    03需求工程-软件建模与分析阅读笔记之三
    第二次冲刺团队绩效评估
    第二次冲刺站立会议07
    第二次冲刺站立会议06
    第二次冲刺站立会议05
  • 原文地址:https://www.cnblogs.com/jiangshiyong/p/2636377.html
Copyright © 2011-2022 走看看