状态机

            状态机是编译原理的内容,看上去挺复杂的,不过说白了就是选择分支结构。但我为什么要提状态机呢?其实它是一个简化问题的好工具。再复杂的问题都可以被分解成若干小问题去解决。虽然一个游戏很复杂,但我们把它分解成若干块,分而治之,就简单多了。分类的依据就是状态。我们可以将一个游戏划分成很多状态。比如主菜单状态,控制主角状态,暂停状态等。在状态中可以再细分子状态。一直分下去,直到问题变简单。下面看看某个游戏的gameLoop片断。

public void gameLoop()
    {        
            switch(gameState){
            case GS_Logo:
                logic_Logo() ;
                break ;
            case GS_MainMenu:
                logic_MainMenu() ;
                break ;
            case GS_PlayerControl:    
                logic_PlayerControl() ;
                break ;
            case GS_PauseMenu:
                logic_PauseMenu() ;
                break ;                                                                                                  
            case GS_GameOver:
                endGame() ;
                break ;
            }        
    }


针对不同的状态进入不同的逻辑处理函数,问题就简单多了。从某个角度来说,游戏就是状态的集合,我们要处理好状态之间的转换,写好每个状态的代码。在写状态机之前,最好先画出状态转换图。这样条理清楚,而且便于观看整个游戏的结构。举个例子,下面是一个ARPG游戏中敌人的AI状态图:(图略)
从状态转换图很容易得到AI代码,而且容易检查错误。常犯的错误是没有处理所有可能的状态。
画出状态转换图并仔细检查是个不错的方法.当然对于简单的AI就不必要麻烦了。中等复杂的AI画一个简单的小图就可以解决问题。

渲染

            首先解释一下渲染这个词,英文是render,用在这里只是绘制的意思,这么用只是一个习惯。上面讲的run函数中并没有任何渲染的代码。这是由J2ME的结构决定的,所有的绘制代码被放在了paint里面。当然如果你使用一个Muttable Image缓冲屏幕,也可以在gameLoop里面对这个Muttable Image进行绘制,然后在paint里一次性的将这个Image贴到屏幕上。不过由于大多数手机硬件上支持双缓冲-paint的参数来自一个后台缓冲,这么做并没有太大的意义,反而白白浪费一块内存。所以J2ME游戏中的渲染往往放在paint中。

            在paint里面我们进行绘制工作也是分状态进行的,比如logo状态,我们在gameLoop里面根据时间(可以用run里面的那个frameCount代替真实时间)设置下一帧将要显示的logo图片,然后在paint里面将当前的图片画出来。其实也有一种写法是将gameLoop的代码写在paint中,因为paint是通过repaint和serviceRepaints每帧强制调用的,所以这样做完全可行。如果这样paint就是gameLoop,不需要另外一个gameLoop了。

            整个游戏的渲染可以按层次分几块进行。首先是场景的渲染,然后是场景中的所有物体,我称之为Sprite。这些Sprite需要根据位置进行排序后按顺序渲染,这样才能显示出正确的遮挡关系。场景有可能是分层的,比如分两层,第一层是地面,第二层是比较高的物体如大树。这样所有的Sprite就要放在这两层之间。当然不排除有些Sprite能飞到树上面,这需要单独处理。我这里说的Sprite并不是MIDP Game API里面的Sprite类,它只是一个概念而以。其实专业手机游戏开发中很少用到Game API,往往是自己开发一套类似于Game API但功能强很多的引擎。对于初学者,可以利用Game API进行比较方便的开发,但一定不能依赖于Game API,毕竟有能力开发出比Game API更好的引擎才是专业游戏开发者应该具有的素质。而且不依赖于某某API对于移植也很有好处。所有的渲染中,最上层的一般是游戏界面(GUI)的绘制。专业的说法是HUD(Headup Display)。这些GUI包括菜单、状态条、数据信息等等。

            渲染除了绘制的简单意思,往往还有一层图形效果的意思。在PC游戏开发中,常常使用Alpha混合、饱和运算、光影等各种效果。这些效果都是通过像素操作实现的。在手机上由于设备能力的限制,这些操作往往速度非常慢,不过随着手机硬件的发展,终究会被用上。除此之外,粒子系统是一种常常使用的效果,在手机游戏开发中也经常运用。有兴趣的读者可以搜索相关的文章学习。

场景与角色

            场景与角色是构成游戏的两大要素。从程序角度说,他们是两个重要的数据结构。本节将从数据结构、渲染方法、物理作用等方面分别讲解。

 

1 场景管理

            场景是什么呢?场景就是游戏角色所存在的世界。在RPG、SLG、RTS等类型的游戏中,场景的概念比较明显,从视觉上看就是游戏的地图。不过从程序的角度看,场景是一种数据结构,不但包含了地图显示的图形信息也包含了角色在场景中活动所需要的物理信息和事件信息。比如地图上有些地方是不可以通过的,有些地方主角走过去会触发一个事件等等,这些信息往往包含在场景中。广义的场景还包含地图上的NPC信息,而狭义的场景仅包含地图和物理层。这里讨论狭义的场景,即不包含NPC信息的场景。

            首先讨论怎样组织图片显示场景。最常用的方法是拼Tile,俗称贴瓷砖。整个场景用有限的图块拼接而成。Game API中的TiledLayer就是这种方式。使用Tile方式,一般用一个二维数组表示一张地图。实际使用中,往往使用一维数组代替二维数组,这里为了讲解方便使用二维数组描述。二维数组很好的对应了二维坐标。二维数组的每一项对应地图上某格使用那个瓷砖,整个二维数组就表示整张地图。为了方便的编辑地图,往往需要开发地图编辑器,所见即所得的编辑地图,将生成的数据存储为文件,在游戏中通过读取文件获取数据。如果场景有多层,比如比较高的建筑物,就要编辑多层地图了。数据组织好了,渲染的时候只要按顺序将二维数组中的每一项对应的Tile在指定位置画出来。实际操作中,地图往往远远超出屏幕大小,这就涉及到卷轴的概念。在卷轴式地图中,屏幕可以看成是一个摄像机,每次只显示地图的一部分。随着主角的移动,屏幕所显示的部分跟着变化,形成卷轴。


