一、项目介绍
本项目是一个基于Java Swing GUI开发的经典15拼图游戏(也称为华容道)。游戏界面由一个4x4的网格组成,其中包含15个编号的方块和一个空白块。玩家需要通过键盘方向键移动方块,最终将所有方块按数字顺序(1-15)排列,空白块位于右下角,即可获得胜利。
该项目完整实现了图形化界面(GUI)、游戏逻辑、用户交互、胜负判定以及多项辅助功能,代码结构清晰,是学习Java面向对象编程和Swing GUI开发的合适案例。
游戏的最终效果实现

二、项目构建
1.图形化游戏界面
使用Swing的JFrame、JLabel、ImageIcon等组件构建了游戏主界面和背景。
窗口初始化(initJFrame 方法)
- 设置窗口的基本属性:尺寸 (603x680)、标题、置顶显示、居中位置和关闭模式
- 取消默认布局管理器,采用绝对定位(按 XY 轴放置组件)
菜单栏初始化(initJMenuBar 方法)
- 创建菜单栏和两个主菜单(“功能” 和 “关于我们”)
- 为菜单添加具体菜单项(重新开始、重新登录、关闭、账号信息等)
- 将菜单组装到菜单栏并设置到窗口上
图像初始化(initImage 方法)
- 清除界面中已有的所有图片组件
- 添加背景图片并设置其位置和大小
- 刷新界面以显示最新的组件布局
private void initJFrame() {
//设置界面的宽高
this.setSize(603,680);
//设置界面的标题
this.setTitle("拼图单机版 v1.0");
//设置界面置顶
this.setAlwaysOnTop(true);
//设置界面居中
this.setLocationRelativeTo(null);
//设置关闭模式
this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
//取消默认的居中放置,只有取消了才会按照XY轴的形式添加组件
this.setLayout(null);
}
private void initJMenuBar() {
//创建整个菜单对象
JMenuBar jMenuBar = new JMenuBar();
//创建菜单上面的两个选项的对象(功能、关于我们)
JMenu functionJMenu = new JMenu("功能");
JMenu abountJMenu = new JMenu("关于我们");
//将每一个选项下面的条目添加到选项当中
functionJMenu.add(replayItem);
functionJMenu.add(reLoginItem);
functionJMenu.add(closeItem);
abountJMenu.add(accountItem);
//将菜单里面的两个选项添加到菜单当中
jMenuBar.add(functionJMenu);
jMenuBar.add(abountJMenu);
//将整个界面设置菜单
this.setJMenuBar(jMenuBar);
}
private void initImage() {
//删除原本已经出现的所有图片
this.getContentPane().removeAll();
//添加背景图片
JLabel background = new JLabel(new ImageIcon("puzzlegame\\image\\background.png"));
background.setBounds(40,40,508,560);
//把管理容器添加到界面中
this.getContentPane().add(background);
//刷新界面
this.getContentPane().repaint();
}
结果如下:

2.随机游戏初始化
每次开始新游戏或重新游戏时,会通过随机算法打乱拼图顺序,确保游戏的随机性和可玩性。
数据初始化(initData 方法)
- 创建一个包含 0-15 的一维数组 tempArr,代表 16 个拼图块(0 通常代表空白块)
- 使用随机数打乱数组顺序,模拟拼图的随机打乱效果
- 将打乱后的一维数组数据转换为 4x4 的二维数组 data,对应拼图的 4 行 4 列布局
- 记录空白块(值为 0)的位置坐标 (x,y),用于后续拼图移动逻辑
图片加载与布局(initImage 方法)
- 先清除界面中已有的所有组件,为重新加载做准备
- 双重循环遍历 4x4 的二维数组 data,根据每个位置的值加载对应的图片
- 计算每个图片的位置坐标(基于 105px 的块大小,加上边距偏移),设置图片尺寸为 105x105px
- 为每个图片添加凹陷边框效果,增强拼图块的视觉区分度
- 将所有图片添加到界面后,再添加背景图片(确保背景在底层)
- 刷新界面以显示所有图片组件
int[][] data = new int[4][4];
private void initData() {
int[] tempArr = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
//打乱顺序
Random r = new Random();
for (int i = 0; i < tempArr.length; i++) {
int index = r.nextInt(tempArr.length);
int temp = tempArr[i];
tempArr[i] = tempArr[index];
tempArr[index] = temp;
}
//添加数据
for (int i = 0; i < tempArr.length; i++) {
if (tempArr[i] == 0){
x = i / 4;
y = i % 4;
}
data[i / 4][i % 4] = tempArr[i];
}
}
private void initImage() {
//删除原本已经出现的所有图片
this.getContentPane().removeAll();
for (int i = 0; i < 4;i++) {
for (int j = 0; j < 4;j++ ) {
//获取当前要加载图片的序号
int num = data[i][j];
//创建一个图片ImageIcon图像
//创建一个JLabel的对象(管理容器)
JLabel jLabel = new JLabel(new ImageIcon("puzzlegame\\image\\animal\\animal3\\" + num + ".jpg"));
//指定图片位置
jLabel.setBounds(105 * j + 83,105 * i + 134,105,105);
//给图片添加边框
//0:表示图片凸起
//1:表示图片凹下去
jLabel.setBorder(new BevelBorder(BevelBorder.LOWERED));
//把管理容器添加到界面中
this.getContentPane().add(jLabel);
}
}
//添加背景图片
JLabel background = new JLabel(new ImageIcon("puzzlegame\\image\\background.png"));
background.setBounds(40,40,508,560);
//把管理容器添加到界面中
this.getContentPane().add(background);
//刷新界面
this.getContentPane().repaint();
}
结果如下:

3.键盘操控
通过实现KeyListener接口,支持使用键盘方向键(↑↓←→) 移动方块。
针对每个方向实现对应的移动逻辑:
- 向左移动:检查空白块是否在最右侧(y=3),若不是则将右侧拼图块左移,更新空白块位置 (y++)
- 向上移动:检查空白块是否在最底部(x=3),若不是则将下方拼图块上移,更新空白块位置 (x++)
- 向右移动:检查空白块是否在最左侧(y=0),若不是则将左侧拼图块右移,更新空白块位置 (y–)
- 向下移动:检查空白块是否在最顶部(x=0),若不是则将上方拼图块下移,更新空白块位置 (x–)
状态更新与界面刷新
- 每次有效移动后,步数(step)自增
- 调用initImage()方法重新加载图片,刷新界面展示最新的拼图状态
边界控制
- 通过判断空白块的坐标 (x,y) 是否到达边界,限制无效移动,防止数组越界
//给整个键盘添加键盘监听事件
this.addKeyListener(this);
@Override
public void keyReleased(KeyEvent e) {
int code = e.getKeyCode();
if (code == 37){
System.out.println("向左移动");
if(y == 3){
System.out.println("无法向左移动");
//表示空白方法已经在最下方了,他的下面没有图片再能移动了
return;
}
data[x][y] =data[x][y + 1];
data[x][y + 1] = 0;
y++;
initImage();
} else if (code == 38) {
System.out.println("向上移动");
if(x == 3){
System.out.println("无法向上移动");
//表示空白方法已经在最下方了,他的下面没有图片再能移动了
return;
}
data[x][y] = data[x + 1][y];
data[x + 1][y] = 0;
x++;
//调用方法按照最新的数字加载图片
initImage();
} else if (code == 39) {
System.out.println("向右移动");
if(y == 0){
System.out.println("无法向右移动");
//表示空白方法已经在最下方了,他的下面没有图片再能移动了
return;
}
data[x][y] = data[x][y - 1 ];
data[x][y - 1] = 0;
y--;
initImage();
} else if (code == 40) {
System.out.println("向下移动");
if(x == 0){
System.out.println("无法向下移动");
//表示空白方法已经在最下方了,他的下面没有图片再能移动了
return;
}
System.out.println("向下移动");
data[x][y] = data[x - 1][y];
data[x - 1][y] = 0;
x--;
initImage();
}
initImage();
}
}
4.步数记录
界面实时显示玩家移动的步数,记录游戏历程。
步数变量与显示组件初始化
- 定义整数变量step用于记录移动步数,初始值为 0
- 创建JLabel组件stepCount,用于在界面上显示当前步数
- 设置标签位置(50,30)和大小(100,20),并添加到界面中
步数更新逻辑
- 在键盘事件处理中(以向左移动为例),每当完成一次有效移动后:
- 将step变量自增 1(step++)
- 调用initImage()刷新界面,间接更新步数显示
//定义步数
int step = 0;
JLabel stepCount = new JLabel("步数: " + step);
stepCount.setBounds(50,30,100,20);
this.getContentPane().add(stepCount);
if (code == 37){
System.out.println("向左移动");
if(y == 3){
System.out.println("无法向左移动");
//表示空白方法已经在最下方了,他的下面没有图片再能移动了
return;
}
data[x][y] =data[x][y + 1];
data[x][y + 1] = 0;
y++;
//每移动一次,自增
step++;
initImage();

5.胜负判定
每次移动后,程序会将当前方块布局与正确的胜利布局进行比较,一旦匹配则弹出胜利提示。
胜利判定核心逻辑(victory 方法)
- 通过双重循环遍历存储当前拼图状态的二维数组data
- 将data与预设的胜利状态数组win逐一比较每个元素
- 若所有元素都对应相等,则返回true(游戏胜利);只要有一个元素不匹配,就返回false(游戏未胜利)
- 这种方式通过数据比对精准判断拼图是否已完成正确拼接
胜利状态的界面反馈(initImage 方法)
- 在每次刷新界面时,首先检查victory()的返回结果
- 当判定为胜利状态时,创建并添加胜利图标(win.png)到界面指定位置
- 胜利图标会覆盖在原有拼图界面上,给用户明确的视觉提示
胜利后的操作锁定(keyReleased 方法)
- 在处理键盘事件的最开始就进行胜利状态检查
- 一旦游戏胜利,立即终止方法执行,不再处理任何方向键移动指令
- 这就实现了胜利后无法继续移动拼图块的功能,保持胜利状态的稳定性
private void initImage() {
//删除原本已经出现的所有图片
this.getContentPane().removeAll();
if (victory()){
//显示胜利图标
JLabel winJLabel = new JLabel(new ImageIcon("E:\\basic_code\\puzzlegame\\image\\win.png"));
winJLabel.setBounds(203,283,197,73);
this.getContentPane().add(winJLabel);
}
public void keyReleased(KeyEvent e) {
//判断游戏是否胜利,如果胜利,此方法直接结束
if (victory()){
return;
}
public boolean victory(){
for (int i = 0; i < data.length; i++) {
//data[i]表示一维数组
for (int j = 0; j < data[i].length; j++) {
if (data[i][j] != win[i][j]){
return false;
}
}
}
return true;
}

6.开发者快捷键
A键:查看完整的参考图片(方便玩家解题)。
W键:一键通关(直接将拼图恢复为胜利状态,用于测试或“作弊”)。
按下 A 键(keyPressed):
- 清除界面所有组件,加载完整图片(all.jpg)并设置其位置(83,134)和大小(420x420)
- 重新添加背景图片并刷新界面,使玩家能看到完整的参考图像
松开 A 键(keyReleased):
- 调用initImage()方法恢复显示当前的拼图状态
W 键一键通关功能
- 在松开 W 键时触发,直接将data数组设置为胜利状态的二维数组(数字 1-15 按顺序排列,0 在最后)
- 调用initImage()刷新界面,立即显示完成状态,实现快速通关
@Override
public void keyPressed(KeyEvent e) {
//快捷键查看完整图片
int code = e.getKeyCode();
if (code == 65){
//把界面所有的图片全部删除
this.getContentPane().removeAll();
//加载第一张完整图片
JLabel all = new JLabel(new ImageIcon("puzzlegame\\image\\animal\\animal3\\all.jpg"));
all.setBounds(83,134,420,420);
this.getContentPane().add(all);
//添加背景图片
JLabel background = new JLabel(new ImageIcon("puzzlegame\\image\\background.png"));
background.setBounds(40,40,508,560);
//把管理容器添加到界面中
this.getContentPane().add(background);
//刷新界面
this.getContentPane().repaint();
}
}
@Override
public void keyReleased(KeyEvent e) {
int code = e.getKeyCode();
else if (code == 65) {
initImage();
}else if (code == 87){
data = new int[][]{
{1,2,3,4},
{5,6,7,8},
{9,10,11,12},
{13,14,15,0}
};
initImage();
}
}


三、总结
对于Java初学者来说,这是一个非常好的综合练习项目,能够有效巩固Java基础语法、Swing组件使用、事件监听机制以及面向对象的编程思想。
源代码
import javax.swing.*;
import javax.swing.border.BevelBorder;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Random;
public class GameJFrame extends JFrame implements KeyListener, ActionListener {
/*
JFrame:界面,窗体
子类也表示界面,窗体
规定:GameJFrame这个界面就表示游戏的主界面
以后跟游戏相关的所有逻辑都写在这个类中
*/
//表示游戏主界面
//创建二维数组
int[][] data = new int[4][4];
//初始化空白位置
int x = 0;
int y = 0;
//定义正确数组
int[][] win = {
{1,2,3,4},
{5,6,7,8},
{9,10,11,12},
{13,14,15,0}
};
//定义步数
int step = 0;
//创建选项下面的条目目录
JMenuItem replayItem = new JMenuItem("重新游戏");
JMenuItem reLoginItem = new JMenuItem("重新登录");
JMenuItem closeItem = new JMenuItem("关闭游戏");
JMenuItem accountItem = new JMenuItem("公众号");
public GameJFrame() {
//初始化界面
initJFrame();
//初始化菜单
initJMenuBar();
//初始化数据(打乱)
initData();
//初始化图片
initImage();
//显示界面
this.setVisible(true);
}
private void initData() {
int[] tempArr = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
//打乱顺序
Random r = new Random();
for (int i = 0; i < tempArr.length; i++) {
int index = r.nextInt(tempArr.length);
int temp = tempArr[i];
tempArr[i] = tempArr[index];
tempArr[index] = temp;
}
//添加数据
for (int i = 0; i < tempArr.length; i++) {
if (tempArr[i] == 0){
x = i / 4;
y = i % 4;
}
data[i / 4][i % 4] = tempArr[i];
}
}
//添加图片的时候,就需要按照二维数组中管理的数据添加图片
private void initImage() {
//删除原本已经出现的所有图片
this.getContentPane().removeAll();
if (victory()){
//显示胜利图标
JLabel winJLabel = new JLabel(new ImageIcon("E:\\basic_code\\puzzlegame\\image\\win.png"));
winJLabel.setBounds(203,283,197,73);
this.getContentPane().add(winJLabel);
}
JLabel stepCount = new JLabel("步数: " + step);
stepCount.setBounds(50,30,100,20);
this.getContentPane().add(stepCount);
for (int i = 0; i < 4;i++) {
for (int j = 0; j < 4;j++ ) {
//获取当前要加载图片的序号
int num = data[i][j];
//创建一个图片ImageIcon图像
//创建一个JLabel的对象(管理容器)
JLabel jLabel = new JLabel(new ImageIcon("puzzlegame\\image\\animal\\animal3\\" + num + ".jpg"));
//指定图片位置
jLabel.setBounds(105 * j + 83,105 * i + 134,105,105);
//给图片添加边框
//0:表示图片凸起
//1:表示图片凹下去
jLabel.setBorder(new BevelBorder(BevelBorder.LOWERED));
//把管理容器添加到界面中
this.getContentPane().add(jLabel);
}
}
//添加背景图片
JLabel background = new JLabel(new ImageIcon("puzzlegame\\image\\background.png"));
background.setBounds(40,40,508,560);
//把管理容器添加到界面中
this.getContentPane().add(background);
//刷新界面
this.getContentPane().repaint();
}
private void initJMenuBar() {
//创建整个菜单对象
JMenuBar jMenuBar = new JMenuBar();
//创建菜单上面的两个选项的对象(功能、关于我们)
JMenu functionJMenu = new JMenu("功能");
JMenu abountJMenu = new JMenu("关于我们");
//将每一个选项下面的条目添加到选项当中
functionJMenu.add(replayItem);
functionJMenu.add(reLoginItem);
functionJMenu.add(closeItem);
abountJMenu.add(accountItem);
//给条目绑定事件
replayItem.addActionListener(this);
reLoginItem.addActionListener(this);
closeItem.addActionListener(this);
accountItem.addActionListener(this);
//将菜单里面的两个选项添加到菜单当中
jMenuBar.add(functionJMenu);
jMenuBar.add(abountJMenu);
//将整个界面设置菜单
this.setJMenuBar(jMenuBar);
}
private void initJFrame() {
//设置界面的宽高
this.setSize(603,680);
//设置界面的标题
this.setTitle("拼图单机版 v1.0");
//设置界面置顶
this.setAlwaysOnTop(true);
//设置界面居中
this.setLocationRelativeTo(null);
//设置关闭模式
this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
//取消默认的居中放置,只有取消了才会按照XY轴的形式添加组件
this.setLayout(null);
//给整个键盘添加键盘监听事件
this.addKeyListener(this);
}
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyPressed(KeyEvent e) {
//快捷键查看完整图片
int code = e.getKeyCode();
if (code == 65){
//把界面所有的图片全部删除
this.getContentPane().removeAll();
//加载第一张完整图片
JLabel all = new JLabel(new ImageIcon("puzzlegame\\image\\animal\\animal3\\all.jpg"));
all.setBounds(83,134,420,420);
this.getContentPane().add(all);
//添加背景图片
JLabel background = new JLabel(new ImageIcon("puzzlegame\\image\\background.png"));
background.setBounds(40,40,508,560);
//把管理容器添加到界面中
this.getContentPane().add(background);
//刷新界面
this.getContentPane().repaint();
}
}
@Override
public void keyReleased(KeyEvent e) {
//判断游戏是否胜利,如果胜利,此方法直接结束
if (victory()){
return;
}
int code = e.getKeyCode();
if (code == 37){
System.out.println("向左移动");
if(y == 3){
System.out.println("无法向左移动");
//表示空白方法已经在最下方了,他的下面没有图片再能移动了
return;
}
data[x][y] =data[x][y + 1];
data[x][y + 1] = 0;
y++;
//每移动一次,自增
step++;
initImage();
} else if (code == 38) {
System.out.println("向上移动");
if(x == 3){
System.out.println("无法向上移动");
//表示空白方法已经在最下方了,他的下面没有图片再能移动了
return;
}
/*
把空白方法下方的数字向上移动
x,y表示空白方块
x+1,y表示空白方块下方的数字
把空白方块下方的数字赋值给空白方块
*/
data[x][y] = data[x + 1][y];
data[x + 1][y] = 0;
x++;
step++;
//调用方法按照最新的数字加载图片
initImage();
} else if (code == 39) {
System.out.println("向右移动");
if(y == 0){
System.out.println("无法向右移动");
//表示空白方法已经在最下方了,他的下面没有图片再能移动了
return;
}
data[x][y] = data[x][y - 1 ];
data[x][y - 1] = 0;
y--;
step++;
initImage();
} else if (code == 40) {
System.out.println("向下移动");
if(x == 0){
System.out.println("无法向下移动");
//表示空白方法已经在最下方了,他的下面没有图片再能移动了
return;
}
System.out.println("向下移动");
data[x][y] = data[x - 1][y];
data[x - 1][y] = 0;
x--;
step++;
initImage();
} else if (code == 65) {
initImage();
}else if (code == 87){
data = new int[][]{
{1,2,3,4},
{5,6,7,8},
{9,10,11,12},
{13,14,15,0}
};
initImage();
}
}
//判断胜利 比较数组
public boolean victory(){
for (int i = 0; i < data.length; i++) {
//data[i]表示一维数组
for (int j = 0; j < data[i].length; j++) {
if (data[i][j] != win[i][j]){
return false;
}
}
}
return true;
}
@Override
public void actionPerformed(ActionEvent e) {
//获取当前被点击的条目对象
Object obj = e.getSource();
if (obj == replayItem){
System.out.println("重新游戏");
//计步清零
step = 0;
//再次打乱二维数组中的数据
initData();
//重新加载图片
initImage();
} else if (obj == reLoginItem) {
} else if (obj == closeItem) {
System.exit(0);
} else if (obj == accountItem) {
//创建一个弹框对象
JDialog jDialog = new JDialog();
//创建管理图片容器
JLabel jLabel = new JLabel(new ImageIcon("E:\\basic_code\\puzzlegame\\image\\about.png"));
jLabel.setBounds(0,0,258,258);
//把图片添加到弹框
jDialog.getContentPane().add(jLabel);
//弹框大小
jDialog.setSize(344,344);
//置顶
jDialog.setAlwaysOnTop(true);
//居中
jDialog.setLocationRelativeTo(null);
//弹框不关闭则无法操作下面的界面
jDialog.setModal(true);
//让弹框显示出来
jDialog.setVisible(true);
}
}
}
1万+

被折叠的 条评论
为什么被折叠?



