27.Java 飞机游戏小项目
阅读原文时间:2022年03月30日阅读:1

开篇

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

键盘控制原理

重点使用键盘进行交互。

使用键盘操控游戏物体,通过鼠标或其他,我们只需要通过相关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();
    }
}

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

炮弹类基本设计

炮弹类我们用实心的黄色椭圆实现,不再加载新的图片。

我们的逻辑是在窗口固定位置(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();
    }
}

游戏中,碰撞是遇到最频繁的技术。

很多游戏引擎内部已经做了碰撞检测处理,我们只需调用即可。

矩形碰撞原理

游戏中,多个元素是否碰到一起,实际上,通常是用“矩形检测”原理实现的。

游戏中所有的物体都可以抽象成“矩形”,我们只需判断两个矩形是否相交即可。对于一些复杂的多边形、不规则物体,实际上是将他分解成多个矩形,继续进行矩形检测。|

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

在 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.时间计算:当前时刻-游戏结束的时刻。

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

最终效果