ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

俄罗斯方块Java源码解析:从Swing渲染到碰撞检测的完整实战

俄罗斯方块Java源码解析:从Swing渲染到碰撞检测的完整实战

简介:这是一份使用Java语言实现的经典俄罗斯方块完整源码,适合正在学习Java基础语法、面向对象编程或游戏开发入门的读者参考。整个压缩包共包含38个文件,其中有3个Java源文件、14个编译后的class字节码文件、18张游戏界面或过程截图,以及Eclipse项目的配置信息,整体体积仅170KB,结构一目了然,导入开发环境后即可运行查看。该资源已有2785人学习,是一个热度不错的小型游戏练手项目。源码按照面向对象思想拆分为GameBoard、Shape、Controller、Main等核心类,完整覆盖游戏循环、方块随机生成与旋转、边界碰撞检测、满行消除计分、游戏结束判断以及键盘交互控制等关键逻辑。通过研读这些代码,读者既能理解一个可运行小游戏从初始化到结束的完整流程,也能掌握模块拆分、状态管理和事件响应等实用技巧;在此基础上继续扩展,还可以加入加速下落、不同难度等级或排行榜功能,很适合作为课程设计或自学实践的有益参考。

1. 俄罗斯方块Java源码:一个能跑、能改、能二次开发的完整游戏项目

某开发者想找一个“能跑起来、能看懂、能改着玩”的Java源码练手项目,发现控制台版本太枯燥,纯算法版本又看不到界面效果,最后锁定了俄罗斯方块源码。反直觉的是,很多人以为它核心只是“下落和消除”,真正拆的时候才发现,旋转、碰撞、落定、消行之间的状态流转才最有嚼劲。这份Java源码用Swing绘制界面、Timer驱动循环、AWT监听键盘,从编译到跑起来不需要额外依赖,适合刚学完基础想读完整工程的初学者,也适合要做课设参考、想加双人模式或AI自动玩的开发者。如果你只看过算法题版的俄罗斯方块,这份带界面的源码会给你一个完整视角:一个输入事件从按下键盘到画面变化,中间经过了哪些调用。

2. 项目结构与技术选型:主循环、事件监听与渲染怎么分工

2.1 目录结构与核心类说明

一份Java源码拿到手,我一般先不看代码,先看目录和文件名,文件名能直接反映模块边界。这个项目的目录结构大致长这样:

tetris-src/ ├── src/ │ └── com/ │ └── tetris/ │ ├── GameFrame.java // 主窗口,组装面板与启动游戏 │ ├── GamePanel.java // 游戏面板,负责所有绘制 │ ├── TetrisGame.java // 游戏主逻辑,状态机与规则 │ ├── Shape.java // 方块形状定义与旋转 │ ├── Grid.java // 20x10 网格与落定方块存储 │ └── ScoreManager.java // 计分、等级与最高分 ├── res/ │ └── scores.properties // 最高分存档 └── README.md // 编译命令、操作说明、类关系
类名职责关键方法
GameFrame窗口与事件入口createUI()、addKeyListener()
GamePanel界面刷新与绘制paintComponent()、drawGrid()
TetrisGame游戏逻辑控制start()、rotate()、move()、stepDown()
Shape形状数据模型getRotatedCells()、getCells()
Grid网格与落定存储isValidPosition()、place()、clearLines()
ScoreManager计分与持久化addScore()、loadHighScore()

每个类大约是几十到两百行不等。读代码时我会用IDE的Structure面板看一遍方法列表,确认类之间的依赖方向。拆这个项目时的直观感受是:TetrisGame是“大脑”,GamePanel是“显示器”,Shape和Grid是“骨骼”。改游戏规则时主要动TetrisGame,改画面表现时基本只动GamePanel,这个分工对二次开发来说非常友好,你不用在几千行里找一处碰撞判断,所有逻辑都按包名和类名归好了位置。

2.2 技术选型:Swing 加上 Timer,为什么不用 JavaFX

这个项目用Swing做界面是合理的选择。Swing是JDK自带的GUI工具包,不需要引入第三方依赖,在任何装了JDK的机器上都能编译运行,这对一个“下载下来就能跑”的源码项目来说是巨大优势。相比之下,JavaFX在部分JDK版本里已经不再默认携带,需要额外配置模块,反而抬高了跑起来的前置门槛。另一个选择是纯控制台输出,但那种实现很难讲清楚“事件驱动”是怎么回事,也失去了游戏类Demo最直观的反馈。

游戏循环用的是javax.swing.Timer而不是while(true)线程循环,这一点值得展开说说。核心代码也就是几行:

Timer timer = new Timer(dropInterval, e -> { if (isPlaying) { stepDown(); } }); timer.start();

这段代码里的dropInterval是方块每次下落的间隔毫秒数,1000就是每秒下落一格。Timer跑在事件分发线程(EDT)上,不会像while循环那样把逻辑更新和界面刷新挤成两条互相干扰的线程。常见做法是把速度控制交给Timer的delay参数,需要加速时直接调用timer.setDelay(),不需要手动管理休眠和唤醒。这里有一个值得注意的边界:如果你的二次开发里要加入暂停功能,不要调用timer.stop()后忘记在恢复时调用timer.start(),否则游戏会静默卡住,界面完全没有任何报错。

键盘输入走的是KeyListener接口,Swing的键盘事件监听在面板setFocusable(true)之后才会生效,这是新手很容易翻车的点——窗口画出来了但按方向键没反应,一查发现焦点没设置。关于这部分踩坑细节,第4章的排查清单里会单独列出。

还有个容易被忽略的细节是绘制方式。GamePanel继承JPanel并重写paintComponent(),而不是paint(),原因很简单:paint()还会处理边框和子组件的绘制,paintComponent()只负责画自己,性能更好也更规范。所有游戏元素都在这个方法里通过Graphics2D画出来,包括背景网格、当前方块、已落定的方块和分数预览。用Swing做2D游戏虽然不如游戏引擎专业,但胜在概念透明,你能清楚看到每一次重绘是怎么被触发的,这对学习渲染与逻辑分离是很好的范本。

3. 核心模块拆解:一个方块从生成、下落到消除的完整链路

3.1 形状定义与旋转算法

俄罗斯方块最基础的数据结构是形状。标准的7种方块(I、O、T、S、Z、J、L)如果只用二维数组表示,旋转时写起来会很别扭。常见做法是把每种方块拆成4个相对坐标点,以自身中心为原点定义初始朝向:

