一、项目介绍
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();
}