毕业设计思路

毕业设计思路
毕业设计思路

Java编写贪吃蛇游戏思路

一、需求分析:

1.游戏以经典贪吃蛇默认规则设计需要蛇能够按照给定的方向每隔一段时

间自动移动;方向键“↑”、“↓”、“←”、“→”可以控制蛇移动的方向,蛇每吃到一个食物后身体长度增加一格,原有食物消失,再随机生成一

个不再石头上的新食物;石头即为地图,蛇吃到石头后游戏结束。

2.为了使游戏更加具备娱乐性和挑战性,增设关卡,随着关卡级别的升高,

蛇移动速度加快,蛇吃到食物的难度加大,吃到一定数量的食物即可过

关进入下一个关卡;为蛇吃到食物后添加声音效果,增设背景音乐,可

以控制背景音乐的开关;

3.为了区别与经典模式的不同,游戏增加新的规则模式,新的模式规则中,

初始化的蛇长度一定,蛇吃到食物后,食物随机产生一个新的食物,原

来的食物经过蛇的消化后变为石头固定在原处不能移动。

二、设计思路:

1.首先考虑所需要的总体框架

贪吃蛇游戏主题应为蛇,需要创建蛇这个类命名为Snake,还要有食物

类命名为Food,石头类命名为Stone,显示面板类命名为DisplayPanel,

逻辑处理类命名为Logic以及主函数类命名为MainOfGreedySnake。

2.搭建类与类之间的关系

蛇吃食物,食物在被蛇吃到后随机产生新的食物,食物不能在石头上生

成,关系复杂,我们可以将这些关系在逻辑处理类Logic中搭建,这样即

降低了上面三个类之间的耦合性,又方便以后对辅助功能的添加。

蛇,食物,石头需要在显示面板中显示出来,而三者又在逻辑处理这个

类Logic中做逻辑关系运算,我们可创建自己的显示面板类DisplayPanel,用逻辑处理类Logic创建对象作为参数传递给显示面板,使得蛇,食物,

石头以及显示面板直接的关系更加简单。

3.添加简单的测试功能

首先需要创建主函数,主函数中创建逻辑处理类Logic的实例对象,创建

自定义的显示面板对象DisplayPanel,将逻辑处理类Logic的实例对象作为参数传递给显示面板DisplayPanel的实例对象中,显示面板需要寄托在Frame框架上,这就需要在主函数中创建一个JFrame对象,然后将自定义的显示面板添加到JFrame对象上。

接着是在每个类中添加一个测试用的功能函数,简单的打印一条输出语句,在蛇类Snake中,创建一个功能函数打印“蛇类被加载”,食物类,石头类,逻辑处理类,显示面板类中添加同样的功能,在逻辑处理类中定义蛇,食物,石头对象,创建构造函数参数传入蛇,食物,石头类型的对象;自定义的显示面板继承JPanel类,定义逻辑处理类的对象,创建构造函数传入逻辑处理类的对象。

通过调用类中的功能函数来测试,确保他们之间已经建立联系。

4.类中填充功能及参数

首先分析蛇:

图2-4.1

我们把显示面板划分为格子,每个单位的蛇身占一个格子,食物,石头同样,通过抽取定义一个格子类命名为Square;蛇的身体由多个格

子组成,需要一个链表集合存储,定义一个LinkedList类型的集合命名为snakeBody,存储类型为格子类型Square的对象;蛇有一个长度,定义一个int型的变量Length存储长度,定义一个int型的direction变量控制蛇移动的方向;定义一个boolean类型的变量iseatfood,来标识蛇是否吃到食物;定义一个boolean类型的变量iseatstone来标识蛇是否吃到石头;

蛇的功能有移动功能,创建功能函数move(),关于蛇移动,我们可以让存储蛇身体的集合snakeBody添加一个蛇头,去掉一个尾巴来实现,蛇头添加的位置又应该由蛇移动的方向控制,因此在添加蛇头之前应该判断蛇头的方向,如果蛇向上移动,direction应为1,我们让改变之前的snakeBody调用addFirst()功能添加蛇头,里面的参数应为改变之前的蛇头的纵坐标减一个格子,向下,向左,向右原理相同。

