在做完连连看以后,想到要做一个多线程游戏,本来是做的一个跳伞的小游戏的。但是做到一半的时候,觉得可玩性太低了。后面想来想去还是打算做一个以前玩过的雷霆战机小游戏,也就是飞机大战。
1.效果展示
2.绘制背景
3.方向类
4.飞机类
5.子弹类
6.爆炸类
7.道具类
8.总结一下界面类里面的绘制线程
9.播放音乐
10.开始界面
1.效果展示
直接放图了。

博主自己特别喜欢的一个特效,吃道具后能够变声,而且附带数码宝贝的音效,但是只能展示动图了,配合音效会更有感觉一点。
然后是动图
2.绘制背景
我们先不管游戏的开始界面啥的,先从主要的开始入手。
第一步就是绘制背景界面了。
在实际的效果中,像是飞机在飞一样,其实只是背景图片在移动,然后看上去就感觉飞机在飞。
在博主画的两个框里面,蓝色代表背景图片。黑色代表软件界面。因为背景图片的长度是大于软件界面的,所以将背景图慢慢移动,就会造成一种动态画面的效果。然后将背景图片调用两次,第一张放完后就放第二张,第二张放完后就再放第一张,循环下去,就会有背景一直在动的感觉。
因为背景图片是bmp格式的,博主试了一下,改为jpg或者png都不能在eclipse里面显示。
所以等下如果要用到这张背景图就不要改格式了。
而且bmp格式的图片,好像不能直接调用,不然显示不出来。
下面的代码博主试了一下就是读取bmp图片且调用的代码。
这段代码是每次读取图片用到的工具类。
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
import javax.imageio.ImageIO;
public class GameImage {
private GameImage() {};//工具类一般设置为私有
public static Image getImage(String path) {
URL u = GameImage.class.getClassLoader().getResource(path);
BufferedImage img = null;
try {
img = ImageIO.read(u);
} catch (IOException e) {
e.printStackTrace();
}
return img;
}
}
下面是关于背景图片部分的代码
其中绘画部分是要放在同一个线程里面的,不然会导致画面中背景或者飞机或者子弹不停的闪烁。
所以这个绘制背景的代码应该放到绘制线程里面。
int posY=-650;//窗体的高度减去图片高度,810-1460=-650
int posY2 = posY-1412;//posY是第一张图片y坐标,posY2是第二张图片y坐标
while(true){
if(posY>=760){//交替
posY = posY2 -gameBg.getHeight(null);
}else{
if(posY2>=760){//交替
posY2 = posY - gameBg.getHeight(null);
}else{
if(begin==false){//开始滚动
posY += 2;
bg.drawImage(gameBg, 0, posY, null);
posY2 +=2;
bg.drawImage(gameBg, 0, posY2, null);
}
}
}
3.方向类
因为敌方飞机和我方飞机都需要移动。
而在键盘监听器里面最好不去键盘按键实现方法,而是键盘按键代表一个状态,然后根据状态调用方法。
所以我们用一个枚举类来写方向。
代码如下:
public enum Direction {//枚举类型
L, R, RD, D, LD, STOP,LU, U, RU
}
4.飞机类
我们先写飞机类的构造函数,方便去调用
public void pic(){
//调用飞机图片
myImgs[0] = GameImage.getImage("resources/plane1.png");
myImgs[1] = GameImage.getImage("resources/plane23.png");
enemyImgs[0] = GameImage.getImage("resources/enemy3.png");
bossImgs[0] = GameImage.getImage("resources/boss6.png");
}
public Plane(int num, int x,int y,int speed,boolean good,gameUI gameui) {
pic();
this.num=num;
this.x=x;
this.y=y;
this.speed=speed;
this.good=good;
this.gameui=gameui;
if(good==true) {//good为true时,为我自己飞机,调用我飞机图片
ensureImg = myImgs[num];
WIDTH = ensureImg.getWidth(null);
HEIGHT = ensureImg.getHeight(null);
}else {
if(num==0) {
ensureImg = enemyImgs[0];
dir = Direction.D;//设置普通敌机方向只有向下
WIDTH = ensureImg.getWidth(null);
HEIGHT = ensureImg.getHeight(null);
}
}
}
然后再写飞机的键盘监听器方法。
因为我在方向类里面说了,在键盘监听器里用按键表示状态。
所以飞机类里面都是按键表示状态。
特别注意,在按键时比如左移bL的状态为true;松开时左移的状态为false;不然会按一次键就一只移动。
public void Press(KeyEvent e) {
int key = e.getKeyCode();
switch(key) {
case KeyEvent.VK_LEFT :
bL = true;
break;
case KeyEvent.VK_UP :
bU = true;
break;
case KeyEvent.VK_RIGHT :
bR = true;
break;
case KeyEvent.VK_DOWN :
bD = true;
break;
}
locateDirection();
}
public void Release(KeyEvent e) {
int key = e.getKeyCode();
switch(key) {
case KeyEvent.VK_LEFT :
bL = false;
break;
case KeyEvent.VK_UP :
bU = false;
break;
case KeyEvent.VK_RIGHT :
bR = false;
break;
case KeyEvent.VK_DOWN :
bD = false;
break;
case KeyEvent.VK_M:
fire();
break;
}
locateDirection();
}
public void locateDirection() {
if(bL && !bU && !bR && !bD) dir = Direction.L;
else if(bL && bU && !bR && !bD) dir = Direction.LU;
else if(!bL && bU && !bR && !bD) dir = Direction.U;
else if(!bL && bU && bR && !bD) dir = Direction.RU;
else if(!bL && !bU && bR && !bD) dir = Direction.R;
else if(!bL && !bU && bR && bD) dir = Direction.RD;
else if(!bL && !bU && !bR && bD) dir = Direction.D;
else if(bL && !bU && !bR && bD) dir = Direction.LD;
else if(!bL && !bU && !bR && !bD) dir = Direction.STOP;
}
监听键盘后,我们根据飞机的状态再来移动。
所以需要写一个移动的方法。
public void move() {
switch(dir) {
case L:
x -= speed;
break;
case LU:
x-=speed;
y-=speed;
break;
case U:
y-=speed;
break;
case RU:
x+=speed;
y-=speed;
break;
case R:
x+=speed;
break;
case RD:
x += speed;
y += speed;
break;
case D:
y += speed;
break;
case LD:
x -= speed;
y += speed;
break;
case STOP:
break;
}
if(x<0) x=0;//左边界
if(y<40) y=40;//上边界
if(x+ensureImg.getWidth(null)>600) x=600-ensureImg.getWidth(null);//右边界
if(y+ensureImg.getHeight(null)>760) y=760-ensureImg.getHeight(null);//
}
然后是画飞机的方法。
这个方法一定在绘画线程里面和其他所有的绘画方法一起被调用,不然会导致画面闪烁。
最后是创建敌机的方法。
public void createEnemy() {
if(this.es.size()<4){//使敌机数量保持在4架
Plane ePlane = new Plane(0,r.nextInt(500),0,5,false,this);//敌机
this.es.add(ePlane);
if(r.nextInt(50)>30) {
ePlane.fire();
}
}
}
5.子弹类
和飞机类的流程差不大多。
为了方便生成子弹,所以我们第一步任然是写一个子弹类的构造方法。
pic()仍为调用图片方法。
public void pic() {
myImgs[0] = GameImage.getImage("resources/m5.png");
enemyImgs[0] = GameImage.getImage("resources/em1.png");
}
public Bullet(int x,int y,int speed,int randIndex,boolean good,gameUI gameui) {
pic();
this.x=x;
this.y=y;
this.randIndex = randIndex;
this.speed = speed;
this.good=good;
this.gameui=gameui;
pic();
if(good==true) {
ensureImg = myImgs[0];
dir = Direction.U;
WIDTH = ensureImg.getWidth(null);
HEIGHT = ensureImg.getHeight(null);
this.power = 20;//我子弹威力为10
}else {
dir = Direction.D;
ensureImg = enemyImgs[0];
WIDTH = ensureImg.getWidth(null);
HEIGHT = ensureImg.getHeight(null);
this.power = 10;//敌方子弹威力为5
}
}
写完构造类以后,我们想一想,子弹剩下的就是移动和达到飞机的情况了。
然后情况又分为我方子弹命中敌机,敌机子弹命中我机两种情况。
所以剩下需要写的方法分别是对子弹的移动和命中飞机的方法。
private void move() {
switch(dir) {
case U:
y -= speed;
break;
case D:
y += speed;
break;
}
if(x < 0 || y < 0 || x > 880 || y > 760) {
isAlive = false;//出界就设置为false
}
}
public boolean hitPlane(Plane p) { //敌方攻击我
//intersects(Rcetangle),判断该Rectangle与当前Rectangle是否相交
if(this.isAlive && this.getRect().intersects(p.getRect()) && p.getAlive() && this.good != p.isgood())
{
this.isAlive = false;
p.setLife(p.getLife()-this.power);
if(p.isgood()==false){//敌方飞机FALSE
p.setAlive(false);//飞机死亡
Explode e = new Explode(x, y, gameui);
gameui.explodes.add(e);//添加爆炸
return true;
}else if(p.isgood()==true){//我方飞机TRUE
if(p.life<=0){
p.setAlive(false);//飞机死亡
//myplane.setAlive(false);
Explode e = new Explode(x, y, gameui);
gameui.explodes.add(e);//添加爆炸
return true;
}
}
}
return false;
}
public boolean hitPlanes(List<Plane> planes) { //我攻击敌方
for(int i=0; i<planes.size(); i++) {
if(hitPlane(planes.get(i))) {
es.remove(i);
return true;
}
}
return false;
}
这里需要注意的点是,先写一个hitPlane敌方攻击我的方法,在另一个攻击敌方的方法里面对这个方法进行调用就可以了,算是一个需要注意的敌方。
另一个是es为敌方飞机的队列,哪架飞机被击中,就在队列里面被移去。
6.爆炸类
爆炸类比飞机类和子弹类都更加简单。
因为只需要在飞机死亡后绘制爆炸。
所以爆炸类的方法就只有构造方法和绘画方法。
public void pic() {
images[0]=GameImage.getImage("Resources/blast_0_5.png");
}
public Explode(int x,int y,gameUI gameui) {
super();
this.x=x;
this.y=y;
this.gameui=gameui;
}
public void draw(Graphics g) {
pic();
WIDTH = images[0].getWidth(null);
HEIGHT = images[0].getWidth(null);
if(!live) {
gameui.explodes.remove(this);//爆炸结束移除
//System.out.println("live="+live+" ");
return;
}
if(live) {
g.drawImage(images[0], x-(WIDTH/2), y-(HEIGHT/2), null);
//System.out.println("画爆炸图片");
live=false;
}
}
相对于前面的飞机类和子弹类,爆炸类也没有什么需要多讲的地方了。
7.道具类
这个道具,博主因为时间的关系只写了一种道具,就是前面展示了的变身的道具类。
方法依然是旧几个,构造方法,移动方法,绘制方法,道具相撞飞机调用的方法。
只是飞机吃到道具后,这里需要特别注意一下!
因为博主所有的绘画方法都是放在一个绘画线程里面被调用。
而我的变身GIF图片有5秒左右,这个时候需要暂停以前的绘画线程,然后把画变身单独做一个新线程用join方法插入,这样才能有我的GIF那样的效果。
控制变身的线程
public class ControlThread extends Thread{
Graphics g;
public void run() {
while(true) {
g.drawImage(jinhua.getImage(),0,0,590,800,null);
}
}
}
道具类的方法
public void draw(Graphics g) {
pic();
if(isAlive==false) {
return;//道具被吃掉则消失
}
T_WIDTH = propImgs[0].getWidth(null);
T_HEIGHT = propImgs[0].getHeight(null);
g.drawImage(propImgs[0],x,y,T_WIDTH,T_HEIGHT,null);
}
public void move() {
if(x<=0 || x>=561) {
speedx = -speedx;
}
if(y<=0 || y>=750) {
speedy = -speedy;
}
x-=speedx;
y+=speedy;
}
public boolean hitProp(Plane myplane) {
if(myplane.isgood()==false) {
return true;
}
else if(myplane.isgood()==true) {
if(this.isAlive && this.getRect().intersects(myplane.getRect()) && myplane.getAlive()) {
this.isAlive=false;
myplane.setAlive(false);
return true;
}
//System.out.println("没有碰到");
}
return false;
}
8.总结一下界面类里面的绘制线程
让大家容易看懂,直接贴代码了。
因为这一段代码稍微有点长,博主直接把注释打在代码注释里面了。
public class BgThread extends Thread{
Graphics g;
JTextField textField;
JTextField hitField;
gameUI gameui;
Image[] myImgs;
ControlThread t2;
public void run() {
while(true){
if(posY>=760){//交替
posY = posY2 -gameBg.getHeight(null);
}else{
if(posY2>=760){//交替
posY2 = posY - gameBg.getHeight(null);
}else{
if(begin==false){//开始滚动
posY += 2;
bg.drawImage(gameBg, 0, posY, null);
posY2 +=2;
bg.drawImage(gameBg, 0, posY2, null);
}
}
}
myplane.draw(bg);
this.textField.setText("剩余生命:"+myplane.life);
//绘制子弹
for(int i=0; i<bs.size(); i++) {//将集合中的子弹都绘制出来
Bullet b = bs.get(i);
b.es=es;//子弹中的飞机列表es
b.draw(bg);
// System.out.println("重绘部分在画子弹");
b.hitPlanes(es);
b.hitPlane(myplane);
}
createEnemy();
//绘制敌机
for(int i=0; i<es.size(); i++) {
Plane p = es.get(i);
p.draw(bg);
p.hitBorder(es);
}
prop.draw(bg);
prop.move();
if(prop.hitProp(myplane)) {
//变身
myplane.num=1;
myplane.ensureImg = myImgs[1];
// bg.drawImage(jinhua.getImage(),0,150,590,500,null);
t2.start();
new PlaySound("进化.mp3", false).start();
try {
t2.join(5000);
t2.stop();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// bg.drawImage(jinhua.getImage(),0,150,590,500,null);
myplane.setAlive(true);
}
//绘爆炸
for(int i=0;i<explodes.size();i++) {
Explode e = explodes.get(i);
e.draw(bg);
count++;
}
this.hitField.setText("击落敌机:"+count/2);
g.drawImage(buffer, 0,0, null);//把所有的东西从缓存画下来
try {
Thread.sleep(50);//滚动速度的设定
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
9.播放音乐。
以前在做连连看的时候,插入的音乐需要转换成为WAV格式才能使用,然后这一段代码可以直接适用MP3格式了。很方便,也直接贴出来了。
import java.io.InputStream;
import javazoom.jl.player.advanced.AdvancedPlayer;
/**
*
* 必须使用多线程,播放音效
*
*/
public class PlaySound extends Thread{
private String mp3Url;
private boolean isLoop;
public PlaySound(String mp3Url, boolean isLoop) {
super();
this.mp3Url = mp3Url;
this.isLoop = isLoop;
}
public void run() {
do{
//读取音频文件流
InputStream mp3 = PlaySound.class.getClassLoader().getResourceAsStream("resources/"+mp3Url);
try {
//创建播放器
AdvancedPlayer adv = new AdvancedPlayer(mp3);
//播放
adv.play();
} catch (Exception e) {
e.printStackTrace();
}
}while(isLoop);
}
}
10.开始界面。
开始界面其实和以前讲过的qq登录界面差不多。
只要在按钮加上监听器就好。所以也直接贴代码了。
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Graphics;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
public class StartUI extends JFrame{
public void showUI() {
StartUI startFrame = new StartUI();
startFrame.setSize(900,650);
startFrame.setTitle("Design By TangNan");
startFrame.setLocationRelativeTo(null);//居中
startFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//关闭
FlowLayout flowl = new FlowLayout();//布局
startFrame.setLayout(flowl);
StartListener startL = new StartListener();
startFrame.addMouseListener(startL);
startL.j=startFrame;
ImageIcon image =new ImageIcon(getClass().getResource("resources/FJDAZ_START1.png"));
JLabel iconLabel = new JLabel(image);
startFrame.add(iconLabel);
Dimension btnsize = new Dimension(125,50);
JButton btn1 = new JButton("开始游戏");
btn1.setPreferredSize(btnsize);
startFrame.add(btn1);
btn1.addActionListener(startL);
JButton btn2 = new JButton("提示说明");
btn2.setPreferredSize(btnsize);
startFrame.add(btn2);
btn2.addActionListener(startL);
startFrame.setVisible(true);
Graphics g = startFrame.getGraphics();//获取画板放在可视化之后
}
}
最后,飞机大战到这里就算简单的完成了。其实博主还有其他蛮多想加的效果和一些BUG都还没改,但没太多时间去继续弄飞机大战,等以后有空闲时间的话,会再继续完善飞机大战。