内容 | 知识点 |
游戏项目基本功能开发 | AWT基本知识 |
ImageIO 加载图片 | |
多线程和内部类 | |
双缓冲技术 | |
飞机类设计 | 键盘控制 |
八个方向自由控制 | |
类的设计 | |
炮弹类设计 | 炮弹任意角度飞行 |
容器或数组存储炮弹 | |
矩形碰撞检测 |
游戏项目基本功能开发
AWT技术画出游戏主窗口(0.1版)
● 基本功能实现
AWT和Swing是Java中常见的GUI(图形用户界面)技术。仅限于画出最基本的窗口和图形加载,本项目中,我们使用的是AWT技术,它是Java中最老的GUI技术,非常简单。
建立Java
项目,并建立类MyGameFrame
。项目结构如下图
【eg】MygameFrame类:画游戏窗口
package com.bjsxt.plane;
import java.awt.Frame;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
public class MyGameFrame extends Frame {
public void launchFrame(){
//在游戏窗口打印标题
setTitle("飞机大战_WWD");
//窗口默认不可见,设为可见
setVisible(true);
//窗口大小:宽度500,高度500
setSize(500, 500);
//窗口左上角顶点的坐标位置
setLocation(300, 300);
//增加关闭窗口监听,这样用户点击右上角关闭图标,可以关闭游戏程序
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
}
public static void main(String[ ] args) {
MyGameFrame f = new MyGameFrame();
f.launchFrame();
}
}
① 继承Frame类,画出窗口
Frame是java.awt中的主要类,我们画的窗口都需要继承Frame。这样,Frame的基本功能我们就可以直接使用了。
② 窗口坐标问题
setLocation(300, 300)
;代码的含义是定位窗口的位置。窗口的位置就是指“窗口左上角顶点的位置”。如下图:
这里Y轴的方向是向下的,跟我们初中数学学的方向不一样,这就是计算机里面的坐标系。
③ 物体就是矩形,物体的位置就是所在矩形左上角顶点的坐标
游戏或开发中,所有物体的都是矩形。即使你看到一个圆在那里,我们处理的本质上是圆的外切矩形。即使你看到一个美女,我们处理的是美女所占用的矩形空间。 不然,给你一个不规则的图形,你没法做很多运算。
游戏开发中的物体,位置通常指的就是:该物体的矩形的左上角顶点位置。
上图中的“飞机”,实际我们在编程时处理的是飞机所在的“矩形区域”。
④ 窗口关闭问题
Frame类默认没有处理关闭窗口功能,我们需要自己添加关闭功能。System.exit(0)
表示应用正常结束。addWindowListener()
表示增加窗口监听事件。
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
此处,如果想深入钻研,可以研究一下AWT的事件机制,在此不做赘述。
图形和文本绘制(0.2版)
● paint方法
① 如果要在窗口中画图或者显示什么内容,重写paint(Graphics g)
方法。
② paint()画出窗口及其内部内容。它会被系统自动调用。我们不需要去调用这个方法。
【eg】paint方法介绍
@Override
public void paint(Graphics g) {
//paint方法作用是:画出整个窗口及内部内容。被系统自动调用。
}
● Graphics画笔对象_画图形
Graphics对象,我们可以把它想象成“一支画笔”,窗口中的图形都由这支“画笔”画出来的。
画出每个图形都需要指定图形所在“矩形区域”的位置和大小。比如绘制椭圆。g.drawOval(100, 50, 300, 300)
; 实际上,就是根据椭圆所在的外切矩形来确定椭圆的基本信息。上面4个参数指的是椭圆外切矩形:左上角顶点(100px,50px),宽度300px,高度300px。
【eg】使用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);
}
执行结果:
ImageIO实现图片加载技术(0.3版)
游戏开发中,图片加载是最常见的技术。我们在此处使用ImageIO类实现图片加载,并且为了代码的复用,将图片加载的方法封装到GameUtil
工具类中,便于我们以后直接调用。
我们要先将项目用到的图片拷贝到项目的src
下面,我们可以建立新的文件夹images
存放所有图片:
● GameUtil工具类
我们可以将一些辅助性的工具方法通通放到GameUtil
中,便于重复调用。
【eg】GameUtil类:加载图片代码
package com.bjsxt.plane;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
import javax.imageio.ImageIO;
public class GameUtil {
// 工具类最好将构造器私有化。
private GameUtil() {
}
public static Image getImage(String path) {
BufferedImage bi = null;
try {
URL u = GameUtil.class.getClassLoader().getResource(path);
bi = ImageIO.read(u);
} catch (IOException e) {
e.printStackTrace();
}
return bi;
}
}
注:
① GameUtil.class.getClassLoader().getResource(path);可以帮助我们获得程序运行类加载器,加载资源的根目录,从而获得相对资源位置。
②
ImageIO.read()
方法是核心方法,帮助我们读取图片信息,并返回Image
对象。
● 加载游戏背景图片和飞机图片
我们将实现准备好的图片放到src/images
下面,然后,开始读取这些图片,并显示在窗口中。
【eg】MyGameFrame类:加载图片并增加paint方法
//将背景图片与飞机图片定义为成员变量
Image bgImg = GameUtil.getImage("images/bg.jpg");
Image planeImg = GameUtil.getImage("images/plane.png");
//paint方法作用是:画出整个窗口及内部内容。被系统自动调用。
@Override
public void paint(Graphics g) {
g.drawImage(bgImg, 0, 0, null);
g.drawImage(planeImg, 200, 200, null);
}
运行MyGameFrame类
,执行结果如图所示:
多线程和内部类实现动画效果(0.4版)
● 增加绘制窗口的线程类
前三个版本,我们步步为营,每个小版本都有功能的突破。但是,目前为止我们的窗口仍然是静态的,并没有像真正的游戏窗口那样“各种动、各种炫”。本节我们结合多线程实现动画效果。
我们在MyGameFrame类
中定义重画窗口线程PaintThread类
,为了方便使用MyGameFrame类
的属性和方法,我们将PaintThread
定义成内部类。
【eg】MyGameFrame类:增加PaintThread内部类
public class MyGameFrame extends Frame {
//其他代码和上个版本一致,限于篇幅,此处只呈现新增的代码
/**
* 定义一个重画窗口的线程类,是一个内部类
* @author 高淇
*
*/
class PaintThread extends Thread {
public void run(){
while(true){
repaint();
try {
Thread.sleep(40); //1s = 1000ms
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
定义好PaintThread内部类
后,我们还需要在窗口的launchFrame()
方法中创建线程对象和启动线程:
【eg】launchFrame方法:增加启动重画线程代码
public void launchFrame(){
//本方法其他代码和上个版本一致,限于篇幅,只显示新增的代码
new PaintThread().start(); //启动重画线程
}
【eg】示例13-7完成后的MyGameFrame类
package com.bjsxt.plane;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
public class MyGameFrame extends Frame {
Image bgImg = GameUtil.getImage("images/bg.jpg");
Image planeImg = GameUtil.getImage("images/plane.png");
static int count = 0;
//paint方法作用是:画出整个窗口及内部内容。被系统自动调用。
@Override
public void paint(Graphics g) {
g.drawImage(bgImg, 0, 0, null);
System.out.println("调用paint,重画窗口,次数:"+(count++));
g.drawImage(planeImg, 200, 200, null);
}
/**
* 定义一个重画窗口的线程类,是一个内部类
* @author 高淇
*/
class PaintThread extends Thread {
public void run(){
while(true){
repaint();
try {
Thread.sleep(40); //1s = 1000ms
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public void launchFrame(){
//在游戏窗口打印标题
setTitle("飞机大战__WWD");
//窗口默认不可见,设为可见
setVisible(true);
//窗口大小:宽度500,高度500
setSize(500, 500);
//窗口左上角顶点的坐标位置
setLocation(300, 300);
//增加关闭窗口监听,这样用户点击右上角关闭图标,可以关闭游戏程序
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
new PaintThread().start(); //启动重画线程
}
public static void main(String[ ] args) {
MyGameFrame f = new MyGameFrame();
f.launchFrame();
}
}
执行结果:
根据控制台打印的数据,我们发现paint方法被系统反复调用,一秒N次。按照线程中我们规定的是40ms画一次,1秒大约调用25次(1秒=1000ms)。也就是说,“现在,窗口被1秒重复绘制25次”,如果我们调整飞机的位置变量,每次画飞机位置都不一致,在肉眼看来不就实现动画了吗?
● 调整飞机位置,让飞机动起来
之前,我们绘制飞机的代码为:g.drawImage(planeImg, 200, 200, **null**)
; 每次都绘制到(200,200)
这个坐标位置。我们将位置定义为变量planeX
,planeY
,每次绘制变量值都发生变化(planeX += 3; )
,这样飞机就动起来了。 代码如下:
【eg】改变飞机的坐标位置
public class MyGameFrame extends Frame {
Image bgImg = GameUtil.getImage("images/bg.jpg");
Image planeImg = GameUtil.getImage("images/plane.png");
//将飞机的坐标设置为变量,初始值为(200,200)
int planeX=200;
int planeY=200;
static int count = 0;
//paint方法作用是:画出整个窗口及内部内容。被系统自动调用。
@Override
public void paint(Graphics g) {
g.drawImage(bgImg, 0, 0, null);
System.out.println("调用paint,重画窗口,次数:"+(count++));
//不再是写死的位置
g.drawImage(planeImg, planeX, planeY, null);
//每次画完以后改变飞机的x坐标
planeX +=3;
}
//限于篇幅,其他代码不放此处,和上个版本一致!
}
运行程序,我们发现,飞机真的飞起来了!
双缓冲技术解决闪烁问题(0.5版)
上个版本,我们实现了动画效果,但是发现窗口会不停的闪烁,体验度非常差。在实际开发中,绘制图形是非常复杂的,绘图可能需要几秒甚至更长时间,也经常发生闪烁现象, 为了解决这个问题,我们通常使用“双缓冲技术”。
“双缓冲技术”的绘图过程如下:
- 在内存中创建与画布一致的缓冲区
- 在缓冲区画图
- 将缓冲区位图拷贝到当前画布上
- 释放内存缓冲区
双缓冲即在内存中创建一个与屏幕绘图区域一致的对象,先将图形绘制到内存中的这个对象上,再一次性将这个对象上的图形拷贝到屏幕上,这样能大大加快绘图的速度。
我们只需将如下“双缓冲”实现代码,放入MyGrameFrame类
中,即可:
【eg】添加双缓冲技术
private Image offScreenImage = null;
public void update(Graphics g) {
if(offScreenImage == null)
offScreenImage = this.createImage(500,500);//这是游戏窗口的宽度和高度
Graphics gOff = offScreenImage.getGraphics();
paint(gOff);
g.drawImage(offScreenImage, 0, 0, null);
}
GameObject类设计
● GameObject类的定义
我们发现,窗口中所有的对象(飞机、炮弹等等)都有很多共性:“图片对象、坐标位置、运行速度、宽度和高度”。为了方便程序开发,我们需要设计一个GameObject类,它可以作为所有游戏物体的父类,方便我们编程。
【eg】GameObject类
package com.bjsxt.plane;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Rectangle;
public class GameObject {
Image img; //该物体对应的图片对象
double x,y; //该物体的坐标
int speed; //该物体的运行速度
int width,height; //该物体所在矩形区域的宽度和高度
/**
* 怎么样绘制本对象
* @param g
*/
public void drawMySelf(Graphics g){
g.drawImage(img, (int)x, (int)y, null);
}
public GameObject(Image img, double x, double y) {
this.img = img;
this.x = x;
this.y = y;
if(img!=null){
this.width = img.getWidth(null);
this.height = img.getHeight(null);
}
}
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 GameObject() {
}
/**
* 返回物体对应矩形区域,便于后续在碰撞检测中使用
* @return
*/
public Rectangle getRect(){
return new Rectangle((int)x,(int) y, width, height);
}
}
● 设计飞机类
有了GameObject类,设计飞机类特别简单。我们只需简单的继承,即可使用:
【eg】Plane类
package com.bjsxt.plane;
import java.awt.Graphics;
import java.awt.Image;
public class Plane extends GameObject {
@Override
public void drawMySelf(Graphics g) {
super.drawMySelf(g);
this.x +=3;//飞机水平飞,我们也可以调整x、y算法,按照我们指定的路径飞行
}
public Plane(Image img, double x, double y) {
super(img,x,y);
}
}
通过继承,我们发现实现新的类,爽了很多!
● MyGameFrame类调用方式的调整
我们将Plane类封装后,也无需在MyGameFrame类中添加那么多飞机的属性,我们全部封装到了Plane类里面,因此,调用也变得更加简单。
【eg】封装后的MyGameFrame类
public class MyGameFrame extends Frame {
Image bgImg = GameUtil.getImage("images/bg.jpg");
Image planeImg = GameUtil.getImage("images/plane.png");
Plane plane = new Plane(planeImg,300,300);
//paint方法作用是:画出整个窗口及内部内容。被系统自动调用。
@Override
public void paint(Graphics g) {
g.drawImage(bgImg, 0, 0, null);
plane.drawMySelf(g); //画出飞机本身
}
//其余代码,没有任何变化,不在附上,自行参考上一个版本。
}
通过面向对象封装后,如果我们要再创建多个飞机,也变得异常简单。
【eg】创建多个飞机
public class MyGameFrame extends Frame {
Image bgImg = GameUtil.getImage("images/bg.jpg");
Image planeImg = GameUtil.getImage("images/plane.png");
Plane plane = new Plane(planeImg,300,300);
Plane plane2 = new Plane(planeImg,300,350);
Plane plane3 = new Plane(planeImg,300,400);
//paint方法作用是:画出整个窗口及内部内容。被系统自动调用。
@Override
public void paint(Graphics g) {
g.drawImage(bgImg, 0, 0, null);
plane.drawMySelf(g); //画出飞机本身
plane2.drawMySelf(g); //画出飞机本身
plane3.drawMySelf(g); //画出飞机本身
}
//其余代码,和上个版本一致,为节省篇幅突出重点,不在附上。
}
执行结果:
飞机设计类(0.6版)
飞机是我们游戏中的主物体,需要由玩家直接控制,手段有:键盘、鼠标、触摸屏等等。无论是什么硬件,本质上都是玩家通过硬件改变游戏物体的坐标,从而实现多种多样的效果。我们重点使用键盘进行交互。学会了使用键盘操控游戏物体,通过鼠标或其他,我们只需要通过相关API的帮助即可轻松实现。
键盘控制原理
键盘和程序交互时,每次按下键、松开键都会触发相应的键盘事件,事件的信息都封装到了KeyEvent
对象中。为了识别按下的键是哪个键, 系统对键盘所有按键做了编号,每个按键都对应相应的数字。 比如:回车键对应数字10,空格键对应数字32等。这些编号,我们都可以通过KeyEvent对象来查询,KeyEvent.**VK_ENTER**
实际就是存储了数字10。本游戏中,我们通过“上下左右”键来控制飞机的移动,因此我们可以设定四个布尔类型的变量表示四个基本方向。
boolean left,up,right,down;
当按下左键时,left=true
;当松开左键时,left=false
;
程序根据四个方向的状态,进行移动,比如:left=true
,即飞机向左移动,那么只需x坐标做减法即可。 其他方向同理。
if (left) {
x -= speed;
}
飞机类:增加操控功能
我们为飞机类增加了4个方向,用来控制飞机的移动。同时,为了后续需求,也增加了live变量,它表示飞机是“活的”还是“死的”,“活的”我们就画出飞机,“死的”就不画飞机了。
【eg】Plane类:增加操控功能
package com.bjsxt.plane;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.KeyEvent;
public class Plane extends GameObject {
boolean left, up, right, down;
boolean live = true;
// 按下上下左右键,则改变方向值。
// 比如:按下上键,则e.getKeyCode()的值就是VK_UP,那么置:up=true;
public void addDirection(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_LEFT:
left = true;
break;
case KeyEvent.VK_UP:
up = true;
break;
case KeyEvent.VK_RIGHT:
right = true;
break;
case KeyEvent.VK_DOWN:
down = true;
break;
default:
break;
}
}
// 松开上下左右键,则改变方向值。
// 比如:松开上键,则e.getKeyCode()的值就是VK_UP,那么置:up=false;
public void minusDirection(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_LEFT:
left = false;
break;
case KeyEvent.VK_UP:
up = false;
break;
case KeyEvent.VK_RIGHT:
right = false;
break;
case KeyEvent.VK_DOWN:
down = false;
break;
default:
break;
}
}
@Override
public void drawMySelf(Graphics g) {
super.drawMySelf(g);
// 根据方向,计算飞机新的坐标
if (left) {
x -= speed;
}
if (right) {
x += speed;
}
if (up) {
y -= speed;
}
if (down) {
y += speed;
}
}
public Plane(Image img, double x, double y, int speed) {
super(img, x, y);
this.speed = speed;
}
}
主窗口类:增加键盘监听
我们通过定义KeyMonitor
内部类来实现键盘监听功能。定义成内部类是为了方便和外部窗口类(MyGameFrame)交互,可以直接调用外部类的属性和方法。
【eg】MyGameFrame类:增加键盘监听功能
//定义为内部类,可以方便的使用外部类的普通属性
class KeyMonitor extends KeyAdapter {
@Override
public void keyPressed(KeyEvent e) {
plane.addDirection(e);
}
@Override
public void keyReleased(KeyEvent e) {
plane.minusDirection(e);
}
}
在launchFrame()
方法中,启动键盘监听:
【eg】启动键盘监听
addKeyListener(new KeyMonitor());//增加键盘的监听
至此,我们就实现了“四个方向”灵活移动飞机的功能!
炮弹类设计(0.7版)
通过炮弹类的设计,我们可以更深入面向对象编程以及数组的知识。同时,可能还需要读者稍微回忆一下初中数学曾学过的三角函数,这样更能理解炮弹飞行路径的计算原理。 当然,如果忘记这些知识了也没关系,毕竟在实际开发中很少涉及数学原理性的内容。
炮弹类基本设计
炮弹类我们用实心的黄色椭圆实现,不再加载新的图片。当然,大家课下可以自行找一些炮弹图片亦可。
我们的逻辑是在窗口固定位置(200,200)
处生成炮弹,炮弹方向是随机的,并且遇到边界会反弹。
【eg】Shell类
package com.bjsxt.plane;
import java.awt.Color;
import java.awt.Graphics;
public class Shell extends GameObject {
double degree;
public Shell(){
degree = Math.random()*Math.PI*2;
x = 200;
y = 200;
width = 10;
height = 10;
speed = 3;
}
public void draw(Graphics g){
//将外部传入对象g的状态保存好
Color c = g.getColor();
g.setColor(Color.yellow);
g.fillOval((int)x, (int)y, width, height);
//炮弹沿着任意角度飞行
x += (int)(speed*Math.cos(degree));
y += (int)(speed*Math.sin(degree));
//如下代码,用来实现碰到边界,炮弹反弹回来(原理和打台球游戏一样)
if(y>Constant.GAME_HEIGHT-height||y<30){
degree = -degree;
}
if(x<0||x>Constant.GAME_WIDTH-width){
degree = Math.PI-degree;
}
//返回给外部,变回以前的颜色
g.setColor(c);
}
}
炮弹任意角度飞行路径
炮弹沿着任意角度飞行,核心代码就两行:
x += speedMath. cos(degree);
y += speedMath. sin(degree);
这里实际用到了初中学的三角函数,通过cos/sin将任意角度分解到X轴、Y轴,从而可以精确的直到x、y坐标的变化情况。初中三角函数忘了的童鞋,自行脑补。
存储多发炮弹
为了存储多发炮弹,我们通过定义一个数组(容器)来管理这些炮弹。
在paint方法中遍历容器中所有对象,并画出这些炮弹。
【eg】MyGameFrame类:增加ArrayList
public class MyGameFrame extends Frame {
Image bgImg = GameUtil.getImage("images/bg.jpg");
Image planeImg = GameUtil.getImage("images/plane.png");
Plane plane = new Plane(planeImg,300,300,3);
ArrayList<Shell> shellList = new ArrayList<Shell>();
//paint方法作用是:画出整个窗口及内部内容。被系统自动调用。
@Override
public void paint(Graphics g) {
g.drawImage(bgImg, 0, 0, null);
plane.drawMySelf(g); //画出飞机本身
//画出容器中所有的子弹
for(int i=0;i<shellList.size();i++){
Shell b = shellList.get(i);
b.draw(g);
}
}
//其余代码,和上个版本一致,限于篇幅,在此不显示。
}
我们初始化50发炮弹,在窗口初始化方法launchFrame()
中添加示例13-20中的代码。
【eg】添加炮弹
//初始化,生成一堆炮弹
for(int i=0;i<50;i++){
Shell b = new Shell();
shellList.add(b);
}
运行MyGameFrame类,执行结果如下图所示:
我们看到图中生成了若干炮弹,游戏窗口热闹了很多!实际上,游戏窗口中的多个怪物、多个汽车、多个飞机都是生成多个对象,使用容器来统一来管理的。
碰撞检测技术(0.8版)
游戏中,碰撞是遇到最频繁的技术。当然,很多游戏引擎内部已经做了碰撞检测处理,我们只需调用即可。
矩形检测原理
游戏中,多个元素是否碰到一起,实际上,通常是用“矩形检测”原理实现的。 我们在前面提到,游戏中所有的物体都可以抽象成“矩形”,我们只需判断两个矩形是否相交即可。对于一些复杂的多边形、不规则物体,实际上是将他分解成多个矩形,继续进行矩形检测。
Java的 API
中,提供了Rectangle类
来表示矩形相关信息,并且提供了intersects()
方法,直接判断矩形是否相交。在前面设计GameObject这个基类的时候,增加过一个方法:
/**
* 返回物体对应矩形区域,便于后续在碰撞检测中使用
* @return
*/
public Rectangle getRect(){
return new Rectangle((int)x,(int) y, width, height);
}
也就是说,本游戏中所有物体都能拿到他自己的矩形对象。
炮弹和飞机碰撞检测
我们的游戏逻辑是:“飞机碰到炮弹,则死亡”。也就是说,我们需要检测:“飞机和所有的炮弹是否碰撞”。如果有50个炮弹对象,则进行50次比对检测即可。
我们修改MyGameFrame类的paint()方法,如示例所示。
【eg】MyGameFrame类:增加碰撞检测
public void paint(Graphics g) {
g.drawImage(bgImg, 0, 0, null);
plane.drawMySelf(g); //画出飞机本身
//画出容器中所有的炮弹
for(int i=0;i<shellList.size();i++){
Shell b = shellList.get(i);
b.draw(g);
//飞机和所有炮弹对象进行矩形检测
boolean peng = b.getRect().intersects(plane.getRect());
if(peng){
plane.live = false; //飞机死掉,画面不显示
}
}
}
上面逻辑要求:plane.live=false时,飞机消失。所以,我们也需要修改Plane的代码。
【eg】Plane类:根据飞机状态判断飞机是否消失
public void drawMySelf(Graphics g) {
if(live){
super.drawMySelf(g);
//根据方向,计算飞机新的坐标
if(left){
x -= speed;
}
if(right){
x += speed;
}
if(up){
y -= speed;
}
if(down){
y += speed;
}
}
}
这样,运行程序时,发生炮弹和飞机的碰撞,飞机消失,结果如下图所示:
爆炸效果的实现(0.9版)
飞机被炮弹击中后,需要出现一个爆炸效果,让我们的画面更刺激。 爆炸效果的实现在游戏开发中也很常见。我们这里准备了一系列爆炸图片:
我们定义Exlode类来表示爆炸的信息,爆炸类和普通类不一样的地方在于他实际上存储了一系列爆炸的图片,然后,进行轮播。最后,我们看到的就是一组酷炫的效果。从爆炸开始的一个小火球到大火球,再到消失时的小火球。爆炸对象只需轮流加载这些图片即可。我们将这些图片拷贝到项目下面,新建:images/explode
文件夹,并将16张图片拷贝到文件夹下面。
爆炸类的基本设计
【eg】爆炸类Explode
package com.bjsxt.plane;
import java.awt.Graphics;
import java.awt.Image;
/*
* 爆炸类
*/
public class Explode {
double x,y;
static Image[ ] imgs = new Image[16];
static {
for(int i=0;i<16;i++){
imgs[i] = GameUtil.getImage("images/explode/e"+(i+1)+".gif");
imgs[i].getWidth(null);
}
}
int count;
boolean live = true;
public void draw(Graphics g){
if(!live){
return;
}
if(count<=15){
g.drawImage(imgs[count], (int)x, (int)y, null);
count++;
}else{
live = false;
}
}
public Explode(double x,double y){
this.x = x;
this.y = y;
}
}
我们定义了Image[ ]
来保存图片信息,并且使用了static
代码块,也就是在类加载时就加载这些图片,并且从属于类,不需要每次创建爆炸对象都加载图片,保证了运行的效率。
通过计数器count
来控制到底画哪个图片,由于我们图片命名非常规范,是按照顺序从1-16,这样程序依次读取这些图片对象即可。
主窗口类创建爆炸对象
如果要显示爆炸对象,我们仍然需要在主窗口中定义爆炸对象,并且在飞机和炮弹碰撞时,在飞机坐标处创建爆炸对象,显示爆炸效果。
【eg】MyGameFrame:增加爆炸效果
public class MyGameFrame extends Frame {
Image bgImg = GameUtil.getImage("images/bg.jpg");
Image planeImg = GameUtil.getImage("images/plane.png");
Plane plane = new Plane(planeImg,300,300,3);
ArrayList<Shell> shellList = new ArrayList<Shell>();
Explode bao;//创建爆炸对象
//paint方法作用是:画出整个窗口及内部内容。被系统自动调用。
@Override
public void paint(Graphics g) {
g.drawImage(bgImg, 0, 0, null);
plane.drawMySelf(g); //画出飞机本身
//画出容器中所有的子弹
for(int i=0;i<shellList.size();i++){
Shell b = shellList.get(i);
b.draw(g);
//飞机和所有炮弹对象进行矩形检测
boolean peng = b.getRect().intersects(plane.getRect());
if(peng){
plane.live = false; //飞机死掉,画面不显示
if(bao==null){
bao = new Explode(plane.x,plane.y);
}
bao.draw(g);
}
}
}
//其余代码和上一个版本一致,限于篇幅,不再展示
}
程序执行结果,当飞机和炮弹碰撞时发生爆炸,如下图所示:
其他功能(1.0版)
完成了基本的功能,这时候体验度还是很一般。为了让玩家更愿意玩我们的游戏,增加一些锦上添花的功能就很有必要。比如:游戏计时功能、全网排名等等。
计时功能
我们希望在玩游戏时,增加计时功能,可以清晰的看到自己玩了多长时间,增加刺激性。这个功能的核心有两点:
① 时间计算:当前时刻- 游戏结束的时刻
② 显示时间到窗口
● 时间计算
我们在初始化窗口时,就保存一个起始时间;当飞机死亡时,保存一个结束时间。我们在MyGameFrame
中定义两个成员变量,如示例13-25所示。
【eg】定义时间变量
Date startTime = new Date(); //游戏起始时刻
Date endTime; //游戏结束时刻
我们在飞机死亡时,给endTime
赋值,修改paint
方法中代码,如下所示。
【eg】计算游戏时间
//paint方法作用是:画出整个窗口及内部内容。被系统自动调用。
@Override
public void paint(Graphics g) {
g.drawImage(bgImg, 0, 0, null);
plane.drawMySelf(g); //画出飞机本身
//画出容器中所有的子弹
for(int i=0;i<shellList.size();i++){
Shell b = shellList.get(i);
b.draw(g);
//飞机和所有炮弹对象进行矩形检测
boolean peng = b.getRect().intersects(plane.getRect());
if(peng){
plane.live = false; //飞机死掉,画面不显示
endTime = new Date();
if(bao==null){
bao = new Explode(plane.x,plane.y);
}
bao.draw(g);
}
}
if(!plane.live){
if(endTime==null){
endTime = new Date();
}
int period = (int)((endTime.getTime()-startTime.getTime())/1000);
printInfo(g, "时间:"+period+"秒", 50, 120, 260, Color.white);
}
}
/**
* 在窗口上打印信息
* @param g
* @param str
* @param size
*/
public void printInfo(Graphics g,String str,int size,int x,int y,Color color){
Color c = g.getColor();
g.setColor(color);
Font f = new Font("宋体",Font.BOLD,size);
g.setFont(f);
g.drawString(str,x,y);
g.setColor(c);
}
执行结果如图所示: