《贪吃蛇》(Greedy Snake)游戏是一款经典的益智游戏,也是Nokia手机上的第一款内置手机游戏,以其操作简单、可玩性强等特点而受到广大玩家的喜爱,以致现在还有大量的游戏玩家,而且还出现了各种各样的版本,甚至还有专门的《贪吃蛇Online》网络游戏。本文将详细介绍如何利用Java语言的J2ME技术开发手机上的贪吃蛇游戏,使读者可以进入多姿多彩的游戏编程世界,领略游戏编程的无穷乐趣。

         《贪吃蛇》游戏的程序虽然比较简单,但是还是要遵循整个游戏的开发流程进行开发,下面简单介绍一下游戏的开发流程。游戏程序的一般开发步骤如下:

1.         界面和游戏操作设计

本步骤主要是将设计游戏的程序界面布局,以及实现游戏操作按键的设定。

2.         游戏核心数据结构的设计

本步骤主要是抽象游戏中的数据,并选择合适的存储方式进行存储。数据结构的设计影响后续游戏逻辑的实现,所以该步设计尽量不要修改,否则将对项目的进度产生比较大的影响。

3.         游戏逻辑的分解

本步骤主要是将游戏中逻辑(游戏规则)分解为一系列的功能方法,通过对游戏逻辑的分解,可以结构化项目的代码,也可以实现对于项目进度的准确把握,方便项目进度的控制。

4.         游戏测试

游戏制作公司一般有专门的游戏测试人员,对于游戏的功能、执行效率、操作友好性等各个方面进行详细的测试,然后程序开发人员针对测试出的bug进行修改,然后由测试人员再次进行测试。

本文将以《贪吃蛇》游戏为例讲解游戏程序开发中的前三个步骤,阅读本文需要具备一定的程序设计基础,最好具备Java语言的基础知识,并且对于J2ME技术有一定的了解。

一、界面和游戏操作设计

1、界面设计

游戏界面是一个游戏展现给玩家的平台,所以界面设计是否美观是很多玩家选择一款游戏的重要标准,一般游戏公司中的游戏界面都有专门的美工制作相关的图片资源文件,从而使界面显得更加美观。

游戏中界面设计和软件界面设计的要求是一致的,主要有以下几条要求:

l  界面美观

l  界面条理

l  符合玩家操作习惯(界面友好)

由于本文主要介绍《贪吃蛇》程序的制作,所以在实际实现时简化了界面的外观,本游戏实现的界面如下图所示:

《贪吃蛇》游戏界面图

                   在本界面中,包含贪吃蛇、食物和两个按钮文字这4个部分,在本界面设计中,根据手机上的操作习惯,将两个按钮的位置分别设置在手机的左下角和右下角,而将整个屏幕(包含按钮文字显示区域)作为游戏区域,以最大限度的利用手机上的屏幕空间,所以本游戏未从屏幕中划分出独立的游戏区域。

对于简单的益智游戏,一般不需要设计滚屏,所以该游戏在设计界面上设计为简单的单屏游戏。

                   2、游戏操作设计

                   游戏操作指玩家以怎样的形式参与游戏,每个游戏的操作需要根据游戏的规则等进行设计,不过在设计游戏操作以前首先需要考虑的问题,这款游戏中有哪些是需要玩家进行参与的,然后才是以如何的形式进行参与,其次需要考虑不同平台以及不同设备上的操作特点,一定要满足硬件的要求。游戏操作设计的规则如下:

l  操作简单

l  操作方便

l  符合用户操作习惯

l  符合设备硬件要求

针对手机设备来说,输入设备一般为:单手键盘、双手键盘(Nokia N-Gage QD)和触摸屏,而大部分的手机都是普通的单手键盘,也就是一般使用单手进行操作的键盘,本游戏就以最常见的单手键盘为基础进行游戏操作设计。

         在本游戏中,需要玩家参与的操作有:控制贪吃蛇移动的方向、暂停和退出功能。在该游戏中,贪吃蛇的速度、程序是否结束等则由游戏程序本身进行控制。基于以上需要控制的内容,本游戏设计的操作方式如下:

1)         使用手机键盘上的4个方向键控制方向,按照手机上的操作习惯,也可以使用数字键2、4、6和8分别控制上、左、右和下。

