【源码】扣丁学堂Java培训之小游戏贪吃蛇
2017-12-27 15:44:45
597浏览
##Java小游戏之贪吃蛇
贪吃蛇是一款经典的益智游戏,既简单又耐玩,游戏通过控制蛇的方向来吃蛋,获得分数的同时也让蛇变得越来越长。
###制作游戏窗口
要做一个游戏,首先得有一个窗口,在Java中可以通过继承JFrame类来定制一个带边框的窗口,继承是从一个已有的类创建新类的过程,子类可以直接得到父类的继承信息,这样的话我们非常轻松的就可以将一个游戏窗口创建出来。代码如下所示:
```Java
importjavax.swing.JFrame;
publicclassGameFrameextendsJFrame{
publicGameFrame(){
setTitle("贪吃蛇");//设置窗口标题
setSize(600,600);//设置窗口大小
setResizable(false);//设置窗口大小不可改变
setLocationRelativeTo(null);//设置窗口居中
setDefaultCloseOperation(EXIT_ON_CLOSE);//设置关窗口结束程序
}
publicstaticvoidmain(String[]args){
newGameFrame().setVisible(true);//创建窗口对象并显示窗口
}
}
```
###在窗口上绘图
如果想在窗口上定制绘图,需要重写继承自JFrame的paint方法,其实JFrame的这个方法也是继承自Window,通过重写方法我们可以让不同子类对父类已有的方法给出不同的实现版本,这正是实现多态最关键的一步。下面的代码演示了如何在屏幕上绘制一个绿色的小方块。
```Java
importjava.awt.Graphics;
importjava.awt.Color;
importjavax.swing.JFrame;
publicclassGameFrameextendsJFrame{
publicGameFrame(){
setTitle("贪吃蛇");
setSize(600,600);
setResizable(false);
setLocationRelativeTo(null);
setDefaultCloseOperation(EXIT_ON_CLOSE);
}
@Override
publicvoidpaint(Graphicsg){
super.paint(g);
g.setColor(Color.GREEN);//设置绘图的颜色
g.fillRect(60,60,20,20);//填充一个矩形区域
}
publicstaticvoidmain(String[]args){
newGameFrame().setVisible(true);
}
}
```
###动画的原理和实现
接下来我们要让窗口中的小方块动起来,想让它动起来,我们就不能将小方块的坐标用字面常量来书写,我们要把小方块在屏幕上的横坐标和纵坐标定义成变量,而且要定时修改变量的值还要刷新窗口(这一点可以通过定时器Timer类或者创建线程来做到),于是就有了下面的代码。
```Java
importjava.awt.Graphics;
importjava.awt.Color;
importjavax.swing.JFrame;
publicclassGameFrameextendsJFrame{
privateintx=60;
privateinty=60;
publicGameFrame(){
setTitle("贪吃蛇");
setSize(600,600);
setResizable(false);
setLocationRelativeTo(null);
setDefaultCloseOperation(EXIT_ON_CLOSE);
//创建线程对象并启动线程这里使用了Java8的Lambda表达式
//低版本的JDK也可以使用匿名内部类的方式创建Runnable对象
newThread(()->{
while(true){
//休眠50毫秒相当于每秒钟20帧
try{
Thread.sleep(50);
}catch(InterruptedExceptionex){
}
x+=5;
repaint();
}
}).start();
}
@Override
publicvoidpaint(Graphicsg){
super.paint(g);
g.setColor(Color.GREEN);
g.fillRect(x,y,20,20);
}
publicstaticvoidmain(String[]args){
newGameFrame().setVisible(true);
}
}
```
其实所谓的动画就是将不连续的图片连续的播放出来,就如同胶片电影一样。每一个图片就是动画中的一帧,如果我们每秒钟能够播放20帧,那就已经是一个非常流畅的动画了。
###双缓冲
做到这里可能大家已经发现屏幕上的方块闪烁得非常厉害,动画的效果并不是很好,原因很简单,如果直接在窗口上绘图就是无法避免这种问题,因此通常在渲染游戏界面的时候我们都是现在内存中进行图像的渲染,然后再把渲染好的整张图直接放到窗口上,这种技术称为“双缓冲”,接下来的代码使用了双缓冲来消除闪烁。
```Java
importjava.awt.Graphics;
importjava.awt.Color;
importjava.awt.image.BufferedImage;
importjavax.swing.JFrame;
publicclassGameFrameextendsJFrame{
privateintx=60;
privateinty=60;
//在内存中创建一张图用于实现双缓冲
privateBufferedImageimage=newBufferedImage(600,600,1);
publicGameFrame(){
setTitle("贪吃蛇");
setSize(600,600);
setResizable(false);
setLocationRelativeTo(null);
setDefaultCloseOperation(EXIT_ON_CLOSE);
newThread(()->{
while(true){
try{
Thread.sleep(200);
}catch(InterruptedExceptionex){
}
x+=5;
repaint();
}
}).start();
}
@Override
publicvoidpaint(Graphicsg){
//获得内存中的BufferedImage对象的绘图上下文
Graphicsg2=image.getGraphics();
//先在内存中进行渲染
super.paint(g2);
g2.setColor(Color.GREEN);
g2.fillRect(x,y,20,20);
//渲染完成后将图直接放到窗口上
g.drawImage(image,0,0,null);
}
publicstaticvoidmain(String[]args){
newGameFrame().setVisible(true);
}
}
```
###处理键盘事件
如果希望用键盘来操控方块移动的方向,我们可以给窗口添加键盘事件监听器来监听键按下的事件,我们可以用“WDSA”四个键来代表“上右下左”,要保存方向,可以定义代表方向的枚举类型,代码如下所示。
```Java
publicenumDirection{
UP,RIGHT,DOWN,LEFT
}
```
```Java
importjava.awt.Graphics;
importjava.awt.Color;
importjava.awt.image.BufferedImage;
importjava.awt.event.KeyAdapter;
importjava.awt.event.KeyEvent;
importjavax.swing.JFrame;
publicclassGameFrameextendsJFrame{
privateintx=60;
privateinty=60;
privateDirectiondir=Direction.RIGHT;
privateBufferedImageimage=newBufferedImage(600,600,1);
publicGameFrame(){
setTitle("贪吃蛇");
setSize(600,600);
setResizable(false);
setLocationRelativeTo(null);
setDefaultCloseOperation(EXIT_ON_CLOSE);
//添加键盘事件监听器(使用了KeyListener的适配器KeyAdapter)
addKeyListener(newKeyAdapter(){
@Override
publicvoidkeyPressed(KeyEvente){
//根据按键来决定方块移动的方向
switch(e.getKeyCode()){
caseKeyEvent.VK_W:
dir=Direction.UP;break;
caseKeyEvent.VK_D:
dir=Direction.RIGHT;break;
caseKeyEvent.VK_S:
dir=Direction.DOWN;break;
caseKeyEvent.VK_A:
dir=Direction.LEFT;break;
}
}
});
newThread(()->{
while(true){
try{
Thread.sleep(200);
}catch(InterruptedExceptionex){
}
//根据方向来控制方块的移动
switch(dir){
caseUP:y-=5;break;
caseRIGHT:x+=5;break;
caseDOWN:y+=5;break;
caseLEFT:x-=5;break;
}
repaint();
}
}).start();
}
@Override
publicvoidpaint(Graphicsg){
Graphicsg2=image.getGraphics();
super.paint(g2);
g2.setColor(Color.GREEN);
g2.fillRect(x,y,20,20);
g.drawImage(image,0,0,null);
}
publicstaticvoidmain(String[]args){
newGameFrame().setVisible(true);
}
}
```
###基于对象编程
写到这里,我们的代码还没有任何面向对象编程的味道,因为我们要做的是贪吃蛇,这明显是整个游戏中最重要的一个对象,为了创建这个对象,我们先要定义个类来描述蛇,而蛇又是由一个一个的节点构成的,因此我们先创建一个类描述蛇身上的节点,再在这个类的基础上创建蛇。
```Java
importjava.awt.Color;
importjava.awt.Graphics;
importjava.io.Serializable;
/**
*蛇身上的节点
*@author骆昊
*
*/
@SuppressWarnings("serial")
publicclassSnakeNodeimplementsSerializable{
privateintx;
privateinty;
privateintsize;
privateColorcolor;
/**
*构造器
*@paramx横坐标
*@paramy纵坐标
*@paramsize大小
*@paramcolor颜色
*/
publicSnakeNode(intx,inty,intsize,Colorcolor){
this.x=x;
this.y=y;
this.size=size;
this.color=color;
}
/**
*绘制蛇节点
*@paramg绘图的上下文
*/
publicvoiddraw(Graphicsg){
g.setColor(color);
g.fillRect(x,y,size,size);
g.setColor(Color.BLACK);
g.drawRect(x,y,size,size);
}
publicintgetX(){
returnx;
}
publicintgetY(){
returny;
}
publicintgetSize(){
returnsize;
}
publicColorgetColor(){
returncolor;
}
}
```
```Java
importjava.awt.Color;
importjava.awt.Graphics;
importjava.io.Serializable;
importjava.util.LinkedList;
importjava.util.List;
/**
*蛇
*@author骆昊
*
*/
@SuppressWarnings("serial")
publicclassSnakeimplementsSerializable{
privateList<SnakeNode>nodes;//保存蛇节点的列表容器
privateDirectiondir;//蛇前进的方向
publicSnake(){
nodes=newLinkedList<>();
//创建5个节点作为初始的蛇
for(inti=0;i<5;++i){
SnakeNodenode=newSnakeNode(280-20*i,280,20,Color.GREEN);
nodes.add(node);
}
dir=Direction.RIGHT;
}
publicDirectiongetDir(){
returndir;
}
/**
*移动(头的前面加一个节点并删掉最后一个节点)
*/
publicvoidmove(){
SnakeNodehead=nodes.get(0);
intx=head.getX();
inty=head.getY();
switch(dir){
caseLEFT:x-=20;break;
caseUP:y-=20;break;
caseRIGHT:x+=20;break;
caseDOWN:y+=20;break;
}
SnakeNodenewHead=newSnakeNode(x,y,20,Color.GREEN);
nodes.add(0,newHead);
nodes.remove(nodes.size()-1);
}
/**
*改变方向
*@paramnewDir新方向
*/
publicvoidchangeDirection(DirectionnewDir){
//如果蛇的新方向和旧方向不冲突就可以改变方向
if((dir.ordinal()+newDir.ordinal())%2!=0){
dir=newDir;
}
}
/**
*绘制蛇(把蛇的每个节点都绘制出来)
*@paramg绘图的上下文
*/
publicvoiddraw(Graphicsg){
for(SnakeNodenode:nodes){
node.draw(g);
}
}
}
```
###让蛇动起来
接下来我们只要在窗口中创建蛇的对象并且按照指定的时间间隔给蛇发出move消息,蛇就可以动起来了。
```Java
importjava.awt.Color;
importjava.awt.Graphics;
importjava.awt.event.KeyAdapter;
importjava.awt.event.KeyEvent;
importjava.awt.image.BufferedImage;
importjavax.swing.JFrame;
@SuppressWarnings("serial")
publicclassGameFrameextendsJFrame{
privateBufferedImageimage=newBufferedImage(600,600,1);
//创建贪吃蛇对象
privateSnakesnake=newSnake();
publicGameFrame(){
setTitle("贪吃蛇");
setSize(600,600);
setResizable(false);
setLocationRelativeTo(null);
setDefaultCloseOperation(EXIT_ON_CLOSE);
addKeyListener(newKeyAdapter(){
@Override
publicvoidkeyPressed(KeyEvente){
DirectionnewDir=snake.getDir();
switch(e.getKeyCode()){
caseKeyEvent.VK_A:
newDir=Direction.LEFT;break;
caseKeyEvent.VK_W:
newDir=Direction.UP;break;
caseKeyEvent.VK_D:
newDir=Direction.RIGHT;break;
caseKeyEvent.VK_S:
newDir=Direction.DOWN;break;
}
if(newDir!=snake.getDir()){
snake.changeDirection(newDir);
}
}
});
newThread(newRunnable(){
@Override
publicvoidrun(){
while(true){
//每隔100毫秒让蛇移动一次
snake.move();
repaint();
delay(100);
}
}
}).start();
}
privatevoiddelay(longmillis){
try{
Thread.sleep(millis);
}catch(InterruptedExceptione){
e.printStackTrace();
}
}
@Override
publicvoidpaint(Graphicsg){
Graphicsg2=image.getGraphics();
super.paint(g2);
snake.draw(g2);
g.drawImage(image,0,0,null);
}
publicstaticvoidmain(String[]args){
newGameFrame().setVisible(true);
}
}
```
###让蛇吃到蛋
要让蛇吃到蛋那么肯定要先把蛋的对象创建出来,为此我们定义了名为Egg的类,同时给Snake类添加了一个名为eatEgg的方法。
```Java
importjava.awt.Color;
importjava.awt.Graphics;
publicclassEgg{
privateintx;
privateinty;
privateintsize;
privateColorcolor;
publicEgg(intx,inty,intsize,Colorcolor){
this.x=x;
this.y=y;
this.size=size;
this.color=color;
}
publicintgetX(){
returnx;
}
publicintgetY(){
returny;
}
publicintgetSize(){
returnsize;
}
publicColorgetColor(){
returncolor;
}
publicvoiddraw(Graphicsg){
g.setColor(color);
g.fillOval(x,y,size,size);
}
}
```
```Java
publicclassSnakeimplementsSerializable{
//......
privatebooleanhasEatenEgg;
/**
*移动
*/
publicvoidmove(){
SnakeNodehead=nodes.get(0);
intx=head.getX();
inty=head.getY();
switch(dir){
caseLEFT:x-=20;break;
caseUP:y-=20;break;
caseRIGHT:x+=20;break;
caseDOWN:y+=20;break;
}
SnakeNodenewHead=newSnakeNode(x,y,20,Color.GREEN);
nodes.add(0,newHead);
//吃到蛋的时候不删除尾巴让蛇变长
if(hasEatenEgg){
hasEatenEgg=false;
}else{
nodes.remove(nodes.size()-1);
}
}
/**
*吃蛋
*@paramegg蛋
*@return吃到蛋返回true否则返回false
*/
publicbooleaneatEgg(Eggegg){
SnakeNodehead=nodes.get(0);
hasEatenEgg=head.getX()==egg.getX()&&
head.getY()==egg.getY();
returnhasEatenEgg;
}
//......
}
```
```Java
@SuppressWarnings("serial")
publicclassGameFrameextendsJFrame{
//......
privateEggegg=generateEgg();
publicGameFrame(){
//......
newThread(newRunnable(){
@Override
publicvoidrun(){
while(true){
snake.move();
//如果蛇吃到了蛋就重新生成一颗蛋
if(snake.eatEgg(egg)){
egg=generateEgg();
}
repaint();
delay(100);
}
}
}).start();
}
privatevoiddelay(longmillis){
try{
Thread.sleep(millis);
}catch(InterruptedExceptione){
e.printStackTrace();
}
}
//用随机的位置创建一颗蛋
privateEgggenerateEgg(){
intx=(int)(Math.random()*28+1)*20;
inty=(int)(Math.random()*27+2)*20;
returnnewEgg(x,y,20,Color.ORANGE);
}
@Override
publicvoidpaint(Graphicsg){
Graphicsg2=image.getGraphics();
super.paint(g2);
egg.draw(g2);
snake.draw(g2);
g.drawImage(image,0,0,null);
}
//......
}
```
###让蛇撞到墙
跟刚才一样,我们需要定义一个类来描述墙并创建墙的对象,然后给Snake类添加一个让蛇撞墙的方法。蛇如果撞到墙就会死掉,可以通过一个boolean类型的属性来表示蛇的死活。
```Java
importjava.awt.BasicStroke;
importjava.awt.Color;
importjava.awt.Graphics;
importjava.awt.Graphics2D;
/**
*围墙
*@author骆昊
*
*/
publicclassWall{
privateintleft;
privateinttop;
privateintright;
privateintbottom;
/**
*构造器
*@paramleft左上角横坐标
*@paramtop左上角纵坐标
*@paramright右下角横坐标
*@parambottom右下角纵坐标
*/
publicWall(intleft,inttop,intright,intbottom){
this.left=left;
this.top=top;
this.right=right;
this.bottom=bottom;
}
publicintgetLeft(){
returnleft;
}
publicintgetTop(){
returntop;
}
publicintgetRight(){
returnright;
}
publicintgetBottom(){
returnbottom;
}
/**
*绘制围墙
*@paramg绘图的上下文
*/
publicvoiddraw(Graphicsg){
g.setColor(newColor(220,100,70));
Graphics2Dg2d=(Graphics2D)g;
//设置绘图的粗细
g2d.setStroke(newBasicStroke(6));
g.drawRect(left,top,right-left,bottom-top);
}
}
```
```Java
publicclassSnakeimplementsSerializable{
//......
privatebooleanalive=true;
publicbooleanisAlive(){
returnalive;
}
/**
*撞墙
*@paramwall围墙
*/
publicvoidcollideWithWall(Wallwall){
SnakeNodehead=nodes.get(0);
//如果蛇头的位置到了围墙的外面就说明蛇撞到了围墙
if(head.getX()<wall.getLeft()||
head.getX()+head.getSize()>wall.getRight()||
head.getY()<wall.getTop()||
head.getY()+head.getSize()>wall.getBottom()){
alive=false;
}
}
//......
}
```
```Java
@SuppressWarnings("serial")
publicclassGameFrameextendsJFrame{
//......
privateWallwall=newWall(20,40,580,580);
publicGameFrame(){
//......
newThread(newRunnable(){
@Override
publicvoidrun(){
while(snake.isAlive()){
snake.move();
if(snake.eatEgg(egg)){
egg=generateEgg();
}
//检查蛇是否撞到围墙
snake.collideWithWall(wall);
//如果蛇没有死就刷新窗口
if(snake.isAlive()){
repaint();
}
delay(100);
}
}
}).start();
}
@Override
publicvoidpaint(Graphicsg){
Graphicsg2=image.getGraphics();
super.paint(g2);
egg.draw(g2);
snake.draw(g2);
wall.draw(g2);
g.drawImage(image,0,0,null);
}
//......
}
```
###让蛇咬到自己
这个我想不需要太多的说明了。
```Java
publicclassSnakeimplementsSerializable{
//......
/**
*咬自己
*/
publicvoideatSelf(){
SnakeNodehead=nodes.get(0);
for(inti=4;i<nodes.size();++i){
SnakeNodecurrent=nodes.get(i);
if(current.getX()==head.getX()
&¤t.getY()==head.getY()){
alive=false;
break;
}
}
}
//......
}
```
```Java
@SuppressWarnings("serial")
publicclassGameFrameextendsJFrame{
//......
publicGameFrame(){
//......
newThread(newRunnable(){
@Override
publicvoidrun(){
while(snake.isAlive()){
snake.move();
if(snake.eatEgg(egg)){
egg=generateEgg();
}
//检查蛇是否咬到了自己
snake.eatSelf();
snake.collideWithWall(wall);
//如果蛇没有死就刷新窗口
if(snake.isAlive()){
repaint();
}
delay(100);
}
}
}).start();
}
//......
}
```
###保存游戏进度
游戏的存档简单的说就是将游戏中对象的状态保存起来,当再次开始游戏时可以通过读取存档来获得游戏中对象的状态。在Java中,要将对象状态持久化可以通过序列化来完成,而要将对象还原回来执行反序列化即可。如果要完成贪吃蛇游戏的存档和读档,可以通过将蛇的状态序列化到文件中以及从文件中反序列化蛇的状态来实现。要支持序列化和反序列化操作的对象必须实现Serializable接口,好在刚才我们已经这样做了。
```Java
publicclassGameFrameextendsJFrame{
//......
privateSnakesnake=null;
publicGameFrame(){
load();//读档
//如果读档没有读到蛇就创建一条蛇
if(snake==null){
snake=newSnake();
}
//......
//关闭窗口时先存档再结束应用程序
addWindowListener(newWindowAdapter(){
@Override
publicvoidwindowClosing(WindowEvente){
//游戏存档-对象写到文件流中
try(OutputStreamout=newFileOutputStream("snake.sav")){
ObjectOutputStreamoos=newObjectOutputStream(out);
oos.writeObject(snake);
}catch(IOExceptionex){
ex.printStackTrace();
}
System.exit(0);
}
});
//......
}
//读档
privatevoidload(){
//游戏读档-从流中读取对象
try(InputStreamin=newFileInputStream("snake.sav")){
ObjectInputStreamois=newObjectInputStream(in);
snake=(Snake)ois.readObject();
}catch(Exceptione){
//e.printStackTrace();
}
}
//......
}
```
###总结一下
到这里一个简单的贪吃蛇游戏就算基本完成了,当然这里面还有一些小的bug,比如如果手速够快,可以让蛇在向左移动时调头向右,也可以让蛇在向上移动时调头向下,这些肯定是不合理的,当然这些bug也很容易解决。如果愿意的话,我们还可以通过蛇吃到的蛋的数量来计算分数,并在游戏结束时记录下最好成绩。在蛇死掉时,我们可以重新开始游戏,也可以支持多人联机对战,这些都是大家可以去自由发挥的内容。最后附上一张游戏运行效果的贴图,祝大家玩得愉快。
【关注微信公众号获取更多学习资料】
查看更多关于“Java开发资讯”的相关文章>>
标签:
JavaEE视频教程
JavaEE培训
JavaEE开发工程师
Java培训