【Java實踐】多線程遊戲之多彩飛機大戰v1(完整代碼、設計思路、技術要點)

主要是利用JAVA的swing和多線程做一個簡易飛機大戰的小遊戲,功能比較簡單。完整代碼已經上傳到 https://download.csdn.net/download/weixin_42368748/12137255 ,可免費下載。

遊戲規則

通過鼠標控制己方飛機的左右移動,移動到不同地方按下空格鍵切換不同的狀態(顏色),它發射出來的子彈要打到相同顏色的敵機才能使其擊毀。擊毀一架敵機加一分,敵機越界則己方扣一滴血,到0則遊戲結束。
在這裏插入圖片描述
在這裏插入圖片描述

基本框架

簡單講述一下設計思路,首先打開UI佈局,然後啓動飛行物管理線程;從而生成己方飛機,並且開啓管理子彈的線程和管理敵機的線程;這兩個線程生成子彈和敵機。

可以看看這個粗略的UML圖,大致理解各個類的關係:在這裏插入圖片描述
GameUI:遊戲佈局類。用JFrame和JPanel實現遊戲佈局;創建FlyCtrl對象,同時啓動後者這一線程。

FlyCtrl:飛行物管理類,同時也是一個管理整個遊戲的線程。包括創建Plane對象、ScoreBoard對象;啓動BulletThread線程和ShipThread線程;存儲上述兩個線程產生的Bullet對象和Ship對象,並進行管理。

Plane:己方飛機類。存儲己方飛機的位置、大小、狀態等信息;受監聽器MListener控制;提供位置信息給BulletThread;包含對自身的繪製。

ScoreBoard:得分和血量顯示類。存儲當前遊戲得分、己方飛機血量;包含加分、扣血等方法;回饋信息給FlyCtrl;作爲難度參考提供信息給ShipThread。

BulletThread:管理子彈生成的線程。根據Plane的位置和狀態定時生成Bullet對象,加入到FlyCtrl的子彈列表中。

ShipThread:管理敵機生成的線程。根據ScoreBoard提供的當前分數,按不同難度隨機生成Ship對象,加入到FlyCtrl的敵機列表中。

Bullet類和Ship類實現FlyObject接口,分別表示一個子彈和一個敵機,包含位置、大小、狀態等自身信息,以及移動、繪製自身、得到自身信息的方法。

重要代碼展示

挑幾個重點的講一下吧!

(1) UI佈局

  • 創建JFrame。
  • 創建中間面板,加進frame中。
  • 創建底部面板,添加標籤插入圖片,加進frame中。
  • 設置frame可見。
  • 創建FlyCtrl對象,傳入中間面板。
  • 創建新線程,傳入上述對象(任務)。
  • 線程start,設置標誌位。

重點就是啓動管理線程:
FlyCtrl實現Runnable接口,所以創建了它的對象後,賦值給一個新的線程,並且start即可。另外,設置標誌位是爲了方便該線程的控制。

JPanel mainPanel= new JPanel(); //中間區域 遊戲主要的區域
mainPanel.setPreferredSize(new Dimension(600,700));
jf.add(mainPanel, BorderLayout.CENTER);		

jf.setVisible(true);
 
FlyCtrl ctrl= new FlyCtrl(mainPanel);  //飛行控制線程,控制所有飛行物	    
Thread ctrlThread = new Thread(ctrl);
ctrlThread.start();
ctrl.setFlag(true);    

(2) 總控線程 FlyCtrl

飛行物管理類,同時也是一個管理整個遊戲的線程。結構如下:

  • 獲取畫板。
  • 創建Plane對象(設置監聽器)、ScoreBoard對象。
  • 創建對象並啓動BulletThread線程和ShipThread線程。
  • 創建兩個列表分別存儲待生成的Bullet對象和Ship對象。
  • 繪製全部物品:
    • 創建緩衝圖像。
    • 繪製己方飛機。
    • 對子彈進行處理:
      • 移動;
      • 檢測是否出界,若出界則刪除該子彈;
      • 檢測碰撞,若與不同顏色敵機碰撞則刪除子彈,同顏色則摧毀處理(刪除、音效)。
      • 繪製現有所有子彈。
    • 對敵機進行處理:
      • 移動;
      • 檢測是否出界,若出界說明越界,己方飛機扣血;
      • 繪製現有所有敵機。
    • 繪製得分血量牌。
    • 以上均是繪製到緩衝圖像上,現在再把緩衝圖像繪製到容器的畫板上。
  • 線程循環運行。

