Bootstrap

利用Java Swing实现在线游戏盒子:弹弹堂游戏

盒子实现游戏🎮:

  • 🔸 推箱子:一款家喻户晓的益智小游戏,玩家控制搬运工上下左右移动,来将箱子推到指定地点。
  • 🔸 飞行射击:一款操纵飞行角色发射炮弹攻击敌机的小游戏。
  • 🔸 对对碰:一款经典的消除类游戏,玩家只要通过点击人物来使人物之间互相换位,连成 3 个以上的人物来消除得分。
  • 🔸 弹弹堂:一款回合制射击类竞技游戏,玩家双方发射炮弹攻击对方,直到一方血量为零。

JAVA程序设计小游戏对战平台,实现了注册、登陆、添加好友、聊天室(弹弹堂、推箱子、雷电、对对碰).zip资源-CSDN文库

1. 弹弹堂

1.1 弹弹堂游戏介绍

        一款回合制射击类竞技游戏🏇🏄,玩家双方发射炮弹攻击对方,直到一方血量为零,游戏结束。弹弹堂游戏原理实现灵感许多都是来自于以上的小游戏,例如:推箱子游戏中使用二维数组存储地图,弹弹堂也使用这一方法设定障碍地图;飞行射击游戏使用多线程实现背景滚动和子弹飞行,以及对对碰也使用了多线程来实现爆炸特效,这也用在了弹弹堂弹道绘制、角色动态和爆炸特效等方面。

1.2 弹弹堂游戏功能

        该游戏有两个模式:联机对战和单击模式。联机对战:通过游戏大厅的联网和好友在线对战;单击模式:不需要联网,可以选择双人对战,也可以选择和电脑 AI 对战。

        进入游戏后会显示菜单,有四个选项“双人对战”、“对战 AI”、“联机对战”、“退出游戏”,通过“↑”“↓”切换当前选项,“Enter”键确定选项。

       📍 游戏操作:

  •                 “←”、“→”控制角色左右移动
  •                 滑动鼠标控制瞄准角度
  •                 数字键 0~2 选择使用的道具

        点击鼠标控制炮弹发射的力度,按住的时间越久,炮弹发射的距离越远。在最下面的“力度”刻度条,会显示当前空格操作所产生的力度。当不小心按过了应有刻度时应持续按下直到 100 时会向回反刻度此时按得时间越久炮弹发射越近。

🎇 游戏界面及相关说明如下图:

游戏相关属性:

1.角色属性

每个角色有体力、生命值和攻击力三个属性。

每回合内,角色走动、使用道具都消耗一定的体力,体力耗尽无法走动或者使用道具。

2.时间限制

 每回合的限定时间是 30⏰ 秒,超过时限自动进入下一回合,切换玩家。

3.风力系统

每回合随机生成一个方向的风力🌀,风力会在水平方向上给子弹🔫施加一定的加速度,从而影响炮弹的弹道轨迹,因此需要玩家注意好控制发射力度和发射角度。

1.3 弹弹堂实现原理

[核心字段、方法、类](由于代码量过大,因此该游戏仅介绍关键部分)

public class DDtank extends JFrame
{
private RoomPanel roomPanel = new RoomPanel();
private final int frequency = 100; //刷新频率
private Timer timer = new Timer(10,new TimeListener());//定时器
 …
private double calAngle = 0; //用于计算的角度
private int power = 0; //投掷的力度
…
private double bulletAngle; //子弹的角度
private double bulletX, bulletY; //子弹的坐标
private double bulletVx, bulletVy; //子弹两个方向的速度
private final double accelerate = 10; //重力加速度
private boolean bulletFlying = false; //子弹是否在飞
private final double bias = 0.01; //偏差
private BufferedImage curBullet; //当前子弹的帧
…
private int turns = 0; //轮到角色的编号
private final int roleCnt = 2; //两个玩家
private Role roles[] = new Role[roleCnt];
…
private Maoyu maoyu; //定义一个 AI,名为毛玉
…
private final int maxWindForce = 5; //风力最大五级
private int curWindForce = 0; //当前的风力
…
private boolean aiMode; //是否为人机对战
private boolean netMode;//是否为联网对战
private int myTurn; //联网对战时玩家被分配的编号(0 或 1)
public static String strRead; //联网时接受到的 String
private String playerName; //联网时指定的对方名字
public DDtank() {}
public void changeTurns() {} //更换玩家
public void reinit() {} //重新初始化
private boolean crash(int objx1, int objy1, int objWidth1, int objHeight1, int 
objx2, int objy2, int objWidth2, int objHeight2) {}
private void parabola() {} //抛物线相关计算
private void loadMap() {} //加载地图
//一些监听
class MyKeyListener implements KeyListener {}
class MyMouseListener implements MouseListener {}
class MyMouseMotionListener implements MouseMotionListener {}
class TimeListener implements ActionListener{}
class RoomPanel extends JPanel{} //自定义面板类
…
private int calPower(double angle, int dx, int dy) //AI 计算力度
private void drawMenu(Graphics2D g2d){} //绘制菜单
public void emitString(String mode, String str){} //发送信息
private void parseMessage(){} //解析信息
private class Role{} //角色类
private class Walls{} //障碍类
private class Maoyu{} //名为 Maoyu 的 AI 类
private class SoundPlayer implements Runnable{} //播放音乐
private static class MapFactory{} //地图数据集
}

