Java實現飛機大戰遊戲

classic123發表於2022-05-23

飛機大戰詳細文件

文末有原始碼,以及本遊戲使用的所有素材,將plane2檔案複製在src檔案下可以直接執行。

實現效果:

結構設計

  • 角色設計
    • 飛行物件類 FlyObject
      • 戰機類
        • 我的飛機 MyPlane
        • 敵方飛機 EnemyPlane
      • 子彈類
        • 我的子彈 MyBullet
        • 敵方子彈 EnemyBullet
      • 道具類 Prop
        • 加分,加血,升級
    • 地圖背景類 Background
    • 玩家類 Player
      • HP,得分
  • 執行緒類
    • 繪製執行緒 DrawThread
    • 移動執行緒 MoveThread
    • 生成敵方飛機執行緒 EnemyPlaneThread
    • 敵方飛機生成子彈執行緒 EnemyButtleThread
    • 檢測碰撞執行緒 TestCrashThread
  • 介面類
    • 主介面 GameUI
    • 選擇地圖介面 SelectMapUI
  • 監聽器類 KListener
    • 通過按壓鍵盤改變我方飛機的速度
  • 資料結構
    • 我方戰機(只有一個)
    • 我方飛機子彈集合
    • 敵方飛機集合
    • 敵方子彈集合
    • 道具集合

詳細分析

Main介面類

  • 使用邊框佈局,給皮膚分三個區,如圖所示

  • 關鍵程式碼:
        JFrame jf = new JFrame("飛機大戰"); //建立窗體
        jf.setSize(670,800);
        jf.setLocationRelativeTo(null);
        jf.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        jf.setLayout(new BorderLayout()); //佈局

        //建立三個JPanel,左上為按鈕,左下為分數顯示 右為遊戲頁面
        JPanel left = new JPanel(); 
        JPanel leftUp = new JPanel();  //左上
        JPanel leftDown = new JPanel();   //左下
        game = new JPanel();   //遊戲顯示區

        left.setPreferredSize(new Dimension(170,800));
        left.setBackground(new Color(-3355444));
        jf.add(left,BorderLayout.WEST);

        jf.add(game,BorderLayout.CENTER);
        game.requestFocus();
        left.setLayout(new BorderLayout());

        leftUp.setPreferredSize(new Dimension(0,250));
        leftUp.setBackground(new Color(-3355444));
        left.add(leftUp,BorderLayout.NORTH);

        leftDown.setBackground(new Color(-6710887));
        leftDown.setPreferredSize(new Dimension(0,550));
        left.add(leftDown,BorderLayout.SOUTH);

繪製背景地圖

飛行道具類

  • UML圖

  • 判斷FlyObject物件是否碰撞
public boolean judge_crash(FlyObject fo){
        if(x+sizeX<fo.x || y+sizeY<fo.y || x > fo.x + fo.sizeX || y > fo.y+ fo.sizeY ){
            return false;
        }else{
            return true;
        }
    }

繪製執行緒: 如何讓我們的遊戲動起來

  • 視訊原理:我們在螢幕上看見的動態影像影像實際上由若干個靜止影像構成,由於人眼有暫留特性,剛顯示的影像在大腦中停留一段時間,若靜態影像每
    秒鐘變化25幅,那麼人的感覺螢幕上的影像是動的。
  • 繪製時要把所有的飛行物都繪製一遍,所以我們需要在每一個飛行物被建立時,新增到相關的飛行物集合中。(為了方便傳值,我們將集合設為靜態變數)
  • 我們的繪製執行緒,選擇每30ms繪製一次,注意先畫背景,然後再遍歷飛行物集合畫飛行物。

背景的繪製

要想繪製動態的背景,首先我們要先畫一張靜態的背景圖,那麼如何繪製一張靜態的背景圖呢?

獲取包中的圖片:

        String fileName_0 = "src\\plane2\\z_img\\img_bg_0.jpg"; //相對地址(和絕對地址區分開)
        BufferedImage bufferedImage;  
        bufferedImage = ImageIO.read(new File(fileName_0));  //將檔案讀出記錄在bufferedImage中,記得丟擲異常
        g.drawImage(bufferedImage,0,0,null);   // 將bufferedImage中的內容畫在畫筆g對應的地方

我們的地圖是一張可以從上往下無縫滾動的圖片,就像是這樣的圖

接下來,如何讓畫出連續的圖片呢?

在繪製函式中,有一個函式可以完美實現我們的需求

img – the specified image to be drawn. This method does nothing if img is null.
        dx1 – the x coordinate of the first corner of the destination rectangle. 
        dy1 – the y coordinate of the first corner of the destination rectangle.
        dx2 – the x coordinate of the second corner of the destination rectangle.
        dy2 – the y coordinate of the second corner of the destination rectangle.
        sx1 – the x coordinate of the first corner of the source rectangle.
        sy1 – the y coordinate of the first corner of the source rectangle.
        sx2 – the x coordinate of the second corner of the source rectangle.
        sy2 – the y coordinate of the second corner of the source rectangle.
        observer – object to be notified as more of the image is scaled and converted.

    public abstract boolean drawImage(Image img,
                                      int dx1, int dy1, int dx2, int dy2,
                                      int sx1, int sy1, int sx2, int sy2,
                                      ImageObserver observer);

比如說,我們的圖片高度為712個畫素點,我們在下一時刻,圖片向下移動了m個畫素點,那麼我們就將這張圖片的0 ~ 712-m 部分,繪製到遊戲介面的m ~ 712部分,
再將712-m ~ 712 部分繪製到遊戲介面的0 ~ m 部分;

接下來,我們就要確定 m 的值,這個就很簡單了,在繪製執行緒中,定義一個整數變數m ,每次繪製完 m++ 就可以了。(個人建議m+=2比較舒服)

/**
* @author liTianLu
* @Date 2022/5/21 23:33
* @purpose 繪製背景
 * 提醒: 這裡我寫了四種地圖的繪製,後面在選擇地圖時會用到。 
*/
public class BackGround {
    Graphics g;
    BufferedImage bufferedImage_1;
    BufferedImage bufferedImage_2;
    BufferedImage bufferedImage_3;
    BufferedImage bufferedImage_4;
    int w;
    int h;
    String fileName_1 = "src\\plane2\\z_img\\img_bg_1.jpg";   //地圖1
    String fileName_2 = "src\\plane2\\z_img\\img_bg_2.jpg";   //地圖2
    String fileName_3 = "src\\plane2\\z_img\\img_bg_3.jpg";   //地圖3
    String fileName_4 = "src\\plane2\\z_img\\img_bg_4.jpg";   //地圖4
    public BackGround(Graphics g) throws IOException {
        this.g = g;
        bufferedImage_1 = ImageIO.read(new File(fileName_1));
        bufferedImage_2 = ImageIO.read(new File(fileName_2));
        bufferedImage_3 = ImageIO.read(new File(fileName_3));
        bufferedImage_4 = ImageIO.read(new File(fileName_4));
        w = bufferedImage_1.getWidth();
        h = bufferedImage_1.getHeight();
    }
    /**
    * i : 向下移動了i個畫素
    * num : 用來控制繪製哪一個地圖
    */
    public void draw(int i , int num){ 
        switch(num){
            case 1 :
                g.drawImage(bufferedImage_1,0,i,w,i+h,0,0,w,h,null);
                g.drawImage(bufferedImage_1,0,0,w,i,0,h-i,w,h,null);
                break;
            case 2 :
                g.drawImage(bufferedImage_2,0,i,w,i+h,0,0,w,h,null);
                g.drawImage(bufferedImage_2,0,0,w,i,0,h-i,w,h,null);
                break;
            case 3 :
                g.drawImage(bufferedImage_3,0,i,w,i+h,0,0,w,h,null);
                g.drawImage(bufferedImage_3,0,0,w,i,0,h-i,w,h,null);
                break;
            case 4 :
                g.drawImage(bufferedImage_4,0,i,w,i+h,0,0,w,h,null);
                g.drawImage(bufferedImage_4,0,0,w,i,0,h-i,w,h,null);
                break;
        }

    }

