飞机大战的项目

1 简介和项目目标

通过游戏项目学习整个 Java 基础知识体系,我们做了精心的设计,让每一章知识都能 获得应用。 比如:多线程用来实现动画效果、容器实现对于多发炮弹的存取和处理、常用类等等的应用。
寓教于乐,让大家迅速入门,更希望通过喜闻乐见的小游戏,让大家爱上编程,爱上“程 序员”。

建议:
很多朋友会疑惑:“游戏项目,又不能拿到企业面试中,为什么要讲?” 这是一种太过 于功利的想法。就像,我们说:“今天吃个馒头,又不是长高,为什么要吃呢?” 游戏项目 的训练,只是为了锻炼大家的基本功,并不是直接用来企业面试的。但是,基本功扎实了, 就可以随心所欲的编程,做企业项目无非就是多一些技术点而已,本质都是“编程”。
 当然,大家也不能沉迷于游戏编程,写一两个项目足矣,不要把时间花在研究这些内容 上。更多的仍然是往后面学习。 所以,任何事情一定要把握“度”!

项目为游戏 J20 飞机游戏,效果如图所示:
我们通过键盘控制飞机前后移动,躲避炮弹,看谁坚持的时间长!如果碰到炮弹,则发 生爆炸,游戏结束!并显示本次生存的时间和等级排名。如果有网络,则会自动读取服务器 内容获取网络排名。

2 游戏项目基本功能开发

2.1 AWT 技术画出游戏主窗口(0.1 版)

·基本功能实现
AWT 和 Swing 是 Java 中常见的 GUI(图形用户界面)技术。本节中,仅限于画出最基 本的窗口和图形加载,所以,大家无需在此花大量时间学习这两门技术。我们会在第十四章 详细讲解此部分内容。
本项目中,我们使用的是 AWT 技术,它是 Java 中最老的 GUI 技术,非常简单。
1.打开IDEA如图所示的界面,点击New Project。在这里插入图片描述
2.选择Empty Java,点击Finish。如图所示:
在这里插入图片描述

3.选中项目,右键点击New–>Module。如图所示:
在这里插入图片描述
4.选择Java和JDK的版本,点击Next。如图所示:在这里插入图片描述
5.填写项目名称,点击Finish即可。如图所示:
在这里插入图片描述
6.项目结构,如图所示:在这里插入图片描述
5.编写画游戏窗口的代码如下:

package com.txw.plan;

import javax.swing.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
/**
 * 画游戏窗口
 * @author Adair
 * @email 1578533828@qq.com
 */
@SuppressWarnings("all")   // 注解警告信息
public class MyGameFrame extends JFrame {
    public void launchFrame(){
        // 在游戏窗口打印标题
        setTitle("飞机大战");
        // 窗口默认为不可见,设置为可见
        setVisible(true);
        // 窗口大小:宽度为500,高度为500
        setSize(500,500);
        // 在窗口左上角顶点的坐标位置
        setLocation(300,300);
        // 增加关闭窗口监听,这样用户可以点击右上角关闭图标,可以关闭游戏程序
        this.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);     // 正常退出程序
            }
        });
    }

    public static void main(String[] args) {
        MyGameFrame myGameFrame = new MyGameFrame();
        myGameFrame.launchFrame();
    }
}

如图所示:在这里插入图片描述
执行的结果为如图所示:在这里插入图片描述
· 要点讲解
1. 继承 Frame 类,画出窗口
Frame 是 java.awt 中的主要类,我们画的窗口都需要继承 Frame。这样,Frame 的基本功能我们就可以直接使用了。
2. 窗口坐标问题,了解坐标系
setLocation(300, 300);代码的含义是定位窗口的位置。窗口的位置就是指“窗口 左上角顶点的位置”。如图所示:在这里插入图片描述
这里 Y 轴的方向是向下的,和我们初中数学学的方向不一样。这就是计算机里面的 坐标系。
3. 物体就是矩形,物体的位置就是所在矩形左上角顶点的坐标 游戏或开发中,所有物体的都是矩形。
即使你看到一个圆在那里,我们处理的本质 上是圆的外切矩形。即使你看到一个美女,我们处理的是美女所占用的矩形空间。 不 然,给你一个不规则的图形,你没法做很多运算。
游戏开发中的物体,位置通常指的就是:该物体的矩形的左上角顶点位置。
在这里插入图片描述
上图中的“飞机”,实际我们在编程时处理的是飞机所在的“矩形区域”。
4. 窗口关闭问题
Frame 类默认没有处理关闭窗口功能,我们需要自己添加关闭功能。System.exit(0) 表示应用正常结束。addWindowListener()表示增加窗口监听事件。

 addWindowListener(new WindowAdapter(){
        @Override
        public void windowClosed(WindowEvent e) {
            System.exit(0);
        }
 });

此处,如果想深入钻研,可以研究一下 AWT 的事件机制,在此不做赘述。

2.2图形和文本绘制 (0.2 版)

·paint 方法 如果要在窗口中画图或者显示什么内容,我们需要重写 paint(Graphics g)方法。 这个 方法的作用是:画出整个窗口及其内部内容。它会被系统自动调用。我们自己不需要去调用 这个方法。
paint 方法介绍

@Override
public void paint(Graphics g) {
	// paint方法作用是:画出整个窗口及内部内容。被系统自动调用。
}

·Graphics 画笔对象_画图形
Graphics 对象,我们可以把它想象成“一支画笔”,窗口中的图形都由这支“画 笔”画出来的。
画出每个图形都需要指定图形所在“矩形区域”的位置和大小。比如绘制椭圆。 g.drawOval(100, 50, 300, 300); 实际上,就是根据椭圆所在的外切矩形来确定椭 圆的基本信息。上面 4 个参数指的是椭圆外切矩形:左上角顶点(100px,50px),宽度 300px,高度 300px。
使用 paint 方法画图形的代码如下:

// paint方法作用是:画出整个窗口及内部内容。被系统自动调用。
@Override
public void paint(Graphics g) {
	// 从坐标点(100,50)到(400,400)画出直线
	g.drawLine(100, 50, 400, 400);   
	// 画出矩形。矩形左上角顶点坐标(100,50),宽度300,高度300 
	g.drawRect(100, 50, 300, 300); 
	// 画出椭圆。椭圆外切矩形为:左上角顶点(100,50),宽度300,高度300 
	g.drawOval(100, 50, 300, 300); 
}

