天天看點

Java編寫貪吃蛇遊戲

貪吃蛇遊戲規則:當蛇吃掉蛋後,蛇的身體變長,而且移動過程中不能碰到自己和牆壁。

項目搭建:

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();
		}
	}
}