[具体实现](该游戏按照游戏逻辑分块介绍)

public class DDtank extends JFrame
和一个顶部面板:
private RoomPanel roomPanel = new RoomPanel()
所有的 UI 界面都是使用 RoomPanel paint() 函数进行绘制的。
动画特效的帧实现依靠定时器:
private Timer timer = new Timer(10,new TimeListener())
和其对应的监听事件:
class TimeListener implements ActionListener{
public void actionPerformed(ActionEvent e) {
…// 相关属性的计算
repaint();// 绘制游戏图形
}
}

游戏的整体逻辑:

         游戏开始运行之后,秒表开始倒计时,如果倒计时到 0 或者玩家发射的炮弹落地后,程序调用 changeTurns 切换玩家,changeTurns 会调用 reinit()重置角色的初始属性(不包括生命值),重新设定风力、重新初始化定时器等。直到有一方玩家生命值归零,游戏结束。

游戏动画逻辑:

        利用定时器 timer 和其监听事件,每隔 0.01 秒计算一次游戏的相关属性并进行重新绘图。在 actionPerformed 进行对:回合倒计时、背景切换、角色力度条、体力条、瞄准线、角色位置、炮弹在空中的坐标、是否切换玩家等属性的计算。最后调用 repaint()对上述属性进行绘制。达到每 10ms 刷新一次屏幕的效果,每次绘制不同的帧,以此绘制出动画效果。

角色核心属性:private class Role{}

int strength; //体力值
int health; //生命值
int damage; //攻击力
int x, y; //角色的坐标
int face; //角色的朝向,朝左为-1,朝右为 1
BufferedImage curImage; //当前的帧
BufferedImage personImage[]; //人物的动作图片
BufferedImage figure; //人物大图
public Role(int _id, int _x, int _y, boolean faceRight, int eI, int bI){}//人物的初始化
部分
public void moveLeft(){}//向左移动一步
public void moveRight(){} //向右移动一步
//角色使用道具(一共三种道具)
public void buff_0(){}
public void buff_1(){}
public void buff_2(){}
public void fire(){} //角色发射炮弹
public void motion(){} //人物动态

         角色可以进行左右移动、使用道具和发射炮弹。其中左右移动是靠 moveLeft和 moveRight 实现,每次移动都改变角色的当前帧 curImage 和角色的坐标,从而产生角色“走动”的视觉效果。使用道具 buff_x 会改变角色的攻击力等属性,但是这些属性都会在 reinit()中被重置回初始值。人物动态 motion 意思是长时间不操作角色,程序会让角色的当前帧 curImage 变成其他图片,换句话说,就是让角色产生其他的“小动作”,让角色看起来就是“活”的一样。角色的发射子弹详见下一段。

