因為課程需要,我們要設計一款多線程的程序。老師推薦說可以寫一個坦克大戰,正好我一直有着寫一個游戲的想法,就借這個機會寫一個坦克大戰的小游戲出來。
項目源碼:https://github.com/SANEBEN/tankBattle
開發環境:
集成開發環境:idea
jdk版本:jdk1.8.0_241
圖形界面使用Javafx實現
最開始在確定圖形界面設計語言的時候考慮過C#,但是一直覺得visual studio開發不如idea舒服,后來思來想去還是決定用Java實現圖形界面。在網絡上搜了一下Java圖形界面的相關博文,發現大部分都在介紹swing,但是我又覺得swing開發太麻煩不夠直觀,就用了快涼了的javafx,雖然網上關於javafx的開發資料少得可憐,但是使用的體驗還是不錯的。下面就具體介紹一下我開發的過程。因為在最開始開發的時候走了不少的彎路,所以開始記錄的時候已經有了一個較完整的版本了。
一、項目結構

二、實際效果


目前實現的功能就是地圖的繪制,子彈發射,簡單的碰撞檢測等一些基礎功能,寫到這里多線程已經在項目中有很多的使用 了,暫時不會再往下寫了。
三、實現思路
首先是貼圖資源的加載。素材可以在這個網站下載(http://www.aigei.com/s?q=%E5%9D%A6%E5%85%8B%E5%A4%A7%E6%88%98&type=2d)
圖片資源可以使用一個類靜態加載,之后如果遇到需要重繪的直接使用就行了
這是一些代碼片段
1 public static final Image TANK_ENEMY_YELLOW_BOTTON = new Image("/img/tank/enemy3D.gif"); 2 public static final Image TANK_ENEMY_YELLOW_LEFT = new Image("/img/tank/enemy3L.gif"); 3 public static final Image TANK_ENEMY_YELLOW_RIGHT = new Image("/img/tank/enemy3R.gif"); 4 public static final Image TANK_ENEMY_YELLOW_TOP = new Image("/img/tank/enemy3U.gif");
在貼圖加載之后就是地圖的設置了,我將游戲中一些重要的信息都放到了一個類進行管理,地圖在代碼中使用二維數組進行存儲
感受一下:
public static int[][] map = { {2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}, {2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}, {2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2}, {2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2}, {2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2}, {2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2}, {2, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2}, {2, 0, 0, 1, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2}, {2, 0, 0, 1, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2}, {2, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}, {2, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2}, {2, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2}, {2, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 2}, {2, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 2}, {2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2}, {2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}, {2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}, {2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 1, 1, 1, 3, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2}, {2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 1, 4, 1, 3, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2}, {2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2} };
下面就到了地圖繪制的環節了,在這個地方我還踩了一個坑,因為涉及到貼圖,所以我第一時間想到的是使用imageView組件,在只貼地圖素材時確實沒啥問題,問題出在后面繪制坦克和子彈上面,具體的之后再說了。反正最后我實現使用的canvas,搞笑的是我搜索canvas相關的api出來的都是html5的信息,不過好在接口的名稱和參數都是一樣的,磕磕絆絆也把功能實現了。在繪圖的時候我使用了三層的canvas,具體原因也和之前提到的bug有關系,這個后面一起說。反正地圖單獨占用了一個canvas。具體的代碼很簡單。
javafx的布局使用了xml文件進行配置,不過在這個程序的實現里面不需要過多的關注,將canvas放置到布局中之后除了按鍵的監聽所有的操作都在canvas上完成。這里代碼就不全放了。
1 public class WallCanvas extends Canvas { 2 3 public WallCanvas(double width, double height) { 4 super(width, height); 5 Game.wall_canvas_gc = getGraphicsContext2D(); 6 7 Game.InitMap(); 8 } 9 }
public static void InitMap() { int height = map.length; int width = map[0].length; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { /* 參數解釋:sx:源矩形的x坐標位置,sy:源矩形的y坐標位置,sw:源矩形的寬度,sh:源矩形的高度 dx:目標矩形的x坐標位置,dx:目標矩形的y坐標位置,dw:目標矩形的寬度,dh:目標矩形的高度 */ int mark = map[y][x]; if (mark == 2) { Wall wall = new Wall(x * UNIT_LENGTH, y * UNIT_LENGTH, Img.STEEL, false); wall.draw(); walls.add(wall); } else if (mark == 1) { Wall wall = new Wall(x * UNIT_LENGTH, y * UNIT_LENGTH, Img.WALL, true); wall.draw(); walls.add(wall); } else if (mark == 3) { //要實現草叢的效果就不把草的繪圖加到障礙物的列表中了,這樣就不會在碰撞檢測里面被檢測 Wall wall = new Wall(x * UNIT_LENGTH, y * UNIT_LENGTH, Img.GRASS, true); wall.draw(); } else if (mark == 4) { //玩家基地作為 Base base = new Base(x * UNIT_LENGTH, y * UNIT_LENGTH, Img.BASE, true); base.draw(); Game.base = base; } } } }
可以注意到我將所有繪制了的障礙物都通過鏈表存儲了下來,應該說我將所有在地圖上繪制的物體都存了下來,這個涉及到后面的碰撞檢測部分。

這里看到的封裝類設計就不詳細介紹了,大概的就是將一些公有的屬性和方法放到抽象類中,然后將一些方法的實現延遲到具體的子類中。

下面就到了坦克的繪制以及移動。
貼圖部分和地圖的繪制類似,但是為了實現移動的效果,我們需要不斷的將原有的貼圖擦除然后繪制新的坦克,所以坦克也單獨占用了一個圖層。前面說到的使用三個圖層的其中一個原因就是,在重繪的時候為了節省資源我們會將整個canvas上的貼圖全部進行擦除然后再進行下一次的繪制,但是障礙物不需要移動也就不需要重繪,這就消耗了額外的性能。
玩家坦克的移動通過監聽鍵盤上wasd鍵來控制。
class KeyPressHandler implements EventHandler<KeyEvent> { @Override public void handle(KeyEvent event) { switch (event.getCode()) { case W: playerTank.setDirection(Direction.UP); BasicMap.isMove = true; playerTank.move(); break; case S: playerTank.setDirection(Direction.DOWN); BasicMap.isMove = true; playerTank.move(); break; case A: playerTank.setDirection(Direction.LEFT); BasicMap.isMove = true; playerTank.move(); break; case D: playerTank.setDirection(Direction.RIGHT); BasicMap.isMove = true; playerTank.move(); break; case SPACE: Game.addBullet(playerTank.fire()); } } }
可以看到在監聽時間中我只是修改了坦克當前的位置以及更換於方向相符的貼圖,並沒有直接重繪。在我最開始的設計中為了方便我就直接針對玩家所在的區域而不是整張地圖進行重繪,這樣實現起來很直觀易於理解。但是當我嘗試在線程中繪制敵方坦克的時候就出現了問題,由於線程沖突,程序時常會崩掉。在排查了一遍之后我發現問題出現在canvas用於重繪的對象上面,當線程執行的頻率過高的時候,這個對象就會報空指針的問題,當降低線程的執行頻率后,bug出現的次數變少了,而如果我不去觸發玩家坦克的重繪則基本不會出bug了。所以我就將坦克的重繪進行了重新的設置,給了它一個單獨的圖層,並集中進行繪制。
這里實現集中重繪就得提到javafx中的ScheduledService類,具體的介紹參考(https://www.jianshu.com/p/97e02eccc87c)
可以看到ScheduledService可以設置每次的執行時間以及兩次之間的間隔時間,還可以暫停重啟等。同時它還是javafx提供的官方線程實現類,不會出現線程沖突的問題。
public class TankScheduleService extends ScheduledService<String> { @Override protected Task<String> createTask() { return new Task<String>() { @Override protected String call() { Game.tankCanvas.cleanCanvas(); Game.tankCanvas.draw(); return null; } }; } }
這就是實現了一個畫布清除和重繪的功能。
同理的,我還實現了 另外兩個類似的線程來分別實現地方坦克下一個位置的計算、子彈飛行軌跡的計算(ps:其實就是單純的往前飛);
最后比較重要的就是碰撞檢測了
//物體碰撞檢測的方法 public BaseObject collisionDetection() { double current_center_x = x + side_length / 2; double current_center_y = y + side_length / 2; for (BaseObject baseObject : baseObjectList) { double center_x = baseObject.x + Game.UNIT_LENGTH / 2; double center_y = baseObject.y + Game.UNIT_LENGTH / 2; if (Math.abs(current_center_x - center_x) < side_length / 2 + baseObject.side_length / 2 &&//橫向判斷 Math.abs(current_center_y - center_y) < side_length / 2 + baseObject.side_length / 2) { return baseObject; } } return null; }
這個方法我放到了所有物體的基類中實現,但是其中baseObjectList由各個子類中設置。這個判斷其實就是在判斷兩個矩形是否有重疊,所以看起來還是有些奇怪,會出現子彈還沒碰到問題就觸發被擊中事件。
被擊中時間延遲到子類中實現。
//物體被擊中的抽象方法,由子類實現。 public abstract void beHit();
感興趣的可以加Q討論:473811301
