一、项目介绍

1、前言

基于韩顺平老师坦克大战的框架和思路,进行了一些优化。编码上尽量按照阿里的代码规约;有非常详尽的注释;引入了线程池,线程安全集合类,原子类等;通过这个小项目的学习,可以深入地理解面向对象、集合、IO、多线程等,特别是多线程JUC,是整个项目的一个灵魂,所有业务都围绕着它。游戏经过测试,高峰时线程100多个,运行流畅。 感谢韩顺平老师的无私奉献!大家可以在B站搜索老师的视频,结合视频做项目,事半功倍!

2、涉及知识点

Java (JDK1.8)

编码:UTF-8

GUI、面向对象、集合、IO、多线程、原子类...

不涉及数据库

3、关键代码

4、运行数据

二、主要优化方面

1、系统常量

定义单独的类存放系统常量 Constant.java,放置在 constant 包下

public class Constant {
    // 游戏界面宽度
    public static final int WINDOW_WIDTH = 1200;
    // 游戏界面高度
    public static final int WINDOW_HEIGHT = 900;
    // 界面顶部标题栏高度
    public static final int WINDOW_TITLE_HEIGHT = 40;
    // 信息栏高度
    public static  final int INFO_BAR_HEIGHT = 100;
    
    ...
}

2、用线程池管理线程

// 我方坦克生成线程池
private ThreadPoolExecutor myTankThreadPool;

/**
* 初始化
*/
private void init() {
   ... 
   myTankThreadPool = new ThreadPoolExecutor(
       1, // 核心线程数
       1, // 最大线程数
       200, // 空闲时间
       TimeUnit.MILLISECONDS, // 时间单位
       new ArrayBlockingQueue<>(5), // 阻塞队列
       new MyThreadFactory("我方坦克"), // 线程工厂
       new ThreadPoolExecutor.DiscardOldestPolicy()); // 拒绝策略
   ... 
}

3、采用线程安全集合类

// 我方坦克集合
private volatile List<herotank> myTanks = new CopyOnWriteArrayList<>();
// 敌方坦克集合
private volatile List<enemytank> enemyTanks = new CopyOnWriteArrayList<>();

4、控制坦克发射频率

在坦克中定义一个布尔变量:boolean allowShot,表示是否可以发射;

同时,定义一个常量:发射的最小时间间隔;

在每次发射时,检查状态,是否可以发射;如是可以发射,就开始发射,发射完之后,把状态设为不可发射;然后开一个新线程,按发射的最小时间间隔来计时,结束后把状态设置可以发射

5、敌方自动追击我方

// 改变方向,追击目标坦克
if (myTanks.size() > 0 && myTanks.get(0) != null && myTanks.get(0).isLive()) {
    Tank myTank = myTanks.get(0);
    // 开启新线程,改变方向
    getShotIntervalThreadPool().execute(() -> {
        try {
            // 随机休眠
            ThreadLocalRandom random = ThreadLocalRandom.current();
            int t = 1000 + 1000 * random.nextInt(10);
            TimeUnit.MILLISECONDS.sleep(t);
            // 判断我方坦克位置,从而改变方向
            // x 轴距离
            int xDistance = myTank.getX() - getX() + Constant.TANK_WHEEL_HEIGHT;
            // y 轴距离
            int yDistance = myTank.getY() - getY() + Constant.TANK_WHEEL_HEIGHT;
            // 改变方向
            if (Math.abs(xDistance) < Math.abs(yDistance)) {
                // 纵向改变方向
                if (yDistance >= 0) {
                    // 向下
                    setDirection(Constant.DOWN);
                } else {
                    // 向上
                    setDirection(Constant.UP);
                }
            } else {
                // 横向改变方向
                if (xDistance >= 0) {
                    // 向右
                    setDirection(Constant.RIGHT);
                } else {
                    // 向左
                    setDirection(Constant.LEFT);
                }
            }
        } catch (InterruptedException interruptedException) {
            interruptedException.printStackTrace();
        }
    });
}

