简介:基于Java Swing与Eclipse实现的俄罗斯方块游戏项目,包含完整源码、开发文档与辅导视频,适合Java课程设计、毕业设计或GUI编程实践者参考。资源共60个文件,核心包括9个Java源文件与21个class文件,涵盖游戏主界面、方块切换、菜单、帮助等模块;附7个wav音效、2个mid背景音乐及6张运行截图,便于还原运行效果;另有doc/docx文档、PPT答辩模板、任务书、中期检查表及2个mp4操作讲解视频,覆盖从设计到答辩全流程。压缩包大小64.17MB,已有566人学习下载。通过源码可掌握Swing组件布局、事件监听、游戏循环与碰撞检测等关键实现,文档与视频则辅助理解项目结构和答辩准备,是一份可直接运行、适合二次开发学习的完整案例。
1. 为什么俄罗斯方块是Java Swing课程设计里最该拆的项目
每年毕设季都能见到大量“俄罗斯方块Java版”,但大多数只能看不能玩——要么方块堆叠时嵌进已有方格,要么旋转时直接飞出边界,要么键盘按下去毫无反应。这个项目把三件最容易翻车的事都处理明白了:它是基于Java Swing + Eclipse的完整实现,包含全部源码、任务书、中期检查表、答辩PPT,还附了主界面模块和游戏操作模块两个辅导视频,运行截图一应俱全。它不是一份能运行就万事大吉的应付代码,而是一套完整的课程设计交付物。适合两类人:正在做Java课程设计或毕设的学生,需要能讲清楚每个类、每条监听、每个坐标变换的源码;以及工作中要维护遗留Swing桌面工具、想快速补一波GUI线程模型和事件机制的Java工程师。毕竟现在很多企业内部工具还是Swing写的,面试里围绕Timer、EDT、键盘焦点的问题也常被翻出来问。
2. 从Eclipse空工程到双缓冲画布:窗口布局与绘制管线的搭建
拿到这套源码,第一步建议在Eclipse里新建一个Java工程,把src目录下的TetrisGame文件夹整体拖进来,确保构建路径正确后直接运行。主类继承JFrame,构造方法里用BorderLayout把窗口切成两半:中央是TetrisBoard画布面板,右侧是控制面板,放“开始”“暂停”“帮助”按钮和分数标签。这个布局在Swing课程设计里属于最稳的写法——不依赖AbsoluteLayout拖拽定位,也不用担心窗口缩放时控件变形。
2.1 JFrame与BorderLayout:主区域和侧边栏怎么切
public class TetrisGame extends JFrame { private TetrisBoard board; private JLabel scoreLabel; public TetrisGame() { setTitle("俄罗斯方块 - Java Swing"); setDefaultCloseOperation(EXIT_ON_CLOSE); setResizable(false); board = new TetrisBoard(); scoreLabel = new JLabel("分数: 0", SwingConstants.CENTER); JPanel sidePanel = new JPanel(new GridLayout(3, 1, 4, 4)); sidePanel.add(new JButton("开始")); sidePanel.add(new JButton("暂停")); sidePanel.add(scoreLabel); add(board, BorderLayout.CENTER); add(sidePanel, BorderLayout.EAST); pack(); setLocationRelativeTo(null); } }这段代码里有一个容易被忽略的细节:setResizable(false)。窗口一旦允许拉伸,画布尺寸和方块格子大小就要做动态适配,那会牵扯到重算格子边长、重绘偏移等一系列额外逻辑,对课程设计来说没必要主动跳进这个坑。pack()的作用是按子组件的首选尺寸来收紧窗口,配合setLocationRelativeTo(null)让窗口居中,这两行连用是Swing小游戏的标准开局。
2.2 绘制管线的取舍:为什么paintComponent里不自己清屏
俄罗斯方块的绘制逻辑其实非常朴素:paintComponent被调用时,先调super.paintComponent(g)清空背景,然后分三层画——棋盘底部的网格线、已固化的静态格子、当前正在下落的方块,每格用fillRect填充。整套源码没有使用图片素材,颜色只用Color常量区分七种方块,这套方案在Swing里性能足够,200ms刷新一次完全不会闪烁。如果你自己上手写过Swing绘制,大概率遇到过重影,这是因为在Swing中paint和repaint的调度不走同一条路径,直接调paint(g)会挤压UI线程。正确的做法是所有绘制逻辑集中在paintComponent,游戏循环里只调board.repaint()。这套源码里负责下落结算的updateBoard()方法末尾就是一行repaint(),其余什么都不碰,让Swing的重绘机制自己合并请求。
2.3 键盘事件和焦点:按了方向键没反应,多数是焦点问题
按键监听绑在JFrame上还是绑在画布面板上,是这个项目里一个值得答辩时展开讲的技术决策点。推荐绑在顶层窗口,因为窗口级KeyListener不会因为点击按钮或切换面板而丢失焦点。看下面这个监听器写法:
addKeyListener(new KeyAdapter() { public void keyPressed(KeyEvent e) { switch (e.getKeyCode()) { case KeyEvent.VK_LEFT: board.moveLeft(); break; case KeyEvent.VK_RIGHT: board.moveRight(); break; case KeyEvent.VK_UP: board.rotate(); break; case KeyEvent.VK_DOWN: board.softDrop(); break; case KeyEvent.VK_SPACE: board.hardDrop(); break; } } }); setFocusable(true); board.setFocusable(true);这里有两个点必须在答辩时能说清楚。一是setFocusable(true)不光要设在主窗口上,画布面板也要设置,因为JFrame默认不参与键盘焦点遍历,只有组件获得焦点后按键事件才会被派发。二是键盘监听里只调游戏逻辑方法,不做任何绘制操作,把“输入”和“渲染”分离。很多Swing小游戏卡死,就是因为监听里直接写了消行的大循环,阻塞了EDT,界面连重绘的机会都没有。
3. 七种方块的坐标矩阵:形状定义、旋转与碰撞检测
俄罗斯方块的核心不在界面,在数据结构。这套源码的棋盘是20行乘10列的int二维数组,0表示空,1表示已占用。所有方块——I型、O型、T型、S型、Z型、J型、L型——都被定义成int[][]矩阵,而不是用坐标点List。选矩阵的好处是旋转可以用统一的数学公式处理,不用为每种形状单独写分支。
3.1 用二进制形状定义七种Tetromino
private static final int[][][] SHAPES = new int[][][]{ {{1, 1, 1, 1}}, // I型:4x1 {{1, 1}, {1, 1}}, // O型:2x2 {{0, 1, 0}, {1, 1, 1}}, // T型:3x2 {{0, 1, 1}, {1, 1, 0}}, // S型:3x2 {{1, 1, 0}, {0, 1, 1}}, // Z型:3x2 {{1, 0, 0}, {1, 1, 1}}, // J型:3x2 {{0, 0, 1}, {1, 1, 1}} // L型:3x2 }; private int[][] shape; // 当前方块矩阵 private int x, y; // 方块左上角在棋盘中的坐标这套定义方式最大的好处是:所有形状都是最小包围盒矩阵,有方块的位置记1,空白位置记0。O型必须单独用2×2全1矩阵表示,如果把它放进4×4矩阵去参与旋转,中心点会偏移,界面上看起来就是方块原地跳动。在Eclipse里想验证这一点,可以在静态初始化块里循环打印每个形状的宽高,对比I型和O型的尺寸差异。新生成的方块出生点通常设在棋盘顶部中间,x = COLS / 2 - shape[0].length / 2,这样做能保证任何宽度的方块都能完整出现在棋盘上边界内。
3.2 旋转公式:转置加行逆序,一次完成90度翻转
public int[][] rotate(int[][] src) { int rows = src.length, cols = src[0].length; int[][] result = new int[cols][rows]; for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { result[j][rows - 1 - i] = src[i][j]; } } return result; }这段代码是标准的90度顺时针旋转:第i行第j列的元素,旋转后落到第j行第(rows-1-i)列。你可以在Eclipse里单步跟踪一次T型的旋转过程:初始{{0,1,0},{1,1,1}}经过转置和行逆序,得到{{0,1},{1,1},{0,1}}的翻转形态。但旋转后的结果不一定能直接放下去,因为旋转会改变方块宽度,贴着左墙或右墙时可能越界,这就是墙踢(Wall Kick)要解决的问题。常见做法是尝试旋转后偏移1格、再偏回1格,三次试探都失败才放弃旋转,实现时在rotate()方法里加一个带偏移参数的碰撞检测。
3.3 碰撞检测:先假想、再验证、成功才落子
源码里的移动和旋转方法都遵循同一套模式:先计算新坐标,再用collidesAt()验证,验证失败就保持原状。
public boolean collidesAt(int[][] shape, int newX, int newY) { for (int i = 0; i < shape.length; i++) { for (int j = 0; j < shape[0].length; j++) { if (shape[i][j] == 1) { int row = newY + i; int col = newX + j; if (col < 0 || col >= COLS || row >= ROWS) return true; if (row >= 0 && board[row][col] == 1) return true; } } } return false; }注意三个边界判定:col < 0挡左墙,col >= COLS挡右墙,row >= ROWS挡地板。这里有一个初学者容易忽略的细节——没有挡row < 0的情况,因为方块刚生成时可以有一部分悬在棋盘上边界之外,等它往下走一格自然就落进来了。这也意味着在顶部附近旋转时,旋转结果里如果行坐标是负数,collidesAt不会拦截,游戏不会崩溃。配合移动方法看:
public void moveLeft() { if (!collidesAt(shape, x - 1, y)) x--; }这个“先验证再更新”的模式在代码评审里非常加分。比直接修改坐标再回滚的方式安全得多,因为回滚会引入一个中间状态,如果这个中间状态恰好触发了一次repaint(),画面上就会出现方块钻进墙壁又弹回来的残影。
4. 下落循环与消行判定:得分、加速和Game Over的完整闭环
游戏的主循环不在main线程里,而是由javax.swing.Timer驱动。Timer每隔固定毫秒数触发一次actionPerformed,把当前方块下移一行;如果下移失败,说明方块碰到了静态格子或地板,这时把方块写进board数组固化成静态方块,然后检查满行、更新分数、生成下一个方块。这套机制的关键点是:Timer的回调运行在EDT上,可以直接操作UI组件,不需要额外加锁来保护board数组。
4.1 Timer与线程:为什么不用Thread.sleep
private Timer timer; public void startGame() { int initialDelay = 400; // 初始下落间隔,单位毫秒 timer = new Timer(initialDelay, e -> { if (!moveCurrentDown()) { fixCurrentToBoard(); int lines = clearFullRows(); updateScore(lines); spawnNextBlock(); if (collidesAt(current.shape, current.x, current.y)) { gameOver(); timer.stop(); } } }); timer.start(); } public void softDrop() { if (timer != null && timer.isRunning()) { timer.setDelay(50); // 按住下方向键时临时加速 } }用Timer而不是原生Thread.sleep的原因是Swing组件不是线程安全的。在子线程里修改board数组再调repaint(),低概率下会出现棋盘显示与实际数据不一致,因为repaint()只是发送一个重绘请求,实际绘制时机由Swing的事件分发线程决定。Timer的回调固定运行在EDT,setDelay()可以动态调整触发频率,按住下键时临时把间隔缩短到50ms,松开后恢复400ms,这样软降落的交互手感就出来了。fixCurrentToBoard()的作用是把当前方块的1值逐个写入board数组,标记该区域已被占用:
private void fixCurrentToBoard() { for (int i = 0; i < shape.length; i++) { for (int j = 0; j < shape[0].length; j++) { if (shape[i][j] == 1) { int row = y + i; int col = x + j; if (row >= 0 && row < ROWS && col >= 0 && col < COLS) { board[row][col] = 1; } } } } }这里要留意,current方块对象在生成时携带了形状矩阵和坐标,写入前要检查越界,尤其是方块还在顶部、部分行坐标为负时,row < 0的格子直接跳过,不写入board。
4.2 消行:从底部倒序扫描,命中即整体下移
clearFullRows()的实现是整套代码里最值得拆开讲解的一段:
private int clearFullRows() { int cleared = 0; for (int row = ROWS - 1; row >= 0;) { boolean full = true; for (int col = 0; col < COLS; col++) { if (board[row][col] == 0) { full = false; break; } } if (full) { cleared++; for (int r = row; r > 0; r--) { System.arraycopy(board[r - 1], 0, board[r], 0, COLS); } java.util.Arrays.fill(board[0], 0); } else { row--; } } return cleared; }这段代码的巧妙之处在于必须倒序遍历。如果从第0行往下扫,消掉一行后上方所有行整体下移,循环的row计数器却已经指到下一行,会跳过紧贴着被消行的那一行,导致连续满行时只消掉一半。倒序配合“消行后row不递减”的设计,让当前索引继续落在同一位置,因为下移后上方有新的行补到这个位置,必须再检查一次才能把连续满行消干净。System.arraycopy是Swing里搬移整行数据最省事的API,一次调用就把上一行的所有列拷贝到下一行,比嵌套for循环快且代码更短。
4.3 计分表与速度阶梯
传统俄罗斯方块规则里,一次消多行有额外奖励。这套源码的计分逻辑也是按消行数分档位,同时用等级来控制下落速度:
| 本次消除行数 | 得分 | 对应下落间隔(ms) |
|---|---|---|
| 1 | 100 | 350 |
| 2 | 300 | 280 |
| 3 | 500 | 220 |
| 4 | 800 | 150 |
实际代码会用level变量配合数组实现:每消除一行累加level,每升一级把下落间隔调小一档。改动点集中在timer.setDelay()上,不要在actionPerformed里再套一层循环去模拟加速,因为Timer本身是周期触发的,setDelay()会在下一次触发时立刻生效。Game Over的判定也在这个闭环里:新方块生成后立即检查当前位置是否与其他格子重叠,重叠就说明棋盘已经堆到顶部,游戏结束。
5. 在Eclipse里跑起来之后的调试与验证技巧
项目在Eclipse里最简化的运行方式是右键TetrisGame.java,Run As → Java Application。如果窗口弹出但方向键没反应,先检查主窗体是否调用了setFocusable(true),再检查是不是某个按钮抢走了焦点。记住一条规则:KeyListener绑在JFrame上,焦点可以交给任何组件,按键事件会冒泡到窗口处理。
调试时有三个可以快速验证逻辑的手段。第一个是在collidesAt()里打断点,这个函数每移动一步都会经过,如果方块重叠进了已有方块,检查board数组里对应格子是否已被置1,以及spawnNextBlock()的出生坐标是否正确。第二个是在clearFullRows()执行前打印整个board数组,用System.out.println逐行输出棋盘状态,肉眼确认倒序下移逻辑比盯着GUI看高效得多。第三个是把Timer的delay改成1000ms,把下落速度降到肉眼可跟踪的节奏,逐帧观察方块从生成、移动、旋转到固化的完整生命周期。这三个手段不依赖任何外部工具,纯Eclipse断点加控制台就能完成。
如果你觉得这套基于KeyListener的方案手感偏硬,可以试试Java 8之后的Swing Timer配合InputMap/ActionMap重做按键绑定,那是把“键盘事件”和“按键动作”解耦的正式做法,也让“按住连发”成为可能。另一个值得扩展的方向是本地最高分持久化,用Properties文件保存历史纪录,游戏结束时自动读写,代码量不大,但能让课程设计在答辩时多一个增量亮点。扩展这些功能不用改动棋盘数据结构和碰撞检测核心,这恰恰说明当初的矩阵建模是对的。
本文还有配套的精品资源,点击获取