    public int getH() {
        return h;
    }
}
  • 繪製執行緒:
            backGround.draw(m, player.mapNum);
            m = m+2;
            if(m>= backGround.getH()){
                m = 0;
            }

我的飛機的繪製

使用的飛機素材圖片:

飛機扇動翅膀的原理與視訊的原理相同,不停更換圖片,形成視覺暫留效果

//這裡僅使用了三張圖片來回切換,更多的圖片會有更好的效果
public void draw(int i){    //此處的i是用來控制顯示哪一張圖片的
        int j = i%30; // 150ms換一張 
        if (j<10){
            g.drawImage(plane_img,x,y,x+sizeX,y+sizeY,0,0,sizeX,sizeY,null);
        }else if(j<20) {
            g.drawImage(plane_img,x,y,x+sizeX,y+sizeY,0,sizeY,sizeX,2*sizeY,null);
        }else if(j<30){
            g.drawImage(plane_img,x,y,x+sizeX,y+sizeY,288,0,424,112,null);
        }

    }

敵方飛機,敵方子彈等飛行物的繪製原理與MyPlane相同,後面不在贅述。(為了簡化開發流程,飛行物可以不”扇動翅膀“)

移動執行緒

  • 我們已經給每個飛行物件設定了X軸移動速度和Y軸移動速度,所以每次移動的時候,我們只需要遍歷所有的飛行物件,
    然後逐個移動一個speedX 和 speedY 單位即可。
  • 多久移動一次呢?和繪製執行緒的間隔時間相同就好了,我們都設為30ms.
  • 當飛行物飛出螢幕時,將飛行物移出集合,減少計算機資源的消耗。

如何控制我的飛機移動?

  • 當然是通過鍵盤的 ↑ ↓ ← → 來控制了,我們需要設定一個鍵盤監聽器給game介面,
  • 注意要先使用 game.requestFocus(); 獲取焦點,鍵盤監聽器才可以使用。
    @Override
        //鍵盤按壓時,設定速度
    public void keyPressed(KeyEvent e) {
        int c = e.getKeyCode();
        if(DrawThread.myPlane!=null){
            switch (c){
                case 37:
                    DrawThread.myPlane.setSpeedX(-speed);
                    break;
                case 38:
                    DrawThread.myPlane.setSpeedY(-speed);
                    break;
                case 39:
                    DrawThread.myPlane.setSpeedX(speed);
                    break;
                case 40:
                    DrawThread.myPlane.setSpeedY(speed);
                    break;
            }
        }
    }

@Override
        //鍵盤釋放時,速度設為0
    public void keyReleased(KeyEvent e) { 
        int c = e.getKeyCode();
        switch (c){
            case 37:
            case 39:
                DrawThread.myPlane.setSpeedX(0);
                break;
            case 38:
            case 40:
                DrawThread.myPlane.setSpeedY(0);
                break;
        }
    }

敵方飛機執行緒 : 如何生成敵方飛機呢?

每隔一段時間,在遊戲皮膚的頂部,產生一個敵方飛機

