本篇基于 Java Swing 从零开发飞机躲避障碍物小游戏,主要修复上一版本遗留的各类 bug,完善护盾、清屏、计时、延时生成障碍物功能,记录开发过程遇到的各类真实坑点。适合 Swing 图形界面初学者阅读。
上期回顾
在上一篇博文中,我们搭建飞机躲避障碍物游戏的基础框架,完成窗体与绘图面板搭建,实现鼠标移动控制飞机位置,使用鼠标滚轮完成子弹发射;完成基础的障碍物逻辑,封装Obstacle、Bullet实体类,实现子弹与障碍物的基础碰撞。
本章功能实现
本篇我们将进一步完善飞机躲避障碍物游戏,实现以下核心功能:
- 障碍物动态移动:实现障碍物持续向下移动,增加游戏挑战性。
- 新增道具系统:引入三种功能道具------护盾、霰弹(火力增强)和清屏。
- 护盾伤害机制 :
- 护盾可为飞机抵挡一次障碍物撞击。
- 撞击后护盾自动消失。
- 若未装备护盾时撞击障碍物,则游戏直接结束。
- 清屏道具逻辑优化 :
- 拾取清屏道具后,屏幕内所有障碍物立即消失。
- 障碍物滑出屏幕底部后会自动复活并重置位置,确保游戏过程中障碍物持续生成,不会中断。
- 精准游戏计时系统 :
- 采用系统时间戳替代帧计数进行游戏计时。
- 从根本上解决因丢帧导致的计时不准问题。
- 实时更新游戏界面中的时间显示标签。
- 代码重构与优化 :
- 复用已封装好的飞机-障碍物碰撞检测方法,消除代码冗余。
- 遵循面向对象封装思想,提升代码可维护性。
- 道具系统完整闭环 :
- 确保护盾、清屏、火力增强三种道具均能正常生效。
- 火力增强效果持续5秒,期间子弹呈现散射效果。
- 道具移出屏幕边界后自动消失,避免内存无效占用。
以下为已处理的道具图片,大家要用自行取用~



本期使用的类有:
Main(程序入口),GameListener(监听器),DrawPanel(绘制),Obstacles(障碍物),Bullet(子弹),Item(道具)
1.Main类的程序逻辑
功能:设计窗体来承载游戏,有窗体的大小,标题等;同时设置窗体的性质:是否允许拖拽来改变大小,关闭窗体的形式等;同时为窗体添加上各种组件:监听器,绘图面板等
·创建showUI方法,用于实现上述功能:
java
public void showUI(){
//基本显示
//导入JFrame对于的包
JFrame jf=new JFrame("飞行器游戏");
//设置窗体大小
jf.setSize(600,800);
//设置窗体大小为"不可更改"
jf.setResizable(false);
//窗体居中显示
jf.setLocationRelativeTo(null);
//设置窗体关闭方式
jf.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
//设置绝对化布局
jf.setLayout(null);
//将我们DrawPanel类初始化
DrawPanel drawPanel=new DrawPanel();
//设置JPanel的大小(覆盖整个页面)
drawPanel.setBounds(0,0,600,800);
//将DrawPanel添加到窗体上
jf.add(drawPanel);
//将我们的监听器类(GameListener)初始化
GameListener gameListener=new GameListener(drawPanel);
//将监听器添加到窗体上
jf.addMouseMotionListener(gameListener);
//设置窗体可见
jf.setVisible(true);
}
}
·建立程序入口,调用showUI方法
java
static void main() {
Main main=new Main();
main.showUI();
}
此时,我们运行起来是这样的:

2.设置Obstacles类与Bullet类
先来看一个较简单的Bullet类:
·设置变量
java
public int x,y,speedX;
public int width=8,height=16;
public boolean isAlive;
这个speedX是我新增的变量,用于控制子弹在水平方向的移动速度
比如说:
当speedX=0:子弹水平方向不偏移,竖直向上飞
当speedX=-6:x方向不断减少,子弹向左上方飞行
先说一下,这个与我们后来的霰弹功能有关
·创建Bullet类,设置变量数据的传入
java
public Bullet(int x, int y, int speedX){
this.x=x;
this.y=y;
this.speedX=speedX;
isAlive=true;
}
·创建子弹移动(move)的方法
java
public void move(){
x += speedX;
y -= 12;
}
}
子弹图片:

