ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

拆解Java俄罗斯方块源码:Swing游戏循环与碰撞检测实战

拆解Java俄罗斯方块源码:Swing游戏循环与碰撞检测实战 简介一份基于Java实现的经典俄罗斯方块游戏完整源代码包面向正在学习Java游戏开发、面向对象编程或算法逻辑的初学者和开发者。资源体积小巧压缩后仅35KB共含6个文件其中5个为Java源文件1个为可直接运行的jar包包内文件结构清晰既可通过jar包快速启动体验游戏也可直接阅读源文件进行学习和修改。源码按照功能拆分为方块定义、游戏面板绘制、逻辑控制、界面交互和得分统计等模块覆盖了方块旋转、碰撞检测、整行消除以及下落加速等经典机制。目前已有2044人学习下载适合用于理解Java游戏程序的基本架构与事件驱动开发思路。读者既能重温经典小游戏的乐趣也能借此掌握Java Swing窗口编程、键盘事件处理、定时器调度和基础游戏循环设计是课程设计或入门练手的一份实用参考。1. Java俄罗斯方块一份老源码包半部游戏开发入门课要说 Java 俄罗斯方块这类项目为什么值得拆原因很简单它麻雀虽小五脏俱全。一份名为 eluosifangkuai---java.rar 的压缩包装着 RussianBlocks 主类、src 完整源码和一个直接就能跑起来的 ErsBlocks.jar。对想弄懂 Swing 游戏循环、碰撞检测和面向对象设计的人来说这份代码比大多数教学 demo 都实在——它把生成、下落、碰撞、消行、计分、加速整条链路完整串了起来。做 java 课程设计的拿它当案例源码参考能讲清逻辑准备 java 面试的借它能把事件处理和状态机摸透。无论哪一类认真读一遍收获都比闷头背八股文实在。2. 源码结构拆解从 src 目录到 ErsBlocks.jar 的完整映射拿到压缩包之后先别急着双击运行花十分钟把目录结构摸一遍。这份源码的组织方式非常典型一个主类负责启动一组工具类负责方块和矩阵的数据结构两个界面类分别承担容器和绘图职责。这种分层在 java 课程设计案例源码里最常见你能把它讲清楚面试里被问到如何设计一个游戏程序的结构时就不会卡壳。2.1 压缩包里的文件清单与启动方式解压 eluosifangkuai---java.rar 之后顶层主要看到三个部分RussianBlocks.java、src 目录和 ErsBlocks.jar。如果用 IDE 打开建议把 src 目录作为源码根路径导入而不是把 .java 文件散落在外层。文件/目录类型职责RussianBlocks.java主类main 方法入口创建顶层窗口并组装各个面板src/源码目录全部游戏类的源码按包路径组织ErsBlocks.jar可执行 JAR编译好的完整游戏可独立运行启动方式按场景选择# 方式一直接运行打包好的 JAR适合先体验原版手感 java -jar ErsBlocks.jar # 方式二从源码编译运行进入 src 目录后执行 javac -encoding UTF-8 *.java java RussianBlocks # 方式三IDE 中打开源码直接运行 RussianBlocks 的 main 方法方式一最省事前提是本机装了 JDK 且环境变量配置正确具体报错信息会直接打到终端比双击 jar 包看到的黑一闪更有价值。方式二适合改代码的场景每次改动后重新 javac任何编译错误都会直接暴露出来。方式三适合断点调试你可以盯着变量看方块旋转过程中状态是怎么流转的。我一般会先用方式一把原版跑起来确认基线版本正常再切到方式二或方式三做改动。提示直接双击 ErsBlocks.jar 没反应多半是清单文件里没有 Main-Class 属性或者 JDK 版本不匹配。用java -jar在终端里跑能看到具体报错。2.2 ErsBox、ErsBlock 与 ErsBoxesMatrix方块的三层抽象俄罗斯方块的基本元素是方块但这个项目没有把所有逻辑塞进一个类里而是拆成了三层。理解这一层基本就理解了面向对象编程 java 的核心套路。最底层是 ErsBox代表方块矩阵中的一个格子。它不关心自己属于哪个方块只用一个布尔值标记实心与否public class ErsBox { private boolean isSolid; // 当前格子是否为实心 public ErsBox(boolean isSolid) { this.isSolid isSolid; } public boolean isSolid() { return isSolid; } public void setSolid(boolean solid) { this.isSolid solid; } }这段代码是整个游戏的数据基础。isSolid 为 true 时绘制阶段填充一个矩形false 时留空。setSolid会被两处调用方块落地的瞬间把格子在外部矩阵里标记为实心消行时把整行所有格子重置为 false。参数说明里值得注意的一点是ErsBox 是可变对象跟很多教程里用不可变对象表示方块的做法不同——因为它要支持落地后状态翻转可变在这套设计里是合理选择。中间层是 ErsBlock代表一个正在下落、可以被旋转的活方块。七种方块的初始形状通常定义在一个形状表里常见实现用二维数组存每一种形状也有用 int 十六进制位图表示的版本——比如0x0f00的低 16 位是0000 1111 0000 0000拆成四组看就是 4x4 网格里中间一行四格全满。见到十六进制数不用慌按位拆开就是网格形状。public class ErsBlock { private boolean[][] body; // 当前形状矩阵true 为实心 private int rowCount; private int colCount; public ErsBlock(boolean[][] body) { this.body body; this.rowCount body.length; this.colCount body[0].length; } // 顺时针旋转 90 度旧矩阵的 (i, j) 映射到新矩阵的 (j, rowCount-1-i) public boolean[][] rotate() { boolean[][] rotated new boolean[colCount][rowCount]; for (int i 0; i rowCount; i) { for (int j 0; j colCount; j) { rotated[j][rowCount - 1 - i] body[i][j]; } } return rotated; } }这里最关键的是 rotate 方法的坐标映射。拿一个 2x2 的矩阵在纸上比划旧 (0,0) 位置的格子旋转后应该到新矩阵的 (0,1)也就是右上角。代入公式rotated[j][rowCount-1-i]旧 (0,0) → 新 (0,1)完全一致。这种映射方式不需要额外的空间交换直接新开一个矩阵就完成旋转。注意 rotate 返回的是新矩阵而不是修改 body 本身。这个设计有个实际意义方块要旋转时可以先用新矩阵做碰撞检测如果撞墙或者撞到已落地方块就保持原形状视觉上表现为转不动。如果原地直接改矩阵旋转失败还得回滚很容易出现状态不一致的 bug。最上层是 ErsBoxesMatrix管理所有已经落地的方块。游戏区域里所有静止的格子都记录在这个矩阵中新方块每尝试移动一步都要跟它做碰撞判断。这三层抽象把格子的状态方块的形状所有方块的聚合彻底分开改动任一层都不会波及另外两层是这份源码最值得学习的地方。2.3 GamePanel 与 GameCanvasSwing 绘制的完整流程界面部分由 GamePanel 和 GameCanvas 两个类配合完成。GamePanel 继承 JPanel是顶层容器负责把 GameCanvas、按钮、计分标签摆到正确位置GameCanvas 继承 JPanel 或 Canvas负责真正绘制游戏画面。拆两层的原因在于职责分离容器管布局和交互组件画布只管像素。public class GameCanvas extends JPanel { private final int BOX_SIZE 20; // 每个格子的像素边长 Override protected void paintComponent(Graphics g) { super.paintComponent(g); ErsBoxesMatrix matrix gamePanel.getMatrix(); int rows matrix.getRows(); int cols matrix.getCols(); for (int i 0; i rows; i) { for (int j 0; j cols; j) { if (matrix.getBox(i, j).isSolid()) { g.fillRect(j * BOX_SIZE, i * BOX_SIZE, BOX_SIZE, BOX_SIZE); } } } } }paintComponent 是 Swing 渲染的入口所有绘制逻辑必须集中在这里不能在外面直接调 Graphics 画图。代码用两层循环遍历整个落地矩阵把实心格子逐个画成 20x20 的矩形。BOX_SIZE 是关键参数它同时决定格子大小和坐标换算比例如果游戏区域设计成 10 列 20 行那画布宽度就是 10 乘 20 等于 200 像素。改大 BOX_SIZE 会让画面更清晰但面板尺寸也要同步调整否则右侧会露白边。Swing 的刷新机制遵循请求-绘制模型。业务逻辑改变游戏状态后调用 repaint()Swing 会把重绘请求合并统一在事件分发线程中执行 paintComponent。不要在 while(true) 循环里直接调 paintComponent也不要试图用 Thread.sleep 控制下落节奏否则界面会卡死这是新手最容易翻车的地方第 4 章会展开讲。3. 核心逻辑跑通下落、碰撞检测与消行的实现细节如果说类结构是骨架那游戏循环就是心脏。俄罗斯方块的核心体验全部集中在一个循环里定期下移方块判断能不能移移不动就落定落定后检查有没有完整行有就消掉并加分。下面把每一步拆开看。3.1 方块下落与游戏循环Swing Timer 的正确用法很多初学者会下意识地写while (true) { block.moveDown(); Thread.sleep(500); }这在 Swing 里是大忌。主线程死循环会占住事件分发线程界面根本来不及刷新表现为窗口白屏或者完全不响应。这份工程采用的方式是用 javax.swing.Timer 代替手写循环public class GameLogic { private Timer timer; private int delay 500; // 初始下落间隔 500ms public void start() { timer new Timer(delay, e - { if (!gamePanel.isPaused()) { gamePanel.moveDown(); } }); timer.start(); } public void speedUp() { delay Math.max(80, delay - 40); // 每次加速减少 40ms下限 80ms timer.setDelay(delay); } }javax.swing.Timer 的回调在 EDT 中执行这意味着在回调里修改 UI 状态不需要额外做线程同步这是它跟 java.util.Timer 最本质的区别。delay 的单位是毫秒500ms 表示每半秒方块下落一格。speedUp 方法里做了两层保护每次减 40ms 但不低于 80ms避免游戏后期速度无限提升导致逻辑跟不上渲染帧率。为什么不用 java.util.Timer 或 ScheduledExecutorService因为它们的回调跑在工作线程改 UI 时还得用 SwingUtilities.invokeLater 切回 EDT代码复杂度立刻上去。在单机小游戏这个场景里Swing Timer 就是最合适的选择这也是它在 Swing 游戏开发里几乎是标配的原因。3.2 碰撞检测边界、堆叠与旋转后的重新判断方块每走一步都要问一个问题目标位置能不能去判断条件分两层第一层是边界第二层是堆叠。两个判断写在同一个方法里返回 boolean调用方根据结果决定是否更新位置public boolean canMove(ErsBlock block, int targetRow, int targetCol) { boolean[][] body block.getBody(); int rows body.length; int cols body[0].length; for (int i 0; i rows; i) { for (int j 0; j cols; j) { if (!body[i][j]) { continue; // 空白格不参与碰撞 } int absRow targetRow i; int absCol targetCol j; // 边界判断不能超出左右墙和底部 if (absCol 0 || absCol matrix.getCols()) { return false; } if (absRow matrix.getRows()) { return false; } // 堆叠判断目标位置已经有实心格子 if (absRow 0 matrix.getBox(absRow, absCol).isSolid()) { return false; } } } return true; }关键点有两个。第一遍历的是方块自身的形状矩阵只对实心格子做判断空白格子直接跳过这样方块无论怎么偏移都不会因为透明格误判碰撞。第二absRow 0这个条件别省——方块刚生成时有一部分可能位于游戏区域上方比如矩阵 20 行而方块初始行在 -2此时不能查矩阵否则数组越界或误判。旋转的碰撞判断是另一个高频坑。有的实现先旋转再检测失败就转回去代码啰嗦还容易漏。稳一点的做法是先用 rotate 得到新矩阵再用新矩阵执行一次 canMove成功才真正更新方块形状。这样移动和旋转共用同一个判断函数逻辑唯一测试也容易覆盖。3.3 行消除与计分从消行到等级曲线方块落定后游戏区域里可能出现了完整的一行。消行逻辑要从底部往上扫因为消掉一行后上方所有行要整体下移从下往上处理可以避免行号错位public int removeFullRows() { int removed 0; int rows matrix.getRows(); int cols matrix.getCols(); for (int i rows - 1; i 0; i--) { if (isRowFull(i)) { removed; // 第 i 行以上的所有行整体下移一行 for (int k i; k 0; k--) { for (int j 0; j cols; j) { matrix.getBox(k, j).setSolid( matrix.getBox(k - 1, j).isSolid()); } } // 最顶行清空 for (int j 0; j cols; j) { matrix.getBox(0, j).setSolid(false); } i; // 下移后当前行有新内容需要重新检查 } } return removed; }isRowFull 遍历一行所有列全部实心才算满。消除后用双重循环把上方行整体下移一层最顶行置空。注意i这行下移后回到 i 位置的内容是原来 i-1 行的必须重新判断一次所以把循环变量减回去。很多人抄这段时漏掉 i导致连续两行同时消不掉。计分规则各版本大同小异常用单消 100、双消 300、三消 500、四消 800。等级提升按累计消除行数计算每过 10 行升一级升级后调用 speedUp 缩短下落间隔。这个设计把分数和难度解耦分数只跟消行数量与质量有关难度只跟时间有关各自独立调整而不互相牵连。拿到源码后可以试着改计分倍率观察对游戏节奏的影响这是理解计分系统最快的方式。4. 避坑排查指南编译、刷新与焦点的四个经典问题每次把这种源码包发给别人复现总会收到一串几乎相同的问题。四个出现频率最高的我整理成现象→原因→解决的格式你在自己环境里遇到类似情况按顺序排查就行省得把时间浪费在玄学调参上。4.1 import 路径错乱导致编译失败现象javac 编译时报找不到符号或程序包不存在明明看着 src 目录下所有 .java 文件都在。原因这类老源码经常把包路径写得很随意有些类在默认包有些类在子目录。直接把所有 .java 复制到同一个目录破坏了原有的包层级import 自然就断了。解决不要平铺文件保持 src 目录的原生结构。从 src 根目录执行javac -encoding UTF-8 *.java如果源码里有package xxx;声明则要按照包路径建好目录或用 IDE 直接打开 src 目录作为源码根。中文注释乱码导致编译失败时javac 强制加-encoding UTF-8Windows 默认 GBK 环境下不加这一项必乱。4.2 画布不刷新repaint 与 paintComponent 的调用时机现象方块在移动但界面纹丝不动。窗口最小化再恢复画面突然跳到最新状态。原因逻辑线程在死循环里改了游戏状态但这些修改发生在工作线程中EDT 上的 repaint 请求一直排队却得不到执行。最小化触发系统重绘EDT 才被迫跑了一次 paintComponent所以画面恢复了。解决回到第 3 章的 Timer 方案让状态变更和 repaint 都发生在 EDT 里。如果确实需要独立线程做计算计算完必须用 SwingUtilities.invokeLater 包裹状态更新和 repaint 调用否则永远只能看到跳变而不是流畅动画。4.3 键盘事件失效焦点被抢走现象游戏刚开始方向键还能控制方块点击了某个按钮或者弹窗之后键盘怎么按都没反应。原因Swing 里键盘事件只会派发给当前焦点组件。GameCanvas 没设置 setFocusable(true)默认拿不到焦点用户点了按钮或计时器弹窗抢走焦点后画布就彻底失联了。解决GameCanvas 构造器里加上 setFocusable(true)并监听焦点丢失事件强制要回焦点setFocusable(true); addFocusListener(new FocusAdapter() { Override public void focusLost(FocusEvent e) { requestFocus(); } });提示按钮也要处理点击按钮后调用 gameCanvas.requestFocus() 把焦点还回去。KeyBinding 是 Swing 官方推荐的键盘处理方式在 JComponent 层面绑定按键动作能绕开大部分焦点问题。4.4 JAR 包双击无反应Main-Class 与 JDK 版本现象双击 ErsBlocks.jar 光标闪一下什么都没发生或者终端报找不到主类。原因可能有两个。一个是 JAR 清单文件里的 Main-Class 指向的类不存在或路径写错另一个是 JDK 版本不兼容Java 8 编译的代码在高版本 JDK 上有时会因模块化限制跑不起来。解决先用命令查看 JAR 内容与清单jar tf ErsBlocks.jar unzip -p ErsBlocks.jar META-INF/MANIFEST.MF确认 Main-Class 存在且类路径正确然后执行java -jar ErsBlocks.jar或手动指定主类java -cp ErsBlocks.jar RussianBlocks。如果报 JDK 版本错误把 JAVA_HOME 指向低版本 JDK或在 IDE 里把项目编译级别调到与源码匹配的版本。5. 改造升级给经典版加三个实用功能源码读顺之后最好的巩固方式是动手改。三个改动量不大但收益明显的方向是我在自己项目里验证过的做法。第一加下一个方块预览面板。核心是让方块生成器暴露下一个形状的信息预览面板把形状矩阵画在一个单独的小画布上public class NextBlockPanel extends JPanel { private boolean[][] nextBody; public void setNextBody(boolean[][] body) { this.nextBody body; repaint(); } Override protected void paintComponent(Graphics g) { super.paintComponent(g); if (nextBody null) return; int offsetX (getWidth() - nextBody[0].length * 20) / 2; for (int i 0; i nextBody.length; i) { for (int j 0; j nextBody[i].length; j) { if (nextBody[i][j]) { g.fillRect(offsetX j * 20, 10 i * 20, 20, 20); } } } } }预览面板只负责显示不参与碰撞逻辑。关键点是偏移量计算offsetX 用面板宽度减去形状实际宽度再除以 2保证不同宽度的方块都能居中显示。方块生成器每次创建新方块时调用 setNextBody 推送下一个形状游戏体验立刻上一个台阶。第二把下落速度从固定减速改成指数曲线。原始逻辑可能是每升一级固定减 40ms到后期减无可减。改成delay (int)(500 * Math.pow(0.88, level - 1))等级越高减速比例越小既保留加速压力又不会在 10 级以后快到手忙脚乱。0.88 是衰减系数想让曲线更陡改成 0.82更平缓就用 0.93。第三给最高分加持久化。用 Properties 文件保存不引第三方依赖public int loadHighScore() { try (FileInputStream in new FileInputStream(tetris.properties)) { Properties p new Properties(); p.load(in); return Integer.parseInt(p.getProperty(highScore, 0)); } catch (IOException e) { return 0; } }文件读写都包在 try-with-resources 里读取失败返回 0不会让游戏启动就崩。Properties 是明文存储玩家能改文件刷高分单机小游戏无伤大雅——如果未来要做排行榜就得换成加密存储或服务端校验。我改这个项目时最常踩的坑是预览面板画完忘了调 repaint导致预览区永远显示第一个方块。从那以后每次改完界面代码我都强制走一遍状态变更→repaint→焦点确认三步检查。这份老源码最难得的地方在于它把游戏开发的基础问题浓缩在了一个小项目里拆透了它Swing 游戏开发的底子就稳了一半。希望这篇笔记能帮到你。本文还有配套的精品资源点击获取
返回列表