6、避免坦克重叠

思路:

在 Tank 父类中增加 allTanks 属性,再增加判断重叠的方法 isTouch(),每次移动时,先判断是否有重叠;如果有重叠,本次不移动;

在 面板类 MyPanel 类有 Tank 属性,可以计算坦克总数,而坦克类不设置 MyPanel 属性;因为面板只有一个,而坦克有多个;所以从 Tank 类中无法直接获取面板类的坦克总数,但坦克类中必须得到所有坦克,才能判断是否与其它坦克发生碰撞,怎么办?想得到而自已又没有能力获取到?

换种思路:可以让别人主动赠予!

让 MyPanel 给 Tank 中的 allTanks 赋值。坦克中只要有个接收方法即可:setAllTanks()

MyPanel 在坦克数量发生变化后 (初始化或者被摧毁),就给所有坦克赋一次值;

  • 具体实现

坦克每次运动时,先判断与其它坦克是否有重叠

/**
* 上移
*/
public void moveUp() {
    // 判断是否有重叠
    if (isTouch()) {
        // 重叠时自动转向
        direction = y % 2 == 0 ? Constant.DOWN : Constant.RIGHT;
    } else {
        y -= Constant.TANK_SPEED;
        // 到达边界时顺时针转向
        if (y < Constant.INFO_BAR_HEIGHT) {
            y = Constant.INFO_BAR_HEIGHT;
            direction = Constant.RIGHT;
        }
    }
}

isTouch() 方法

判断 当前坦克this 是否与其它坦克发生碰撞

方法中有大量重复代码,把重复代码提取成单独方法:

isTouchCurrentTankAndOtherTank(this, p1, p2) :判断当前坦克的两点与其它坦克是否相撞

/**
 * 判断 当前坦克this 是否与其它坦克发生碰撞
 *
 * @return
 */
private boolean isTouch() {
    // 坦克数量大于1时再判断。至少两个坦克才可能发生碰撞
    if (allTanks.size() > 1) {
        // 当前坦克的前方一点
        int x1,y1;
        Point p1;
        // 当前坦克的前方另一点
        int x2,y2;
        Point p2;

        // 根据当前坦克的方向分类判断
        switch (getDirection()) {
            /*
             向上
             判断当前坦克的 上面左右任一端 是否进入另一辆坦克的区域
             */
            case Constant.UP:
                // 当前坦克上方左侧
                x1 = this.getX();
                y1 = this.getY();
                p1 = new Point(x1, y1);
                // 当前坦克上方右侧
                x2 = x1 + 2 * Constant.TANK_WHEEL_WIDTH + Constant.TANK_BODY_WIDTH;
                y2 = y1;
                p2 = new Point(x2, y2);

                // 判断当前坦克与其它坦克是否相撞
                if(isTouchCurrentTankAndOtherTank(this, p1, p2)){
                    return true;
                }

                break;

            /*
             向右
             判断当前坦克的 右面上下任一端 是否进入另一辆坦克的区域
             */
            case Constant.RIGHT:
                // 当前坦克右侧上方
                x1 = this.getX() + 2 * Constant.TANK_WHEEL_WIDTH + Constant.TANK_BODY_WIDTH;
                y1 = this.getY();
                p1 = new Point(x1, y1);
                // 当前坦克右侧下方
                x2 = x1;
                y2 = y1 + 2 * Constant.TANK_WHEEL_WIDTH + Constant.TANK_BODY_WIDTH;
                p2 = new Point(x2, y2);

                // 判断当前坦克与其它坦克是否相撞
                if(isTouchCurrentTankAndOtherTank(this, p1, p2)){
                    return true;
                }

                break;

            /*
             向下
             判断当前坦克的 下面左右任一端 是否进入另一辆坦克的区域
             */
            case Constant.DOWN:
                // 当前坦克下方左侧
                x1 = this.getX();
                y1 = this.getY() + Constant.TANK_WHEEL_HEIGHT;
                p1 = new Point(x1, y1);
                // 当前坦克下方右侧
                x2 = x1 + 2 * Constant.TANK_WHEEL_WIDTH + Constant.TANK_BODY_WIDTH;
                y2 = y1;
                p2 = new Point(x2, y2);

                // 判断当前坦克与其它坦克是否相撞
                if(isTouchCurrentTankAndOtherTank(this, p1, p2)){
                    return true;
                }

                break;

            /*
             向左
             判断当前坦克的 左面上下任一端 是否进入另一辆坦克的区域
             */
            default:
                // 当前坦克左侧上方
                x1 = this.getX();
                y1 = this.getY();
                p1 = new Point(x1, y1);
                // 当前坦克左侧下方
                x2 = x1;
                y2 = y1 + 2 * Constant.TANK_WHEEL_WIDTH + Constant.TANK_BODY_WIDTH;
                p2 = new Point(x2, y2);

                // 判断当前坦克与其它坦克是否相撞
                if(isTouchCurrentTankAndOtherTank(this, p1, p2)){
                    return true;
                }
        }
    }
    return false;
}

