相信大家小时候都玩过贪吃蛇,对这很熟悉,所以不多说废话,直接来分析思路。
游戏中主要有这几个类,院子(Yard)、蛇(Snake)、给蛇吃的蛋(Egg)、构成蛇的结点类(Node)。当然,构成蛇结点的类最好作为蛇的内部类,但为了好具体操作,我将构成蛇的结点类没有设置成蛇的内部类。
接下来便是对游戏的功能进行分析。首先,我们得有一个院子,这是最基础的部分,接着得画出一条蛇,让它能够在键盘控制下移动。最后,蛇挂掉之后我们得让游戏画面停止刷新,并提示游戏结束,按相关键后又可以重新游戏。
设计这个游戏的难点就是实现蛇的移动。
其余情况不一一列举了,综上我们可以发现,蛇的移动相当于将尾结点直接放到了头结点,所以我们可以用一个双向链表来存储蛇的结点,每当蛇移动一格时,我们就先将双向链表的尾结点加到头结点前,成为新的头结点,再删除尾结点,此时的尾结点相当于原来尾结点的前一个结点。又由于蛇吃蛋时需要插入结点到头部或者尾部,移动时必须头插法。所以,我最终选择头插法,当然也可以加上尾插法。
第一步:绘制出院子(Yard),主要绘制出有多少行多少列格子,并定义枚举类型变量Direction,分别用来指示四个方向U, D, L, R(上下左右)
第二步:设计结点Node类,写出move方法, draw方法,绘制一个Node结点到屏幕上,并能通过键盘控制一个Node结点在屏幕上移动,其中Node的move方法仅做测试用,第二部完成即可删除。
第三步:设计蛇(Snake)类,最重要的便是头插法建立一个带头结点和带尾结点的双向链表,蛇的移动概括为先将尾结点加到头结点(此时还没删除尾结点),再来删除尾结点,新生成的双向链表的尾结点为原来尾部结点的前一个结点。最后,让蛇在键盘控制下移动。
第四步:设计蛋(Egg)类,让Egg被Snake吃掉后能够随机出现在屏幕上另外一个地方。当然,蛇吃掉蛋后通过头插法增加自己的长度
第五步:设计检测蛇是否死了的方法,一是越界,二是咬到自己。当蛇死了的时候,我们就将线程中repaint的方法挂起,当按F2时,又启用repaint方法,使得游戏重新开始。
第六步:当然是愉快的玩耍,测试bug罗。
是否觉得废话太多,那就直接看代码吧
/** * 用来指示方向的枚举变量,分别为上、下、左、右 * @author lu * */
public enum Direction {
U, D, L, R
}
import java.awt.Color;
import java.awt.Font;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
public class Yard extends Frame {
/** * 屏幕行数 */
public static final int ROWS = 40;
/** * 屏幕列数 */
public static final int COLS = 40;
/** * 屏幕方块宽度 */
public static final int BLOCK_SIZE = 15;
//flag,start用来控制线程中rePaint方法执行
private boolean flag = true;
private boolean start = true;
//记录得分
private int score = 0;
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
//用4个结点初始化蛇
Node node1 = new Node(25, 25, Direction.R);
Node node2 = new Node(30, 30, Direction.U);
Node node3 = new Node(40, 40, Direction.R);
Node node4 = new Node(50, 50, Direction.L);
Snake snake = new Snake(this);
Egg egg = new Egg();
//用于双缓冲,可以跳过不看
Image offScreenImage = null;
/** * 双缓冲,可以不用看,对最终结果影响不大 */
@Override
public void update(Graphics g) {
if (offScreenImage == null)
offScreenImage = this.createImage(Yard.COLS * Yard.BLOCK_SIZE,
Yard.ROWS * Yard.BLOCK_SIZE);
Graphics gOffScreen = offScreenImage.getGraphics();
gOffScreen.setColor(Color.DARK_GRAY);
gOffScreen.fillRect(0, 0, Yard.COLS * Yard.BLOCK_SIZE, Yard.ROWS
* Yard.BLOCK_SIZE);
paint(gOffScreen);
g.drawImage(offScreenImage, 0, 0, null);
}
public static void main(String[] args) {
new Yard().launchFrame();
}
public void launchFrame() {
this.setBackground(Color.DARK_GRAY);
this.setBounds(200, 50, COLS * BLOCK_SIZE, ROWS * BLOCK_SIZE);
this.setResizable(false);
this.addKeyListener(new KeyMonitor());
this.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent arg0) {
System.exit(0);
}
});
this.setVisible(true);
//构建4个结点的蛇
snake.addToHead(node1);
snake.addToHead(node2);
snake.addToHead(node3);
snake.addToHead(node4);
//启动重画的线程
new Thread(new myPaint()).start();
}
/** * 将所有对象画在院子里面画出来 */
@Override
public void paint(Graphics g) {
Color c = g.getColor();
//画线画笔颜色为黑色
g.setColor(Color.black);
// 画横线
for (int i = 1; i < ROWS; i++) {
g.drawLine(0, i * BLOCK_SIZE, COLS * BLOCK_SIZE, i * BLOCK_SIZE);
}
// 画竖线
for (int i = 1; i < COLS; i++) {
g.drawLine(i * BLOCK_SIZE, 0, i * BLOCK_SIZE, ROWS * BLOCK_SIZE);
}
//画得到的分数,画笔颜色设置为红色
g.setColor(Color.red);
g.drawString("Score:" + score, 30, 50);
//将画笔颜色设置为默认颜色
g.setColor(c);
//当吃掉一个蛋的时候,分数加5
if (snake.eat(egg))
score += 5;
//将蛋和蛇画到屏幕上
egg.draw(g);
snake.draw(g);
//flag默认值为true,当蛇死掉时候,flag = false,此时,执行下面语句,使得结束提示在屏幕上出现
if (!flag) {
Font f = g.getFont();
g.setFont(new Font("宋体", Font.BOLD, 50));
g.setColor(Color.red);
g.drawString("Game Over", 150, 200);
g.setFont(new Font("宋体",Font.BOLD, 30));
g.drawString("请按F2重新开始", 150, 250);
start = false;
}
g.setColor(c);
}
/** * 当蛇死掉时候,将flag设置为false的函数 */
public void stop() {
this.setFlag(false);
}
/** * 重新开始游戏的函数,当按F2时,执行这个方法,将snake指向一只新构建的蛇对象,再将该对象设置为与一开始蛇一样大小,一样的位置 */
public void reStart() {
start = true;
flag = true;
snake = new Snake(this);
snake.addToHead(node1);
snake.addToHead(node2);
snake.addToHead(node3);
snake.addToHead(node4);
}
/** * 用来监听键盘上的按键,主要为上下左右, F2 * @author Lu * */
class KeyMonitor extends KeyAdapter {
/** * 重写KeyPressed方法,监听蛇上下左右,重新开始F2 */
@Override
public void keyPressed(KeyEvent e) {
snake.keyPressed(e);
int keyCode = e.getKeyCode();
if(keyCode == KeyEvent.VK_F2) {
reStart();
}
}
}
/** * 线程类,对屏幕上的图不停进行重画 * @author lu * */
class myPaint implements Runnable {
public void run() {
while (true) {
if (start) {
repaint();
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
/** * 构成蛇的结点类 * @author lu * */
public class Node {
/** * 结点宽度 */
int w = Yard.BLOCK_SIZE;
/** * 结点高度 */
int h = Yard.BLOCK_SIZE;
/** * 结点方向 */
Direction dir = Direction.D;
/** * 结点行数 */
int row;
/** * 结点列数 */
int col;
/** * 指向后一个结点的指针 */
Node next = null;
/** * 指向前一个结点的指针 */
Node pre = null;
/** * 因为结点长度和宽度都已经确定,故构造方法中给出结点所在行数、列数和方向即可 * @param row 行数 * @param col 列数 * @param dir 方向 */
public Node(int row, int col, Direction dir) {
this.row = row;
this.col = col;
this.dir = dir;
}
/** * 画出结点 * @param g 画结点的画笔 */
public void draw(Graphics g) {
Color c = g.getColor();
g.setColor(Color.red);
g.fillRect(col * Yard.BLOCK_SIZE, row * Yard.BLOCK_SIZE, w, h);
g.setColor(c);
}
/** * 为了进行碰撞检验的函数 * @return 返回包围结点的矩形 */
public Rectangle getRect() {
return new Rectangle(this.col * Yard.BLOCK_SIZE, this.row * Yard.BLOCK_SIZE, Yard.BLOCK_SIZE, Yard.BLOCK_SIZE);
}
}
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
/** * 蛇是由一个一个结点构成,为了处理方便,结点单独作为一个类,注意蛇的头结点存放数据,并不是头结点的下一个结点存放数据 * @author Lu * */
public class Snake {
//头结点
private Node head = null;
//尾结点
private Node tail = null;
//得到Yard的一个引用,方便调用Yard里面的方法
private Yard y = null;
/** * 将Yard对象引用传进来 * @param y Yard对象的引用 */
public Snake(Yard y) {
this.y = y;
}
//记录蛇是否还活着
private boolean live = true;
/** * 头插法构建双向链表,将新生成的结点插入到蛇头 * @param node 待插入的结点 */
public void addToHead(Node node) {
//当蛇还是空的时候,插入第一个结点的处理
if(head == null) {
head = tail = node;
head.next = null;
tail.next = null;
head.pre = null;
tail.pre = null;
return;
}
/** * 下面的为处理的具体过程 * 当头结点向下时,此时加入的结点应加在头结点的正下方 * 当头结点向上时,此时加入的结点位置为头结点的正上方 * 当头结点向左时,此时加入的结点位置为头结点的正左方 * 当头结点向右时,此时加入的结点位置为头结点的正右方 */
Node nTemp = null;
if(head.dir == Direction.D)
nTemp = new Node(head.row + 1, head.col, Direction.D);
else
if(head.dir == Direction.U)
nTemp = new Node(head.row - 1, head.col, Direction.U);
else
if(head.dir == Direction.L)
nTemp = new Node(head.row, head.col - 1, Direction.L);
else
if(head.dir == Direction.R)
nTemp = new Node(head.row, head.col + 1, Direction.R);
//插入结点,注意是双向链表
nTemp.next = head;
head.pre = nTemp;
head = nTemp;
}
/** * 蛇移动时改变方向 * 蛇移动的核心就是将蛇尾结点加入到蛇头,再将蛇头结点删除 */
public void changeDiretion() {
//将蛇尾结点插入到蛇头
this.addToHead(tail);
//将蛇尾结点删除
tail = tail.pre;
tail.next = null;
}
/** * 将蛇画出来 * @param g 画蛇的画笔 */
public void draw(Graphics g) {
//如果蛇死掉了,直接返回
if(!this.live) return;
//当蛇没有一个结点时,直接返回
if(head == null) return;
//检查蛇是否越过Yard的四边,如果越过,会调用y.stop方法,将线程中rePaint方法跳过
this.check();
//进行移动
this.changeDiretion();
//依次画出蛇的每一个结点
Node n = head;
while(n != null) {
n.draw(g);
n = n.next;
}
}
/** * 此方法用来监听键盘按键,改变蛇头方向,在Yard方法中调用 * @param e 监听键盘的类 */
public void keyPressed(KeyEvent e) {
int keyCode = e.getKeyCode();
switch(keyCode) {
case KeyEvent.VK_UP:
if(head.dir != Direction.D)
head.dir = Direction.U;
break;
case KeyEvent.VK_DOWN:
if(head.dir != Direction.U)
head.dir = Direction.D;
break;
case KeyEvent.VK_LEFT:
if(head.dir != Direction.R)
head.dir = Direction.L;
break;
case KeyEvent.VK_RIGHT:
if(head.dir != Direction.L)
head.dir = Direction.R;
break;
}
}
/** * 得到蛇头结点的矩形位置和大小,用于碰撞检测, * @return 返回头结点的矩形具体位置和大小 */
public Rectangle getRect() {
return new Rectangle(head.col * Yard.BLOCK_SIZE, head.row * Yard.BLOCK_SIZE, Yard.BLOCK_SIZE, Yard.BLOCK_SIZE);
}
/** * 检测蛋是否被吃掉 * @param egg 蛇要吃的蛋 * @return 如果吃掉,就 */
public boolean eat(Egg egg) {
//判断蛋是否被吃用碰撞检测
if(this.live && egg.isLive() && this.getRect().intersects(egg.getRect())) {
egg.setLive(false);
this.addToHead(new Node(egg.row * Yard.BLOCK_SIZE, egg.col * Yard.BLOCK_SIZE, Direction.D));
return true;
}
return false;
}
/** * 检测蛇头是否出界,蛇是否咬到自己 * */
public void check() {
//出界检测
if(this.head.col < 1 || this.head.row < 3 || this.head.col > Yard.COLS - 2 || this.head.row > Yard.ROWS - 2) {
y.stop();
}
//是否咬到自己检测
Node temp = head.next;
while(temp != null) {
if(temp.col == head.col && temp.row == head.row)
y.stop();
temp = temp.next;
}
}
}
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.util.Random;
public class Egg {
//蛋所在的行数和列数
int row, col;
//蛋的长和宽
int w = Yard.BLOCK_SIZE;
int h = Yard.BLOCK_SIZE;
//产生随机整数的类
private static Random r = new Random();
private boolean live = true;
public boolean isLive() {
return live;
}
public void setLive(boolean live) {
this.live = live;
}
/** * 蛋的构造方法 * @param row 蛋所在的行数 * @param col 蛋所在的列数 */
public Egg(int row, int col) {
this.row = row;
this.col = col;
}
/** * 蛋的另外一种构造方法,当蛋被吃掉时,调用此构造方法,使蛋出现在另外一个地方 */
public Egg() {
this(r.nextInt(Yard.ROWS - 4) + 3, r.nextInt(Yard.COLS - 4) + 3);
}
/** * 将蛋画出来 * @param g 画蛋的画笔 */
public void draw(Graphics g) {
this.reAppear();
Color c = g.getColor();
g.setColor(Color.blue);
g.fillOval(col * Yard.BLOCK_SIZE, row * Yard.BLOCK_SIZE, w, h);
g.setColor(c);
}
/** * 当蛋被蛇吃掉时,将蛋随机画到屏幕上另外一个地方 * @return 如果被蛇吃掉,重复出现在屏幕上另外一处的时候返回true, 否则返回false */
public boolean reAppear() {
if(!this.live ) {
this.row = r.nextInt(Yard.ROWS - 4) + 3;
this.col = r.nextInt(Yard.COLS - 4) + 3;
this.live = true;
return true;
}
return false;
}
/** * 得到包围蛋的矩形,用于碰撞检测 * @return 返回包围蛋的矩形 */
public Rectangle getRect() {
return new Rectangle(col * Yard.BLOCK_SIZE, row * Yard.BLOCK_SIZE, w, h);
}
}