介绍

  • 通过本项目能够更直观地理解应用层和运输层网络协议, 以及继承封装多态的运用. 网络部分是本文叙述的重点, 你将看到如何使用Java建立TCP和UDP连接并交换报文, 你还将看到如何自己定义一个简单的应用层协议来让自己应用进行网络通信.

加群即可获取坦克大战(联网版)三个版本源码,还有价值3000的Java学习资料:731892759

加群即可收获:

  1. Java自学0基础攻略
  2. 读书笔记,自学书籍pdf
  3. 大厂面试题,优质项目练习题
  4. 你可能需要的答疑解惑和助力小伙伴(下图为一部分资源~)

基础版本

游戏的原理, 图形界面(非重点)

  • 多张图片快速连续地播放, 图片中的东西就能动起来形成视频, 对视频中动起来的东西进行操作就变成游戏了. 在一个坦克对战游戏中, 改变一辆坦克每一帧的位置, 当多帧连续播放的时候, 视觉上就有了控制坦克的感觉. 同理, 改变子弹每一帧的位置, 看起来就像是发射了一发炮弹. 当子弹和坦克的位置重合, 也就是两个图形的边界相碰时, 在碰撞的位置放上一个爆炸的图片, 就完成了子弹击中坦克发生爆炸的效果.
  • 在本项目借助坦克游戏认识网络知识和面向对象思想, 游戏的显示与交互使用到了Java中的图形组件, 如今Java已较少用于图形交互程序开发, 本项目也只是使用了一些简单的图形组件.
  • 在本项目中, 游戏的客户端由TankClient类控制, 游戏的运行和所有的图形操作都包含在这个类中, 下面会介绍一些主要的方法.
//类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() {//为窗口的关闭按钮添加监听@Overridepublic 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;//子弹生命设置为falset.setLive(false);//坦克生命设置为falsetc.getExplodes().add(new Explode(x, y, tc));//产生一个爆炸, 坐标为子弹的坐标return true;}return false;
}
  • 补充, 坦克和子弹都以图形的方式显示, 在本游戏中通过Java的原生api获得图形的矩形框并判断是否重合(碰撞)
public Rectangle getRect() {return new Rectangle(x, y, WIDTH, HEIGHT);
}
  • 在了解游戏中两个主要对象后, 下面介绍整个游戏的逻辑.
  • 加载游戏窗口后, 客户端会创建一个我的坦克对象, 初始化三个容器, 它们分别用于存放其他坦克, 子弹和爆炸.
  • 当按下开火键后, 会创建一个子弹对象, 并加入到子弹容器中(主战坦克发出一棵炮弹), 如果子弹没有击中坦克, 穿出游戏窗口边界后判定子弹死亡, 从容器中移除; 如果子弹击中了敌方坦克, 敌方坦克死亡从容器移出, 子弹也死亡从容器移出, 同时会创建一个爆炸对象放到容器中, 等爆炸的图片轮播完, 爆炸移出容器.
  • 以上就是整个坦克游戏的逻辑. 下面将介绍重头戏, 网络联机.

网络联机

客户端连接上服务器

  • 首先客户端通过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();}}}
}

定义应用层协议

  • 客户机连上服务器后, 两边分别获取了初始信息, 且客户端和服务器均开启了UDP线程. 客户端通过保存的服务器UDP端口号可以向服务器的UDP套接字发送UDP包, 服务器保存了所有连上它的Client客户端信息, 它可以向所有客户端的UDP端口发送UDP包.
  • 此后, 整个坦克游戏的网络模型已经构建完毕, 游戏中的网络传输道路已经铺设好, 但想要在游戏中进行网络传输还差一样东西, 它就是这个网络游戏的应用层通信协议.
  • 在本项目中, 应用层协议很简单, 只有两个字段, 一个是消息类型, 一个是消息数据(有效载荷).
  • 这里先列出所有的具体协议, 后面将进行逐一讲解.
