我们仿照 QQ游戏“”雷电这个风靡全球的游戏,来把所有知识点串接起来了
多线程用来实现动画效果、容器实现对于多发炮弹的存取和处理、常用类等等的应用。
但跟随本文只能做出雷电的雏形,但你可以在这个基础上改造出属于你的“雷电”
这是最后效果图:
游戏开发中,图片加载是最常见的技术。我们在此处使用ImageIO类实现图片加载,并且为了代码的复用,将图片加载的方法封装到GameUtil工具类中,便于我们以后直接调用。
我们要先将项目用到的图片拷贝到项目的src下面,我们可以建立新的文件夹images存放所有图片
方法1:通过在JFrame中添加一个JPanel,背景图片放在JPanel上来实现
方法2:我们用JLayeredPane,JLayeredPane 为 JFC/Swing 容器添加了深度,允许组件在需要时互相重叠。Integer
对象指定容器中每个组件的深度,其中编号较高的组件位于其他组件之上。常用的几个层如下图:
之前我再添加背景图片时一贯使用绝对路径,每次添加修改极为麻烦,本次我们学习如何使用相对路径,首先通过GameUtil.class.getClaaaLoader().getResource来获得资源的根目录,从而获取相对位置
paint会被自动被调用,g相当于一只画笔
代码如下:
/**
* 2019年9月21日
*/
package Thunder;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.KeyListener;
import java.util.ArrayList;
import javax.swing.JFrame;
/**
* 飞机大战雏形:可以发炮弹,但是没有解决连发炮弹的问题
*
* 之后有两个方向:画面上有很多炮弹,控制飞机移动躲避炮弹
* 或者经典的雷电游戏,击落敌机
*
* 2019年9月21日
*/
public class GameUI extends JFrame {
// 将背景和飞机图片定义为成员变量
Image background = GameUtil.getImage("images/back3.jpg");
Image planeImg = GameUtil.getImage("images/plane1.png");
Image shootImg = GameUtil.getImage("images/shoot1.png");
public static int UIWidth = 1000;
public static int UIHeigh = 1000;
//ArrayList shellList = new ArrayList();
Plane plane1 = new Plane(planeImg,200,200,2,100,100);
Shell shoot1 = new Shell(shootImg, 237,170,2,25,35);
public void GFrame() {
this.setTitle("雷電");
this.setSize(UIWidth, UIHeigh);
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(3);
this.addKeyListener(plane1);
this.addKeyListener(shoot1);
this.setVisible(true);
//启动线程
PaintThread pt =new PaintThread();
pt.start();
}
// paint方法作用是:画出整个窗口及内部内容。被系统自动调用。
public void paint(Graphics g) {
g.drawImage(background, 0, 0, 1000, 1000, null);
plane1.drawMySelf(g);
shoot1.drawMySelf(g);
}
// 定义内部类
class PaintThread extends Thread {
// 重写
public void run() {
while (true) {
repaint();// 重画
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
//双缓冲解决闪烁
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);
}
// 主方法
public static void main(String[] args) {
// 类里面调用窗体
GameUI gu = new GameUI();
gu.GFrame();
}
}
【要點】:
1.继承Frame类,画出窗口
2. 了解坐标系,窗口坐标以左上角为(0,0)点
3. 物体就是矩形,物体的位置就是所在矩形左上角顶点的坐标
4. 窗口关闭,居中等我们需要自己添加功能,如下
在JFrame中我们可以这样写:
drawframe.setDefaultCloseOperation(3);//关闭时程序结束
drawframe.setLocationRelativeTo(null);
但在Frame中,我们需要
/ 增加关闭窗口监听,这样用户点击右上角关闭图标,可以关闭游戏程�?
this.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
/**
* 2019年9月21日
*/
package Thunder;
import java.awt.Graphics;
import java.awt.Image;
import javax.swing.JFrame;
/**
*
*
* 2019年9月21日
*/
public class GameUI extends JFrame {
// 将背景和飞机图片定义为成员变量
Image background = GameUtil.getImage("images/back3.jpg");
Image planeImg = GameUtil.getImage("images/plane1.png");
public static int UIWidth = 1000;
public static int UIHeigh = 1000;
Plane plane1 = new Plane(planeImg,200,200,2,100,100);
public void GFrame() {
this.setTitle("雷電");
this.setSize(UIWidth, UIHeigh);
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(3);
//添加监听器
//Listener li = new Listener();
//this.addKeyListener(li);
this.addKeyListener(plane1);
this.setVisible(true);
//启动线程
PaintThread pt =new PaintThread();
pt.start();
}
// paint方法作用是:画出整个窗口及内部内容。被系统自动调用。
public void paint(Graphics g) {
g.drawImage(background, 0, 0, 1000, 1000, null);
plane1.drawMySelf(g);
}
// 定义内部类
class PaintThread extends Thread {
// 重写
public void run() {
while (true) {
repaint();// 重画
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
//双缓冲解决闪烁
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);
}
// 主方法
public static void main(String[] args) {
// 类里面调用窗体
GameUI gu = new GameUI();
gu.GFrame();
}
}
package Thunder;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
import javax.imageio.ImageIO;
/**
* GameUtil类:加载图片代码
*GameUtil获得程序运行类加载器,加载资源的根目录,
* 2019年9月22日
*/
public class GameUtil {
//工具类一般将构造器私有化
public 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;
}
}
/**
* 2019年9月22日
*/
package Thunder;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Rectangle;
/**
* 游戏中所有物体的父类
*
* 2019年9月22日
*/
public class GameObject {
Image img; //该物体对应的图片对象
double x,y; //该物体的坐标
int speed; //该物体的运行速度
int width,height; //该物体所在矩形区域的宽度和高度
public void drawMySelf(Graphics g){
g.drawImage(img, (int)x, (int)y, (int)width, (int)height, 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 width,
int height) {
this.img = img;
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
public GameObject(Image img, double x, double y, int speed, int width,
int height) {
this.img = img;
this.x = x;
//这只是构造方法初始化化时的值,并不能作为约束
this.y = y;
this.speed = speed;
this.width = width;
this.height = height;
}
public GameObject() {
}
/**
* 返回物体对应矩形区域,便于后续在碰撞检测中使用
* @return
*/
public Rectangle getRect(){
return new Rectangle((int)x,(int) y, width, height);
}
}
package Thunder;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
/**
*
*
* 2019年9月22日
*/
public class Plane extends GameObject implements KeyListener {
boolean left, right, up, down,shoot;
boolean live = true;
public void drawMySelf(Graphics g) {
super.drawMySelf(g);
// 这里可移动,但是下面不行,应该是判断的问题
this.x+=2;
System.out.println("此时的坐标"+x);
//System.out.println(left);
if (left) {
System.out.println("此时的坐标"+x);
x -= speed;
if(x<=0){
x=0;
}
}
if (right) {
x += speed;
if(x>=900){
x=900;
}
}
if (up) {
y -= speed;
if(y<=10){
y=10;
}
}
if (down) {
y += speed;
if(y>=900){
y=900;
}
}
}
public Plane(Image img, double x, double y, int speed, int width, int height) {
super(img, x, y,width, height);
//System.out.println("有被重画");
//this.speed = speed;
}
public void keyTyped(KeyEvent e) {
}
public void keyPressed(KeyEvent e) {
System.out.println(e.getKeyCode());
int key = e.getKeyCode();
switch (key) {
case 37:
left = true;
break;
case 38:
up = true;
break;
case 39:
right = true;
break;
case 40:
down = true;
break;
case 32:
shoot = true;
break;
default:
break;
}
}
public void keyReleased(KeyEvent e) {
//System.out.println(e.getKeyCode());
int key = e.getKeyCode();
switch (key) {
case 37:
left = false;
break;
case 38:
up = false;
break;
case 39:
right = false;
break;
case 40:
down = false;
break;
case 32:
shoot = true;
break;
default:
break;
}
}
}
我原来打算共享飞机坐标,这样就可以将子弹和飞机关联起来,但是这个语句导致 飞机完全不能动,因为坐标与图片没有关联了,重绘只画了一次
该怎么把子弹放入数组列表中呢?
现在子弹是一个新的类,而且有构造器,得随着飞机移动,但是
应该直接放入监听就可以了,点一下,延迟几秒,画一个。
现在到这个方向有两种结果可以走:
第一种:屏幕随机生成炮弹,我们控制飞机去躲避,生存时间越长,得分越高
第二钟:和游戏雷电一样,对面有敌机,我们在躲避敌机炮弹的同时击落敌机,获取分数。
在炮弹的任意角度飞行时,如果触碰到边界就反转角度,模拟碰撞效果。
同时为了效果美观,我们需要在一些细节上注意:例如碰撞边界时要减去球 的直径,碰撞标题栏时需要减去标题栏的宽度。
但是一个炮弹远远不够,我们希望增大一些挑战难度,有很多炮弹,就可以用到数组了。
分为三步:
1)定义数组
2)初始化五十个炮弹
3)画出炮弹
当使用Frame时,屏幕闪烁如图:
当使用JFrame解决屏幕闪烁问题时:
最后我们采用了frame+双缓冲的技术来解决,解决效果如图:
只需要把这段代码放入界面的类中(任意位置即可)
//双缓冲解决闪烁
private Image offScreenImage = null;
public void update(Graphics g) {
if(offScreenImage == null)
offScreenImage = this.createImage(1000,1000);//这是游戏窗口的宽度和高度
Graphics gOff = offScreenImage.getGraphics();
paint(gOff);
g.drawImage(offScreenImage, 0, 0, null);
}
在Jfram和frame切换中可能的报错:
当我们使用JFrame时可以调用这行语句关闭窗口
但是在Frame里面没有,所以我们需要添加监听器
//this.setDefaultCloseOperation(3);
//增加关闭窗口监听,这样用户点击右上角关闭图标,可以关闭游戏程序
this.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
游戏中,多个元素是否碰到一起,实际上,通常是用“矩形检测”原理实现的。 我们在前面提到,游戏中所有的物体都可以抽象成“矩形”,我们只需判断两个矩形是否相交即可。对于一些复杂的多边形、不规则物体,实际上是将他分解成多个矩形,继续进行矩形检测。
Java的API中,为我们提供了Rectangle类来表示矩形相关信息,并且提供了intersects()方法,直接判断矩形是否相交。
我们使用GameObject里面的矩形检测来判断,飞机有,炮弹也有,这也是继承的隐形好处.
爆炸图片轮播:
图片加载一次很耗费资源,所以我们将他设为static,只初始化一次,之后直接用就可以,而不需要new了,
爆炸类如下
package Thunder_two;
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;
public void draw(Graphics g){
if(count<=15){
g.drawImage(imgs[count], (int)x, (int)y, null);
count++;
}
}
public Explode(double x,double y){
this.x = x;
this.y = y;
}
}
到这一步,雷电游戏的雏形大概就完成了,但是我们却少了最重要的奖励机制,玩家不知道自己的得分,自然也不会有兴趣继续挑战了,所有我们加入时间类,在玩家死亡之后,在屏幕上显示获得的分数。
先定义两个成员变量
Date startTime = new Date(); //游戏起始时刻
Date endTime; //游戏结束时刻
如果飞机死亡,调用显示的方法
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);
}
//显示的方法
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);
}
至此,雷电小游戏的第一个方向就完成了,但大家也许还有点意犹未尽呢吧!这和我们儿时玩的雷电差距太大了吧,下一篇文章中,我们就来开发自动发射+击落敌机的部分,翘首以待吧。
package Thunder_one;
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;
public void draw(Graphics g){
if(count<=15){
g.drawImage(imgs[count], (int)x, (int)y, null);
count++;
}
}
public Explode(double x,double y){
this.x = x;
this.y = y;
}
}
/**
* 2019年9月22日
*/
package Thunder_one;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Rectangle;
/**
* 游戏中所有物体的父类
*
* 2019年9月22日
*/
public class GameObject {
Image img; //该物体对应的图片对象
double x,y; //该物体的坐标
int speed; //该物体的运行速度
int width,height; //该物体所在矩形区域的宽度和高度
public void drawMySelf(Graphics g){
g.drawImage(img, (int)x, (int)y, (int)width, (int)height, 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 width,
int height) {
this.img = img;
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
public GameObject(Image img, double x, double y, int speed, int width,
int height) {
this.img = img;
this.x = x;
//这只是构造方法初始化化时的值,并不能作为约束
this.y = y;
this.speed = speed;
this.width = width;
this.height = height;
}
public GameObject() {
}
/**
* 返回物体对应矩形区域,便于后续在碰撞检测中使用
* @return
*/
public Rectangle getRect(){
return new Rectangle((int)x,(int) y, width, height);
}
}
package Thunder_one;
import java.awt.Color;
import java.awt.Font;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import javax.swing.Timer;
import javax.swing.JFrame;
import javax.swing.JLabel;
/**
* :雷电第一个版本:
* 屏幕有很多随机生成的炮弹,我们需要控制飞机来躲避炮弹
* 存活时间越长,得分越高
*
*
*/
public class GameUI extends Frame {
Date startTime = new Date(); // 游戏起始时刻
Date endTime; // 游戏结束时刻
private int score = 0;// �?获分�?
// 界面宽和�?
public static int UIWidth = 1000;
public static int UIHeigh = 1000;
// 炮弹的数量
int ShellNum = 20;
// 飞机的速度
int PlSpeed = 20;
// 将背景和飞机图片定义为成员变量
Image background = GameUtil.getImage("images/back3.jpg");
Image planeImg = GameUtil.getImage("images/plane1.png");
Image shootImg = GameUtil.getImage("images/shoot1.png");
Plane plane1 = new Plane(planeImg, 500, 800, PlSpeed, 100, 100);
// 炮弹的数�?
Shell[] shellArr = new Shell[ShellNum];
// 创建爆炸对象
Explode bao;
// 爆炸的轮播图
Image[] imgs = new Image[16];
// 主界面
public void GFrame() {
this.setTitle("雷電");
this.setSize(UIWidth, UIHeigh);
this.setLocationRelativeTo(null);
// 增加关闭窗口监听,这样用户点击右上角关闭图标,可以关闭游戏程�?
this.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
this.addKeyListener(plane1);
// 初始化,生成炮弹
for (int i = 0; i < ShellNum; i++) {
shellArr[i] = new Shell();
}
this.setVisible(true);
// 启动线程
PaintThread pt = new PaintThread();
pt.start();
}
// paint方法作用是:画出整个窗口及内部内容�被系统自动调用�
public void paint(Graphics g) {
g.drawImage(background, 0, 0, 1000, 1000, null);
plane1.drawMySelf(g);
// 画出容器中所有的子弹
for (int i = 0; i < shellArr.length; i++) {
shellArr[i].draw(g);
// 检测碰撞
boolean peng = shellArr[i].getRect().intersects(plane1.getRect());
if (peng) {
plane1.live = false;// 飞机死掉
endTime = new Date();
if (bao == null) {
bao = new Explode(plane1.x, plane1.y);
}
bao.draw(g);
}
}
if (!plane1.live) {
if (endTime == null) {
endTime = new Date();
}
int period = (int) ((endTime.getTime() - startTime.getTime()) / 1000);
printInfo(g, "您的得分为:" + period + "�?", 50, 120, 260, Color.white);
}
}
// �?後計�?
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);
}
// 定义内部�?
class PaintThread extends Thread {
// 重写
public void run() {
while (true) {
repaint();// 重画
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
// 双缓冲解决闪�?
private Image offScreenImage = null;
public void update(Graphics g) {
if (offScreenImage == null)
offScreenImage = this.createImage(1000, 1000);
// 这是游戏窗口的宽度和高度
Graphics gOff = offScreenImage.getGraphics();
paint(gOff);
g.drawImage(offScreenImage, 0, 0, null);
}
// 主方�?
public static void main(String[] args) {
// 类里面调用窗�?
GameUI gu = new GameUI();
gu.GFrame();
}
}
package Thunder_one;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
import javax.imageio.ImageIO;
/**
* GameUtil类:加载图片代码
*GameUtil获得程序运行类加载器,加载资源的根目录,
* 2019年9月22日
*/
public class GameUtil {
//工具类一般将构造器私有化
public 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;
}
}
package Thunder_one;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
/**
*
*
* 2019年9月22日
*/
public class Plane extends GameObject implements KeyListener {
boolean left, right, up, down, shoot;
boolean live = true;
public void drawMySelf(Graphics g) {
if(live){
super.drawMySelf(g);
// 这里可移动,但是下面不行,应该是判断的问题
// this.x+=2;
// System.out.println(left);
if (left) {
x -= speed;
if (x <= 0) {
x = 0;
}
}
if (right) {
x += speed;
if (x >= 900) {
x = 900;
}
}
if (up) {
y -= speed;
if (y <= 10) {
y = 10;
}
}
if (down) {
y += speed;
if (y >= 900) {
y = 900;
}
}
}
}
public Plane(Image img, double x, double y, int speed, int width, int height) {
super(img, x, y, width, height);
this.speed = speed;
}
public void keyTyped(KeyEvent e) {
}
public void keyPressed(KeyEvent e) {
//System.out.println(e.getKeyCode());
int key = e.getKeyCode();
switch (key) {
case 37:
left = true;
break;
case 38:
up = true;
break;
case 39:
right = true;
break;
case 40:
down = true;
break;
default:
break;
}
}
public void keyReleased(KeyEvent e) {
// System.out.println(e.getKeyCode());
int key = e.getKeyCode();
switch (key) {
case 37:
left = false;
break;
case 38:
up = false;
break;
case 39:
right = false;
break;
case 40:
down = false;
break;
default:
break;
}
}
}
/**
* 2019年9月26日
*/
package Thunder_one;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Image;
import java.util.Random;
import Thunder.GameUtil;
/**
*
*
* 2019年9月26日
*/
public class Shell extends GameObject{
double degree;
Random ra = new Random();
public Shell(){
degree = Math.random()*Math.PI*2;
x = ra.nextInt(700)+250;
y = ra.nextInt(700)+150;
width = ra.nextInt(15)+10;
height = ra.nextInt(15)+10;
speed = ra.nextInt(3)+2;
}
public void draw(Graphics g){
//将外部传入对象g的状态保存好
Color c = g.getColor();
g.setColor(Color.yellow);
Image shootImg = GameUtil.getImage("images/shoot1.png");
g.drawImage(shootImg, (int)x, (int)y, width, height, null);
//g.fillOval((int)x, (int)y, width, height);
//炮弹沿着任意角度飞行
x += speed*Math.cos(degree);
y += speed*Math.sin(degree);
//如下代码,用来实现碰到边界,炮弹反弹回来(原理和打台球游戏一样)
if(y>GameUI.UIHeigh-height||y<40){
degree = -degree;
}
if(x<0||x>GameUI.UIWidth-width){
degree = Math.PI-degree;
}
//返回给外部,变回以前的颜色
//g.setColor(c);
}
}