27.Java 飛機遊戲小項目

開篇

  1. 遊戲項目基本功能開發
  2. 飛機類設計
  3. 炮彈類設計
  4. 碰撞檢測設計
  5. 爆炸效果的實現
  6. 其他功能
    • 計時功能

遊戲項目基本功能開發

這裡將會一步步實現遊戲項目的基本功能。

使用 AWT 技術畫出遊戲主窗口

AWT 和 Swing 是 Java 中常見的 GUI(圖形用戶介面)技術。
本次使用的是 AWT 技術。
首先是建立一個 Java 項目,並創建類 MyGameFrame。

程式碼示例:MyGameFrame 類:畫出遊戲介面

package come.jungle.plane;

import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

/**
* 遊戲主窗口
* 使用 AWT 技術畫出遊戲主窗口
*/
// 讓 MyGameFrame 繼承 Frame,這樣子可以在方法里使用 Frame 的所有方法
public class MyGameFrame extends Frame {
    // 定義一個初始化窗口
    public void launchFrame(){
        this.setTitle("The Plane War - 阿jun"); // 定義圖形化介面右上角的標題
        setVisible(true); // 定義窗口是否可見,默認是不可見,所以需要設置為 true 可見
        setSize(500,500);  // 設置窗口大小的高寬均為 500
        setLocation(100,100);   // 定義打開窗口的位置,程式坐標系默認以右上角為零點
        // 增加關閉窗口的動作,通過方法重寫和匿名內部類
        this.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);  // 讓程式正常退出
            }
        });
    }
    public static void main(String[] args){
        // 定義類對象,對象調用方法
        MyGameFrame gameFrame = new MyGameFrame();
        gameFrame.launchFrame();
    }
}

圖形和文本繪製

如果要在窗口中畫圖或者顯示什麼內容,我們需要重寫 paint(Graphics g) 方法。
這個方法的作用是:畫出整個窗口以及內部內容。
它會被系統自動調用,我們無需去調用這個方法。

paint 使用方法介紹

// 重寫 paint 方法
@Override
public void paint(Graphics g){
    // paint 方法作用是:畫出整個窗口及內部內容。被系統自動調用。
}

Graphics 畫筆對象_畫圖形

程式碼示例:

package come.jungle.plane;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import static java.awt.Color.red;

/**
* 遊戲主窗口
* 使用 AWT 技術畫出遊戲主窗口
*/
// 讓 MyGameFrame 繼承 Frame,這樣子可以在方法里使用 Frame 的所有方法
public class MyGameFrame extends Frame {
    // 重寫 paint 方法
    @Override
    public void paint(Graphics g) { // 把 g 當成是一支畫筆來幫助實現繪製
        // 由於 g 是屬於對外借過來使用的,所以在使用以後需要將其還原,否則會導致下次其他地方使用時出現錯誤。
        Color c = g.getColor();
        // 在這個方法里添加我們要繪製的內容
        // 可以自定義畫筆顏色,兩種方式
        g.setColor(red);
//        g.setColor(new Color(255,220,3));
        // 畫直線:使用 drawLine() 畫一條線,確定起點和終點的坐標
        g.drawLine(100,100,400,400);
        // 畫矩形:起點為(100,100),寬高分別為300,200的矩形
        g.drawRect(100,100,300,200);
        // 畫橢圓:起點為(100,100),寬高分別為300,200的矩形
        g.drawOval(100,100,300,200); // 橢圓的寬高來源是外切矩形的寬高,在程式里任何物體都是矩形。
        // 畫字元串
        g.drawString("阿jun",300,300);
        // 返還畫筆最初傳進方法來時的顏色
        g.setColor(c);
    }

    // 定義一個初始化窗口
    public void launchFrame(){
        this.setTitle("The Plane War - 阿jun"); // 定義圖形化介面右上角的標題
        setVisible(true); // 定義窗口是否可見,默認是不可見,所以需要設置為 true 可見

        setSize(500,500);  // 設置窗口大小的高寬均為 500

        setLocation(100,100);   // 定義打開窗口的位置,程式坐標系默認以右上角為零點

        // 增加關閉窗口的動作,通過方法重寫和匿名內部類
        this.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);  // 讓程式正常退出
            }
        });
    }
    public static void main(String[] args){
        // 定義類對象,對象調用方法
        MyGameFrame gameFrame = new MyGameFrame();
        gameFrame.launchFrame();
    }
}

ImageIO實現圖片載入技術

遊戲開發中,圖片載入是最常見的技術。
在此處使用 ImageIO 類實現圖片載入,並且為了程式碼的復用,將圖片載入的方法封裝到 GameUtil 工具類中,便於我們以後直接調用。
需要先將項目所需圖片拷貝到項目的 src 下面,去建立新的文件夾 images 包用於存放所有圖片。即在 src 下創建一個名為 images 的 package 包。

目前有兩個文件:MyGameFrame.java 和 GameUtil.java

程式碼示例:GameUtil工具類:載入圖片程式碼

可以將一些輔助性的工具方法放到 GameUtil 中,便於重複調用。

MyGameFrame.java:

package come.jungle.plane;

import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import static java.awt.Color.red;

/**
* 遊戲主窗口
* 使用 AWT 技術畫出遊戲主窗口
*/
// 讓 MyGameFrame 繼承 Frame,這樣子可以在方法里使用 Frame 的所有方法
public class MyGameFrame extends Frame {
    // 在窗口中載入 png 圖片
    // 創建圖片類對象,利用 getImage 方法載入圖片
    Image plane = GameUtil.getImage("images/plane.png");
    Image bg = GameUtil.getImage("images/bg.png");

    // 重寫 paint 方法
    @Override
    public void paint(Graphics g) { // 把 g 當成是一支畫筆來幫助實現繪製
        // 利用 drawImage() 方法在窗口中畫圖
        // 載入 bg 作為背景圖片,從(0,0) 開始,佔用寬高為 500,500
        g.drawImage(bg,0,0,500,500,null);
        // 載入 plane 作為飛機圖片
        g.drawImage(plane,100,100,22,33,null);
    }

    // 定義一個初始化窗口
    public void launchFrame(){
        this.setTitle("The Plane War - 阿jun"); // 定義圖形化介面右上角的標題
        setVisible(true); // 定義窗口是否可見,默認是不可見,所以需要設置為 true 可見

        setSize(500,500);  // 設置窗口大小的高寬均為 500

        setLocation(100,100);   // 定義打開窗口的位置,程式坐標系默認以右上角為零點

        // 增加關閉窗口的動作,通過方法重寫和匿名內部類
        this.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);  // 讓程式正常退出
            }
        });
    }
    public static void main(String[] args){
        // 定義類對象,對象調用方法
        MyGameFrame gameFrame = new MyGameFrame();
        gameFrame.launchFrame();
    }
}

GameUtil.java:

package come.jungle.plane;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;

/**
* 遊戲的工具類
* 工具類中一般都是放置一些 static 方法
*/
public class GameUtil {
    // 構造器私有,防止別人創建本類對象(本項目可有可無)
    private GameUtil(){
    }
    public static Image getImage(String path){ // 將對應字元串的路徑傳入,比如 /images/plane.png
        // 根據傳入的字元串路徑來載入對應的圖片,載入以後形成一個圖片對象返回
        BufferedImage img = null; // 定義一個外部空對象
        // 先獲得當前類的一個 class 對象,然後獲得一個類載入器,通過類載入器獲得一個 resource 資源
        URL u = GameUtil.class.getClassLoader().getResource(path);
        // 通過 ImageIO 調用 read() 方法,然後去讀文件,會產生異常,需要去處理
        try {
            img = ImageIO.read(u);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return img;
    }
    public static void main(String[] args) {
        // 測試程式是否異常,通過列印對象的方式,如果列印報異常或為空,則說明有異常
        Image img = GameUtil.getImage("images/plane.png");  // 列印飛機圖片對象,路徑前方不加斜杠
        System.out.println(img);
    }
}

多執行緒和內部類實現動畫效果:增加繪製窗口的執行緒類。雙緩衝技術解決窗口閃爍問題。

目前窗口仍然是靜態的。本次結合多執行緒實現動畫效果。
在 MyGameFrame 類中定義 「重畫窗口執行緒 PaintThread 類」,將 PaintThread 定義成內部類,是為了方便使用 MyFameFrame 類的屬性和方法。
同時利用雙緩衝技術解決窗口閃爍問題。
雙緩衝技術介紹:
雙緩衝即在記憶體中創建一個與螢幕繪圖區域一致的對象,先將圖形繪製到記憶體中的這個對象上,再一次性將這個對象上的圖形拷貝到螢幕上,這樣能大大加快繪圖的速度。

程式碼示例:

MyGameFrame.java

package come.jungle.plane;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import static java.awt.Color.red;

/**
* 遊戲主窗口
* 使用 AWT 技術畫出遊戲主窗口
*/
// 讓 MyGameFrame 繼承 Frame,這樣子可以在方法里使用 Frame 的所有方法
public class MyGameFrame extends Frame {
    // 定義雙緩衝技術所需要的窗口長寬屬性,基於初始化遊戲窗口的長寬而定

    // 在窗口中載入 png 圖片
    // 創建圖片類對象,利用 getImage 方法載入圖片
    Image plane = GameUtil.getImage("images/plane.png");
    Image bg = GameUtil.getImage("images/bg.png");