判断当前坦克的两点与其它坦克是否相撞

isTouchCurrentTankAndOtherTank(this, p1, p2)

引用其它方法:

getBoundsByTank(tank) 得到 tank 的区域面

isPointInBounds(p1, b) 判断 p1 是否与 目标坦克区域面 b 重叠

/**
* 判断当前坦克与其它坦克是否相撞
* @param currentTank
* @param p1 当前坦克前方一点
* @param p2 当前坦克前方另一点
* @return
*/
private boolean isTouchCurrentTankAndOtherTank(Tank currentTank,Point p1,Point p2){
    // 目标坦克区域面
    Bounds b;
    // 遍历坦克集合。比较与其它坦克是否发生碰撞
    for (Tank tank : allTanks) {
        // 避免与自已比较
        if (!currentTank.equals(tank)) {
            // 得到 tank 的区域面
            b = getBoundsByTank(tank);
            // 判断当前坦克的 上左点p1 或 上右点p2 是否与 目标坦克区域面b 重叠
            if (isPointInBounds(p1, b) || isPointInBounds(p2, b)) {
                return true;
            }
        }
    }
    return false;
}

得到 tank 的区域面

getBoundsByTank(tank)

由于面是一个规则的矩形,由 左上(xy)、右上(x)、右下(y) 三个点即可确定位置

/**
* 获取坦克的区域面
* @param tank
* @return
*/
private Bounds getBoundsByTank(Tank tank){
    // 目标坦克的上左
    int op1_x = tank.getX();
    int op1_y = tank.getY();
    Point op1 = new Point(op1_x, op1_y);

    // 目标坦克的上右x
    int op2_x;
    // 目标坦克的下右y
    int op3_y;

    // 目标坦克方向
    int d = tank.getDirection();
    if (d == Constant.UP || d == Constant.DOWN) { // 上下移动:计算方法一致
        // 目标坦克的上右x
        op2_x = op1_x + 2 * Constant.TANK_WHEEL_WIDTH + Constant.TANK_BODY_WIDTH;
        // 目标坦克的下右y
        op3_y = op1_y + Constant.TANK_WHEEL_HEIGHT;
    } else { // 左右移动:计算方法一致
        // 目标坦克的上右x
        op2_x = op1_x + Constant.TANK_WHEEL_HEIGHT;
        // 目标坦克的下右y
        op3_y = op1_y + 2 * Constant.TANK_WHEEL_WIDTH + Constant.TANK_BODY_WIDTH;
    }
    // 目标坦克的区域面
    return new Bounds(op1, op2_x, op3_y);
}

判断 p1 是否与 目标坦克区域面 b 重叠

isPointInBounds(p1, b)

/**
 * 判断点 p 是否在面 b 内部
 *
 * @param p
 * @param b
 * @return
 */