接下来是 Obstacle 类的核心逻辑
Obstacle 类定义了游戏中障碍物的基本属性和行为。以下是其核心代码实现:
设置变量
障碍物的坐标(x,y)与障碍物的大小,状态(已击碎/未击碎)
java
public int x,y;
public int width,height;
public boolean isAlive;
构造函数 :初始化障碍物的位置、大小,并默认将其状态设为"存活"(isAlive=true)。
java
public Obstacle(int x, int y, int width, int height){
this.x=x;
this.y=y;
this.width=width;
this.height=height;
isAlive=true;
}
move方法 :根据传入的 speed 参数,使障碍物沿 Y 轴(垂直方向)向下移动,实现持续下落的效果。
java
public void move(int speed){
y += speed;
}
isOutOfScreen方法:判断障碍物是否已移出屏幕底部(Y坐标大于800),为后续的"复活重置"逻辑提供条件。
java
public boolean isOutOfScreen(){
return y > 800;
}
reset方法 :这是本期实现"障碍物源源不断生成"的关键。当障碍物移出屏幕后,此方法会将其重置到屏幕顶部上方(y = -40),并赋予一个新的 X 坐标,然后重新激活。这避免了频繁创建新对象,优化了性能。
java
//障碍物重置复活,飞到屏幕顶端外面,给新x坐标
public void reset(int newX){
this.y = -40;
this.x = newX;
this.isAlive = true;
}
障碍物图片:

