新闻详情

新闻详情

首页 / 资讯中心 / 详情

Java俄罗斯方块实战:面向对象建模与游戏循环设计

发布时间:2026/10/1 9:34:50来源:尧图网络
Java俄罗斯方块实战:面向对象建模与游戏循环设计
简介这是一份基于Java Swing实现的经典俄罗斯方块游戏源码包面向Java初学者与GUI编程入门者帮助其通过完整可运行项目理解事件驱动、图形绘制、游戏循环与状态管理等核心机制。资源共19个文件含7个Java源文件涵盖主窗体、游戏逻辑、方块定义等模块、9个编译生成的class文件以及.project、.classpath和.org.eclipse.jdt.core.prefs等Eclipse工程配置文件结构规范开箱即用压缩包仅23KB轻量易导入。已有810人学习下载适合课堂实践、课程设计或自学复现。读者可直接编译运行结合配套博文中的分步实现解析与视频演示深入掌握Swing组件布局、双缓冲绘图、键盘响应与碰撞检测等关键技术点同时获得清晰的MVC式代码组织范例。1. Java俄罗斯方块老程序员花了一个周末连接中学年代不是怀旧玩具而是Java基础能力的「压力测试仪」你写过一个能真正旋转、消行、计分、暂停、碰撞检测全闭环的俄罗斯方块吗不是网上抄来的300行“Hello Tetris”而是从Block类开始建模、用Board封装状态、靠GameLoop驱动帧逻辑、在Swing里手写双缓冲绘图——这种代码现在连Java校招笔试都快绝迹了。但恰恰是它能把Java面向对象设计、集合操作、事件响应、线程控制、坐标变换这些「课本概念」一次性焊死在肌肉记忆里。我带过6届实习生凡是完整手撸过一版可玩Tetris的三个月内就能独立接手Spring Boot接口开发而只刷过LeetCode数组题的卡在DTO转VO上要两周。这不是玄学——Tetris天然包含状态机空/满/移动中、边界约束左/右/底、异步触发定时下落键盘响应、资源复用7种方块复用同一套render逻辑四大硬核场景。本文就带你用纯JavaJDK 8、零第三方依赖、不到500行核心代码把中学机房里那个绿色屏幕的感动变成你IDEA里可调试、可打断点、可单元测试的真实工程。2. 从7个方块建模开始为什么不用enum而坚持用class继承体系俄罗斯方块的7种基础形状I、O、T、S、Z、J、L表面看是静态常量但实际运行时每个方块都要携带位置、旋转态、颜色、是否锁定等动态属性。若用enum硬编码所有旋转形态比如I型有2种朝向T型有4种会导致状态爆炸——7种方块 × 平均3.5种旋转态 × 每种含4个坐标点 超过100个坐标组合维护成本极高。更致命的是enum无法动态修改坐标比如旋转后需实时计算新位置也无法挂载行为方法如canRotate()需校验边界。所以必须用面向对象建模抽象基类Tetromino定义公共接口子类按形状实现具体坐标逻辑。2.1 抽象基类Tetromino封装共性暴露契约public abstract class Tetromino { protected int x, y; // 当前方块左上角基准坐标以格子为单位 protected int rotation; // 0-3对应0°/90°/180°/270° protected final Color color; public Tetromino(Color color) { this.color color; this.x 5; // 初始出生列居中 this.y 0; // 初始出生行顶部 this.rotation 0; } // 子类必须提供4个格子的相对坐标相对于基准点x,y public abstract int[][] getShape(); // 旋转核心顺时针90° (x,y) → (y, -x)再平移回原点 public void rotate() { rotation (rotation 1) % 4; } // 获取当前所有格子的绝对坐标用于碰撞检测 public int[][] getAbsolutePositions() { int[][] relative getShape(); int[][] absolute new int[4][2]; for (int i 0; i 4; i) { absolute[i][0] x relative[i][0]; absolute[i][1] y relative[i][1]; } return absolute; } // 移动接口由GameController调用 public void moveDown() { y; } public void moveLeft() { x--; } public void moveRight() { x; } }提示getShape()返回的是相对坐标而非绝对坐标这是关键设计。例如I型方块在rotation0时返回{{0,0},{1,0},{2,0},{3,0}}rotation1时返回{{0,-1},{0,0},{0,1},{0,2}}——所有子类只需覆盖此方法无需关心坐标系转换。2.2 具体子类实现以T型为例展示旋转矩阵的实际应用T型方块的4种旋转态不能靠肉眼硬记要用坐标变换公式推导。以中心点为原点实际取中间格子为pivot顺时针90°旋转公式为(x,y) → (y, -x)。T型默认形态rotation0的4个格子相对坐标设为{{0,0},{-1,0},{1,0},{0,-1}}中心左右上则rotation0:{{0,0},{-1,0},{1,0},{0,-1}}rotation1: 应用(x,y)→(y,-x)得{{0,0},{0,1},{0,-1},{1,0}}→ 整理为{{0,0},{0,1},{0,-1},{1,0}}rotation2: 再次变换得{{0,0},{-1,0},{1,0},{0,1}}rotation3: 最终得{{0,0},{0,-1},{0,1},{-1,0}}public class TetrominoT extends Tetromino { private static final int[][][] SHAPES { {{0,0},{-1,0},{1,0},{0,-1}}, // 0° {{0,0},{0,1},{0,-1},{1,0}}, // 90° {{0,0},{-1,0},{1,0},{0,1}}, // 180° {{0,0},{0,-1},{0,1},{-1,0}} // 270° }; public TetrominoT() { super(Color.PINK); } Override public int[][] getShape() { return SHAPES[rotation]; } }参数说明SHAPES是三维数组第一维索引rotation第二维是4个格子第三维是[x,y]。这样避免每次旋转都重新计算性能提升明显。其他方块I/O/S/Z/J/L同理实现I型只需2种形态水平/垂直O型固定不变rotation无意义S/Z型各2种J/L型各4种。2.3 方块工厂用Map替代switch支持运行时扩展当新增方块类型比如自定义的「闪电型」时不应修改游戏主逻辑。采用策略模式用Map注册方块构造器public class TetrominoFactory { private static final MapString, SupplierTetromino registry new HashMap(); static { registry.put(I, TetrominoI::new); registry.put(O, TetrominoO::new); registry.put(T, TetrominoT::new); registry.put(S, TetrominoS::new); registry.put(Z, TetrominoZ::new); registry.put(J, TetrominoJ::new); registry.put(L, TetrominoL::new); } public static Tetromino createRandom() { String[] keys registry.keySet().toArray(new String[0]); String key keys[new Random().nextInt(keys.length)]; return registry.get(key).get(); } // 支持热插拔运行时注册新方块 public static void register(String name, SupplierTetromino constructor) { registry.put(name, constructor); } }为什么不用switch因为switch编译期绑定新增类型需改源码而Map支持插件化扩展。面试官问「如何让游戏支持Mod」时这就是标准答案。3. 游戏主干用Swing Timer做游戏循环而不是Thread.sleep()很多新手用while(true) { update(); render(); Thread.sleep(50); }写主循环这会导致UI冻结Swing非线程安全、帧率不可控、暂停功能失效。正确做法是用javax.swing.Timer——它在EDTEvent Dispatch Thread中触发天然兼容Swing组件更新且stop()/start()可精准控制暂停/继续。3.1GamePanel双缓冲绘图避免闪烁public class GamePanel extends JPanel implements ActionListener { private static final int BOARD_WIDTH 10; private static final int BOARD_HEIGHT 20; private final TetrisBoard board; private Tetromino currentPiece; private Timer gameTimer; private int score; public GamePanel() { this.board new TetrisBoard(BOARD_WIDTH, BOARD_HEIGHT); this.currentPiece TetrominoFactory.createRandom(); this.gameTimer new Timer(500, this); // 初始下落间隔500ms this.score 0; setPreferredSize(new Dimension(BOARD_WIDTH * 30, BOARD_HEIGHT * 30)); setBackground(Color.BLACK); setFocusable(true); requestFocusInWindow(); // 键盘监听方向键控制空格旋转P暂停 addKeyListener(new KeyAdapter() { Override public void keyPressed(KeyEvent e) { switch (e.getKeyCode()) { case KeyEvent.VK_LEFT - movePiece(-1, 0); case KeyEvent.VK_RIGHT - movePiece(1, 0); case KeyEvent.VK_DOWN - movePiece(0, 1); case KeyEvent.VK_UP - rotatePiece(); case KeyEvent.VK_SPACE - dropPiece(); case KeyEvent.VK_P - togglePause(); } } }); } Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2d (Graphics2D) g; g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // 双缓冲先画到BufferedImage再整体贴到面板 BufferedImage buffer new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB); Graphics2D bufG2d buffer.createGraphics(); // 绘制游戏板已锁定的方块 for (int row 0; row board.getHeight(); row) { for (int col 0; col board.getWidth(); col) { Color c board.getCellColor(row, col); if (c ! null) { drawCell(bufG2d, col, row, c); } } } // 绘制当前活动方块 if (currentPiece ! null) { int[][] positions currentPiece.getAbsolutePositions(); for (int[] pos : positions) { int col pos[0]; int row pos[1]; if (row 0 row board.getHeight() col 0 col board.getWidth()) { drawCell(bufG2d, col, row, currentPiece.color); } } } g2d.drawImage(buffer, 0, 0, null); bufG2d.dispose(); } private void drawCell(Graphics2D g, int col, int row, Color color) { int x col * 30; int y row * 30; g.setColor(color); g.fillRect(x 1, y 1, 28, 28); g.setColor(color.darker()); g.drawRect(x 1, y 1, 28, 28); } }关键细节paintComponent中创建BufferedImage作为离屏缓冲区所有绘制操作先在bufG2d上完成最后g2d.drawImage()一次性贴图。这彻底解决Swing重绘闪烁问题。drawCell里的1和28是为了留出1像素边框视觉更清爽。3.2actionPerformed游戏逻辑的唯一入口Override public void actionPerformed(ActionEvent e) { if (!isPaused !isGameOver()) { // 1. 尝试下落 if (!tryMove(currentPiece, 0, 1)) { // 下落失败锁定当前方块生成新方块 lockPiece(); clearFullRows(); currentPiece TetrominoFactory.createRandom(); // 检查新方块是否能生成出生点被占即Game Over if (!isValidPosition(currentPiece)) { gameOver(); } } } } private boolean tryMove(Tetromino piece, int dx, int dy) { int oldX piece.x; int oldY piece.y; piece.x dx; piece.y dy; if (isValidPosition(piece)) { return true; } else { // 撤销移动 piece.x oldX; piece.y oldY; return false; } } private void lockPiece() { int[][] positions currentPiece.getAbsolutePositions(); for (int[] pos : positions) { int row pos[1]; int col pos[0]; if (row 0) { board.setCellColor(row, col, currentPiece.color); } } }为什么tryMove要撤回坐标因为moveLeft()等方法直接修改piece.x/y若不撤回下次getAbsolutePositions()会基于错误坐标计算导致碰撞检测失准。这是新手最易翻车的点——以为move()只是临时尝试忘了它已永久改变状态。4. 碰撞检测与消行逻辑别用for嵌套if用位运算加速标准俄罗斯方块的碰撞检测需判断4个格子是否全部在界内且未被占用。常见写法是遍历positions数组逐个检查board[row][col]是否为空。但当消行后需重绘整个板面时这种O(n)遍历会成为性能瓶颈。更优解是用long型位图表示一行10列 → 10位用位运算批量判断。4.1TetrisBoard用long[]替代Color[][]public class TetrisBoard { private final int width; private final int height; private final long[] rows; // 每行用一个long存储bit0~bit9表示col0~col9 private final Color[] colors; // 对应每格的颜色仅用于渲染 public TetrisBoard(int width, int height) { this.width width; this.height height; this.rows new long[height]; this.colors new Color[height * width]; } // 检查某位置是否为空位图颜色数组双校验 public boolean isEmpty(int row, int col) { if (row 0 || row height || col 0 || col width) { return false; // 越界视为占用 } return (rows[row] (1L col)) 0; } // 设置格子为占用 public void setOccupied(int row, int col, Color color) { if (row 0 row height col 0 col width) { rows[row] | (1L col); colors[row * width col] color; } } // 获取某行的占用位图用于消行检测 public long getRowBits(int row) { return rows[row]; } // 清除指定行位图清零颜色置null public void clearRow(int row) { rows[row] 0L; Arrays.fill(colors, row * width, (row 1) * width, null); } }位运算原理1L col生成第col位为1的掩码rows[row] mask结果为0表示该位空闲。rows[row] | mask将该位置1。10列只需10位long64位绰绰有余且位运算比数组访问快10倍以上。4.2 消行检测一行到位图全1即消行private void clearFullRows() { int cleared 0; for (int row 0; row board.getHeight(); row) { long rowBits board.getRowBits(row); // 检查低width位是否全1(1 width) - 1 生成width个1的掩码 long fullMask (1L board.getWidth()) - 1; if ((rowBits fullMask) fullMask) { board.clearRow(row); cleared; // 向下移动所有上方行 for (int r row; r 0; r--) { board.copyRow(r - 1, r); // 自定义copyRow方法 } row--; // 重新检查当前行因上方行已下移 } } if (cleared 0) { // 计分单行100双行300三行500四行800 int points new int[]{0, 100, 300, 500, 800}[Math.min(cleared, 4)]; score points; // 加速每消10行下落间隔减50ms最低100ms int newDelay Math.max(100, 500 - (score / 1000) * 50); gameTimer.setDelay(newDelay); } }注意copyRow需用System.arraycopy高效复制位图和颜色数组避免逐格赋值。消行后row--是关键——否则会跳过刚下移至当前位置的行。4.3 边界碰撞优化用预计算掩码替代实时计算对于I型方块4格直线其4种形态的相对坐标可预先计算成位掩码水平态0b0000001111低4位为1垂直态0b0000000001仅bit0为1但需考虑4行实际中我们对每个方块形态预存「占用掩码」和「行偏移」检测时只需rows[r offset] mask一次到位。这部分代码因篇幅略去但它是让游戏在低端笔记本上仍保持60FPS的核心。5. 避坑指南老程序员血泪总结的5个必踩深坑写俄罗斯方块看似简单但90%的开源实现都栽在以下细节。这些坑不会报错但会让游戏「感觉不对」——比如方块穿墙、旋转卡死、消行后残影、暂停时计时器还在跑。以下是我在3个不同版本中反复验证的避坑清单5.1 现象方块在墙边旋转时部分格子消失原因旋转后未做「墙壁碰撞校正」。例如T型在右边界旋转右侧格子越界但代码只检查「是否全在界内」没把越界的格子往左拉。解决在rotate()后立即调用adjustToWall()计算所有格子的minX/maxX若越界则整体平移private void adjustToWall() { int[][] positions currentPiece.getAbsolutePositions(); int minX Arrays.stream(positions).mapToInt(p - p[0]).min().orElse(0); int maxX Arrays.stream(positions).mapToInt(p - p[0]).max().orElse(0); int shift 0; if (minX 0) shift -minX; if (maxX board.getWidth()) shift board.getWidth() - 1 - maxX; currentPiece.x shift; // 注意这里修改x不是positions数组 }5.2 现象连续按↓键下落速度越来越快松开后还多下一行原因KeyAdapter.keyPressed被重复触发系统自动重复但tryMove(0,1)未做防抖导致一帧内多次下落。解决添加按键状态标记只在首次按下时生效private final SetInteger pressedKeys new HashSet(); Override public void keyPressed(KeyEvent e) { int code e.getKeyCode(); if (!pressedKeys.contains(code)) { pressedKeys.add(code); // 处理按键... } } Override public void keyReleased(KeyEvent e) { pressedKeys.remove(e.getKeyCode()); }5.3 现象暂停时背景音乐还在响或计时器未停止原因gameTimer.stop()只停了下落但KeyAdapter仍在监听paintComponent持续重绘CPU占用100%。解决暂停时需三停——停Timer、停KeyAdaptersetFocusable(false)、停repaintsetVisible(false)临时隐藏面板。恢复时反向操作。5.4 现象消行动画卡顿尤其多行同时消除原因clearFullRows()中copyRow用for循环逐格复制10×20板面每次消4行需复制800次。解决用System.arraycopy批量复制public void copyRow(int srcRow, int destRow) { System.arraycopy(rows, srcRow, rows, destRow, 1); int start srcRow * width; int end start width; System.arraycopy(colors, start, colors, destRow * width, width); }5.5 现象游戏结束时最后一行方块残留或计分显示错误原因gameOver()调用时机错误——在lockPiece()后立即检查但此时新方块尚未生成isValidPosition()检测的是旧方块位置。解决lockPiece()后先生成新方块再检测lockPiece(); clearFullRows(); currentPiece TetrominoFactory.createRandom(); if (!isValidPosition(currentPiece)) { gameOver(); // 此时检测新方块的出生点 }6. 进阶技巧用JUnit 5写可验证的游戏逻辑单元测试写完能玩的俄罗斯方块只是第一步。真正的工程能力体现在——你能用单元测试证明rotate()不会越界、tryMove()在墙边返回false、clearFullRows()准确计算得分。我坚持给每个核心方法配测试这让我在重构时敢删代码、敢改算法。6.1 测试TetrominoT.rotate()的4种形态class TetrominoTTest { Test void testRotationCycle() { TetrominoT t new TetrominoT(); // rotation0: center left right top assertArrayEquals(new int[][]{{0,0},{-1,0},{1,0},{0,-1}}, t.getShape()); t.rotate(); // rotation1: center up down right assertArrayEquals(new int[][]{{0,0},{0,1},{0,-1},{1,0}}, t.getShape()); t.rotate(); assertArrayEquals(new int[][]{{0,0},{-1,0},{1,0},{0,1}}, t.getShape()); t.rotate(); assertArrayEquals(new int[][]{{0,0},{0,-1},{0,1},{-1,0}}, t.getShape()); t.rotate(); // 回到初始态 assertArrayEquals(new int[][]{{0,0},{-1,0},{1,0},{0,-1}}, t.getShape()); } }为什么测旋转循环因为手动算坐标极易出错且面试官最爱问「如何保证旋转不丢失格子」。这个测试就是你的「后悔药」——改代码前先跑test绿了才敢提交。6.2 测试TetrisBoard的消行逻辑Test void testClearFourRows() { TetrisBoard board new TetrisBoard(10, 20); // 填满最后4行 for (int r 16; r 20; r) { for (int c 0; c 10; c) { board.setOccupied(r, c, Color.RED); } } // 模拟消行 int cleared countFullRows(board); // 自定义辅助方法 assertEquals(4, cleared); // 检查第16行原第16行是否清空 assertEquals(0L, board.getRowBits(16)); // 检查第0行原第16行是否有内容应被下移的行填充 assertFalse(board.isEmpty(0, 0)); // 原第16行第0列的内容已移到第0行 }6.3 测试GamePanel的暂停/继续状态机Test void testPauseResume() { GamePanel panel new GamePanel(); // 初始状态Timer启动 assertTrue(panel.gameTimer.isRunning()); panel.togglePause(); assertFalse(panel.gameTimer.isRunning()); panel.togglePause(); assertTrue(panel.gameTimer.isRunning()); // 关键验证暂停时移动操作应被忽略 Tetromino originalPiece panel.currentPiece; panel.movePiece(1, 0); // 向右移动 assertEquals(originalPiece.x, panel.currentPiece.x); // x未变 }我的习惯每个.java文件对应一个Test.java测试覆盖率目标85%。不是为了应付SonarQube而是因为——当你发现rotate()在rotation3时返回了错误坐标而测试早在你改代码前就红了那种踏实感比通关俄罗斯方块还上头。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

支付行业中的T0、T1、TS、D1是什么 2026/10/1 10:23:18

支付行业中的T0、T1、TS、D1是什么

1、定义这些字母是支付行业描述结算到账周期的通用缩写,核心先记住三个字母:T Trade(交易日):仅指工作日,周末和法定节假日不算D Day(自然日):一年 365 天都算&#xf…

阅读更多 →
nanoGPT OpenWebText 数据管线拆解:801 万篇网页如何变成 90 亿个 token 2026/10/1 10:23:18

nanoGPT OpenWebText 数据管线拆解:801 万篇网页如何变成 90 亿个 token

nanoGPT OpenWebText 数据管线拆解:801 万篇网页如何变成 90 亿个 token 【免费下载链接】nanoGPT The simplest, fastest repository for training/finetuning medium-sized GPTs. 项目地址: https://gitcode.com/GitHub_Trending/na/nanoGPT 8,013,769 篇网…

阅读更多 →
Radioss的1D单元应用 2026/10/1 10:23:18

Radioss的1D单元应用

这篇教程里&#xff0c;你会学到哪些1D单元&#xff1f;<汽车中1D单元运用示例>TRUSS TYPE2杆单元两个节点构成的杆单元具有一个自由度&#xff0c;只能描述压缩/拉伸性质。与线弹性材料LAW1和弹塑性材料LAW2兼容。需要定义横截面积和初始长度&#xff1a;BEAM TYPE3梁单…

阅读更多 →
Agent 的困境与运筹学解法:从“会聊天”到“会决策” 2026/10/1 10:23:18

Agent 的困境与运筹学解法:从“会聊天”到“会决策”

1. 引言 2025 年以来&#xff0c;Agent&#xff08;智能体&#xff09;成为大模型落地最热的方向。从自动写代码、操作浏览器&#xff0c;到多 Agent 协作完成复杂任务&#xff0c;Agent 展现出惊人的潜力。然而&#xff0c;随着应用深入&#xff0c;一系列问题也开始集中暴露&…

阅读更多 →
OpenBMC:传感器不显示问题排查 2026/10/1 10:23:17

OpenBMC:传感器不显示问题排查

OpenBMC&#xff1a;传感器不显示问题排查 1. 先定位数据消失的位置 传感器从硬件到页面通常经过&#xff1a; 硬件 → 内核驱动 → hwmon → Sensor 服务 → D-Bus → Redfish → WebUI“页面没有传感器”可能发生在其中任意一层。首先判断它是完全没有创建&#xff0c;还是已…

阅读更多 →
保险代理人 GEO 实操:让 AI 推荐你作为本地家庭保障顾问 2026/10/1 10:23:11

保险代理人 GEO 实操:让 AI 推荐你作为本地家庭保障顾问

真实案例&#xff1a;一位在二线城市从业 6 年的保险代理人&#xff0c;过去主要靠熟人转介绍获客&#xff0c;每月新增咨询量长期在个位数徘徊。她按照 GEO 方法&#xff0c;先统一了全网执业信息&#xff0c;再围绕「重疾险保额怎么定」「网上买保险理赔麻烦吗」等高频问题&a…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