通過本專案能夠更直觀地理解應用層和運輸層網路協議, 以及繼承封裝多型的運用. 網路部分是本文敘述的重點, 你將看到如何使用Java建立TCP和UDP連接并交換報文, 你還將看到如何自己定義一個簡單的應用層協議來讓自己應用進行網路通信
一.主要設計內容:
1、需要有圖形用戶界面,讓用戶能看到游戲給出的反饋,
2、不同隊伍的坦克,須顯示不同的外觀,以區分敵我,
3、坦克能夠開火,攻擊敵方,但不能攻擊隊友,
4、需要有不可被子彈穿透的墻體,以及墻體有可被摧毀和不可被摧毀兩種,
5、敵方坦克有生命值,并非被擊中一次就會爆炸,
6、游戲結束時會有勝利或失敗場景
7、游戲結束后可重新開始,
二. 程式原始碼基本結構

三.設計的環境、方法及措施:
- 主要設備:電腦,
- 軟體:eclipse軟體,
- 方法措施:
- 利用學到的java基礎應用程式知識實作需求分析上的功能模塊,并在設計與實作時分析遇到的問題并解決,對實作的模塊進行測驗,查找不足,盡量做到滿足用戶要求
四. 實作了哪些功能
此系統是使用Java語言實作坦克大戰游戲程式,玩家通過連接訪問進入游戲,通過操縱坦克來守衛基地,玩家還可以獲得超級武器來提升坦克的屬性,摧毀全部敵方坦克來取得勝利,本系統結構如下:
(1)面板功能:
對雙方坦克、基地、河道、草坪、普通墻與鐵墻等地圖元素,還實作了頁面按鈕功能,玩家可以點擊按鈕來實作相應的功能,
(2)坦克功能:
操作玩家坦克的方法,還設定了超級武器,玩家吃掉后會獲得特殊技能,
(3)子彈功能:
設定了子彈打中不同物體物件產生的不同效果,
五. 運行效果






