Java修行实录:从零开始Java坦克大战(v2.0)
0. 新增功能介绍
- 修改背景为黑色
- 坦克可以根据WASD旋转方向,子弹的朝向也发生变化
- 添加墙体类wall,坦克和子弹的墙体的碰撞检测,子弹碰到墙体会消失,坦克碰到墙体会顶牛
- 自定义地图
- 添加敌方坦克类,敌方坦克移动策略和射击策略(敌方坦克子弹和墙体的碰撞检测还没有实现)
- 导入了经典游戏的图片素材
坦克游戏(v2.0)代码地址:x-Achan/TankGame at v2.0
https://github.com/x-Achan/TankGame/tree/v2.0

1. 枚举类 enum Direction
public enum Direction {
UP , DOWN , LEFT , RIGHT;
}
创建 enum Direction枚举类:UP、DOWN、LEFT、RIGHT,所有的坦克和子弹对象都只有四个移动方向,将四个方向放进Direction中
为坦克和子弹类都加上一个方向Direction dir
protected Direction dir;
protected 保护的枚举类Direction,在子弹类中调用 owner.dir程序不会出错,但是建议用getter方法owner.getDir()
对于敌军坦克EnemyTank类,不同于玩家坦克通过键盘控制移动,移动是随机的,出生时或发生碰撞时,都需要随机选择一个方向(即改变speedx和speedy的值),需要一个改变方向的函数changeDirtection()
枚举的一个核心用法:Direction.values(),返回Direction[](一个包含所有枚举常量的数组,顺序与声明一致)
// 改变方向:随机枚举一个方向,不同的方向有不同的speedx和speedy用来控制移动方向
public void changeDirection(){
Direction[] dirs = Direction.values(); // 返回所有枚举常量的数组
Direction newDir = dirs[random.nextInt(dirs.length)]; //从数组中取出一个变量
this.dir = newDir;
if(dir == Direction.UP){
speedx = 0;
speedy = -GameConfig.ENEMY_SPEED;
}else if(dir == Direction.DOWN){
speedx = 0;
speedy = GameConfig.ENEMY_SPEED;
}else if(dir == Direction.LEFT){
speedx = -GameConfig.ENEMY_SPEED;
speedy = 0;
}else if(dir == Direction.RIGHT){
speedx = GameConfig.ENEMY_SPEED;
speedy = 0;
}
}
2. 通过墙体类 Wall 创建地图
墙体不会移动,定义简单,整个地图的大小为800*600,坦克和墙体为20*20(地图的格子大小为:40*30,4:3的比例)
创建一个String的二维数组Map:
- 1 的位置表示墙体
- 0 的位置表示空地
- * 的位置表示敌军坦克
public static final String[] MAP_1 = {
"1111111111111111111111111111111111111111", // 0 (顶部加固边界)
"1111111111111111111111111111111111111111", // 1 (三个敌军固定出生点)
"10*0000000000000000*00000000000000000*01", // 2
"1011100111001110000000011100111001110001", // 3
"1011100111001110011110011100111001110001", // 4
"1011100111001110011110011100111001110001", // 5
"1011100111001110011110011100111001110001", // 6
"1000000*000000000000000000000*0000000001", // 7 (横向通道)
"1000000000000000000000000000000000000001", // 8
"1011111100011111100001111110001111110001", // 9 (长条掩体)
"1011111100011111100001111110001111110001", // 10
"1000000000000000001100000000000000000001", // 11
"1011100111111000001100000011111100111001", // 12
"1011100111111000000000000011111100111001", // 13
"10111000000*0000011110000000000000111001", // 14
"1011100000000000011110000000000000111001", // 15
"1011100111111000000000000011111100111001", // 16
"1011100111111000001100000011111100111001", // 17
"100000000000000000110000000000*000000001", // 18
"1011111100011111100001111110001111110001", // 19
"1011111100011111100001111110001111110001", // 20
"1000000000000000000000000000000000000001", // 21 (横向通道)
"1000000000000000*00000000000000000000001", // 22
"1011100111001110011110011100111001110001", // 23
"1011100111001110011110011100111001110001", // 24
"1011100111001110000000011100111001110001", // 25
"1000000000000000000000000000000000000001", // 26 (基地外围)
"1000000000000000000000000000000000000001", // 27 (老家顶部护甲)
"1000000000000000000000000000000000000001", // 28 (老家侧边护甲)
"1111111111111111111111111111111111111111" // 29 (底部加固边界)
};
遍历这个二维数组,扫描到1,就计算坐标,并将墙体加入到墙体列表中,同样扫描到坦克同理
for (int row = 0; row < GameConfig.MAP_1.length; row++) {
String line = GameConfig.MAP_1[row];
for (int col = 0; col < line.length(); col++) {
if(line.charAt(col) == '1'){
//计算墙体生成的位置
int wallX = col * GameConfig.WALL_SIZE;
int wallY = row * GameConfig.WALL_SIZE;
Wall w = new Wall(wallX,wallY);
//将墙体加入到墙体列表中
gameUI.wallList.add(w);
//绘制敌军坦克
}else if(line.charAt(col) == '*'){
int tankX = col * GameConfig.TANK_SIZE;
int tankY = row * GameConfig.TANK_SIZE;
EnemyTank enemyTank = new EnemyTank(tankX,tankY,Direction.DOWN);
gameUI.enemyTankList.add(enemyTank);
}
}
}
3. 碰撞检测
3.1 坦克和墙体
顶牛:当坦克进入墙体时,立刻将坦克恢复到安全合法的位置
3.1.1 玩家坦克
在每个游戏元素GameElement 的大类中,添加一个函数:getRect() 返回矩形函数
public Rectangle getRect(){
return new Rectangle(x,y,width,height);
}
Rectangle 表示一个矩形区域,由左上角坐标(x,y)和宽度和高度决定,提供的方法:intersects()相交、contains()包含、union(合并)等
用intersects()来判断是否碰撞(两个矩形是否相交)
遍历所有墙体,通过墙体和玩家坦克矩形相交检测,判断是否发生碰撞
gameUI.wallList.get(i).getRect().intersects(gameUI.playerTank.getRect())
如果碰撞,则顶牛
gameUI.playerTank.setX(gameUI.playerTank.getX() - gameUI.playerTank.getSpeedx());
gameUI.playerTank.setY(gameUI.playerTank.getY() - gameUI.playerTank.getSpeedy());
完整检测过程:
// 遍历所有墙体wall,判断是否和坦克发生碰撞
for (int i = 0; i < gameUI.wallList.size(); i++) {
// intersects() 方法检测两个矩形是否重叠
if(gameUI.wallList.get(i).getRect().intersects(gameUI.playerTank.getRect())){
System.out.println("发生碰撞了");
//顶牛:碰撞了,将刚刚加上的速度减回去(退回到碰撞前的坐标)
gameUI.playerTank.setX(gameUI.playerTank.getX() - gameUI.playerTank.getSpeedx());
gameUI.playerTank.setY(gameUI.playerTank.getY() - gameUI.playerTank.getSpeedy());
}
}
3.1.2 敌军坦克
敌军坦克首先是自主移动的,同样遍历每个墙体元素,遍历每个敌军坦克,判断是否有碰撞发生,如果发生碰撞,先实现顶牛,再调用EnemyTank的changeDirection()方法,随机改变方向
如果不先顶牛会怎么样? 问题:碰撞穿模