1.选中项目,右键点击New–>Module。如图所示:在这里插入图片描述
2.选择Java和JDK的版本,点击Next。如图所示:在这里插入图片描述
3.填写项目名称,点击Finish即可。如图所示:在这里插入图片描述
4.编写游戏主窗口的代码如下:

package com.txw.plan;

import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
/**
 * 游戏主窗口
 * @author Adair
 * @email 1578533828@qq.com
 */
@SuppressWarnings("all")   // 注解警告信息
public class MyGameFrame extends Frame {

    @Override
    public void paint(Graphics g) {     // g当做是一支画笔
        Color color = g.getColor();
        g.setColor(new Color(255,0,255));
        // 画直线
        g.drawLine(100,100,400,400);
        // 画矩形
        g.drawRect(100,100,300,200);
        // 画椭圆
        g.drawOval(100,100,300,200);
        // 画字符串
        g.drawString("com.txw.plan",300,300);
        g.setColor(color);
    }

    // 初始化窗口
    public void launchFrame(){
        // 在游戏窗口打印标题
        setTitle("飞机大战");
        // 窗口是否可见
        setVisible(true);
        // 窗口大小
        setSize(500,500);
        // 窗口打开的位置
        setLocation(400,400);
         // 增加关闭窗口的动作
        this.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);     // 正常退出程序
            }
        });
    }

    public static void main(String[] args) {
        MyGameFrame myGameFrame = new MyGameFrame();
        myGameFrame.launchFrame();
    }
}

执行的结果为如图所示:
在这里插入图片描述## 2.3 ImageIO 实现图片加载技术(0.3 版)
游戏开发中,图片加载是最常见的技术。我们在此处使用 ImageIO 类实现图片加载, 并且为了代码的复用,将图片加载的方法封装到 GameUtil 工具类中,便于我们以后直接调用。
1.选中项目,右键点击New–>Module。如图所示:在这里插入图片描述
2.选择Java和JDK的版本,点击Next。如图所示:在这里插入图片描述
3.填写项目名称,点击Finish即可。如图所示:在这里插入图片描述
我们要先将项目用到的图片拷贝到项目的 src 下面,我们可以建立新的文件夹 images 存放所有图片,项目结构如下:
在这里插入图片描述
GameUtil 工具类
我们可以将一些辅助性的工具方法通通放到 GameUtil 中,便于重复调用。
GameUtil 类:加载图片代码如下:

package com.txw.plan;

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

/**
 * GameUtil类:加载图片
 * @author Adair
 * @email 1578533828@qq.com
 */
@SuppressWarnings("all")   // 注解警告信息
public class GameUtil {
    // 构造器私有。防止别人创建本类的对象.
    private GameUtil(){

    }

    /**
     * 获取图片
     * @param path
     * @return
     */
    public static Image getImage(String path){  // images/plane.png
        BufferedImage image = null;
        URL u = GameUtil.class.getClassLoader().getResource(path);
        try {
            image = ImageIO.read(u);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return image;
    }

    public static void main(String[] args) {
        Image image = GameUtil.getImage("images/plane.png");
        System.out.println(image);
    }
}

如图所示:在这里插入图片描述

注:
1. GameUtil.class.getClassLoader().getResource(path);可以帮助我们获得程序运 行类加载器,加载资源的根目录,从而获得相对资源位置。
2.ImageIO.read()方法是核心方法,帮助我们读取图片信息,并返回Image对象。

加载游戏背景图片和飞机图片
我们将实现准备好的图片放到 src/images下面,然后,开始读取这些图片,并显示在窗 口中。代码如下:

package com.txw.plan;

import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
/**
 * 游戏主窗口
 * @author Adair
 * @email 1578533828@qq.com
 */
@SuppressWarnings("all")   // 注解警告信息
public class MyGameFrame extends Frame {

    // 将背景图片与飞机图片定义为成员变量
    Image  plane = GameUtil.getImage("images/plane.png");
    Image  bg = GameUtil.getImage("images/bg.jpg");

    /**
     * 画出整个窗口及内部内容。被系统自动调用
     * @param g
     */
    @Override
    public void paint(Graphics g) {     // g当做是一支画笔
       g.drawImage(bg,0,0,500,500,null);
       g.drawImage(plane,100,100,22,33,null);
    }

    // 初始化窗口
    public void launchFrame(){
        // 在游戏窗口打印标题
        setTitle("飞机大战");
        // 窗口是否可见
        setVisible(true);
        // 窗口大小
        setSize(500,500);
        // 窗口打开的位置
        setLocation(400,400);
        // 增加关闭窗口的动作
        this.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);     // 正常退出程序
            }
        });
    }

    public static void main(String[] args) {
        MyGameFrame myGameFrame = new MyGameFrame();
        myGameFrame.launchFrame();
    }
}

如图所示:在这里插入图片描述
执行的结果为如图所示:
在这里插入图片描述

2.4 多线程和内部类实现动画效果(0.4 版)

·增加绘制窗口的线程类

前三个版本,我们步步为营,每个小版本都有功能的突破。但是,目前为止我们的窗口 仍然是静态的,并没有像真正的游戏窗口那样“各种动、各种炫”。本节我们结合多线程实 现动画效果。
我们在 MyGameFrame 类中定义“重画窗口线程 PaintThread 类”,为了方便使用 MyGameFrame 类的属性和方法,我们将 PaintThread 定义成内部类。
1.选中项目,右键点击New–>Module。如图所示:在这里插入图片描述
2.选择Java和JDK的版本,点击Next。如图所示:在这里插入图片描述
3.填写项目名称,点击Finish即可。如图所示:在这里插入图片描述
MyGameFrame类:增加PaintThread内部类的代码如下:

// 其他代码和上个版本一致,限于篇幅,此处只呈现新增的代码
    /**
     * 定义一个重画窗口的线程类,是一个内部类
     */
    class PaintThread extends Thread {
        public void run() {
            while (true) {
                repaint();
                try {
                    // 1s = 1000ms
                    Thread.sleep(40);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

如图所示:在这里插入图片描述
定义好 PaintThread 内部类后,我们还需要在窗口的 launchFrame()方法中创建线程 对象和启动线程:

public void launchFrame(){ 
	// 本方法其他代码和上个版本一致,限于篇幅,只显示新增的代码 
	new PaintThread().start(); 
	// 启动重画线程 
}

如图所示:
在这里插入图片描述
完成后的MyGameFrame类的代码如下:

package com.txw.plan;

import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
/**
 * 游戏主窗口
 * @author Adair
 * @email 1578533828@qq.com
 */
@SuppressWarnings("all")   // 注解警告信息
public class MyGameFrame extends Frame {
    // 将背景图片与飞机图片定义为成员变量
    Image planeImage = GameUtil.getImage("images/plane.png");
    Image bgImage = GameUtil.getImage("images/bg.jpg");
    static int count = 0;

    /**
     * paint方法作用是:画出整个窗口及内部内容。被系统自动调用。
     * @param g
     */
    @Override
    public void paint(Graphics g) {     // g当做是一支画笔
        g.drawImage(bgImage, 0, 0, null);
        System.out.println("调用paint,重画窗口,次数:" + (count++));
        g.drawImage(planeImage, 200, 200, null);
    }

// 其他代码和上个版本一致,限于篇幅,此处只呈现新增的代码
    /**
     * 定义一个重画窗口的线程类,是一个内部类
     */
    class PaintThread extends Thread {
        public void run() {
            while (true) {
                repaint();
                try {
                    // 1s = 1000ms
                    Thread.sleep(40);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 初始化窗口
    public void launchFrame() {
        // 在游戏窗口打印标题
        setTitle("飞机大战");
        // 窗口是否可见
        setVisible(true);
        // 窗口大小
        setSize(500, 500);
        // 窗口打开的位置
        setLocation(400, 400);
        // 增加关闭窗口的动作
        this.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);     // 正常退出程序
            }
        });
        // 启动重画线程
        new PaintThread().start();
    }

    public static void main(String[] args) {
        MyGameFrame myGameFrame = new MyGameFrame();
        myGameFrame.launchFrame();
    }
}

执行的结果为如图所示:
在这里插入图片描述
根据控制台打印的数据,我们发现 paint 方法被系统反复调用,一秒 N 次。按照线程 中我们规定的是 40ms 画一次,1 秒大约调用 25 次(1 秒=1000ms)。也就是说,“现在, 窗口被 1 秒重复绘制 25 次”,如果我们调整飞机的位置变量,每次画飞机位置都不一致,
在肉眼看来不就实现动画了吗? ·调整飞机位置,让飞机动起来 之前,我们绘制飞机的代码为:g.drawImage(planeImg, 200, 200, null); 每次都绘 制到(200,200)这个坐标位置。我们将位置定义为变量 planeX,planeY,每次绘制变量值都 发生变化(planeX += 3; ),这样飞机就动起来了。
改变飞机的坐标位置的代码如下:

package com.txw.plan;

import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
/**
 * 游戏主窗口
 * @author Adair
 * @email 1578533828@qq.com
 */
@SuppressWarnings("all")   // 注解警告信息
public class MyGameFrame extends Frame {
    // 将背景图片与飞机图片定义为成员变量
    Image planeImage = GameUtil.getImage("images/plane.png");
    Image bgImage = GameUtil.getImage("images/bg.jpg");
    static int count = 0;
    // 将飞机的坐标设置为变量,初始值为(200,200)
    int planeX = 0;
    int planeY = 0;
    /**
     * paint方法作用是:画出整个窗口及内部内容。被系统自动调用。
     * @param g
     */
    @Override
    public void paint(Graphics g) {     // g当做是一支画笔
        g.drawImage(bgImage, 0, 0, null);
        System.out.println("调用paint,重画窗口,次数:" + (count++));
        // 不再是写死的位置
        g.drawImage(planeImage, planeX, planeY, null);
        // 每次画完以后改变飞机的x坐标
        planeX += 3;
    }

// 其他代码和上个版本一致,限于篇幅,此处只呈现新增的代码
    /**
     * 定义一个重画窗口的线程类,是一个内部类
     */
    class PaintThread extends Thread {
        public void run() {
            while (true) {
                repaint();
                try {
                    // 1s = 1000ms
                    Thread.sleep(40);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 初始化窗口
    public void launchFrame() {
        // 在游戏窗口打印标题
        setTitle("飞机大战");
        // 窗口是否可见
        setVisible(true);
        // 窗口大小
        setSize(500, 500);
        // 窗口打开的位置
        setLocation(400, 400);
        // 增加关闭窗口的动作
        this.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);     // 正常退出程序
            }
        });
        // 启动重画线程
        new PaintThread().start();
    }

    public static void main(String[] args) {
        MyGameFrame myGameFrame = new MyGameFrame();
        myGameFrame.launchFrame();
    }
}

如图所示:在这里插入图片描述
运行程序,我们发现,飞机真的飞起来了!

2.5 双缓冲技术解决闪烁问题(0.4)

上个版本,我们实现了动画效果,但是发现窗口会不停的闪烁,体验度非常差。在实际 开发中,绘制图形是非常复杂的,绘图可能需要几秒甚至更长时间,也经常发生闪烁现象,为了解决这个问题,我们通常使用“双缓冲技术”。 “双缓冲技术”的绘图过程如下:
1.在内存中创建与画布一致的缓冲区
2.在缓冲区画图
3.将缓冲区位图拷贝到当前画布上
4.释放内存缓冲区
双缓冲即在内存中创建一个与屏幕绘图区域一致的对象,先将图形绘制到内存中的这个 对象上,再一次性将这个对象上的图形拷贝到屏幕上,这样能大大加快绘图的速度。
我们只需将如下“双缓冲”实现代码,放入MyGrameFrame 类中,即可:
添加双缓冲技术的代码如下:

package com.txw.plan;

import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
/**
 * 游戏主窗口
 * @author Adair
 * @email 1578533828@qq.com
 */
@SuppressWarnings("all")   // 注解警告信息
public class MyGameFrame extends Frame {
    // 将背景图片与飞机图片定义为成员变量
    Image planeImage = GameUtil.getImage("images/plane.png");
    Image bgImage = GameUtil.getImage("images/bg.jpg");
    static int count = 0;
    // 将飞机的坐标设置为变量,初始值为(200,200)
    int planeX = 0;
    int planeY = 0;
    /**
     * paint方法作用是:画出整个窗口及内部内容。被系统自动调用。
     * @param g
     */
    @Override
    public void paint(Graphics g) {     // g当做是一支画笔
        System.out.println("绘制窗口次数:"+count);
        count++;
        g.drawImage(bgImage,0,0,500,500,null);
        g.drawImage(planeImage,planeX,100,22,33,null);
        planeX += 1;
    }

// 其他代码和上个版本一致,限于篇幅,此处只呈现新增的代码
    /**
     * 定义一个重画窗口的线程类,是一个内部类
     */
    class PaintThread extends Thread {
        public void run() {
            while (true) {
                // 内部类可以直接使用外部类的成员!
                repaint();
                try {
                    // 1s = 1000ms
                    Thread.sleep(40);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 初始化窗口
    public void launchFrame() {
        // 在游戏窗口打印标题
        setTitle("飞机大战");
        // 窗口是否可见
        setVisible(true);
        // 窗口大小
        setSize(Constant.GAME_WIDTH, Constant.GAME_HEIGHT);
        // 窗口打开的位置
        setLocation(400, 400);
        // 增加关闭窗口的动作
        this.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);     // 正常退出程序
            }
        });
        // 启动重画线程
        new PaintThread().start();
    }
    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 myGameFrame = new MyGameFrame();
        myGameFrame.launchFrame();
    }
}

如图所示:在这里插入图片描述
编写存放相关的常量的代码如下:

package com.txw.plan;

/**
 * 存放相关的常量
 * @author Adair
 * @email 1578533828@qq.com
 */
@SuppressWarnings("all")   // 注解警告信息
public class Constant {
    // 游戏窗口的宽度
    public static final int GAME_WIDTH = 500;
    // 游戏窗口的高度
    public static final int GAME_HEIGHT = 500;
}

如图所示:在这里插入图片描述
执行的结果为如图所示:在这里插入图片描述

2.6 GameObject 类设计(0.5 版)

·GameObject 类的定义
我们发现,窗口中所有的对象(飞机、炮弹等等)都有很多共性:“图片对象、坐标位置、 运行速度、宽度和高度”。为了方便程序开发,我们需要设计一个 GameObject 类,它可 以作为所有游戏物体的父类,方便我们编程。
1.选中项目,右键点击New–>Module。如图所示:在这里插入图片描述
2.选择Java和JDK的版本,点击Next。如图所示:在这里插入图片描述
3.填写项目名称,点击Finish即可。如图所示:在这里插入图片描述
GameObject 类的代码如下:

package com.txw.plan;

import java.awt.*;
/**
 * 游戏物体的根类
 * @author Adair
 * @email 1578533828@qq.com
 */
@SuppressWarnings("all")  // 注解警告信息
public class GameObject {
    // 该物体对应的图片对象
    Image image;
    // 该物体的坐标
    double x,y;
    // 该物体的运行速度
    int speed;
    // 该物体所在矩形区域的宽度和高度
    int width,height;

    public GameObject(Image image, double x, double y, int speed) {
        this.image = image;
        this.x = x;
        this.y = y;
        this.speed = speed;
        this.width = image.getWidth(null);
        this.height = image.getHeight(null);
    }

    public GameObject(Image image, double x, double y, int speed, int width, int height) {
        this.image = image;
        this.x = x;
        this.y = y;
        this.speed = speed;
        this.width = width;
        this.height = height;
    }

    public GameObject() {
    }

    /**
     * 怎么样绘制本对象
     * @param g
     */
    public void drwaMyself(Graphics g){
        g.drawImage(image,(int) x,(int) y,null);
    }

    /**
     * 所有的物体都是矩形。当你获得对应的矩形的时候,我们就可以做一些相关的判断的操作!
     * @return
     */
    public Rectangle getRect(){
        return new Rectangle((int)x,(int) y, width, height);
    }
}

如图所示:在这里插入图片描述
·设计飞机类 有了 GameObject 这个父类,我们设计飞机类特别简单,目前飞机类没有特别复杂的要 求。我们只需简单的继承,即可使用的代码如下:

package com.txw.plan;

import java.awt.*;
/**
 * 飞机类
 * @author Adair
 * @email 1578533828@qq.com
 */
@SuppressWarnings("all")  // 注解警告信息
public class Plane extends GameObject {

    @Override
    public void drwaMyself(Graphics g) {
        super.drwaMyself(g);
        //飞机飞行的算法,可以自行设定
        x += speed;
    }

    public Plane(Image image, double x, double y, int speed) {
        super(image, x, y, speed);
    }
}

如图所示:在这里插入图片描述

通过继承,我们发现实现新的类,爽了很多! ·MyGameFrame 类调用方式的调整 我们将 Plane 类封装后,也无需在 MyGameFrame 类中添加那么多飞机的属性,我们 全部封装到了 Plane 类里面,因此,调用也变得更加简单。代码如下:

package com.txw.plan;

import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
/**
 * 游戏主窗口
 * @author Adair
 * @email 1578533828@qq.com
 */
@SuppressWarnings("all")   // 注解警告信息
public class MyGameFrame extends Frame {
    // 将背景图片与飞机图片定义为成员变量
    Image planeImage = GameUtil.getImage("images/plane.png");
    Image bgImage = GameUtil.getImage("images/bg.jpg");
    static int count = 0;
    Plane plane1 = new Plane(planeImage,100,100,3);
    Plane plane2 = new Plane(planeImage,100,200,4);
    Plane plane3 = new Plane(planeImage,100,300,2);


    /**
     * paint方法作用是:画出整个窗口及内部内容。被系统自动调用。
     * @param g
     */
    @Override
    public void paint(Graphics g) {     // g当做是一支画笔
        System.out.println("绘制窗口次数:"+count);
        count++;
        g.drawImage(bgImage,0,0,500,500,null);
        // 画出飞机本身
        plane1.drwaMyself(g);
        plane2.drwaMyself(g);
        plane3.drwaMyself(g);
    }

// 其他代码和上个版本一致,限于篇幅,此处只呈现新增的代码
    /**
     * 定义一个重画窗口的线程类,是一个内部类
     */
    class PaintThread extends Thread {
        public void run() {
            while (true) {
                // 内部类可以直接使用外部类的成员!
                repaint();
                try {
                    // 1s = 1000ms 1s画20次(20*50=1000)
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 初始化窗口
    public void launchFrame() {
        // 在游戏窗口打印标题
        setTitle("飞机大战");
        // 窗口是否可见
        setVisible(true);
        // 窗口大小
        setSize(Constant.GAME_WIDTH, Constant.GAME_HEIGHT);
        // 窗口打开的位置
        setLocation(400, 400);
        // 增加关闭窗口的动作
        this.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);     // 正常退出程序
            }
        });
        // 启动重画线程
        new PaintThread().start();
    }
    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 myGameFrame = new MyGameFrame();
        myGameFrame.launchFrame();
    }
}

执行的结果为如图所示:在这里插入图片描述

3 飞机类设计(0.6 版)

飞机是我们游戏中的主物体,需要由玩家直接控制,手段有:键盘、鼠标、触摸屏等等。 无论是什么硬件,本质上都是玩家通过硬件改变游戏物体的坐标,从而实现多种多样的效果。 本节中,我们重点使用键盘进行交互。大家学会了使用键盘操控游戏物体,通过鼠标或 其他,我们只需要通过相关 API 的帮助即可轻松实现。

3.1 键盘控制原理

键盘和程序交互时,每次按下键、松开键都会触发相应的键盘事件,事件的信息都封装 到了 KeyEvent 对象中。
为了识别按下的键是哪个键, 系统对键盘所有按键做了编号,每个按键都对应相应的 数字。 比如:回车键对应数字 10,空格键对应数字 32 等。这些编号,我们都可以通过 KeyEvent 对象来查询,KeyEvent.VK_ENTER 实际就是存储了数字 10。
本游戏中,我们通过“上下左右”键来控制飞机的移动,因此我们可以设定四个布尔类 型的变量表示四个基本方向。 boolean left,up,right,down; 当按下左键时,left=true;当松开左键时,left=false;
程序根据四个方向的状态,进行移动,比如:left=true,即飞机向左移动,那么只需 x 坐标做减法即可。 其他方向同理。

if (left) {
	x -= speed;
}

3.2 飞机类:增加操控功能

我们为飞机类增加了 4 个方向,用来控制飞机的移动。同时,为了后续需求,也增加 了 live 变量,它表示飞机是“活的”还是“死的”,“活的”我们就画出飞机,“死的” 就不画飞机了。代码如下:

package com.txw.plan;

import java.awt.*;
import java.awt.event.KeyEvent;
/**
 * 飞机类
 * @author Adair
 * @email 1578533828@qq.com
 */
@SuppressWarnings("all")  // 注解警告信息
public class Plane extends GameObject {
    // 飞机的方向控制
    boolean left,up,right,down;

    @Override
    public void drwaMyself(Graphics g) {
        super.drwaMyself(g);
        //飞机飞行的算法,可以自行设定
       // x += speed;
        if (left){
            x += speed;
        }
        if(right){
            x -= speed;
        }
        if (up){
            y -= speed;
        }
        if (down){
            y += speed;
        }
    }

    /**
     * 按下上下左右键,则改变方向值。
     * 比如:按下上键,则 e.getKeyCode()的值就是VK_UP,那么置:up=true;
     * @param e
     */
    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;
        }
    }

    /**
     * 松开上下左右键,则改变方向值。
     * 比如:松开上键,则 e.getKeyCode()的值就是VK_UP,那么置:up
     * @param e
     */
    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 image, double x, double y, int speed) {
        super(image, x, y, speed);
    }
}