蛇已更改具有改变方向的功能,以便于后面控制蛇移动的时候改变方向,定义函数changeDirection(),传入int型的方向值。

蛇还应该有判断是否吃到食物的功能,定义功能函数isEatFood(),判断蛇是否吃到食物只需判断蛇头的坐标是否和食物的坐标相同,这里有一个小问题,蛇类中没有定义食物Food类型的引用,判断时还要有食物类型的对象,为了降低蛇与食物之间的联系,不得不传入一个Food 类型的参数。

图:2-4.2

蛇应该有判断是否吃到石头的功能,由于石头是一个比较大的集合,若沿用判断是否吃到食物的方法必将浪费大量的资源,在后面设计石头的时候我们可以用一个boolean类型的二维数组来存储一组石头命名为map[][],是石头设置这个坐标为true,不是则设为false,这样判断蛇是否吃到食物就较为简单,只需判断蛇头的坐标作为石头数组时是否为treu即可。如图2-4.2所示,也即是直接反回map[snakeBody.getfirst().getx()][snakeBody.getfirst().getx()]即可。

蛇的另一个功能是判断蛇是否吃到自己的身体,需要定义一个功能函数isEatBoody()判断蛇是否吃到身体同判断是否吃到食物相同,只需判断蛇头的坐标与蛇身的坐标是否相同,为了减少CPU的工作量,可以稍微优化一点,判断蛇头与蛇身的坐标是否相同时从第4截身体开始判断即可,因为不管蛇怎么移动都不会与第二截、第三截身体相撞。

蛇要在显示面板中显示,按照面向对象的思想,蛇要在面板中显示身体,设计到的两个对象,蛇、面板,显然显示这个动作蛇自身应该最具权威性,蛇对自己的属性最了解,就应该把显示蛇这个功能定义在蛇这个类中,创建功能函数drawMe()给它传递一个画笔Graphics g,从蛇头到蛇身依次画出每个格子。

最后考虑到以后重新开始游戏,以及多个关卡的问题,这里先定义好一个蛇初始化的功能函数init()。

关于蛇的分析先到这里。

其次是对食物的分析:

食物在游戏中自始至终只会有一个,只需定义一个Square对象即可,命名为food;由于后面蛇吃到食物后要随机生成一个新的食物,需要用到Random类中的随机函数,这里事先定义好一个Random ran;

确定好参数后确定功能函数,首先应该是判断食物是否被吃掉,定义函数isFoodEaten();同isEatFood相似。

其次食物被吃掉之后会随机产生一个新的食物,定义函数setFood

(),产生新食物的瞬间,旧食物已经消失,没有必要在去创建一个food 对象,只需将food中的坐标改变一下即可,x坐标传入一个随机的参数,y坐标传入一个随机的参数,显示面板大小先确定划分为30*30个格子,x坐标和y坐标应该在0~29之间,只需简单的一句话x = (int)(ran.nextFloat()*30),通过Random类获取0~1之间的小数,乘以30得到0~30之间的数,在强制转换为int类型的数传递给x,y;

最后同蛇相似,食物也应该在面板中显示,定义显示的功能函数drawMe()传入参数Graphics g。

再接着对石头分析:

在对蛇分析的时候依然考虑到石头的存储,定义一个boolean类型的数组记录是否画石头,boolean map[][] = new boolean [30][30];

考虑到以后地图的改变,在创建石头的时候应该设计不同的地图,根据传入参数的不同,创建不同的地图,定义构造函数Stone();传入一个int类型的参数,根据参数创建不同的地图;地图设计较为简单,利用switch()case:判断需要创建何种地图。创建地图既是在map[][]数组中不同的位置设置成true。