public enum ShapeType { I(new int[][]{{-1, 0}, {0, 0}, {1, 0}, {2, 0}}), O(new int[][]{{0, 0}, {1, 0}, {0, 1}, {1, 1}}), T(new int[][]{{-1, 0}, {0, 0}, {1, 0}, {0, 1}}), // S、Z、J、L 省略,结构相同 ; private final int[][] cells; ShapeType(int[][] cells) { this.cells = cells; } }

每个int[]{x, y}表示一个方块相对形状中心的位置。把形状和网格分离的好处是,碰撞检测可以统一变成“形状坐标 + 偏移坐标”的组合判断,旋转时也不必遍历整个矩阵,只对4个点做坐标换算即可。方块生成时,TetrisGame从枚举里随机取一个类型,同时设置初始偏移位置在网格顶部的中间列,这个初始偏移决定游戏难度:偏移列越小,玩家反应时间越短。

旋转用数学上的旋转公式实现:顺时针旋转90度后,(x, y)变成(-y, x)。代码里把它提前算好,做一个坐标映射:

public int[][] getRotatedCells() { int[][] rotated = new int[4][2]; for (int i = 0; i < cells.length; i++) { rotated[i][0] = -cells[i][1]; rotated[i][1] = cells[i][0]; } return rotated; }

这里没有用矩阵乘法,是因为四向旋转本质上就是坐标互换加符号变换,“-y变x、x变y就是顺时针90度”这个规律记住就够了。O形方块旋转后和原来一样,实际游戏里可以跳过旋转逻辑,但保留通用实现更利于代码统一。旋转之前,TetrisGame会先调用canRotate()检查旋转后的位置是否越界或撞到已落定方块,检查通过才真正替换当前形状的坐标。这个顺序不能反,先旋转再检查会导致方块转进网格墙里,画面表现就是“半个方块嵌在墙里”。

3.2 碰撞检测与落定逻辑

碰撞检测是整个游戏最频繁调用的逻辑,每一帧下落、左右移动、旋转都要跑一遍。网格用20行10列的二维数组存储,0表示空格,1表示已被占用。判断“当前位置是否合法”的代码是:

public boolean isValidPosition(int[][] cells, int offsetX, int offsetY) { for (int[] cell : cells) { int x = offsetX + cell[0]; int y = offsetY + cell[1]; if (x < 0 || x >= COLS || y >= ROWS) return false; if (y >= 0 && grid[y][x] != 0) return false; } return true; }

offsetX、offsetY是形状在网格中的基准坐标,也就是形状中心所在格子。x越界判断是撞墙,y大于等于ROWS代表落到网格底部以下;y小于0允许存在,是因为方块在顶部队列里还没完全进入可视区域。第二个if里专门加y >= 0再检查重叠,是防止把“还没进入网格的方块”误判成和顶部已落定方块冲突,这个细节容易漏,漏了会导致新方块一格都落不下来,游戏一开局就卡死。

当方块向下移动时发现不合法,就触发落定逻辑:

public void place(int[][] cells, int offsetX, int offsetY) { for (int[] cell : cells) { int x = offsetX + cell[0]; int y = offsetY + cell[1]; grid[y][x] = 1; } }

落定之后立即调用clearLines()检查是否有整行都是1,把满足条件的行从数组中移除,并在顶部补一行空格。这个“先落定、再消行、最后生成下一块”的顺序是固定的,顺序反了会出现已落定方块还没写入网格就生成下一块的情况,导致新方块直接与旧方块重叠。这个bug我自己写的时候踩过,现象是方块明明没到底却卡在半空,排查半天才发现是place()在clearLines()之后才被调用。

3.3 消行判断与计分

消行逻辑不复杂,但对性能有一点要求。每次落定一个方块都要从上往下扫描整个网格,如果某一行grid[row][col]全不为0,就把它消除:

public int clearLines() { int lines = 0; for (int row = 0; row < ROWS; row++) { boolean full = true; for (int col = 0; col < COLS; col++) { if (grid[row][col] == 0) { full = false; break; } } if (full) { lines++; for (int r = row; r > 0; r--) { grid[r] = Arrays.copyOf(grid[r - 1], COLS); } Arrays.fill(grid[0], 0); } } return lines; }

这段代码有一个容易被忽视的点:消掉一行后,当前扫描位置已经被上方数据填满,所以不能重置循环,要继续检查当前row索引是否又变成满行,否则会出现“连续消行时漏掉其中一行”的情况。计分规则是常见做法:1行得100分、2行得300分、3行得500分、4行得800分,连消和一次落定消多行的奖励比单消累加更高,这个规则集中在ScoreManager里维护,改起来不会牵连到界面代码。

计分模块还有一个容易处理错的地方:同时消除多行时,得分计算必须在全部消行完成后统一结算,不能消一行算一次,否则“一次四行”就享受不到额外奖励了。实际开发里一般用一个ArrayList收集所有满行的行号,全部收集完再统一删除,比边扫描边删除更容易保证行号不偏移。游戏结束判断也放在新方块生成时:生成后的起始位置调用isValidPosition()返回false,说明网格已经堆到顶部,游戏置为over状态。

4. 本地运行与常见问题排查:编译启动、乱码与键盘失灵

4.1 三步把项目跑起来

从下载源码到看到游戏窗口,正常情况不超过两分钟。前提是机器上已经装了JDK,版本8以上即可,不需要任何额外库。打开终端进到项目根目录,执行:

javac -encoding UTF-8 src/com/tetris/*.java -d out

这条命令把所有Java文件编译到out目录。加-encoding参数是防止Windows平台默认GBK编码导致中文注释乱码,Linux和macOS可以不加,但加上更保险。编译完成后启动游戏:

java -cp out com.tetris.GameFrame

如果main方法写在TetrisGame类里,把第二行换成java -cp out com.tetris.TetrisGame。

提示:如果机器上同时装了多个JDK版本,先执行 java -version 确认默认版本,防止高版本语法写在源码里、老版本编译报错。这类错误提示里会直接指出不支持的内容,按提示调低语法级别或切换JDK即可。

速度控制也在这个阶段可以快速验证:修改TetrisGame里Timer构造器的dropInterval值,比如从1000改成300,游戏会立刻变快,这是最简单的“改动可见”测试,适合确认整个编译运行链路没问题。

4.2 常见问题排查:乱码、键盘无响应、画面闪烁

拆这套源码时最常撞见的问题是下面三个,按现象、原因、解决列出来。

问题一:编译时中文乱码。现象是javac报“不可映射的字符”错误,或者编译出来的class文件里中文注释变成问号。原因是源码文件是UTF-8编码,而Windows终端默认命令行的编码不是UTF-8。解决方法是编译命令里显式加上-encoding UTF-8。如果源码本身是GBK编码,就换成-encoding GBK,以实际文件编码为准,不要盲目复制命令。用IDE打开时也要检查右下角Encoding显示的是UTF-8还是系统默认编码,这决定了你在IDE里改代码后重新编译是否还会乱码。

问题二:方向键没有反应。现象是窗口正常显示,方块正常下落,但按上下左右完全没反应。原因九成是窗口没有获得焦点:JPanel默认focusable是false,键盘事件不会派发到面板上。解决办法是在GameFrame里加panel.setFocusable(true),然后在窗口显示后调用panel.requestFocusInWindow()。还有一个衍生坑是鼠标点了一下游戏窗口外部再点回来,焦点丢失后按键又失灵,需要在焦点丢失时重新请求,常见做法是给窗口加WindowListener,在windowActivated里重新requestFocusInWindow。

问题三:画面闪烁。现象是格子下落时窗口闪得厉害,尤其在大屏高分辨率下特别明显。原因是绘制路径绕过了Swing的双缓冲机制。JPanel默认isDoubleBuffered是true,但如果自己重写了paint()而不是paintComponent(),或者用Timer把刷新间隔调到10毫秒以下,就会把双缓冲破坏掉。解决办法是确认只重写paintComponent(),并且不要在paintComponent里new对象,把绘制用的颜色、字体、矩形对象提前创建好,绘制方法里只做draw操作。

第四个问题比较隐蔽:玩到一定分数后游戏闪退,控制台打印NullPointerException堆栈。原因一般是ScoreManager读取最高分存档时文件不存在,读取逻辑没有做try-catch。解决方法是改读取代码为“文件不存在就用0分初始化”,或者在res目录下手动创建scores.properties并写一行score=0,前者更省事也更健壮。

5. 二次开发改造思路:从预览队列到AI自动玩,动刀前先看这几个关键入口

5.1 增加幽灵方块与预览队列

最值得先做的改造是“幽灵方块”——当前方块垂直投影到落定点位的半透明阴影。实现思路其实很简单:方块开始下落后,用碰撞检测不断尝试向下移动,直到找到最后一个合法位置,把这个位置的方块用半透明颜色绘制出来。绘制部分全在GamePanel里完成,逻辑部分完全不用改TetrisGame,非常适合作为热身改动。改动量大约20行,改完你会对“渲染和逻辑分离”有非常直观的感受。预览队列则在Shape枚举上方加一个List ,存接下来几个方块,生成新方块时从这个队列里取。

5.2 调整速度曲线与计分规则

第二个改造入口是难度曲线。原版速度靠Timer的delay控制,想要“随消除行数变快”,就在ScoreManager里加一个等级计算函数,每隔固定分数把dropInterval缩短一点。注意Timer的setDelay()要在EDT线程里调用,消行逻辑返回后直接调用是安全的。计分规则也可以改成分数倍率与等级挂钩,改动集中在addScore()一个方法里,属于低风险修改。我一般会顺带把消除动画做成“行消失后先闪白再压缩”,这个只影响GamePanel的draw逻辑,不会碰到规则层。

5.3 加一个双人模式或AI自动玩

第三个方向是双人模式或AI自动玩,这个改动动到了架构层。双人模式需要两个Grid、两套形状状态、两套计分,但可以共用一套Shape枚举和旋转逻辑;AI自动玩则需要一个评估函数,对当前网格算出一个分数,然后枚举每一种形状的每一种旋转和列位置,用碰撞检测判断合法性,选分数最高的落点。评估函数可以先只用三条规则:消除行数越多越好、堆放高度越低越好、凹凸度越小越好。每加一条规则,AI的路子就会肉眼可见地变聪明,这就成了你反复调整评估权重的实验场。验证改造最好的办法是自己写一个“自动下落脚本”:把move和rotate封装成可调用方法,然后在控制台模拟输入事件,做一个极简的自动化测试。从那以后我每次改完游戏逻辑,都会先跑一遍20局的自动模拟,确认没有死锁和越界再开窗口手玩。“能自动跑”和“人工觉得没问题”是两种完全不同的信心,希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进