在敌军坦克和墙体的碰撞检测中,如果检测到碰撞,无顶牛,就直接让坦克转向,代码如下
for (int i = 0; i < gameUI.wallList.size(); i++) {
for (int j = 0; j < gameUI.enemyTankList.size(); j++){
EnemyTank et = gameUI.enemyTankList.get(j);
if(gameUI.wallList.get(i).getRect().intersects(et.getRect())){
System.out.println("敌军坦克撞墙");
et.changeDirection();
}
}
}
分析:坦克进入墙体后执行转向,但是转向并不会改变坦克的位置坐标(坦克还在墙体里),转向只是修改了speedx和speedy的值,没有改变x和y的值,所以坦克根据新的方向执行移动move(),坦克有可能还是卡在墙体里,下一次碰撞检测发现坦克还是在墙体里,如此循环,只要坦克有一帧在墙里,每一帧都会强制改变方向,从而造成屏幕效果:坦克冲进墙里,像触电一样疯狂改变方向,彻底卡在墙里出不来了
在改变方向前,先顶牛,先让坦克回到墙体外,安全的执行改变方向,不会卡墙
enemy.setX(enemy.getX() - enemy.getSpeedx());
enemy.setY(enemy.getY() - enemy.getSpeedy());
关键就是:让改变方向时,确保坦克在一个安全的位置(在墙里改变方向,就会死循环)

