简介:这是一份用Java编写的俄罗斯方块小游戏完整源码,面向具备一定Java基础、希望研究桌面小游戏实现或做课程设计的学习者。程序需JRE环境,通过java -jar方式运行,并支持将声音、显示、关卡等配置以cfg文件形式保存在jar同级目录。资源包共49个文件,以32个java源文件为核心,另含8个wav音效、3个res资源、3个xml配置及png图标等,整体约224KB,结构紧凑便于阅读。游戏提供标准、速度、复杂性三组关卡,可自定义控制键、切换单色或彩色显示、控制网格显隐,复杂性关卡还会随机生成障碍物并扩展至16种形状。源码中亦保留了形状擦除异常与蒙层覆盖等已知Bug的说明,可作为调试与排错思路的参考。目前已有1996人学习下载,适合想深入理解游戏循环、形状旋转与碰撞检测实现的开发者。
1. Java版俄罗斯方块源码:从课程设计到面试项目的落地判断
很多 Java 初学者在搜「Java版俄罗斯方块小游戏源码」时,真实诉求并不是要一个能玩的游戏,而是想找一个结构清晰、代码量可控、能直接跑起来、还能在课程设计或简历里讲清楚的项目。俄罗斯方块恰好卡在这个位置上:规则人人懂,逻辑不复杂,但麻雀虽小五脏俱全——Swing 事件循环、二维数组、定时器、状态机、碰撞检测、分数与等级系统,一个都不少。它不像贪吃蛇那样简单到没什么可讲,也不像完整 RPG 那样大到新手啃不动。
这篇文章不假设你手上已经有一份现成源码,而是按一线开发最常见的做法,把「Java 版俄罗斯方块」这个方向从选型、搭建、核心算法到避坑完整走一遍。你照着做,能自己写出一份可运行、可讲解、可扩展的源码;你手上如果已经有别人的代码,也能拿这套标准去判断它值不值得改、哪里是坑。适合正在做 Java 课程设计、准备 Java 基础面试、或者想用一个小项目把面向对象编程真正落地的人。
2. 技术选型与工程骨架:为什么用 Swing 而不是 JavaFX
2.1 图形库选型:Swing、JavaFX、还是纯控制台
先把这个方向最容易纠结的问题定下来:用什么画界面。俄罗斯方块是网格类游戏,画面元素就是方块,对渲染要求极低,选型的核心不是性能,而是「依赖少、能跑、好讲」。
| 方案 | 依赖 | 上手成本 | 适合场景 | 主要问题 |
|---|---|---|---|---|
| Swing | JDK 自带 | 低 | 课程设计、面试项目 | 界面偏旧,高分屏需处理缩放 |
| JavaFX | JDK 11 后需单独引入 | 中 | 想展示现代 UI | 模块化配置容易卡住新手 |
| 控制台字符画 | 无 | 极低 | 纯逻辑练习 | 无法体现事件与渲染,简历价值低 |
| LibGDX 等游戏引擎 | 第三方 | 高 | 想往游戏开发走 | 对一个小方块游戏属于杀鸡用牛刀 |
我一般会直接选 Swing。原因很实际:JDK 装好就能编译运行,不需要配 Maven 依赖、不需要处理模块化报错,评审老师或面试官拿到你的代码,双击就能看到效果。JavaFX 的界面确实更好看,但新手在module-info.java和运行时组件上翻车的概率很高,为了一个方块游戏不值得。如果你确实想用 JavaFX,逻辑层代码可以完全复用,只换渲染层即可,这也是下面要把逻辑和界面分离的原因。
提示:如果你的 JDK 版本较新,Swing 依然在
java.desktop模块里,标准 JDK 自带,不需要额外安装任何东西。
2.2 工程目录结构:把逻辑和渲染拆开
很多人写俄罗斯方块,把所有代码塞进一个Tetris.java,能跑,但一旦要改就痛苦。常见做法是按职责拆成几个类,目录结构如下:
tetris/ ├── src/ │ └── com/example/tetris/ │ ├── Main.java // 程序入口,创建窗口 │ ├── GamePanel.java // 渲染 + 键盘事件 + 游戏主循环 │ ├── GameLogic.java // 棋盘状态、碰撞、消行、计分 │ ├── Tetromino.java // 方块定义与旋转 │ └── ShapeType.java // 七种方块的枚举与形状矩阵 └── out/ // 编译输出这个拆法的核心是:GameLogic不引用任何 Swing 类,只操作二维数组和坐标。这样你以后想换 JavaFX、想加单元测试、想在面试时单独讲算法,都不会被界面代码绑死。GamePanel负责把逻辑状态画出来,并接收键盘事件转成逻辑操作。
2.3 最小可运行骨架:先让窗口出来
不要一上来就写完整游戏,先跑通「窗口 + 定时器 + 重绘」这条链路,确认环境没问题。
// Main.java package com.example.tetris; import javax.swing.*; public class Main { public static void main(String[] args) { // 所有 Swing 组件必须在事件分发线程中创建 SwingUtilities.invokeLater(() -> { JFrame frame = new JFrame("Java 俄罗斯方块"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setResizable(false); // 面板尺寸由逻辑层决定,这里先给一个占位 frame.add(new GamePanel()); frame.pack(); frame.setLocationRelativeTo(null); // 居中显示 frame.setVisible(true); }); } }// GamePanel.java(骨架版) package com.example.tetris; import javax.swing.*; import java.awt.*; public class GamePanel extends JPanel { private static final int CELL = 30; // 每个格子的像素边长 private static final int COLS = 10; // 棋盘列数 private static final int ROWS = 20; // 棋盘行数 public GamePanel() { setPreferredSize(new Dimension(COLS * CELL, ROWS * CELL)); setBackground(Color.BLACK); // 定时器驱动游戏下落,间隔越小下落越快 Timer timer = new Timer(500, e -> { // 后续在这里调用逻辑层的 tick() repaint(); }); timer.start(); } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 画网格线,确认坐标系正确 g.setColor(Color.DARK_GRAY); for (int x = 0; x <= COLS; x++) { g.drawLine(x * CELL, 0, x * CELL, ROWS * CELL); } for (int y = 0; y <= ROWS; y++) { g.drawLine(0, y * CELL, COLS * CELL, y * CELL); } } }这段代码的逻辑说明:SwingUtilities.invokeLater保证 UI 在事件分发线程创建,避免偶发的界面卡死;Timer是 Swing 自带的定时器,回调也在事件分发线程执行,所以可以直接在里面改状态再repaint,不需要额外加锁。参数上,CELL决定视觉大小,COLS和ROWS决定棋盘规模,标准俄罗斯方块是 10×20,这个比例是长期验证过的,改成 10×16 会显得太矮,改成 12×24 则方块偏小。
编译运行:
# 在项目根目录执行 javac -d out src/com/example/tetris/*.java java -cp out com.example.tetris.Main如果窗口能弹出并显示网格,说明环境、包名、编译路径都没问题,可以进入下一步。如果报NoClassDefFoundError,八成是-cp路径写错或者包名和目录对不上,这是新手最常见的翻车点之一。
3. 核心逻辑实现:七种方块、旋转与消行
3.1 用矩阵定义七种方块与旋转
俄罗斯方块有七种基本形状,业内叫 I、O、T、S、Z、J、L。最稳的定义方式是用 4×4 或 3×3 的二维数组表示每种形状,旋转就是矩阵转置加翻转。用 4×4 统一尺寸,旋转时不用改数组大小,代价是有些形状周围有空白,碰撞检测时要跳过空格。
// ShapeType.java package com.example.tetris; public enum ShapeType { I, O, T, S, Z, J, L; // 每种形状用 4x4 矩阵表示,1 表示实心格 public int[][] getMatrix() { switch (this) { case I: return new int[][]{ {0,0,0,0}, {1,1,1,1}, {0,0,0,0}, {0,0,0,0} }; case O: return new int[][]{ {0,0,0,0}, {0,1,1,0}, {0,1,1,0}, {0,0,0,0} }; case T: return new int[][]{ {0,0,0,0}, {0,1,0,0}, {1,1,1,0}, {0,0,0,0} }; // S、Z、J、L 同理,此处省略,按标准形状补全 default: return new int[4][4]; } } }逻辑说明:用枚举而不是字符串,是为了在switch里做穷举,编译器能帮你检查遗漏。矩阵里 1 的位置就是方块实际占用的格子,旋转时对整个 4×4 矩阵做顺时针旋转:
// Tetromino.java 中的旋转方法 public int[][] rotateClockwise(int[][] m) { int n = m.length; int[][] r = new int[n][n]; for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { // 顺时针旋转:新矩阵的 (j, n-1-i) 来自原矩阵的 (i, j) r[j][n - 1 - i] = m[i][j]; } } return r; }参数说明:n固定为 4,因为所有形状都用 4×4。旋转本身是纯函数,不改原矩阵,返回新矩阵,这样「旋转失败要回退」时直接丢弃新矩阵即可,不用做反向旋转,这是避免玄学 bug 的关键设计。
3.2 碰撞检测与消行:两个必须写对的函数
碰撞检测决定方块能不能移动或旋转,消行决定分数。这两个函数写错,游戏就会出现方块穿墙、消行错位等血泪问题。
// GameLogic.java 片段 private static final int COLS = 10; private static final int ROWS = 20; private int[][] board = new int[ROWS][COLS]; // 0 为空,非 0 为已固定方块 /** * 判断某个形状在 (row, col) 位置是否合法 * @param shape 4x4 形状矩阵 * @param row 形状左上角在棋盘的行 * @param col 形状左上角在棋盘的列 */ public boolean isValid(int[][] shape, int row, int col) { for (int i = 0; i < 4; i++) { for (int j = 0; j < 4; j++) { if (shape[i][j] == 0) continue; // 跳过空格 int r = row + i; int c = col + j; // 越界检查 if (c < 0 || c >= COLS || r >= ROWS) return false; // 与已固定方块重叠检查(r < 0 允许,表示方块刚生成时部分在顶部外) if (r >= 0 && board[r][c] != 0) return false; } } return true; } /** * 消除满行并返回消除的行数 */ public int clearFullRows() { int cleared = 0; for (int r = ROWS - 1; r >= 0; r--) { boolean full = true; for (int c = 0; c < COLS; c++) { if (board[r][c] == 0) { full = false; break; } } if (full) { // 把这一行以上的所有行下移一行 for (int rr = r; rr > 0; rr--) { System.arraycopy(board[rr - 1], 0, board[rr], 0, COLS); } // 顶部补空行 java.util.Arrays.fill(board[0], 0); cleared++; r++; // 因为整体下移,当前行需要重新检查 } } return cleared; }逻辑说明:isValid里r < 0不判越界,是因为方块生成时可能有一部分在棋盘上方,这是标准做法,否则新方块一出生就被判定非法。消行用System.arraycopy整行下移,比逐格赋值快且不易错;r++配合外层r--,保证下移后当前行位置重新检查,避免连续满行漏消。计分可以按消除行数给 100、300、500、800,这是经典规则,面试时能讲出「为什么不是线性」——因为一次消多行难度更高,奖励递增。
3.3 键盘事件与游戏主循环的衔接
键盘控制要处理左右移动、旋转、加速下落、直接落底。用KeyAdapter监听,把按键转成逻辑层调用,再repaint。
// GamePanel 构造函数中补充 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: logic.move(-1, 0); break; case java.awt.event.KeyEvent.VK_RIGHT: logic.move(1, 0); break; case java.awt.event.KeyEvent.VK_UP: logic.rotate(); break; case java.awt.event.KeyEvent.VK_DOWN: logic.move(0, 1); break; case java.awt.event.KeyEvent.VK_SPACE: logic.hardDrop(); break; } repaint(); } });逻辑说明:setFocusable(true)是新手最常漏的一行,漏了之后键盘完全没反应,很多人以为是监听器写错,其实是面板没拿到焦点。move、rotate、hardDrop都先调isValid判断,合法才改坐标,不合法就保持原状,这样旋转贴墙时不会穿模。hardDrop就是循环下移直到不合法,再把方块固定到棋盘并触发消行。
4. 避坑与排查:俄罗斯方块源码里最容易翻车的五件事
4.1 方块旋转后穿墙或错位
现象:方块贴着左右边界旋转时,直接嵌进墙里或者位置跳变。原因:旋转只改了形状矩阵,没有对旋转后的位置做合法性校验,或者校验时用了旋转前的坐标。解决:旋转时先算出新矩阵,用当前坐标调isValid,不合法就尝试左右各偏移一格再试(业内叫 wall kick),还不行就放弃这次旋转,保留原矩阵。不要用「旋转后再修正坐标」的写法,容易越修越乱。
4.2 消行后上方方块没有整体下移
现象:消掉一行后,上面出现空洞或者方块错位。原因:逐格复制时循环方向写反,或者只把当前行清空没有下移。解决:从满行位置向上遍历,用System.arraycopy把上一行整行复制到当前行,最后把第 0 行清空。注意外层循环要从下往上,且消行后当前行索引要回退重查。
4.3 定时器越跑越快或界面卡顿
现象:玩一会儿方块下落速度失控,或者界面一顿一顿。原因:每次重新开始游戏都new Timer但没有停掉旧定时器,多个定时器叠加;或者把耗时计算放进了paintComponent。解决:定时器作为成员变量只创建一次,用setDelay调整速度;paintComponent里只做绘制,所有逻辑计算放在定时器回调和按键回调里。
4.4 键盘没反应或方向键触发两次
现象:按方向键方块不动,或者一次移动两格。原因:面板没setFocusable(true),或者同时监听了keyPressed和keyTyped。解决:只监听keyPressed,并确保面板获得焦点;如果面板被其他组件遮挡,用requestFocusInWindow()主动请求焦点。
4.5 高分屏下方块模糊或窗口尺寸不对
现象:在 4K 屏上格子边缘发虚,或者窗口比预期大很多。原因:Swing 默认不做 DPI 缩放,CELL是逻辑像素。解决:用Toolkit.getDefaultToolkit().getScreenResolution()估算缩放,或者干脆把CELL调大、用RenderingHints开启抗锯齿。课程设计场景下,直接把CELL设成 30 到 40 之间,兼容性最好。
5. 进阶技巧:把源码变成能讲的项目
5.1 用状态机管理游戏生命周期
新手常把「运行中、暂停、结束」用一堆布尔变量表示,改起来容易漏。用一个枚举状态机更清晰:
public enum GameState { READY, RUNNING, PAUSED, GAME_OVER }定时器回调里先判断状态,只有RUNNING才执行下落;按键处理里,空格在READY时开始游戏,在RUNNING时硬降,在GAME_OVER时重开。这样面试时你能讲出「状态驱动」的设计,比堆if高一个层次。
5.2 加一个「下一个方块」预览
预览的逻辑很简单:维护一个nextShape变量,当前方块固定后,把nextShape变成当前方块,再随机生成新的nextShape。渲染时在棋盘右侧画一个小区域,把nextShape的矩阵画出来。这个功能代码量不到 30 行,但视觉完整度提升明显,课程设计答辩时很加分。
5.3 用单元测试验证核心算法
GameLogic不依赖 Swing,可以直接写 JUnit 测试。比如验证消行:
@Test public void testClearOneRow() { GameLogic logic = new GameLogic(); // 手动把最后一行填满 for (int c = 0; c < 10; c++) { logic.setCell(19, c, 1); } int cleared = logic.clearFullRows(); assertEquals(1, cleared); // 消行后最后一行应该全空 for (int c = 0; c < 10; c++) { assertEquals(0, logic.getCell(19, c)); } }这就是把逻辑和界面分离的回报:算法可以脱离界面验证,面试时你能拿出测试用例,说服力完全不一样。
5.4 难度曲线与参数调优
下落速度用Timer.setDelay控制,常见做法是每消 10 行升一级,延迟从 500ms 递减到 100ms 左右。不要线性递减到 0,否则后期方块瞬间落底,体验崩坏。我一般用delay = Math.max(100, 500 - level * 40),保证下限。分数规则用经典的四档:1 行 100、2 行 300、3 行 500、4 行 800,再乘以当前等级。
5.5 我踩过的最大一个坑
最后说个血泪教训。我早期写俄罗斯方块时,把方块坐标和棋盘坐标混用,方块用 4×4 矩阵的局部坐标,棋盘用 20×10 的全局坐标,结果在旋转和消行时反复出现「差一格」的玄学 bug,调了两个晚上。后来我强制约定:所有对外接口一律用棋盘全局坐标,形状矩阵只在isValid和绘制时临时展开。这个约定定下来之后,同类 bug 再没出现过。写小游戏最怕的不是算法难,而是坐标体系不统一。希望你一开始就把这个规矩立好,能省下大量后悔药。希望帮到你。
本文还有配套的精品资源,点击获取