Java拼图游戏项目实战-基于Swing的面向对象程序设计

今天我要分享一个我大一做的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:安装开发环境

  1. 下载并安装 JDK(Java Development Kit),推荐版本 JDK 8 或 JDK 11
  2. 下载并安装集成开发环境(IDE),推荐使用 EclipseIntelliJ IDEA

步骤2:创建项目

  1. 打开IDE,创建一个新的 Java Project
  2. 项目名称设置为:puzzlegame
  3. 在src目录下创建包:puzzlegame.ui

步骤3:复制代码

  1. puzzlegame.ui 包下创建4个Java文件:App.java, GameJFrame.java, LoginJFrame.java, RegisterJFrame.java
  2. 将上面的代码分别复制到对应文件中

步骤4:准备图片资源(关键)

  1. 在项目根目录(D:\java\puzzlegame\)下创建以下文件夹结构:
    • pic\picture0\pic\picture23\(共24个文件夹)
  2. 在每个 pictureN 文件夹中放入:
    • 0.jpg (空白块,可以用纯色图片代替)
    • 1.jpg15.jpg (16张拼图碎片,尺寸105×105像素)
    • all.jpg (完整原图,尺寸420×420像素)
  3. 在项目根目录放入:
    • background.png (背景图,508×560像素)
    • win.png (胜利图标,197×73像素)
    • code.jpg (你的微信二维码,344×344像素)

步骤5:创建用户数据文件

  1. D:\ 盘根目录创建一个文本文件:用户信息2.0.txt
  2. 可以先留空,或者手动添加一些测试账号(格式:用户名 密码,中间用空格分隔)

步骤6:运行程序

  1. 在IDE中找到 App.java 文件
  2. 右键 → Run AsJava Application
  3. 程序启动后会同时弹出登录窗口和注册窗口

 

五、游戏操作指南

操作 按键/方式 功能说明
移动拼图 ↑ ↓ ← → 方向键 向对应方向移动拼图块
预览原图 长按 A 键 按住不放显示完整图,松开恢复
一键通关 按 W 键 作弊功能,直接完成拼图
重新开始 菜单 → 功能 → 重新游戏 打乱拼图,步数清零,换新图片
查看二维码 菜单 → 关于我们 → 联系方式 弹窗显示开发者微信二维码
退出游戏 菜单 → 功能 → 关闭游戏 关闭整个程序

 

 六、常见问题解答(FAQ)

Q1:运行后看不到图片,只显示了空白窗口?

A:这是最常见的错误!请检查:

  • 图片文件夹路径是否正确?必须在 D:\java\puzzlegame\ 目录下
  • 图片文件名是否正确?必须是 0.jpg, 1.jpg15.jpg
  • 图片格式是否正确?代码中使用的是 .jpg.png 格式
  • 控制台是否有报错?如果有 FileNotFoundException 说明路径不对

Q2:为什么我的拼图无法还原?(出现无解情况)

A:理论上Fisher-Yates洗牌算法不会产生无解情况。但如果遇到,可以点击”重新游戏”重新打乱即可。

Q3:想修改图片存放路径怎么办?

A:修改 GameJFrame.java 中的 path 变量和所有 new ImageIcon(...) 中的路径即可。建议使用相对路径或者从配置文件读取。

Q4:如何更换成自己的照片?

A:

  1. 选一张你喜欢的照片
  2. 用图片编辑软件将其切成 4×4 = 16 等份(每份 105×105 像素)
  3. 将这16张小图分别命名为 1.jpg15.jpg(注意:0.jpg是空白块,用纯色图代替)
  4. 保存一张完整的原图命名为 all.jpg(420×420 像素)
  5. 放入任意一个 pictureN 文件夹中即可

Q5:用户数据文件在哪里?可以修改吗?

A:用户数据保存在 D:\用户信息2.0.txt 中,每行一个账号,格式为:用户名 密码(中间用空格分隔)。你可以直接编辑这个文件来添加或删除账号。

 

七、技术要点总结

通过这个项目,你学到了以下核心技术:

技术点 应用场景 掌握程度
Swing GUI 窗口、按钮、标签、菜单等界面组件 ⭐⭐⭐⭐⭐
事件监听 键盘事件、按钮点击事件 ⭐⭐⭐⭐⭐
二维数组 存储和管理拼图状态 ⭐⭐⭐⭐
文件IO 读写用户账号数据 ⭐⭐⭐
随机算法 Fisher-Yates洗牌算法 ⭐⭐⭐
面向对象 类的封装、继承、接口实现 ⭐⭐⭐⭐

     八、进阶优化方向

如果你想继续提升这个项目,可以尝试以下优化:

  • 安全性增强:对密码进行MD5加密存储,不要使用明文
  • 数据库升级:用SQLite或MySQL替代文本文件存储用户数据
  • 鼠标支持:增加鼠标点击移动拼图的功能(目前只能用键盘)
  • 计时功能:增加游戏计时器,记录完成时间
  • 排行榜:记录玩家的最佳成绩和历史记录
  • 多难度:支持3×3(简单)、4×4(中等)、5×5(困难)多种难度
  • 音效:添加移动音效、胜利音效等
  • 自定义图片:允许玩家上传自己的照片作为拼图

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部