今天我要分享一个我大一做的Java拼图游戏项目。这是一个非常适合新手的实战项目,涵盖了GUI界面开发、事件处理、文件操作等核心知识。我会从零开始,手把手教大家如何实现这个游戏。
一、项目功能介绍
先来看看这个游戏能做什么:
- 核心玩法:4×4的拼图游戏,通过键盘方向键移动方块,把打乱的图片还原
- 用户系统:支持注册和登录,账号密码保存在本地文件中
- 游戏辅助:
- 按
A键可以预览完整图片(松开后恢复) - 按
W键可以一键通关(测试用) - 自动统计移动步数
- 按
- 多图切换:内置24套不同的拼图图片,每次随机选择
- 联系方式:菜单栏可以查看开发者微信二维码

二、项目结构说明
在开始写代码之前,我们需要先准备好项目目录结构。
1、总体目录结构
D:\java\puzzlegame\ ← 项目根目录 ├── src/ │ └── puzzlegame/ │ └── ui/ ← Java源代码包 │ ├── App.java ← 程序入口 │ ├── GameJFrame.java ← 游戏主界面(核心!) │ ├── LoginJFrame.java ← 登录窗口 │ └── RegisterJFrame.java ← 注册窗口 ├── pic/ ← 拼图图片文件夹 │ ├── picture0/ ← 第1套图片 │ │ ├── 0.jpg ← 空白块 │ │ ├── 1.jpg ~ 15.jpg ← 拼图碎片 │ │ └── all.jpg ← 完整原图 │ ├── picture1/ ← 第2套图片 │ │ ├── 0.jpg ~ 15.jpg │ │ └── all.jpg │ ├── picture2/ │ ├── ... │ └── picture23/ ← 第24套图片(共24套) ├── background.png ← 游戏背景图 ├── win.png ← 胜利提示图 └── code.jpg ← 微信二维码图片
2、图片资源详解
| 资源类型 | 文件路径 | 用途说明 | 尺寸要求 |
|---|---|---|---|
| 拼图碎片 | pic/pictureN/X.jpg |
16张碎片 + 1张空白 = 17张 | 105×105 像素 |
| 完整原图 | pic/pictureN/all.jpg |
用于A键预览 | 420×420 像素 |
| 背景图 | background.png |
游戏界面背景装饰 | 508×560 像素 |
| 胜利图标 | win.png |
通关后显示的”胜利”标志 | 197×73 像素 |
| 二维码 | code.jpg |
开发者微信二维码 | 344×344 像素 |
新手必读:
N代表 0-23 的数字(共24套图片)X代表 0-15 的数字(0是空白块,1-15是拼图碎片)- 所有图片必须严格按照命名规则放置,否则程序会找不到图片!
- 你可以用任何图片编辑工具(如Photoshop、美图秀秀)将一张大图切成16小块
小技巧:如果没有现成的图片素材,可以去网上搜索”拼图游戏素材包”,或者用自己喜欢的照片切割成16份即可。
三、核心代码实现
现在进入最核心的部分!我会把每个文件的代码完整展示出来,并加上详细的中文注释。建议你一边看一边跟着敲代码。
文件1:App.java – 程序入口
这是程序的启动入口,非常简单,只有几行代码:
package puzzlegame.ui;
/**
* 程序主入口类
* 功能:启动时打开登录窗口和注册窗口
*/
public class App {
public static void main(String[] args) {
// 创建注册窗口对象(让用户可以注册新账号)
new RegisterJFrame();
// 创建登录窗口对象(让用户可以登录已有账号)
new LoginJFrame();
// 注意:GameJFrame(游戏主界面)不在这里创建
// 而是在用户登录成功后才创建,这样能保证只有登录的用户才能玩游戏
// new GameJFrame();
}
}
代码解读:
package puzzlegame.ui;– 声明这个类属于puzzlegame.ui包main方法– Java程序的入口点,程序从这里开始执行- 同时打开两个窗口(登录+注册),用户可以选择操作
文件2:GameJFrame.java – 游戏主界面(核心中的核心!)
这是整个项目最重要的文件,包含了游戏的全部逻辑。代码比较长,我会分段讲解。
【第1部分】类的定义与成员变量
package puzzlegame.ui;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Random;
import javax.swing.*;
import javax.swing.border.BevelBorder;
/**
* 游戏主界面类
* 继承自JFrame(Java的窗口类)
* 实现了KeyListener(键盘监听)和ActionListener(按钮点击监听)接口
*/
public class GameJFrame extends JFrame implements KeyListener, ActionListener {
// ========== 数据存储相关 ==========
// 二维数组:用来存储当前拼图的状态
// 4行4列,共16个格子
// 数组里的数字代表该位置显示哪张图片(0表示空白格,1-15表示拼图块)
int[][] data = new int[4][4];
// 记录空白格子的位置(坐标)
int x = 0; // 行号(0-3)
int y = 0; // 列号(0-3)
// 定义正确的拼图顺序(用于判断是否胜利)
// 当data数组的值和这个win数组完全一致时,说明拼图完成了
int[][] win = {
{1, 2, 3, 4}, // 第1行
{5, 6, 7, 8}, // 第2行
{9, 10, 11, 12}, // 第3行
{13, 14, 15, 0} // 第4行(最后一个0是空白格)
};
// 记录玩家移动了多少步
int step = 0;
// 随机选择一套图片(0-23,共24套可选)
int k = (int)(Math.random() * 24);
// 图片存储路径的前缀
// 最终路径会是:..\\puzzlegame\\pic\\picture0\\ 或 picture1\\ 等
String path = "..\\puzzlegame\\pic\\picture";
// ========== 菜单栏相关 ==========
// 创建菜单项(就是窗口顶部的那些按钮)
JMenuItem replayItem = new JMenuItem("重新游戏"); // 重新开始
JMenuItem reLoginItem = new JMenuItem("重新登陆"); // 重新登录
JMenuItem closeItem = new JMenuItem("关闭游戏"); // 退出程序
JMenuItem accountItem = new JMenuItem("联系方式"); // 显示二维码}
重点理解:data二维数组是这个游戏的核心数据结构。比如data[0][0] = 5表示左上角显示的是编号为5的那张图片。data[3][3] = 0表示右下角是空白格。
【第2部分】构造方法 – 初始化游戏
/**
* 构造方法:当创建 GameJFrame 对象时会自动执行
* 负责初始化整个游戏界面
*/
public GameJFrame() {
// 步骤1:初始化窗口的基本属性(大小、标题、位置等)
initJFrame();
// 步骤2:初始化菜单栏(功能、关于我们等选项)
initJMenuBar();
// 步骤3:打乱拼图数据(让游戏开始时是乱序状态)
initData();
// 步骤4:根据打乱的数据加载并显示图片
initImage();
// 步骤5:让窗口显示出来(这一步要放在最后!)
this.setVisible(true);
}
【第3部分】打乱拼图的算法
/**
* 初始化并打乱拼图数据
* 使用Fisher-Yates洗牌算法,确保每个排列出现的概率相等
*/
private void initData() {
// 先创建一个有序的一维数组:[0, 1, 2, 3, ..., 15]
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++) {
// 生成一个随机索引(0-15之间)
int index = r.nextInt(tempArr.length);
// 交换当前位置和随机位置的元素
int temp = tempArr[i];
tempArr[i] = tempArr[index];
tempArr[index] = temp;
}
// 将一维数组转换成二维数组(4×4)
for(int i = 0; i < tempArr.length; i++) {
if(tempArr[i] == 0) {
// 如果遇到0(空白格),记录它的位置
x = i / 4; // 计算行号(整数除法)
y = i % 4; // 计算列号(取余数)
}
// 将一维数组的值填入二维数组
data[i / 4][i % 4] = tempArr[i];
}
}
/**
* 示例说明:
* 假设打乱后 tempArr = [5, 0, 3, 8, 1, 12, 7, 14, 2, 9, 6, 11, 4, 10, 13, 15]
*
* 转换后的 data 二维数组:
* data[0][0]=5 data[0][1]=0 data[0][2]=3 data[0][3]=8
* data[1][0]=1 data[1][1]=12 data[1][2]=7 data[1][3]=14
* data[2][0]=2 data[2][1]=9 data[2][2]=6 data[2][3]=11
* data[3][0]=4 data[3][1]=10 data[3][2]=13 data[3][3]=15
*
* 此时空白格(0)在位置 (0, 1),即 x=0, y=1
*/
【第4部分】加载和显示图片
/**
* 根据data数组的数据加载对应的图片并显示在界面上
* 这个方法会在每次移动拼图后调用,用于刷新界面
*/
private void initImage() {
// 清空界面上已有的所有组件(防止图片重叠)
this.getContentPane().removeAll();
// 检查是否已经胜利
if(victory()) {
// 如果胜利了,显示"胜利"图片
JLabel winJLabel = new JLabel(new ImageIcon("D:\\java\\puzzlegame\\win.png"));
winJLabel.setBounds(203, 283, 197, 73); // 设置位置(x,y)和尺寸(宽,高)
this.getContentPane().add(winJLabel);
}
// 遍历二维数组,为每个格子加载对应的图片
for(int i = 0; i < 4; i++) { // i 控制行(0-3)
for(int j = 0; j < 4; j++) { // j 控制列(0-3)
// 获取当前位置应该显示的图片编号
int num = data[i][j];
/**
* 创建图片标签
* path + k + "\\" + num + ".jpg"
* 例如:..\\puzzlegame\\pic\\picture3\\5.jpg
* 表示加载第3套图片中的第5张碎片
*/
JLabel jLabel = new JLabel(new ImageIcon(path + k + "\\" + num + ".jpg"));
// 设置图片在窗口中的显示位置
// 公式:像素位置 = 格子尺寸 × 编号 + 偏移量
// 105是每个格子的宽度/高度
// 83是左边距,143是上边距
jLabel.setBounds(105 * j + 83, 105 * i + 143, 105, 105);
// 给图片添加凹陷边框效果(让它看起来像一个真实的拼图块)
jLabel.setBorder(new BevelBorder(BevelBorder.LOWERED));
// 将图片添加到窗口中
this.getContentPane().add(jLabel);
}
}
// 显示步数统计
JLabel stepCount = new JLabel("步数" + step);
stepCount.setBounds(50, 30, 100, 20); // 显示在左上角
this.getContentPane().add(stepCount);
// 加载背景图片(放在最后,这样会显示在最底层)
JLabel background = new JLabel(new ImageIcon("..\\puzzlegame\\background.png"));
background.setBounds(40, 40, 508, 560);
this.getContentPane().add(background);
// 刷新界面(非常重要!否则修改不会生效)
this.getContentPane().repaint();
}
【第5部分】初始化菜单栏
/**
* 初始化窗口顶部的菜单栏
* 结构:菜单栏 → 菜单 → 菜单项
* 例如:JMenuBar → "关于我们" JMenu → "联系方式" JMenuItem
*/
public void initJMenuBar() {
// 创建菜单栏容器
JMenuBar jMenuBar = new JMenuBar();
// 创建两个菜单分类
JMenu functionJMenu = new JMenu("功能"); // 功能菜单
JMenu aboutJMenu = new JMenu("关于我们"); // 关于我们菜单
// 将菜单项添加到对应的菜单下
functionJMenu.add(replayItem); // 功能 → 重新游戏
functionJMenu.add(closeItem); // 功能 → 关闭游戏
aboutJMenu.add(accountItem); // 关于我们 → 联系方式
// 为菜单项绑定点击事件监听器(this表示由当前类处理事件)
replayItem.addActionListener(this);
closeItem.addActionListener(this);
accountItem.addActionListener(this);
// 将菜单添加到菜单栏
jMenuBar.add(functionJMenu);
jMenuBar.add(aboutJMenu);
// 将菜单栏设置到窗口上
this.setJMenuBar(jMenuBar);
}
【第6部分】初始化窗口属性
/**
* 设置窗口的基本属性
*/
public void initJFrame() {
this.setSize(603, 680); // 设置窗口大小:宽603像素,高680像素
this.setTitle("拼图单机版 v1.0"); // 设置窗口标题
this.setAlwaysOnTop(true); // 窗口始终显示在最前面
this.setLocationRelativeTo(null); // 窗口居中显示(null表示相对于屏幕居中)
this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); // 点击关闭按钮时退出程序
this.setLayout(null); // 取消默认布局(使用绝对定位)
this.addKeyListener(this); // 为窗口添加键盘监听器
}
【第7部分】键盘事件处理 – A键预览功能
/**
* 键盘按下事件(按键不松开时持续触发)
* 这里处理A键长按预览功能
*/
@Override
public void keyPressed(KeyEvent e) {
int code = e.getKeyCode(); // 获取按下的是哪个键(用数字编码表示)
// 65 是字母'A'的ASCII码
if(code == 65) {
// 清空界面上所有内容
this.getContentPane().removeAll();
// 加载完整的原图(all.jpg),让玩家看到最终效果
JLabel all = new JLabel(new ImageIcon(path + k + "\\all.jpg"));
all.setBounds(83, 134, 420, 420);
this.getContentPane().add(all);
// 加载背景图
JLabel background = new JLabel(new ImageIcon("..\\puzzlegame\\background.png"));
background.setBounds(40, 40, 508, 560);
this.getContentPane().add(background);
// 刷新显示
this.getContentPane().repaint();
}
}
【第8部分】键盘松开事件 – 移动逻辑(最核心!)
/**
* 键盘松开事件(按键弹起时触发一次)
* 这里处理所有的游戏移动逻辑
*/
@Override
public void keyReleased(KeyEvent e) {
// 如果已经胜利了,就不再响应移动操作
if(victory()) {
return; // 直接结束方法
}
int code = e.getKeyCode(); // 获取按键编码
// ========== 方向键移动逻辑 ==========
// 注意:实际上是空白格在移动,但视觉上是数字块在移动!
// 左箭头(keyCode = 37):向左移动
if(code == 37) {
System.out.println("向左移动");
if(y == 3) return; // 边界检查:如果空白格已经在最右边,就不能再左移了
// 核心移动逻辑:
// 1. 把空白格右边的数字移到空白格位置
data[x][y] = data[x][y + 1];
// 2. 空白格原来的位置变成空白(0)
data[x][y + 1] = 0;
// 3. 更新空白格的位置(向右移动了一格)
y++;
// 4. 步数+1
step++;
// 5. 重新加载图片刷新界面
initImage();
}
// 上箭头(keyCode = 38):向上移动
else if(code == 38) {
System.out.println("向上移动");
if(x == 3) return; // 空白格已经在最下面
data[x][y] = data[x + 1][y]; // 下面的数字上来
data[x + 1][y] = 0; // 下面变空白
x++;
step++;
initImage();
}
// 右箭头(keyCode = 39):向右移动
else if(code == 39) {
System.out.println("向右移动");
if(y == 0) return; // 空白格已经在最左边
data[x][y] = data[x][y - 1]; // 左边的数字过去
data[x][y - 1] = 0; // 左边变空白
y--;
step++;
initImage();
}
// 下箭头(keyCode = 40):向下移动
else if(code == 40) {
System.out.println("向下移动");
if(x == 0) return; // 空白格已经在最上面
data[x][y] = data[x - 1][y]; // 上面的数字下来
data[x - 1][y] = 0; // 上面变空白
x--;
step++;
initImage();
}
// A键松开(keyCode = 65):恢复拼图界面
else if(code == 65) {
initImage(); // 重新加载打乱的拼图
}
// W键(keyCode = 87):一键通关(作弊功能,测试用)
else if(code == 87) {
// 直接将data数组设置为正确顺序
data = new int[][] {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 0}
};
initImage(); // 刷新显示
}
}
/**
* 移动原理图解:
*
* 假设当前状态(按左箭头前):
* ┌───┬───┬───┬───┐
* │ 1 │ 2 │ 3 │ 4 │
* ├───┼───┼───┼───┤
* │ 5 │ 6 │ 7 │ 8 │
* ├───┼───┼───┼───┤
* │ 9 │10 │ │12 │ ← 空白格在(2,2)
* ├───┼───┼───┼───┤
* │13 │14 │15 │11 │
* └───┴───┴───┴───┘
*
* 按左箭头后(空白格右移,视觉上11左移):
* ┌───┬───┬───┬───┐
* │ 1 │ 2 │ 3 │ 4 │
* ├───┼───┼───┼───┤
* │ 5 │ 6 │ 7 │ 8 │
* ├───┼───┼───┼───┤
* │ 9 │10 │11 │12 │ ← 11移到了原来空白格的位置
* ├───┼───┼───┼───┤
* │13 │14 │15 │ │ ← 空白格现在在(3,3)
* └───┴───┴───┴───┘
*/
【第9部分】判断胜利条件
/**
* 判断当前拼图是否已经完成
* @return true表示胜利,false表示还没完成
*/
public boolean victory() {
// 双重循环遍历整个二维数组
for(int i = 0; i < data.length; i++) { // 遍历每一行
for(int j = 0; j < data[i].length; j++) { // 遍历每一列
// 如果发现任何一个位置的数字和正确答案不一样
// 就立即返回false(还没赢)
if(data[i][j] != win[i][j]) {
return false;
}
}
}
// 如果循环结束都没有返回false,说明所有位置都匹配了
return true; // 赢了
}
/**
* 未实现的键盘输入事件(接口要求必须重写,但不需要用到)
*/
@Override
public void keyTyped(KeyEvent e) {
// 空实现
}
【第10部分】菜单项点击事件处理(包含二维码功能!)
/**
* 处理按钮/菜单项的点击事件
* 当用户点击菜单栏上的选项时会自动调用这个方法
*/
@Override
public void actionPerformed(ActionEvent e) {
// 获取被点击的是哪个组件
Object obj = e.getSource();
// ========== 点击"重新游戏" ==========
if(obj == replayItem) {
System.out.println("重新游戏");
// 1. 重新打乱拼图数据
initData();
// 2. 步数归零
step = 0;
// 3. 随机换一套新的图片(0-23之间随机)
k = (int)(Math.random() * 24);
// 4. 重新加载显示
initImage();
}
// ========== 点击"重新登录" ==========
else if(obj == reLoginItem) {
System.out.println("重新登陆");
// 关闭当前游戏窗口
this.setVisible(false);
// 打开登录窗口
new LoginJFrame();
}
// ========== 点击"关闭游戏" ==========
else if(obj == closeItem) {
System.out.println("关闭游戏");
// 退出整个程序(关闭虚拟机)
System.exit(0);
}
// ========== 点击"联系方式"(显示微信二维码!)==========
else if(obj == accountItem) {
System.out.println("联系方式");
// 🎯 这里就是显示二维码的核心代码!
// 1. 创建一个对话框窗口(弹窗)
JDialog jDialog = new JDialog();
// 2. 创建一个图片标签,加载二维码图片
// 路径:..\\puzzlegame\\code.jpg
JLabel jLabel = new JLabel(new ImageIcon("..\\puzzlegame\\code.jpg"));
// 3. 设置图片在弹窗中的位置和大小
jLabel.setBounds(0, 0, 499, 499);
// 4. 将图片添加到弹窗中
jDialog.getContentPane().add(jLabel);
// 5. 设置弹窗属性
jDialog.setSize(344, 344); // 弹窗大小
jDialog.setAlwaysOnTop(true); // 弹窗置顶
jDialog.setLocationRelativeTo(null); // 弹窗居中
jDialog.setModal(true); // 模态窗口(不关闭弹窗无法操作其他地方)
// 6. 显示弹窗
jDialog.setVisible(true);
/**
* 效果说明:
* 当用户点击"关于我们" → "联系方式"时
* 会弹出一个344×344像素的窗口
* 窗口中显示你的微信二维码图片(code.jpg)
* 用户扫描后可以关注你的公众号或加好友
*/
}
}
} // GameJFrame 类结束
文件3:LoginJFrame.java – 登录窗口
package puzzlegame.ui;
import javax.swing.*;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
/**
* 登录窗口类
* 功能:让已注册的用户输入用户名和密码进行登录
*/
public class LoginJFrame extends JFrame implements ActionListener {
// 创建窗口对象
JFrame dk = new JFrame("登录");
// 创建按钮
JButton login = new JButton("登录"); // 登录按钮
JButton exit = new JButton("退出"); // 退出按钮
// 创建标签(显示文字提示)
JLabel name1 = new JLabel("用户名"); // 用户名标签
JLabel pwd1 = new JLabel("密码"); // 密码标签
// 创建文本输入框(让用户输入内容)
JTextField name = new JTextField(13); // 用户名输入框(宽度13个字符)
JTextField password = new JTextField(13); // 密码输入框
/**
* 构造方法:初始化登录窗口
*/
public LoginJFrame() {
initLoginJFrame(); // 调用初始化方法
}
/**
* 初始化登录窗口的界面
*/
private void initLoginJFrame() {
dk.setSize(210, 200); // 窗口大小
dk.setAlwaysOnTop(true); // 窗口置顶
dk.setLocationRelativeTo(null); // 窗口居中
dk.setLocation(200, 200); // 窗口位置(距离屏幕左边200,上边200)
dk.setDefaultCloseOperation(2); // 点击关闭按钮时的行为(2=隐藏窗口)
dk.setLayout(new FlowLayout()); // 使用流式布局(组件从左到右排列)
// 将所有组件依次添加到窗口中
dk.add(name1); // 添加"用户名"标签
dk.add(name); // 添加用户名输入框
dk.add(pwd1); // 添加"密码"标签
dk.add(password); // 添加密码输入框
dk.add(login); // 添加登录按钮
dk.add(exit); // 添加退出按钮
// 为按钮绑定点击事件监听器
login.addActionListener(this); // 登录按钮
exit.addActionListener(this); // 退出按钮
// 显示窗口
dk.setVisible(true);
}
/**
* 处理按钮点击事件
*/
@Override
public void actionPerformed(ActionEvent e) {
// ========== 点击"登录"按钮 ==========
if(e.getSource() == login) {
// 检查输入框是否为空
if(name.getText().equals("") || password.getText().equals("")) {
// 弹出提示框:"用户名或密码不能为空"
JOptionPane.showMessageDialog(this, "用户名或密码不能为空");
// 清空输入框
name.setText("");
password.setText("");
} else {
try {
// 准备写入用户信息(虽然登录不需要写,但这里可能是为了记录日志)
BufferedWriter w = new BufferedWriter(
new FileWriter("D:\\用户信息2.0.txt", true) // true表示追加模式
);
// 将用户输入的用户名和密码拼接成一行
String sum = name.getText() + " " + password.getText();
// 例如:"zhangsan 123456"
// 读取用户数据文件
BufferedReader r = new BufferedReader(
new FileReader("D:\\用户信息2.0.txt")
);
String text;
Boolean c = false; // 用于标记是否找到匹配的账号
// 逐行读取文件,查找是否有匹配的账号
while((text = r.readLine()) != null) {
if(sum.equals(text)) {
c = true; // 找到了!账号密码匹配
}
}
// 判断登录结果
if(c == true) {
// 登录成功
JOptionPane.showMessageDialog(this, "登录中!!!");
dk.setVisible(false); // 关闭登录窗口
new GameJFrame(); // 打开游戏主界面
} else {
// 登录失败
JOptionPane.showMessageDialog(this,
"用户名或密码错误或没有注册,请重新输入或进入注册!!!");
new RegisterJFrame(); // 打开注册窗口
// 清空输入框
name.setText("");
password.setText("");
}
} catch(IOException ee) {
// 异常处理(文件读写错误)
ee.printStackTrace();
}
}
}
// ========== 点击"退出"按钮 ==========
if(e.getSource() == exit) {
dk.setVisible(false); // 关闭登录窗口
}
}
}
文件4:RegisterJFrame.java – 注册窗口
package puzzlegame.ui;
import javax.swing.*;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
/**
* 注册窗口类
* 功能:让新用户创建账号
*/
public class RegisterJFrame extends JFrame implements ActionListener {
// 创建窗口对象
JFrame dk = new JFrame("注册");
// 创建按钮
JButton regist = new JButton("注册"); // 注册按钮
JButton exit = new JButton("退出"); // 退出按钮
// 创建标签
JLabel name1 = new JLabel("用户名");
JLabel pwd1 = new JLabel("密码");
// 创建文本输入框
JTextField name = new JTextField(13);
JTextField password = new JTextField(13);
/**
* 构造方法
*/
public RegisterJFrame() {
initregistJFrame(); // 初始化界面
}
/**
* 初始化注册窗口
*/
private void initregistJFrame() {
dk.setSize(210, 200);
dk.setAlwaysOnTop(true);
dk.setLocationRelativeTo(null);
dk.setLocation(200, 200);
dk.setDefaultCloseOperation(2);
dk.setLayout(new FlowLayout());
// 添加组件到窗口
dk.add(name1);
dk.add(name);
dk.add(pwd1);
dk.add(password);
dk.add(regist);
dk.add(exit);
// 绑定事件
regist.addActionListener(this);
exit.addActionListener(this);
// 显示窗口
dk.setVisible(true);
}
/**
* 处理按钮点击事件
*/
@Override
public void actionPerformed(ActionEvent e) {
// ========== 点击"注册"按钮 ==========
if(e.getSource() == regist) {
// 检查输入是否为空
if(name.getText().equals("") || password.getText().equals("")) {
JOptionPane.showMessageDialog(this, "用户名或密码不能为空");
name.setText("");
password.setText("");
} else {
try {
// 创建文件写入流(追加模式)
BufferedWriter w = new BufferedWriter(
new FileWriter("D:\\用户信息2.0.txt", true)
);
// 拼接用户名和密码
String sum = name.getText() + " " + password.getText();
// 创建文件读取流(用于检查用户是否已存在)
BufferedReader r = new BufferedReader(
new FileReader("D:\\用户信息2.0.txt")
);
Boolean c = true; // 默认可以注册(用户不存在)
String text;
// 逐行读取文件,检查是否已经有相同的账号
while((text = r.readLine()) != null) {
if(sum.equals(text)) {
c = false; // 找到了相同账号,不能重复注册
}
}
// 判断注册结果
if(c == true) {
// 注册成功
w.write(sum); // 写入用户信息
w.newLine(); // 写入换行符(每条记录占一行)
w.close(); // 关闭写入流
r.close(); // 关闭读取流
JOptionPane.showMessageDialog(this, "注册成功!");
dk.setVisible(false); // 关闭注册窗口
new LoginJFrame(); // 跳转到登录窗口
} else {
// 注册失败(用户已存在)
JOptionPane.showMessageDialog(this, "该用户已存在!");
name.setText(""); // 清空输入框
password.setText("");
}
} catch(IOException ee) {
ee.printStackTrace();
}
}
}
// ========== 点击"退出"按钮 ==========
if(e.getSource() == exit) {
dk.setVisible(false); // 关闭注册窗口
}
}
}
四、运行步骤详解
代码都写好了,怎么运行呢?跟着下面的步骤来:
步骤1:安装开发环境
- 下载并安装 JDK(Java Development Kit),推荐版本 JDK 8 或 JDK 11
- 下载并安装集成开发环境(IDE),推荐使用 Eclipse 或 IntelliJ IDEA
步骤2:创建项目
- 打开IDE,创建一个新的 Java Project
- 项目名称设置为:
puzzlegame - 在src目录下创建包:
puzzlegame.ui
步骤3:复制代码
- 在
puzzlegame.ui包下创建4个Java文件:App.java,GameJFrame.java,LoginJFrame.java,RegisterJFrame.java - 将上面的代码分别复制到对应文件中
步骤4:准备图片资源(关键)
- 在项目根目录(
D:\java\puzzlegame\)下创建以下文件夹结构:pic\picture0\到pic\picture23\(共24个文件夹)
- 在每个
pictureN文件夹中放入:0.jpg(空白块,可以用纯色图片代替)1.jpg到15.jpg(16张拼图碎片,尺寸105×105像素)all.jpg(完整原图,尺寸420×420像素)
- 在项目根目录放入:
background.png(背景图,508×560像素)win.png(胜利图标,197×73像素)code.jpg(你的微信二维码,344×344像素)
步骤5:创建用户数据文件
- 在
D:\盘根目录创建一个文本文件:用户信息2.0.txt - 可以先留空,或者手动添加一些测试账号(格式:
用户名 密码,中间用空格分隔)
步骤6:运行程序
- 在IDE中找到
App.java文件 - 右键 → Run As → Java Application
- 程序启动后会同时弹出登录窗口和注册窗口
五、游戏操作指南
| 操作 | 按键/方式 | 功能说明 |
|---|---|---|
| 移动拼图 | ↑ ↓ ← → 方向键 | 向对应方向移动拼图块 |
| 预览原图 | 长按 A 键 | 按住不放显示完整图,松开恢复 |
| 一键通关 | 按 W 键 | 作弊功能,直接完成拼图 |
| 重新开始 | 菜单 → 功能 → 重新游戏 | 打乱拼图,步数清零,换新图片 |
| 查看二维码 | 菜单 → 关于我们 → 联系方式 | 弹窗显示开发者微信二维码 |
| 退出游戏 | 菜单 → 功能 → 关闭游戏 | 关闭整个程序 |
六、常见问题解答(FAQ)
Q1:运行后看不到图片,只显示了空白窗口?
A:这是最常见的错误!请检查:
- 图片文件夹路径是否正确?必须在
D:\java\puzzlegame\目录下 - 图片文件名是否正确?必须是
0.jpg,1.jpg…15.jpg - 图片格式是否正确?代码中使用的是
.jpg和.png格式 - 控制台是否有报错?如果有
FileNotFoundException说明路径不对
Q2:为什么我的拼图无法还原?(出现无解情况)
A:理论上Fisher-Yates洗牌算法不会产生无解情况。但如果遇到,可以点击”重新游戏”重新打乱即可。
Q3:想修改图片存放路径怎么办?
A:修改 GameJFrame.java 中的 path 变量和所有 new ImageIcon(...) 中的路径即可。建议使用相对路径或者从配置文件读取。
Q4:如何更换成自己的照片?
A:
- 选一张你喜欢的照片
- 用图片编辑软件将其切成 4×4 = 16 等份(每份 105×105 像素)
- 将这16张小图分别命名为
1.jpg到15.jpg(注意:0.jpg是空白块,用纯色图代替) - 保存一张完整的原图命名为
all.jpg(420×420 像素) - 放入任意一个
pictureN文件夹中即可
Q5:用户数据文件在哪里?可以修改吗?
A:用户数据保存在 D:\用户信息2.0.txt 中,每行一个账号,格式为:用户名 密码(中间用空格分隔)。你可以直接编辑这个文件来添加或删除账号。
七、技术要点总结
通过这个项目,你学到了以下核心技术:
| 技术点 | 应用场景 | 掌握程度 |
|---|---|---|
| Swing GUI | 窗口、按钮、标签、菜单等界面组件 | ⭐⭐⭐⭐⭐ |
| 事件监听 | 键盘事件、按钮点击事件 | ⭐⭐⭐⭐⭐ |
| 二维数组 | 存储和管理拼图状态 | ⭐⭐⭐⭐ |
| 文件IO | 读写用户账号数据 | ⭐⭐⭐ |
| 随机算法 | Fisher-Yates洗牌算法 | ⭐⭐⭐ |
| 面向对象 | 类的封装、继承、接口实现 | ⭐⭐⭐⭐ |
八、进阶优化方向
如果你想继续提升这个项目,可以尝试以下优化:
- 安全性增强:对密码进行MD5加密存储,不要使用明文
- 数据库升级:用SQLite或MySQL替代文本文件存储用户数据
- 鼠标支持:增加鼠标点击移动拼图的功能(目前只能用键盘)
- 计时功能:增加游戏计时器,记录完成时间
- 排行榜:记录玩家的最佳成绩和历史记录
- 多难度:支持3×3(简单)、4×4(中等)、5×5(困难)多种难度
- 音效:添加移动音效、胜利音效等
- 自定义图片:允许玩家上传自己的照片作为拼图