资讯详情

资讯详情

Java飞机大战小游戏源码:Swing实现与课程设计完整指南

简介这是一份面向Java初学者与课程设计需求的飞机大战小游戏项目源码适合想通过完整案例理解面向对象编程、Swing图形界面与游戏循环机制的开发者练手。压缩包共73个文件约774KB其中7个java源文件承载核心逻辑10个class为编译产物52个jpg图片提供飞机、爆炸等游戏素材另有project、classpath及prefs等Eclipse工程配置导入后即可直接运行调试。项目按src与bin分目录组织plane与explode等模块划分清晰涵盖玩家战机、敌机生成、碰撞检测、爆炸动画与分数统计等典型玩法便于读者梳理游戏主循环、事件监听与资源加载的完整实现思路。目前已有444人学习适合作为Java入门练手、期末大作业参考或二次开发的基础框架。1. 飞机大战小游戏项目源码 Java一份能跑、能改、能交课程设计的完整拆解很多人搜「飞机大战小游戏项目源码 Java」其实心里想的不是同一件事。有人是要交 Java 课程设计需要一个能直接演示、代码结构清晰的成品有人是想拿它练手把 Java 基础、集合、多线程、Swing 绘图串起来还有人是在做小游戏方向的副业尝试想先摸清一个 2D 射击游戏的最小闭环长什么样。这三种诉求落到同一份源码上关注点完全不同交作业的看能不能跑、有没有注释练手的看架构分层、类怎么拆做产品的看帧循环、碰撞检测、资源管理这些能不能撑住后续扩展。飞机大战这个题材之所以常年出现在 Java 小游戏源码的搜索里是因为它刚好卡在一个甜点区逻辑量不大但把游戏开发的核心要素几乎全占了——游戏主循环、玩家输入、敌机生成、子弹发射、碰撞判定、分数与生命值、暂停与重开。用 Java 实现最省事的路径是 Swing AWT不依赖任何第三方引擎JDK 装好就能编译运行这对课程设计和初学者极其友好。下面我按「先想清楚架构再动手写核心模块最后处理那些一跑就翻车的地方」的顺序把这份源码该怎么做讲透。2. 用 Swing 搭飞机大战从游戏主循环到类结构设计2.1 为什么选 Swing 而不是 JavaFX 或游戏引擎先解决选型问题因为这决定了你后面所有代码的写法。Java 做 2D 小游戏常见三条路Swing/AWT、JavaFX、以及 LibGDX 这类游戏框架。飞机大战这种量级我一般直接选 Swing理由很实在。第一零依赖。Swing 是 JDK 自带的javac编译、java运行不需要配 Maven 拉一堆包课程设计答辩现场换台电脑也能跑。第二绘图模型简单直接paintComponent里拿到Graphics对象就能画图配合Timer做定时刷新游戏主循环的骨架十几行就能立起来。第三资料多、坑位明确遇到问题好搜。JavaFX 的动画和时间轴 API 更现代但它的模块化在 JDK 11 之后需要额外配置对新手反而是负担。LibGDX 适合做真正要发布的产品但它的学习曲线和项目结构对「就想跑通一个飞机大战」的人来说太重了。所以这份源码的定位很清晰Swing AWT 定时器驱动的主循环纯 JDK拿来即用。2.2 游戏主循环Timer 驱动与帧刷新节奏游戏和普通程序最大的区别是它有一个持续运行的循环不断更新游戏状态飞机移动、子弹飞行、敌机下落然后重绘画面。Swing 里实现这个循环最稳的方式是javax.swing.Timer它会在事件调度线程EDT上按固定间隔触发actionPerformed。import javax.swing.*; import java.awt.*; public class GamePanel extends JPanel { // 16ms 约等于 60 帧每秒是 2D 小游戏比较舒服的刷新节奏 private static final int FRAME_DELAY 16; private Timer gameTimer; public GamePanel() { setPreferredSize(new Dimension(480, 700)); setBackground(Color.BLACK); // 定时器每 16ms 触发一次 tick驱动整个游戏世界前进 gameTimer new Timer(FRAME_DELAY, e - { updateGame(); // 先更新逻辑状态 repaint(); // 再请求重绘顺序不能反 }); gameTimer.start(); } private void updateGame() { // 玩家、子弹、敌机的状态更新都放这里 } Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 所有绘制操作基于当前状态不在这里改状态 } }这段代码有两个关键点。一是updateGame()和repaint()的顺序必须先更新逻辑再重绘否则画面永远慢一帧。二是FRAME_DELAY的取值16ms 对应约 60FPS是 2D 游戏的主流选择如果你把逻辑写成「每帧移动固定像素」那帧率一变速度就变这是后面避坑章节要重点讲的。提示所有游戏状态的读写都发生在 EDT 上因为 Timer 的回调就在 EDT。这意味着你不需要为「玩家对象」加锁但也意味着任何耗时操作比如读大图、网络请求都不能放进updateGame否则整个画面会卡住。2.3 类结构怎么拆实体、管理器、面板三层一份能改的源码类不能全堆在一个文件里。我习惯拆成三层实体层、管理层、面板层。实体层是游戏里会动的东西抽一个基类GameObject把坐标、宽高、存活标志、update()和draw()放进去然后Player、Enemy、Bullet各自继承。这样碰撞检测、遍历更新都能用统一接口处理。import java.awt.*; public abstract class GameObject { protected int x, y, width, height; protected boolean alive true; public GameObject(int x, int y, int width, int height) { this.x x; this.y y; this.width width; this.height height; } public abstract void update(); public abstract void draw(Graphics g); // 矩形碰撞检测飞机大战用矩形近似足够不必上像素级 public Rectangle getBounds() { return new Rectangle(x, y, width, height); } public boolean isAlive() { return alive; } public void setAlive(boolean alive) { this.alive alive; } }管理层负责「批量操作」敌机什么时候生成、子弹和敌机怎么配对检测、越界的对象怎么清理。面板层只做两件事——接收键盘输入、把管理层的数据画出来。这个分层的好处是你想加「道具」「Boss」时只需要在实体层加类、在管理层加生成逻辑面板层几乎不用动。2.4 键盘输入与玩家移动的绑定玩家移动靠键盘监听。Swing 里推荐用KeyAdapter监听keyPressed和keyReleased用一个布尔数组记录当前哪些键被按下然后在updateGame里根据按键状态移动玩家。为什么不直接在keyPressed里改坐标因为按键事件触发频率和游戏帧率不一致直接改会导致移动一顿一顿的。private boolean leftPressed, rightPressed, upPressed, downPressed; // 在面板构造时注册监听 setFocusable(true); addKeyListener(new java.awt.event.KeyAdapter() { Override public void keyPressed(java.awt.event.KeyEvent e) { switch (e.getKeyCode()) { case java.awt.event.KeyEvent.VK_LEFT: leftPressed true; break; case java.awt.event.KeyEvent.VK_RIGHT: rightPressed true; break; case java.awt.event.KeyEvent.VK_UP: upPressed true; break; case java.awt.event.KeyEvent.VK_DOWN: downPressed true; break; } } Override public void keyReleased(java.awt.event.KeyEvent e) { switch (e.getKeyCode()) { case java.awt.event.KeyEvent.VK_LEFT: leftPressed false; break; case java.awt.event.KeyEvent.VK_RIGHT: rightPressed false; break; case java.awt.event.KeyEvent.VK_UP: upPressed false; break; case java.awt.event.KeyEvent.VK_DOWN: downPressed false; break; } } });然后在updateGame里统一处理位移并做边界钳制防止飞机飞出屏幕。setFocusable(true)这行很容易被漏掉漏了键盘就没反应这是新手最常见的翻车点之一。3. 核心玩法实现敌机生成、子弹发射与碰撞检测3.1 敌机生成随机位置与难度递增敌机生成的核心是「随机 节奏控制」。最简单的做法是每帧按概率生成但这样难度不可控。更稳的做法是用一个计数器每隔固定帧数生成一架随着分数提高缩短间隔。private int enemySpawnCounter 0; private int spawnInterval 60; // 每 60 帧生成一架敌机 private void spawnEnemy() { enemySpawnCounter; if (enemySpawnCounter spawnInterval) { enemySpawnCounter 0; // 敌机 x 坐标在屏幕宽度内随机y 从顶部外开始 int ex (int) (Math.random() * (getWidth() - 40)); enemies.add(new Enemy(ex, -40, 40, 40)); // 分数越高生成越快但设一个下限防止难度爆炸 if (spawnInterval 20) { spawnInterval 60 - score / 100; } } }spawnInterval从 60 帧逐步降到 20 帧对应大约 1 秒到 0.33 秒一架。这个参数是手感的关键调太快玩家反应不过来调太慢又没挑战。我一般会把它做成可配置的常量方便试玩时快速调整。3.2 子弹发射与冷却机制子弹不能每帧都发否则屏幕瞬间被塞满。需要一个冷却计数器按住空格时每隔若干帧发一颗。private int bulletCooldown 0; private static final int BULLET_INTERVAL 10; // 每 10 帧一发 private void handleShooting() { if (bulletCooldown 0) { bulletCooldown--; } if (spacePressed bulletCooldown 0) { // 子弹从玩家机头位置发出 bullets.add(new Bullet(player.getX() player.getWidth() / 2 - 3, player.getY() - 10, 6, 16)); bulletCooldown BULLET_INTERVAL; } }BULLET_INTERVAL取 10 帧约每秒 6 发是飞机大战里比较舒服的射速。子弹的初始位置要算上玩家宽度的一半再减去子弹宽度的一半这样子弹才是从机头正中出去的不然会偏。3.3 碰撞检测矩形相交与遍历清理碰撞检测用Rectangle.intersects子弹和敌机、玩家和敌机各检测一轮。关键是遍历时不能边遍历边删否则会抛ConcurrentModificationException这是 Java 集合的经典坑。private void checkCollisions() { // 子弹 vs 敌机 for (Bullet b : bullets) { if (!b.isAlive()) continue; for (Enemy e : enemies) { if (!e.isAlive()) continue; if (b.getBounds().intersects(e.getBounds())) { b.setAlive(false); e.setAlive(false); score 10; break; // 一颗子弹只打掉一架敌机 } } } // 玩家 vs 敌机 for (Enemy e : enemies) { if (e.isAlive() player.getBounds().intersects(e.getBounds())) { e.setAlive(false); player.loseLife(); } } // 统一清理死亡对象避免遍历中修改集合 bullets.removeIf(obj - !obj.isAlive()); enemies.removeIf(obj - !obj.isAlive()); }removeIf是 Java 8 之后清理集合的推荐写法比手动Iterator.remove()简洁。注意碰撞检测里用break让一颗子弹只命中一架敌机否则一发子弹可能同时打掉重叠的两架分数会虚高。3.4 分数、生命值与游戏状态机游戏状态用一个枚举管理READY、RUNNING、PAUSED、GAMEOVER。所有更新逻辑先判断当前状态只有RUNNING才推进世界。这样暂停和重开就变得很干净。public enum GameState { READY, RUNNING, PAUSED, GAMEOVER } private GameState state GameState.READY; private void updateGame() { if (state ! GameState.RUNNING) return; player.update(); handleShooting(); spawnEnemy(); updateBullets(); updateEnemies(); checkCollisions(); if (player.getLives() 0) { state GameState.GAMEOVER; } }状态机的好处是paintComponent里可以根据状态画不同的界面READY画开始提示PAUSED画暂停字样GAMEOVER画最终分数和重开提示。这套结构清晰扩展起来也方便。4. 避坑与排查飞机大战源码里最容易翻车的 5 个地方4.1 画面闪烁严重飞机拖影现象运行后画面一直闪移动的飞机后面拖着残影。原因Swing 默认在重绘前用背景色填充整个面板如果绘制逻辑和刷新节奏没对齐就会看到闪烁。这是 AWT/Swing 动画的经典问题。解决开启双缓冲。JPanel默认是双缓冲的但如果你在paint而不是paintComponent里画或者手动调用了setDoubleBuffered(false)就会关掉它。确保重写的是paintComponent并调用super.paintComponent(g)同时不要在updateGame里调用repaint之外的绘制操作。4.2 键盘按了没反应现象程序能跑画面也在动但按方向键飞机不动。原因面板没有获得焦点。Swing 里键盘事件只发给拥有焦点的组件如果面板没setFocusable(true)或者焦点被其他组件抢走监听就收不到事件。解决构造时调用setFocusable(true)和requestFocusInWindow()。如果嵌在JFrame里确保在setVisible(true)之后再请求焦点。实在不行用KeyEventDispatcher做全局键盘分发但那是下策。4.3 帧率一变游戏速度就变现象在性能好的机器上飞机飞快在旧电脑上慢如蜗牛。原因逻辑写成了「每帧移动固定像素」而帧率受硬件和系统调度影响帧率不稳定时速度就不稳定。解决要么把移动量乘以时间增量delta time要么接受固定帧率的假设并保证 Timer 间隔稳定。飞机大战这种量级我一般用固定帧率方案把FRAME_DELAY设成 16ms并在文档里注明「逻辑基于固定帧率」。如果要做跨设备一致就得引入 delta time把x speed改成x speed * delta。4.4 对象越界后没清理内存持续上涨现象玩久了越来越卡最后卡死。原因飞出屏幕的子弹和敌机没有被移除集合越来越大每帧遍历和绘制的对象越来越多。解决在update里判断越界就标记alive false然后统一removeIf清理。子弹飞出上边界、敌机飞出下边界都要处理。这个清理逻辑必须每帧执行不能只在碰撞时清理。4.5 图片资源路径写死换台电脑就找不到现象在自己电脑上跑得好好的拷给别人就报NullPointerException图片加载失败。原因用了绝对路径比如C:/Users/xxx/images/plane.png或者用了相对路径但工作目录不对。解决把图片放进src下的资源目录用getClass().getResource(/images/plane.png)加载。这样资源会被打包进 classpath无论在哪运行都能找到。如果暂时没有美术资源用Graphics.fillRect画色块代替先跑通逻辑再换图。5. 从能跑到能交源码工程化与二次开发的几个实用技巧把游戏跑起来只是第一步真正决定这份源码「值不值得留着」的是它能不能被改、被扩展、被讲清楚。我踩过的坑告诉我课程设计答辩时老师最爱问的两个问题是「你这个类为什么这么拆」和「如果我要加一个新功能你改哪里」。所以工程化不是花架子是让你能答上来的底气。第一个技巧是把可调参数集中到一个配置类。射速、敌机生成间隔、玩家速度、初始生命值这些全部抽成GameConfig里的static final常量。这样调手感时只改一个文件也方便在答辩时展示「我知道哪些参数影响体验」。public final class GameConfig { public static final int FRAME_DELAY 16; public static final int PLAYER_SPEED 5; public static final int BULLET_INTERVAL 10; public static final int INITIAL_LIVES 3; public static final int ENEMY_BASE_INTERVAL 60; public static final int ENEMY_MIN_INTERVAL 20; private GameConfig() {} // 工具类不允许实例化 }第二个技巧是用状态机把「重开」做干净。很多源码重开时只重置了分数敌机和子弹的集合还留着上一局的对象导致一重开就满屏敌机。正确做法是写一个resetGame()把玩家、子弹、敌机、分数、生命值全部重置状态切回READY。第三个技巧是给关键逻辑加注释尤其是碰撞和状态切换。答辩时老师翻代码看到checkCollisions里那句break有注释说明「一颗子弹只命中一架」会觉得你是想清楚了才写的而不是抄来的。第四个技巧是预留扩展点。比如敌机生成那里可以留一个EnemyType枚举现在只有一种但结构上支持以后加「快速敌机」「血厚敌机」。玩家射击那里可以留一个weaponLevel字段现在恒为 1但加道具时直接改这个值就能实现火力升级。这些预留不需要真的实现功能但能让源码显得有设计感。最后一个习惯是我自己每次做完小项目都会做的写一个 README把运行方式、操作说明、类结构图、可调参数列清楚。不是为了好看是为了三个月后自己回头看还能秒懂。飞机大战这种项目代码量不大但涉及的知识点密度高一份清晰的说明能让它从「一次性作业」变成「可复用的练手模板」。如果你打算拿这份源码去交课程设计我的建议是别只交代码把「为什么用 Swing」「主循环为什么这么写」「碰撞为什么用矩形」这几个问题想清楚答辩时能讲出取舍比代码本身更加分。如果你是想借它入门 Java 游戏开发那就从改参数开始——把射速调快、把敌机调密、加一种新敌机改着改着你就理解这套结构了。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →