JAVA基礎小項目 - 坦克大戰
前言:
這個項目是之前備份電腦資料的時候看到的,不禁一陣感慨自己當初自學程式設計的心酸和淚水。是以分享一下自己當初寫的的垃圾代碼。雖然我不是任天堂忠實粉絲,但是對于90後來說坦克大戰基本是人人都玩過的一款小霸王遊戲機的遊戲。
這個項目對于已經入行的人來說沒有價值,分享出來主要是希望對于初學程式設計的人給一點 “吸引”吧,原來代碼可以做到這麼愉快的事情。這個坦克大戰也是自己跟着教育訓練機構的教學視訊邊看邊敲的。
花了一天左右的時間把代碼稍微整理了一下代碼同時寫了這份文檔,這裡分享出來。
對于入行程式設計的同學個人的建議如果要快速成長還是多練,多做。很多東西做多了之後,涉及自己的盲區會促使你不斷的學習和進步。
PS:代碼使用Eclipse寫的,這裡用IDEA整理了一下代碼
此代碼如果閱讀存在難度,建議看一下韓順平的JAVA基礎坦克大戰的課程。這裡的源代碼也是跟着課程敲的
真是一個好時代,當初這些資源還要網上翻半天
項目位址
後續文檔更新請看readme
項目簡介:
個人前幾年自學的時候從一個教學視訊的,韓順平老師的JAVA初級坦克大戰。個人跟着視訊邊學邊敲之後,對于程式設計的興趣大大的提升。後面越學越快樂。
所用技術
- JAVA GUI(遠古技術,千萬别深入,看看即可)
- JAVA
面向群體:
- 初學JAVA者,可以看看這個項目鍛煉動手能力
- 完全不了解什麼是面向對象
- 對于小遊戲有點興趣的
- 如果你厭惡枯燥的學習,做一個小遊戲或許能給你一點動力
項目截圖:
操作坦克的方法:
最後一版本有效,早起版本部分功能或者所有功能按鍵無效
- WASD
- J:為射出子彈
需求文檔(或許是):
由于當初是跟着視訊做的,雖然具體的記憶忘了,但是自己跟着敲的同時忘了做需求的更新,是以這裡有部分需求和思路斷了。如果有不同的,後續補充GIT的README文檔。
/*
* 需求:
* 坦克大戰:
* 功能:
* 1.畫出坦克,
*
* 思路:
* 1.首先坦克想象由五個部件組成兩個矩形,一個長方形或者正方形,一個圓
* 一條直線
*
* 2.畫坦克的時候需要使用到畫筆工具
* 必須在構造函數初始化使用畫筆工具
*
* 3.在設定方向以及畫出不同方向的坦克
*
* 4.敵方坦克畫出來需要使用父類方法
* 敵方坦克的坐标需要設定,
* 使用一個集合儲存敵方坦克Vector集合便于删除和添加
*
* 5.發射子彈是一個線程
* 具有線程的功能
* 另外線程對與子彈方向運動軌迹不同
*
* 6.需要把子彈畫出來
* 在按下J鍵的時候發射子彈
* 實作連發使用集合存儲
*
* 更新:
* 1.讓敵人能夠發射子彈
解決方法
1.敵人發射子彈是一個多線程方法,應當在敵人的run函數當中實作
2.坦克發射子彈和移動都是坦克本身具有的功能
*
* 思路:
* 1.在敵人類裡面需要添加一個射擊方法
* 與我方一樣,但是敵人是自動射擊或者說每過幾秒射擊一次
*
* 2.我方坦克子彈連發
* 使用一個集合儲存建立的對象,畫出子彈使用集合中的對象
* 我方坦克子彈連發過快,需要限定
*
* 3.
* 我方坦克擊中敵人坦克之後,敵人坦克就要消失
* 需要擷取到敵人的一個定點坐标,然後界定一個範圍
* 寫一個專門的函數判斷是否擊中敵人
*
* 在哪裡判斷是否擊中敵人
* · 因為每一顆子彈都要與所有的坦克比對,并且每一次比對都要
* 雙重判斷每次都要進行建立對象
* 圖檔問題沒有得到解決
*
* 更新
* 1.需要實作敵人的坦克不斷的移動使用多線程的手段實作
*
* 2.需要實作敵人能夠發射子彈的功能
* 實作方法:
* 建立一個敵人的子彈集合
* 如何敵人何時發射子彈?
* 使用多重循環判斷是否需要添加薪子彈
*
* 3.實作自己被子彈擊中也會消失
* 對于摧毀坦克進行更新
*
* 4.
* 較難!
* 實作坦克不覆寫運動,
* 1.首先改判斷在坦克類中實作
* 2.需要用到一個方法擷取到生成的坦克類
* 3.對于地方其中一輛坦克的選擇,都要循環與其他所有坦克進行比對
* 并且要事先判斷是否為我方坦克
* 4.**對于點位的判斷要判斷兩個點,才能夠保證不會産生碰撞
*
* 5.實作選擇關卡的功能
* 思路:
* 1.可以建立一個選擇關卡的面闆
* 2.暫時先實作不同的關卡敵人坦克的數量不同
* 3.實作閃爍功能,使用多線程的方法,注意線程的關閉
* 4.對于選項添加事件屬性,添加事件
*
* 5.畫出我方坦克擊中了多少輛地方坦克
* 1.對于總體界面進行修改
* 2.顯示敵人坦克的數量
* 擴充:
* 1.建立幫助文檔
* 3.擴充:我方坦克的生命值,當生命值為0的時候遊戲結束
* 4.記錄我方擊中了多少地方坦克
* 使用檔案操作完成
*
* 6.實作重新開始的功能
*
* 7.實作存盤退出的功能
* 思路:
* 選在主界面增加兩個按鈕
* 1.記錄所有坦克的坐标
*
* 8.實作暫停的功能
* 思路:
* 暫停功能可以通過一個布爾值進行判斷,當按下某個按鈕的時候就要進行布爾值的改變
* 需要暫停的對象
* 将多線程子彈的速度前進功能暫停
* 敵人坦克無法轉向和前進
* 我方坦克無法轉向和前進
*
* 9.實作播放音樂的功能
* 自學 - 未實作
*
*
*
*
* */
版本疊代和介紹:
介紹:
代碼比較多,我會抽幾處了解起來比較難以了解的地方說明一下,其他的代碼需要看細節。如果有不懂的歡迎在issue提出,個人隻要有空一定給予答複。
第一個版本
版本概述:畫出坦克(version1)
我們的第一步是畫出我們的坦克,畫出我方坦克的方法還是非常簡單的。
* 思路:
* 1.首先坦克想象由五個部件組成兩個矩形,一個長方形或者正方形,一個圓
* 一條直線
*
* 2.畫坦克的時候需要使用到畫筆工具
* 必須在構造函數初始化使用畫筆工具
*
* 3.在設定方向以及畫出不同方向的坦克
擁有坦克的第一步是畫出坦克
畫出坦克的核心代碼如下:
/**
* 畫坦克需要提取封裝
* 1.畫出來之前先确定顔色,是敵人坦克還是我方坦克
* 2.參數為坐标做,畫筆(重要),以及坦克類型和方向
*/
private void paintMyTank(int x, int y, Graphics g, int direct, String type) {
//畫之前先确定坦克的顔色
switch (type) {
case "mytank": {
g.setColor(Color.red);
break;
}
case "enemytank": {
g.setColor(Color.cyan);
break;
}
}
//向上
if (direct == 0) {//先畫出我的坦克
//畫出左邊的矩形,先設定顔色
g.fill3DRect(x, x, 5, 30, false);
//畫出中間的長方形
g.fill3DRect(x + 5, x + 5, 10, 20, false);
//畫出中間圓圈,使用填充橢圓
g.fillOval(x + 6, x + 9, 7, 7);
//畫出一條直線
g.drawLine(x + 10, x, x + 10, x + 15);
//畫出另一邊矩形
g.fill3DRect(x + 15, x, 5, 30, false);
}
}
如果知道數字的意思,直接将數字修改大小就可以知道效果了
第二個版本
版本概述:畫出我方坦克不同形狀,敵方坦克(version2),我方坦克可以進行行動
在上個版本當中,我們發現我們的坦克隻有一個朝向,在這個版本中,增加了坦克的不同朝向。同時增加了敵人的坦克類。
由于敵人有很多個,是以用了一個集合來維護和設定。同時加入了坐标系統,可以實作不同的坦克挪到不同的位置。
這個版本的關鍵代碼,不是在畫坦克的上面,而是在于加入了鍵盤的監聽事件:
// version2.DrawTank.java 更多細節請檢視
public class DrawTank extends JPanel implements KeyListener {
// 省略一大坨代碼
/**
* 使用wsad進行控制
* 也可以改為上下左右鍵
*
* @param e
*/
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_W) {
this.mytank.setDirect(0);
this.mytank.move_up();
} else if (e.getKeyCode() == KeyEvent.VK_D) {
this.mytank.setDirect(1);
this.mytank.move_right();
} else if (e.getKeyCode() == KeyEvent.VK_S) {
this.mytank.setDirect(2);
this.mytank.move_down();
} else if (e.getKeyCode() == KeyEvent.VK_A) {
//改變方向
this.mytank.setDirect(3);
this.mytank.move_left();
}
}
}
實作KeyListener接口并且監聽對應的方法。
JAVA的GUI有一個事件監聽驅動模型,意思就是說我們實作對應的驅動接口,并且覆寫對應的方法,在代碼運作并且觸發相關事件的适合,模型就可以觸發我們實作定義好的代碼,這裡很明顯就是設計模式,有興趣可以去了解一下
第三個版本
從這個版本就開始變得稍微複雜一點了,用了多線程的内容,因為要讓我們的坦克和敵人的坦克“動”起來,其實讓坦克移動和我方坦克移動的道理都是一樣的:高速的擦寫和描繪。和我們的滑鼠以及計算機顯示畫面的本質都是一樣的。
這個版本中,比較核心的内容是如何發射子彈和讓子彈消失:
public class Bullet implements Runnable {
/**
* 定義子彈的xy坐标
*/
private int x, y;
/**
* 子彈的顔色
*/
int color;
/**
* 子彈的方向
*/
int direct;
/**
* 子彈移動速度
*/
int screed;
/**
* 判斷是否越界
*/
boolean isOut = false;
/**
* 越界範圍
*/
int outx = 400;
/**
* 越界範圍
*/
int outy = 300;
public Bullet(int x, int y, int direct) {
this.x = x;
this.y = y;
this.direct = direct;
this.screed = 1;
}
// 省略get/set
@Override
public void run() {
//坦克一旦建立就要運動
//因為移動的太快,需要減慢速度
while (true) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
//
e.printStackTrace();
}
switch (this.direct) {
case 0:
y -= screed;
break;
case 1:
x += screed;
break;
case 2:
y += screed;
break;
case 3:
x -= screed;
break;
}
System.out.println(x + "..." + y);
//碰到邊緣消失
if (x < 0 || x > outx || y < 0 || y > outy) {
isOut = true;
break;
}
}
// 子彈什麼時候消亡?
}
/**
* 判斷是否越界
*/
public void outLine() {
}
}
- 在坦克的内部維護一個變量isOut,判定有沒有越界
- 如果出現了越界,則flag進行設定
接着,在繪畫的方法裡面,判定有沒有越界:
/**
* 繪畫方法
* @param g
*/
@Override
public void paint(Graphics g) {
super.paint(g);
//畫出背景色
g.fill3DRect(0, 0, 600, 400, false);
//畫出自己的坦克
paintMyTank(mytank.getX(), mytank.getY(), g, mytank.getDirect(), mytank.getColor());
//畫出敵人的坦克
paintEnemyTank(g);
//畫出子彈并且确定沒有越界
if (mytank.but != null && !mytank.but.isOut) {
g.fill3DRect(mytank.but.getX(), mytank.but.getY(), 5, 5, false);
}
}
第四個版本:
從這一個版本開始,一個遊戲的簡單雛形已經有了,這一個版本實作了讓敵人移動的同時發射子彈的功能,同時我方的坦克射擊敵人的時候,可以讓敵人消失
怎麼樣讓敵人可以邊移動邊發射子彈:
我們需要在敵人的多線程run代碼裡面,然敵人進行間歇性的走動:
@Override
//我們發現坦克在原地抽搐,我們要實作坦克的平穩運作
//實作坦克運動不會越界
public void run() {
do {
switch (this.direct) {
case 0:
for (int i = 0; i < 30; i++) {
if (y > 0)
y -= sreed;
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
break;
case 1:
for (int i = 0; i < 30; i++) {
if (x < 500)
x += sreed;
try {
// 短暫的停頓
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
break;
case 2:
for (int i = 0; i < 30; i++) {
if (y < 400)
y += sreed;
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
break;
case 3:
for (int i = 0; i < 30; i++) {
if (x > 0)
x -= sreed;
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
break;
}
//不同的方向移動的方向不同
this.direct = (int) (Math.random() * 4);
} while (this.isLive);
}
至于生成子彈,需要定時去輪詢所有的坦克,檢查坦克中組合的子彈集合是否存在子彈,如果小于一定的數量,需要生成對應的子彈對象同時加入到敵人的坦克當中。由于子彈建立就會開始執行線程進行
@Override
public void run() {
//限定一段時間重新繪制
while (true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//判斷是否擊中
for (int x = 0; x < mytank.vecs.size(); x++) {
//每一顆子彈和每一個坦克比對
//取出一顆子彈之前判斷是否有子彈
buts = mytank.vecs.get(x);
//判斷子彈是否有效
if (buts.isOut()) {
continue;
}
//取出每一個坦克與它判斷
for (int y = 0; y < vec.size(); y++) {
//判斷敵方坦克是否死亡
if (vec.get(y).isLive) {
en = vec.get(y);
//記性判斷是否擊中操作
hitTank(en, buts);
}
}
}
//如果子彈數小于一定數目
for (int x = 0; x < vec.size(); x++) {
EnemyTank et = vec.get(x);
//周遊每一輛坦克的子彈集合
if (!et.isLive()) {
continue;
}
if (et.vecs.size() < 1) {
//對于不同的坦克方向生成子彈的方向也不同
Bullet enybut = null;
switch (et.getDirect()) {
case 0:
enybut = new Bullet(et.getX() + 10, et.getY(), 0);
//将建立的子彈加入到集合當中
et.vecs.addElement(enybut);
break;
case 1:
enybut = new Bullet(et.getX() + 30, et.getY() + 10, 1);
et.vecs.addElement(enybut);
break;
case 2:
enybut = new Bullet(et.getX() + 10, et.getY() + 30, 2);
et.vecs.addElement(enybut);
break;
case 3:
enybut = new Bullet(et.getX(), et.getY() + 10, 3);
et.vecs.addElement(enybut);
break;
}
new Thread(enybut).start();
}
}
//重繪
this.repaint();
}
}
在子彈類當中進行不斷的數值改變:
下面的内容表示子彈的類
public class Bullet implements Runnable {
//隐藏一大段代碼:
public void run() {
while (true) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
switch (this.direct) {
case 0:
this.y -= screed;
break;
case 1:
this.x += screed;
break;
case 2:
this.y += screed;
break;
case 3:
this.x -= screed;
break;
}
//碰到邊緣消失
if (x < 0 || x > outx || y < 0 || y > outy) {
isOut = true;
break;
}
}
}
}
第五個版本:
在第五個版本當中,我們實作了開始菜單的界面,同時視線菜單的不斷顯示:
界面會不斷的閃爍
接着,敵人增加了子彈可以摧毀我們的方法
接着,我們可以實作爆炸的效果:
由于爆炸的效果不好截圖,請看源代碼
/**
* 實作閃爍功能
* 重構坦克 - 第五版
* @author zxd
* @version 1.0
* @date 2021/1/29 23:54
*/
class SelectIsSallup extends JPanel implements Runnable {
/**
* 時間屬性
*/
int times = 0;
public void paint(Graphics g) {
super.paint(g);
g.fillRect(0, 0, 600, 400);
if (times % 2 == 0) {
//畫出文字
Font font1 = new Font("華文新魏", Font.BOLD, 20);
//設定字型的顔色
g.setColor(Color.yellow);
g.setFont(font1);
g.drawString("stage 1", 200, 150);
}
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(750);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (times > 500)
times = 0;
times++;
this.repaint();
}
}
}
如何讓敵人的子彈對我們造成傷害:
/**
* 建立一個方法,判斷是否産生碰撞
* 是否攻擊了其他的坦克
* @return
*/
private boolean isTouchOther() {
// 根據自己的方向進行選擇判斷
switch (this.direct) {
// 坦克向上走的時候
case 0:
// 取出所有的坦克對象
for (int x = 0; x < enevec.size(); x++) {
EnemyTank et = enevec.get(x);
//如果不是自己的坦克
if (et != this) {
//如果敵人的坦克朝上或者朝下的時候
if (et.direct == 0 || et.direct == 2) {
//判斷邊界
//對于第一個點進行判斷
if (this.x >= et.x && this.x <= et.x + 20
&& this.y >= et.y && this.y <= et.y + 30) {
return true;
}
//對于第二個點進行判斷
if (this.x + 20 >= et.x && this.x + 20 <= et.x + 20
&& this.y >= et.y && this.y <= et.y + 30) {
return true;
}
}
//如果敵人是朝左邊或者右邊的時候
if (et.direct == 1 || et.direct == 3) {
//判斷邊界
//對于第一個點進行判斷
if (this.x >= et.x && this.x <= et.x + 30
&& this.y >= et.y && this.y <= et.y + 20) {
return true;
}
//對于第二個點進行判斷
if (this.x + 20 >= et.x && this.x + 20 <= et.x + 30
&& this.y >= et.y && this.y <= et.y + 20) {
return true;
}
}
}
}
break;
// 坦克想右邊走的時候
case 1:
// 取出所有的坦克對象
for (int x = 0; x < enevec.size(); x++) {
EnemyTank et = enevec.get(x);
//如果不是自己的坦克
if (et != this) {
//如果敵人的坦克朝上或者朝下的時候
if (et.direct == 0 || et.direct == 2) {
//判斷邊界
//對于第一個點進行判斷
if (this.x + 30 >= et.x && this.x + 30 <= et.x + 20
&& this.y >= et.y && this.y <= et.y + 30) {
return true;
}
//對于第二個點進行判斷
if (this.x + 30 >= et.x && this.x + 30 <= et.x + 20
&& this.y >= et.y && this.y <= et.y + 30) {
return true;
}
}
//如果敵人是朝左邊或者右邊的時候
if (et.direct == 1 || et.direct == 3) {
//判斷邊界
//對于第一個點進行判斷
if (this.x + 30 >= et.x && this.x + 30 <= et.x + 30
&& this.y + 20 >= et.y && this.y <= et.y + 20) {
return true;
}
//對于第二個點進行判斷
if (this.x + 30 >= et.x && this.x + 30 <= et.x + 30
&& this.y + 20 >= et.y && this.y <= et.y + 20) {
return true;
}
}
}
}
// 坦克想下的時候
case 2:
// 取出所有的坦克對象
for (int x = 0; x < enevec.size(); x++) {
EnemyTank et = enevec.get(x);
//如果不是自己的坦克
if (et != this) {
//如果敵人的坦克朝上或者朝下的時候
if (et.direct == 0 || et.direct == 2) {
//判斷邊界
//對于第一個點進行判斷
if (this.x >= et.x && this.x <= et.x + 20
&& this.y + 30 >= et.y && this.y + 30 <= et.y + 30) {
return true;
}
//對于第二個點進行判斷
if (this.x + 20 >= et.x && this.x + 20 <= et.x + 20
&& this.y + 30 >= et.y && this.y + 30 <= et.y + 30) {
return true;
}
}
//如果敵人是朝左邊或者右邊的時候
if (et.direct == 1 || et.direct == 3) {
//判斷邊界
//對于第一個點進行判斷
if (this.x >= et.x && this.x <= et.x + 30
&& this.y + 30 >= et.y && this.y + 30 <= et.y + 20) {
return true;
}
//對于第二個點進行判斷
if (this.x + 20 >= et.x && this.x + 20 <= et.x + 30
&& this.y + 30 >= et.y && this.y + 30 <= et.y + 20) {
return true;
}
}
}
}
break;
// 坦克向左移動的時候
case 3:
// 取出所有的坦克對象
for (int x = 0; x < enevec.size(); x++) {
EnemyTank et = enevec.get(x);
//如果不是自己的坦克
if (et != this) {
//如果敵人的坦克朝上或者朝下的時候
if (et.direct == 0 || et.direct == 2) {
//判斷邊界
//對于第一個點進行判斷
if (this.x >= et.x && this.x <= et.x + 20
&& this.y >= et.y && this.y <= et.y + 30) {
return true;
}
//對于第二個點進行判斷
if (this.x >= et.x && this.x <= et.x + 20
&& this.y + 20 >= et.y && this.y + 20 <= et.y + 30) {
return true;
}
}
//如果敵人是朝左邊或者右邊的時候
if (et.direct == 1 || et.direct == 3) {
//判斷邊界
//對于第一個點進行判斷
if (this.x >= et.x && this.x <= et.x + 30
&& this.y >= et.y && this.y <= et.y + 20) {
return true;
}
//對于第二個點進行判斷
if (this.x >= et.x && this.x <= et.x + 30
&& this.y + 20 >= et.y && this.y + 20 <= et.y + 20) {
return true;
}
}
}
}
}
return false;
}
最終版本:
在最終的版本當中,一個坦克大戰的基本遊戲算是完成了,當然還有很多需要完成點。
//暫停功能
if(e.getKeyCode()==KeyEvent.VK_P)
{
if(this.clickcount%2 == 0)
mytank.setSuspend(false);
else
mytank.setSuspend(true);
//利用循環将坦克類中的子彈速度變成0
for(int x=0; x<vec.size(); x++)
{
en = vec.get(x);
//敵方坦克移動速度歸于0
//坦克不允許移動
if(this.clickcount%2 == 0)
en.setSuspend(false);
else
en.setSuspend(true);
for(int y=0; y<en.vecs.size(); y++)
{
//子彈的速度變成0
if(this.clickcount%2 == 0)
en.vecs.get(y).setSuspend(false);
else
en.vecs.get(y).setSuspend(true);
}
}
this.clickcount++;
}