卷轴式地图显示的时候,从屏幕(摄像机)所覆盖的第一个Tile开始绘制。需要注意的是屏幕所能覆盖的Tile包括部分覆盖的Tile。上图中第一个Tile就是一个部分覆盖的Tile,尽管只是部分覆盖,它也是需要绘制的第一个Tile,否则屏幕上就会出现没有图像的部分了。造成部分覆盖的原因是屏幕的卷动速度并不是Tile宽度的整数倍。在实际开发中,卷轴速度往往可能是变化的,这样的卷轴显得比较自然。按照一定的顺序将屏幕所能覆盖到的所有Tile都绘制一遍,场景的绘制工作就完成了。这些Tile绘制的时候要采用相对于屏幕的坐标,而不是相对于地图的坐标。在2D游戏中经常用到屏幕坐标和地图坐标的转换。我的原则是所有的逻辑计算都统一在地图坐标上,只在最后渲染时才将逻辑坐标转换成屏幕坐标,对于场景和角色都一样。转换的方法很简单。设一个Tile(或角色)在地图上的坐标为(x,y),屏幕相对于地图的坐标为(sx,sy),那么Tile在屏幕上的坐标就是(x-sx,y-sy)。注意单位要统一,往往Tile的单位是格,所以要转换成像素单位,即格子坐标乘上Tile宽度或高度。将到这儿你会发现,其实我们已经实现了TiledLayer的主要功能,再加上管理Tile图片的部分一个简单的Tile引擎就成形了。不过你可千万不要满足,真正的游戏开发中场景引擎比这复杂的多,除了这种2D引擎还有2.5D的斜视角引擎,在渲染速度方面更是做了多方面的优化。

            最后谈谈场景中的物理作用和事件检测。在场景中有些地方是无法通过的,有些地方走上去会触发某个机关,这些信息往往也存储在场景信息中,用另一个数组表示,一般称为物理层、地形层或事件层等等。最常用的信息就是碰撞信息,它用来处理角色和地图的碰撞。碰撞检测在游戏开发中是一个永恒的话题,而2D地图碰撞是其中最简单的一种。由于使用了物理层,我们只要计算出下一帧角色所要到达的位置是哪一格,如果这一格的物理层信息是不可通过,则组织角色前进。这种碰撞检测不同于物体间的碰撞检测,不必和所有的物体进行遍历判断,缺点是不够精确。减小物理层格子的大小可以提高精确度,但这会使数据变多。所以物理层的碰撞检测一般只用在地图上,如果有比较特殊的物体,就让它作为一个角色存在,利用角色间的碰撞检测。

2 角色管理

            角色就是存在于场景中的一切东西,它可以是活动的如RPG中的NPC,也可以是静止的,如一个箱子。角色的数据结构视作用而不同,但基本的都有坐标、速度、使用到的图片等数据。具体到不同的游戏会增加各种数据,比如RPG中,会增加很多角色属性。这里我们只谈最根本的一些东西,主要讲角色的绘制和相互作用。

            绘制角色简单的只是绘制一个帧序列的动画。不同于动画片,角色的动画根据AI变化。不同的AI对应不同的动作和动画。复杂些的角色是由很多部分组合而来,可称之为组合精灵技术,或者称为纸娃娃系统(Avatar)。这种技术可以实现换装效果。组合精灵的复杂之处就在于要组织好每个小部分之间的关系,这需要组合精灵编辑器完成。在专业游戏开发中,会大量运用到各种工具,编写工具也是游戏程序员的工作之一。无论什么样的绘制系统,在最终绘制到屏幕时都要注意两个基本点 - 角色的坐标和层次。角色的坐标是AI运算的结果,绘制时要将地图坐标转换成屏幕坐标。角色的层次需要排序得到,最后根据从远到近的顺序绘制各个角色,这样才能显示正确的遮挡关系。

            角色的物理作用分为和场景之间的物理作用和角色间的物理作用。前者已经在场景管理中讲过。角色间的碰撞检测的时机是某个角色运动时。对于RPG这种类型的游戏,一个矩形碰撞框就可以表示角色的轮廓。对于格斗等类型的游戏,需要用多个碰撞框表示角色。碰撞框的检测非常简单,只要进行矩形相交测试。也有用圆形表示碰撞框的,利用圆心距离和半径的关系就可以判断。在2D飞机类游戏中,三角型的碰撞框也常常使用到。总之用一些几何形状粗略的描述角色的轮廓是2D游戏碰撞检测的通用方法。

            场景中的所有角色往往用一个数组存储起来,这样方便遍历操作。主角做为一个例外,是需要玩家操纵的角色,而其他角色都是用AI控制的。AI虽然是人工智能的缩写,但这里借用来表示操纵角色的代码。

 

            场景和角色是游戏中两个重要的基础组成部分,而开发一个游戏的工作主要部分就是设计各种场景和角色,按照设计编写角色的AI。