Java实现坦克大战小游戏(源码+注释)

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

一.主要设计内容:

1、需要有图形用户界面,让用户能看到游戏给出的反馈。
2、不同队伍的坦克,须显示不同的外观,以区分敌我。
3、坦克能够开火,攻击敌方,但不能攻击队友。
4、需要有不可被子弹穿透的墙体,以及墙体有可被摧毁和不可被摧毁两种。
5、敌方坦克有生命值,并非被击中一次就会爆炸。
6、游戏结束时会有胜利或失败场景
7、游戏结束后可重新开始。

二. 程序源码基本结构


Java实现坦克大战小游戏(源码+注释)_第1张图片

三.设计的环境、方法及措施:

  • 主要设备:电脑。
  • 软件:eclipse软件。
  • 方法措施:
  • 利用学到的java基础应用程序知识实现需求分析上的功能模块,并在设计与实现时分析遇到的问题并解决,对实现的模块进行测试,查找不足,尽量做到满足用户要求

四. 实现了哪些功能

此系统是使用Java语言实现坦克大战游戏程序,玩家通过连接访问进入游戏,通过操纵坦克来守卫基地,玩家还可以获得超级武器来提升坦克的属性,摧毁全部敌方坦克来取得胜利。本系统结构如下:
(1)面板功能:
对双方坦克、基地、河道、草坪、普通墙与铁墙等地图元素,还实现了页面按钮功能,玩家可以点击按钮来实现相应的功能。

(2)坦克功能:
操作玩家坦克的方法,还设置了超级武器,玩家吃掉后会获得特殊技能。

(3)子弹功能:
设置了子弹打中不同物体对象产生的不同效果。

五. 运行效果


Java实现坦克大战小游戏(源码+注释)_第2张图片


Java实现坦克大战小游戏(源码+注释)_第3张图片
Java实现坦克大战小游戏(源码+注释)_第4张图片
Java实现坦克大战小游戏(源码+注释)_第5张图片
Java实现坦克大战小游戏(源码+注释)_第6张图片
Java实现坦克大战小游戏(源码+注释)_第7张图片

六. 实现思路

  • 数据存储表示: 在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端口号产生疑问, 别急, 后面一小节将描述它的作用

    Java实现坦克大战小游戏(源码+注释)_第8张图片

附上这部分的代码片段:

//客户端
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软件开发的一款坦克大战游戏, 该游戏包括对面板功能、坦克功能、子弹功能的设计,在面板功能中对双方坦克、基地、河道、草坪、普通墙与铁墙等地图元素进行创建并设置其属性,还实现了页面按钮功能,玩家可以点击按钮来实现相应的功能。在坦克功能中,设计了操作玩家坦克的方法,还设置了超级武器,玩家吃掉后会获得特殊技能。在子弹功能中,设置了子弹打中不同物体对象产生的不同效果。另外,还实现了服务器与客户端的连接,加载关卡等功能,玩家再游戏面板中可以实时查看自己坦克的生命数量和分数以及敌方坦克的数量,基本上完成了设计任务。总体来说,本游戏有一定的逻辑性和复杂性,对玩家有一定的吸引力。

在设计与实现游戏的过程中,遇到一些逻辑问题和技术故障都是在所难免的,例如如何加载地图关卡和物体对象等、监探坦克与地图元素是否碰撞等,都是需要完全克服的。该游戏还需要进一步的优化,需要在更大的程度上提升敌方坦克的智能化、在地图中添加物体对象来增强可玩性等等。

你可能感兴趣的:(java,游戏,游戏开发)