結合註釋看看代碼吧:

public class FlyCtrl implements Runnable{

	Plane myPlane;
	MListener listener;
	ScoreBoard scoreBoard;
	List<FlyObject> bulletList;
	List<FlyObject> enemyList;	
	AudioClip boomSound;
	AudioClip debloodSound;
	Graphics2D g;
	boolean flag; //線程運行的標誌
		
	//初始化
	public void init(){
		//創建己方飛機對象
		Plane myPlane = new Plane();  
	    listener.myplane= myPlane;
	    this.myPlane= myPlane;
	    //創建得分、血量顯示牌
	    scoreBoard = new ScoreBoard();
	    //存儲子彈和敵機的列表
	    bulletList = new ArrayList<FlyObject>();
		enemyList = new ArrayList<FlyObject>();
	    //啓動子彈管理線程
	    BulletThread bulletThread = new BulletThread(myPlane,bulletList);
	    bulletThread.start();
	    //啓動敵機管理線程
	    ShipThread shipThread = new ShipThread(enemyList,scoreBoard);
	    shipThread.start();
	}
	
	//構造方法
	public FlyCtrl(JPanel mainPanel){
		//獲取畫板
		g = (Graphics2D)mainPanel.getGraphics();
		g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON); //抗鋸齒
		//創建監聽器
		listener = new MListener();
		mainPanel.addMouseMotionListener(listener); //添加鼠標監聽器
		mainPanel.addKeyListener(listener);   //添加按鍵監聽器
		mainPanel.requestFocusInWindow();     //獲得焦點
		
		init(); //初始化
	    
	    try {  //生成聲音
			boomSound = JApplet.newAudioClip(new File(".../boom.wav").toURI().toURL());
			debloodSound = JApplet.newAudioClip(new File(".../deblood.wav").toURI().toURL());
		} catch (MalformedURLException e) {
			e.printStackTrace();
		}
	}
	
	void setFlag(boolean t){  //設置線程運行標誌
		flag = t;
	}
	
	void crash(int bulletNo, int shipNo){ //碰撞處理方法
		bulletList.remove(bulletNo);
		enemyList.remove(shipNo);
	}
	
	//對子彈和敵機的移動、繪製、碰撞檢測
	void drawAll(Graphics g){		
		//創建帶緩衝區圖像
		BufferedImage bi = new BufferedImage(600, 700, BufferedImage.TYPE_4BYTE_ABGR);
		//可以理解爲獲取臨時畫布
		Graphics tmp_g = bi.getGraphics();
		//背景覆蓋
		tmp_g.setColor(Color.white);
		tmp_g.fillRect(0, 0, 600, 700);
    	//繪製己方飛機
    	myPlane.draw(tmp_g);
    	
    	//子彈的管理:移動、碰撞檢測、繪製
    	for(int i=0;i<bulletList.size();i++){
    		FlyObject tmp_b = bulletList.get(i);
    		if(tmp_b.move()){  //未越界
    			
    			boolean wh = true;  //是否有碰撞
    			for(int j=0;j<enemyList.size()&&wh;j++){ //遍歷敵機檢測碰撞
    				FlyObject tmp_ship = enemyList.get(j);
    				if( Math.abs(tmp_b.getX()-tmp_ship.getX()) < (tmp_b.getWidth()+tmp_ship.getWidth())
    						&& Math.abs(tmp_b.getY()-tmp_ship.getY()) < (tmp_b.getHeight()+tmp_ship.getHeight()) ){
    					//子彈和敵機碰撞
    					wh=false;
    					//相同顏色
    					if(tmp_b.getState() == tmp_ship.getState()){
    						boomSound.play(); //播放音效
    						scoreBoard.addPoint();
        					crash(i,j);        					
    					}else{ //不同顏色
    						bulletList.remove(i);
    					}   						
    				}   				
    			}    			
    			if(wh) //若沒有碰撞
    				tmp_b.draw(tmp_g);    			
    		}else{  //已越界
    			bulletList.remove(i);
    		}   		
    	}
    	
    	//敵機的管理:移動、越界處理、繪製
    	for(int i=0;i<enemyList.size();i++){
    		FlyObject tmp = enemyList.get(i);
    		if(tmp.move()){
    			tmp.draw(tmp_g);
    		}else{
    			enemyList.remove(i);
    			debloodSound.play();
    			if(!scoreBoard.deBlood()){  //飛船越界則扣血
    				//血量爲0
    				setFlag(false);				
    			}
    		}
    		
    	}
    	
    	//繪製得分和血量板
    	scoreBoard.draw(tmp_g);
    	
    	//把緩存畫布上的所有東西真正畫到JPanel的畫板上
    	g.drawImage(bi,0,0, null);
		try {
			Thread.sleep(10);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

	}
	
	//線程運行方法
	public void run(){
		while(flag){ //遊戲運行時
			drawAll(g);
		}
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		//遊戲結束彈窗
		String tip="遊戲結束,得分爲:"+scoreBoard.score+"。是否重新開始?";
		int i =JOptionPane.showOptionDialog(null, tip, "遊戲結束", JOptionPane.YES_OPTION, 0, null, null, null);
		if(i==0){  //重新開始
			init();        //初始化
			setFlag(true); //設置標誌位
			this.run();    //重新運行
		}
	}
}