开发经验:先解决空间重叠,再处理逻辑状态:第一优先级,永远是把对象退回到合法位置
完整过程:
for (int i = 0; i < gameUI.wallList.size(); i++) {
for (int j = 0; j < gameUI.enemyTankList.size(); j++){
EnemyTank et = gameUI.enemyTankList.get(j);
if(gameUI.wallList.get(i).getRect().intersects(et.getRect())){
System.out.println("敌军坦克撞墙");
// 先让坦克回到安全位置,再改变方向
et.setX(et.getX() - et.getSpeedx());
et.setY(et.getY() - et.getSpeedy());
et.changeDirection(); //改变方向
}
}
}
3.2 玩家子弹和墙体
在GameUI中维护一个玩家子弹的列表 bulletList,遍历所有墙体和子弹列表,检测是否发生碰撞,如果碰撞,则让子弹死亡,isAlive = flase,
//循环判断每个墙体是否和子弹碰撞
for (int i = 0; i < gameUI.wallList.size(); i++) {
for (int j = 0; j < gameUI.bulletsLsit.size(); j++) {
Bullet bullet = gameUI.bulletsLsit.get(j);
if(gameUI.wallList.get(i).getRect().intersects(bullet.getRect())){
System.out.println("子弹和墙体发生了碰撞!");
bullet.setAlive(false);
}
}
}
在后续的子弹移除,会检测到死亡的子弹会被移除 bulletList
用子弹和墙体碰撞的过程为例,总结一下Java中的对象与引用
方案A:无意义的对象创建+内存浪费
new Bullet(),在堆内存中开辟一块新的内存空间,返回其引用(然后执行构造函数将基础的变量存进去)
bullet只是一个栈上的引用变量,存储对象的地址,
赋值操作 = 只是改变了引用指向,不复制对象
刚才new的Bullet对象,失去了所有引用,成为了垃圾对象(无用内存)
IDEA中,new Bullet(gameUI.playerTank)这段代码会变成灰色,是IEDA提醒这段代码是没必要的,但是实际执行时还会执行
for (int i = 0; i < gameUI.wallList.size(); i++) { for (int j = 0; j < gameUI.bulletsLsit.size(); j++) { Bullet bullet = new Bullet(gameUI.playerTank); bullet = gameUI.bulletsLsit.get(j); if(gameUI.wallList.get(i).getRect().intersects(bullet.getRect())){ System.out.println("子弹和墙体发生了碰撞!"); bullet.setAlive(false); } } }方案B(最优方案):高效引用复用
创建一个新的引用 bullet,直接引用已有对象,没有额外内存分配,代码可读性高
局部变量bullet在栈上,比访问gameUI.bulletList.get(i)寻址更快,提高访问速度
for (int i = 0; i < gameUI.wallList.size(); i++) { for (int j = 0; j < gameUI.bulletsLsit.size(); j++) { Bullet bullet = gameUI.bulletsLsit.get(j); if(gameUI.wallList.get(i).getRect().intersects(bullet.getRect())){ System.out.println("子弹和墙体发生了碰撞!"); bullet.setAlive(false); } } }方案C:复用方法调用,可读性差
相比方案B,访问 gameUI.wallList.get(i) 比访问 bullet 更慢,代码冗长,易出错,违反了DRY原则(Don't Repeat Yourself)
for (int i = 0; i < gameUI.wallList.size(); i++) { for (int j = 0; j < gameUI.bulletsLsit.size(); j++) { if(gameUI.wallList.get(i).getRect().intersects(gameUI.bulletsLsit.get(j).getRect())){ System.out.println("子弹和墙体发生了碰撞!"); gameUI.bulletsLsit.get(j).setAlive(false); } } }
4. 敌军坦克 EnemyTank 类
4.1 改变方向changeDirection方法
前面第1节已经介绍,在敌军坦克的碰撞到墙体时,需要随机转向
坦克和子弹不会斜着走,代表speedx和speedy两个方向,同一时间有一个为0
// 改变方向:随机枚举一个方向,不同的方向有不同的speedx和speedy用来控制移动方向
public void changeDirection(){
Direction[] dirs = Direction.values(); // 返回所有枚举常量的数组
Direction newDir = dirs[random.nextInt(dirs.length)]; //从数组中取出一个变量
this.dir = newDir;
if(dir == Direction.UP){
speedx = 0;
speedy = -GameConfig.ENEMY_SPEED;
}else if(dir == Direction.DOWN){
speedx = 0;
speedy = GameConfig.ENEMY_SPEED;
}else if(dir == Direction.LEFT){
speedx = -GameConfig.ENEMY_SPEED;
speedy = 0;
}else if(dir == Direction.RIGHT){
speedx = GameConfig.ENEMY_SPEED;
speedy = 0;
}
}
4.2 子弹射击 fire()方法
每一个敌军坦克都有自己的 bulletList子弹列表
public ArrayList<Bullet> bulletList = new ArrayList<>();
fire()会添创建一个子弹,添加到子弹列表中
public void fire(){
Bullet bullet = new Bullet(this);
bulletList.add(bullet);
}
在绘制敌军坦克时,会有2%的概率调用坦克的fire()
用random.nextInt(100) < 2 控制概率
for (int i = 0; i < gameUI.enemyTankList.size(); i++) {
//取出每一个坦克的子弹列表,然后移动和绘制子弹
//每移动一步,只有2%的概率开火
if(random.nextInt(100) < 2){
gameUI.enemyTankList.get(i).fire();
}
gameUI.enemyTankList.get(i).move();
gameUI.enemyTankList.get(i).draw(bg);
}
绘制敌军坦克的子弹,依旧遍历
// 绘制敌军坦克的子弹
for (int i = 0; i < gameUI.enemyTankList.size(); i++) {
//取出每一个坦克的子弹列表,然后移动和绘制子弹
for (int j = 0; j < gameUI.enemyTankList.get(i).bulletList.size(); j++) {
Bullet b = gameUI.enemyTankList.get(i).bulletList.get(j);
b.move();
b.draw(bg);
}
}
5. 子弹类的构造函数
子弹的发射位置、移动轨迹和方向都是在发射的一刻就决定了
所以在构造函数中:
- 发射位置:x和y的初值
- 移动轨迹和方向:dir、speedx,speedy
这些值需要在构造函数中直接赋值,其中dir就是owner发射子弹那一刻的方向
speedx和speedy是要根据dir来决定值,只有四种方向
public Bullet(Tank owner){
super();
this.owner = owner;
//在创建的那一刻,就已经将owner的坐标值取了出来
this.width = GameConfig.BULLET_SIZE;
this.height = GameConfig.BULLET_SIZE;
//获取 owner的初始方向,将初始方向保存
this.dir = owner.getDir();
//子弹的 移动方向 和 发射位置 在出生的时候就已经决定了 (在创建的那一刻,就已经将owner的坐标值取了出来)
if(dir == Direction.UP){
this.x = owner.getX();
this.y = owner.getY() - this.height;
this.speedx = 0;
this.speedy = -GameConfig.BULLET_SPEED;
}else if(dir == Direction.DOWN){
this.x = owner.getX();
this.y = owner.getY() + this.height;
this.speedx = 0;
this.speedy = GameConfig.BULLET_SPEED;
}else if(dir == Direction.LEFT){
this.x = owner.getX() - this.width;
this.y = owner.getY();
this.speedx = -GameConfig.BULLET_SPEED;
this.speedy = 0;
}else if(dir == Direction.RIGHT){
this.x = owner.getX() + this.width;
this.y = owner.getY();
this.speedx = GameConfig.BULLET_SPEED;
this.speedy = 0;
}
}
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)