实现的游戏功能及思路
弹弹堂曾经是我非常喜欢玩的一款游戏,前段时间打算写一个线程游戏,于是萌生了写一个模拟弹弹堂的游戏的想法。但由于技术有限,只做到了部分还原。目前具有的功能有:小地图的拖拽,倒计时,发射炮弹(调整角度和力度等),人物的左右移动,游戏道具(如增加攻击次数,增加炮弹威力),有人物的血量和体力值。不足之处在于无法将人物沿着游戏地图运动以及没有炮弹挖坑效果,此外,风力和怒气值满了之后放大招的功能暂时未添加。
由于弹弹堂的游戏特性,我们需要两张缓冲图片,一张为整个游戏背景(由于面板处于固定位置,要想看到背景中不同的场景需要改变游戏背景绘制的位置。比如想看到右边的场景,则需将游戏背景向左移动。),人物与炮弹等都在游戏背景上(可以随着它一起移动),另一张有:小地图、倒计时、人物血量、蓄力槽、体力值等(相对于屏幕在固定的位置)。然后将两张图片按先后顺序绘制在另一张缓冲图片上,最后在绘制在面板上。这个过程采用一个线程实现。
Java线程小游戏(模拟弹弹堂)
小地图和倒计时
接下来讲讲几个主要的功能实现。首先考虑小地图的功能,在小地图内有一个移动框,通过拖拽移动框来控制需要观看的区域(改变游戏背景的位置)。移动框与小地图的相对位置通过游戏背景与可见区域的相对位置按比例确定。
private int map_x, map_y, map_w, map_h;//小地图的属性
private int move_x, move_y, move_w, move_h;//移动框的属性
private int beforeMove_x, beforeMove_y, now_x, now_y;//游戏背景的位置属性
private int x0, y0, x1, y1;//鼠标操作的坐标
//以下的main_Image代表游戏背景,由于要将游戏背景画在可见的区域内,它的位置相对可见区域的位置为负数
@Override
public void mousePressed(MouseEvent e) {
x0 = e.getX();
y0 = e.getY();
if (x0 > map_x && x0 < map_x + map_w && y0 > map_y && y0 < map_y + map_h) {
//鼠标按下的位置在小地图的区域内
if (x0 > map_x + move_x && x0 < map_x + move_x + move_w && y0 > map_y + move_y
&& y0 < map_y + move_y + move_h) {
//鼠标按下的位置在移动框内,不需要进行任何操作
} else {
//鼠标按下的位置不在移动框内,则需要将移动框移动至以当前位置为中心
if (x0 < map_x + move_w / 2) {
//若鼠标按下的位置过于边缘,为了防止游戏背景超出范围,设置它的位置
now_x = 0;
} else if (x0 > map_x + map_w - move_w / 2) {
//同上
now_x = -(main_Image.getWidth() - center.getWidth());
} else {
//若鼠标按下的位置在合适的位置,则以当前位置为中心,设置游戏背景的位置
now_x = -(x0 - move_w / 2 - map_x) * main_Image.getWidth() / map_w;
}
main_Image.setX(now_x);
if (y0 < map_y + move_h / 2) {
now_y = 0;
} else if (y0 > map_y + map_h - move_h / 2) {
now_y = -(main_Image.getHeight() - center.getHeight());
} else {
now_y = -(y0 - move_h / 2 - map_y) * main_Image.getHeight() / map_h;
}
main_Image.setY(now_y);
}
//设置完位置后记录下当前的游戏背景位置,用于计算后面拖拽后的位置
beforeMove_x = main_Image.getX();
beforeMove_y = main_Image.getY();
}
}
@Override
public void mouseDragged(MouseEvent e) {
//移动框移动的距离与游戏背景移动的距离的比例
int move_rateX = main_Image.getWidth() / map_w;
int move_rateY = main_Image.getHeight() / map_h;
x1 = e.getX();
y1 = e.getY();
//根据移动框移动的距离计算游戏背景的位置
now_x = beforeMove_x - move_rateX * (x1 - x0);
now_y = beforeMove_y - move_rateY * (y1 - y0);
//防止超出范围,center为可见区域
if (now_x > 0) {
now_x = 0;
} else if (now_x < -(main_Image.getWidth() - center.getWidth())) {
now_x = -(main_Image.getWidth() - center.getWidth());
}
main_Image.setX(now_x);
if (now_y > 0) {
now_y = 0;
} else if (now_y < -(main_Image.getHeight() - center.getHeight())) {
now_y = -(main_Image.getHeight() - center.getHeight());
}
main_Image.setY(now_y);
}
倒计时的功能考虑使用另一个线程来实现,在run方法中使用while循环,每隔一定的时间间隔将时间减一,当时间为零时则重置。同时设置有时间暂停和开始的方法,当开始蓄力时便暂停,回合结束便重新开始。
人物的移动与炮弹的运动
要实现控制人物移动和炮弹的运动,则需引入速度这一属性。因为炮弹运动是一条抛物线,所以还需要引入重力加速度这一属性。游戏线程的run方法中的while循环在一遍一遍地绘制游戏元素,此时执行人物对象和炮弹对象的move方法即可改变它们的位置,实现运动的效果。接着再执行accelerate方法,改变炮弹的垂直速度。
private int x, y;//位置
private double vx, vy;//速度
private double gravity=2.4;//重力加速度
public void move() {
x += vx;
y += vy;
}
public void accelerate() {
vy += gravity;
}
随着炮弹速度方向的改变,炮弹的图片也要进行相应的旋转。这里用到了Graphics2D中的rotate方法
int bullet_center_x = bullet.getX() + bullet.getWidth() / 2;
int bullet_center_y = bullet.getY() + bullet.getHeight() / 2;
if (bullet.getVx() > 0) {//向右运动
//炮弹速度方向与水平方向的夹角
double angle = Math.atan(-bullet.getVy() / bullet.getVx());
//以炮弹图片中心为旋转点,旋转相应的角度
main_g.rotate(Math.PI / 2 - angle, bullet_center_x, bullet_center_y);
//炮弹对象里的绘制方法,main_g为游戏背景的画布
bullet.draw(main_g);
//绘制完图片后旋转回去,避免影响其他元素的绘制
main_g.rotate(angle - Math.PI / 2, bullet_center_x, bullet_center_y);
} else {
double angle = Math.atan(bullet.getVy() / bullet.getVx());
main_g.rotate(angle - Math.PI / 2, bullet_center_x, bullet_center_y);
bullet.draw(main_g);
main_g.rotate(Math.PI / 2 - angle, bullet_center_x, bullet_center_y);
}
镜头的捕捉效果
当人物或炮弹运动时,游戏背景要随着它们运动,这样能保证它们处于屏幕的中间。若是人物或炮弹在游戏背景中的位置处于边缘,则游戏背景暂时不动,等到它们运动到适当的位置再进行跟随。
//此方法的功能为捕捉role对象
public void catchRole(GameRole role) {//GameRole为一个类,游戏背景,人物和炮弹都是GameRole的对象
//此方法与移动小地图时的方法类似,都是保证游戏背景移动不超出范围,同时以某个地方为中心来设置位置
int x, y;
if (role.getX() <= (center.getWidth() / 2 - role.getWidth() / 2)) {
x = 0;
} else if (role.getX() >= (main_Image.getWidth() - center.getWidth() / 2 - role.getWidth() / 2)) {
x = -(main_Image.getWidth() - center.getWidth());
} else {
x = -(role.getX() + role.getWidth() / 2 - center.getWidth() / 2);
}
main_Image.setX(x);
if (role.getY() <= (center.getHeight() / 2 - role.getHeight() / 2)) {
y = 0;
} else if (role.getY() >= (main_Image.getHeight() - center.getHeight() / 2 - role.getHeight() / 2)) {
y = -(main_Image.getHeight() - center.getHeight());
} else {
y = -(role.getY() + role.getHeight() / 2 - center.getHeight() / 2);
}
main_Image.setY(y);
}
炮弹的击中目标效果
当炮弹击中目标或飞出游戏背景时,要根据情况进行相应的处理。若未使用游戏道具则默认炮弹发射一次,此时若飞出游戏背景或击中目标则回合结束,击中目标时目标需扣除相应的血量。若使用了增加攻击次数的游戏道具,则将之前发射时记录的炮弹信息重新设置并发射。在这里我想讲讲我对炮弹击中目标的判定(暂时没想到其他算法)。
首先获得炮弹中心的坐标,用二重循环与目标图片的每一个像素点的位置进行对比,若有相同的,即击中目标,同时退出循环。这种算法的缺陷在于判定不够精确。
public boolean collision(GameRole role) {
int x = role.getX();
int y = role.getY();
int width = role.getWidth();
int height = role.getHeight();
int bulletX = bullet.getX() + bullet.getWidth() / 2;
int bulletY = bullet.getY() + bullet.getHeight() / 2;
for (int i = x; i < x + width; i++) {
for (int j = y; j < y + height; j++) {
if (bulletX == i && bulletY == j) {
return true;
}
}
}
return false;
}
游戏道具
游戏道具的信息采用一个列表存储,每次绘制时遍历链表,对比它们的属性与游戏中的体力来决定怎样绘制。
for(int i=0;i<buff_east.size();i++) {
BulletBuff buff=buff_east.get(i);
if(buff.getEnergyNeed()<=energy) {
buff.drawNormal(g, buff_east_x, buff_east_y+i*buff_size, buff_size);
}else {
buff.drawGray(g, buff_east_x, buff_east_y+i*buff_size, buff_size);
}
}
道具被使用后,需要在屏幕中心显示
for(int i=0;i<buff_south.size();i++) {
BulletBuff buff=buff_south.get(i);
buff.drawNormal(g, buff_south_x+i*buff_size, buff_south_y, buff_size);
}
而当回合结束时,屏幕中心显示的道具需要被移除。
public void cleanBuff() {
buff_south.removeAll(buff_south);
}
个人感受
这个项目前前后后修改了很多个版本,也重写了几遍,希望能使代码的逻辑性更强,可读性更好,同时也更加简洁。这其中也遇到了很多问题,最主要的问题在于操作人物和发射炮弹的一些细节上,此外还有倒计时的控制。在一些极端的情况下,会导致游戏无法正常的运行下去。在重写的过程中,不断地尝试不同的方法避免这些问题,也让我体会到,要做到逻辑严谨不出现漏洞需要对各方面进行考虑,这并不是一件容易的事。
完整代码与游戏素材
以下是完整的代码与素材,将所有内容放入同一个包内便可运行(有兴趣的读者可进行尝试)
链接:https://pan.baidu.com/s/1J5JbM37UAtyaBZVs_h4pQA
提取码:n43x