判斷碰撞:| Xa - Xb | <= (Wa + Wb) 且 | Ya - Yb | <= (Ha + Hb) ,也就是橫(縱)座標之差的絕對值不大於兩者寬度(高度)之和即爲碰撞。另外,還要顏色相同纔算有效擊毀。

雙緩存圖像顯示聲音播放在後面技術要點中講。

(3) 管理生成的線程

BulletThread是管理子彈生成的線程:根據Plane的位置和狀態定時生成Bullet對象,加入到FlyCtrl的子彈列表中。類似的,ShipThread是管理敵機生成的線程:根據ScoreBoard提供的當前分數,按不同難度隨機生成Ship對象,加入到FlyCtrl的敵機列表中。

重點是把己方飛機、Ctrl中的子彈(敵機)列表傳進來,然後定期(隨機)產生新的子彈(敵機)對象,並放到列表中。

那麼只展示BulletThread的代碼吧(完整代碼):

public class BulletThread extends Thread{
	
	Plane myPlane;
	List<FlyObject> bulletList;
	Color colors[] = {new Color(176,153,23),new Color(34,177,76),
			new Color(0,162,232),new Color(137,2,145)};
	
	public BulletThread(Plane plane,List<FlyObject> bulletList){
		this.myPlane=plane;
		this.bulletList=bulletList;
	}
	
	
	//新建一個子彈
	void newBullet(int initX,int initY,int state){
		Bullet tempBullet = new Bullet(initX,initY,colors[state],state);
		bulletList.add(tempBullet);
	}
	