六. 實作思路
-
資料存盤表示: 在JPanel繪制影像,統一規定各個方塊的大小為同一大小(如墻壁,坦克之類,子彈除外),從而方便使用二維陣列存盤地圖的各個元素,
-
關于檢測物體碰撞,這里使用了一個MyImage的父類,將坦克,墻壁
定義為繼承這個父類的一個類,
class MyImage {
int width = Game.width;
int height = Game.height;
//二維地圖的坐標
Coord coord;
//螢屏上的像素坐標
int x;
int y;
MyImage(Coord coord) {
x = coord.x * width;
y = coord.y * height;
this.coord = coord;
}
private Rectangle getRect() {
return new Rectangle(x, y, width, height);
}
//碰撞檢測
boolean isIntersects(MyImage other) {
return other.getRect().intersects(getRect());
}
}
- 影像列印則借助遍歷兩個ConcurrentHashMap分別儲存坦克和其他型別的方塊,將這些方塊使用Map而不是使用陣列是因為管理起來比較方便,而二維陣列則是為了尋路演算法而準備的,防止了頻繁使用上面的兩個Map而導致執行緒鎖的問題,
七. 遇到的問題
ava的按鍵監聽在回應按鍵長按時會有1-2秒的延遲,導致操作手感極差
解決方法:在坦克類中設定一個布爾屬性move以及整形變數key儲存鍵入的按鍵值,創建一個執行緒來回應按鍵,
實作代碼:
//按鍵回應的執行緒類
class MyTankMove implements Runnable{
public void run(){
while(flag){
GetKey(key);
while(move){//決定是否移動
try {
e.printStackTrace();
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
Thread.sleep(10);//防止無按鍵時陷入死回圈導致執行緒堵塞
} catch (InterruptedException e) {
}
}
}
按鍵監聽事件,即給key和move賦值
//按鍵監聽介面
private class KeyBoradListener extends KeyAdapter{
public void keyPressed(KeyEvent e){
super.keyPressed(e);
int key = e.getKeyCode();
if(key<65){//為了實作雙人對戰而設定的,,,
if(key!=KeyEvent.VK_SHIFT&&!MyTank.isEmpty()){
MyTank.getFirst().key=key;
MyTank.getFirst().move=true;
}
}
else{
if(key!=KeyEvent.VK_G&&!MyTank.isEmpty()){
switch (key){
case KeyEvent.VK_W:key = KeyEvent.VK_UP;break;
case KeyEvent.VK_A:key = KeyEvent.VK_LEFT;break;
case KeyEvent.VK_S:key = KeyEvent.VK_DOWN;break;
case KeyEvent.VK_D:key = KeyEvent.VK_RIGHT;break;
}
MyTank.getLast().key=key;
MyTank.getLast().move=true;
}
}
}
public void keyReleased(KeyEvent e){
super.keyReleased(e);
int key = e.getKeyCode();
if(key<65){
if(!MyTank.isEmpty()){
if(key!=KeyEvent.VK_SHIFT&&key==MyTank.getFirst().key){//避免了同時按兩個以上按鍵后會卡住
MyTank.getFirst().move=false;
}
else{
MyTank.getFirst().GetKey(key);
}
}
}
else{
switch (key){
case KeyEvent.VK_W:key = KeyEvent.VK_UP;break;
case KeyEvent.VK_A:key = KeyEvent.VK_LEFT;break;
case KeyEvent.VK_S:key = KeyEvent.VK_DOWN;break;
case KeyEvent.VK_D:key = KeyEvent.VK_RIGHT;break;
case KeyEvent.VK_G:key = KeyEvent.VK_SHIFT;break;
}
if(!MyTank.isEmpty()){
if(key!=KeyEvent.VK_SHIFT&&key==MyTank.getLast().key){
MyTank.getLast().move=false;
}
else{
MyTank.getLast().GetKey(key);
}
}
}
}
}
專案內的圖片資源如何在專案匯出后也能使用
解決方法:假設在專案的scr檔案夾中建立img檔案夾,在專案的.classpath中加一句(Eclipse),也可以通過設定專案的Modules,將影像檔案夾設定為resources(IDEA),并使用當前類的名.class.getResource("/(檔案名)")).getImage()獲得影像物件,
尋路的實作
實作思路:通過使用一個存盤了地圖內各個元素的二維陣列Game.map,使用廣度優先演算法遍歷出一條路線,將結果存放于堆疊之中,
實作代碼:
/**
* 使用廣度遍歷演算法,使用佇列存盤遍歷的節點
*
* @return 移動的路徑
*/
private Stack<Coord> GetPath() {
Coord target = Game.tanks.get(Game.P1_TAG).coord;
Queue<Coord> d_q = new LinkedBlockingQueue<>();
ArrayList<Coord> IsMove = new ArrayList<>();
d_q.offer(coord);
IsMove.add(coord);
Coord last = null;
boolean flag;
while (!d_q.isEmpty()) {
Coord t = d_q.poll();
int tx = t.x;
int ty = t.y;
int i;
//遍歷所有的方向
for (i = 0; i < 4; ++i) {
switch (i) {
case Game.UP:
ty -= 1;
break;
case Game.LEFT:
tx -= 1;
break;
case Game.RIGHT:
tx += 1;
break;
case Game.DOWN:
ty += 1;
break;
}
//判斷該點是否可行
flag = true;
Coord z = new Coord(tx, ty);
//檢查是否為目標終點
if (z.equals(target)) {
z.per = t;
last = z;
break;
}
//檢查該坐標是否已經遍歷了
for (Coord c : IsMove) {
if (c.equals(z)) {
flag = false;
break;
}
}
if (flag) {
//檢查下一格是否可以抵達
flag = !(Game.map[ty][tx] == Game.BLANK || Game.map[ty][tx] == Game.WALLS);
}
//該點可以用
if (flag) {
//將坐標納入已經遍歷的佇列中
d_q.offer(z);
IsMove.add(z);
z.per = t;
last = z;
}
IsMove.add(z);
//重新選擇方向遍歷
tx = t.x;
ty = t.y;
}
//如果沒有四個方向都遍歷完就跳出,說明已經找到了終點
if (i != 4) {
break;
}
}
Stack<Coord> coords = new Stack<>();
while (null != last && last.per != null) {
coords.push(last);
last = last.per;
}
return coords;
}
八.網路聯機
客戶端連接上服務器
- 首先客戶端通過TCP連接上服務器, 并把自己的UDP埠號發送給服務器, 這里省略描述TCP連接機制, 但是明白了連接機制后對為什么需要填寫服務器埠號和IP會有更深的理解, 它們均為TCP報文段中必填的欄位
- 服務器通過TCP和客戶端連上后收到客戶端的UDP埠號資訊, 并將客戶端的IP地址和UDP埠號封裝成一個Client物件, 保存在容器中
- 這里補充一點, 為什么能獲取客戶端的IP地址? 因為服務器收到鏈路層幀后會提取出網路層資料報, 源地址的IP地址在IP資料報的首部欄位中, Java對這一提取程序進行了封裝, 所以我們能夠直接在Java的api中獲取源地址的IP
- 服務器封裝完Client物件后, 為客戶端的主機坦克分配一個id號, 這個id號將用于往后游戲的網路傳輸中標識這臺坦克
- 同時服務器也會把自己的UDP埠號發送客戶端, 因為服務器自身會開啟一條UDP執行緒, 用于接收轉發UDP包. 具體作用在后面會講到
- 客戶端收到坦克id后設定到自己的主戰坦克的id欄位中. 并保存服務器的UDP埠號.
這里你可能會對UDP埠號產生疑問, 別急, 后面一小節將描述它的作用

附上這部分的代碼片段:
//客戶端
public void connect(String ip, int port){
serverIP = ip;
Socket s = null;
try {
ds = new DatagramSocket(UDP_PORT);//創建UDP套接字
s = new Socket(ip, port);//創建TCP套接字
DataOutputStream dos = new DataOutputStream(s.getOutputStream());
dos.writeInt(UDP_PORT);//向服務器發送自己的UDP埠號
DataInputStream dis = new DataInputStream(s.getInputStream());
int id = dis.readInt();//獲得服務器分配給自己坦克的id號
this.serverUDPPort = dis.readInt();//獲得服務器的UDP埠號
tc.getMyTank().id = id;
tc.getMyTank().setGood((id & 1) == 0 ? true : false);//根據坦克的id號的奇偶性設定坦克的陣營
} catch (IOException e) {
e.printStackTrace();
}finally {
try{
if(s != null) s.close();//資訊交換完畢后客戶端的TCP套接字關閉
} catch (IOException e) {
e.printStackTrace();
}
}
TankNewMsg msg = new TankNewMsg(tc.getMyTank());
send(msg);//發送坦克出生的訊息(后面介紹)
new Thread(new UDPThread()).start();//開啟UDP執行緒
}
//服務器
public void start(){
new Thread(new UDPThread()).start();//開啟UDP執行緒
ServerSocket ss = null;
try {
ss = new ServerSocket(TCP_PORT);//創建TCP歡迎套接字
} catch (IOException e) {
e.printStackTrace();
}
while(true){//監聽每個客戶端的連接
Socket s = null;
try {
s = ss.accept();//為客戶端分配一個專屬TCP套接字
DataInputStream dis = new DataInputStream(s.getInputStream());
int UDP_PORT = dis.readInt();//獲得客戶端的UDP埠號
Client client = new Client(s.getInetAddress().getHostAddress(), UDP_PORT);//把客戶端的IP地址和UDP埠號封裝成Client物件, 以備后面使用
clients.add(client);//裝入容器中
DataOutputStream dos = new DataOutputStream(s.getOutputStream());
dos.writeInt(ID++);//給客戶端的主戰坦克分配一個id號
dos.writeInt(UDP_PORT);
}catch (IOException e) {
e.printStackTrace();
}finally {
try {
if(s != null) s.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
結論
該游戲是基于Java語言,使用Eclipse軟體開發的一款坦克大戰游戲, 該游戲包括對面板功能、坦克功能、子彈功能的設計,在面板功能中對雙方坦克、基地、河道、草坪、普通墻與鐵墻等地圖元素進行創建并設定其屬性,還實作了頁面按鈕功能,玩家可以點擊按鈕來實作相應的功能,在坦克功能中,設計了操作玩家坦克的方法,還設定了超級武器,玩家吃掉后會獲得特殊技能,在子彈功能中,設定了子彈打中不同物體物件產生的不同效果,另外,還實作了服務器與客戶端的連接,加載關卡等功能,玩家再游戲面板中可以實時查看自己坦克的生命數量和分數以及敵方坦克的數量,基本上完成了設計任務,總體來說,本游戲有一定的邏輯性和復雜性,對玩家有一定的吸引力,
在設計與實作游戲的程序中,遇到一些邏輯問題和技術故障都是在所難免的,例如如何加載地圖關卡和物體物件等、監探坦克與地圖元素是否碰撞等,都是需要完全克服的,該游戲還需要進一步的優化,需要在更大的程度上提升敵方坦克的智能化、在地圖中添加物體物件來增強可玩性等等,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/280561.html
標籤:其他
上一篇:Java貪吃蛇全代碼
下一篇:2021-04-26
