介绍
通过本项目能够更直观地理解应用层和运输层网络协议, 以及继承封装多态的运用. 网络部分是本文叙述的重点, 你将看到如何使用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消息中除了包含坦克的方向, 也包含坦克的坐标, 相当于做了客户端线程同步. 所以考虑暂时不需要再额外进行客户端同步了.
添加图片
在基础版本中, 坦克和子弹都是通过画一个圆表示, 现在添加坦克和子弹的图片为游戏注入灵魂.
总结与致谢
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对亿速云的支持。
亿速云「云服务器」,即开即用、新一代英特尔至强铂金CPU、三副本存储NVMe SSD云盘,价格低至29元/月。点击查看>>
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。