	public void run(){
		while(true){
			newBullet(myPlane.planeX,myPlane.planeY-35,myPlane.state);
			try {
				sleep(150);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

(4) 飛行物接口

自定義了一個飛行物的接口,讓子彈和敵機實現它,主要是爲了統一方法,和創建列表的時候可以把它們都放在一起。我原來的設計是己方飛機、子彈、敵機、分數牌都實現該接口,都放在一個列表裏面的,但是後面覺得這樣判斷的時候還要先識別類型,更加麻煩,所以最後就只是讓子彈和敵機實現該接口,己方飛機和分數牌單獨處理。如果後續還想添加可撿的道具、大boss等等這個接口的作用就更明顯了。

方法的作用看註釋:

public interface FlyObject {
	public boolean move();	//移動
	public void draw(Graphics g);	//繪製
	public int getX();	//返回自身橫座標
	public int getY();	//返回自身縱座標
	public int getHeight();	 //返回自身高度
	public int getWidth();	 //返回自身寬度
	public int getState();   //返回自身狀態(顏色)
}

技術要點

(1)線程創建

先說爲什麼用多線程。使用多線程可以讓不同的事情看上去可以同時被執行,例如生成子彈和生成敵機可以。代碼最終產生了:主線程、總控線程、子彈生成線程、敵機生成線程。線程的獨立性使得總控制、子彈生成、敵機生成可以分開進行,而不會互相牽制(至少在代碼層面是這樣)。

Thread是Java中用來表示線程的類,要建立線程就要: ①創建Thread對象,②給它賦值一個Runnable(任務),③啓動。

例如像這樣:

FlyCtrl ctrl= new FlyCtrl(mainPanel); //本質是一個Runnable
Thread ctrlThread = new Thread(ctrl);
ctrlThread.start();

又或者像這樣,三步並作兩步走:

//啓動子彈管理線程
BulletThread bulletThread = new BulletThread(myPlane,bulletList); //直接繼承Thread類
bulletThread.start();
//啓動敵機管理線程
ShipThread shipThread = new ShipThread(enemyList,scoreBoard);
shipThread.start();

(2)鼠標監聽器、按鍵監聽器

監聽鼠標拖拽就要用到 MouseMotionListener 這個監聽器接口,主要有兩個方法,分別是鼠標按下拖動和鼠標不按下拖動:

public void mouseDragged(MouseEvent e);
public void mouseMoved(MouseEvent e);

而監聽鍵盤按鍵則用到 KeyListener 這個接口,主要有三個方法,具體可以看我前幾天寫的這篇博客:【JAVA入門】鍵盤監聽器KeyListener

public void keyTyped(KeyEvent e);   //敲擊
public void keyPressed(KeyEvent e); //按下
public void keyReleased(KeyEvent e) //鬆開

(3)雙緩存圖像顯示

如果每個物品一產生或變化就直接畫在JPanel的Graphics上的話,就會有閃爍的現象,按我的理解是因爲顯示器從顯示器緩衝區獲取圖形,而圖形沒有一次性完整地顯示出來,而是每次顯示一部分,從而造成閃爍。具體原理可以看看這篇博客:http://blog.csdn.net/xiaohui_hubei/article/details/16319249

解決的方法是具有先創建一個可訪問圖像數據緩衝區的圖像BufferedImage,獲取它的Graphics,先把圖像畫到這個Graphics中,最後再把整個圖像畫到JPanel的Graphics中。可以理解爲,每次先把圖像都畫在一個臨時的緩衝畫板上,最後再把整個畫板畫在容器的畫板上。

例如:

BufferedImage bi = new BufferedImage(600, 700, BufferedImage.TYPE_4BYTE_ABGR);
Graphics tmp_g = bi.getGraphics(); //獲取緩衝圖像上的畫筆
tmp_g.setColor(Color.white); //背景覆蓋
tmp_g.fillRect(0, 0, 600, 700);
myPlane.draw(tmp_g); //繪製己方飛機
// ... bullet.draw(tmp_g);  //繪製所有子彈
// ... ship.draw(tmp_g);    //繪製所有敵機
// ...
g.drawImage(bi,0,0, null); //把緩衝圖像畫到JPanel的畫板上

(4)聲音播放

先把wav音頻文件賦值給File對象,然後用toURL方法把File轉爲urlAudio對象,然後用newAudioClip方法轉爲AudioClip對象,就可以在需要播放的時候直接對AudioClip對象用play方法播放了。不過運行時第一次播放的時候會有較大延遲。

//詳細分步:
File f = new File(".../XXX.wav");
URL urlAudio = f.toURL();
AudioClip ac = Applet.newAudioClip(urlAudio);
//一步搞定:
//AudioClip ac = JApplet.newAudioClip(new File(".../XXX.wav").toURI().toURL());

//播放
ac.play();  //單次播放
//ac.loop();  //循環播放
//ac.stop();  //停止播放

(5)彈窗

彈窗就要用到java.swing中的 JOptionPane 了,它主要有4個方法:

方法名 描述
showConfirmDialog 詢問一個確認問題 選擇有 yes/no/cancel
showInputDialog 提示要求某些輸入
showMessageDialog 告知用戶某事已發生
showOptionDialog 上述的集合

調用這些方法,然後設置參數即可。詳細可以參考一下這一篇博客:https://blog.csdn.net/qq_40791843/article/details/91047377

另外,調用這些方法會返回一個int,例如“是”就返回0,“否”就返回1,其他就返回-1,所以我這裏當返回0時就重新開始:

String tip="遊戲結束,得分爲:"+scoreBoard.score+"。是否重新開始?";
int i =JOptionPane.showOptionDialog(null, tip, "遊戲結束", JOptionPane.YES_OPTION, 0, null, null, null);
if(i==0){  //重新開始
	//...
}

後續可拓展方向

  • 增加道具,例如加快子彈發射,多子彈齊發,防護罩,清屏大招等等。只要增加一些實現飛行物FlyObject接口的類,並在敵機生成的線程中隨機生成它們的方法就行了。
  • 增加單機雙人玩法。增加己方飛機控制線程,並且更好地利用鍵盤監聽器即可。
  • 通信結合,實現在線雙人PK等。

以後想到什麼再隨時更新吧。如果有進階版也會分享出來。

點個贊吧!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章