private boolean isPointInBounds(Point p, Bounds b) {
    /*
    原理:
    用点 p ,分别与面 b 的四个值比较
    同时满足以下三个条件即表示点在面内,返回 true
    1) p 在面左上角的右下方
    2) p 在面右上角的左下方
    3) p 在面右下角的左上方
    */

    /*
     p 与 b 的左上角比较
     如果不返回 false,说明 p 在 矩形左上角 的右下方
     ●---------------
     |      |
     |      |
     |      |
     |      |
     --------
     |
     |

     */
    if (p.x < b.p1.x || p.y < b.p1.y) {
        return false;
    }

    /*
     p 与 b 的右上角 x 比较
     如果不返回 false ,说明 p 不在矩形的右方
     -------●
     |      |
     |      |
     |      |
     |      |
     --------
     |      |
     |      |

     */
    if (p.x > b.p2x) {
        return false;
    }

    /*
     p 与 b 的右下角 y 比较
     如果不返回 false ,说明 p 不在矩形的下方,可以确定 p 在 b 中(包括边界)
     --------
     |      |
     |      |
     |      |
     |      |
     -------●
     */
    if (p.y > b.p3y) {
        return false;
    }

    return true;
}

内部类:点、面

/**
 * 内部类:点
 */
class Point {
    int x;
    int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

/**
 * 内部类:面
 * 由于面是一个规则的矩形,由 左上(xy)、右上(x)、右下(y) 三个点即可确定位置
 * 右上点的 y 和 左上点的 y 相等
 * 右下点的 x 和 右上点的 x 相等
 * 左下点的 x 和 左上点的 x 相等
 * 左下点的 y 和 右下点的 y 相等
 */
class Bounds {
    // 左上
    Tank.Point p1;
    // 右上x
    int p2x;
    // 右下y
    int p3y;

    public Bounds(Tank.Point p1, int p2x, int p3y) {
        this.p1 = p1;
        this.p2x = p2x;
        this.p3y = p3y;
    }
}

7、生成一组不重复随机整数

实现原理:

利用 Set 集合不能存放重复元素的特性

用于初始化坦克的位置

package util;

import java.util.HashSet;
import java.util.Random;
import java.util.Set;

/**
 * @author ajun
 * Date 2021/7/14
 * @version 1.0
 * 工具类
 */
public class MyUitls {
    public static void main(String[] args) {
        Set<integer> set = getRandoms(-30, -20, -5);
        System.out.println("数量:" + set.size());
        for (Integer s : set) {
            System.out.println(s);
        }
    }

    /**
     * 生成一组不重复随机数
     *
     * @param start 开始位置:可以为负数
     * @param end   结束位置:end > start
     * @param count 数量 >= 0
     * @return
     */
    public static Set<integer> getRandoms(int start, int end, int count) {
        // 参数有效性检查
        if (start > end || count < 1) {
            count = 0;
        }
        // 结束值 与 开始值 的差小于 总数量
        if ((end - start) < count) {
            count = (end - start) > 0 ? (end - start) : 0;
        }

        // 定义存放集合
        Set<integer> set = new HashSet<>(count);
        if (count > 0) {
            Random r = new Random();
            // 一直生成足够数量后再停止
            while (set.size() < count) {
                set.add(start + r.nextInt(end - start));
            }
        }
        return set;
    }
}
//结果
数量:5
-25
-26
-27
-30
-23

8、统计数量时采用原子类

每一辆坦克、每一发炮弹都是一个线程,如果有两发炮弹同时摧毁两辆坦克,要想统计结果准确,就要保证线程安全性,这时采用原子类,效率高

// 摧毁敌方坦克数量
public volatile static AtomicInteger destroyEnemyTankNum = new AtomicInteger(0);
// 我方战毁数量
public volatile static AtomicInteger destroyMyNum = new AtomicInteger(0);
// 我方发射子弹数量
public volatile static AtomicInteger myBulletNum = new AtomicInteger(0);

// 摧毁敌方坦克数量加1
public static void destroyEnemyTankNumAdd() {
    destroyEnemyTankNum.getAndIncrement();
}

Gitee:韩顺平老师坦克大战优化版

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