如图所示:在这里插入图片描述
我们通过定义 KeyMonitor 内部类来实现键盘监听功能。定义成内部类是为了方便和 外部窗口类(MyGameFrame)交互,可以直接调用外部类的属性和方法。
MyGameFrame 类:增加键盘监听功能代码如下:

package com.txw.plan;

import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
/**
 * 游戏主窗口
 * @author Adair
 * @email 1578533828@qq.com
 */
@SuppressWarnings("all")   // 注解警告信息
public class MyGameFrame extends Frame {
    // 将背景图片与飞机图片定义为成员变量
    Image planeImage = GameUtil.getImage("images/plane.png");
    Image bgImage = GameUtil.getImage("images/bg.jpg");
    Plane plane1 = new Plane(planeImage,100,100,7);

    /**
     * paint方法作用是:画出整个窗口及内部内容。被系统自动调用。
     * @param g
     */
    @Override
    public void paint(Graphics g) {     // g当做是一支画笔
        g.drawImage(bgImage,0,0,500,500,null);
        // 画出飞机本身
        plane1.drwaMyself(g);
    }

// 其他代码和上个版本一致,限于篇幅,此处只呈现新增的代码
    /**
     * 定义一个重画窗口的线程类,是一个内部类
     */
    class PaintThread extends Thread {
        public void run() {
            while (true) {
                // 内部类可以直接使用外部类的成员!
                repaint();
                try {
                    // 1s = 1000ms 1s画20次(20*50=1000)
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 初始化窗口
    public void launchFrame() {
        // 在游戏窗口打印标题
        setTitle("飞机大战");
        // 窗口是否可见
        setVisible(true);
        // 窗口大小
        setSize(Constant.GAME_WIDTH, Constant.GAME_HEIGHT);
        // 窗口打开的位置
        setLocation(400, 400);
        // 增加关闭窗口的动作
        this.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);     // 正常退出程序
            }
        });
        // 启动重画线程
        new PaintThread().start();
        // 启动键盘监听
        this.addKeyListener(new KeyMonitor());
    }