注意:当用户按下和当前移动方向相反的方向键时,程序不改变方向。例如当前贪吃蛇的移动方向是向上,而玩家按下向下的控制键时,则程序不改变贪吃蛇的移动方向。

2)         使用手机键盘上的左软键控制游戏的“暂停”和“继续”功能,使得该游戏可以中断。可中断性强也是手机游戏的特点,是操作友好性的一个体现。当然,由于该游戏的控制比较简单,也可以将导航键的中键和数字键5作为暂停的控制,这样更方便玩家的操作,本游戏中未实现该功能。

3)         使用手机键盘上的右软键控制游戏的“退出”功能,使得游戏可以结束。当然,在手机上也可以通过挂机键退出程序,但是一般不推荐玩家使用该操作实现退出。因为在退出以前需要程序进行一些清理工作,而直接按挂机键则不一定能够执行该类代码。

本部分介绍了游戏程序的开发设计过程,详细介绍了游戏设计中的界面设计和操作设计,在接下来的文章中将介绍游戏开发中的数据设计方法以及针对《贪吃蛇》游戏进行的数据设计。


二、游戏核心数据结构设计

在完成游戏的界面和操作设计以后,就是设计游戏的核心数据结构了。数据是一个程序的灵魂,数据的存放方式被称之为数据结构(Data Structure),不同的程序需要根据自身的需要,设计不同的数据存储方式,而数据结构有将对后续的程序算法产生直接的影响,所以数据结构设计的好坏,对于整个项目的影响是很严重的。

                   在程序开发中,设计数据结构的步骤一般如下:

l  分析需要存储的信息

l  将这些信息抽象为程序中的数据

l  根据程序中的数据进行结构设计

                   下面以《贪吃蛇》程序为例来介绍如何进行游戏核心数据结构的设计。

1、  分析需要存储的信息

在程序中需要存储的信息一般分为两部分:界面控制信息和逻辑控制信息。界面控制信息用于控制界面上各个元素的显示等,逻辑控制信息用于进行程序内部的逻辑处理,一般界面控制信息是可见的,而逻辑控制信息在界面上不是直接可见的。

在《贪吃蛇》游戏中,界面控制信息主要包含两个部分:贪吃蛇的位置信息,存储贪吃蛇的具体位置,另外一个就是闪烁的食物的位置。

而逻辑控制信息主要包含三个部分:贪吃蛇的移动方向、闪烁控制以及程序暂停控制。

由于《贪吃蛇》游戏比较简单,所以在实际存储时,分析出来需要存储的信息不多,但是这种分析数据的方法,值得大家在实际的开发过程中进行借鉴。

2、  将这些信息抽象为程序中的数据

程序中需要存储的信息抽象出来了以后,就是以什么类型的数据来存储这些信息的问题了,这里是计算机编程中对于数据的抽象。

对于界面控制信息的存储,计算机编程中使用的知识和数学上是一样的,都是利用坐标系的知识来存储位置信息。对于平面游戏(2D游戏)来说,存储位置时使用的也是直角坐标系(笛卡尔坐标系),只是坐标系的形式和数学上的坐标系不完全一致。在计算机中,一般以屏幕的左上角作为坐标原点,以水平向右的方向为x轴的正方向,以垂直向下的方向作为y轴的正方向,这样整个屏幕中的所有点均位于坐标系的第一象限中。

有了坐标系的知识以后,就方便了界面中位置的存储了。对于贪吃蛇来说,以为其在屏幕上可以到处移动,而且可以在屏幕上转弯等,所以需要对于其位置分开进行存储。将贪吃蛇的每个节点进行分开存储,换句话说,存储贪吃蛇的位置,也就是存储贪吃蛇上每一个节点的位置。另外,由于每个节点都是一个区域,程序中一般存储每个节点左上角的坐标,而将节点的宽度和高度处理成常量。这样每个贪吃蛇的节点就需要两个整数分别存储x坐标和y坐标了,而贪吃蛇的整个结构则需要一组这样的整数进行实际的存储了。

对于食物的位置则比较简单,只需要存储食物的x坐标和y坐标即可。