同蛇与食物相同,石头类应该具有显示功能drawMe(),传入参数Graphics g。

完善以上功能之后就可以在逻辑处理类Logic中先搭建简单的处理关系,以保证蛇,食物,石头可以显示出来;Snake snake = new Sanke();

Food food = new Food();Stone stone = new Stone(1);先显示地图1作为测试。

在显示面板类DisplayPanel中创建构造函数传入Logic的实例对象;

重写paint方法,paint()方法中调用logic.Snake.drawMe() logic.Stone.drawMe() logic.food.drawMe() ;运行可显示效果,看到蛇,食物,石头已经显示到面板中。

5.逻辑关系处理

做好的蛇还不会移动,接下来就来实现此功能:

蛇要在每隔一段时间移动一个格子。蛇要移动,还要在没有死亡之前一直移动,主函数所启动的主线程的任务是启动窗体以及在内存中创建好所需要的资源,这里必需另开一个新的线程。开启线程的方法有两种,这里选择实现Runnable接口的方法,让Logic implements Runnable,实现run()方法,由于蛇要不停的运动在蛇的move方法之外加入while (true),蛇每隔一段时间会自动移动一格,在move方法之后添加Thread.sleep(300),经过多次运行测试得出的时间;在主函数MainOfGreedySnake中创建线程thread t = new Thread(logic);运行观察效果,会发现蛇依旧不会移动,这是因为显示面板中的paint函数需要重新被调用才会重新绘制图形,在DisplayPanel的paint最后加上repaint ();再次运行,蛇已经可以自己移动。

现在的蛇依然可以移动,但不受控制,会移动出边界,因此需要在蛇移动之后判断蛇是否移出边界,将此功能封装成函数,显然应该定义在蛇类中,命名为isOverBand(),函数所需的功能即判断蛇头是否移出边界,若是则将蛇头的坐标更换为另一边界的坐标,实现了蛇在规定范围内自动移动。

蛇可以自动移动,但仍然还不受控制,后面将实现键盘控制蛇移动方向:键盘控制蛇移动方向,也即是每按下一个键,通过某种响应,让蛇的方向改变,很容易想到Java GUI开发中提供的事件监听机制,但问题在于事件监听机制是针对GUI组件设计的,这就需要将蛇的方向和主函数创建的JFrame联系到一起,主函数中需要创建Logic实例对象,Logic 中有创建蛇对象,可以直接在主函数的JFrame上添加监听器,关于监听器的创建这里为了以后添加更多功能不至于关系复杂化,采用匿名内部类的方法为JFrame添加监听器,JFrame jf = new JFrame();jf.addKeyListener(new KeyListener(){}),监听器里面重写keyPressed ()功能函数,利用switch(){case}语句判断方向键调用logic.snake.chageDirection()方法改变方向。

到这里程序调试运行方向以然可以控制,但按照规定,蛇不能向相反方向运行,在改变方向的时候应该做一个判断,新传入的方向值若和snake对象中方向值相同的话,snake方向值不做改变,加以判断之后,蛇的运行轨迹同想象中的运行规则几乎没有差别了,但是还有一个小小的bug存在,就是如果蛇向下移动的话,在下一次移动触发前,先按下向左,在按下向上,蛇还会向相反的方向移动,对于这个问题的解决首先要找到问题的根源,如果蛇向下移动的时候,先按下向左的方向,蛇的direction会被赋值为向左,但在蛇下一次移动之前,又按下向上的方向键,在判断方向的时候,向上的方向与向左的方向值不是相反方向,又会被赋值,这就导致了相反方向仍然会存在的可能性,关于此问题的解决,可以定义两个方向值,一个用于保存蛇移动之前的方向,命名为oldDirection,一个用于保存蛇移动之后的方向,命名为newDirection,在做方向判断的时候,拿新来的方向跟oldDirection向比较,方向不相反,把新传入的方向值副给newDirection,在蛇移动一次之后,再将newDirection赋值给oldDirection,这样便解决了无效方向的问题。