子弹的发射和飞行:

     角色发射子弹前可以:通过移动鼠标来调整子弹的发射角度:calAngle(通过 MyMouseMotionListener 实现),长按鼠标左键控制子弹的发射力度:power(通过 MyMouseListener 实现,这里设定点击一次即代表开始蓄力,蓄力发射后直到下一回合再次点击左键不能再次发射)。角度规定是角色水平朝向的0°到垂直90°,只能通过“转身”来完成向后发射子弹。另外,角色蓄力过程中不能移动。

        长按鼠标左键后松开,会调用角色相应的 fire()函数,实现角色发射子弹。fire()函数会设置:bulletFlying=true 表示子弹处于飞行状态,bulletX、bulletY 的值表示子弹的初始坐标,bulletVx、bulletVy 的值表示子弹的水平和垂直方向的初速度。这以后,之前提到的 actionPerformed 函数会检测到 bulletFlying==true,从而进行抛物线相关属性的计算(这会调用 parabola 进行相关计算)。

        parabola()方法进行抛物线和子弹有关属性的计算,包括子弹下一帧的水平、垂直方向速度,横纵坐标,还会计算子弹朝向和水平方向的角度 bulletAngle,从而据此选定相应的子弹图 curBullet。最后是调用 crash 方法检测子弹是否撞到人或障碍物。这里的 crash 原理和第二个游戏:飞行射击游戏中 crash 方法的原理一样,这里不再赘述。如果检测到碰到角色,则扣除相应角色的血量并检测剩余血量是否小于 0,如果小于 0 则游戏结束。

游戏 AI 原理:

这个游戏 AI 的计算核心无非是:根据两个坐标,如果在攻击范围内,给出正确的角度和力度。相关的类就是:

private class Maoyu
{
…//基本属性和角色类似
public Maoyu(int _x, int _y, int _xmin, int _xmax, int eI, int bi){}
public void motion(){}
public void AI(){}
public void fire(){}
}

除了 AI()方法,其他都和 Role 中的定义类似。如果碰到下图的情况(AI 就是右上角的小球,这里要的是根据角度 angle 计算小球攻击小人的力度):

为了简便,这里只把 45°和 135°纳入考虑,程序的实现如下:

public void AI()
{
int dx = x - roles[0].x + (roles[0].width / 2);
int dy = roles[0].y - y + roles[0].height;
if (dx > 0)
{
power = calPower(leftAngle, dx, dy);
calAngle = leftAngle;
}
else
{
power = calPower(rightAngle, dx, dy);
calAngle = rightAngle;
 }
fire();
}
private int calPower(double angle, int dx, int dy){
 ……
 double u = Math.abs(Math.sin(angle));
 double v = Math.abs(Math.cos(angle));
 if (dy > 0)
 for (power = 1; power < 100; power++){
 tmp = power*u + Math.sqrt(power*power*u*u+20*dy);
 tmp *= (power*v);
 if (Math.abs(tmp - Math.abs(10 * dx)) <= bias)
 return power;
 }
 ……//其他情况
}

游戏联网:

联网整体逻辑:玩家在菜单中选择“联机对战”选项后,输入对战对方的名字,发送“CONNECT@<对方玩家名字>@”给服务器后,等待服务器为两个玩家搭建连接以及分配角色编号(0或1)。玩家这边收到角色编号之后将其赋值给myTurn,并设置 netMode=true。

玩家发送信息通过 public void emitString(String mode, String str),接收信号则通过读取 public static String strRead 实现。

📰 信号协议如下:

  •  CONNECT@<对方玩家名字>@ //向服务器发送请求连接。
  •  DDTANK@<对方玩家名字>@<游戏操作> //表示发送到对方程序的操作。
  •  SCORE@<游戏名,这里为 DDTANK>@<玩家分数> //发给服务器统计分数。

如果当前对局轮到玩家(turns == myTurn),则每次玩家的鼠标移动、左右移动、道具使用、发射力度的信息编码成“<游戏操作>”通过 emitString 发送到服务器既而传达给对方玩家的程序。如果当前对局是轮到对方玩家(turns != myTurn),则该玩家的程序隔一段时间就读取一次 strRead 直到 strRead != null,然后调用parseMessage 解析其中的<游戏操作>,然后运行相关函数同步本机玩家的行为。

游戏地图🌏:

  • private class Walls{}定义了一个方块障碍的属性,包括是否可消除、是否已经被消除、所在的横纵坐标以及加载的图片等属性。
  • private static class MapFactory{} 的 功 能 和 第 一 个 游 戏 推 箱 子 中 的MapFactory 一样,都是存储地图数据集,不再赘述。
  • loadMap()方法能在游戏开始前加载地图,从而打印出不同的地图样式,如该游戏样例图中的“HELLO WORLD”的障碍形状。障碍分两种类型,在 MapFactory的三维数组 map 中,如果值为 1 代表该障碍可以被消除,如果为 2 代表不能消除,如果为 0 表示没有障碍。
;