消息类型 消息数据
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
  • 在描述整个应用层协议体系及具体应用前需要补充一下, 文章前面提到TankClient类用于控制整个游戏客户端, 但为了解耦, 客户端将需要进行的网络操作使用另外一个NetClient类进行封装.
  • 回到正题, 我们把应用层协议定义为一个接口, 具体到每个消息协议有具体的实现类, 这里我们将用到多态.
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坦克出生协议, 消息类型为1. 它包含的字段有坦克id, 坦克坐标, 坦克方向, 坦克好坏.
  • 当我们的客户端和服务器完成TCP连接后, 客户端的UDP会向服务器的UDP发送一个TankNewMsg消息, 告诉服务器自己加入到了游戏中, 服务器会将这个消息转发到所有在服务器中注册过的客户端. 这样每个客户端都知道了有一个新的坦克加入, 它们会根据TankNewMsg中新坦克的信息创建出一个新的坦克对象, 并加入到自己的坦克容器中.
  • 但是这里涉及到一个问题: 已经连上服务器的客户端会收到新坦克的信息并把新坦克加入到自己的游戏中, 但是新坦克的游戏中并没有其他已经存在的坦克信息.
  • 一个较为简单的方法是旧坦克在接收到新坦克的信息后也发送一条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);//生成MissileNewMsgtc.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, 其他坦克接收到TankNewMsg会往自己的坦克容器中添加这辆新的坦克.
  • 之前描述过存在的问题: 旧坦克能把新坦克加入到游戏中, 但是新坦克不能把旧坦克加入到游戏中, 当时使用的临时解决方案是: 旧坦克接收到TankNewMsg后判断该坦克是否已经存在自己的容器中, 如果不存在则添加进容器, 并且自己发送一个TankNewMsg, 这样新的坦克接收到旧坦克的TankNewMsg, 就能把旧坦克加入到游戏里.
  • 但是, 我们定义的TankNewMsg是发出一个坦克出生的信息, 如果把TankNewMsg同时用于引入旧坦克, 如果以后要修改TankNewMsg就会牵涉到其他的代码, 我们应该用一个新的消息来让新坦克把旧坦克加入到游戏中.
  • 当旧坦克接收TankNewMsg后证明有新坦克加入, 它先把新坦克加入到容器中, 再向服务器发送一个TankAlreadyExistMsg, 其他坦克检查自己的容器中是否有已经准备的坦克的信息, 如果有了就不添加, 没有则把它添加到容器中.
  • 不得不说, 使用多态后扩展协议就变得很方便了.
//修改后, 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();}
}

坦克战亡后服务器端的处理

  • 当一辆坦克死后, 服务器应该从Client集合中删除掉该客户端的信息, 从而不用向该客户端发送信息, 减轻负载.而且服务器应该开启一个新的UDP端口号用于接收坦克死亡的消息, 不然这个死亡的消息会转发给其他客户端.
  • 所以在客户端进行TCP连接的时候要把这个就收坦克死亡信息的UDP端口号也发送给客户端.
  • 被击败后, 弹框通知游戏结束.
//服务端添加的代码片段
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消息中除了包含坦克的方向, 也包含坦克的坐标, 相当于做了客户端线程同步. 所以考虑暂时不需要再额外进行客户端同步了.

添加图片

  • 在基础版本中, 坦克和子弹都是通过画一个圆表示, 现在添加坦克和子弹的图片为游戏注入灵魂。

在本项目中运输层协议可以直接调用Java api实现, 但是应用层协议就要自己定义了. 尽管只是定义了几个超级简单的协议, 但是定义过的协议在发送端和接收端是如何处理的, 是落实到代码敲出来的。

加群即可获取坦克大战(联网版)三个版本源码,还有价值3000的Java学习资料:731892759

一起玩游戏吧~