对于逻辑控制信息的存储,贪吃蛇的移动方向在实际存储时,需要进行抽象,在该游戏中,贪吃蛇的移动方向不外乎四种:上、下、左、右。在程序中只需要找出能够存储四种状态的类型即可,一般选择整数型,而为了便于程序的阅读,一般将四种方向声明为程序中的常量。闪烁食物的控制变量和暂停控制变量都是开关变量,也就是只需要两个状态即可,在程序中,一般使用boolean类型来进行存储。

3、  根据程序中的数据进行结构设计

将数据抽象成程序中的数据以后,就需要设计使用什么样的结构来存储这些数据了。

对于贪吃蛇节点的存储,可以采用的数据结构有很多,例如数组、链表等线性的结构都可以,由于贪吃蛇各个节点之间常见的操作是节点的添加以及节点的遍历,所以无论使用数组或链表都不是完全合适,这里为了从控制方便以及遍历的效率考虑,选择数组进行实现。由于贪吃蛇的节点需要经常变化,所以在使用数组时,首先声明一个长度比较大的数组,开始只使用其中的一部分,当节点添加以后,变化使用的数据即可。则设计出的结果如下:

         /** 贪吃蛇节点坐标,其中第二维下标为0代表x坐标,下标是1代表y */

         int[][] snake = new int[200][2];

         /** 已经使用的节点数量 */

         int snakeNum;

           /** 蛇身单元宽度 */

         private final byte SNAKEWIDTH = 4;

这里使用snake[0]存储贪吃蛇第一个节点,其中snake[0][0]存储第一个节点的x坐标,snake[0][1]存储第一个节点的y坐标,snake[1]存储贪吃蛇第二个节点,其中snake[1][0]存储第二个节点的x坐标,snake[1][1]存储第二个节点的y坐标,依次类推。snakeNum用来表示使用了snake数组中的多少个节点。例如初始时snakeNum的值是7,则代表snake[0]到snake[6]是已经使用的数组元素。

闪烁的食物的位置比较简单,设计出的结果如下:

/** 食物的X坐标 */

         int foodX;

         /** 食物的Y坐标 */

         int foodY;

贪吃蛇的移动方向使用一个int类型进行表示,设计结果如下:

           /** 贪吃蛇运动方向*/

         int direction;

           /** 向上 */

         private final int DIRECTION_UP = 0;

           /** 向下 */

         private final int DIRECTION_DOWN = 1;

           /** 向左 */

         private final int DIRECTION_LEFT = 2;

           /** 向右 */

         private final int DIRECTION_RIGHT = 3;

而闪烁食物和暂停只需要分别使用一个boolean类型的值代表即可,设计结果如下:

           /** 是否处于暂停状态,true代表暂停 */

         boolean isPaused = false;

           /** 食物的闪烁控制 */

         boolean b = true;

本部分主要介绍了游戏程序中数据结构设计的方法,并且以《贪吃蛇》游戏为例详细介绍了设计的过程,在下一部分将进行游戏逻辑(规则)实现的讲解。

 

三、游戏逻辑的分解

在游戏的核心数据结构设计完成以后,就可以针对设计出的数据结构进行游戏逻辑的实现了。游戏逻辑即游戏规则,是游戏编程中最核心的部分,也是最难实现的部分,在游戏程序的开发过程中,大部分时间都是用在游戏逻辑的实现上。

游戏逻辑基于游戏数据结构,从程序开发角度来看,游戏逻辑就是对于游戏数据的规则变换。当然,这些数据的变换需要根据游戏规则进行实现。然后把最终变化的结果以界面的形式显示给最终用户,对于游戏程序来说也就是游戏玩家。

进行游戏逻辑的设计,首先要把游戏规则分析出来,所谓游戏规则,就是在游戏中需要程序设计人员实现的规定和控制,这些可以根据游戏的功能进行实现。例如《贪吃蛇》游戏需要实现的游戏规则如下:

l  游戏初始化

l  贪吃蛇的移动

l  贪吃蛇方向控制

l  贪吃蛇和食物的碰撞和处理

l  食物坐标的随机生成

l  游戏结束的判别

l  游戏暂停的控制

在程序实际实现时,一般使用方法来组织游戏逻辑相关的代码,也就是将对应的游戏逻辑转换为一个方法或一组方法。由于以上逻辑都比较简单,所以在实际实现时都转换为一个方法。下面依次来讲解以上游戏逻辑的实现,并介绍实现时需要注意的一些问题。

