介绍
通过本项目能够更直观地理解应用层和运输层网络协议, 以及继承封装多态的运用. 网络部分是本文叙述的重点, 你将看到如何使用Java建立TCP和UDP连接并交换报文, 你还将看到如何自己定义一个简单的应用层协议来让自己应用进行网络通信,文末附源码。
本项目的Github地址
基础版本
游戏的原理, 图形界面(非重点)
- 多张图片快速连续地播放, 图片中的东西就能动起来形成视频, 对视频中动起来的东西进行操作就变成游戏了. 在一个坦克对战游戏中, 改变一辆坦克每一帧的位置, 当多帧连续播放的时候, 视觉上就有了控制坦克的感觉. 同理, 改变子弹每一帧的位置, 看起来就像是发射了一发炮弹. 当子弹和坦克的位置重合, 也就是两个图形的边界相碰时, 在碰撞的位置放上一个爆炸的图片, 就完成了子弹击中坦克发生爆炸的效果.
- 在本项目借助坦克游戏认识网络知识和面向对象思想, 游戏的显示与交互使用到了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() {//为窗口的关闭按钮添加监听
@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 -