1 本节目标
- 使用 Java Swing 开发一个 4×4 的数字华容道游戏(石头迷阵)。
- 游戏规则:点击键盘上下左右键,移动数字方块,将数字按 1~15 顺序排列,0(空白)位于右下角。
- 实现功能:随机打乱、步数统计、胜利检测、胜利图片显示、系统菜单(重启/退出)。
- 最终效果:你读完这篇博客后,能够独立写出完整的游戏代码。
2 整体开发思路(按顺序走)

3关键代码(分模块实现)
3.1 窗口框架(initFrame)
java
private void initFrame() {
setTitle("石头迷阵 v1.0");
setSize(465, 575);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLocationRelativeTo(null);
setLayout(null); // 绝对定位,便于控制每个图片的精确位置
}
为什么绝对布局 :拼图游戏需要手动计算每个方块的 (x, y) 坐标(公式:20 + j100, 60 + i100),null 布局配合 setBounds 最直观。
3.2 数据结构与胜利状态
java
// 当前游戏数据(0 代表空白块)
private int[][] imageData = {
{1,2,3,4},
{5,6,7,8},
{9,10,11,12},
{13,14,15,0}
};
// 胜利时的目标状态
private int[][] winData = {
{1,2,3,4},
{5,6,7,8},
{9,10,11,12},
{13,14,15,0}
};
private int row = 3, col = 3; // 空白块当前位置(初始在右下角)
private int count = 0; // 移动步数
方向枚举
java
enum Direction { UP, DOWN, LEFT, RIGHT }
3.3 绘制界面(initImage)
每次移动后都需要重新绘制,所以将清空窗口、添加所有组件(步数标签、数字图片、胜利图片、背景)集中在此方法中。
java
private void initImage() {
// 1. 清除所有已有组件
getContentPane().removeAll();
// 2. 显示步数
JLabel stepLabel = new JLabel("移动步数:" + count);
stepLabel.setBounds(20, 20, 120, 30);
stepLabel.setFont(new Font("楷体", Font.BOLD, 14));
stepLabel.setForeground(Color.RED);
add(stepLabel);
// 3. 绘制数字方块(根据 imageData)
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
int num = imageData[i][j];
// 图片路径,例如 image/1.png、image/0.png(空白图)
ImageIcon icon = new ImageIcon("image/" + num + ".png");
JLabel label = new JLabel(icon);
label.setBounds(20 + j * 100, 60 + i * 100, 100, 100);
add(label);
}
}
// 4. 胜利时额外显示胜利图片(覆盖在窗口中央)
if (isWin()) {
JLabel winLabel = new JLabel(new ImageIcon("image/win.png"));
winLabel.setBounds(124, 230, 266, 88);
add(winLabel);
}
// 5. 背景图片(必须最后添加,避免遮挡其他组件?实际背景应最先添加,但最后添加会被覆盖?)
// 经验证:Swing 中后添加的组件会显示在上层,所以背景要最先添加。这里调整顺序。
// 正确做法:先添加背景,再添加其他组件。但为了代码清晰,此处说明。
JLabel bg = new JLabel(new ImageIcon("image/background.png"));
bg.setBounds(0, 0, 450, 484);
add(bg);
// 6. 刷新窗口
repaint();
}
常见坑:背景图片如果最后添加,会覆盖掉数字图片。解决方案:将背景图片添加的代码移到最前面(在 removeAll() 之后立即添加背景,然后再添加步数和数字图片)。上面代码顺序仅为说明,实际使用时请自行调整。
3.4 打乱算法(保证有解)
最常见的错误是随机交换元素可能导致无解。正确做法:从胜利状态出发,随机执行多次合法移动
java
private void shuffle(int steps) {
Random rand = new Random();
for (int s = 0; s < steps; s++) {
int dir = rand.nextInt(4); // 0上 1下 2左 3右
switch (dir) {
case 0: // 上 → 空白块与下方块交换(空白向下移动)
if (row < 3) {
swap(row, col, row+1, col);
row++;
}
break;
case 1: // 下
if (row > 0) {
swap(row, col, row-1, col);
row--;
}
break;
case 2: // 左
if (col < 3) {
swap(row, col, row, col+1);
col++;
}
break;
case 3: // 右
if (col > 0) {
swap(row, col, row, col-1);
col--;
}
break;
}
}
}
// 交换二维数组中的两个元素
private void swap(int r1, int c1, int r2, int c2) {
int temp = imageData[r1][c1];
imageData[r1][c1] = imageData[r2][c2];
imageData[r2][c2] = temp;
}
3.5 键盘监听与移动逻辑
使用 KeyAdapter 监听窗口的按键事件,根据方向调用 tryMove 方法。
java
private void initKeyPressEvent() {
this.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
int key = e.getKeyCode();
switch (key) {
case KeyEvent.VK_UP: tryMove(Direction.UP); break;
case KeyEvent.VK_DOWN: tryMove(Direction.DOWN); break;
case KeyEvent.VK_LEFT: tryMove(Direction.LEFT); break;
case KeyEvent.VK_RIGHT: tryMove(Direction.RIGHT); break;
}
}
});
}
// 移动逻辑(与打乱中的方向定义一致)
private void tryMove(Direction dir) {
switch (dir) {
case UP:
if (row < 3) {
swap(row, col, row+1, col);
row++;
count++;
}
break;
case DOWN:
if (row > 0) {
swap(row, col, row-1, col);
row--;
count++;
}
break;
case LEFT:
if (col < 3) {
swap(row, col, row, col+1);
col++;
count++;
}
break;
case RIGHT:
if (col > 0) {
swap(row, col, row, col-1);
col--;
count++;
}
break;
}
initImage(); // 移动后刷新界面
}
3.6 胜利检测
java
private boolean isWin() {
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
if (imageData[i][j] != winData[i][j]) {
return false;
}
}
}
return true;
}
3.7 菜单栏(退出/重启)
java
private void initMenu() {
JMenuBar menuBar = new JMenuBar();
JMenu menu = new JMenu("系统");
JMenuItem restartItem = new JMenuItem("重启");
JMenuItem exitItem = new JMenuItem("退出");
menu.add(restartItem);
menu.add(exitItem);
menuBar.add(menu);
setJMenuBar(menuBar);
// 重启游戏
restartItem.addActionListener(e -> {
// 重置为胜利状态
initBoardToWin();
// 重新打乱
shuffle(200);
count = 0;
initImage();
});
// 退出游戏
exitItem.addActionListener(e -> System.exit(0));
}
// 重置数组为胜利顺序(并重新定位空白位置)
private void initBoardToWin() {
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
imageData[i][j] = winData[i][j];
}
}
row = 3;
col = 3;
}
3.8 构造方法中调用所有初始化
java
public MainFrame() {
initFrame(); // 窗口基础设置
initBoardToWin(); // 数组置为胜利顺序(可选)
shuffle(200); // 打乱
initImage(); // 绘制界面
initKeyPressEvent(); // 键盘监听
initMenu(); // 菜单栏
setVisible(true); // 显示窗口
}
4 成果展示