1、  游戏初始化

游戏初始化实现的功能是初始化游戏的相关数据,一般在游戏开始以及过关游戏的关卡切换时调用。

实现该功能首先需要清晰的知道需要初始化那些数据,如何进行初始化。在《贪吃蛇》游戏中,需要初始化的主要数据是贪吃蛇的位置、方向和食物的位置,另外还包含一些系统控制变量,例如暂停的控制变量等。

在本游戏中,采用如下的策略进行初始化:将贪吃蛇基本初始化在屏幕的中央,初始移动方向和贪吃蛇节点的排列顺序一致,食物的坐标固定位置。

则游戏初始化的代码如下:

         

/**初始化开始数据*/
         private void init() {
                    // 初始化节点数量
                    snakeNum = 7;
                    // 初始化节点数据
                    for (int i = 0; i < snakeNum; i++) {
                             snake[i][0] = 100 - SNAKEWIDTH * i;   
                             snake[i][1] = 40;
                    }
                    // 初始化移动方向
                    direction = DIRECTION_RIGHT;
                    // 初始化食物坐标
                    foodX = 100;
                    foodY = 100;
                    isPaused = false;  //初始化暂停
         }

首先初始化贪吃蛇的节点数量为7个,然后按照在屏幕上从右向左的顺序依次初始化各个节点的x和y坐标,并且初始化贪吃蛇的移动方向为向右,然后硬性初始化食物的坐标为(100,100),最后初始化暂停控制变量为正常运行状态。

2、  贪吃蛇的移动

贪吃蛇的移动是贪吃蛇游戏的核心规则,也是需要考虑时间比较长的规则。贪吃蛇移动的规则如下:

l  除第一个节点以外,其它每个节点跟随前一个节点移动

跟随的实现只需要将前一个节点的坐标赋值给下一个节点即可,赋值的时候注意顺序,下面的代码实现从对最后一个节点的赋值开始。

l  贪吃蛇第一个节点沿着移动方向移动一个单位

判断贪吃蛇的移动方向,然后根据方向改变第一个节点对应的x坐标或y坐标。

                            实现贪吃蛇移动的代码如下:

                                

/**贪吃蛇移动*/
                                     private void move() {
                                               // 蛇身移动
                                               for (int i = snakeNum; i > 0; i--) {
                                                        snake[i][0] = snake[i - 1][0];
                                                        snake[i][1] = snake[i - 1][1];
                                              }
                                               // 第一个单元格移动
                                              switch (direction) {
                                                        case DIRECTION_UP:
                                                                 snake[0][1] = snake[0][1] - SNAKEWIDTH;
                                                                 break;
                                                        case DIRECTION_DOWN:
                                                                 snake[0][1] = snake[0][1] + SNAKEWIDTH;
                                                                 break;
                                                        case DIRECTION_LEFT:
                                                                 snake[0][0] = snake[0][0] - SNAKEWIDTH;
                                                                 break;
                                                        case DIRECTION_RIGHT:
                                                                 snake[0][0] = snake[0][0] + SNAKEWIDTH;
                                                                 break;
                                               }
                                     }

3、  贪吃蛇方向控制

贪吃蛇方向控制需要根据玩家的按键改变贪吃蛇的方向变量,在改变时需要注意,不能改变为当前方向的相反方向,例如当前方向是向下时,按向上的方向键是无效的。

根据J2ME技术中事件处理的编程方式,实现的代码如下:

     

/**事件处理*/
         public void keyPressed(int keyCode) {
                   int action = this.getGameAction(keyCode);
                   switch (action) {
                   case UP:
                            if (direction != DIRECTION_DOWN) {
                                     direction = DIRECTION_UP;
                            }
                            break;
                   case DOWN:
                            if (direction != DIRECTION_UP) {
                                     direction = DIRECTION_DOWN;
                            }
                            break;
                   case LEFT:
                            if (direction != DIRECTION_RIGHT) {
                                     direction = DIRECTION_LEFT;
                            }
                            break;
                   case RIGHT:
                            if (direction != DIRECTION_LEFT) {
                                     direction = DIRECTION_RIGHT;
                            }
                            break;
                   }
         }