/**
* @author liTianLu
* @Date 2022/5/22 0:30
* @purpose 產生敵機的執行緒
*/
    @Override
    public void run() {
        int sleepTime  = 800;
        while (true){

            if(DrawThread.player.score>=500){  //當分數高於500時,加快敵機產生的頻率
                sleepTime = 300;
            }

            EnemyPlane enemyPlane = null;
            try {
                enemyPlane = new EnemyPlane();
            } catch (IOException e) {
                e.printStackTrace();
            }
            enemyPlanes.add(enemyPlane);
            new Thread(new EnemyBulletThread(enemyPlane)).start(); //啟動一個發射子彈執行緒
            try {
                sleep(sleepTime+ random.nextInt(300));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }

敵方子彈執行緒 : 使每一個敵方飛機開火

我們為每一個敵方飛機建立一個生成子彈的執行緒,要確定子彈產生的具體位置,就要知道敵方飛機的位置,所以我們要傳入一個敵方飛機物件給該執行緒。

public EnemyBulletThread(EnemyPlane enemyPlane){
        this.enemyPlane = enemyPlane;
    }
    
    @Override
    public void run() {

        try {
            sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        while(enemyPlane.isAlive() ){
            EnemyBullet enemyBullet = null;
            int enemyBullet_x = enemyPlane.getX()+25;
            int enemyBullet_y = enemyPlane.getY()+66;
            try {
                enemyBullet = new EnemyBullet(enemyBullet_x,enemyBullet_y);
            } catch (IOException e) {
                e.printStackTrace();
            }
            enemyBullets.add(enemyBullet);

            try {
                sleep(2000+ random.nextInt(2000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
        }
    }

檢測碰撞執行緒 : 在子彈與敵機碰撞時,移除敵機

  • 此時我們會遇到一個問題,就是在遍歷時,move移動執行緒有可能將其中的一個飛行物移出集合,會出現IndexOutOfBoundsException異常
    ,我們只需要在兩個執行緒使用飛行物集合時,加上synchronized關鍵字,即可解決。

  • MoveThread 遍歷我的子彈集合

synchronized (MyPlane.myBulletList){
    if(MyPlane.myBulletList.size()!=0){
        for (int i = 0; i < MyPlane.myBulletList.size(); i++) {
            MyPlane.myBulletList.get(i).setY(MyPlane.myBulletList.get(i).getY()+MyPlane.myBulletList.get(i).getSpeedY()    );

            if(MyPlane.myBulletList.get(i).getY() <= -100){
                MyPlane.myBulletList.remove(i);
                continue;
            }
        }
    }
}
  • TestCrashThread 檢測我的子彈與敵方飛機碰撞
synchronized (MyPlane.myBulletList){
    for (int i = 0; i < MyPlane.myBulletList.size(); i++) {
        for (int j = 0; j < EnemyPlaneThread.enemyPlanes.size() ;j++) {
            if(MyPlane.myBulletList.get(i).judge_crash(EnemyPlaneThread.enemyPlanes.get(j)) ){
                EnemyPlaneThread.enemyPlanes.get(j).setAlive(false);  //關執行緒
                DrawThread.player.score+=5; //分數+5
                EnemyPlaneThread.enemyPlanes.remove(j);
                MyPlane.myBulletList.remove(i);
                j = -1;
            }
            if(i >= MyPlane.myBulletList.size()){
                break;
            }
        }

    }
}

其他功能:顯示玩家hp,掉落道具,得分,升級,更換地圖

  • 顯示hp:每次檢測到我的飛機與敵方飛機,敵方子彈碰撞,就減分。減到<=0時,遊戲結束。
  • 得分:子彈打到敵方飛機時,加分,並將當前分數通過繪製執行緒繪製在螢幕上。
  • 掉落道具:敵機消失的時候,隨機掉落一個道具,我的飛機碰到道具時,回血/加分/升級
  • 升級:我的飛機初始為1級,最高為3級,等級改變時,使用switch 根據等級改變我的飛機的子彈發射方式。
  • 更換地圖: 使用一個新的窗體,設定幾個單選按鈕,選擇時通過監聽器,改變地圖的控制變數,從而改變地圖的繪製。

原始碼:連結:https://pan.baidu.com/s/1DXIASEHg5JUdqEptoMNImw
提取碼:ltlt

相關文章