Java实现简易联网坦克对战小游戏(内涵源码)//Java+Java游戏+拓展学习+资源分享相关推荐

  1. 手把手教你用Java实现一个简易联网坦克对战小游戏

    作者:炭烧生蚝 cnblogs.com/tanshaoshenghao/p/10708586.html 介绍 通过本项目能够更直观地理解应用层和运输层网络协议, 以及继承封装多态的运用. 网络部分是本 ...

  2. java怎么实现网络对战平台_手把手教你用Java实现一个简易联网坦克对战小游戏...

    介绍 通过本项目能够更直观地理解应用层和运输层网络协议, 以及继承封装多态的运用. 网络部分是本文叙述的重点, 你将看到如何使用Java建立TCP和UDP连接并交换报文, 你还将看到如何自己定义一个简 ...

  3. java联机_Java实现简易联网坦克对战小游戏

    介绍 通过本项目能够更直观地理解应用层和运输层网络协议, 以及继承封装多态的运用. 网络部分是本文叙述的重点, 你将看到如何使用Java建立TCP和UDP连接并交换报文, 你还将看到如何自己定义一个简 ...

  4. 如何用java让坦克发射子弹_手把手教你用Java实现一个简易联网坦克对战小游戏 !...

    介绍 通过本项目能够更直观地理解应用层和运输层网络协议, 以及继承封装多态的运用. 网络部分是本文叙述的重点, 你将看到如何使用Java建立TCP和UDP连接并交换报文, 你还将看到如何自己定义一个简 ...

  5. java计算机毕业设计社区微服务平台小程序服务器端源码+系统+数据库+lw文档

    java计算机毕业设计社区微服务平台小程序服务器端源码+系统+数据库+lw文档 java计算机毕业设计社区微服务平台小程序服务器端源码+系统+数据库+lw文档 本源码技术栈: 项目架构:B/S架构 开 ...

  6. Java和uniapp开发的聚合快递小程序软件系统源码功能介绍

    目前快递公司很多,每家都有自己的软件系统,导致用户使用的时候需要下载或者记住不少对应软件. 你是不是也希望有一款可以将这些快递公司聚合在一起的工具呢? 为了解决这个问题,我们做了一款聚合快递小程序.这 ...

  7. java版b2b2c多商家入驻微信小程序商城源码Spring Cloud+Spring Boot+mybatis+security+uniapp+直播带货+VR全景+前后端分离微服务商城源码

    1. 涉及平台 平台管理.商家端(PC端.手机端).买家平台(H5/公众号.小程序.APP端(IOS/Android).微服务平台(业务服务.系统服务.中间件服务) 2. 核心架构 Spring Cl ...

  8. 微信第三方扫描登录 java源代码_微信开放平台基于网站应用授权登录源码(java)...

    1. 第三方发起微信授权登录请求,微信用户允许授权第三方应用后,微信会拉起应用或重定向到第三方网站,并且带上授权临时票据code参数: 2. 通过code参数加上AppID和AppSecret等,通过 ...

  9. java实现游戏对战过程_【Java实战】Java实现简易坦克对战小游戏

    //此程序用来实现一个简易的坦克对战小游戏! //Version:1.0 // @Author:Yongchun_zha package cha09; import javax.swing.*; im ...

最新文章

  1. 【爬虫】Scrapy爬取腾讯社招信息
  2. java设计模式适配器模式_Java中的适配器设计模式
  3. Auto activation triggers for Java(代码提示)功能扩展
  4. Adobe Illustrator CS6 绿色简体中文版下载地址
  5. MySQL入门 (二) : SELECT 基础查询
  6. 装修时水电如何开槽?沟槽如何封堵?有哪些防止沟槽开裂的方法
  7. Shell:Lite OS在线调试工具知多少
  8. 【理解】 Error 10053和 Error 10054
  9. 面向对象三大特征:封装、继承、多态
  10. Java WebService视频教程
  11. 联想笔记本键盘亮屏幕不亮_笔记本电脑进入睡眠状态后无法通过鼠标或键盘来唤醒屏幕怎么解决...
  12. 数字孪生智慧选煤厂:数据监控赋能矿山高效生产
  13. Google Guice简介
  14. stm32 代码加密
  15. oc引导windows蓝屏_蓝屏错误疑难解答
  16. matlab幻方置乱,幻方置乱,magic scrambling,音标,读音,翻译,英文例句,英语词典
  17. 无线覆盖商场微信吸粉解决方案
  18. python爬取微博图片教程_Python爬取微博实例分析
  19. 神奇的主力成本线!居然能将主力成本运筹帷幄?!
  20. 端午送粽子祝福微信小程序源码下载支持打赏模式带背景音乐

热门文章

  1. 3d游戏编程(转帖)
  2. 对Java中常见的四种I/O模型理解
  3. 想看两不厌,笑我太疯癫
  4. 计算机内存和磁盘的关系
  5. Leetcode滑窗系列(java):643. 子数组最大平均数 I
  6. 【100题】给定入栈序列,判断一个序列是否可能为输出序列
  7. Qt调试器出现:the selected debugger may be inappropriate for the inferior的解决方案
  8. thinkadmin下拉框实现默认选中
  9. ios降级鸿蒙,iOS 降级 13.3 系统方法,你成功了没
  10. c语言程序设计 学籍,c语言学籍信息管理系统设计