    // 内部类,实现键盘的监听处理
    class KeyMonitor extends KeyAdapter{
        @Override
        public void keyPressed(KeyEvent e) {
          //System.out.println("按下:"+e.getKeyCode());
           /* if(e.getKeyCode()==KeyEvent.VK_LEFT){
                left = true;
            }
            if(e.getKeyCode()==KeyEvent.VK_RIGHT){
                right = true;
            }*/
            plane1.addDirection(e);
        }

        @Override
        public void keyReleased(KeyEvent e) {
            // System.out.println("抬起:"+e.getKeyCode());
            plane1.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 myGameFrame = new MyGameFrame();
        myGameFrame.launchFrame();
    }
}

如图所示:在这里插入图片描述
执行的结果为如图所示:
在这里插入图片描述
至此,我们就实现了“四个方向”灵活移动飞机的功能!让我们终于可以和游戏物体直 接互动了,大家一定要试试哦。

4 炮弹类设计(0.7 版)

通过炮弹类的设计,我们可以更深入了解构造器的用法以及容器的用法。同时,可能还 需要读者稍微回忆一下初中数学曾学过的三角函数,这样更能理解炮弹飞行路径的计算原 理。 当然,如果忘记这些知识了也没关系,毕竟在实际开发中很少涉及数学原理性的内容。

4.1 炮弹类基本设计

炮弹类我们用实心的黄色椭圆实现,不再加载新的图片。当然,大家课下可以自行找一 些炮弹图片亦可。
我们的逻辑是在窗口固定位置(200,200)处生成炮弹,炮弹方向是随机的,并且遇到边 界会反弹。
编写Shell类的代码如下:

package com.txw.plan;

import java.awt.*;
/**
 * 炮弹类
 * @author Adair
 * @email 1578533828@qq.com
 */
@SuppressWarnings("all")  // 注解警告信息
public class Shell extends GameObject{