当然,完整代码中的事件处理中需要进行的操作比控制方向要多一些,这里的事件处理代码只是方向控制部分。

4、  贪吃蛇和食物的碰撞和处理

贪吃蛇和食物的碰撞和处理也是贪吃蛇游戏的核心规则,本来碰撞检测是游戏编程中的一个基础算法,本游戏在实际实现时简化了碰撞检测的实现。

本规则的实现如下:将食物和贪吃蛇节点的坐标都处理成贪吃蛇单位宽度的整数倍,这样在贪吃蛇和食物碰撞时则可以在坐标上完全重叠,这样就简化了碰撞检测的判别。当然由于贪吃蛇和食物的碰撞只是第一个节点和食物的重叠,所以在实际实现检测时的条件实现的比较简单。

当检测到碰撞以后,需要进行的处理有两个:贪吃蛇增加一个节点和重新生成食物的坐标。贪吃蛇增加一个节点只需要将snakeNum变量的值增加1即可,在移动时则自然增加一个节点到贪吃蛇的末尾。而重新生成食物的坐标则由后续的规则实现。

则本规则的实现代码如下:

    

/**吃掉食物,自身增长*/
         private void eatFood() {
                    // 判别蛇头是否和食物重叠
                    if (snake[0][0] == foodX && snake[0][1] == foodY) {
                             snakeNum++;
                             generateFood();
                    }
         }

                   本部分介绍了游戏开发中游戏逻辑的分解方法,以及《贪吃蛇》游戏中游戏逻辑的分解,并且实际介绍了游戏初始化、贪吃蛇移动、贪吃蛇方向控制和贪吃蛇和食物的碰撞检测规则的实现,后续的规则实现将在接下来的文章中继续进行介绍。

 

5、  食物坐标的随机生成

当食物被贪吃蛇吃掉以后,需要重新生成贪吃蛇的坐标,在生成贪吃蛇的坐标时,需要实现如下要求:

a、  坐标位于屏幕以内

b、  坐标不能和贪吃蛇任何一个节点重合

c、  坐标必须是贪吃蛇节点宽度的整数倍(该要求和贪吃蛇的碰撞检测算法匹配)。

对于以上要求,在程序中实现时这样实现:坐标位于屏幕以内只需要控制x坐标在0和屏幕宽度之间,y坐标在0和屏幕高度之间即可;坐标不能和贪吃蛇节点重合需要将生成的坐标和贪吃蛇的每个节点进行比较,如果相同则重新生成食物的坐标;坐标是节点宽度的整数倍,则通过将随机出的数字除以节点宽度以后再乘以节点宽度,按照程序中整数除整数还是整数的语法,得到的数字肯定是节点宽度的整数倍。

按照以上逻辑实现的程序代码如下:

     

/**产生食物*/
         private void generateFood() {
                    while (true) {
                             //屏幕范围内,且是蛇身宽度的整数倍
                             foodX = Math.abs(random.nextInt() % 
                                                (width - SNAKEWIDTH + 1))
                                                / SNAKEWIDTH * SNAKEWIDTH;
                             foodY = Math.abs(random.nextInt() % (height - SNAKEWIDTH + 1))
                                                / SNAKEWIDTH * SNAKEWIDTH;
                             //判断是否和蛇节点重叠
                             boolean b = true;
                             for (int i = 0; i < snakeNum; i++) {
                                      if (foodX == snake[i][0] && snake[i][1] == foodY) {
                                                b = false;
                                                break;
                                      }
                             }
                             if (b) {
                                      break;
                             }
                    }
         }

该算法在实际使用时效率不是很稳定,有可能产生迟钝。

6、  游戏结束的判别

游戏结束也是益智类游戏开发过程经常需要判别的条件,不同的游戏结束条件需要根据游戏规则进行实现。

《贪吃蛇》游戏结束的规则主要有两个:

a、  贪吃蛇超出游戏区域

b、  贪吃蛇自身的节点之间存在重叠

贪吃蛇的移动规则是每个节点都跟随前一个节点移动,所以贪吃蛇超出游戏区域时一定是贪吃蛇的第一个节点首先超出游戏区域,这样在判断时只需要判断第一个节点是否超出游戏区域即可。