解决了控制蛇移动的问题之后,应该为程序设计蛇吃食物的逻辑关系:蛇吃食物的问题较为简单,当蛇吃到食物后,随机产生新的食物,蛇是否吃到食物,在蛇移动一次之后调用一下snake.isEatFood()函数即可,随机产生食物只需在蛇吃到食物的前提下调用一次food.setFood()函数便可以完成。关于蛇吃到食物后蛇的变化,身体长度增加一格,snake.length++,只是实现了蛇的长度变量增加了一格,还需要在snakeBody集合中真正的添加一个格子单元,问题在于Square的一个对象应该加在集合的哪个位置、添加的格子坐标应该是多少,首先看一下蛇移动的功能实现模式,每移动一次是加头去尾,可以在蛇移动之后添加一个不再面范围之内的虚拟格子到snakeBody的末尾,这样蛇在下一次移动的时候就会去掉的尾巴便是虚拟的尾巴,相当于没有去尾,这样就实现了蛇自身长度加一格的功能。测试运行,效果已经不错了。

多次控制蛇吃到食物后,会发现食物会跑到石头上面,蛇吃到食物

的同时会吃到石头,意味着游戏结束,这就需要在生成食物之后判断一下食物是否在石头上,调用一下isFoodOnStone();问题基本解决,同样测试运行,又会发现小问题,有时候食物会在四回头上显示一下,在蛇移动一下之后再重新生成新的食物,这是由于生成食物和蛇移动是在同一个线程中锁产生的,有不可能单独为食物创建一个线程,解决此问题就需要先让食物生成一次,循环判断食物是否在石头上,若是,继续重新生成,不是才跳出循环,可以用到do{}while()语句来实现。

最后一个逻辑问题便是蛇吃到石头后,游戏结束:

run方法中while(true)条件需要重新定义,在Logic中定义一个boolean 类型的变量isGameOver,在move之后加以判断蛇是否吃到石头,是则设置isGameOver为true,while中的条件应该设置为!

isGameOver。吃到石头后,在下一次蛇移动之前判断条件的时候便会跳出循环,执行完run()方法体中的语句之后,logic的线程会自动结束,完成了游戏结束的功能。

到此为止,贪吃蛇游戏的基本功能都以实现,做一个标志性的存盘处理。

6.增设暂停\开始按钮,实现游戏暂停\开始的功能

暂停游戏的方法有多种,可以从线程的方向去考虑,让游戏暂停可以使用线中的waite和notify机制,但是waite和notify机制中必需有多个线程,还必须在同一个锁中才可以唤醒另一个等待的线程过于繁琐。

让游戏暂停也即是让蛇不在移动,食物不再随机产生,可以在Logic中设置一个标记变量,定义为boolean类型的isSuspend,在蛇移动的时候先判断游戏是否为暂停状态,暂停状态步调用move方法,实现游戏的暂停效果。

考虑好暂停的逻辑之后,可以做暂停效果了,先在主函数中创建JButtion suspendButtion按钮,将暂停按钮suspendButtion添加到JFrame 中,JFrame默认布局管理器为边界布局BorderLayout,为了不覆盖原来

的显示面板,可以在jf.add(),中传入两个参数,suspendButtion,BorderLayout.NORTH,在Frame的最上边添加一个按钮。为按钮添加监听器也采用匿名内部类的方法来实现,suspendButtion.addMouseListener(new MousListener(){}),实现未创建的功能函数,主要为监听器添加单击事件的监听,在mouseClicked()函数中设置单击后改变logic的isSuspend值为!isSuspend。暂停\开始的按钮已添加完毕,功能也已实现。测试没什么问题继续后面辅助功能的添加。

7.增设“开始新游戏”按钮,并能够在游戏时重新开始游戏:

先考虑开始新游戏的思路,开始新游戏之后蛇的位置会被初始化,食物被初始化,地图被初始化,线程需要被重新开启,有了这些基本思路后开始实现功能,首先是创建按钮JButtion restarButtion,将按钮添加到JFrame中,为restarButtion添加监听器,监听器的添加同样采用创建匿名内部类的方法创建,restarButtion.addMousListener(new Moustener(){}),实现功能函数mouseClicked();函数中初始化logic,调用函数logic.init()。初始化完成之后创建新的线程,Thread t = new Thread (logic),开启线程t.start();测试新加功能的效果。当游戏结束之后,单击“开始新游戏”按钮,游戏会重新开始。

在游戏过程中不小心单击了一下重新开始按钮,游戏依然重新被创建,但速度会被加快,再次单击重新开始游戏,速度更快,寻找问题的根源,当我们重新开始游戏的时候,logic对象被初始化,创建了新的线程,可是原来的线程并没有结束,两个线程操控的依旧是同一个对象,相当于在同一段时间内,调用了两次move()函数,因此在restar的时候应该对线程进行判断,判断线程是否结束,借用之前的标记isGameOver 若是游戏结束的话,以为着线程结束,再重新创建新的线程,开启线程。

若是游戏还没有结束,意味着原来的线程还没有结束,就不需要创建新的线程,只需对logic进行初始化即可。在对程序进行测试,在开始游戏之前点击一下暂停游戏,在点击开始新游戏按钮,蛇不会动,只有再次点击暂停\开始按钮游戏才会被启动,这有违正常的操作习惯,需要在点击开始新游戏按钮的功能中强制将isSuspend设为false。

到此程序除了框架之外也有的往常游戏最基本的功能,再做一标志性的存盘处理。

8.为游戏添设不同的关卡:

首先创建一个关卡信息的类StageInfo,这个类用于存储关卡的信息,应该具有改变关卡的功能,通过传入不同的关卡参数,创建不同的关卡。

同样是处理逻辑的一个类,与Logic放在同一个包下,至于Logic与

StageInfo两个类之间的关系是Logic处理StageInfo还是相反的处理方式

都无所谓,这里采用让StageInfo处理Logic,那么StageInfo中应该创建

Logic的实例对象,不同关卡之间的区别主要是地图和蛇移动速度的不

同,根据前面石头类设置的7个地图把关卡设为7关,关卡没提高一个

层次,调用的石头类对象产生的地图难度加大,在Logic中需要新曾一个

speed的int型变量用于控制蛇移动的速度,将speed传递给Logic中run

方法的sleep中,通过移动之间线程休眠时间的减短与增加控制蛇的移

动速度,改变关卡只需对logic中的snake,stone,food对象赋不同的值

即可,相当与是初始化StageInfo,改变关卡的功能函数命名为init()。

关卡信息处理好,要在游戏中实践需要在框架上添加新的按钮“上一关”、“下一关”都用来实现调用不同的关卡。同样添加MouseListener

监听器上一关对单击事件的处理方式为让StageInfo中的stage—然后调

用StageInfo的init();下一关对单击事件的处理方式为让StageInfo中

的stage++然后调用StageInfo的init()。

为了游戏的美观,在游戏开始之前让界面实现关卡信息,这里借用坦克大战开始信息界面作为游戏开始前关卡的信息,在StageInfo中添加

drawStage()函数,实现显示一张类似于图片的信息,设置背景等自己

认为比较好的效果。

三、备注:后面的功能在编写的程序中还未实现或者功能还不够完美,这里

只阐述实现功能的思想,方法,可能会存在不足之处,待程序完善功能并测试之后详细阐述程序设计步骤。

1.为游戏增加背景音乐以及不同场景的音频处理:

要为游戏添加音乐,首先选好所需声音素材,背景音乐选Linkin Park的Pale;蛇吃到食物时的声音选取最进比较流行的植物大战僵尸中大嘴花吃僵尸的声音;游戏开始\暂停选童年时期玩的街机游戏中的暂停\开始音效;游戏结束选名为“啊喔”的消息提示声音。