    // 角度。炮弹沿着指定的角度飞行
    double degree;

    public  Shell(){
        x = 200;
        y = 200;

        degree = Math.random()*Math.PI*2;

        width = 5;
        height = 5;

        speed = 3;
    }

    @Override
    public void drwaMyself(Graphics g) {
        Color c = g.getColor();
        g.setColor(Color.yellow);
        g.fillOval((int)x,(int)y,width,height);
        g.setColor(c);
        // 根据自己算法指定移动的路径
        x += speed*Math.cos(degree);
        y += speed*Math.sin(degree);
        // 碰到边界改变方向
        if(y > Constant.GAME_HEIGHT - this.height || y < 40) {
            degree = -degree;
        }

        if(x < 0 || x > Constant.GAME_WIDTH - this.width) {
            degree = Math.PI-degree;
        }
    }
}

如图所示:在这里插入图片描述

4.2 炮弹任意角度飞行路径

炮弹沿着任意角度飞行,核心代码就两行:

x += speed*Math.cos(degree); 
y += speed*Math.sin(degree);

这里实际用到了初中学的三角函数,通过 cos/sin 将任意角度分解到 X 轴、Y 轴,从而 可以精确的直到 x、y 坐标的变化情况。初中三角函数忘了的童鞋,自行脑补。如图所示:在这里插入图片描述

4.3 容器对象存储多发炮弹

为了存储多发炮弹,我们通过定义一个容器 ArrayList 来管理这些对象。在 paint 方法 中遍历容器中所有对象,并画出这些炮弹。
MyGameFrame 类:增加 ArrayList和炮弹。
代码如下:

package com.txw.plan;

import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
/**
 * 游戏主窗口
 * @author Adair
 * @email 1578533828@qq.com
 */
@SuppressWarnings("all")   // 注解警告信息
public class MyGameFrame extends Frame {
    // 将背景图片与飞机图片定义为成员变量
    Image planeImage = GameUtil.getImage("images/plane.png");
    Image bgImage = GameUtil.getImage("images/bg.jpg");
    Plane plane = new Plane(planeImage,100,100,7);
    Shell[] shells = new Shell[50];
    
    /**
     * paint方法作用是:画出整个窗口及内部内容。被系统自动调用。
     * @param g
     */
    @Override
    public void paint(Graphics g) {     // g当做是一支画笔
        g.drawImage(bgImage,0,0,500,500,null);
        // 画出飞机本身
        plane.drwaMyself(g);
        // 画炮弹
        for (int i = 0; i < shells.length; i++) {
            shells[i].drwaMyself(g);
        }
    }

    /**
     * 定义一个重画窗口的线程类,是一个内部类
     */
    class PaintThread extends Thread {
        public void run() {
            while (true) {
                // 内部类可以直接使用外部类的成员!
                repaint();
                try {
                    // 1s = 1000ms 1s画20次(20*50=1000)
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 初始化窗口
    public void launchFrame() {
        // 在游戏窗口打印标题
        setTitle("飞机大战");
        // 窗口是否可见
        setVisible(true);
        // 窗口大小
        setSize(Constant.GAME_WIDTH, Constant.GAME_HEIGHT);
        // 窗口打开的位置
        setLocation(400, 400);
        // 增加关闭窗口的动作
        this.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);     // 正常退出程序
            }
        });
        // 启动重画线程
        new PaintThread().start();
        // 启动键盘监听
        this.addKeyListener(new KeyMonitor());
        // 初始化创建50个炮弹对象
        for (int i = 0; i < shells.length; i++) {
            shells[i] = new Shell();
        }
    }

    // 内部类,实现键盘的监听处理
    class KeyMonitor extends KeyAdapter{
        @Override
        public void keyPressed(KeyEvent e) {
            plane.addDirection(e);
        }

        @Override
        public void keyReleased(KeyEvent e) {
            plane.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 myGameFrame = new MyGameFrame();
        myGameFrame.launchFrame();
    }
}

如图所示:在这里插入图片描述
执行的结果为如图所示:在这里插入图片描述
我们可以看到图中生成了若干炮弹,游戏窗口热闹了很多!大家可以举一反三 想一想,实际上,游戏窗口中的多个怪物、多个汽车、多个飞机都是生成多个对象,使用容 器来统一来管理的。

5 碰撞检测技术(0.8 版)

游戏中,碰撞是遇到最频繁的技术。当然,很多游戏引擎内部已经做了碰撞检测处理, 我们只需调用即可。本节课是从碰撞的原理进行讲解,大家自己去实现基本的碰撞检测。

5.1 矩形检测原理

游戏中,多个元素是否碰到一起,实际上,通常是用“矩形检测”原理实现的。 我们 在前面提到,游戏中所有的物体都可以抽象成“矩形”,我们只需判断两个矩形是否相交即 可。对于一些复杂的多边形、不规则物体,实际上是将他分解成多个矩形,继续进行矩形检测。
Java的API 中,为我们提供了Rectangle类来表示矩形相关信息,并且提供了intersects()方法,直接判断矩形是否相交。
我们在前面设计 GameObject 这个基类的时候,增加过一个方法:

 /**
     * 所有的物体都是矩形。当你获得对应的矩形的时候,我们就可以做一些相关的判断的操作!
     * @return
     */
    public Rectangle getRect(){
        return new Rectangle((int)x,(int) y, width, height);
    }

也就是说,本游戏中所有物体都能拿到他自己的矩形对象。

5.2 炮弹和飞机碰撞检测

我们的游戏逻辑是:“飞机碰到炮弹,则死亡”。也就是说,我们需要检测:“飞机和 所有的炮弹是否碰撞”。如果有 50 个炮弹对象,则进行 50 次比对检测即可。
我们修改 MyGameFrame 类的 paint()方法,:增加碰撞检测的代码如下:

package com.txw.plan;

import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
/**
 * 游戏主窗口
 * @author Adair
 * @email 1578533828@qq.com
 */
@SuppressWarnings("all")   // 注解警告信息
public class MyGameFrame extends Frame {
    // 将背景图片与飞机图片定义为成员变量
    Image planeImage = GameUtil.getImage("images/plane.png");
    Image bgImage = GameUtil.getImage("images/bg.jpg");
    Plane plane = new Plane(planeImage,100,100,7);
    Shell[] shells = new Shell[3];
    /**
     * paint方法作用是:画出整个窗口及内部内容。被系统自动调用。
     * @param g
     */
    @Override
    public void paint(Graphics g) {     // g当做是一支画笔
        g.drawImage(bgImage,0,0,500,500,null);
        // 画出飞机本身
        plane.drwaMyself(g);
        // 画炮弹
        for (int i = 0; i < shells.length; i++) {
            shells[i].drwaMyself(g);
            // 碰撞检测。将所有的炮弹和飞机进行进行检测,看有没有碰撞
            boolean peng = shells[i].getRect().intersects(plane.getRect());
            if (peng){
                // System.out.println("飞机被击中了!!");
                plane.live = false;
            }
        }
    }