    static int count = 0;
    int planeX = 100; // 將飛機的X坐標設置成一個變數。
    // 重寫 paint 方法
    @Override
    public void paint(Graphics g) { // 把 g 當成是一支畫筆來幫助實現繪製
        System.out.println("繪製窗口次數:" + count);
        count++;
        // 利用 drawImage() 方法在窗口中畫圖
        // 載入 bg 作為背景圖片,從(0,0) 開始,佔用寬高為 500,500
        g.drawImage(bg,0,0,Constant.GAME_WIDTH,Constant.GAME_HEIGHT,null);
        // 載入 plane 作為飛機圖片
        g.drawImage(plane,planeX,100,22,33,null);
        planeX += 1;
    }

    // 定義一個初始化窗口
    public void launchFrame(){
        this.setTitle("The Plane War - 阿jun"); // 定義圖形化介面右上角的標題
        setVisible(true); // 定義窗口是否可見,默認是不可見,所以需要設置為 true 可見
        // Constant.GAME_WIDTH 用於調用 Constant 類的 GAME_WIDTH 常量來使用
        setSize(Constant.GAME_WIDTH,Constant.GAME_HEIGHT);  // 設置窗口大小的高寬均為 500

        setLocation(100,100);   // 定義打開窗口的位置,程式坐標系默認以右上角為零點

        // 增加關閉窗口的動作,通過方法重寫和匿名內部類
        this.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);  // 讓程式正常退出
            }
        });

        // 將重畫窗口的執行緒類放入初始化窗口方法中,在程式初始化窗口時,就會調用這個方法,順便調用執行緒類對象。
        new PaintThread().start();  // 啟動重畫窗口的執行緒。
    }
    /**
    *  定義一個重畫窗口的執行緒類。
    *  定義成內部類是為了方便直接使用窗口類的相關方法。
    */
    class PaintThread extends Thread{
        // 重寫 Thread類的 run() 方法
        @Override
        public void run() {
            // 只要程式是活著的,就要求一直重畫窗口,所以是 while(true) 死循環
            while(true){
                // repaint() 是外部類 Frame 的成員方法
                repaint();
                // 給程式設定一個重畫時間間隔
                try {
                    // 程式執行到此處時,進度停 50 s
                    Thread.sleep(50);  // 1s=1000ms,即1s畫20次(20*50=1000)
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 雙緩衝技術解決窗口閃爍問題
    private Image offScreenImage = null;

    public void update(Graphics g){
        if(offScreenImage == null){
            // 這裡是遊戲窗口的寬度和高度
            offScreenImage = this.createImage(Constant.GAME_WIDTH,Constant.GAME_HEIGHT);
        }
        Graphics gOff = offScreenImage.getGraphics();
        paint(gOff);
        g.drawImage(offScreenImage,0,0,null);
    }


    public static void main(String[] args){
        // 定義類對象,對象調用方法
        MyGameFrame gameFrame = new MyGameFrame();
        gameFrame.launchFrame();
    }
}

Constant.java

package come.jungle.plane;
/**
 * 存放相關常量
 * */
public class Constant {
    // 定義雙緩衝技術所需要的窗口長寬屬性,基於初始化遊戲窗口的長寬而定
    // 遊戲窗口的寬度
    public static final int GAME_WIDTH = 500;
    // 遊戲窗口的寬度
    public static final int GAME_HEIGHT = 500;
}

遊戲物體根類 GameObject 的實現(0.5版)

窗口中所有的對象(飛機、炮彈等等)都有很多共性:「圖片對象、坐標位置、運行速度、寬度和高度」。
為了方便程式開發,我們需要設計一個GameObject類,它可以作為所有遊戲物體的父類,方便我們編程。

程式碼示例:MyGameFrame.java的修改和GameObject物體根類實現

MyGameFrame.java

package come.jungle.plane;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import static java.awt.Color.red;

/**
* 遊戲主窗口
* 使用 AWT 技術畫出遊戲主窗口
*/
// 讓 MyGameFrame 繼承 Frame,這樣子可以在方法里使用 Frame 的所有方法
public class MyGameFrame extends Frame {
    // 定義雙緩衝技術所需要的窗口長寬屬性,基於初始化遊戲窗口的長寬而定

    // 在窗口中載入 png 圖片
    // 創建圖片類對象,利用 getImage 方法載入圖片
    Image planeImg = GameUtil.getImage("images/plane.png");
    Image bg = GameUtil.getImage("images/bg.png");

    static int count = 0;

    // 創建一個 GameObject 類的飛機對象,定義對應屬性進行傳入
    // 要傳入的屬性值,GameObject(Image img, double x, double y, int speed, int width, int height)
    GameObject plane2 = new GameObject(planeImg,100,100,3,22,33);
    // 定義第二個GameObject類的飛機對象
//    GameObject plane3 = new GameObject(planeImg,200,200,3,22,33);

    // 重寫 paint 方法
    @Override
    public void paint(Graphics g) { // 把 g 當成是一支畫筆來幫助實現繪製
        System.out.println("繪製窗口次數:" + count);
        count++;
        // 利用 drawImage() 方法在窗口中畫圖
        // 載入 bg 作為背景圖片,從(0,0) 開始,佔用寬高為 500,500
        g.drawImage(bg,0,0,Constant.GAME_WIDTH,Constant.GAME_HEIGHT,null);
        // 飛機對象調用方法中的畫筆屬性 g,自己畫自己
        plane2.drawMyself(g);
        // 畫第二個飛機物體,直接創建GameObject類的第二個飛機對象並調用 drawMyself(g) 方法即可。
//        plane3.drawMyself(g);
    }

    // 定義一個初始化窗口
    public void launchFrame(){
        this.setTitle("The Plane War - 阿jun"); // 定義圖形化介面右上角的標題
        setVisible(true); // 定義窗口是否可見,默認是不可見,所以需要設置為 true 可見
        // Constant.GAME_WIDTH 用於調用 Constant 類的 GAME_WIDTH 常量來使用
        setSize(Constant.GAME_WIDTH,Constant.GAME_HEIGHT);  // 設置窗口大小的高寬均為 500

        setLocation(100,100);   // 定義打開窗口的位置,程式坐標系默認以右上角為零點

        // 增加關閉窗口的動作,通過方法重寫和匿名內部類
        this.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);  // 讓程式正常退出
            }
        });

        // 將重畫窗口的執行緒類放入初始化窗口方法中,在程式初始化窗口時,就會調用這個方法,順便調用執行緒類對象。
        new PaintThread().start();  // 啟動重畫窗口的執行緒。
    }
    /**
    *  定義一個重畫窗口的執行緒類。
    *  定義成內部類是為了方便直接使用窗口類的相關方法。
    */
    class PaintThread extends Thread{
        // 重寫 Thread類的 run() 方法
        @Override
        public void run() {
            // 只要程式是活著的,就要求一直重畫窗口,所以是 while(true) 死循環
            while(true){
                // repaint() 是外部類 Frame 的成員方法
                repaint();
                // 給程式設定一個重畫時間間隔
                try {
                    // 程式執行到此處時,進度停 50 s
                    Thread.sleep(50);  // 1s=1000ms,即1s畫20次(20*50=1000)
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 雙緩衝技術解決窗口閃爍問題
    private Image offScreenImage = null;

    public void update(Graphics g){
        if(offScreenImage == null){
            // 這裡是遊戲窗口的寬度和高度
            offScreenImage = this.createImage(Constant.GAME_WIDTH,Constant.GAME_HEIGHT);
        }
        Graphics gOff = offScreenImage.getGraphics();
        paint(gOff);
        g.drawImage(offScreenImage,0,0,null);
    }


    public static void main(String[] args){
        // 定義類對象,對象調用方法
        MyGameFrame gameFrame = new MyGameFrame();
        gameFrame.launchFrame();
    }
}

GameObject.java

package come.jungle.plane;

import java.awt.*;

/**
 * 遊戲物體的根類
 * */
public class GameObject {
    // 將這些屬性定義好以後,之前定義的跟飛機相關的程式碼就可以去掉了
    Image img;   // 圖片
    double x,y;  // 物體的坐標
    int speed;   // 物體移動的速度
    int width,height; // 物體的寬度和高度

    // 屬性值可以通過初始化去獲取,使用構造器去生成相關內容。
    // 通過外部傳入,對屬性進行賦值。
    public GameObject(Image img, double x, double y, int speed, int width, int height) {
        this.img = img;
        this.x = x;
        this.y = y;
        this.speed = speed;
        this.width = width;
        this.height = height;
    }

    // 定義一個方法來畫自身
    public void drawMyself(Graphics g){
        // 傳入double,參數值要求是int,所以需要int強制轉型一下
        g.drawImage(img,(int)x,(int)y,width,height,null);
    }

    /**
     * 所有的物體都是矩形,當你獲得對應的矩形時,就可以做一些相關的判斷操作
     * 返回物體對應的矩形區域,便於後續在碰撞檢測中使用
     * @retrun
     */
    public Rectangle getRect(){
        // 返回飛機位置的坐標位置,寬度高度
        return new Rectangle((int)x,(int)y,width,height);
    }
}

設計飛機類 Plane(0.5版本):創建多個飛機

有了 GameObject 這個父類,設計飛機類只需要簡單繼承即可。
重新設計了飛機類,通過繼承 GameObject 父類以後,重寫構造方法,實現飛機的移動。

程式碼示例:增加了一個Plane飛機類,修改了 GameObject.java 和 MyGameFrame.java

Plane.java

package come.jungle.plane;

import java.awt.*;

public class Plane extends GameObject{
    // 重寫父類方法
    @Override
    public void drawMyself(Graphics g) {
        super.drawMyself(g);
        // 飛機飛行的演算法,可以自行設定
        x += speed;
    }
    // 定義構造方法
    public Plane(Image img, double x, double y, int speed) {
        this.img = img;
        this.x = x;
        this.y = y;
        this.speed = speed;
        // 不傳寬度高度時,就自動獲取傳入圖片的寬度高度
        this.width = img.getWidth(null);
        this.height = img.getHeight(null);
    }
}

GameObject.java

多了一個無參構造方法

package come.jungle.plane;

import java.awt.*;

/**
 * 遊戲物體的根類
 * */
public class GameObject {
    // 將這些屬性定義好以後,之前定義的跟飛機相關的程式碼就可以去掉了
    Image img;   // 圖片
    double x,y;  // 物體的坐標
    int speed;   // 物體移動的速度
    int width,height; // 物體的寬度和高度

    // 屬性值可以通過初始化去獲取,使用構造器去生成相關內容。
    // 通過外部傳入,對屬性進行賦值。
    public GameObject(Image img, double x, double y, int speed, int width, int height) {
        this.img = img;
        this.x = x;
        this.y = y;
        this.speed = speed;
        this.width = width;
        this.height = height;
    }

    // 定義一個方法來畫自身
    public void drawMyself(Graphics g){
        // 傳入double,參數值要求是int,所以需要int強制轉型一下
        g.drawImage(img,(int)x,(int)y,width,height,null);
    }

    // Plane 飛機子類繼承 GameObject父類時,子類的無參構造器會調用父類的無參構造器,所以需定義一個GameObject父類的無參構造器
    public GameObject(){}
    /**
     * 所有的物體都是矩形,當你獲得對應的矩形時,就可以做一些相關的判斷操作
     * 返回物體對應的矩形區域,便於後續在碰撞檢測中使用
     * @retrun
     */
    public Rectangle getRect(){
        // 返回飛機位置的坐標位置,寬度高度
        return new Rectangle((int)x,(int)y,width,height);
    }
}

MyGameFrame.java

package come.jungle.plane;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import static java.awt.Color.red;

/**
* 遊戲主窗口
* 使用 AWT 技術畫出遊戲主窗口
*/
// 讓 MyGameFrame 繼承 Frame,這樣子可以在方法里使用 Frame 的所有方法
public class MyGameFrame extends Frame {

    // 在窗口中載入 png 圖片
    // 創建圖片類對象,利用 getImage 方法載入圖片
    Image planeImg = GameUtil.getImage("images/plane.png");
    Image bg = GameUtil.getImage("images/bg.png");

    static int count = 0;
    // 創建一個 Plane 類的飛機對象,定義對應屬性進行傳入
    Plane p1 = new Plane(planeImg,200,200,2);
    Plane p2 = new Plane(planeImg,200,200,3);
    Plane p3 = new Plane(planeImg,200,200,4);
    // 重寫 paint 方法
    @Override
    public void paint(Graphics g) { // 把 g 當成是一支畫筆來幫助實現繪製
        System.out.println("繪製窗口次數:" + count);
        count++;
        // 利用 drawImage() 方法在窗口中畫圖
        // 載入 bg 作為背景圖片,從(0,0) 開始,佔用寬高為 500,500
        g.drawImage(bg,0,0,Constant.GAME_WIDTH,Constant.GAME_HEIGHT,null);
        // 飛機對象調用方法中的畫筆屬性 g,自己畫自己
        p1.drawMyself(g);
        p2.drawMyself(g);
        p3.drawMyself(g);
    }

    // 定義一個初始化窗口
    public void launchFrame(){
        this.setTitle("The Plane War - 阿jun"); // 定義圖形化介面右上角的標題
        setVisible(true); // 定義窗口是否可見,默認是不可見,所以需要設置為 true 可見
        // Constant.GAME_WIDTH 用於調用 Constant 類的 GAME_WIDTH 常量來使用
        setSize(Constant.GAME_WIDTH,Constant.GAME_HEIGHT);  // 設置窗口大小的高寬均為 500

        setLocation(100,100);   // 定義打開窗口的位置,程式坐標系默認以右上角為零點

        // 增加關閉窗口的動作,通過方法重寫和匿名內部類
        this.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);  // 讓程式正常退出
            }
        });

        // 將重畫窗口的執行緒類放入初始化窗口方法中,在程式初始化窗口時,就會調用這個方法,順便調用執行緒類對象。
        new PaintThread().start();  // 啟動重畫窗口的執行緒。
    }
    /**
    *  定義一個重畫窗口的執行緒類。
    *  定義成內部類是為了方便直接使用窗口類的相關方法。
    */
    class PaintThread extends Thread{
        // 重寫 Thread類的 run() 方法
        @Override
        public void run() {
            // 只要程式是活著的,就要求一直重畫窗口,所以是 while(true) 死循環
            while(true){
                // repaint() 是外部類 Frame 的成員方法
                repaint();
                // 給程式設定一個重畫時間間隔
                try {
                    // 程式執行到此處時,進度停 50 s
                    Thread.sleep(50);  // 1s=1000ms,即1s畫20次(20*50=1000)
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 雙緩衝技術解決窗口閃爍問題
    // 定義雙緩衝技術所需要的窗口長寬屬性,基於初始化遊戲窗口的長寬而定
    private Image offScreenImage = null;

    public void update(Graphics g){
        if(offScreenImage == null){
            // 這裡是遊戲窗口的寬度和高度
            offScreenImage = this.createImage(Constant.GAME_WIDTH,Constant.GAME_HEIGHT);
        }
        Graphics gOff = offScreenImage.getGraphics();
        paint(gOff);
        g.drawImage(offScreenImage,0,0,null);
    }


    public static void main(String[] args){
        // 定義類對象,對象調用方法
        MyGameFrame gameFrame = new MyGameFrame();
        gameFrame.launchFrame();
    }
}

飛機類設計(0.6版)

鍵盤控制原理

重點使用鍵盤進行交互。
使用鍵盤操控遊戲物體,通過滑鼠或其他,我們只需要通過相關API的幫助即可輕鬆實現。
使用面向對象的思想,實現鍵盤控制。
鍵盤控制原理:
鍵盤和程式交互時,每次按下鍵、鬆開鍵都會觸發相應的鍵盤事件,事件的資訊都封裝到了KeyEvent對象中。
為了識別按下的鍵是哪個鍵,系統對鍵盤所有按鍵做了編號,每個按鍵都對應相應的數字。
比如∶回車鍵對應數字10,空格鍵對應數字32等。這些編號,我們都可以通過KeyEvent對象來查詢,KeyEvent.VK_ENTER實際就是存儲了數字10。

程式碼示例:Plane.java 和 MyGameFrame.java

將飛機的鍵盤控制程式碼,放入了 Plane.java 飛機類中,在 MyGameFrame.java 中傳入參數,調用對應方法即可。

Plane.java

package come.jungle.plane;

import java.awt.*;
import java.awt.event.KeyEvent;

public class Plane extends GameObject{
    // 飛機方向的控制
    boolean left,right,up,down;

    // 重寫父類方法
    @Override
    public void drawMyself(Graphics g) {
        super.drawMyself(g);
        // 飛機飛行的演算法,可以自行設定
//        x += speed;
        if (left){
            x -= speed;
        }
        if (right){
            x += speed;
        }
        if (up){
            y -= speed;
        }
        if (down){
            y += speed;
        }
    }
    // 定義飛機方向的方法
    // 實現鍵盤按下對應的監聽方法
    public void addDirection(KeyEvent e){
        switch(e.getKeyCode()){
            case KeyEvent.VK_LEFT:
                left=true;
                break;
            case KeyEvent.VK_RIGHT:
                right=true;
                break;
            case KeyEvent.VK_UP:
                up=true;
                break;
            case KeyEvent.VK_DOWN:
                down=true;
                break;
        }
    }
    // 實現鍵盤鬆開的監聽方法
    public void minusDirection(KeyEvent e){
        switch(e.getKeyCode()){
            case KeyEvent.VK_LEFT:
                left=false;
                break;
            case KeyEvent.VK_RIGHT:
                right=false;
                break;
            case KeyEvent.VK_UP:
                up=false;
                break;
            case KeyEvent.VK_DOWN:
                down=false;
                break;
        }
    }

    // 定義構造方法
    public Plane(Image img, double x, double y, int speed) {
        this.img = img;
        this.x = x;
        this.y = y;
        this.speed = speed;
        // 不傳寬度高度時,就自動獲取傳入圖片的寬度高度
        this.width = img.getWidth(null);
        this.height = img.getHeight(null);
    }
}

MyGameFrame.java

package come.jungle.plane;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import static java.awt.Color.red;

/**
* 遊戲主窗口
* 使用 AWT 技術畫出遊戲主窗口
*/
// 讓 MyGameFrame 繼承 Frame,這樣子可以在方法里使用 Frame 的所有方法
public class MyGameFrame extends Frame {

    // 在窗口中載入 png 圖片
    // 創建圖片類對象,利用 getImage 方法載入圖片
    Image planeImg = GameUtil.getImage("images/plane.png");
    Image bg = GameUtil.getImage("images/bg.png");

    // 創建一個 Plane 類的飛機對象,定義對應屬性進行傳入
    Plane p1 = new Plane(planeImg,200,200,7);

    // 重寫 paint 方法
    @Override
    public void paint(Graphics g) { // 把 g 當成是一支畫筆來幫助實現繪製
        // 利用 drawImage() 方法在窗口中畫圖
        // 載入 bg 作為背景圖片,從(0,0) 開始,佔用寬高為 500,500
        g.drawImage(bg,0,0,Constant.GAME_WIDTH,Constant.GAME_HEIGHT,null);
        // 飛機對象調用方法中的畫筆屬性 g,自己畫自己
        p1.drawMyself(g);
        // 測試方向鍵的效果:右上角為 (0,0),通過更改坐標來實現移動
    }

    // 定義一個初始化窗口
    public void launchFrame(){
        this.setTitle("The Plane War - 阿jun"); // 定義圖形化介面右上角的標題
        setVisible(true); // 定義窗口是否可見,默認是不可見,所以需要設置為 true 可見
        // Constant.GAME_WIDTH 用於調用 Constant 類的 GAME_WIDTH 常量來使用
        setSize(Constant.GAME_WIDTH,Constant.GAME_HEIGHT);  // 設置窗口大小的高寬均為 500

        setLocation(100,100);   // 定義打開窗口的位置,程式坐標系默認以右上角為零點

        // 增加關閉窗口的動作,通過方法重寫和匿名內部類
        this.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);  // 讓程式正常退出
            }
        });

        // 將重畫窗口的執行緒類放入初始化窗口方法中,在程式初始化窗口時,就會調用這個方法,順便調用執行緒類對象。
        new PaintThread().start();  // 啟動重畫窗口的執行緒。
        // 在程式初始化窗口時,啟動鍵盤監聽
        this.addKeyListener(new KeyMonditor()); // 啟動鍵盤監聽
    }
    /**
    *  定義一個重畫窗口的執行緒類。
    *  定義成內部類是為了方便直接使用窗口類的相關方法。
    */
    class PaintThread extends Thread{
        // 重寫 Thread類的 run() 方法
        @Override
        public void run() {
            // 只要程式是活著的,就要求一直重畫窗口,所以是 while(true) 死循環
            while(true){
                // repaint() 是外部類 Frame 的成員方法
                repaint();
                // 給程式設定一個重畫時間間隔
                try {
                    // 程式執行到此處時,進度停 50 s
                    Thread.sleep(50);  // 1s=1000ms,即1s畫20次(20*50=1000)
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 增加一個內部類,實現鍵盤的監聽處理
     * */
    class KeyMonditor extends KeyAdapter{
        // 重寫方法,實現對按鍵狀態(按下或抬起)的監聽
        // 重寫鍵盤按下對應的監聽方法
        @Override
        public void keyPressed(KeyEvent e) {
            // 獲取按下的按鍵是哪一個,每個按鍵都有一個特定的 code 值
            System.out.println("按下:" + e.getKeyCode());
            p1.addDirection(e);
        }
        // 重寫鍵盤抬起(鬆開)對應的監聽方法
        @Override
        public void keyReleased(KeyEvent e) {
            // 獲取抬起(鬆開)的按鍵是哪一個,每個按鍵都有一個特定的 code 值
            System.out.println("抬起:" + e.getKeyCode());
            p1.minusDirection(e);
        }
    }

    // 雙緩衝技術解決窗口閃爍問題
    // 定義雙緩衝技術所需要的窗口長寬屬性,基於初始化遊戲窗口的長寬而定
    private Image offScreenImage = null;

    public void update(Graphics g){
        if(offScreenImage == null){
            // 這裡是遊戲窗口的寬度和高度
            offScreenImage = this.createImage(Constant.GAME_WIDTH,Constant.GAME_HEIGHT);
        }
        Graphics gOff = offScreenImage.getGraphics();
        paint(gOff);
        g.drawImage(offScreenImage,0,0,null);
    }


    public static void main(String[] args){
        // 定義類對象,對象調用方法
        MyGameFrame gameFrame = new MyGameFrame();
        gameFrame.launchFrame();
    }
}

炮彈類設計(0.7版)

可更深入了解構造器的用法以及容器的用法。

炮彈類基本設計

炮彈類我們用實心的黃色橢圓實現,不再載入新的圖片。
我們的邏輯是在窗口固定位置(200,200)處生成炮彈,炮彈方向是隨機的,並且遇到邊界會反彈。
需要考慮的是炮彈的長寬對於碰撞邊界值計算的影響。
使用容器或數組產生多發炮彈,本次用的是容器。

程式碼示例:新增了一個炮彈類 Shell.java

Shell.java

package come.jungle.plane;

import java.awt.*;

/**
 * 炮彈類
 * */
public class Shell extends GameObject{
    // 繼承了遊戲物體根類的各個屬性,比如坐標、速度、長寬。
    double degree; // 定義炮彈的角度屬性,炮彈沿著指定的角度飛行。
    // 定義炮彈的初始構造器方法
    public Shell(){
        // 定義初始坐標
        x = 200;
        y = 200;
        degree = Math.random()*Math.PI*2;  // 生成一個任意角度數值
//        System.out.println(degree);
        width = 5;
        height = 5;
        speed = 5;

    }

    // 通過重寫 drawMyself() 方法來畫炮彈
    @Override
    public void drawMyself(Graphics g) {
        // 定義炮彈的參數,畫一個紅色實心圓
        Color c = g.getColor();
        g.setColor(Color.yellow);
        g.fillOval((int)x,(int)y,width,height);
        g.setColor(c);  // 用完以後,將畫筆原本顏色還原。
        // 根據自己的演算法指定移動路徑,speed是速度,利用角度進行x,y的距離計算
        x += speed*Math.cos(degree);
        y += speed*Math.sin(degree);

        // 碰到邊界改變方向,右上角為零點(0,0)
        // 由於炮彈有寬度,所以需要在計算碰撞邊界時,去掉長寬值的影響
        if (x < 0 || x > Constant.GAME_WIDTH - this.width){
            degree = Math.PI-degree;
        }
        if (y < 40 || y > Constant.GAME_HEIGHT - this.height){
            degree = -degree;
        }
    }
}

MyGameFrame.java

package come.jungle.plane;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import static java.awt.Color.red;

/**
* 遊戲主窗口
* 使用 AWT 技術畫出遊戲主窗口
*/
// 讓 MyGameFrame 繼承 Frame,這樣子可以在方法里使用 Frame 的所有方法
public class MyGameFrame extends Frame {

    // 在窗口中載入 png 圖片
    // 創建圖片類對象,利用 getImage 方法載入圖片
    Image planeImg = GameUtil.getImage("images/plane.png");
    Image bg = GameUtil.getImage("images/bg.png");

    // 創建一個 Plane 類的飛機對象,定義對應屬性進行傳入
    Plane p1 = new Plane(planeImg,200,200,10);
    // 定義炮彈對象
//    Shell s1 = new Shell();
    // 使用數組定義50個炮彈數組對象
    Shell[] shells = new Shell[50];

    // 重寫 paint 方法
    @Override
    public void paint(Graphics g) { // 把 g 當成是一支畫筆來幫助實現繪製
        // 利用 drawImage() 方法在窗口中畫圖
        // 載入 bg 作為背景圖片,從(0,0) 開始,佔用寬高為 500,500
        g.drawImage(bg,0,0,Constant.GAME_WIDTH,Constant.GAME_HEIGHT,null);
        // 飛機對象調用方法中的畫筆屬性 g,自己畫自己
        p1.drawMyself(g);
        // 畫炮彈
        for (int i=0;i <shells.length;i++){
            shells[i].drawMyself(g);
        }
    }

    // 定義一個初始化窗口
    public void launchFrame(){
        this.setTitle("The Plane War - 阿jun"); // 定義圖形化介面右上角的標題
        setVisible(true); // 定義窗口是否可見,默認是不可見,所以需要設置為 true 可見
        // Constant.GAME_WIDTH 用於調用 Constant 類的 GAME_WIDTH 常量來使用
        setSize(Constant.GAME_WIDTH,Constant.GAME_HEIGHT);  // 設置窗口大小的高寬均為 500

        setLocation(100,100);   // 定義打開窗口的位置,程式坐標系默認以右上角為零點

        // 增加關閉窗口的動作,通過方法重寫和匿名內部類
        this.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);  // 讓程式正常退出
            }
        });

        // 將重畫窗口的執行緒類放入初始化窗口方法中,在程式初始化窗口時,就會調用這個方法,順便調用執行緒類對象。
        new PaintThread().start();  // 啟動重畫窗口的執行緒。
        // 在程式初始化窗口時,啟動鍵盤監聽
        this.addKeyListener(new KeyMonditor()); // 啟動鍵盤監聽
        // 利用for循環初始化創建 50 個炮彈對象
        for (int i=0;i<50;i++){
            shells[i] = new Shell();
        }
    }
    /**
    *  定義一個重畫窗口的執行緒類。
    *  定義成內部類是為了方便直接使用窗口類的相關方法。
    */
    class PaintThread extends Thread{
        // 重寫 Thread類的 run() 方法
        @Override
        public void run() {
            // 只要程式是活著的,就要求一直重畫窗口,所以是 while(true) 死循環
            while(true){
                // repaint() 是外部類 Frame 的成員方法
                repaint();
                // 給程式設定一個重畫時間間隔
                try {
                    // 程式執行到此處時,進度停 50 s
                    Thread.sleep(50);  // 1s=1000ms,即1s畫20次(20*50=1000)
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 增加一個內部類,實現鍵盤的監聽處理
     * */
    class KeyMonditor extends KeyAdapter{
        // 重寫方法,實現對按鍵狀態(按下或抬起)的監聽
        // 重寫鍵盤按下對應的監聽方法
        @Override
        public void keyPressed(KeyEvent e) {
            // 獲取按下的按鍵是哪一個,每個按鍵都有一個特定的 code 值
            System.out.println("按下:" + e.getKeyCode());
            p1.addDirection(e);
        }
        // 重寫鍵盤抬起(鬆開)對應的監聽方法
        @Override
        public void keyReleased(KeyEvent e) {
            // 獲取抬起(鬆開)的按鍵是哪一個,每個按鍵都有一個特定的 code 值
            System.out.println("抬起:" + e.getKeyCode());
            p1.minusDirection(e);
        }
    }

    // 雙緩衝技術解決窗口閃爍問題
    // 定義雙緩衝技術所需要的窗口長寬屬性,基於初始化遊戲窗口的長寬而定
    private Image offScreenImage = null;

    public void update(Graphics g){
        if(offScreenImage == null){
            // 這裡是遊戲窗口的寬度和高度
            offScreenImage = this.createImage(Constant.GAME_WIDTH,Constant.GAME_HEIGHT);
        }
        Graphics gOff = offScreenImage.getGraphics();
        paint(gOff);
        g.drawImage(offScreenImage,0,0,null);
    }


    public static void main(String[] args){
        // 定義類對象,對象調用方法
        MyGameFrame gameFrame = new MyGameFrame();
        gameFrame.launchFrame();
    }
}

碰撞檢測技術(0.8版)

遊戲中,碰撞是遇到最頻繁的技術。
很多遊戲引擎內部已經做了碰撞檢測處理,我們只需調用即可。

矩形碰撞原理

遊戲中,多個元素是否碰到一起,實際上,通常是用「矩形檢測」原理實現的。
遊戲中所有的物體都可以抽象成「矩形」,我們只需判斷兩個矩形是否相交即可。對於一些複雜的多邊形、不規則物體,實際上是將他分解成多個矩形,繼續進行矩形檢測。|
Java的 API中,為我們提供了Rectangle類來表示矩形相關資訊,並且提供了intersects)方法,直接判斷矩形是否相交。

程式碼示例:更改了兩個文件,MyGameFrame.java 和 Plane.java

實現了飛機碰到炮彈以後消失(即不再重畫)的效果

MyGameFrame.java

package come.jungle.plane;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import static java.awt.Color.red;

/**
* 遊戲主窗口
* 使用 AWT 技術畫出遊戲主窗口
*/
// 讓 MyGameFrame 繼承 Frame,這樣子可以在方法里使用 Frame 的所有方法
public class MyGameFrame extends Frame {

    // 在窗口中載入 png 圖片
    // 創建圖片類對象,利用 getImage 方法載入圖片
    Image planeImg = GameUtil.getImage("images/plane.png");
    Image bg = GameUtil.getImage("images/bg.png");

    // 創建一個 Plane 類的飛機對象,定義對應屬性進行傳入
    Plane p1 = new Plane(planeImg,300,300,10);
    // 定義炮彈對象
//    Shell s1 = new Shell();
    // 使用數組定義50個炮彈數組對象
    Shell[] shells = new Shell[3];

    // 重寫 paint 方法
    @Override
    public void paint(Graphics g) { // 把 g 當成是一支畫筆來幫助實現繪製
        // 利用 drawImage() 方法在窗口中畫圖
        // 載入 bg 作為背景圖片,從(0,0) 開始,佔用寬高為 500,500
        g.drawImage(bg,0,0,Constant.GAME_WIDTH,Constant.GAME_HEIGHT,null);
        // 飛機對象調用方法中的畫筆屬性 g,自己畫自己
        p1.drawMyself(g);
        // 畫炮彈
        for (int i=0;i <shells.length;i++){
            shells[i].drawMyself(g);
            // 碰撞檢測,將所有的炮彈和飛機進行矩形碰撞檢測,p1.getRect()表示為獲取飛機對應的矩形
            boolean peng = shells[i].getRect().intersects(p1.getRect());
            if (peng){
//                System.out.println("飛機被擊中了!!");
                p1.live = false;
//                System.out.println(p1);
            }
        }
    }

    // 定義一個初始化窗口
    public void launchFrame(){
        this.setTitle("The Plane War - 阿jun"); // 定義圖形化介面右上角的標題
        setVisible(true); // 定義窗口是否可見,默認是不可見,所以需要設置為 true 可見
        // Constant.GAME_WIDTH 用於調用 Constant 類的 GAME_WIDTH 常量來使用
        setSize(Constant.GAME_WIDTH,Constant.GAME_HEIGHT);  // 設置窗口大小的高寬均為 500

        setLocation(100,100);   // 定義打開窗口的位置,程式坐標系默認以右上角為零點

        // 增加關閉窗口的動作,通過方法重寫和匿名內部類
        this.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);  // 讓程式正常退出
            }
        });

        // 將重畫窗口的執行緒類放入初始化窗口方法中,在程式初始化窗口時,就會調用這個方法,順便調用執行緒類對象。
        new PaintThread().start();  // 啟動重畫窗口的執行緒。
        // 在程式初始化窗口時,啟動鍵盤監聽
        this.addKeyListener(new KeyMonditor()); // 啟動鍵盤監聽
        // 利用for循環初始化創建 50 個炮彈對象
        for (int i=0;i<shells.length;i++){
            shells[i] = new Shell();
        }
    }
    /**
    *  定義一個重畫窗口的執行緒類。
    *  定義成內部類是為了方便直接使用窗口類的相關方法。
    */
    class PaintThread extends Thread{
        // 重寫 Thread類的 run() 方法
        @Override
        public void run() {
            // 只要程式是活著的,就要求一直重畫窗口,所以是 while(true) 死循環
            while(true){
                // repaint() 是外部類 Frame 的成員方法
                repaint();
                // 給程式設定一個重畫時間間隔
                try {
                    // 程式執行到此處時,進度停 50 s
                    Thread.sleep(50);  // 1s=1000ms,即1s畫20次(20*50=1000)
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 增加一個內部類,實現鍵盤的監聽處理
     * */
    class KeyMonditor extends KeyAdapter{
        // 重寫方法,實現對按鍵狀態(按下或抬起)的監聽
        // 重寫鍵盤按下對應的監聽方法
        @Override
        public void keyPressed(KeyEvent e) {
            // 獲取按下的按鍵是哪一個,每個按鍵都有一個特定的 code 值
//            System.out.println("按下:" + e.getKeyCode());
            p1.addDirection(e);
        }
        // 重寫鍵盤抬起(鬆開)對應的監聽方法
        @Override
        public void keyReleased(KeyEvent e) {
            // 獲取抬起(鬆開)的按鍵是哪一個,每個按鍵都有一個特定的 code 值
//            System.out.println("抬起:" + e.getKeyCode());
            p1.minusDirection(e);
        }
    }

    // 雙緩衝技術解決窗口閃爍問題
    // 定義雙緩衝技術所需要的窗口長寬屬性,基於初始化遊戲窗口的長寬而定
    private Image offScreenImage = null;

    public void update(Graphics g){
        if(offScreenImage == null){
            // 這裡是遊戲窗口的寬度和高度
            offScreenImage = this.createImage(Constant.GAME_WIDTH,Constant.GAME_HEIGHT);
        }
        Graphics gOff = offScreenImage.getGraphics();
        paint(gOff);
        g.drawImage(offScreenImage,0,0,null);
    }


    public static void main(String[] args){
        // 定義類對象,對象調用方法
        MyGameFrame gameFrame = new MyGameFrame();
        gameFrame.launchFrame();
    }
}

Plane.java

package come.jungle.plane;

import java.awt.*;
import java.awt.event.KeyEvent;

public class Plane extends GameObject{
    // 飛機方向的控制
    boolean left,right,up,down;
    public boolean live = true; // 定義飛機是否存活的變數,默認為 true。
    // 重寫父類方法
    @Override
    public void drawMyself(Graphics g) {
        // 如果飛機沒有存活,live=false,就不畫了
        if (live){
            super.drawMyself(g);
            // 飛機飛行的演算法,可以自行設定
//        x += speed;
            if (left){
                x -= speed;
            }
            if (right){
                x += speed;
            }
            if (up){
                y -= speed;
            }
            if (down){
                y += speed;
            }
        }
    }
    // 定義飛機方向的方法
    // 實現鍵盤按下對應的監聽方法
    public void addDirection(KeyEvent e){
        switch(e.getKeyCode()){
            case KeyEvent.VK_LEFT:
                left=true;
                break;
            case KeyEvent.VK_RIGHT:
                right=true;
                break;
            case KeyEvent.VK_UP:
                up=true;
                break;
            case KeyEvent.VK_DOWN:
                down=true;
                break;
        }
    }
    // 實現鍵盤鬆開的監聽方法
    public void minusDirection(KeyEvent e){
        switch(e.getKeyCode()){
            case KeyEvent.VK_LEFT:
                left=false;
                break;
            case KeyEvent.VK_RIGHT:
                right=false;
                break;
            case KeyEvent.VK_UP:
                up=false;
                break;
            case KeyEvent.VK_DOWN:
                down=false;
                break;
        }
    }

    // 定義構造方法
    public Plane(Image img, double x, double y, int speed) {
        this.img = img;
        this.x = x;
        this.y = y;
        this.speed = speed;
        // 不傳寬度高度時,就自動獲取傳入圖片的寬度高度
        this.width = img.getWidth(null);
        this.height = img.getHeight(null);
    }
}

爆炸效果的實現(0.9版本)

在 src 目錄的 images 文件夾下存放了爆炸相關係列圖片。

圖片輪播處理,實現爆炸效果

程式碼示例

新增了一個爆炸類文件 Explode.java,修改了 MyGameFrame.java 文件。

MyGameFrame.java

package come.jungle.plane;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import static java.awt.Color.red;

/**
* 遊戲主窗口
* 使用 AWT 技術畫出遊戲主窗口
*/
// 讓 MyGameFrame 繼承 Frame,這樣子可以在方法里使用 Frame 的所有方法
public class MyGameFrame extends Frame {

    // 在窗口中載入 png 圖片
    // 創建圖片類對象,利用 getImage 方法載入圖片
    Image planeImg = GameUtil.getImage("images/plane.png");
    Image bg = GameUtil.getImage("images/bg.png");

    // 創建一個 Plane 類的飛機對象,定義對應屬性進行傳入
    Plane p1 = new Plane(planeImg,300,300,10);
    // 使用數組定義炮彈數組對象
    Shell[] shells = new Shell[50];
    // 爆炸
    Explode explode;

    // 重寫 paint 方法
    @Override
    public void paint(Graphics g) { // 把 g 當成是一支畫筆來幫助實現繪製
        // 利用 drawImage() 方法在窗口中畫圖
        // 載入 bg 作為背景圖片,從(0,0) 開始,佔用寬高為 500,500
        g.drawImage(bg,0,0,Constant.GAME_WIDTH,Constant.GAME_HEIGHT,null);
        // 飛機對象調用方法中的畫筆屬性 g,自己畫自己
        p1.drawMyself(g);
        // 畫炮彈
        for (int i=0;i <shells.length;i++){
            shells[i].drawMyself(g);
            // 碰撞檢測,將所有的炮彈和飛機進行矩形碰撞檢測,p1.getRect()表示為獲取飛機對應的矩形
            boolean peng = shells[i].getRect().intersects(p1.getRect());
            if (peng){
                p1.live = false;

                // 處理爆炸效果,默認炸了一次以後,炮彈再碰到就不炸了
                if (explode==null){
                    // 當飛機被碰撞時,創建爆炸類的對象,並傳入飛機當時的(x,y)坐標值
                    explode = new Explode(p1.x,p1.y);
                }
                // 畫出爆炸效果系列圖片
                explode.drawMyself(g);
            }
        }
    }

    // 定義一個初始化窗口
    public void launchFrame(){
        this.setTitle("The Plane War - 阿jun"); // 定義圖形化介面右上角的標題
        setVisible(true); // 定義窗口是否可見,默認是不可見,所以需要設置為 true 可見
        // Constant.GAME_WIDTH 用於調用 Constant 類的 GAME_WIDTH 常量來使用
        setSize(Constant.GAME_WIDTH,Constant.GAME_HEIGHT);  // 設置窗口大小的高寬均為 500

        setLocation(100,100);   // 定義打開窗口的位置,程式坐標系默認以右上角為零點

        // 增加關閉窗口的動作,通過方法重寫和匿名內部類
        this.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);  // 讓程式正常退出
            }
        });

        // 將重畫窗口的執行緒類放入初始化窗口方法中,在程式初始化窗口時,就會調用這個方法,順便調用執行緒類對象。
        new PaintThread().start();  // 啟動重畫窗口的執行緒。
        // 在程式初始化窗口時,啟動鍵盤監聽
        this.addKeyListener(new KeyMonditor()); // 啟動鍵盤監聽
        // 利用for循環初始化創建 50 個炮彈對象
        for (int i=0;i<shells.length;i++){
            shells[i] = new Shell();
        }
    }
    /**
    *  定義一個重畫窗口的執行緒類。
    *  定義成內部類是為了方便直接使用窗口類的相關方法。
    */
    class PaintThread extends Thread{
        // 重寫 Thread類的 run() 方法
        @Override
        public void run() {
            // 只要程式是活著的,就要求一直重畫窗口,所以是 while(true) 死循環
            while(true){
                // repaint() 是外部類 Frame 的成員方法
                repaint();
                // 給程式設定一個重畫時間間隔
                try {
                    // 程式執行到此處時,進度停 50 s
                    Thread.sleep(50);  // 1s=1000ms,即1s畫20次(20*50=1000)
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 增加一個內部類,實現鍵盤的監聽處理
     * */
    class KeyMonditor extends KeyAdapter{
        // 重寫方法,實現對按鍵狀態(按下或抬起)的監聽
        // 重寫鍵盤按下對應的監聽方法
        @Override
        public void keyPressed(KeyEvent e) {
            // 獲取按下的按鍵是哪一個,每個按鍵都有一個特定的 code 值
//            System.out.println("按下:" + e.getKeyCode());
            p1.addDirection(e);
        }
        // 重寫鍵盤抬起(鬆開)對應的監聽方法
        @Override
        public void keyReleased(KeyEvent e) {
            // 獲取抬起(鬆開)的按鍵是哪一個,每個按鍵都有一個特定的 code 值
//            System.out.println("抬起:" + e.getKeyCode());
            p1.minusDirection(e);
        }
    }

    // 雙緩衝技術解決窗口閃爍問題
    // 定義雙緩衝技術所需要的窗口長寬屬性,基於初始化遊戲窗口的長寬而定
    private Image offScreenImage = null;

    public void update(Graphics g){
        if(offScreenImage == null){
            // 這裡是遊戲窗口的寬度和高度
            offScreenImage = this.createImage(Constant.GAME_WIDTH,Constant.GAME_HEIGHT);
        }
        Graphics gOff = offScreenImage.getGraphics();
        paint(gOff);
        g.drawImage(offScreenImage,0,0,null);
    }


    public static void main(String[] args){
        // 定義類對象,對象調用方法
        MyGameFrame gameFrame = new MyGameFrame();
        gameFrame.launchFrame();
    }
}

Explode.java

package come.jungle.plane;

import java.awt.*;

/**
 * 爆炸類
 **/
public class Explode {
    // 定義爆炸位置
    double x,y;
    // 創建一個數組存放爆炸相關圖片
    static Image[] imgs = new Image[16];
    // 計數器:計算現在畫了多少次了(確定圖片次序)
    int count;
    // 靜態初始化塊初始化靜態屬性
    static {
        // 通過 for 循環,將16個圖片全部初始化完成。
        for(int i=0;i<16;i++){
            imgs[i] = GameUtil.getImage("images/Explode/e"+(i+1)+".png");
            // 需要用數組對象去調用真正的方法,圖片才會真正載入進來
            imgs[i].getWidth(null); // 解決程式懶載入問題
        }
    }

    // 定義方法來花自己
    public void drawMyself(Graphics g){
        if (count < 16){
            g.drawImage(imgs[count],(int)x,(int)y,null );
            count++;
        }
    }

    // 定義無參構造器
    public Explode(){
    }
    // 定義形參構造器
    public Explode(double x, double y) {
        this.x = x;
        this.y = y;
    }
}

其它功能設計(1.0版)

基本功能實現以後,增加一些錦上添花的功能。
比如:遊戲計時功能、全網排名等。

計時功能

我們希望在玩遊戲時,增加計時功能,可以清晰的看到自己玩了多長時間,增加刺激性。
這個功能的核心有兩點:
1.時間計算:當前時刻-遊戲結束的時刻。
2.顯示時間到窗口。

程式碼示例:只修改了 MyGameFrame.java

MyGameFrame.java

package come.jungle.plane;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.Date;

import static java.awt.Color.red;

/**
* 遊戲主窗口
* 使用 AWT 技術畫出遊戲主窗口
*/
// 讓 MyGameFrame 繼承 Frame,這樣子可以在方法里使用 Frame 的所有方法
public class MyGameFrame extends Frame {

    // 在窗口中載入 png 圖片
    // 創建圖片類對象,利用 getImage 方法載入圖片
    Image planeImg = GameUtil.getImage("images/plane.png");
    Image bg = GameUtil.getImage("images/bg.png");

    // 創建一個 Plane 類的飛機對象,定義對應屬性進行傳入
    Plane p1 = new Plane(planeImg,300,300,10);
    // 使用數組定義炮彈數組對象
    Shell[] shells = new Shell[50];
    // 爆炸
    Explode explode;

    Date start = new Date();  // 遊戲開始時間(以當前遊戲運行時間作為一個開始)
    Date end;    // 遊戲結束時間(飛機碰撞炮彈時結束)
    long period = 0; // 遊戲時長多少秒

    // 重寫 paint 方法
    @Override
    public void paint(Graphics g) { // 把 g 當成是一支畫筆來幫助實現繪製
        // 利用 drawImage() 方法在窗口中畫圖
        // 載入 bg 作為背景圖片,從(0,0) 開始,佔用寬高為 500,500
        g.drawImage(bg,0,0,Constant.GAME_WIDTH,Constant.GAME_HEIGHT,null);
        // 調用計算遊戲時長相關方法,計算遊戲時間
        drawtime(g);


        // 飛機對象調用方法中的畫筆屬性 g,自己畫自己
        p1.drawMyself(g);
        // 畫炮彈
        for (int i=0;i <shells.length;i++){
            shells[i].drawMyself(g);
            // 碰撞檢測,將所有的炮彈和飛機進行矩形碰撞檢測,p1.getRect()表示為獲取飛機對應的矩形
            boolean peng = shells[i].getRect().intersects(p1.getRect());
            if (peng){
                p1.live = false;

                // 處理爆炸效果,默認炸了一次以後,炮彈再碰到就不炸了
                if (explode==null){
                    // 當飛機被碰撞時,創建爆炸類的對象,並傳入飛機當時的(x,y)坐標值
                    explode = new Explode(p1.x,p1.y);
                }
                // 畫出爆炸效果系列圖片
                explode.drawMyself(g);
            }
        }
    }

    // 對計算遊戲時長的程式碼進行封裝
    public void drawtime(Graphics g){
        // 獲取畫筆傳進來時的屬性
        Color c = g.getColor();
        Font f = g.getFont();
        g.setColor(Color.green);
        if (p1.live){
            // 將計時功能文字畫在遊戲窗口中
            period = (System.currentTimeMillis()-start.getTime())/1000; // 計算遊戲時長,單位是毫秒,所以除以1000變成秒
            g.drawString("堅持:"+period+"秒",30,50);
        }else{
            if (end==null){ // 只有第一次的時候才為null,後面再畫的時候就不為 null 了
                end = new Date();
                period = (end.getTime() - start.getTime())/1000;  // 計算遊戲結束時的總時長
            }
            // 設置字體樣式
            g.setColor(Color.red);
            g.setFont((new Font("微軟雅黑",Font.BOLD,30)));
            g.drawString("最終遊戲時長:"+period+"秒",200,200);
        }
        // 還原畫筆傳進來時的屬性
        g.setColor(c);
        g.setFont(f);
    }

    // 定義一個初始化窗口
    public void launchFrame(){
        this.setTitle("The Plane War - 阿jun"); // 定義圖形化介面右上角的標題
        setVisible(true); // 定義窗口是否可見,默認是不可見,所以需要設置為 true 可見
        // Constant.GAME_WIDTH 用於調用 Constant 類的 GAME_WIDTH 常量來使用
        setSize(Constant.GAME_WIDTH,Constant.GAME_HEIGHT);  // 設置窗口大小的高寬均為 500

        setLocation(100,100);   // 定義打開窗口的位置,程式坐標系默認以右上角為零點

        // 增加關閉窗口的動作,通過方法重寫和匿名內部類
        this.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);  // 讓程式正常退出
            }
        });

        // 將重畫窗口的執行緒類放入初始化窗口方法中,在程式初始化窗口時,就會調用這個方法,順便調用執行緒類對象。
        new PaintThread().start();  // 啟動重畫窗口的執行緒。
        // 在程式初始化窗口時,啟動鍵盤監聽
        this.addKeyListener(new KeyMonditor()); // 啟動鍵盤監聽
        // 利用for循環初始化創建 50 個炮彈對象
        for (int i=0;i<shells.length;i++){
            shells[i] = new Shell();
        }
    }
    /**
    *  定義一個重畫窗口的執行緒類。
    *  定義成內部類是為了方便直接使用窗口類的相關方法。
    */
    class PaintThread extends Thread{
        // 重寫 Thread類的 run() 方法
        @Override
        public void run() {
            // 只要程式是活著的,就要求一直重畫窗口,所以是 while(true) 死循環
            while(true){
                // repaint() 是外部類 Frame 的成員方法
                repaint();
                // 給程式設定一個重畫時間間隔
                try {
                    // 程式執行到此處時,進度停 50 s
                    Thread.sleep(50);  // 1s=1000ms,即1s畫20次(20*50=1000)
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 增加一個內部類,實現鍵盤的監聽處理
     * */
    class KeyMonditor extends KeyAdapter{
        // 重寫方法,實現對按鍵狀態(按下或抬起)的監聽
        // 重寫鍵盤按下對應的監聽方法
        @Override
        public void keyPressed(KeyEvent e) {
            // 獲取按下的按鍵是哪一個,每個按鍵都有一個特定的 code 值
//            System.out.println("按下:" + e.getKeyCode());
            p1.addDirection(e);
        }
        // 重寫鍵盤抬起(鬆開)對應的監聽方法
        @Override
        public void keyReleased(KeyEvent e) {
            // 獲取抬起(鬆開)的按鍵是哪一個,每個按鍵都有一個特定的 code 值
//            System.out.println("抬起:" + e.getKeyCode());
            p1.minusDirection(e);
        }
    }

    // 雙緩衝技術解決窗口閃爍問題
    // 定義雙緩衝技術所需要的窗口長寬屬性,基於初始化遊戲窗口的長寬而定
    private Image offScreenImage = null;

    public void update(Graphics g){
        if(offScreenImage == null){
            // 這裡是遊戲窗口的寬度和高度
            offScreenImage = this.createImage(Constant.GAME_WIDTH,Constant.GAME_HEIGHT);
        }
        Graphics gOff = offScreenImage.getGraphics();
        paint(gOff);
        g.drawImage(offScreenImage,0,0,null);
    }


    public static void main(String[] args){
        // 定義類對象,對象調用方法
        MyGameFrame gameFrame = new MyGameFrame();
        gameFrame.launchFrame();
    }
}

最終版本

共有 7 個 java 文件,同時還有許多圖片(可自行去截圖尋找,比如尚學堂B站影片:鏈接地址)
7個Java文件分別為:MyGameFrame.java、Shell.java、Plane.java、GameObject.java、GameUtil.java、Explode.java、Constant.java

總程式碼

MyGameFrame.java

package come.jungle.plane;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.Date;

import static java.awt.Color.red;

/**
* 遊戲主窗口
* 使用 AWT 技術畫出遊戲主窗口
*/
// 讓 MyGameFrame 繼承 Frame,這樣子可以在方法里使用 Frame 的所有方法
public class MyGameFrame extends Frame {

    // 在窗口中載入 png 圖片
    // 創建圖片類對象,利用 getImage 方法載入圖片
    Image planeImg = GameUtil.getImage("images/plane.png");
    Image bg = GameUtil.getImage("images/bg.png");

    // 創建一個 Plane 類的飛機對象,定義對應屬性進行傳入
    Plane p1 = new Plane(planeImg,300,300,10);
    // 使用數組定義炮彈數組對象
    Shell[] shells = new Shell[50];
    // 爆炸
    Explode explode;

    Date start = new Date();  // 遊戲開始時間(以當前遊戲運行時間作為一個開始)
    Date end;    // 遊戲結束時間(飛機碰撞炮彈時結束)
    long period = 0; // 遊戲時長多少秒

    // 重寫 paint 方法
    @Override
    public void paint(Graphics g) { // 把 g 當成是一支畫筆來幫助實現繪製
        // 利用 drawImage() 方法在窗口中畫圖
        // 載入 bg 作為背景圖片,從(0,0) 開始,佔用寬高為 500,500
        g.drawImage(bg,0,0,Constant.GAME_WIDTH,Constant.GAME_HEIGHT,null);
        // 調用計算遊戲時長相關方法,計算遊戲時間
        drawtime(g);


        // 飛機對象調用方法中的畫筆屬性 g,自己畫自己
        p1.drawMyself(g);
        // 畫炮彈
        for (int i=0;i <shells.length;i++){
            shells[i].drawMyself(g);
            // 碰撞檢測,將所有的炮彈和飛機進行矩形碰撞檢測,p1.getRect()表示為獲取飛機對應的矩形
            boolean peng = shells[i].getRect().intersects(p1.getRect());
            if (peng){
                p1.live = false;

                // 處理爆炸效果,默認炸了一次以後,炮彈再碰到就不炸了
                if (explode==null){
                    // 當飛機被碰撞時,創建爆炸類的對象,並傳入飛機當時的(x,y)坐標值
                    explode = new Explode(p1.x,p1.y);
                }
                // 畫出爆炸效果系列圖片
                explode.drawMyself(g);
            }
        }
    }

    // 對計算遊戲時長的程式碼進行封裝
    public void drawtime(Graphics g){
        // 獲取畫筆傳進來時的屬性
        Color c = g.getColor();
        Font f = g.getFont();
        g.setColor(Color.green);
        if (p1.live){
            // 將計時功能文字畫在遊戲窗口中
            period = (System.currentTimeMillis()-start.getTime())/1000; // 計算遊戲時長,單位是毫秒,所以除以1000變成秒
            g.drawString("堅持:"+period+"秒",30,50);
        }else{
            if (end==null){ // 只有第一次的時候才為null,後面再畫的時候就不為 null 了
                end = new Date();
                period = (end.getTime() - start.getTime())/1000;  // 計算遊戲結束時的總時長
            }
            // 設置字體樣式
            g.setColor(Color.red);
            g.setFont((new Font("微軟雅黑",Font.BOLD,30)));
            g.drawString("最終遊戲時長:"+period+"秒",200,200);
        }
        // 還原畫筆傳進來時的屬性
        g.setColor(c);
        g.setFont(f);
    }

    // 定義一個初始化窗口
    public void launchFrame(){
        this.setTitle("The Plane War - 阿jun"); // 定義圖形化介面右上角的標題
        setVisible(true); // 定義窗口是否可見,默認是不可見,所以需要設置為 true 可見
        // Constant.GAME_WIDTH 用於調用 Constant 類的 GAME_WIDTH 常量來使用
        setSize(Constant.GAME_WIDTH,Constant.GAME_HEIGHT);  // 設置窗口大小的高寬均為 500

        setLocation(100,100);   // 定義打開窗口的位置,程式坐標系默認以右上角為零點

        // 增加關閉窗口的動作,通過方法重寫和匿名內部類
        this.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);  // 讓程式正常退出
            }
        });

        // 將重畫窗口的執行緒類放入初始化窗口方法中,在程式初始化窗口時,就會調用這個方法,順便調用執行緒類對象。
        new PaintThread().start();  // 啟動重畫窗口的執行緒。
        // 在程式初始化窗口時,啟動鍵盤監聽
        this.addKeyListener(new KeyMonditor()); // 啟動鍵盤監聽
        // 利用for循環初始化創建 50 個炮彈對象
        for (int i=0;i<shells.length;i++){
            shells[i] = new Shell();
        }
    }
    /**
    *  定義一個重畫窗口的執行緒類。
    *  定義成內部類是為了方便直接使用窗口類的相關方法。
    */
    class PaintThread extends Thread{
        // 重寫 Thread類的 run() 方法
        @Override
        public void run() {
            // 只要程式是活著的,就要求一直重畫窗口,所以是 while(true) 死循環
            while(true){
                // repaint() 是外部類 Frame 的成員方法
                repaint();
                // 給程式設定一個重畫時間間隔
                try {
                    // 程式執行到此處時,進度停 50 s
                    Thread.sleep(50);  // 1s=1000ms,即1s畫20次(20*50=1000)
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 增加一個內部類,實現鍵盤的監聽處理
     * */
    class KeyMonditor extends KeyAdapter{
        // 重寫方法,實現對按鍵狀態(按下或抬起)的監聽
        // 重寫鍵盤按下對應的監聽方法
        @Override
        public void keyPressed(KeyEvent e) {
            // 獲取按下的按鍵是哪一個,每個按鍵都有一個特定的 code 值
//            System.out.println("按下:" + e.getKeyCode());
            p1.addDirection(e);
        }
        // 重寫鍵盤抬起(鬆開)對應的監聽方法
        @Override
        public void keyReleased(KeyEvent e) {
            // 獲取抬起(鬆開)的按鍵是哪一個,每個按鍵都有一個特定的 code 值
//            System.out.println("抬起:" + e.getKeyCode());
            p1.minusDirection(e);
        }
    }

    // 雙緩衝技術解決窗口閃爍問題
    // 定義雙緩衝技術所需要的窗口長寬屬性,基於初始化遊戲窗口的長寬而定
    private Image offScreenImage = null;

    public void update(Graphics g){
        if(offScreenImage == null){
            // 這裡是遊戲窗口的寬度和高度
            offScreenImage = this.createImage(Constant.GAME_WIDTH,Constant.GAME_HEIGHT);
        }
        Graphics gOff = offScreenImage.getGraphics();
        paint(gOff);
        g.drawImage(offScreenImage,0,0,null);
    }


    public static void main(String[] args){
        // 定義類對象,對象調用方法
        MyGameFrame gameFrame = new MyGameFrame();
        gameFrame.launchFrame();
    }
}

Shell.java

package come.jungle.plane;

import java.awt.*;

/**
 * 炮彈類
 * */
public class Shell extends GameObject{
    // 繼承了遊戲物體根類的各個屬性,比如坐標、速度、長寬。
    double degree; // 定義炮彈的角度屬性,炮彈沿著指定的角度飛行。
    // 定義炮彈的初始構造器方法
    public Shell(){
        // 定義初始坐標
        x = 200;
        y = 200;
        degree = Math.random()*Math.PI*2;  // 生成一個任意角度數值
//        System.out.println(degree);
        width = 5;
        height = 5;
        speed = 5;

    }

    // 通過重寫 drawMyself() 方法來畫炮彈
    @Override
    public void drawMyself(Graphics g) {
        // 定義炮彈的參數,畫一個紅色實心圓
        Color c = g.getColor();
        g.setColor(Color.yellow);
        g.fillOval((int)x,(int)y,width,height);
        g.setColor(c);  // 用完以後,將畫筆原本顏色還原。
        // 根據自己的演算法指定移動路徑,speed是速度,利用角度進行x,y的距離計算
        x += speed*Math.cos(degree);
        y += speed*Math.sin(degree);

        // 碰到邊界改變方向,右上角為零點(0,0)
        // 由於炮彈有寬度,所以需要在計算碰撞邊界時,去掉長寬值的影響
        if (x < 0 || x > Constant.GAME_WIDTH - this.width){
            degree = Math.PI-degree;
        }
        if (y < 40 || y > Constant.GAME_HEIGHT - this.height){
            degree = -degree;
        }
    }
}

Plane.java

package come.jungle.plane;

import java.awt.*;
import java.awt.event.KeyEvent;

public class Plane extends GameObject{
    // 飛機方向的控制
    boolean left,right,up,down;
    public boolean live = true; // 定義飛機是否存活的變數,默認為 true。
    // 重寫父類方法
    @Override
    public void drawMyself(Graphics g) {
        // 如果飛機沒有存活,live=false,就不畫了
        if (live){
            super.drawMyself(g);
            // 飛機飛行的演算法,可以自行設定
//        x += speed;
            if (left){
                x -= speed;
            }
            if (right){
                x += speed;
            }
            if (up){
                y -= speed;
            }
            if (down){
                y += speed;
            }
        }
    }
    // 定義飛機方向的方法
    // 實現鍵盤按下對應的監聽方法
    public void addDirection(KeyEvent e){
        switch(e.getKeyCode()){
            case KeyEvent.VK_LEFT:
                left=true;
                break;
            case KeyEvent.VK_RIGHT:
                right=true;
                break;
            case KeyEvent.VK_UP:
                up=true;
                break;
            case KeyEvent.VK_DOWN:
                down=true;
                break;
        }
    }
    // 實現鍵盤鬆開的監聽方法
    public void minusDirection(KeyEvent e){
        switch(e.getKeyCode()){
            case KeyEvent.VK_LEFT:
                left=false;
                break;
            case KeyEvent.VK_RIGHT:
                right=false;
                break;
            case KeyEvent.VK_UP:
                up=false;
                break;
            case KeyEvent.VK_DOWN:
                down=false;
                break;
        }
    }

    // 定義構造方法
    public Plane(Image img, double x, double y, int speed) {
        this.img = img;
        this.x = x;
        this.y = y;
        this.speed = speed;
        // 不傳寬度高度時,就自動獲取傳入圖片的寬度高度
        this.width = img.getWidth(null);
        this.height = img.getHeight(null);
    }
}

GameObject.java

package come.jungle.plane;

import java.awt.*;

/**
 * 遊戲物體的根類
 * */
public class GameObject {
    // 將這些屬性定義好以後,之前定義的跟飛機相關的程式碼就可以去掉了
    Image img;   // 圖片
    double x,y;  // 物體的坐標
    int speed;   // 物體移動的速度
    int width,height; // 物體的寬度和高度

    // 屬性值可以通過初始化去獲取,使用構造器去生成相關內容。
    // 通過外部傳入,對屬性進行賦值。
    public GameObject(Image img, double x, double y, int speed, int width, int height) {
        this.img = img;
        this.x = x;
        this.y = y;
        this.speed = speed;
        this.width = width;
        this.height = height;
    }

    // 定義一個方法來畫自身
    public void drawMyself(Graphics g){
        // 傳入double,參數值要求是int,所以需要int強制轉型一下
        g.drawImage(img,(int)x,(int)y,width,height,null);
    }

    // Plane 飛機子類繼承 GameObject父類時,子類的無參構造器會調用父類的無參構造器,所以需定義一個GameObject父類的無參構造器
    public GameObject(){}
    /**
     * 所有的物體都是矩形,當你獲得對應的矩形時,就可以做一些相關的判斷操作
     * 返回物體對應的矩形區域,便於後續在碰撞檢測中使用
     * @retrun
     */
    public Rectangle getRect(){
        // 返回飛機位置的坐標位置,寬度高度
        return new Rectangle((int)x,(int)y,width,height);
    }
}

GameUtil.java

package come.jungle.plane;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;

/**
* 遊戲的工具類
* 工具類中一般都是放置一些 static 方法
*/
public class GameUtil {
    // 構造器私有,防止別人創建本類對象(本項目可有可無)
    private GameUtil(){
    }
    // 重點功能就是載入圖片文件,並提供給外部引用
    public static Image getImage(String path){ // 將對應字元串的路徑傳入,比如 /images/plane.png
        // 根據傳入的字元串路徑來載入對應的圖片,載入以後形成一個圖片對象返回
        BufferedImage img = null; // 定義一個外部空對象
        // 先獲得當前類的一個 class 對象,然後獲得一個類載入器,通過類載入器獲得一個 resource 資源
        URL u = GameUtil.class.getClassLoader().getResource(path);
        // 通過 ImageIO 調用 read() 方法,然後去讀文件,會產生異常,需要去處理
        try {
            img = ImageIO.read(u);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return img;
    }
    public static void main(String[] args) {
        // 測試程式是否異常,通過列印對象的方式,如果列印報異常或為空,則說明有異常
        Image img = GameUtil.getImage("images/plane.png");  // 列印飛機圖片對象,路徑前方不加斜杠
        System.out.println(img);
    }
}

Explode.java

package come.jungle.plane;

import java.awt.*;

/**
 * 爆炸類
 **/
public class Explode {
    // 定義爆炸位置
    double x,y;
    // 創建一個數組存放爆炸相關圖片
    static Image[] imgs = new Image[16];
    // 計數器:計算現在畫了多少次了(確定圖片次序)
    int count;
    // 靜態初始化塊初始化靜態屬性
    static {
        // 通過 for 循環,將16個圖片全部初始化完成。
        for(int i=0;i<16;i++){
            imgs[i] = GameUtil.getImage("images/Explode/e"+(i+1)+".png");
            // 需要用數組對象去調用真正的方法,圖片才會真正載入進來
            imgs[i].getWidth(null); // 解決程式懶載入問題
        }
    }

    // 定義方法來花自己
    public void drawMyself(Graphics g){
        if (count < 16){
            g.drawImage(imgs[count],(int)x,(int)y,null );
            count++;
        }
    }

    // 定義無參構造器
    public Explode(){
    }
    // 定義形參構造器
    public Explode(double x, double y) {
        this.x = x;
        this.y = y;
    }
}

Constant.java

package come.jungle.plane;
/**
 * 存放相關常量
 * */
public class Constant {
    // 定義雙緩衝技術所需要的窗口長寬屬性,基於初始化遊戲窗口的長寬而定
    // 遊戲窗口的寬度
    public static final int GAME_WIDTH = 500;
    // 遊戲窗口的寬度
    public static final int GAME_HEIGHT = 500;
}

最終效果

Tags: