在做完連連看以後,想到要做一個多線程遊戲,本來是做的一個跳傘的小遊戲的。但是做到一半的時候,覺得可玩性太低了。後面想來想去還是打算做一個以前玩過的雷霆戰機小遊戲,也就是飛機大戰。
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都還沒改,但沒太多時間去繼續弄飛機大戰,等以後有空閑時間的話,會再繼續完善飛機大戰。