    /**
     * 定义一个重画窗口的线程类,是一个内部类
     */
    class PaintThread extends Thread {
        public void run() {
            while (true) {
                // 内部类可以直接使用外部类的成员!
                repaint();
                try {
                    // 1s = 1000ms 1s画20次(20*50=1000)
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 初始化窗口
    public void launchFrame() {
        // 在游戏窗口打印标题
        setTitle("飞机大战");
        // 窗口是否可见
        setVisible(true);
        // 窗口大小
        setSize(Constant.GAME_WIDTH, Constant.GAME_HEIGHT);
        // 窗口打开的位置
        setLocation(400, 400);
        // 增加关闭窗口的动作
        this.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);     // 正常退出程序
            }
        });
        // 启动重画线程
        new PaintThread().start();
        // 启动键盘监听
        this.addKeyListener(new KeyMonitor());
        // 初始化创建50个炮弹对象
        for (int i = 0; i < shells.length; i++) {
            shells[i] = new Shell();
        }
    }

    // 内部类,实现键盘的监听处理
    class KeyMonitor extends KeyAdapter{
        @Override
        public void keyPressed(KeyEvent e) {
            plane.addDirection(e);
        }

        @Override
        public void keyReleased(KeyEvent e) {
            plane.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 myGameFrame = new MyGameFrame();
        myGameFrame.launchFrame();
    }
}

如图所示:在这里插入图片描述
上面逻辑要求:plane.live=false 时,飞机消失。所以,我们也需要修改 Plane 的代码。
Plane 类:根据飞机状态判断飞机是否消失的代码如下:

package com.txw.plan;

import java.awt.*;
import java.awt.event.KeyEvent;
/**
 * 飞机类
 * @author Adair
 * @email 1578533828@qq.com
 */
@SuppressWarnings("all")  // 注解警告信息
public class Plane extends GameObject {
    // 飞机的方向控制
    boolean left,up,right,down;
    // 活着
    boolean live = true;

    @Override
    public void drwaMyself(Graphics g) {
        if (live){
            super.drwaMyself(g);
            //飞机飞行的算法,可以自行设定
            // x += speed;
            if (left){
                x += speed;
            }
            if(right){
                x -= speed;
            }
            if (up){
                y -= speed;
            }
            if (down){
                y += speed;
            }
        }
    }

    /**
     * 按下上下左右键,则改变方向值。
     * 比如:按下上键,则 e.getKeyCode()的值就是VK_UP,那么置:up=true;
     * @param e
     */
    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;
        }
    }

    /**
     * 松开上下左右键,则改变方向值。
     * 比如:松开上键,则 e.getKeyCode()的值就是VK_UP,那么置:up
     * @param e
     */
    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 image, double x, double y, int speed) {
        super(image, x, y, speed);
    }
}

如图所示:在这里插入图片描述
这样,运行程序时,发生炮弹和飞机的碰撞,飞机消失,结果为如图所示:在这里插入图片描述

6 爆炸效果的实现(0.9 版)

飞机被炮弹击中后,需要出现一个爆炸效果,让我们的画面更刺激。 爆炸效果的实现 在游戏开发中也很常见。 我们定义 Exlode 类来表示爆炸的信息,爆炸类和普通类不一样的地方在于他实际上存 储了一系列爆炸的图片,然后,进行轮播。最后,我们看到的就是一组酷炫的效果。 我们这里准备了一系列爆炸图片:在这里插入图片描述
从爆炸开始的一个小火球到大火球,再到消失时的小火球。爆炸对象只需轮流加载这些 图片即可。
我们将这些图片拷贝到项目下面,新建:images/explode 文件夹,并将 16 张图片拷 贝到文件夹下面。在这里插入图片描述

6.1 爆炸类的基本设计

爆炸类 Explode的代码如下:

package com.txw.plan;

import java.awt.*;
/**
 * 爆炸类
 * @author Adair
 * @email 1578533828@qq.com
 */
@SuppressWarnings("all")   // 注解警告信息
public class Explode {
    // 位置
    double x,y;
    static Image[] imgs = new Image[16];
    int count;

    static {
        for(int i=0;i<16;i++) {
            imgs[i] = GameUtil.getImage("images/explode/e" + (i + 1) + ".gif");
            // 解决懒加载问题。目前没有问题,加和不加都行。
//          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;
    }
}

如图所示:在这里插入图片描述
我们定义了 Image[ ]来保存图片信息,并且使用了 static 代码块,也就是在类加载时 就加载这些图片,并且从属于类,不需要每次创建爆炸对象都加载图片,保证了运行的效率。 通过计数器 count 来控制到底画哪个图片,由于我们图片命名非常规范,是按照顺序 从 1-16,这样程序依次读取这些图片对象即可。

6.2 主窗口类创建爆炸对象

如果要显示爆炸对象,我们仍然需要在主窗口中定义爆炸对象,并且在飞机和炮弹碰撞 时,在飞机坐标处创建爆炸对象,显示爆炸效果。
MyGameFrame:增加爆炸效果代码如下:

package com.txw.plan;

import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
/**
 * 游戏主窗口
 * @author Adair
 * @email 1578533828@qq.com
 */
@SuppressWarnings("all")   // 注解警告信息
public class MyGameFrame extends Frame {
    // 将背景图片与飞机图片定义为成员变量
    Image planeImage = GameUtil.getImage("images/plane.png");
    Image bgImage = GameUtil.getImage("images/bg.jpg");
    Plane plane = new Plane(planeImage,100,100,7);
    Shell[] shells = new Shell[50];
    // 爆炸
    Explode explode;
    /**
     * paint方法作用是:画出整个窗口及内部内容。被系统自动调用。
     * @param g
     */
    @Override
    public void paint(Graphics g) {     // g当做是一支画笔
        // 画背景
        g.drawImage(bgImage,0,0,500,500,null);
        // 画出飞机本身
        plane.drwaMyself(g);
        // 画炮弹
        for (int i = 0; i < shells.length; i++) {
            shells[i].drwaMyself(g);
            // 碰撞检测。将所有的炮弹和飞机进行进行检测,看有没有碰撞
            boolean peng = shells[i].getRect().intersects(plane.getRect());
            if (peng){
                // System.out.println("飞机被击中了!!");
                plane.live = false;
                // 处理爆炸效果
                if (explode == null){
                    explode = new Explode(plane.x,plane.y);
                }
                explode.drawMySelf(g);
            }
        }
    }

