飛機(jī)大戰(zhàn)詳細(xì)文檔
文末有源代碼沼填,以及本游戲使用的所有素材,將plane2文件復(fù)制在src文件下可以直接運(yùn)行馏段。
實(shí)現(xiàn)效果:
結(jié)構(gòu)設(shè)計(jì)
- 角色設(shè)計(jì)飛行對(duì)象類 FlyObject戰(zhàn)機(jī)類我的飛機(jī) MyPlane敵方飛機(jī) EnemyPlane子彈類我的子彈 MyBullet敵方子彈 EnemyBullet道具類 Prop加分堰塌,加血涡拘,升級(jí)地圖背景類 Background玩家類 PlayerHP,得分
- 線程類繪制線程 DrawThread移動(dòng)線程 MoveThread生成敵方飛機(jī)線程 EnemyPlaneThread敵方飛機(jī)生成子彈線程 EnemyButtleThread檢測(cè)碰撞線程 TestCrashThread
- 界面類主界面 GameUI選擇地圖界面 SelectMapUI
- 監(jiān)聽器類 KListener通過按壓鍵盤改變我方飛機(jī)的速度
- 數(shù)據(jù)結(jié)構(gòu)我方戰(zhàn)機(jī)(只有一個(gè))我方飛機(jī)子彈集合敵方飛機(jī)集合敵方子彈集合道具集合
詳細(xì)分析
Main界面類
使用邊框布局嚼锄,給面板分三個(gè)區(qū)减拭,如圖所示
關(guān)鍵代碼:
JFrame jf = new JFrame("飛機(jī)大戰(zhàn)"); //創(chuàng)建窗體
jf.setSize(670,800);
jf.setLocationRelativeTo(null);
jf.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
jf.setLayout(new BorderLayout()); //布局
//創(chuàng)建三個(gè)JPanel,左上為按鈕,左下為分?jǐn)?shù)顯示 右為游戲頁面
JPanel left = new JPanel();
JPanel leftUp = new JPanel(); //左上
JPanel leftDown = new JPanel(); //左下
game = new JPanel(); //游戲顯示區(qū)
left.setPreferredSize(new Dimension(170,800));
left.setBackground(new Color(-3355444));
jf.add(left,BorderLayout.WEST);
jf.add(game,BorderLayout.CENTER);
game.requestFocus();
left.setLayout(new BorderLayout());
leftUp.setPreferredSize(new Dimension(0,250));
leftUp.setBackground(new Color(-3355444));
left.add(leftUp,BorderLayout.NORTH);
leftDown.setBackground(new Color(-6710887));
leftDown.setPreferredSize(new Dimension(0,550));
left.add(leftDown,BorderLayout.SOUTH);
繪制背景地圖
飛行道具類
- UML圖
- 判斷FlyObject對(duì)象是否碰撞
public boolean judge_crash(FlyObject fo){
if(x+sizeX<fo.x || y+sizeY<fo.y || x > fo.x + fo.sizeX || y > fo.y+ fo.sizeY ){
return false;
}else{
return true;
}
}
繪制線程: 如何讓我們的游戲動(dòng)起來
- 視頻原理:我們?cè)谄聊簧峡匆姷膭?dòng)態(tài)圖像圖像實(shí)際上由若干個(gè)靜止圖像構(gòu)成区丑,由于人眼有暫留特性拧粪,剛顯示的圖像在大腦中停留一段時(shí)間,若靜態(tài)圖像每
秒鐘變化25幅刊苍,那么人的感覺屏幕上的圖像是動(dòng)的既们。 - 繪制時(shí)要把所有的飛行物都繪制一遍濒析,所以我們需要在每一個(gè)飛行物被創(chuàng)建時(shí)正什,添加到相關(guān)的飛行物集合中。(為了方便傳值号杏,我們將集合設(shè)為靜態(tài)變量)
- 我們的繪制線程婴氮,選擇每30ms繪制一次,注意先畫背景盾致,然后再遍歷飛行物集合畫飛行物主经。
背景的繪制
要想繪制動(dòng)態(tài)的背景,首先我們要先畫一張靜態(tài)的背景圖庭惜,那么如何繪制一張靜態(tài)的背景圖呢罩驻?
獲取包中的圖片:
String fileName_0 = "src\\plane2\\z_img\\img_bg_0.jpg"; //相對(duì)地址(和絕對(duì)地址區(qū)分開)
BufferedImage bufferedImage;
bufferedImage = ImageIO.read(new File(fileName_0)); //將文件讀出記錄在bufferedImage中,記得拋出異常
g.drawImage(bufferedImage,0,0,null); // 將bufferedImage中的內(nèi)容畫在畫筆g對(duì)應(yīng)的地方
我們的地圖是一張可以從上往下無縫滾動(dòng)的圖片护赊,就像是這樣的地圖
[圖片上傳失敗...(image-c0606c-1653285940927)]
接下來惠遏,如何畫出連續(xù)的圖片呢?
在繪制函數(shù)中骏啰,有一個(gè)函數(shù)可以完美實(shí)現(xiàn)我們的需求
img – the specified image to be drawn. This method does nothing if img is null.
dx1 – the x coordinate of the first corner of the destination rectangle.
dy1 – the y coordinate of the first corner of the destination rectangle.
dx2 – the x coordinate of the second corner of the destination rectangle.
dy2 – the y coordinate of the second corner of the destination rectangle.
sx1 – the x coordinate of the first corner of the source rectangle.
sy1 – the y coordinate of the first corner of the source rectangle.
sx2 – the x coordinate of the second corner of the source rectangle.
sy2 – the y coordinate of the second corner of the source rectangle.
observer – object to be notified as more of the image is scaled and converted.
public abstract boolean drawImage(Image img,
int dx1, int dy1, int dx2, int dy2,
int sx1, int sy1, int sx2, int sy2,
ImageObserver observer);
比如說节吮,我們的圖片高度為712個(gè)像素點(diǎn),我們?cè)谙乱粫r(shí)刻判耕,圖片向下移動(dòng)了m個(gè)像素點(diǎn)透绩,那么我們就將這張圖片的0 ~ 712-m 部分,繪制到游戲界面的m ~ 712部分,
再將712-m ~ 712 部分繪制到游戲界面的0 ~ m 部分壁熄;
接下來帚豪,我們就要確定 m 的值,這個(gè)就很簡(jiǎn)單了草丧,在繪制線程中狸臣,定義一個(gè)整數(shù)變量m ,每次繪制完 m++ 就可以了。(個(gè)人建議m+=2比較舒服)
/**
* @author liTianLu
* @Date 2022/5/21 23:33
* @purpose 繪制背景
* 提醒: 這里我寫了四種地圖的繪制方仿,后面在選擇地圖時(shí)會(huì)用到固棚。
*/
public class BackGround {
Graphics g;
BufferedImage bufferedImage_1;
BufferedImage bufferedImage_2;
BufferedImage bufferedImage_3;
BufferedImage bufferedImage_4;
int w;
int h;
String fileName_1 = "src\\plane2\\z_img\\img_bg_1.jpg"; //地圖1
String fileName_2 = "src\\plane2\\z_img\\img_bg_2.jpg"; //地圖2
String fileName_3 = "src\\plane2\\z_img\\img_bg_3.jpg"; //地圖3
String fileName_4 = "src\\plane2\\z_img\\img_bg_4.jpg"; //地圖4
public BackGround(Graphics g) throws IOException {
this.g = g;
bufferedImage_1 = ImageIO.read(new File(fileName_1));
bufferedImage_2 = ImageIO.read(new File(fileName_2));
bufferedImage_3 = ImageIO.read(new File(fileName_3));
bufferedImage_4 = ImageIO.read(new File(fileName_4));
w = bufferedImage_1.getWidth();
h = bufferedImage_1.getHeight();
}
/**
* i : 向下移動(dòng)了i個(gè)像素
* num : 用來控制繪制哪一個(gè)地圖
*/
public void draw(int i , int num){
switch(num){
case 1 :
g.drawImage(bufferedImage_1,0,i,w,i+h,0,0,w,h,null);
g.drawImage(bufferedImage_1,0,0,w,i,0,h-i,w,h,null);
break;
case 2 :
g.drawImage(bufferedImage_2,0,i,w,i+h,0,0,w,h,null);
g.drawImage(bufferedImage_2,0,0,w,i,0,h-i,w,h,null);
break;
case 3 :
g.drawImage(bufferedImage_3,0,i,w,i+h,0,0,w,h,null);
g.drawImage(bufferedImage_3,0,0,w,i,0,h-i,w,h,null);
break;
case 4 :
g.drawImage(bufferedImage_4,0,i,w,i+h,0,0,w,h,null);
g.drawImage(bufferedImage_4,0,0,w,i,0,h-i,w,h,null);
break;
}
}
public int getH() {
return h;
}
}
- 繪制線程:
backGround.draw(m, player.mapNum);
m = m+2;
if(m>= backGround.getH()){
m = 0;
}
我的飛機(jī)的繪制
使用的飛機(jī)素材圖片:
飛機(jī)扇動(dòng)翅膀的原理與視頻的原理相同统翩,不停更換圖片,形成視覺暫留效果
//這里僅使用了三張圖片來回切換此洲,更多的圖片會(huì)有更好的效果
public void draw(int i){ //此處的i是用來控制顯示哪一張圖片的
int j = i%30; // 150ms換一張
if (j<10){
g.drawImage(plane_img,x,y,x+sizeX,y+sizeY,0,0,sizeX,sizeY,null);
}else if(j<20) {
g.drawImage(plane_img,x,y,x+sizeX,y+sizeY,0,sizeY,sizeX,2*sizeY,null);
}else if(j<30){
g.drawImage(plane_img,x,y,x+sizeX,y+sizeY,288,0,424,112,null);
}
}
敵方飛機(jī)厂汗,敵方子彈等飛行物的繪制原理與MyPlane相同,后面不在贅述呜师。(為了簡(jiǎn)化開發(fā)流程娶桦,飛行物可以不”扇動(dòng)翅膀“)
移動(dòng)線程
- 我們已經(jīng)給每個(gè)飛行對(duì)象設(shè)置了X軸移動(dòng)速度和Y軸移動(dòng)速度,所以每次移動(dòng)的時(shí)候汁汗,我們只需要遍歷所有的飛行對(duì)象衷畦,
然后逐個(gè)移動(dòng)一個(gè)speedX 和 speedY 單位即可。 - 多久移動(dòng)一次呢知牌?和繪制線程的間隔時(shí)間相同就好了祈争,我們都設(shè)為30ms.
- 當(dāng)飛行物飛出屏幕時(shí),將飛行物移出集合角寸,減少計(jì)算機(jī)資源的消耗菩混。
如何控制我的飛機(jī)移動(dòng)?
- 當(dāng)然是通過鍵盤的 ↑ ↓ ← → 來控制了扁藕,我們需要設(shè)置一個(gè)鍵盤監(jiān)聽器給game界面沮峡,
- 注意要先使用 game.requestFocus(); 獲取焦點(diǎn),鍵盤監(jiān)聽器才可以使用亿柑。
@Override
//鍵盤按壓時(shí)邢疙,設(shè)置速度
public void keyPressed(KeyEvent e) {
int c = e.getKeyCode();
if(DrawThread.myPlane!=null){
switch (c){
case 37:
DrawThread.myPlane.setSpeedX(-speed);
break;
case 38:
DrawThread.myPlane.setSpeedY(-speed);
break;
case 39:
DrawThread.myPlane.setSpeedX(speed);
break;
case 40:
DrawThread.myPlane.setSpeedY(speed);
break;
}
}
}
@Override
//鍵盤釋放時(shí),速度設(shè)為0
public void keyReleased(KeyEvent e) {
int c = e.getKeyCode();
switch (c){
case 37:
case 39:
DrawThread.myPlane.setSpeedX(0);
break;
case 38:
case 40:
DrawThread.myPlane.setSpeedY(0);
break;
}
}
敵方飛機(jī)線程 : 如何生成敵方飛機(jī)呢望薄?
每隔一段時(shí)間疟游,在游戲面板的頂部,產(chǎn)生一個(gè)敵方飛機(jī)
/**
* @author liTianLu
* @Date 2022/5/22 0:30
* @purpose 產(chǎn)生敵機(jī)的線程
*/
@Override
public void run() {
int sleepTime = 800;
while (true){
if(DrawThread.player.score>=500){ //當(dāng)分?jǐn)?shù)高于500時(shí)式矫,加快敵機(jī)產(chǎn)生的頻率
sleepTime = 300;
}
EnemyPlane enemyPlane = null;
try {
enemyPlane = new EnemyPlane();
} catch (IOException e) {
e.printStackTrace();
}
enemyPlanes.add(enemyPlane);
new Thread(new EnemyBulletThread(enemyPlane)).start(); //啟動(dòng)一個(gè)發(fā)射子彈線程
try {
sleep(sleepTime+ random.nextInt(300));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
敵方子彈線程 : 使每一個(gè)敵方飛機(jī)開火
我們?yōu)槊恳粋€(gè)敵方飛機(jī)創(chuàng)建一個(gè)生成子彈的線程乡摹,要確定子彈產(chǎn)生的具體位置,就要知道敵方飛機(jī)的位置采转,所以我們要傳入一個(gè)敵方飛機(jī)對(duì)象給該線程聪廉。
public EnemyBulletThread(EnemyPlane enemyPlane){
this.enemyPlane = enemyPlane;
}
@Override
public void run() {
try {
sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
while(enemyPlane.isAlive() ){
EnemyBullet enemyBullet = null;
int enemyBullet_x = enemyPlane.getX()+25;
int enemyBullet_y = enemyPlane.getY()+66;
try {
enemyBullet = new EnemyBullet(enemyBullet_x,enemyBullet_y);
} catch (IOException e) {
e.printStackTrace();
}
enemyBullets.add(enemyBullet);
try {
sleep(2000+ random.nextInt(2000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
檢測(cè)碰撞線程 : 在子彈與敵機(jī)碰撞時(shí),移除敵機(jī)
- 此時(shí)我們會(huì)遇到一個(gè)問題故慈,就是在遍歷時(shí)板熊,move移動(dòng)線程有可能將其中的一個(gè)飛行物移出集合,會(huì)出現(xiàn)IndexOutOfBoundsException異常
- 察绷,我們只需要在兩個(gè)線程使用飛行物集合時(shí)干签,加上synchronized關(guān)鍵字,即可解決拆撼。
- MoveThread 遍歷我的子彈集合
synchronized (MyPlane.myBulletList){
if(MyPlane.myBulletList.size()!=0){
for (int i = 0; i < MyPlane.myBulletList.size(); i++) {
MyPlane.myBulletList.get(i).setY(MyPlane.myBulletList.get(i).getY()+MyPlane.myBulletList.get(i).getSpeedY() );
if(MyPlane.myBulletList.get(i).getY() <= -100){
MyPlane.myBulletList.remove(i);
continue;
}
}
}
}
- TestCrashThread 檢測(cè)我的子彈與敵方飛機(jī)碰撞
synchronized (MyPlane.myBulletList){
for (int i = 0; i < MyPlane.myBulletList.size(); i++) {
for (int j = 0; j < EnemyPlaneThread.enemyPlanes.size() ;j++) {
if(MyPlane.myBulletList.get(i).judge_crash(EnemyPlaneThread.enemyPlanes.get(j)) ){
EnemyPlaneThread.enemyPlanes.get(j).setAlive(false); //關(guān)線程
DrawThread.player.score+=5; //分?jǐn)?shù)+5
EnemyPlaneThread.enemyPlanes.remove(j);
MyPlane.myBulletList.remove(i);
j = -1;
}
if(i >= MyPlane.myBulletList.size()){
break;
}
}
}
}
其他功能:顯示玩家hp容劳,掉落道具喘沿,得分,升級(jí)竭贩,更換地圖
- 顯示hp:每次檢測(cè)到我的飛機(jī)與敵方飛機(jī)蚜印,敵方子彈碰撞,就減分留量。減到<=0時(shí)窄赋,游戲結(jié)束。
- 得分:子彈打到敵方飛機(jī)時(shí)楼熄,加分忆绰,并將當(dāng)前分?jǐn)?shù)通過繪制線程繪制在屏幕上。
- 掉落道具:敵機(jī)消失的時(shí)候可岂,隨機(jī)掉落一個(gè)道具错敢,我的飛機(jī)碰到道具時(shí),回血/加分/升級(jí)
- 升級(jí):我的飛機(jī)初始為1級(jí)青柄,最高為3級(jí)伐债,等級(jí)改變時(shí)预侯,使用switch 根據(jù)等級(jí)改變我的飛機(jī)的子彈發(fā)射方式致开。
- 更換地圖: 使用一個(gè)新的窗體,設(shè)置幾個(gè)單選按鈕萎馅,選擇時(shí)通過監(jiān)聽器双戳,改變地圖的控制變量,從而改變地圖的繪制糜芳。
</article>