在做完连连看以后,想到要做一个多线程游戏,本来是做的一个跳伞的小游戏的。但是做到一半的时候,觉得可玩性太低了。后面想来想去还是打算做一个以前玩过的雷霆战机小游戏,也就是飞机大战。
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 .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=-650int 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){//敌方飞机FALSEp.setAlive(false);//飞机死亡 Explode e = new Explode(x, y, gameui); gameui.explodes.add(e);//添加爆炸return true;}else if(p.isgood()==true){//我方飞机TRUEif(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;//子弹中的飞机列表esb.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 blocke.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都还没改,但没太多时间去继续弄飞机大战,等以后有空闲时间的话,会再继续完善飞机大战。