贪吃蛇游戏规则:当蛇吃掉蛋后,蛇的身体变长,而且移动过程中不能碰到自己和墙壁。
项目搭建:
Snake(蛇):Snake有int x,int y所在窗口的x,y点的位置和direction运动方向三个属性;有两个方法:移动(move(Dir dir))和吃蛋(eat(Box e))方法;Box(蛋):Box有int x,int y所在窗口的x,y点的位置 ,并且有boolean live属性设置是否被吃掉 ;
Snode(蛇尾巴): 有int x,int y所在窗口的x,y点的位置
Yard(草坪):Yard有int width、int height宽和高的属性;
Dir(方向):蛇移动有四个方向 上、下、左、右可以定义一个枚举类型,因为枚举类型是定义有限个变量的类型;
项目分析:
Yard和Snake是聚合关系,并且是一对一;Box和Yard是组合关系,也是一对一;Snake吃Box是关联关系,Snake和Snode是组合关系,并且是一对多关系。
Yard和Box是组合关系,可以在Yard中调用BOX的setLive()方法,而 BOX彩蛋和蛇是关联关系,在关联关系中虽然可以调用 BOX的set方法 改变Box的属性值,但是并不符合软件编程思想,对于关联关系,一般使用观察者模式去改变关联关系对象的属性值,所以可以使用观察者模式创建蛋,当蛇吃到彩蛋时发出通知,然后Box自动创建彩蛋,此种方式在Yard中的paintComponent 方法就减少了获取彩蛋的功能,在 Yard中的paintComponent 方法承担了太多的任务,既要画横线,也要画竖线,也要画彩蛋,还要获取蛋!这个是比较重任务型的方法了;所以使用观察者模式,蛇吃到蛋之后,给Box发一个通知,让Box产生一个彩蛋的新坐标点;
给snake实现Serializable接口,实现点击关闭按钮,将贪吃蛇的运行状态保存到文件中,当再次启动的时候,将会从文件获取上次贪吃蛇的状态。
box.java
import java.util.Observable;
import java.util.Observer;
import java.util.Random;
public class Box implements Observer{
/**********Begin:省略代码:定义属性:End******************/
private int x;
private int y;
private boolean live;
/*********End:定义属性******************/
/**********Begin:定义构造方法:**********************/
private Box(){
Random random=new Random();
this.live=true;
//Box生成的位置为10的倍数,并且都在窗体的大小范围之内
x = random.nextInt(80)*10;
y = random.nextInt(60)*10;
}
/*******Begin:set/get方法*****************************************/
public int getX() {return x;}
public void setX(int x) {this.x = x;}
public int getY() {return y;}
public void setY(int y) {this.y = y;}
public boolean isLive() {return live;}
public void setLive(boolean live) {this.live = live;}
/*******End:set/get方法*****************************************/
/**********************Begin:创建一个蛋,当蛋被蛇吃了之后再次产生一个蛋**********/
/**********************End:创建一个蛋,当蛋被蛇吃了之后再次产生一个蛋*****************/
/************* Begin:重写toString方法 *******************************************************/
public String toString() {
//将hashcode转为十六进制显示
return String.valueOf(Integer.toHexString(hashCode())).toUpperCase();
}
/************* End:重写toString方法 **********************************************************/
private static Box box;
public static Box takeBox(){
if(box==null){
synchronized(Object.class){
if(box==null){
box=new Box();
}
}
}
return box;
}
//观察者模式,当蛋被吃掉时,再次创建新的蛋。
public void update(Observable o, Object obj) {
if(live==false){
Random random=new Random();
this.x=random.nextInt(80)*10;
this.y=random.nextInt(60)*10;
this.setLive(true);
}
}
}
dir.java 移动的方向
public enum Dir {
LEFT,RIGHT,UP,DOWN,STOP;
}
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Observable;
public class Snake extends Observable implements Serializable{
/**********Begin:定义属性******************/
private int x=50;
private int y=50;
private Dir dir;
private ArrayList<Snode> snodeBox=new ArrayList<Snode>();
private boolean run;
/**********End:定义属性******************/
/**********Begin:定义构造方法**********************/
public Snake() {
//让蛇的默认方向为右
this.dir=Dir.RIGHT;
this.run=true;
snodeBox.add(new Snode());
}
/**********End:定义构造方法**********************/
/**********Begin :定义set/get方法*******************/
public int getX() {return x;}
public void setX(int x) {this.x = x;}
public int getY() {return y;}
public void setY(int y) {this.y = y;}
public ArrayList<Snode> getSnodeBox() {return snodeBox;}
public void setSnodeBox(ArrayList<Snode> snodeBox) {this.snodeBox = snodeBox;}
public Dir getDir() {return dir;}
public void setDir(Dir dir) {this.dir = dir;}
public boolean isRun() {
return run;
}
public void setRun(boolean run) {
this.run = run;
}
/************End:定义set/get方法******************************/
/***********Begin:蛇根据方向改变x、y的值*********************/
public void move(Dir dir){
switch (dir) {
case UP:
y-=10;
break;
case DOWN:
y+=10;
break;
case LEFT:
x-=10;
break;
case RIGHT:
x+=10;
break;
}
// 设置前一个节点获取上上个节点的位置
for (int j = snodeBox.size() - 1; j > 0; j--) {
snodeBox.get(j).setX(snodeBox.get(j - 1).getX()); // 2 1 2
snodeBox.get(j).setY(snodeBox.get(j - 1).getY());
}
// 给头设置一个新的位置
snodeBox.get(0).setX(this.x);
snodeBox.get(0).setY(this.y);
}
/***********End:蛇根据方向改变x、y的值*********************/
/*******Begin:省略代码:创建一个蛋,当蛋被蛇吃了之后通知再次产生一个蛋:End**********/
public void eat(Box box){
Snode snode = new Snode();
snodeBox.add(snode);
setChanged();
addObserver(box);
notifyObservers();
}
}
蛇的节点类sonde.java
import java.io.Serializable;
public class Snode implements Serializable {
private int x;
private int y;
/*****Begin:定义set/get方法**********************/
public int getX() {return x;}
public void setX(int x) {this.x = x;}
public int getY() {return y;}
public void setY(int y) {this.y = y;}
/*****End:定义set/get方法**********************/
}
常量类
public class Variables {
public static final int WIDTH=800;
public static final int HEIGHT=600;
}
yard类
因为重绘和蛇的移动是两个方法,应该放在两个线程中,一个线程负责重绘,一个线程负责移动蛇,这样更符合软件工程编程思想;可以使用公平锁,让重绘线程和蛇移动线程"一人一次"的方式做运作;
package snake;
import java.awt.Color;
import java.awt.Graphics;
import java.io.File;
import java.util.concurrent.locks.ReentrantLock;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
public class Yard extends JPanel {
private Snake snake;
private Box box;
public Yard(final Snake snake) {
box = Box.takeBox();
this.snake = snake;
this.setSize(Variables.WIDTH, Variables.HEIGHT);
this.setBackground(Color.white);
final ReentrantLock lock = new ReentrantLock(true);
/********** Begin:在线程的死循环中执行repaint方法 **************************/
new Thread(new Runnable() {
public void run() {
// 监控循环
try {
while (true) {
Thread.sleep(5);
while (snake.isRun() == true) {
lock.lock();
Thread.sleep(30);
repaint();
lock.unlock();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
/*************End:省略代码:蛇移动*************************************/
/*************Begin:检测碰撞自身*******/
/**
加了双层循环,最外层循环是监控空白键是否按下,内存死循环是用来执行重绘和蛇进行移动的必须给双重死循环之间加一个Thread.sleep(5),当snake.isRun()变量发生变化时,内层循环才能根据snake.isRun()的变量值判断是否终止循环;如果不在双层循环之间加上Thread.sleep(5)的方法,则当snake.isRun()变量发生变化时,内层循环的while监控不到snake.isRun()的变量变化;
*/
new Thread(new Runnable() {
public void run() {
try {
while (true) {
Thread.sleep(5);
while (snake.isRun() == true) {
lock.lock();
Thread.sleep(30);
snake.move(snake.getDir());
lock.unlock();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
public void run() {
try {
while(true){
Thread.sleep(2);
//蛇头是碰不到自己的前4个点的
for (int j = 5;j<snake.getSnodeBox().size();j++) {
Snode head=snake.getSnodeBox().get(0);
//因为下次重启的时候,在Snake的move方法,是先将头节点的坐标赋值给后面的节点,
//导致头节点的坐标和第一个节点的坐标一样,导致会进入if判断中;所以应该等蛇移动之后再去判断
Thread.sleep(2);
Snode snode=snake.getSnodeBox().get(j);
//因为图像在不断的重绘,重绘过程中会有延迟,所以要完全对准XY的坐标不是很现实,所以指定一个5的边界范围
if( Math.abs(head.getX()-snode.getX())<=5&&Math.abs(head.getY()-snode.getY())<=5){
snake.setRun(false);
Thread.sleep(5);
int option = JOptionPane.showConfirmDialog(Yard.this,"挑战失败,继续挑战?");
switch (option) {
case JOptionPane.OK_OPTION:
snake.setRun(true);
break;
case JOptionPane.NO_OPTION:
new File("D:/snake.data").delete();
break;
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
/***********End:检测碰撞自身************************************************/
}
/****************Begin:添加sankeMove方法************************************/
public void snakeMove(Dir dir) {
switch (dir) {
case UP:
if (snake.getDir() != Dir.DOWN) {
snake.setDir(Dir.UP);
}
break;
case DOWN:
if (snake.getDir() != Dir.UP) {
snake.setDir(Dir.DOWN);
}
break;
case LEFT:
if (snake.getDir() != Dir.RIGHT) {
snake.setDir(Dir.LEFT);
}
break;
case RIGHT:
if (snake.getDir() != Dir.LEFT) {
snake.setDir(Dir.RIGHT);
}
break;
case STOP:
snake.setRun((snake.isRun() == true) ? false : true);
break;
}
//snake.move(snake.getDir());
}
/****************Begin:添加sankeMove方法************************************/
/*************** Begin:重写paintComponet方法:当new Yard()时,该方法被执行***************/
protected void paintComponent(Graphics g) {
super.paintComponent(g);
// 画横线
for (int i = 0; i <= (Variables.HEIGHT / 10); i++) {
g.drawLine(0, 10 * i, Variables.WIDTH, 10 * i);
}
// 画竖线
for (int i = 0; i <= Variables.WIDTH / 10; i++) {
g.drawLine(10 * i, 0, 10 * i, Variables.HEIGHT);
}
// 让蛇的每一个节点都运动
for (int i = 0; i < snake.getSnodeBox().size(); i++) {
g.fillOval(snake.getSnodeBox().get(i).getX(), snake.getSnodeBox().get(i).getY(), 10, 10);
}
// 取巧
// box = Box.takeBox();
g.setColor(Color.pink);
g.fillOval(box.getX(), box.getY(), 10, 10);
// 吃到彩蛋
if (box.getX() == snake.getX() && box.getY() == snake.getY()) {
box.setLive(false);
snake.eat(box);
}
}
/***************** End:重写paintComponet方法:当new Yard()时,该方法被执行*******************************/
}
main.java
package snake;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import javax.swing.JFrame;
public class Main {
public static void main(String[] args) {
try {
JFrame win = new JFrame("贪吃蛇");
win.setSize(Variables.WIDTH, Variables.HEIGHT);
final File file =new File("D:/snake.data");
Object data=null;
if(file.length()!=0){
final ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file));
data = oin.readObject();
oin.close();
}
final Snake snake =(data==null)?new Snake():(Snake)data;
/************Begin:给窗口添加关闭事件End*********************************************/
win.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent event) {
try {
ObjectOutputStream ow=new ObjectOutputStream(new FileOutputStream(file));
ow.writeObject(snake);
ow.close();
} catch (Exception e) {
e.printStackTrace();
}
System.exit(0);
}
});
/************Begin:给窗口添加关闭事件*********************************************/
final Yard yard = new Yard(snake);
/***********************Begin:监听左、上、右、下键盘按键*******************************/
win.addKeyListener(new KeyAdapter() {
public void keyPressed(KeyEvent e) {
switch (e.getKeyCode()) {
//上箭头
case KeyEvent.VK_UP:
yard.snakeMove(Dir.UP);
break;
//下箭头
case KeyEvent.VK_DOWN:
yard.snakeMove(Dir.DOWN);
break;
//左箭头
case KeyEvent.VK_LEFT:
yard.snakeMove(Dir.LEFT);
break;
//右箭头
case KeyEvent.VK_RIGHT:
yard.snakeMove(Dir.RIGHT);
break;
case KeyEvent.VK_SPACE:
yard.snakeMove(Dir.STOP);
break;
}
}
});
/*********************End:监听左、上、右、下键盘按键*******************************/
win.setLocationRelativeTo(null);
win.add(yard);
win.setVisible(true);
} catch (Exception e) {
e.printStackTrace();
}
}
}