3. Item 道具类代码逻辑
Item 类负责管理游戏中的三种道具:护盾、清屏和火力增强。它定义了道具的属性、移动、绘制以及碰撞检测等核心行为。
成员变量 :
* x, y: 道具在屏幕上的坐标。
* width, height: 道具的尺寸,默认均为 32 像素。
* type: 道具类型(1-护盾,2-清屏,3-火力增强),用于区分不同功能。
* isAlive: 道具是否处于活动状态(是否可被拾取)。
java
public class Item {
public int x;
public int y;
public int width=32;
public int height=32;
// 1护盾 2清屏 3火力加强
public int type;
public boolean isAlive;
构造函数Item(int x, int y, int type) :
初始化道具的位置、类型,并默认将其状态设为"存活"(isAlive=true)。
java
public Item(int x,int y,int type){
this.x=x;
this.y=y;
this.type=type;
isAlive=true;
}
** move()方法 **:
每帧调用,使道具以固定速度(y += 4)向下移动,模拟从屏幕上方掉落的效果。
java
//道具向下移动
public void move(){
y +=4;
}
isOutOfScreen()方法 :
判断道具是否已移出屏幕底部(Y坐标大于800)。若移出,则应在逻辑层将其标记为失效或移除,以优化性能。
java
//判断飞出屏幕底部
public boolean isOutOfScreen(){
return y>800;
}
draw(Graphics g, Image shieldImg, Image clearImg, Image fireImg)方法 :
根据 type 的值,使用 switch 语句选择对应的图片进行绘制。
仅在道具存活(isAlive 为 true)时执行绘制,避免绘制无效道具。
java
//绘制道具,接收三张道具图片
public void draw(Graphics g,Image shieldImg,Image clearImg,Image fireImg){
if(!isAlive) return;
switch (type){
case 1:
g.drawImage(shieldImg,x,y,width,height,null);
break;
case 2:
g.drawImage(clearImg,x,y,width,height,null);
break;
case 3:
g.drawImage(fireImg,x,y,width,height,null);
break;
}
}
checkHit(int planeX, int planeY, int planeW, int planeH)方法 :
检测道具与飞机(用一个矩形区域表示)是否发生碰撞。
采用轴对齐包围盒(AABB) 算法进行矩形碰撞检测,逻辑清晰高效。
同样,只在道具存活时进行检测,避免与已失效的道具发生交互。
java
//检测飞机和道具碰撞
public boolean checkHit(int planeX,int planeY,int planeW,int planeH){
if(!isAlive) return false;
boolean hit = (planeX < x+width)
&&(planeX+planeW >x)
&&(planeY < y+height)
&&(planeY+planeH>y);
return hit;
}
}
设计要点:
- 类型驱动 :通过
type字段区分道具,使draw方法能动态切换显示,逻辑与表现分离。 - 状态管理 :
isAlive字段有效管理了道具的生命周期,避免对无效对象进行不必要的计算和绘制。 - 通用碰撞检测 :
checkHit方法参数化,可复用检测飞机与任何矩形物体的碰撞。
4. DrawPanel 类的代码逻辑
DrawPanel 类是游戏的核心绘制与逻辑控制类,继承自 JPanel,负责管理游戏状态、绘制所有游戏元素、处理碰撞检测以及道具系统等核心功能。
java
import javax.sound.sampled.*;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Random;
public class DrawPanel extends JPanel{
// 游戏核心变量
public int obstacleSpeed=20; // 障碍物移动速度
public int destroyCount=0; // 摧毁障碍物计数,用于难度递增
// 飞机相关变量
public int planeX,planeY; // 飞机坐标
public int planeW=30,planeH=30; // 飞机尺寸
public int gameState=0; // 游戏状态:0-开始界面,1-游戏中,2-游戏结束
// 图片资源
private final Image bgImage; // 背景图片
private final Image planeImage; // 飞机图片
private final Image themeImage; // 开始界面主题图片
private final Image bulletImage; // 子弹图片
private final Image obstacleImage; // 障碍物图片
// 游戏数据与UI组件
public int score=0; // 游戏得分
public int time=0; // 游戏时间
private JLabel scoreLabel; // 积分显示标签
private JLabel timeLabel; // 时间显示标签
// 障碍物系统
Obstacle[] obsArray; // 障碍物数组
int obsCount=8; // 障碍物数量
// 子弹系统
Bullet[] bulletArray; // 子弹数组
int bulletMaxCount=20; // 最大子弹数量
int bulletIndex; // 当前子弹索引
Random random; // 随机数生成器
private Clip bgMusic; // 背景音乐
//=============道具相关新增变量================
public ArrayList<Item> itemList=new ArrayList<>(); // 道具列表
public boolean hasShield=false; // 是否拥有护盾
public boolean fireBoost=false; // 是否处于火力增强状态
public long fireBoostEndTime=0; // 火力增强结束时间
public long lastItemSpawnTime=0; // 上次生成道具的时间
public long lastClearItemSpawnTime=0; // 上次生成清屏道具的时间
private Image shieldImg; // 护盾道具图片
private Image clearImg; // 清屏道具图片
private Image fireImg; // 火力增强道具图片
public long LastItemSpawnTime=0; // 上次生成道具的时间(备用变量)
加载背景音乐
从指定路径加载WAV音频文件并准备播放
java
public void loadMusic(){
try {
File file = new File("且听风吟 - 轻音乐网.wav");
AudioInputStream ais = AudioSystem.getAudioInputStream(file);
bgMusic = AudioSystem.getClip();
bgMusic.open(ais);
bgMusic.loop(Clip.LOOP_CONTINUOUSLY); // 设置循环播放
bgMusic.stop(); // 初始状态为停止
} catch (UnsupportedAudioFileException | IOException | LineUnavailableException e) {
e.printStackTrace();
System.out.println("音乐加载失败!检查桌面wav文件");
}
}
播放背景音乐
从开头开始播放,如果音乐对象存在
java
public void playBgm(){
if(bgMusic!=null){
bgMusic.setFramePosition(0); // 从头开始
bgMusic.start();
}
}
停止背景音乐
java
public void stopBgm(){
if(bgMusic!=null){
bgMusic.stop();
}
}
DrawPanel构造函数 - 初始化游戏面板
设置初始参数、加载资源、创建UI组件
java
public DrawPanel(){
//初始化飞机起始位置,避免开局飞到左上角
planeX=250;
planeY=600;
// 加载所有游戏图片资源
bgImage=new ImageIcon("background.jpg").getImage();
planeImage=new ImageIcon("plane.png").getImage();
themeImage=new ImageIcon("theme.jpg").getImage();
bulletImage=new ImageIcon("bullet.png").getImage();
obstacleImage=new ImageIcon("obstacle.png").getImage();
// 加载道具图片
try {
shieldImg = new ImageIcon("protect.png").getImage();
clearImg = new ImageIcon("sweep.png").getImage();
fireImg = new ImageIcon("fire.png").getImage();
}catch (Exception e){
System.out.println("道具图片加载失败,请检查桌面图片文件名");
e.printStackTrace();
}
this.setLayout(null); // 使用绝对布局
// 创建并配置积分标签
scoreLabel=new JLabel("积分:0");
scoreLabel.setFont(new Font("黑体",Font.BOLD,20));
scoreLabel.setBounds(20,50,150,30);
scoreLabel.setForeground(Color.WHITE);
this.add(scoreLabel);
// 创建并配置时间标签
timeLabel=new JLabel("时间:0");
timeLabel.setFont(new Font("黑体",Font.BOLD,20));
timeLabel.setForeground(Color.white);
timeLabel.setBounds(420,50,150,30);
this.add(timeLabel);
// 创建开始游戏按钮
JButton gameStart=new JButton("开始游戏");
gameStart.setBounds(230,490,140,40);
this.add(gameStart);
random=new Random();
obsArray=new Obstacle[obsCount];
//初始化障碍物,全部设置为死亡,游戏开始再激活
for (int i=0;i<obsCount;i++){
int w=40,h=40,rx=random.nextInt(50,560),ry= random.nextInt(70,640);
obsArray[i]=new Obstacle(rx,ry,w,h);
obsArray[i].isAlive = false; // 初始状态为死亡
}
bulletArray=new Bullet[bulletMaxCount]; // 初始化子弹数组
// 创建游戏监听器并注册事件
GameListener gameListener=new GameListener(this);
gameStart.addActionListener(gameListener); // 按钮点击事件
this.addMouseWheelListener(gameListener); // 鼠标滚轮事件(发射子弹)
this.addMouseMotionListener(gameListener); // 鼠标移动事件(控制飞机)
loadMusic(); // 加载背景音乐
}
障碍物移动逻辑
处理所有障碍物的移动和复活机制
java
public void obstacleMove(){
for (int i=0;i<obsCount;i++){
Obstacle ob=obsArray[i];
if(!ob.isAlive){
//已经死亡,跳过移动;等它滑出屏幕底部就复活
ob.move(obstacleSpeed);
if(ob.isOutOfScreen()){
//飞出屏幕,直接复活出新障碍物
ob.reset(random.nextInt(520));
}
continue;
}
ob.move(obstacleSpeed);
if (ob.isOutOfScreen()){
ob.reset(random.nextInt(520));
}
}
}
子弹移动与碰撞检测
处理子弹移动、边界检测、与障碍物的碰撞
java
public void bulletMoveAndCollision(){
for(int i=0;i<bulletMaxCount;i++){
Bullet b=bulletArray[i];
if(b==null || !b.isAlive) continue; // 跳过无效子弹
b.move(); // 调慢子弹速度
if(b.y<0){
b.isAlive=false; // 子弹飞出屏幕顶部,标记为失效
continue;
}
// 检测子弹与所有障碍物的碰撞
for(int j=0;j<obsCount;j++){
Obstacle ob=obsArray[j];
if(!ob.isAlive) continue; // 跳过已死亡的障碍物
// AABB碰撞检测算法
boolean hit = (b.x < ob.x+ob.width)
&&(b.x+b.width>ob.x)
&&(b.y < ob.y+ob.height)
&&(b.y+b.height>ob.y);
if(hit){
ob.isAlive=false; // 障碍物被击中
b.isAlive=false; // 子弹消失
score +=10; // 增加分数
updateScore(); // 更新分数显示
destroyCount+=1; // 增加摧毁计数
// 每摧毁3个障碍物,增加障碍物速度(难度递增)
if (destroyCount%3==0){
obstacleSpeed+=2;
if (obstacleSpeed>=30){
obstacleSpeed=30; // 速度上限30
}
}
// 重置障碍物位置和大小
ob.y=-50;
ob.x= random.nextInt(40,560);
int randomSize=random.nextInt(20,60);
ob.width= randomSize;
ob.height=randomSize;
//删掉 ob.isAlive=true; 不会立刻复活障碍物
break; // 一颗子弹只能击中一个障碍物
}
}
}
}
检测飞机与障碍物的碰撞
return true表示发生碰撞,false表示未碰撞
java
public boolean checkPlaneHitObstacle(){
// 计算飞机的碰撞矩形(以飞机中心为原点,±15像素)
int planeLeft=planeX-15;
int planeTop=planeY-15;
int planeRight=planeX+15;
int planeBottom=planeY+15;
for (int i=0;i<obsCount;i++){
Obstacle ob=obsArray[i];
if (!ob.isAlive){
continue; // 跳过已死亡的障碍物
}
// 计算障碍物的碰撞矩形
int obLeft=ob.x;
int obTop=ob.y;
int obRight=ob.x+ob.width;
int obBottom=ob.y+ob.height;
// AABB碰撞检测(反向逻辑:判断是否不碰撞)
boolean isHit=!(planeRight<=obLeft||planeLeft>=obRight||planeBottom<=obTop||planeTop>=obBottom);
if(isHit){
if(hasShield){
hasShield=false; // 消耗护盾
}else{
gameState=2; // 游戏结束
}
return true; // 发生碰撞
}
}
return false; // 未发生碰撞
}
重写绘制方法
根据游戏状态绘制不同的界面
java
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
// 绘制背景
if (bgImage!=null){
g.drawImage(bgImage,0,0,getWidth(),getHeight(),null);
}
//绘制道具
for(Item item : itemList){
item.draw(g,shieldImg,clearImg,fireImg);
}
//绘制护盾光圈(如果有护盾)
if(hasShield){
g.setColor(Color.CYAN);
g.drawOval(planeX-30,planeY-30,60,60);
}
// 根据游戏状态绘制不同界面
if (gameState==1&&planeImage!=null){
// 游戏进行中界面
//绘制障碍物
for(int i=0; i<obsCount; i++){
Obstacle ob = obsArray[i];
if(ob.isAlive){
g.drawImage(obstacleImage,ob.x, ob.y, ob.width, ob.height,null);
}
}
//绘制子弹
for(int i=0;i<bulletMaxCount;i++){
Bullet b=bulletArray[i];
if(b!=null && b.isAlive){
g.drawImage(bulletImage,b.x,b.y,b.width,b.height,null);
}
}
// 绘制飞机(以飞机中心为基准)
g.drawImage(planeImage,planeX-15,planeY-15,30,30,null);
} else if (gameState==0) {
// 开始界面
if(themeImage!=null){
g.drawImage(themeImage,0,0,getWidth(),getHeight(),null);
}
} else if (gameState==2) {
// 游戏结束界面
g.drawImage(bgImage,0,0,getWidth(),getHeight(),null);
// 绘制所有障碍物(包括死亡的)
for (int i=0;i<obsCount;i++){
Obstacle ob=obsArray[i];
if (ob.isAlive){
g.drawImage(obstacleImage,ob.x,ob.y,ob.width,ob.height,null);
}
}
// 绘制所有子弹
for (int i=0;i<bulletMaxCount;i++){
Bullet b=bulletArray[i];
if(b!=null && b.isAlive){
g.drawImage(bulletImage,b.x,b.y,b.width,b.height,null);
}
}
// 绘制飞机
g.drawImage(planeImage,planeX,planeY,30,30,null);
// 绘制游戏结束文字
g.setColor(Color.red);
g.setFont(new Font("黑体",Font.BOLD,45));
g.drawString("游戏结束!",180,350);
// 绘制最终分数
g.setFont(new Font("黑体",Font.BOLD,22));
g.drawString("最终分数:"+score,200,410);
}
}
时间分数显示
java
/**
* 更新时间显示
*/
public void updateTime() {
timeLabel.setText("时间:"+time);
}
/**
* 更新分数显示
*/
public void updateScore(){
scoreLabel.setText("积分:"+score);
}
}
代码解析:
-
核心成员变量:
gameState:游戏状态(0-开始界面,1-游戏中,2-游戏结束)。planeX,planeY:飞机坐标,通过鼠标监听器实时更新。obsArray:障碍物数组,存储所有障碍物实例。bulletArray:子弹数组,管理所有子弹实例。itemList:道具列表,使用ArrayList动态管理道具。hasShield,fireBoost:道具状态标志,分别表示是否拥有护盾和火力增强效果。scoreLabel,timeLabel:积分和时间显示标签。
-
构造函数
DrawPanel():- 初始化飞机起始位置,避免开局时飞机出现在左上角。
- 加载所有游戏图片资源(背景、飞机、主题图、子弹、障碍物、道具图片)。
- 设置面板布局为
null(绝对布局),便于精确控制组件位置。 - 创建并配置积分、时间标签和开始游戏按钮。
- 初始化障碍物数组,所有障碍物初始状态为"死亡",游戏开始后再激活。
- 初始化子弹数组。
- 创建
GameListener监听器实例,并注册到按钮和面板上。 - 调用
loadMusic()加载背景音乐。
-
obstacleMove()方法:- 遍历所有障碍物,根据其状态执行移动逻辑。
- 对于已死亡的障碍物,仍会移动,当其移出屏幕底部时调用
reset()方法复活。 - 对于存活的障碍物,正常移动并在移出屏幕时复活。
- 实现了障碍物的循环生成机制,避免频繁创建新对象。
-
bulletMoveAndCollision()方法:- 遍历所有子弹,处理移动和碰撞检测。
- 子弹移出屏幕顶部时标记为失效。
- 使用轴对齐包围盒(AABB) 算法检测子弹与障碍物的碰撞。
- 碰撞后更新积分、增加摧毁计数,并根据摧毁数每3个增加一次障碍物移动速度(上限30)。
- 碰撞后重置障碍物位置和大小,但不立即激活,等待其自然复活。
-
checkPlaneHitObstacle()方法:- 检测飞机与障碍物的碰撞。
- 同样使用 AABB 算法,但采用反向逻辑判断(判断是否不碰撞)。
- 如果发生碰撞且飞机有护盾,则消耗护盾;否则游戏结束(
gameState=2)。
-
paintComponent(Graphics g)方法:- 游戏核心绘制方法,根据
gameState绘制不同界面。 - 游戏进行中(
gameState==1):绘制背景、道具、护盾光圈、存活的障碍物、存活的子弹和飞机。 - 开始界面(
gameState==0):绘制主题背景图。 - 游戏结束界面(
gameState==2):绘制背景、所有障碍物、子弹、飞机,并显示"游戏结束"和最终分数。
- 游戏核心绘制方法,根据
-
辅助方法:
updateTime(),updateScore():更新界面上的时间和积分显示。loadMusic(),playBgm(),stopBgm():背景音乐的加载、播放和停止控制。
设计要点:
- 状态驱动绘制 :通过
gameState变量控制三种不同界面的绘制逻辑,代码结构清晰。 - 对象池优化 :障碍物和子弹使用数组预创建,通过
isAlive状态管理生命周期,避免频繁的垃圾回收。 - 碰撞检测复用:子弹与障碍物、飞机与障碍物的碰撞检测都采用 AABB 算法,但实现方式略有不同,体现了算法的灵活性。
- 道具系统集成 :通过
itemList管理动态生成的道具,hasShield和fireBoost状态标志实现道具效果。 - 资源管理:所有图片和音乐资源在构造函数中集中加载,便于管理和错误处理。
- 界面与逻辑分离 :绘制逻辑集中在
paintComponent中,游戏状态更新和碰撞检测在独立的方法中,符合 MVC 设计思想。
5. GameListener 类代码实现
GameListener 类实现了游戏的核心事件监听与逻辑驱动,它集成了 MouseMotionListener、ActionListener 和 MouseWheelListener 接口,负责处理鼠标移动、按钮点击和滚轮事件,并驱动游戏主循环。
java
public class GameListener implements MouseMotionListener, ActionListener, MouseWheelListener {
private DrawPanel drawPanel; // 游戏绘制面板引用
private Timer timer; // 游戏主循环计时器
/**
* 构造函数:初始化监听器并设置游戏主循环
* @param dp 游戏绘制面板实例
*/
public GameListener(DrawPanel dp){
this.drawPanel = dp;
// 创建游戏主循环计时器,每40毫秒(约25帧/秒)触发一次
timer = new Timer(40, new ActionListener() {
private long gameStartTime = 0; // 记录游戏开始的时刻
@Override
public void actionPerformed(ActionEvent e) {
// 仅在游戏进行中(state=1)执行逻辑
if(drawPanel.gameState != 1){
return;
}
// 初始化游戏开始时间(仅第一次进入循环时记录)
if(gameStartTime == 0){
gameStartTime = System.currentTimeMillis();
}
// 计算已游戏时长(秒)
long second = (System.currentTimeMillis() - gameStartTime) / 1000;
drawPanel.time = (int) second;
drawPanel.updateTime(); // 更新界面时间显示
// 执行障碍物移动与子弹碰撞逻辑
drawPanel.obstacleMove();
drawPanel.bulletMoveAndCollision();
// ============ 道具生成与处理逻辑 ============
long now = System.currentTimeMillis();
// 每10秒生成一个新道具
if(now - drawPanel.lastItemSpawnTime > 10000){
int randomType = 1 + drawPanel.random.nextInt(3); // 随机1~3
int rx = drawPanel.random.nextInt(520); // 随机X坐标
drawPanel.itemList.add(new Item(rx, -30, randomType));
drawPanel.lastItemSpawnTime = now;
}
// 遍历所有道具,处理移动、边界检测与碰撞
for(Item item : drawPanel.itemList){
if(!item.isAlive) continue;
item.move(); // 道具下落
if(item.isOutOfScreen()){
item.isAlive = false; // 移出屏幕则标记为失效
}
// 检测道具与飞机碰撞
if(item.checkHit(drawPanel.planeX, drawPanel.planeY, drawPanel.planeW, drawPanel.planeH)){
item.isAlive = false; // 拾取后道具消失
if(item.type == 1){
drawPanel.hasShield = true; // 护盾道具
} else if(item.type == 2){
// 清屏道具:将所有障碍物标记为死亡
for(int k = 0; k < drawPanel.obsCount; k++){
drawPanel.obsArray[k].isAlive = false;
}
} else if(item.type == 3){
// 火力增强道具:激活5秒散射效果
drawPanel.fireBoost = true;
drawPanel.fireBoostEndTime = System.currentTimeMillis() + 5000;
}
}
}
// 检查火力增强效果是否到期
if(drawPanel.fireBoost && System.currentTimeMillis() > drawPanel.fireBoostEndTime){
drawPanel.fireBoost = false;
}
// ========= 飞机与障碍物碰撞检测 =========
boolean isHit = false;
for(int i = 0; i < drawPanel.obsCount; i++){
Obstacle ob = drawPanel.obsArray[i];
if(!ob.isAlive) continue;
// 矩形碰撞检测(AABB算法)
boolean collide =
drawPanel.planeX < ob.x + ob.width
&& drawPanel.planeX + drawPanel.planeW > ob.x
&& drawPanel.planeY < ob.y + ob.height
&& drawPanel.planeY + drawPanel.planeH > ob.y;
if(collide){
isHit = true;
break;
}
}
// 处理碰撞结果
if(isHit){
if(drawPanel.hasShield){
// ✅ 有护盾:消耗护盾,游戏继续
drawPanel.hasShield = false;
} else {
// ❌ 无护盾:游戏结束
drawPanel.gameState = 2;
timer.stop(); // 停止游戏计时器
drawPanel.stopBgm(); // 停止背景音乐
}
}
drawPanel.repaint(); // 触发界面重绘
}
});
}
/**
* 处理按钮点击事件(开始游戏)
*/
@Override
public void actionPerformed(ActionEvent e) {
String ac = e.getActionCommand();
if("开始游戏".equals(ac)){
drawPanel.gameState = 1; // 进入游戏状态
// 隐藏所有按钮(开始游戏按钮)
for(var comp : drawPanel.getComponents()){
if(comp instanceof JButton){
comp.setVisible(false);
}
}
// 激活所有障碍物
for(int i = 0; i < drawPanel.obsCount; i++){
drawPanel.obsArray[i].isAlive = true;
}
timer.start(); // 启动游戏主循环
drawPanel.loadMusic(); // 加载背景音乐
drawPanel.playBgm(); // 播放背景音乐
}
}
/**
* 鼠标滚轮事件:发射子弹
* 根据火力增强状态发射单发或三发散射子弹
*/
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
// 仅游戏进行中响应
if(drawPanel.gameState != 1){
return;
}
// 寻找子弹数组中的空位
int pos = -1;
for(int i = 0; i < drawPanel.bulletMaxCount; i++){
if(drawPanel.bulletArray[i] == null || !drawPanel.bulletArray[i].isAlive){
pos = i;
break;
}
}
if(pos == -1) return; // 子弹数组已满,不生成新子弹
// 计算子弹发射起点(飞机中心上方)
int px = drawPanel.planeX + drawPanel.planeW / 2;
int py = drawPanel.planeY;
if(!drawPanel.fireBoost){
// 普通状态:发射单发垂直子弹
drawPanel.bulletArray[pos] = new Bullet(px, py, 0);
} else {
// 火力增强状态:发射三发散射子弹(左、中、右)
drawPanel.bulletArray[pos] = new Bullet(px, py, -6); // 向左偏移
// 寻找第二个空位
int pos2 = -1;
for(int i = 0; i < drawPanel.bulletMaxCount; i++){
if(drawPanel.bulletArray[i] == null || !drawPanel.bulletArray[i].isAlive){
pos2 = i;
break;
}
}
if(pos2 != -1){
drawPanel.bulletArray[pos2] = new Bullet(px, py, 0); // 垂直
}
// 寻找第三个空位
int pos3 = -1;
for(int i = 0; i < drawPanel.bulletMaxCount; i++){
if(drawPanel.bulletArray[i] == null || !drawPanel.bulletArray[i].isAlive){
pos3 = i;
break;
}
}
if(pos3 != -1){
drawPanel.bulletArray[pos3] = new Bullet(px, py, 6); // 向右偏移
}
}
}
/**
* 鼠标移动事件:控制飞机位置
* 将鼠标坐标转换为飞机中心坐标
*/
@Override
public void mouseMoved(MouseEvent e) {
if(drawPanel.gameState != 1){
return;
}
drawPanel.planeX = e.getX() - drawPanel.planeW / 2;
drawPanel.planeY = e.getY() - drawPanel.planeH / 2;
}
/**
* 鼠标拖拽事件:复用鼠标移动逻辑
*/
@Override
public void mouseDragged(MouseEvent e) {
mouseMoved(e);
}
}
GameListener 类总结
GameListener 作为游戏的事件驱动与逻辑调度中心,承担了以下核心职责:
- 游戏主循环驱动 :通过
Timer每 40 毫秒触发一次actionPerformed,实现稳定的游戏帧更新(约 25 FPS)。循环内集成了时间计算、障碍物移动、子弹碰撞、道具生成与拾取、飞机碰撞检测等所有核心游戏逻辑。 - 事件监听与响应 :
- 按钮事件:响应"开始游戏"按钮点击,切换游戏状态、激活障碍物、启动计时器与背景音乐。
- 鼠标移动:实时将鼠标坐标映射为飞机中心位置,实现平滑的飞机控制。
- 鼠标滚轮:作为子弹发射触发器,根据火力增强状态发射单发或三发散射子弹。
- 道具系统管理 :
- 定时生成:每 10 秒在屏幕顶部随机位置生成一种随机类型的道具(护盾、清屏、火力增强)。
- 效果触发:检测道具与飞机的碰撞,并触发对应效果(激活护盾、清除所有障碍物、开启5秒散射火力)。
- 状态维护 :管理
fireBoost的持续时间,到期后自动关闭效果。
- 碰撞检测与游戏状态管理 :
- 使用 AABB 算法检测飞机与障碍物的碰撞。
- 根据碰撞结果和护盾状态决定游戏是否结束(有护盾则消耗,无护盾则游戏结束)。
- 在游戏结束时停止计时器和背景音乐。
- 资源与状态同步 :
- 维护游戏时间(基于系统时间戳,避免帧率波动影响计时)。
- 调用
drawPanel.repaint()驱动界面重绘,确保视觉状态与逻辑状态同步。
设计要点:
- 职责清晰:将用户输入、游戏逻辑更新、状态判断集中在一个类中,符合"监听器"模式的初衷。
- 帧驱动更新 :所有动态元素(障碍物、子弹、道具)的移动和碰撞检测都在
Timer的actionPerformed中统一处理,保证了逻辑执行的时序一致性。 - 状态驱动 :严格依赖
drawPanel.gameState控制逻辑执行范围(如只在游戏进行中响应鼠标滚轮、更新游戏逻辑)。 - 资源管理 :在游戏结束时正确停止
Timer和背景音乐,避免资源泄漏。
通过 GameListener,我们将离散的用户操作和持续的游戏逻辑更新整合为一个连贯、响应迅速的游戏体验。
开发遇坑记录
在开发过程中,我们遇到了一些典型的逻辑与实现问题。以下是关键问题的总结与解决方案,供大家参考避坑:
-
护盾永久无敌 Bug
- 问题现象 :碰撞检测到护盾后,仅判断
hasShield为true,但未将其置为false,导致护盾永不消耗,飞机处于永久无敌状态。 - 解决方案 :在护盾成功抵挡一次伤害后,必须立即将
hasShield手动设置为false,确保护盾被正确消耗。
- 问题现象 :碰撞检测到护盾后,仅判断
-
清屏后无新障碍物生成
- 问题现象 :使用清屏道具后,仅将障碍物的
isAlive设为false,但障碍物不会立即复活,导致屏幕上一段时间内没有障碍物。 - 解决方案 :障碍物复活依赖于
reset()方法。清屏后,障碍物仍需移动至屏幕底部并触发isOutOfScreen()判断,才会调用reset()重新激活。因此,清屏效果是"标记死亡",而非立即生成新障碍物。
- 问题现象 :使用清屏道具后,仅将障碍物的
-
避免使用
Thread.sleep()实现等待- 问题现象 :在 Swing 事件分发线程(EDT)中调用
Thread.sleep()会导致界面完全卡死,鼠标事件与绘图渲染均无响应。 - 解决方案:改用时间戳比对实现延时逻辑。例如,记录某个动作的开始时间,在游戏主循环中检查当前时间与开始时间的差值,达到预定间隔后再执行后续操作,从而避免阻塞 EDT。
- 问题现象 :在 Swing 事件分发线程(EDT)中调用
-
方法定义未调用
- 问题现象 :编写了
checkPlaneHitObstacle()碰撞检测方法,但在主逻辑中忘记调用,导致方法从未执行,功能缺失。 - 解决方案 :在
GameListener的游戏主循环或相应事件处理中显式调用该方法,确保碰撞检测逻辑生效。
- 问题现象 :编写了
-
鼠标与飞机中心点错位
- 问题现象 :在
mouseMoved事件中已将鼠标坐标转换为飞机中心坐标,但在paintComponent绘制飞机时又额外减去了 15 像素的偏移,导致飞机显示位置与鼠标控制位置不一致。 - 解决方案 :确保坐标转换逻辑唯一。既然
mouseMoved中已完成中心点换算,绘图时直接使用planeX和planeY作为飞机中心坐标即可,无需再次偏移。
- 问题现象 :在
-
避免使用帧计数器统计游戏时间
- 问题现象 :依赖
Timer触发次数(帧数)来累加时间,由于Timer执行间隔可能波动或丢帧,导致游戏计时不准。 - 解决方案 :改用
System.currentTimeMillis()获取系统真实时间戳。在游戏开始时记录一个起始时间戳,每次更新时计算与起始时间的差值,从而得到精确的游戏时长,从根本上解决因帧率不稳定导致的计时误差。
- 问题现象 :依赖
总结
到这里,我们修复了上一版本遗留的全部Bug,完成了护盾抵挡单次伤害、清屏重置障碍物、系统时间计时、开局延时生成障碍等功能。
本次开发让我体会到面向对象编程带来的便利:将子弹、障碍物、道具各自的数据与行为封装在独立类中,监听器只负责事件接收与逻辑调度,做到职责分离。同时也踩了不少Swing图形编程的经典坑点:不能使用sleep()阻塞UI线程、定义方法不等于调用、护盾伤害需要手动置空状态变量,这些都是GUI开发很容易忽略的细节。
游戏目前依旧还有可以完善的地方:分数系统、游戏结束弹窗、重新开始游戏、随游戏时间动态提升难度都还没有实现。
下期展望
下一篇博客,我们将新增计分统计、游戏结束界面,实现游戏重启功能;同时优化游戏难度,让障碍物速度随游玩时间逐渐加快,进一步完善整个小游戏。
代码运行:




