Java综合项目(二)之拼图游戏

一、项目介绍

  本项目是一个基于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);
        }
    }
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值