ARTICLE DETAIL

资讯详情

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

Java Swing俄罗斯方块课程设计:从布局到碰撞检测的完整实现

Java Swing俄罗斯方块课程设计:从布局到碰撞检测的完整实现 简介基于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 用二进制形状定义七种Tetrominoprivate 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.sleepprivate 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的回调固定运行在EDTsetDelay()可以动态调整触发频率按住下键时临时把间隔缩短到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)1100350230028035002204800150实际代码会用level变量配合数组实现每消除一行累加level每升一级把下落间隔调小一档。改动点集中在timer.setDelay()上不要在actionPerformed里再套一层循环去模拟加速因为Timer本身是周期触发的setDelay()会在下一次触发时立刻生效。Game Over的判定也在这个闭环里新方块生成后立即检查当前位置是否与其他格子重叠重叠就说明棋盘已经堆到顶部游戏结束。5. 在Eclipse里跑起来之后的调试与验证技巧项目在Eclipse里最简化的运行方式是右键TetrisGame.javaRun 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文件保存历史纪录游戏结束时自动读写代码量不大但能让课程设计在答辩时多一个增量亮点。扩展这些功能不用改动棋盘数据结构和碰撞检测核心这恰恰说明当初的矩阵建模是对的。本文还有配套的精品资源点击获取
返回列表