今天写了一个贪吃蛇的小游戏,我给他取名叫贪吃蛇无限版。这里开始讲解这个小游戏的编程思路以及代码实现。
游戏分析
我们应该要知道做的这个贪吃蛇的小游戏应该展示的状况:
使用上下左右键来控制蛇的移动,蛇在一个棋盘状的正方形中移动,棋盘中随机出现食物,蛇吃掉一个食物,身体变长一节,当蛇吃到自己的身体或者碰到墙壁,蛇死亡,游戏结束。
蛇的活动空间的设计(Yard)
- 因为需要显示出来,所以Yard类继承Frame类
- 可以把游戏的窗口大小看成一个二维坐标,蛇活动的区域就是二维坐标轴里的一个平面图形(默认为正方形)。
- 在程序主方法执行之前,需要确定离原点最近的图形的点的坐标(这里我们假设图形在坐标轴的第四象限)。我们可以理解为需要确定的点是正方形在坐标轴的左上角的点。
- 本次代码中我们将蛇活动的区域设置的是游戏窗口的一半大小,自己做的时候可以直接将蛇活动的区域设置为窗口大小
- 编写Yard的构造方法,在方法中设置Yard的大小(Yard的大小应该根据蛇的每一节的大小来确定)。
设置游戏窗口的宽度和高度 。
初始化yard对象并进行显示。 - 因为游戏是一个可以随时关闭的窗口,所以编写关闭窗口事件的代码,在窗口添加一个Windows事件消息,目的是我们关闭窗口的时候可以正常的退出。
- 因为游戏中蛇活动在网格中,现在我们来整理编写显示网格的代码的思路。
- 重写paint方法,并在pain方法中调用drawLine方法,来画蛇活动的网格。网格并不是一条线可以完成的,所以在这里使用for循环来完成画线的循环动作。
- 循环中,将i设置为i<=NodeCount,这样,就可以画出所需要的网格线,并且不会出现网格有一面没有框住的情况。
- 在drawLine()方法中 x1,y1; x2,y2分别代表的是横线和竖线的起始点和终点。
这样,蛇活动的网格就画好了,运行程序的效果就是一个窗口中有一个30*30的网格,并且窗口可以关闭。
public class Yard extends Frame { //因为蛇活动的空间需要显示出来
//蛇活动的空间的大小必须由蛇的每一节决定
public static final int NodeSize = 15;
public static final int NodeCount = 30; //count表示的是蛇的活动空间一共有多少行或者多少列
public static final int AreaSize = NodeSize * NodeCount;//将区域的大小设置成一个常量,以后访问会比较方便
//这是蛇活动的正方形所在游戏窗口的左上角点的位置
static int x = AreaSize / 2;
static int y = AreaSize / 2;
public static void main(String[] args) {
new Yard();
}
//书写Yard的构造方法
Yard(){
//设置蛇活动的正方形的宽度和高度
this.setSize(2*AreaSize,2*AreaSize);
this.setVisible(true); //初始化yard对象并进行显示
//在窗口添加一个Windows事件消息,目的是我们关闭窗口的时候可以正常的退出
this.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
//这个模块处理的是蛇活动的正方形的横线竖线问题
@Override
public void paint(Graphics g) {
//每次重画时,要使用白色将整个窗口重画一遍
//保存现场
Color c = g.getColor();
g.setColor(Color.WHITE);
g.fillRect(0,0,this.getWidth(),this.getHeight());
//画线
g.setColor(Color.BLACK);
//利用for循环来完成循环画线的动作并显示出网格
for (int i = 0; i <= NodeCount; i++){
//x1,y1; x2,y2分别代表的是横线和竖线的起始点和终点
g.drawLine(x,y+NodeSize*i, x+AreaSize, y+NodeSize*i);
g.drawLine(x+NodeSize*i, y, x+NodeSize*i, y+AreaSize);
}
}
}
蛇的节点(Node)
在上面我们已经整理好关于蛇的活动区域的整体思路以及代码展示,这时,开始整理画蛇的思路:
在贪吃蛇这个游戏中,蛇的身体是由一节一节组成的,而且蛇的移动也是按照一节一节的进行移动的,所以,我们先来画蛇的身体的节点。
- 因为蛇的活动区域是一个网格,所以,他一定要有两个属性,一个是他所在的行(row),另一个是他所在的列(col),而且还会有对他的两个属性写一个构造方法Node()。
- 因为Node是一个双向链表,所以他需要有前面节点和后面节点。
- 因为蛇是需要画(展现)出来,所以在这里构造一个paint方法。
public class Node {
//对于蛇的节点,要知道他是位于哪一行那一列
//row行 col列
int row,col;
//计算小方格所在的位置
//由于Node是一个双向链表,所以他一定会有两个节点
// prev前面的节点 next后面的节点
//这两个节点默认都为空
Node prev, next;
public Node(int row, int col) {
this.row = row;
this.col = col;
}
public void paint(Graphics g) {
//计算小方格所在的位置
int x = Yard.x + col * Yard.NodeSize;
int y = Yard.y + row * Yard.NodeSize;
Color c = g.getColor();
//设置小方格的颜色
g.setColor(Color.black);
//要上色的小方块所在的位置
g.fillRect(x, y, Yard.NodeSize, Yard.NodeSize);
//返回之前的颜色
g.setColor(c);
}
}
蛇本体(Snake)
怎么表示一条蛇?
贪吃蛇的身体可以使用一段可增长的数据进行表示,这里我们就想到了数组与链表这两种可以表示一段数据的类。
- 如果使用数组来表示蛇的移动的话
蛇移动时,蛇的身体移动的每一节都需要数组的所有元素的位置进行移动。 - 如果使用链表来表示蛇的移动的话
将链表的尾部删掉,在链表的头部new出来。(将链表的尾部的一部分删掉,然后在链表的头部添加与删除掉的部分等长的元素)
经过上述分析,我们可以知道数组和链表都可以表示这样的一条蛇,但是相比较而言,链表使用起来更方便,所以选择链表来表示这条蛇。
作为一条蛇,他一定要有一个脑袋和一个尾巴。
根据前面的分析,如果没有尾巴,那么蛇进行移动的时候每移动一格就要遍历一次,十分浪费时间, 所以添加尾巴的作用,就是使用空间换取时间。
设置头和尾 Node head,tail;
编写snake的构造方法,在方法中,新建一个head对象,将head的初始值设置出来。 - 已经知道了这条蛇的具体的属性,怎么来画这条蛇?
因为我们已经在蛇的节点中已经建立了画的paint方法,所以我们这里,假设蛇的长度是3个节点,首先确定蛇的头部,然后利用while循环链表从头到尾,挨个将蛇的节点画出来。
public class Snake {
//作为一条蛇,他一定要有一个脑袋和一个尾巴
Node head, tail;
Snake() {
head = new Node(20,20); //设置蛇的初始位置
tail = head; //此时,我们将蛇的长度设置为一节
}
public void paint(Graphics g) {
Node n = head;
while (n != null){
//根据面向对象编程的思想,只有Node自己知道怎么填充自己
n.paint(g);
n = n.next;
}
}
}
怎么使用链表来实现蛇的移动?
- 首先我们考虑 链表从头开始一个个向后推,移动到要删除的元素(链表尾)前,删除链表尾,链表尾前面的元素成为新的链表尾。
但是这个方法的缺点很明显:蛇的每次移动,链表都需要进行一次遍历,十分麻烦,而且效率不高。
所以我们对这个方法进行改进。 - 将链表中的所有元素都设置为互相指向(前后元素都可以互相指向,A->B的同时B->A)。将链表尾指向的上一个元素找出,然后删除链表尾,原链表尾指向的元素就是新的链表尾。
这样,蛇移动时就不需要每移动一格就遍历一次链表了。
在代码中考虑如何书写蛇的移动
可以考虑一下电影,电影的播放是一帧一帧完成的,从而完成人物的动作等等,让蛇动起来的原理和放电影一样,只需要 让窗口进行不断的重画,就可以让蛇动起来。
- 首先重画这个操作需要在Yard类中完成。
调用repaint方法,就能进行重画,但是一次重画就能让蛇动起来是不现实的,所以,使用 while循环让repaint方法不断的进行重复。但是重画的频率太快了也不行,所以使用Thread.sleep(),来进行间隔。
===>>这里有个小扩展,我们这样编写好程序后会出现窗口闪烁的问题,而且频率很快,这里我们使用一段代码来解决。
Image offScreenImage = null;
public void update(Graphics g) {
if (offScreenImage == null){
offScreenImage = this.createImage(this.getWidth(),this.getHeight());
}
Graphics gOff = offScreenImage.getGraphics();
paint(gOff);
g.drawImage(offScreenImage,0,0,null);
}
经过前面的操作,已经可以把蛇画出来了,现在我们编写让蛇动起来的代码。
- 首先,写一个添加头部的方法addToHead
- 这时,有一个问题,我们不知道这个新添加的头部应该放到哪个位置,上下左右?所以设计一个枚举对象来存储蛇的动作。
- 新建一个Node对象,代表添加到头部的Node,利用switch选择判断一下当前的蛇的动作朝向,并且计算出新添加的Node将要添加到哪里。新添加的元素作为链表的头部进行展示。
- 将新元素添加到链表头部,首先,将新建元素n作为新的head的值。将原来的链表头作为链表的下一个,指向新的元素。
- 因为蛇在移动过程中他的长度应该保持一定,在分析过程中我们已经知道蛇的移动大体上概括为"删头去尾"。所以,在这里我们新建一个deleteTail()方法来实现删除尾部的操作。
- 先进行一个判断,如果蛇的长度是一个空值,删除的操作毫无意义,所以进行一个判断
- 最后感谢大家的关注 欢迎添加qq 1723082823 进入我们的粉丝群 获取更多更新视频资料!。
- 将tail指向的上一节指向的下一节的元素的值设置为空。(也就是将原来的tail删除)在这个过程中需要将新tail与老tail的连接完全打断。如果不完全打断的话会出现内存泄漏的问题。
//添加头部的动作
public void addToHead(){
//新建一个对象,代表将要添加到头部的Node
Node n = null;
//利用switch判断蛇的动作,并且计算出新添加节点的位置
switch (dir){
case D:
n = new Node(head.row+1, head.col);
break;
case L:
n = new Node(head.row, head.col-1);
break;
case R:
n = new Node(head.row, head.col+1);
break;
case U:
n = new Node(head.row-1, head.col);
break;
}
/* 将原来的链表头作为链表的 下一个,指向新的元素
将新建元素n作为新的head的值*/
n.next = head;
head.prev = n;
head = n;
}
public void paint(Graphics g) {
Node n = head;
while (n != null){
//根据面向对象编程的思想,只有Node自己知道怎么填充自己
n.paint(g);
n = n.next;
}
//每重画一次,都需要让蛇移动一次
move();
}
private void move() {
addToHead();
deleteTail();
boundaryCheck();
}
- 两个方法都写完了,理论上蛇的移动就可以完成了,但是在我们的测试过程中发现蛇是无限延长而不是移动。我们可以使用背景重画来解决这个问题。将背景重画写到Yard类的paint方法中。
//让蛇动起来的原理就是不断地进行重画
while (true){
try {//重画的频率太高,所以加上限制
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.repaint();
}
蛇的控制
到了这里,我们的代码可以实现蛇的移动,蛇活动区域的显示。但是,我们发现,蛇的移动不受我们控制,所以我们现在开始编写控制蛇移动的代码。
在Yard类中添加键盘监听事件,添加键盘事件来控制蛇的移动。
this.addKeyListener(new KeyAdapter() {//KeyListener键盘监听器
public void keyPressed(KeyEvent e) {//keyPressed事件
s.keyPressed(e); //这里让snake自己处理键盘事件-->在Snake中重keyPressed
}
});
最后感谢大家的关注 欢迎添加qq 1723082823 进入我们的粉丝群 获取更多更新视频资料!
然后在Snake中重写keyPressed方法,使用switch判断按下的方向键,并且在方向键中添加蛇的动作。
public void keyPressed(KeyEvent e) {
int key = e.getKeyCode(); //判断按下的键
switch (key){
case KeyEvent.VK_LEFT: //按下的方向键
dir = Dir.L; //蛇行动的方向
break;
case KeyEvent.VK_UP:
dir = Dir.U;
break;
case KeyEvent.VK_RIGHT:
dir = Dir.R;
break;
case KeyEvent.VK_DOWN:
dir = Dir.D;
break;
}
}
蛇吃食物的动作
首先判断蛇是否吃到了食物,蛇吃到食物的结果我们可以理解为蛇的头部与食物重合,当蛇的头部与食物重合后,蛇的身体变长一节,同时原食物消失,在一个新的随机的位置出现一个新的食物,这个新食物出现的方法在egg的reAppear中实现。
//蛇吃掉食物的动作
public void eat(Egg egg) {
//判断蛇是否吃掉了食物
if (head.row == egg.row && head.col == egg.col){
//蛇的身体增加一节
addToHead();
//食物被吃掉,让食物在其他的地方再次出现
egg.reAppear();
}
}
蛇的食物(egg)
这个时候,我们的代码已经完成了蛇活动区域的展现,使用键盘对蛇移动的进行控制,这时,整个游戏就差蛇的食物没有完成了。
创建食物的属性,用来确定食物出现的位置。因为食物要出现在网格中,所以重写paint方法,让他把自己画出来。
这里,我们回到Yard类,设置食物的初始位置,在蛇的paint方法之前,调用食物的paint方法把食物画出来。
因为食物在被蛇吃掉后会消失,而且游戏还需要进行下去,所以我们写一个在原食物消失后,新食物随机出现的方法reAppear。
public class Egg {
//蛇的食物必须的属性
int row, col;
Random r = new Random();
//需要把这个食物画出来,新建一个构造方法构造Egg
public Egg(int row, int col) {
this.row = row;
this.col = col;
}
//画食物的方法
public void paint(Graphics g) {
//计算小方格所在的位置
int x = Yard.x + col * Yard.NodeSize;
int y = Yard.y + row * Yard.NodeSize;
Color c = g.getColor();
//设置小方格的颜色
g.setColor(Color.RED);
//要上色的小方块所在的位置
g.fillOval(x, y, Yard.NodeSize, Yard.NodeSize);
//返回之前的颜色
g.setColor(c);
}
//这时一个食物被吃掉后随即显示在其他位置的方法
public void reAppear() {
this.row = r.nextInt(Yard.NodeCount);
this.col = r.nextInt(Yard.NodeCount);
}
}
最后感谢大家的关注
欢迎添加qq 1723082823 进入我们的粉丝群 获取更多更新视频资料!