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();
}
}
执行的结果为如图所示: