主要是利用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等。
以後想到什麼再隨時更新吧。如果有進階版也會分享出來。
點個贊吧!