    /**
     * 定义一个重画窗口的线程类,是一个内部类
     */
    class PaintThread extends Thread {
        public void run() {
            while (true) {
                // 内部类可以直接使用外部类的成员!
                repaint();
                try {
                    // 1s = 1000ms 1s画20次(20*50=1000)
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 初始化窗口
    public void launchFrame() {
        // 在游戏窗口打印标题
        setTitle("飞机大战");
        // 窗口是否可见
        setVisible(true);
        // 窗口大小
        setSize(Constant.GAME_WIDTH, Constant.GAME_HEIGHT);
        // 窗口打开的位置
        setLocation(400, 400);
        // 增加关闭窗口的动作
        this.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);     // 正常退出程序
            }
        });
        // 启动重画线程
        new PaintThread().start();
        // 启动键盘监听
        this.addKeyListener(new KeyMonitor());
        // 初始化创建50个炮弹对象
        for (int i = 0; i < shells.length; i++) {
            shells[i] = new Shell();
        }
    }

    // 内部类,实现键盘的监听处理
    class KeyMonitor extends KeyAdapter{
        @Override
        public void keyPressed(KeyEvent e) {
            plane.addDirection(e);
        }

        @Override
        public void keyReleased(KeyEvent e) {
            plane.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 myGameFrame = new MyGameFrame();
        myGameFrame.launchFrame();
    }
}

如图所示:在这里插入图片描述
程序执行结果,当飞机和炮弹碰撞时发生爆炸,如图所示:
在这里插入图片描述

7 其他功能(1.0 版)

完成了基本的功能,这时候体验度还是很一般。为了让玩家更愿意玩我们的游戏,增加 一些锦上添花的功能就很有必要。比如:游戏计时功能、全网排名等等。

7.1 计时功能

我们希望在玩游戏时,增加计时功能,可以清晰的看到自己玩了多长时间,增加刺激性。 这个功能的核心有两点: 1. 时间计算:当前时刻- 游戏结束的时刻。
2. 显示时间到窗口。
时间计算 我们在初始化窗口时,就保存一个起始时间;当飞机死亡时,保存一个结束时间。我们 在 MyGameFrame 中定义两个成员变量。
定义时间变量,代码如下:

Date startTime = new Date();		// 游戏起始时刻 
Date endTime; 						// 游戏结束时刻

我们在飞机死亡时,给 endTime 赋值,修改 paint 方法中代码如下:

package com.txw.plan;

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;

/**
 * 游戏主窗口
 * @author Adair
 * @email 1578533828@qq.com
 */
@SuppressWarnings("all")   // 注解警告信息
public class MyGameFrame extends Frame {
    // 将背景图片与飞机图片定义为成员变量
    Image planeImage = GameUtil.getImage("images/plane.png");
    Image bgImage = GameUtil.getImage("images/bg.jpg");
    Plane plane = new Plane(planeImage,100,100,7);
    Shell[] shells = new Shell[50];
    // 爆炸
    Explode explode;
    // 游戏开始的时间
    Date start = new Date();
    // 游戏结束的时间(飞机死的那一刻)
    Date end;
    // 玩了多少秒
    long period = 0;

    /**
     * paint方法作用是:画出整个窗口及内部内容。被系统自动调用。
     * @param g
     */
    @Override
    public void paint(Graphics g) {     // g当做是一支画笔
        // 画背景
        g.drawImage(bgImage,0,0,500,500,null);
        // 画时间
        drawTime(g);
        // 画出飞机本身
        plane.drwaMyself(g);
        // 画炮弹
        for (int i = 0; i < shells.length; i++) {
            shells[i].drwaMyself(g);
            // 碰撞检测。将所有的炮弹和飞机进行进行检测,看有没有碰撞
            boolean peng = shells[i].getRect().intersects(plane.getRect());
            if (peng){
                // System.out.println("飞机被击中了!!");
                plane.live = false;
                // 处理爆炸效果
                if (explode == null){
                    explode = new Explode(plane.x,plane.y);
                }
                explode.drawMySelf(g);
            }
        }
    }
    
    public void drawTime(Graphics g){
        Color c = g.getColor();
        Font  f = g.getFont();

        g.setColor(Color.green);
        if(plane.live) {
            period = (System.currentTimeMillis() - start.getTime()) / 1000;
            g.drawString("坚持:"+period,30,50);
        }else{
            if(end == 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);
    }
    
    /**
     * 定义一个重画窗口的线程类,是一个内部类
     */
    class PaintThread extends Thread {
        public void run() {
            while (true) {
                // 内部类可以直接使用外部类的成员!
                repaint();
                try {
                    // 1s = 1000ms 1s画20次(20*50=1000)
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 初始化窗口
    public void launchFrame() {
        // 在游戏窗口打印标题
        setTitle("飞机大战");
        // 窗口是否可见
        setVisible(true);
        // 窗口大小
        setSize(Constant.GAME_WIDTH, Constant.GAME_HEIGHT);
        // 窗口打开的位置
        setLocation(400, 400);
        // 增加关闭窗口的动作
        this.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);     // 正常退出程序
            }
        });
        // 启动重画线程
        new PaintThread().start();
        // 启动键盘监听
        this.addKeyListener(new KeyMonitor());
        // 初始化创建50个炮弹对象
        for (int i = 0; i < shells.length; i++) {
            shells[i] = new Shell();
        }
    }

    // 内部类,实现键盘的监听处理
    class KeyMonitor extends KeyAdapter{
        @Override
        public void keyPressed(KeyEvent e) {
            plane.addDirection(e);
        }

        @Override
        public void keyReleased(KeyEvent e) {
            plane.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 myGameFrame = new MyGameFrame();
        myGameFrame.launchFrame();
    }
}

执行的结果为如图所示:在这里插入图片描述

  • 1
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学无止路

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值