贪吃蛇移动规则也决定了自身的节点之间重叠时,首先也是第一个节点和自身的其它节点重合,而由于贪吃蛇在移动时移动规则规定不能反向,所以在贪吃蛇和节点重叠时,第一个节点只能和第五个节点以及以后的节点重合,而且贪吃蛇各节点每次移动的单位都是节点宽度,所以当两个节点重叠时,对应的坐标完全相等,所以实现的代码如下所示:

     

/**判断游戏是否结束*/
         private boolean isGameOver() {
                    // 边界判别
                   if (snake[0][0] < 0 || snake[0][0] > (width - SNAKEWIDTH)
                                     || snake[0][1] < 0 || snake[0][1] > (height - SNAKEWIDTH)) {
                                       return true;
                   }
                    // 碰到自身
                   for (int i = 4; i < snakeNum; i++) {
                            if (snake[0][0] == snake[i][0] && snake[0][1] == snake[i][1]) {
                                     return true;
                            }
                   }
                   return false;
         }

然后在程序中利用该方法的返回值,控制游戏中界面的切换等,从而实现游戏结束的功能。

7、  游戏暂停的控制

在手机游戏中,可中断性强是一个基本的要求,其中游戏暂停就是实现该要求的一种方式。

游戏暂停功能实现的原理为:暂停游戏逻辑的执行,并控制游戏相关的操作无法实现,例如暂停时无法改变贪吃蛇的方向等。所以实现游戏暂停,需要控制线程的执行以及事件处理。

在程序中这样来实现暂停,使用变量isPaused的值来控制线程逻辑和事件处理,当按下暂停键时设置该变量为true,当按下继续键时设置该变量为false。实现的代码如下:

                 

/**事件处理*/
                  public void keyPressed(int keyCode) {
                            if(keyCode == -6){ //左软键
                                     isPaused = !isPaused;                         
                            }
                            if(keyCode == -7){ //右软键
                                     SnakeMIDlet.quitApp();
                            }
                            if(isPaused){ //如果暂停则不能控制移动方向
                                     return;
                            }
                             ……
                    }

当按下左软键时,改变变量isPaused的值,然后判断当isPaused变量为true时,不响应和改变贪吃蛇方向相关的按键,这样在暂停时就无法进行游戏相关的操作。而关于逻辑执行控制的代码则放在线程内部,代码如下:

               

/**线程方法 使用精确延时*/
                  public void run() {
                            try {
                                     while (isRun) {
                                              // 开始时间
                                              long start = System.currentTimeMillis();
                                              if (!isPaused) { //如果不暂停
                                                        eatFood();  // 吃食物
                                                        move();  // 移动
                                                        if (isGameOver()) { // 结束游戏
                                                                 break;
                                                        }
                                                        b = !b; // 控制闪烁
                                              }
                                              repaint();  // 重新绘制
                                              long end = System.currentTimeMillis();
                                              if (end - start < SLEEP_TIME) { // 延时
                                                        Thread.sleep(SLEEP_TIME - (end - start));
                                              }
                                     }
                           } catch (Exception e) {}
         }

这里通过isPaused变量控制在每个线程循环中是否执行相关的程序逻辑,当暂停时不移动贪吃蛇的位置也不闪烁食物,这样在玩家看来就是整个游戏的进程实现了暂停。

在游戏逻辑编写完成以后,需要考虑的问题就是将逻辑放在事件处理方法内部还是放在线程内部了。对于玩家操作的接收以及对于玩家操作的反馈的逻辑可以在事件处理的方法内部进行调用,而对于无需玩家操作也执行的逻辑如贪吃蛇的移动,则需要在线程的run方法内部进行调用。

四、总结

         本文介绍了游戏程序开发的总体过程,并以《贪吃蛇》游戏为例子详细介绍了各个步骤的实现,但是游戏程序开发是一个系统工程,本文介绍的也是最核心的游戏逻辑的实现,而一个实际的游戏程序还需要很多相关的功能,例如各个界面如菜单、帮助等,游戏的背景音乐,游戏难度,游戏积分,高分排行榜等功能,在手机游戏中还需要实现手机来电的暂停处理等各个系统功能,才能称作完整的游戏。但是本文经历了游戏开发的主要过程,希望能将各位读者带入无限变化的游戏编程世界。