您好,登錄后才能下訂單哦!
介紹
通過本項目能夠更直觀地理解應用層和運輸層網絡協議, 以及繼承封裝多態的運用. 網絡部分是本文敘述的重點, 你將看到如何使用Java建立TCP和UDP連接并交換報文, 你還將看到如何自己定義一個簡單的應用層協議來讓自己應用進行網絡通信.
獲取源碼 (本地下載)
基礎版本
游戲的原理, 圖形界面(非重點)
//類TankClient, 繼承自Frame類 //繼承Frame類后所重寫的兩個方法paint()和update() //在paint()方法中設置在一張圖片中需要畫出什么東西. @Override public void paint(Graphics g) { //下面三行畫出游戲窗口左上角的游戲參數 g.drawString("missiles count:" + missiles.size(), 10, 50); g.drawString("explodes count:" + explodes.size(), 10, 70); g.drawString("tanks count:" + tanks.size(), 10, 90); //檢測我的坦克是否被子彈打到, 并畫出子彈 for(int i = 0; i < missiles.size(); i++) { Missile m = missiles.get(i); if(m.hitTank(myTank)){ TankDeadMsg msg = new TankDeadMsg(myTank.id); nc.send(msg); MissileDeadMsg mmsg = new MissileDeadMsg(m.getTankId(), m.getId()); nc.send(mmsg); } m.draw(g); } //畫出爆炸 for(int i = 0; i < explodes.size(); i++) { Explode e = explodes.get(i); e.draw(g); } //畫出其他坦克 for(int i = 0; i < tanks.size(); i++) { Tank t = tanks.get(i); t.draw(g); } //畫出我的坦克 myTank.draw(g); } /* * update()方法用于寫每幀更新時的邏輯. * 每一幀更新的時候, 我們會把該幀的圖片畫到屏幕中. * 但是這樣做是有缺陷的, 因為把一副圖片畫到屏幕上會有延時, 游戲顯示不夠流暢 * 所以這里用到了一種緩沖技術. * 先把圖像畫到一塊幕布上, 每幀更新的時候直接把畫布推到窗口中顯示 */ @Override public void update(Graphics g) { if(offScreenImage == null) { offScreenImage = this.createImage(800, 600);//創建一張畫布 } Graphics gOffScreen = offScreenImage.getGraphics(); Color c = gOffScreen.getColor(); gOffScreen.setColor(Color.GREEN); gOffScreen.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT); gOffScreen.setColor(c); paint(gOffScreen);//先在畫布上畫好 g.drawImage(offScreenImage, 0, 0, null);//直接把畫布推到窗口 } //這是加載游戲窗口的方法 public void launchFrame() { this.setLocation(400, 300);//設置游戲窗口相對于屏幕的位置 this.setSize(GAME_WIDTH, GAME_HEIGHT);//設置游戲窗口的大小 this.setTitle("TankWar");//設置標題 this.addWindowListener(new WindowAdapter() {//為窗口的關閉按鈕添加監聽 @Override public void windowClosing(WindowEvent e) { System.exit(0); } }); this.setResizable(false);//設置游戲窗口的大小不可改變 this.setBackground(Color.GREEN);//設置背景顏色 this.addKeyListener(new KeyMonitor());//添加鍵盤監聽, this.setVisible(true);//設置窗口可視化, 也就是顯示出來 new Thread(new PaintThread()).start();//開啟線程, 把圖片畫出到窗口中 dialog.setVisible(true);//顯示設置服務器IP, 端口號, 自己UDP端口號的對話窗口 } //在窗口中畫出圖像的線程, 定義為每50毫秒畫一次. class PaintThread implements Runnable { public void run() { while(true) { repaint(); try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } } } }
以上就是整個游戲圖形交互的主要部分, 保證了游戲能正常顯示后, 下面我們將關注于游戲的邏輯部分.
游戲邏輯
在游戲的邏輯中有兩個重點, 一個是坦克, 另一個是子彈. 根據面向對象的思想, 分別把這兩者封裝成兩個類, 它們所具有的行為都在類對應有相應的方法.
坦克的字段
public int id;//作為網絡中的標識 public static final int XSPEED = 5;//左右方向上每幀移動的距離 public static final int YSPEED = 5;//上下方向每幀移動的距離 public static final int WIDTH = 30;//坦克圖形的寬 public static final int HEIGHT = 30;//坦克圖形的高 private boolean good;//根據true和false把坦克分成兩類, 游戲中兩派對戰 private int x, y;//坦克的坐標 private boolean live = true;//坦克是否活著, 死了將不再畫出 private TankClient tc;//客戶端類的引用 private boolean bL, bU, bR, bD;//用于判斷鍵盤按下的方向 private Dir dir = Dir.STOP;//坦克的方向 private Dir ptDir = Dir.D;//炮筒的方向
由于在TankClient類中的paint方法中需要畫出圖形, 根據面向對象的思想, 要畫出一輛坦克, 應該由坦克調用自己的方法畫出自己.
public void draw(Graphics g) { if(!live) { if(!good) { tc.getTanks().remove(this);//如果坦克死了就把它從容器中去除, 并直接結束 } return; } //畫出坦克 Color c = g.getColor(); if(good) g.setColor(Color.RED); else g.setColor(Color.BLUE); g.fillOval(x, y, WIDTH, HEIGHT); g.setColor(c); //畫出炮筒 switch(ptDir) { case L: g.drawLine(x + WIDTH/2, y + HEIGHT/2, x, y + HEIGHT/2); break; case LU: g.drawLine(x + WIDTH/2, y + HEIGHT/2, x, y); break; case U: g.drawLine(x + WIDTH/2, y + HEIGHT/2, x + WIDTH/2, y); break; //...省略部分方向 } move();//每次畫完改變坦克的坐標, 連續畫的時候坦克就動起來了 }
上面提到了改變坦克坐標的move()方法, 具體代碼如下:
private void move() { switch(dir) {//根據坦克的方向改變坐標 case L://左 x -= XSPEED; break; case LU://左上 x -= XSPEED; y -= YSPEED; break; //...省略 } if(dir != Dir.STOP) { ptDir = dir; } //防止坦克走出游戲窗口, 越界時要停住 if(x < 0) x = 0; if(y < 30) y = 30; if(x + WIDTH > TankClient.GAME_WIDTH) x = TankClient.GAME_WIDTH - WIDTH; if(y + HEIGHT > TankClient.GAME_HEIGHT) y = TankClient.GAME_HEIGHT - HEIGHT; }
上面提到了根據坦克的方向改變坦克的左邊, 而坦克的方向通過鍵盤改變. 代碼如下:
public void keyPressed(KeyEvent e) {//接收接盤事件 int key = e.getKeyCode(); //根據鍵盤按下的按鍵修改bL, bU, bR, bD四個布爾值, 回后會根據四個布爾值判斷上, 左上, 左等八個方向 switch (key) { case KeyEvent.VK_A://按下鍵盤A鍵, 意味著往左 bL = true; break; case KeyEvent.VK_W://按下鍵盤W鍵, 意味著往上 bU = true; break; case KeyEvent.VK_D: bR = true; break; case KeyEvent.VK_S: bD = true; break; } locateDirection();//根據四個布爾值判斷八個方向的方法 } private void locateDirection() { Dir oldDir = this.dir;//記錄下原來的方法, 用于聯網 //根據四個方向的布爾值判斷八個更細分的方向 //比如左和下都是true, 證明玩家按的是左下, 方向就該為左下 if(bL && !bU && !bR && !bD) dir = Dir.L; else if(bL && bU && !bR && !bD) dir = Dir.LU; else if(!bL && bU && !bR && !bD) dir = Dir.U; else if(!bL && bU && bR && !bD) dir = Dir.RU; else if(!bL && !bU && bR && !bD) dir = Dir.R; else if(!bL && !bU && bR && bD) dir = Dir.RD; else if(!bL && !bU && !bR && bD) dir = Dir.D; else if(bL && !bU && !bR && bD) dir = Dir.LD; else if(!bL && !bU && !bR && !bD) dir = Dir.STOP; //可以先跳過這段代碼, 用于網絡中其他客戶端的坦克移動 if(dir != oldDir){ TankMoveMsg msg = new TankMoveMsg(id, x, y, dir, ptDir); tc.getNc().send(msg); } } //對鍵盤釋放的監聽 public void keyReleased(KeyEvent e) { int key = e.getKeyCode(); switch (key) { case KeyEvent.VK_J://設定J鍵開火, 當釋放J鍵時發出一發子彈 fire(); break; case KeyEvent.VK_A: bL = false; break; case KeyEvent.VK_W: bU = false; break; case KeyEvent.VK_D: bR = false; break; case KeyEvent.VK_S: bD = false; break; } locateDirection(); }
上面提到了坦克開火的方法, 這也是坦克最后一個重要的方法了, 代碼如下, 后面將根據這個方法引出子彈類.
private Missile fire() { if(!live) return null;//如果坦克死了就不能開火 int x = this.x + WIDTH/2 - Missile.WIDTH/2;//設定子彈的x坐標 int y = this.y + HEIGHT/2 - Missile.HEIGHT/2;//設定子彈的y坐標 Missile m = new Missile(id, x, y, this.good, this.ptDir, this.tc);//創建一顆子彈 tc.getMissiles().add(m);//把子彈添加到容器中. //網絡部分可暫時跳過, 發出一發子彈后要發送給服務器并轉發給其他客戶端. MissileNewMsg msg = new MissileNewMsg(m); tc.getNc().send(msg); return m; }
子彈類, 首先是子彈的字段
public static final int XSPEED = 10;//子彈每幀中坐標改變的大小, 比坦克大些, 子彈當然要飛快點嘛 public static final int YSPEED = 10; public static final int WIDTH = 10; public static final int HEIGHT = 10; private static int ID = 10; private int id;//用于在網絡中標識的id private TankClient tc;//客戶端的引用 private int tankId;//表明是哪個坦克發出的 private int x, y;//子彈的坐標 private Dir dir = Dir.R;//子彈的方向 private boolean live = true;//子彈是否存活 private boolean good;//子彈所屬陣營, 我方坦克自能被地方坦克擊斃
子彈類中同樣有draw(), move()等方法, 在此不重復敘述了, 重點關注子彈打中坦克的方法. 子彈是否打中坦克, 是調用子彈自身的判斷方法判斷的.
public boolean hitTank(Tank t) { //如果子彈是活的, 被打中的坦克也是活的 //子彈和坦克不屬于同一方 //子彈的圖形碰撞到了坦克的圖形 //認為子彈打中了坦克 if(this.live && t.isLive() && this.good != t.isGood() && this.getRect().intersects(t.getRect())) { this.live = false;//子彈生命設置為false t.setLive(false);//坦克生命設置為false tc.getExplodes().add(new Explode(x, y, tc));//產生一個爆炸, 坐標為子彈的坐標 return true; } return false; }
補充, 坦克和子彈都以圖形的方式顯示, 在本游戲中通過Java的原生api獲得圖形的矩形框并判斷是否重合(碰撞)
public Rectangle getRect() { return new Rectangle(x, y, WIDTH, HEIGHT); }
網絡聯機
客戶端連接上服務器
附上這部分的代碼片段:
//客戶端 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(); } } } }
定義應用層協議
消息類型 | 消息數據 |
---|---|
1.TANK_NEW_MSG(坦克出生信息) | 坦克id, 坦克坐標, 坦克方向, 坦克好壞 |
2.TANK_MOVE_MSG(坦克移動信息) | 坦克id, 坦克坐標, 坦克方向, 炮筒方向 |
3.MISSILE_NEW_MESSAGE(子彈產生信息) | 發出子彈的坦克id, 子彈id, 子彈坐標, 子彈方向 |
4.TANK_DEAD_MESSAGE(子彈死亡的信息) | 發出子彈的坦克id, 子彈id |
5.MISSILE_DEAD_MESSAGE(坦克死亡的信息) | 坦克id |
public interface Msg { public static final int TANK_NEW_MSG = 1; public static final int TANK_MOVE_MSG= 2; public static final int MISSILE_NEW_MESSAGE = 3; public static final int TANK_DEAD_MESSAGE = 4; public static final int MISSILE_DEAD_MESSAGE = 5; //每個消息報文, 自己將擁有發送和解析的方法, 為多態的實現奠定基礎. public void send(DatagramSocket ds, String IP, int UDP_Port); public void parse(DataInputStream dis); }
下面將描述多態的實現給本程序帶來的好處.
在NetClient這個網絡接口類中, 需要定義發送消息和接收消息的方法. 想一下, 如果我們為每個類型的消息編寫發送和解析的方法, 那么程序將變得復雜冗長. 使用多態后, 每個消息實現類自己擁有發送和解析的方法, 要調用NetClient中的發送接口發送某個消息就方便多了. 下面代碼可能解釋的更清楚.
//如果沒有多態的話, NetClient中將要定義每個消息的發送方法 public void sendTankNewMsg(TankNewMsg msg){ //很長... } public void sendMissileNewMsg(MissileNewMsg msg){ //很長... } //只要有新的消息類型, 后面就要接著定義... //假如使用了多態, NetClient中只需要定義一個發送方法 public void send(Msg msg){ msg.send(ds, serverIP, serverUDPPort); } //當我們要發送某個類型的消息時, 只需要 TankNewMsg msg = new TankNewMsg(); NetClient nc = new NetClient();//實踐中不需要, 能拿到唯一的NetClient的引用 nc.send(msg) //在NetClient類中, 解析的方法如下 private void parse(DatagramPacket dp) { ByteArrayInputStream bais = new ByteArrayInputStream(buf, 0, dp.getLength()); DataInputStream dis = new DataInputStream(bais); int msgType = 0; try { msgType = dis.readInt();//先拿到消息的類型 } catch (IOException e) { e.printStackTrace(); } Msg msg = null; switch (msgType){//根據消息的類型, 調用具體消息的解析方法 case Msg.TANK_NEW_MSG : msg = new TankNewMsg(tc); msg.parse(dis); break; case Msg.TANK_MOVE_MSG : msg = new TankMoveMsg(tc); msg.parse(dis); break; case Msg.MISSILE_NEW_MESSAGE : msg = new MissileNewMsg(tc); msg.parse(dis); break; case Msg.TANK_DEAD_MESSAGE : msg = new TankDeadMsg(tc); msg.parse(dis); break; case Msg.MISSILE_DEAD_MESSAGE : msg = new MissileDeadMsg(tc); msg.parse(dis); break; } }
接下來介紹每個具體的協議.
TankNewMsg
//下面是TankNewMsg中解析本消息的方法 public void parse(DataInputStream dis){ try{ int id = dis.readInt(); if(id == this.tc.getMyTank().id){ return; } int x = dis.readInt(); int y = dis.readInt(); Dir dir = Dir.values()[dis.readInt()]; boolean good = dis.readBoolean(); //接收到別人的新信息, 判斷別人的坦克是否已將加入到tanks集合中 boolean exist = false; for (Tank t : tc.getTanks()){ if(id == t.id){ exist = true; break; } } if(!exist) {//當判斷到接收的新坦克不存在已有集合才加入到集合. TankNewMsg msg = new TankNewMsg(tc); tc.getNc().send(msg);//加入一輛新坦克后要把自己的信息也發送出去. Tank t = new Tank(x, y, good, dir, tc); t.id = id; tc.getTanks().add(t); } } catch (IOException e) { e.printStackTrace(); } }
TankMoveMsg
下面將介紹TankMoveMsg協議, 消息類型為2, 需要的數據有坦克id, 坦克坐標, 坦克方向, 炮筒方向. 每當自己坦克的方向發生改變時, 向服務器發送一個TankMoveMsg消息, 經服務器轉發后, 其他客戶端也能收該坦克的方向變化, 然后根據數據找到該坦克并設置方向等參數. 這樣才能相互看到各自的坦克在移動.
下面是發送TankMoveMsg的地方, 也就是改變坦克方向的時候.
private void locateDirection() { Dir oldDir = this.dir;//記錄舊的方向 if(bL && !bU && !bR && !bD) dir = Dir.L; else if(bL && bU && !bR && !bD) dir = Dir.LU; else if(!bL && bU && !bR && !bD) dir = Dir.U; else if(!bL && bU && bR && !bD) dir = Dir.RU; else if(!bL && !bU && bR && !bD) dir = Dir.R; else if(!bL && !bU && bR && bD) dir = Dir.RD; else if(!bL && !bU && !bR && bD) dir = Dir.D; else if(bL && !bU && !bR && bD) dir = Dir.LD; else if(!bL && !bU && !bR && !bD) dir = Dir.STOP; if(dir != oldDir){//如果改變后的方向不同于舊方向也就是說方向發生了改變 TankMoveMsg msg = new TankMoveMsg(id, x, y, dir, ptDir);//創建TankMoveMsg消息 tc.getNc().send(msg);//發送 } }
MissileNewMsg
下面將介紹MissileNewMsg協議, 消息類型為3, 需要的數據有發出子彈的坦克id, 子彈id, 子彈坐標, 子彈方向. 當坦克發出一發炮彈后, 需要將炮彈的信息告訴其他客戶端, 其他客戶端根據子彈的信息在游戲中創建子彈對象并加入到容器中, 這樣才能看見相互發出的子彈.
MissileNewMsg在坦克發出一顆炮彈后生成.
private Missile fire() { if(!live) return null; int x = this.x + WIDTH/2 - Missile.WIDTH/2; int y = this.y + HEIGHT/2 - Missile.HEIGHT/2; Missile m = new Missile(id, x, y, this.good, this.ptDir, this.tc); tc.getMissiles().add(m); MissileNewMsg msg = new MissileNewMsg(m);//生成MissileNewMsg tc.getNc().send(msg);//發送給其他客戶端 return m; } //MissileNewMsg的解析 public void parse(DataInputStream dis) { try{ int tankId = dis.readInt(); if(tankId == tc.getMyTank().id){//如果是自己發出的子彈就跳過(已經加入到容器了) return; } int id = dis.readInt(); int x = dis.readInt(); int y = dis.readInt(); Dir dir = Dir.values()[dis.readInt()]; boolean good = dis.readBoolean(); //把收到的這顆子彈添加到子彈容器中 Missile m = new Missile(tankId, x, y, good, dir, tc); m.setId(id); tc.getMissiles().add(m); } catch (IOException e) { e.printStackTrace(); } }
TankDeadMsg和MissileDeadMsg
下面介紹TankDeadMsg和MissileDeadMsg, 它們是一個組合, 當一臺坦克被擊中后, 發出TankDeadMsg信息, 同時子彈也死亡, 發出MissileDeadMsg信息. MissileDeadMsg需要數據發出子彈的坦克id, 子彈id, 而TankDeadMsg只需要坦克id一個數據.
//TankClient類, paint()中的代碼片段, 遍歷子彈容器中的每顆子彈看自己的坦克有沒有被打中. for(int i = 0; i < missiles.size(); i++) { Missile m = missiles.get(i); if(m.hitTank(myTank)){ TankDeadMsg msg = new TankDeadMsg(myTank.id); nc.send(msg); MissileDeadMsg mmsg = new MissileDeadMsg(m.getTankId(), m.getId()); nc.send(mmsg); } m.draw(g); } //MissileDeadMsg的解析 public void parse(DataInputStream dis) { try{ int tankId = dis.readInt(); int id = dis.readInt(); //在容器找到對應的那顆子彈, 設置死亡不再畫出, 并產生一個爆炸. for(Missile m : tc.getMissiles()){ if(tankId == tc.getMyTank().id && id == m.getId()){ m.setLive(false); tc.getExplodes().add(new Explode(m.getX(), m.getY(), tc)); break; } } } catch (IOException e) { e.printStackTrace(); } } //TankDeadMsg的解析 public void parse(DataInputStream dis) { try{ int tankId = dis.readInt(); if(tankId == this.tc.getMyTank().id){//如果是自己坦克發出的死亡消息舊跳過 return; } for(Tank t : tc.getTanks()){//否則遍歷坦克容器, 把死去的坦克移出容器, 不再畫出. if(t.id == tankId){ t.setLive(false); break; } } } catch (IOException e) { e.printStackTrace(); } }
到此為止, 基礎版本就結束了, 基礎版本已經是一個能正常游戲的版本了.
改進版本.
定義更精細的協議
//修改后, TankNewMsg的解析部分如下 public void parse(DataInputStream dis){ try{ int id = dis.readInt(); if(id == this.tc.getMyTank().getId()){ return; } int x = dis.readInt(); int y = dis.readInt(); Dir dir = Dir.values()[dis.readInt()]; boolean good = dis.readBoolean(); Tank newTank = new Tank(x, y, good, dir, tc); newTank.setId(id); tc.getTanks().add(newTank);//把新的坦克添加到容器中 //發出自己的信息 TankAlreadyExistMsg msg = new TankAlreadyExistMsg(tc.getMyTank()); tc.getNc().send(msg); } catch (IOException e) { e.printStackTrace(); } } //TankAlreadyExist的解析部分如下 public void parse(DataInputStream dis) { try{ int id = dis.readInt(); if(id == tc.getMyTank().getId()){ return; } boolean exist = false;//判定發送TankAlreadyExist的坦克是否已經存在于游戲中 for(Tank t : tc.getTanks()){ if(id == t.getId()){ exist = true; break; } } if(!exist){//不存在則添加到游戲中 int x = dis.readInt(); int y = dis.readInt(); Dir dir = Dir.values()[dis.readInt()]; boolean good = dis.readBoolean(); Tank existTank = new Tank(x, y, good, dir, tc); existTank.setId(id); tc.getTanks().add(existTank); } } catch (IOException e) { e.printStackTrace(); } }
坦克戰亡后服務器端的處理
//服務端添加的代碼片段 int deadTankUDPPort = dis.readInt();//獲得死亡坦克客戶端的UDP端口號 for(int i = 0; i < clients.size(); i++){//從Client集合中刪除該客戶端. Client c = clients.get(i); if(c.UDP_PORT == deadTankUDPPort){ clients.remove(c); } } //而客戶端則在向其他客戶端發送死亡消息后通知服務器把自己從客戶端容器移除 for(int i = 0; i < missiles.size(); i++) { Missile m = missiles.get(i); if(m.hitTank(myTank)){ TankDeadMsg msg = new TankDeadMsg(myTank.getId());//發送坦克死亡的消息 nc.send(msg); MissileDeadMsg mmsg = new MissileDeadMsg(m.getTankId(), m.getId());//發送子彈死亡的消息, 通知產生爆炸 nc.send(mmsg); nc.sendTankDeadMsg();//告訴服務器把自己從Client集合中移除 gameOverDialog.setVisible(true);//彈窗結束游戲 } m.draw(g); }
完成這個版本后, 多人游戲時游戲性更強了, 當一個玩家死后他可以重新開啟游戲再次加入戰場. 但是有個小問題, 他可能會加入到擊敗他的坦克的陣營, 因為服務器為坦克分配的id好是遞增的, 而判定坦克的陣營僅通過id的奇偶判斷. 但就這個版本來說服務器端處理死亡坦克的任務算是完成了.
客戶端線程同步
在完成基礎版本后考慮過這個問題, 因為在游戲中, 由于延時的原因, 可能會造成各個客戶端線程不同步. 處理手段可以是每隔一定時間, 各個客戶端向服務器發送自己坦克的位置消息, 服務器再將該位置消息通知到其他客戶端, 進行同步. 但是在本游戲中, 只要坦克的方向一發生移動就會發送一個TankMoveMsg包, TankMoveMsg消息中除了包含坦克的方向, 也包含坦克的坐標, 相當于做了客戶端線程同步. 所以考慮暫時不需要再額外進行客戶端同步了.
添加圖片
在基礎版本中, 坦克和子彈都是通過畫一個圓表示, 現在添加坦克和子彈的圖片為游戲注入靈魂.
總結與致謝
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,謝謝大家對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。