选好素材之后,实现声音播放的功能。Java se中处理声音有3种方法,一种是利用Applet类中的play()方法,一种是安装sun公司提供的专门处理声音视频的开发包,还有一种是利用输入输出流处理,这里选则第三种处理方式,先创建字节型输入输出流,创建一个字节输入输出流的缓冲区,由于播放声音与玩游戏是同步进行的,这里关于声音的处理应该再开启一条线程,同样实现Runnable接口中的run()方法,在游戏逻辑类中通过传入不同的音频文件来实现不同场景下播放部同的声音。

2.实现游戏存盘和读盘的功能:

存盘功能的实现要考虑需要存储的数据,第一条应该是关卡信息,这样在读盘时便可以根据读到的数据创建相同的关卡地图、速度等信息,第二条应该是当前的分数,直接换行存储即可,第三条最复杂应该是蛇身的信息,要保存蛇身体每一截的坐标,还要存储蛇当前移动的方向。

最所需保存的数据分析过之后,就该考虑存储的方式,由于索要存储的数据仅仅是一些简单的数据,数据量还很小,只需要用一个文件存储即可。接下来便是实现功能,首先应该创建一个输入输出流,然后将关卡信息、当前分数,当前蛇移动的方向按照writeLine()方式写入到信息文件下,关于蛇身体的存储,从蛇头到蛇尾依次换行写入,最后关闭文件,关闭流。

读盘与存盘相似,先创建输入流,从存储文件中readLine(),第一行信息应该是关卡信息,将读取的String数据转换成int型传给StageInfo,第二行信息应该是当前分数,同读取关卡信息相同将读到的数据转换类型传给score,方向也以同样的方式得到,到蛇身数据的读取了,由于不知道之前存储是蛇身有多少截,只能加一个while循环以读取的下一行是否为空做为循环条件,没读到一次,在存储蛇的集合中

添加一个Square对象,这样即可实现读盘的功能,再在主函数中创建新赋值后的logic的线程即可实现接着上次存储的游戏进度继续游戏。3.创新模式的添加:

新模式的规则是根据经典模式逆向思维得到,当蛇吃到食物后,经过蛇的消化,会变为石头存在与面板中,蛇的长度会减短,直到蛇只剩下蛇头与蛇尾游戏胜利。

功能实现需要重新定义一个处理逻辑的类,与logic相似,同样要实现Runnable接口中的run()方法,方法中功能与logic基本相同,蛇吃到食物后,食物随机产生,蛇吃到石头后游戏结束,蛇吃到自己的身体后游戏结束,不同之处是在判断蛇是否吃到食物的处理,蛇吃到食物后,不在蛇身体上添加虚拟的一个格子,相反是在蛇尾与食物位置相同的时候再次removeLast(),这就需要定义一个oldFood,用来存储刚刚被吃到的食物的坐标信息,还需在蛇移动一格之后将map[oldFood.getx()][oldFood.gety()]赋值为true,表示食物以变成石头被加载到地图中。

4.游戏中随机生成一个心形的食物,吃到心形食物可以多加游戏成绩,心

形的食物每隔10秒会生成一次,食物在5秒之后会消失,或者在蛇吃到这个食物后消失:

再次创建类Prize,心形的食物与游戏关联性较小,令创建新的线程比较容易实现,先实现每隔十秒生成一次,定义boolean型变量isBe,每隔十秒生成一次,isBe = true;Thread.sleep(10000);就是每隔十秒给isBe赋值为ture,然后是新食物的存在时间只有5秒,5秒内,蛇可以移动5000/speed次,这里判断蛇是否吃到心形食物,没有则让线程休眠speed段时间,然后5000/speed次数--,直到次数变为0,跳出循环,while(time!=0&&!snake.isEatPrize){time -- };isBe = false。

完成线程的功能,在游戏创建的时候创建这个线程即可。

5.完成所有功能后对游戏综合测试。

相关文档
最新文档