16-JavaSE基础巩固项目:拼图小游戏

阶段项目-拼图小游戏

一、项目介绍

16-JavaSE基础巩固项目:拼图小游戏_第1张图片


1、目的

  • 锻炼逻辑思维能力,让我们知道前面学习的知识点在实际开发中的应用场景

  • 1、为了学习一个新知识:GUI

    • GUI全称:Graphical User Interface(又称图形用户接口)
    • 是指采用图形化的方式显示操作界面。
  • 2、为什么图形化用户界面这个知识点很多Java课程中都不讲呢?

    • 因为我们Java语言主要是做后台服务器开发的,在企业中很少用得到GUI这个技术的。
    • 那服务器又是什么??
      • 举个例子:
        • 1、比如我们在网页上看小说,那些小说内容就是服务器通过网络传递给客户端(电脑、手机等)的。
          • 这样我们才能看到小说内容。
        • 2、比如网页上的登录、注册、内容、订阅、打赏等等这些功能其实都是服务器在做的。
        • 3、在一个项目中,用户是接触不到服务器的,只能接触到客户端:
          • 客户端:浏览器、客户端的安装包(用户需要下载、安装使用)、手机中写一个APP
          • 这些都是跟用户直接接触的。
    • 因此,我们Java是主要做后端服务器开发的,是很少去跟用户直接接触的
    • 所以,图形化(GUI)这个知识点在企业中很少会有人去用,因为这个原因,很多课程当中,就把这章节的内容删掉了
  • 3、难道图形化用户界面这个知识点是真的一点用都没有了吗?

    • 我是不敢苟同的,因为我觉得这个知识点对于初学者来说用处很大:
      • (1)我们之前学习的知识点,都是一个一个模块进行学习的,非常的零散,而且用的并不是很多
      • (2)所以在我们的头脑中,这些一个一个模块的知识点是零散的,哪怕我们都学完了,代码也能写出来了!依然没有太大的底气和别人说我们有技术!
    • 因此,我们要做一个阶段项目,将这些零零散散的知识点整合起来,形成系统的概念(重点)
      • 假如说现在我们不写这个阶段项目,继续的往后学习新的知识点,接下来要学习第二阶段的JavaWeb
      • 这样的话,又要用一两个月学完一整个JavaWeb之后,才会有项目做,这样的话,就会产生一个问题:
        • 就是JavaSE基础各模块的知识点都没有整合成系统的概念,都没做过项目整合过。
        • 就开始做JavaSE、JavaWeb一起学习完之后的项目,根本就是很难接受的,脑子就会乱。
    • 所以,我们要做一个阶段项目,练练手,将JavaSE基础知识点整合起来!用起来!
  • 4、那我们之前做的学生管理系统、文字版格斗游戏、评委打分等等那些难道不是项目吗?

    • 可以很负责任的告诉你,那些只能算是综合小案例,完全不能算是项目。
    • 为什么这么说?
      • 因为你想啊!我们平时玩电脑游戏、看小说、登录注册等等这些,是不是都是用鼠标点一下就会出界面了!
      • 点一下就做对应的事情!这种思想是非常重要的!
      • 没有图形化界面,是实现不了这种效果的。


2、所用知识

  • 1、面向对象三大特征:
    • 封装、继承、多态
  • 2、抽象类、接口、内部类
  • 3、集合、字符串、数组、循环、判断
  • 4、…


3、主界面分析

  • 拆分成三部分:

    • 1、最外层的窗体

      • JFrame
        • 全称Java Frame:
          • Java:就是我们的Java语言
          • Frame:窗体、边框、界面

      16-JavaSE基础巩固项目:拼图小游戏_第2张图片



    • 2、最上层的菜单

      • JMenuBar
        • 全称Java Menu Bar:
          • Java:就是我们的Java语言
          • Meun:菜单
          • Bar:栏目、条目

      16-JavaSE基础巩固项目:拼图小游戏_第3张图片



    • 3、管理文字和图片的容器

      • JLabel

        • 全称Java Label
          • Java:就是我们的Java语言
          • Label:理解为容器、区域的意思就可以了

        16-JavaSE基础巩固项目:拼图小游戏_第4张图片



  • 最外层的窗体、最上层的菜单、管理文字和图片的容器都称为组件。
  • 包括以后我们看到的图片、按钮、进度条、文字等等都是组件。
  • 组件它是一个统称

16-JavaSE基础巩固项目:拼图小游戏_第5张图片




二、界面搭建

1、需求

  • 创建主界面1
    • 最外层的窗体:
      • 1、创建一个宽603像素,高680像素的游戏主界面
      • 2、创建一个宽501像素,高437像素的登录界面
      • 3、创建一个宽501像素,高437像素的注册界面

2、分析

  • 从需求可以看出,游戏主界面、登录界面、注册界面都有一个共同的地方。
  • 那就是它们都是界面,因此它们都是JFrame的子类,所以需要将每个界面都独立成一个界面类,继承父类:JFrame
  • 如此一来:
    • 1、关于游戏相关的逻辑代码就可以全部写在游戏主界面类中
    • 2、关于登录相关的逻辑代码就可以全部写在登录界面类中
    • 3、关于注册相关的逻辑代码就可以全部写在注册界面类中
  • 最后,只需要在main方法中构造各个界面就可以显示出来了

3、实现

(1)构建项目文件

16-JavaSE基础巩固项目:拼图小游戏_第6张图片


(2)游戏主界面类

package cn.edu.gxufe.ui;

import javax.swing.*;

/**
 * 游戏主界面类:
 * 继承父类:JFrame
 */
public class GameJFrame extends JFrame{
    // 表示游戏相关的逻辑代码都写在这!

    /*
        提供无参数的构造器
        需求:初始化一个宽603像素,高680像素的游戏主界面
     */
    public GameJFrame() {
        // 注:如果当前类没有setSize、setVisible方法,就会自动调用父类JFrame的
        // 调用setSize方法,初始化一个宽603像素,高680像素大小的界面
        this.setSize(603, 680);
        // 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面
        // 由于界面默认是隐藏起来的,因此需要调用setVisible方法,设置显示界面
        this.setVisible(true);
    }
}

(3)登录界面类

package cn.edu.gxufe.ui;

import javax.swing.*;

/**
 * 登录界面类:
 * 继承父类:JFrame
 */
public class LoginJFrame extends JFrame {
    // 表示登录相关的逻辑代码都写在这!

    /*
        提供无参数的构造器
        需求:初始化一个宽488像素,高430像素的登录界面
     */
    public LoginJFrame() {
        // 注:如果当前类没有setSize、setVisible方法,就会自动调用父类JFrame的
        // 设置界面宽高
        // 调用setSize方法,初始化一个宽501像素,高437像素大小的界面
        this.setSize(501, 437);
        // 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面
        // 由于界面默认是隐藏起来的,因此需要调用setVisible方法,设置显示界面
        this.setVisible(true);
    }
}

(4)注册界面类

package cn.edu.gxufe.ui;

import javax.swing.*;

/**
 * 注册界面类:
 * 继承父类:JFrame
 */
public class RegisterJFrame extends JFrame{
    // 表示注册相关的逻辑代码都写在这!

    /*
        提供无参数的构造器
        需求:初始化一个宽488像素,高500像素的登录界面
     */
    public RegisterJFrame() {
        // 注:如果当前类没有setSize、setVisible方法,就会自动调用父类JFrame的
        // 调用setSize方法,初始化一个宽501像素,高437像素大小的界面
        this.setSize(501, 437);
        // 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面
        // 由于界面默认是隐藏起来的,因此需要调用setVisible方法,设置显示界面
        this.setVisible(true);
    }
}

(5)程序的启动入口类

import cn.edu.gxufe.ui.GameJFrame;
import cn.edu.gxufe.ui.LoginJFrame;
import cn.edu.gxufe.ui.RegisterJFrame;

public class App {
    public static void main(String[] args) {
        // 表示程序启动的入口

        // 如果我们想要开启一个界面,就创建对应界面的对象即可!
        new GameJFrame();       // 游戏主界面
        new LoginJFrame();      // 登录界面
        new RegisterJFrame();   // 注册界面
    }
}

(6)测试结果

  • 可以看到,三个界面已经构建好了!

16-JavaSE基础巩固项目:拼图小游戏_第7张图片


(7)使用继承的好处

16-JavaSE基础巩固项目:拼图小游戏_第8张图片

  • 可以将代码分类出来,避免将全部逻辑代码全部写在一个类中



三、界面设置、菜单搭建

1、界面设置需求

  • 设置各个界面:
    • 设置界面宽高:
      • 已设置
    • 设置界面标题:
      • 游戏主界面:奥利gei拼图单机版 v1.0
      • 登录界面:奥利gei拼图-登录
      • 注册界面:奥利gei拼图-注册
    • 设置界面置顶:
      • 用户在点击其他界面时,该界面都永远置顶。
    • 设置界面居中:
      • 用户打开界面时,自动居中。
    • 设置界面的关闭模式:
      • 用户只需要关闭一个界面,其他界面自动关闭,并且结束JVM虚拟机运行。
    • 设置界面为显示的:
      • 已设置。
      • 由于界面默认是隐藏的,因此需要设置为显示的。

2、界面设置实现

  • 游戏主界面类

    package cn.edu.gxufe.ui;
    
    import javax.swing.*;
    
    /**
     * 游戏主界面类:
     * 继承父类:JFrame
     */
    public class GameJFrame extends JFrame{
        // 表示游戏相关的逻辑代码都写在这!
    
        /*
            提供无参数的构造器
            需求:初始化一个宽603像素,高680像素的游戏主界面
         */
        public GameJFrame() {
            // 注:如果当前类没有以下这些方法,就会自动调用父类JFrame的
            // 1、初始化游戏主界面
            initJFrame();
    
            // 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面
            // 由于界面默认是隐藏起来的,因此需要调用当前GameJFrame类的setVisible方法,设置显示界面
            this.setVisible(true);
        }
        
        // 初始化游戏主界面
        private void initJFrame() {
            // 设置界面宽高
            // 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面
            this.setSize(603, 680);
            // 设置界面标题
            this.setTitle("奥利gei拼图单机版 v1.0");
            // 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的
            this.setAlwaysOnTop(true);
            // 设置界面打开时自动居中
            this.setLocationRelativeTo(null);
            // 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行
            // this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能
            this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
    
            // 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件
            // 因为只有这样,添加图片的时候才不会一直处于居中位置
            this.setLayout(null);
        }
    }
    

  • 登录界面类

    package cn.edu.gxufe.ui;
    
    import javax.swing.*;
    
    /**
     * 登录界面类:
     * 继承父类:JFrame
     */
    public class LoginJFrame extends JFrame {
        // 表示登录相关的逻辑代码都写在这!
    
        /*
            提供无参数的构造器
            需求:初始化一个宽501像素,高437像素的登录界面
         */
        public LoginJFrame() {
            // 初始化界面
            initJFrame();
    
            // 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面
            // 由于界面默认是隐藏起来的,因此需要调用setVisible方法,设置显示界面
            this.setVisible(true);
        }
        
        // 初始化登录界面
        private void initJFrame() {
            // 注:如果当前类没有以下这些方法,就会自动调用父类JFrame的
    
            // 设置界面宽高
            // 调用setSize方法,初始化一个宽501像素,高437像素大小的界面
            this.setSize(501, 437);
    
            // 设置界面标题
            this.setTitle("奥利gei拼图-登录");
            // 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的
            this.setAlwaysOnTop(true);
            // 设置界面打开时自动居中
            this.setLocationRelativeTo(null);
            // 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行
            this.setDefaultCloseOperation(3);
            // 取消默认布局,这样子才可以让布局按照x和y轴的形式添加组件
            this.setLayout(null);
        }
    }
    

  • 注册界面类

    package cn.edu.gxufe.ui;
    
    import javax.swing.*;
    
    /**
     * 注册界面类:
     * 继承父类:JFrame
     */
    public class RegisterJFrame extends JFrame{
        // 表示注册相关的逻辑代码都写在这!
    
        /*
            提供无参数的构造器
            需求:初始化一个宽501像素,高437像素的登录界面
         */
        public RegisterJFrame() {
            // 注:如果当前类没有以下这些方法,就会自动调用父类JFrame的
            // 初始化注册界面
            initJFrame();
    
            // 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面
            // 由于界面默认是隐藏起来的,因此需要调用setVisible方法,设置显示界面
            this.setVisible(true);
        }
        
        // 初始化注册界面
        private void initJFrame() {
            // 设置界面宽高
            // 调用setSize方法,初始化一个宽501像素,高437像素大小的界面
            this.setSize(501, 437);
    
            // 设置界面标题
            this.setTitle("奥利gei拼图-注册");
            // 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的
            this.setAlwaysOnTop(true);
            // 设置界面打开时自动居中
            this.setLocationRelativeTo(null);
            // 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行
            // this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能
            this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
            // 取消默认布局,这样子才可以让布局按照x和y轴的形式添加组件
            this.setLayout(null);
        }
    }
    

  • 程序的启动入口类

    import cn.edu.gxufe.ui.GameJFrame;
    import cn.edu.gxufe.ui.LoginJFrame;
    import cn.edu.gxufe.ui.RegisterJFrame;
    
    public class App {
        public static void main(String[] args) {
            // 表示程序启动的入口
    
            // 如果我们想要开启一个界面,就创建对应界面的对象即可!
            new GameJFrame();       // 游戏主界面
            new LoginJFrame();      // 登录界面
            new RegisterJFrame();   // 注册界面
        }
    }
    

  • 测试结果

    16-JavaSE基础巩固项目:拼图小游戏_第9张图片



3、游戏主界面的菜单搭建需求

16-JavaSE基础巩固项目:拼图小游戏_第10张图片

16-JavaSE基础巩固项目:拼图小游戏_第11张图片


4、游戏主界面的菜单结构分析

16-JavaSE基础巩固项目:拼图小游戏_第12张图片

  • 1、需要用到JMenuBar菜单栏类:
    • JMenuBar:是一整根大长条的菜单栏
    • JMeun:是菜单栏下的一个个的菜单
    • JMenuItem:是菜单下的一个个条目
  • 2、因此需要先创建JMenuBar:菜单栏
  • 3、接着创建JMenu:菜单
  • 4、然后创建JMenuItem:菜单条目
  • 5、最后做整合操作
    • 先把JMenuItem放到JMenu里面
    • 然后把JMenu放到JMenuBar里面
    • 最后把JMenuBar放到GameJFrame里面
  • 注意:更换图片的功能较为复杂,我们放到后面再写!!

5、游戏界面的菜单搭建实现

(1)游戏主界面类
package cn.edu.gxufe.ui;

import javax.swing.*;

/**
 * 游戏主界面类:
 * 继承父类:JFrame
 */
public class GameJFrame extends JFrame{
    // 表示游戏相关的逻辑代码都写在这!
    
    /*
        创建二维数组:
            目的:管理数据
            加载图片的时候,会根据二位数组中的数据进行加载
     */
    int[][] data = new int[4][4];

    // 定义x、y变量,用于记录空白方块所在的XY的坐标位置
    int x = 0;
    int y = 0;

    // 创建一个成员的随机数对象,用于生成一个随机数
    Random rd = new Random();

    // 定义path变量,用于记录图片的相对路径,方便以后做更换图片、重新游戏等功能的实现
    String path = "puzzle_game\puzzleimages\girl\girl1\";

    // 定义一个正确顺序数据的二维数组
    int[][] win = {
            {1, 2, 3, 4},
            {5, 6, 7, 8},
            {9, 10, 11, 12},
            {13, 14, 15, 0}
    };


    // 定义一个计数器变量,用于记录玩家拼图移动的步数
    int step = 0;


    // c.创建功能菜单下的三个栏目:重新游戏、重新登录、关闭游戏
    // 更换图片栏目放到后面再写,因为比较复杂
    JMenuItem replayGameItem = new JMenuItem("重新游戏");
    JMenuItem replayLoginItem = new JMenuItem("重新登录");
    JMenuItem closeGameItem = new JMenuItem("关闭游戏");

    // d.创建关于我们菜单下的一个栏目:公众号
    JMenuItem accountItem = new JMenuItem("公众号");

    // 创建更换图片菜单下的两个栏目:美女、动物
    JMenuItem belleItem = new JMenuItem("美女");
    JMenuItem animalItem = new JMenuItem("动物");

    /*
        提供无参数的构造器
        需求:初始化一个宽603像素,高680像素的游戏主界面
     */
    public GameJFrame() {
        // 注:如果当前类没有以下这些方法,就会自动调用父类JFrame的
        // 1、初始化游戏主界面
        initJFrame();
        
        // 2、初始化游戏主界面里的菜单栏
        initJMenu();

        // 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面
        // 由于界面默认是隐藏起来的,因此需要调用当前GameJFrame类的setVisible方法,设置显示界面
        this.setVisible(true);
    }
    
    // 初始化游戏主界面
    private void initJFrame() {
        // 设置界面宽高
        // 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面
        this.setSize(603, 680);
        // 设置界面标题
        this.setTitle("奥利gei拼图单机版 v1.0");
        // 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的
        this.setAlwaysOnTop(true);
        // 设置界面打开时自动居中
        this.setLocationRelativeTo(null);
        // 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行
        // this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能
        this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        // 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件
        // 因为只有这样,添加图片的时候才不会一直处于居中位置
        this.setLayout(null);
    }
    
    // 初始化游戏主界面里的菜单栏
    private void initJMenu() {
        // a.创建一个菜单栏对象
        JMenuBar jMenuBar = new JMenuBar();

        // b.创建两个菜单栏下的菜单:功能、关于我们
        JMenu functionJMenu = new JMenu("功能");
        JMenu aboutUSJMenu = new JMenu("关于我们");

        // 创建菜单栏下的菜单:更换图片
        JMenu changeJMenu = new JMenu("更换图片");
        // 将更换图片的菜单嵌套进功能菜单中
        functionJMenu.add(changeJMenu);

        // e.将重新游戏、重新登录、关闭游戏这些栏目放到功能菜单下
        functionJMenu.add(replayGameItem);
        functionJMenu.add(replayLoginItem);
        functionJMenu.add(closeGameItem);

        // 将美女、动物这两个栏目放到更换图片菜单下
        changeJMenu.add(belleItem);
        changeJMenu.add(animalItem);

        // f.将公众号这个栏目放到关于我们菜单下
        aboutUSJMenu.add(accountItem);

        // g.将功能、关于我们这两个菜单放到菜单栏下
        jMenuBar.add(functionJMenu);
        jMenuBar.add(aboutUSJMenu);

        // h.最后将菜单栏放到设置到游戏界面中
        this.setJMenuBar(jMenuBar);
    }
}

(2)程序的启动入口类
import cn.edu.gxufe.ui.GameJFrame;
import cn.edu.gxufe.ui.LoginJFrame;
import cn.edu.gxufe.ui.RegisterJFrame;

public class App {
    public static void main(String[] args) {
        // 表示程序启动的入口

        // 如果我们想要开启一个界面,就创建对应界面的对象即可!
        new GameJFrame();       // 游戏主界面
//         new LoginJFrame();      // 登录界面
//         new RegisterJFrame();   // 注册界面
    }
}

(3)测试结果

16-JavaSE基础巩固项目:拼图小游戏_第13张图片


16-JavaSE基础巩固项目:拼图小游戏_第14张图片




四、添加图片

1、需求

16-JavaSE基础巩固项目:拼图小游戏_第15张图片

  • 一整张图片的像素是420x420
  • 将一整张图片拆分成15张小图片,每张小图片的像素是105x105

2、分析

  • 1、需要用到管理容器类JLabel
    • 用于管理文字、图片
  • 2、需要用到图像类ImageIcon
    • 用于根据指定文件路径创建一个图片对象
  • 3、使用循环嵌套动态控制添加4行图片,每行添加4张小图片
    • 根据指定文件路径创建一个图片对象
    • 创建一个管理容器,并将图片对象放进来
    • 将管理容器放到游戏主界面中

3、实现

(1)游戏主界面类

package cn.edu.gxufe.ui;

import javax.swing.*;

/**
 * 游戏主界面类:
 * 继承父类:JFrame
 */
public class GameJFrame extends JFrame{
    // 表示游戏相关的逻辑代码都写在这!
    
    /*
        创建二维数组:
            目的:管理数据
            加载图片的时候,会根据二位数组中的数据进行加载
     */
    int[][] data = new int[4][4];

    // 定义x、y变量,用于记录空白方块所在的XY的坐标位置
    int x = 0;
    int y = 0;

    // 创建一个成员的随机数对象,用于生成一个随机数
    Random rd = new Random();

    // 定义path变量,用于记录图片的相对路径,方便以后做更换图片、重新游戏等功能的实现
    String path = "puzzle_game\puzzleimages\girl\girl1\";

    // 定义一个正确顺序数据的二维数组
    int[][] win = {
            {1, 2, 3, 4},
            {5, 6, 7, 8},
            {9, 10, 11, 12},
            {13, 14, 15, 0}
    };


    // 定义一个计数器变量,用于记录玩家拼图移动的步数
    int step = 0;


    // c.创建功能菜单下的三个栏目:重新游戏、重新登录、关闭游戏
    // 更换图片栏目放到后面再写,因为比较复杂
    JMenuItem replayGameItem = new JMenuItem("重新游戏");
    JMenuItem replayLoginItem = new JMenuItem("重新登录");
    JMenuItem closeGameItem = new JMenuItem("关闭游戏");

    // d.创建关于我们菜单下的一个栏目:公众号
    JMenuItem accountItem = new JMenuItem("公众号");

    // 创建更换图片菜单下的两个栏目:美女、动物
    JMenuItem belleItem = new JMenuItem("美女");
    JMenuItem animalItem = new JMenuItem("动物");

    /*
        提供无参数的构造器
        需求:初始化一个宽603像素,高680像素的游戏主界面
     */
    public GameJFrame() {
        // 注:如果当前类没有以下这些方法,就会自动调用父类JFrame的
        // 初始化游戏主界面
        initJFrame();
        
        // 初始化游戏主界面里的菜单栏
        initJMenu();
        
        // 初始化图片到游戏主界面中
        initImages(path);

        // 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面
        // 由于界面默认是隐藏起来的,因此需要调用当前GameJFrame类的setVisible方法,设置显示界面
        this.setVisible(true);
    }
    
    // 初始化游戏主界面
    private void initJFrame() {
        // 设置界面宽高
        // 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面
        this.setSize(603, 680);
        // 设置界面标题
        this.setTitle("奥利gei拼图单机版 v1.0");
        // 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的
        this.setAlwaysOnTop(true);
        // 设置界面打开时自动居中
        this.setLocationRelativeTo(null);
        // 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行
        // this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能
        this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        // 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件
        // 因为只有这样,添加图片的时候才不会一直处于居中位置
        this.setLayout(null);
    }
    
    // 初始化游戏主界面里的菜单栏
    private void initJMenu() {
        // a.创建一个菜单栏对象
        JMenuBar jMenuBar = new JMenuBar();

        // b.创建两个菜单栏下的菜单:功能、关于我们
        JMenu functionJMenu = new JMenu("功能");
        JMenu aboutUSJMenu = new JMenu("关于我们");

        // 创建菜单栏下的菜单:更换图片
        JMenu changeJMenu = new JMenu("更换图片");
        // 将更换图片的菜单嵌套进功能菜单中
        functionJMenu.add(changeJMenu);

        // e.将重新游戏、重新登录、关闭游戏这些栏目放到功能菜单下
        functionJMenu.add(replayGameItem);
        functionJMenu.add(replayLoginItem);
        functionJMenu.add(closeGameItem);

        // 将美女、动物这两个栏目放到更换图片菜单下
        changeJMenu.add(belleItem);
        changeJMenu.add(animalItem);

        // f.将公众号这个栏目放到关于我们菜单下
        aboutUSJMenu.add(accountItem);

        // g.将功能、关于我们这两个菜单放到菜单栏下
        jMenuBar.add(functionJMenu);
        jMenuBar.add(aboutUSJMenu);

        // h.最后将菜单栏放到设置到游戏界面中
        this.setJMenuBar(jMenuBar);
    }
    
    /*
        初始化图片到游戏主界面中
        细节:先加载的图片在上方,后加载的图片会在下方
     */
    private void initImages(String path) {
        // 先清空原本出现的所有图片
        this.getContentPane().removeAll();

        // 判断victory方法的返回结果是否为true
        if (victory()) {
            // 是,则说明玩家胜利了!显示胜利图标!
            // 根据指定图片的相对路径创建图片对象,并添加到管理容器中
            JLabel winJLabel = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\win.jpg"));
            // 设置胜利图片的坐标位置以及宽高
            winJLabel.setBounds(193, 300, 194, 75);
            // 将管理容器添加到游戏主界面中
            this.getContentPane().add(winJLabel);
        }

        // 统计步数
        // 创建一个管理容器对象,用于管理文字(步数: 0)
        JLabel stepJLabel = new JLabel("步数:" + step);
        // 设置管理容器的坐标位置
        stepJLabel.setBounds(40, 20, 400, 50);
        // 将容器添加到游戏主界面中
        this.getContentPane().add(stepJLabel);

        // 再加载图片
        /*
           外循环和内循环执行流程解析:
             外循环第一次执行:
                当 i = 0 时:i<4,为true,表示添加第一行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=1,表示添加第一行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=2,表示添加第一行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=3,表示添加第一行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=4,表示添加第一行的第四张图片
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第二次执行:
                当 i = 1 时:i<4,为true,表示添加第二行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=5,表示添加第二行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=6,表示添加第二行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=7,表示添加第二行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=8,表示添加第二行的第四张图片
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第三次执行:
                当 i = 2 时:i<4,为true,表示添加第三行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=9,表示添加第三行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=10,表示添加第三行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=11,表示添加第三行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=12,表示添加第三行的第四张图片
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第四次执行:
                当 i = 3 时:i<4,为true,表示添加第四行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=13,表示添加第四行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=14,表示添加第四行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=15,表示添加第四行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=16,表示添加第四行的第四张图片(由于文件中没有16这个命名的图片,因此会添加一个空白)
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第五次执行:
                当 i = 4 时:i<4,为false,外循环结束!
        */
        // 外循环:控制添加4行图片
        for (int i = 0; i < data.length; i++) { // Y轴:纵向
            // 内循环:控制每行添加4张图片
            for (int j = 0; j < data[i].length; j++) { // X轴:横向
                /*
                    解析:
                        比如二维数组中的数据:[{3, 9, 7, 5} {8, 10, 14, 11} {4, 2, 12, 13} {0, 15, 1, 6} ]
                   二维数组中的一维数组的索引:  0  1  2  3   0   1  2   3    0  1   2   3   0   1  2  3
                             二维数组的索引:       0             1              2              3
                      (1) 外循环执行第一次:
                        i=0, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (2) 外循环执行第二次:
                        i=1, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (3) 外循环执行第三次:
                        i=2, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (4) 外循环执行第四次:
                        i=3, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (5) 外循环执行第五次:
                        i=0, i<二维数组长度(4), 为false, 外循环结束!!
                 */
                // 接收二维数组i索引的一维数组j索引的数据
                int number = data[i][j];
                // 1、根据指定的文件相对路径创建一个ImageIcon图片对象
                ImageIcon imageIcon = new ImageIcon(path + number + ".jpg");

                // JLabel:管理容器,用于管理图片、文字
                // 2、创建一个管理容器JLabel对象,用于管理图片对象ImageIcon,
                // 并将图片对象imageIcon放到管理容器jLabel中。
                JLabel jLabel = new JLabel(imageIcon);

                // 3、指定图片位置:XY轴
                jLabel.setBounds(105 * j + 80, 105 * i + 130, 105, 105);

                // 4、每添加完一张小图片,给这张小图片添加边框
                /*
                    0: 表示让图片凸起来
                    1: 表示让图片凹下去
                 */
                jLabel.setBorder(new BevelBorder(1));

                // 5、将管理容器JLabel对象放到游戏主界面中
                // getContentPane:获取隐藏容器
                this.getContentPane().add(jLabel);
            }
        }

        // 在所有小图片都添加到界面之后,开始添加背景图片
        // 根据指定的相对路径创建背景图片对象,并添加到管理容器中
        JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));
        // 设置背景图片的坐标位置以及宽高
        background.setBounds(36, 36, 508, 560);
        // 将背景图片管理容器添加到界面中
        this.getContentPane().add(background);

        // 最后刷新一下游戏界面
        this.getContentPane().repaint();
    }
}

(2)程序启动入口类

import cn.edu.gxufe.ui.GameJFrame;
import cn.edu.gxufe.ui.LoginJFrame;
import cn.edu.gxufe.ui.RegisterJFrame;

public class App {
    public static void main(String[] args) {
        // 表示程序启动的入口

        // 如果我们想要开启一个界面,就创建对应界面的对象即可!
        new GameJFrame();       // 游戏主界面
//         new LoginJFrame();      // 登录界面
//         new RegisterJFrame();   // 注册界面
    }
}

(3)测试结果

  • 这是我自己
    16-JavaSE基础巩固项目:拼图小游戏_第16张图片



五、打乱图片

1、需求

  • 打乱图片顺序

    16-JavaSE基础巩固项目:拼图小游戏_第17张图片


2、分析

  • 1、可以用0~15(包含15)来代表每张小的图片

    //定义一个一维数组,将0~15的数据先存储起来
    int arr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
    

    16-JavaSE基础巩固项目:拼图小游戏_第18张图片



  • 2、然后将这些数据随机打乱顺序

    // 打乱后
    int arr = {10, 5, 14, 15, 6, 12, 3, 11, 0, 8, 4, 13, 1, 9, 7, 2};
    


  • 3、将这些随机打乱后的数据按照每4个作为一个一维数组存储到二维数组中

    // 二维数组
    int[][] data = { {10, 5, 14, 15}, {6, 12, 3, 11}, {0, 8, 4, 13}, {1, 9, 7, 2} };
    

    16-JavaSE基础巩固项目:拼图小游戏_第19张图片


    16-JavaSE基础巩固项目:拼图小游戏_第20张图片



  • 4、最终按照二维数组中的每个一维数组的数据来初始化每张小图片到游戏主界面中


3、实现

(1)游戏主界面类

package cn.edu.gxufe.ui;

import javax.swing.*;

/**
 * 游戏主界面类:
 * 继承父类:JFrame
 */
public class GameJFrame extends JFrame{
    // 表示游戏相关的逻辑代码都写在这!
    
    /*
        创建二维数组:
            目的:管理数据
            加载图片的时候,会根据二位数组中的数据进行加载
     */
    int[][] data = new int[4][4];

    // 定义x、y变量,用于记录空白方块所在的XY的坐标位置
    int x = 0;
    int y = 0;

    // 创建一个成员的随机数对象,用于生成一个随机数
    Random rd = new Random();

    // 定义path变量,用于记录图片的相对路径,方便以后做更换图片、重新游戏等功能的实现
    String path = "puzzle_game\puzzleimages\girl\girl1\";

    // 定义一个正确顺序数据的二维数组
    int[][] win = {
            {1, 2, 3, 4},
            {5, 6, 7, 8},
            {9, 10, 11, 12},
            {13, 14, 15, 0}
    };


    // 定义一个计数器变量,用于记录玩家拼图移动的步数
    int step = 0;


    // c.创建功能菜单下的三个栏目:重新游戏、重新登录、关闭游戏
    // 更换图片栏目放到后面再写,因为比较复杂
    JMenuItem replayGameItem = new JMenuItem("重新游戏");
    JMenuItem replayLoginItem = new JMenuItem("重新登录");
    JMenuItem closeGameItem = new JMenuItem("关闭游戏");

    // d.创建关于我们菜单下的一个栏目:公众号
    JMenuItem accountItem = new JMenuItem("公众号");

    // 创建更换图片菜单下的两个栏目:美女、动物
    JMenuItem belleItem = new JMenuItem("美女");
    JMenuItem animalItem = new JMenuItem("动物");

    /*
        提供无参数的构造器
        需求:初始化一个宽603像素,高680像素的游戏主界面
     */
    public GameJFrame() {
        // 注:如果当前类没有以下这些方法,就会自动调用父类JFrame的
        // 初始化游戏主界面
        initJFrame();
        
        // 初始化游戏主界面里的菜单栏
        initJMenu();
        
        // 初始化数据:打乱
        initData();
        
        // 初始化图片到游戏主界面中
        initImages(path);

        // 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面
        // 由于界面默认是隐藏起来的,因此需要调用当前GameJFrame类的setVisible方法,设置显示界面
        this.setVisible(true);
    }
    
    // 初始化数据:打乱
    private void initData() {
        // 1、创建一个一维数组,用于存储一些数据
        int[] arr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};

        // 2、开始打乱
        // 遍历数组,依次获取到数组中的每个数据
        for (int i = 0; i < arr.length; i++) {
            // 循环每执行一次,就生成一个随机索引
            int rdIndex = rd.nextInt(arr.length);
            // 每遍历到一个数据,就用临时变量存储一下
            int temp = arr[i];
            // 开始交换
            // 将随机索引位置的数据 放到 当前遍历到的数据的位置
            arr[i] = arr[rdIndex];
            // 将当前遍历到的数据 放到 随机索引位置
            arr[rdIndex] = temp;
        }

        /*
            解析:
                比如打乱后的一维数组的数据:[0, 14, 8, 3, 13, 15, 6, 4, 10, 12, 7, 1, 9, 5, 11, 2]
                                  索引: 0  1   2  3  4   5   6  7  8   9   10 11 12 13 14  15
                循环第一次执行:
                    i=0,i<数组长度(16),为true,将0索引的数据:0 添加到二维数组的 i/4=0索引的一维数组的 i%4=0索引,i++
                循环第二次执行:
                    i=1,i<数组长度(16),为true,将1索引的数据:14 添加到二维数组的 i/4=0索引的一维数组的 i%4=1索引,i++
                循环第三次执行:
                    i=2,i<数组长度(16),为true,将2索引的数据:8 添加到二维数组的 i/4=0索引的一维数组的 i%4=2索引,i++
                后面的都是以此类推了!直到i<16,为false,循环结束!
         */
        // 3、遍历arr一维数组,依次得到打乱后的每个数据
        for (int i = 0; i < arr.length; i++) {
            // 判断当前数据是否为0
            if (arr[i] == 0) {
                // 是,则记录空白方块XY的坐标位置
                x = i / 4;
                y = i % 4;
            }
            // 否,则依次将打乱后的数据添加到二维数组中
            data[i / 4][i % 4] = arr[i];
        }
    }
    
    // 初始化游戏主界面
    private void initJFrame() {
        // 设置界面宽高
        // 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面
        this.setSize(603, 680);
        // 设置界面标题
        this.setTitle("奥利gei拼图单机版 v1.0");
        // 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的
        this.setAlwaysOnTop(true);
        // 设置界面打开时自动居中
        this.setLocationRelativeTo(null);
        // 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行
        // this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能
        this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        // 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件
        // 因为只有这样,添加图片的时候才不会一直处于居中位置
        this.setLayout(null);
    }
    
    // 初始化游戏主界面里的菜单栏
    private void initJMenu() {
        // a.创建一个菜单栏对象
        JMenuBar jMenuBar = new JMenuBar();

        // b.创建两个菜单栏下的菜单:功能、关于我们
        JMenu functionJMenu = new JMenu("功能");
        JMenu aboutUSJMenu = new JMenu("关于我们");

        // 创建菜单栏下的菜单:更换图片
        JMenu changeJMenu = new JMenu("更换图片");
        // 将更换图片的菜单嵌套进功能菜单中
        functionJMenu.add(changeJMenu);

        // e.将重新游戏、重新登录、关闭游戏这些栏目放到功能菜单下
        functionJMenu.add(replayGameItem);
        functionJMenu.add(replayLoginItem);
        functionJMenu.add(closeGameItem);

        // 将美女、动物这两个栏目放到更换图片菜单下
        changeJMenu.add(belleItem);
        changeJMenu.add(animalItem);

        // f.将公众号这个栏目放到关于我们菜单下
        aboutUSJMenu.add(accountItem);

        // g.将功能、关于我们这两个菜单放到菜单栏下
        jMenuBar.add(functionJMenu);
        jMenuBar.add(aboutUSJMenu);

        // h.最后将菜单栏放到设置到游戏界面中
        this.setJMenuBar(jMenuBar);
    }
    
    /*
        初始化图片到游戏主界面中
        细节:先加载的图片在上方,后加载的图片会在下方
     */
    private void initImages(String path) {
        // 先清空原本出现的所有图片
        this.getContentPane().removeAll();

        // 判断victory方法的返回结果是否为true
        if (victory()) {
            // 是,则说明玩家胜利了!显示胜利图标!
            // 根据指定图片的相对路径创建图片对象,并添加到管理容器中
            JLabel winJLabel = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\win.jpg"));
            // 设置胜利图片的坐标位置以及宽高
            winJLabel.setBounds(193, 300, 194, 75);
            // 将管理容器添加到游戏主界面中
            this.getContentPane().add(winJLabel);
        }

        // 统计步数
        // 创建一个管理容器对象,用于管理文字(步数: 0)
        JLabel stepJLabel = new JLabel("步数:" + step);
        // 设置管理容器的坐标位置
        stepJLabel.setBounds(40, 20, 400, 50);
        // 将容器添加到游戏主界面中
        this.getContentPane().add(stepJLabel);

        // 再加载图片
        /*
           外循环和内循环执行流程解析:
             外循环第一次执行:
                当 i = 0 时:i<4,为true,表示添加第一行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=1,表示添加第一行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=2,表示添加第一行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=3,表示添加第一行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=4,表示添加第一行的第四张图片
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第二次执行:
                当 i = 1 时:i<4,为true,表示添加第二行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=5,表示添加第二行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=6,表示添加第二行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=7,表示添加第二行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=8,表示添加第二行的第四张图片
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第三次执行:
                当 i = 2 时:i<4,为true,表示添加第三行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=9,表示添加第三行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=10,表示添加第三行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=11,表示添加第三行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=12,表示添加第三行的第四张图片
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第四次执行:
                当 i = 3 时:i<4,为true,表示添加第四行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=13,表示添加第四行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=14,表示添加第四行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=15,表示添加第四行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=16,表示添加第四行的第四张图片(由于文件中没有16这个命名的图片,因此会添加一个空白)
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第五次执行:
                当 i = 4 时:i<4,为false,外循环结束!
        */
        // 外循环:控制添加4行图片
        for (int i = 0; i < data.length; i++) { // Y轴:纵向
            // 内循环:控制每行添加4张图片
            for (int j = 0; j < data[i].length; j++) { // X轴:横向
                /*
                    解析:
                        比如二维数组中的数据:[{3, 9, 7, 5} {8, 10, 14, 11} {4, 2, 12, 13} {0, 15, 1, 6} ]
                   二维数组中的一维数组的索引:  0  1  2  3   0   1  2   3    0  1   2   3   0   1  2  3
                             二维数组的索引:       0             1              2              3
                      (1) 外循环执行第一次:
                        i=0, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (2) 外循环执行第二次:
                        i=1, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (3) 外循环执行第三次:
                        i=2, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (4) 外循环执行第四次:
                        i=3, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (5) 外循环执行第五次:
                        i=0, i<二维数组长度(4), 为false, 外循环结束!!
                 */
                // 接收二维数组i索引的一维数组j索引的数据
                int number = data[i][j];
                // 1、根据指定的文件相对路径创建一个ImageIcon图片对象
                ImageIcon imageIcon = new ImageIcon(path + number + ".jpg");

                // JLabel:管理容器,用于管理图片、文字
                // 2、创建一个管理容器JLabel对象,用于管理图片对象ImageIcon,
                // 并将图片对象imageIcon放到管理容器jLabel中。
                JLabel jLabel = new JLabel(imageIcon);

                // 3、指定图片位置:XY轴
                jLabel.setBounds(105 * j + 80, 105 * i + 130, 105, 105);

                // 4、每添加完一张小图片,给这张小图片添加边框
                /*
                    0: 表示让图片凸起来
                    1: 表示让图片凹下去
                 */
                jLabel.setBorder(new BevelBorder(1));

                // 5、将管理容器JLabel对象放到游戏主界面中
                // getContentPane:获取隐藏容器
                this.getContentPane().add(jLabel);
            }
        }

        // 在所有小图片都添加到界面之后,开始添加背景图片
        // 根据指定的相对路径创建背景图片对象,并添加到管理容器中
        JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));
        // 设置背景图片的坐标位置以及宽高
        background.setBounds(36, 36, 508, 560);
        // 将背景图片管理容器添加到界面中
        this.getContentPane().add(background);

        // 最后刷新一下游戏界面
        this.getContentPane().repaint();
    }
}

(2)程序启动入口类

import cn.edu.gxufe.ui.GameJFrame;
import cn.edu.gxufe.ui.LoginJFrame;
import cn.edu.gxufe.ui.RegisterJFrame;

public class App {
    public static void main(String[] args) {
        // 表示程序启动的入口

        // 如果我们想要开启一个界面,就创建对应界面的对象即可!
        new GameJFrame();       // 游戏主界面
//         new LoginJFrame();      // 登录界面
//         new RegisterJFrame();   // 注册界面
    }
}

(3)测试结果

  • 多执行几次,看看是不是随机的

    16-JavaSE基础巩固项目:拼图小游戏_第21张图片


    16-JavaSE基础巩固项目:拼图小游戏_第22张图片




六、事件

1、什么是事件?

  • 事件是可以被组件识别的操作。
  • 当你对组件做了某件事情之后,就会执行对应的程序代码。
  • 举个例子
    • 比如一个猥琐男对另一个男的施行了猥亵,就触发了警察抓他的事件!
    • 比如小时候我们拿弹弓打碎了玻璃,就触发了妈妈打我们屁股的事件!
    • 比如当用户点击登录按钮之后,就触发了校验用户名和密码的事件!
    • 比如…

2、事件三大个核心要素

  • 1、事件源:按钮、图片、窗体…
  • 2、事件:某些操作
    • 如:鼠标单击,鼠标划入…
  • 3、绑定监听:当事件源上发生了某个事件,就会执行相应的程序代码。
    • 如:当登录按钮被点击之后,就会执行校验用户名和密码的程序代码。

3、介绍常见监听

  • KeyListener:键盘监听

    • 比如电脑的快捷键就是这么干的!
    • 当你按下键盘的Ctrl+C的快捷键时,就会执行复制的程序代码!
  • MouseListener:鼠标监听

    • 比如lol英雄联盟,当你用鼠标点击界面时,会出现很多问号!并且还会出现叮叮叮的声音!!

      16-JavaSE基础巩固项目:拼图小游戏_第23张图片

  • ActionListener:动作监听

    • 可以说是键盘监听、鼠标监听的精简版!
    • 动作监听,只能监听鼠标的左键点击操作、键盘的空格操作!


(1)ActionListener:动作监听

  • 只能监听到鼠标的左键点击操作、键盘的空格操作!
实现方式一:创建实现ActionListener实现类
package cn.edu.gxufe.test;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

// 创建动作监听ActionListener接口的实现类,用于实现接口里的单击按钮方法
public class MyActionListenerImpl implements ActionListener{
    // 实现ActionListener接口中的单击按钮方法
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("我是ActionListener动作监听接口的单击按钮实现方法,我被你点击了!");
    }
}
package cn.edu.gxufe.test;

import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

// 测试类
public class Test2 extends JFrame {
    public static void main(String[] args) {
        // 初始化界面
        JFrame jFrame = new JFrame();
        // 设置界面宽高
        // 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面
        jFrame.setSize(603, 680);
        // 设置界面标题
        jFrame.setTitle("奥利gei拼图单机版 v1.0");
        // 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的
        jFrame.setAlwaysOnTop(true);
        // 设置界面打开时自动居中
        jFrame.setLocationRelativeTo(null);
        // 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行
        // this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能
        jFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        // 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件
        // 因为只有这样,添加图片的时候才不会一直处于居中位置
        jFrame.setLayout(null);

        // 重点:
        // 创建一个按钮对象
        JButton jbt1 = new JButton("点我呗");
        // 设置位置和宽高
        jbt1.setBounds(0, 0, 100, 50);
        // 给按钮添加动作监听
        // jbt1: 组件对象,表示你要给哪个组件添加事件
        // addActionListener: 表示我要给组件添加哪个事件监听(动作监听包含鼠标左键点击,空格)
        // 参数: 表示事件被触发之后要执行的代码
        // 方式一:实现类实现接口的单击按钮方法
		jbt1.addActionListener(new MyActionListenerImpl());


        // 把按钮添加到界面当中
        jFrame.getContentPane().add(jbt1);


        // 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面
        // 由于界面默认是隐藏起来的,因此需要调用当前GameJFrame类的setVisible方法,设置显示界面
        jFrame.setVisible(true);
    }
}

实现方式一的测试结果

在这里插入图片描述



实现方式二:匿名内部类
package cn.edu.gxufe.test;

import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

// 测试类
public class Test2 extends JFrame {
    public static void main(String[] args) {
        // 初始化界面
        JFrame jFrame = new JFrame();
        // 设置界面宽高
        // 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面
        jFrame.setSize(603, 680);
        // 设置界面标题
        jFrame.setTitle("奥利gei拼图单机版 v1.0");
        // 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的
        jFrame.setAlwaysOnTop(true);
        // 设置界面打开时自动居中
        jFrame.setLocationRelativeTo(null);
        // 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行
        // this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能
        jFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        // 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件
        // 因为只有这样,添加图片的时候才不会一直处于居中位置
        jFrame.setLayout(null);

        // 重点:
        // 创建一个按钮对象
        JButton jbt1 = new JButton("点我呗");
        // 设置位置和宽高
        jbt1.setBounds(0, 0, 100, 50);
        // 给按钮添加动作监听
        // jbt1: 组件对象,表示你要给哪个组件添加事件
        // addActionListener: 表示我要给组件添加哪个事件监听(动作监听包含鼠标左键点击,空格)
        // 参数: 表示事件被触发之后要执行的代码
        // 方式一:实现类实现接口的单击按钮方法
//        jbt1.addActionListener(new MyActionListenerImpl());

        // 方式二:匿名内部类直接实现接口的单击按钮方法
        jbt1.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println("达咩!不要点我喔~ 会疼喔!!");
            }
        });


        // 把按钮添加到界面当中
        jFrame.getContentPane().add(jbt1);


        // 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面
        // 由于界面默认是隐藏起来的,因此需要调用当前GameJFrame类的setVisible方法,设置显示界面
        jFrame.setVisible(true);
    }
}

实现方式二的测试结果

16-JavaSE基础巩固项目:拼图小游戏_第24张图片



开发中的写法
  • 直接在本界面类中实现ActionListener接口的单击按钮方法
(1)界面类
package cn.edu.gxufe.test;

import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Random;

public class MyFrame extends JFrame implements ActionListener {
    // 创建按钮对象1
    JButton jbt1 = new JButton("点我啊");
    // 创建按钮对象2
    JButton jbt2 = new JButton("再点我啊~");

    public MyFrame() {
        // 初始化界面
        initJFrame();


        // 设置按钮的位置和宽高
        jbt1.setBounds(0, 0, 100, 50);
        // 给按钮添加动作监听事件
        // jbt1: 组件对象,表示你要给哪个组件添加事件
        // addActionListener: 表示我要给组件添加哪个事件监听(动作监听包含鼠标左键点击,空格)
        // 参数: 表示事件被触发之后要执行的代码
        jbt1.addActionListener(this);

        // 设置按钮的位置和宽高
        jbt2.setBounds(100, 0, 100, 50);
        // 给按钮添加动作监听事件
        jbt2.addActionListener(this);

        // 添加两个按钮到当前界面中
        this.getContentPane().add(jbt1);
        this.getContentPane().add(jbt2);

        // 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面
        // 由于界面默认是隐藏起来的,因此需要调用当前GameJFrame类的setVisible方法,设置显示界面
        this.setVisible(true);
    }

    private void initJFrame() {
        // 设置界面宽高
        // 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面
        this.setSize(603, 680);
        // 设置界面标题
        this.setTitle("奥利gei拼图单机版 v1.0");
        // 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的
        this.setAlwaysOnTop(true);
        // 设置界面打开时自动居中
        this.setLocationRelativeTo(null);
        // 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行
        // this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能
        this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        // 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件
        // 因为只有这样,添加图片的时候才不会一直处于居中位置
        this.setLayout(null);
    }

    /*
        直接在本类实现ActionListener接口的单击按钮
     */
    @Override
    public void actionPerformed(ActionEvent e) {
        // 获取当前的按钮对象
        Object source = e.getSource();

        // 判断当前的按钮对象是否为jbt1
        if (source == jbt1) {
            // 是,则将jbt1按钮的宽高都设置为150像素
            jbt1.setSize(150, 150);
        }else if (source == jbt2) { // 判断当前的按钮对象是否为jbt2
            // 是,则当按下jbt2按钮后,该按钮的坐标随机到500内的范围
            Random rd = new Random();
            jbt2.setLocation(rd.nextInt(500), rd.nextInt(500));
        }
    }
}

(2)测试类
package cn.edu.gxufe.test;

public class Test3 {
    public static void main(String[] args) {
        // 构造界面
        new MyFrame();
    }
}

(3)测试结果

16-JavaSE基础巩固项目:拼图小游戏_第25张图片




(2)MouseListener:鼠标监听

  • 鼠标监听

    • 单击事件:
      • 按下动作、松开动作
    • 鼠标划入动作、划出动作
  • 思考

    1. 如果以后我想监听一个按钮的单击事件,有几种方式?
      • 动作监听:ActionListener
      • 鼠标监听中的单击事件
      • 鼠标监听中的松开事件


方法摘要
返回类型 方法名称
void mouseClicked(MouseEvent e) 在组件上单击(按下并释放)鼠标按钮时调用
void mouseEntered(MouseEvent e) 当鼠标进入组件时调用
void mouseExited(MouseEvent e) 当鼠标退出组件时调用
void mousePressed(MouseEvent e) 在组件上按下鼠标按钮时调用
void mouseReleased(MouseEvent e) 在组件上释放鼠标按钮时调用


实现
(1)界面类
package cn.edu.gxufe.test;

import javax.swing.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;

// 自定义界面类:需要继承父类(JFrame)
public class MyJFrame2 extends JFrame implements MouseListener {
    // 创建按钮对象
    JButton jbt = new JButton("点我啊");

    public MyJFrame2() {
        // 初始化界面
        initJFrame();

        // 设置按钮在界面中的坐标位置
        jbt.setBounds(0, 0, 100, 50);
        // 给按钮添加鼠标监听事件
        jbt.addMouseListener(this);
        // 将按钮添加到界面中
        this.getContentPane().add(jbt);

        // 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面
        // 由于界面默认是隐藏起来的,因此需要调用当前界面类的setVisible方法,设置显示界面
        this.setVisible(true);
    }

    // 初始化界面
    private void initJFrame() {
        // 设置界面宽高
        // 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面
        this.setSize(603, 680);
        // 设置界面标题
        this.setTitle("奥利gei拼图单机版 v1.0");
        // 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的
        this.setAlwaysOnTop(true);
        // 设置界面打开时自动居中
        this.setLocationRelativeTo(null);
        // 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行
        // this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能
        this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        // 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件
        // 因为只有这样,添加图片的时候才不会一直处于居中位置
        this.setLayout(null);
    }

    // 实现鼠标监听接口MouseListener的鼠标单击事件方法
    @Override
    public void mouseClicked(MouseEvent e) {
        System.out.println("鼠标单击了一下");
    }

    // 实现鼠标监听接口MouseListener的按下鼠标事件方法
    @Override
    public void mousePressed(MouseEvent e) {
        System.out.println("被鼠标按了一下");
    }

    // 实现鼠标监听接口MouseListener的松开鼠标事件方法
    @Override
    public void mouseReleased(MouseEvent e) {
        System.out.println("鼠标松开了");
    }

    // 实现鼠标监听接口MouseListener的鼠标划入事件方法
    @Override
    public void mouseEntered(MouseEvent e) {
        System.out.println("鼠标划进来了");
    }

    // 实现鼠标监听接口MouseListener的鼠标划出事件方法
    @Override
    public void mouseExited(MouseEvent e) {
        System.out.println("鼠标划走了");
    }
}

(2)测试类
package cn.edu.gxufe.test;

public class Test4 {
    public static void main(String[] args) {
        // 构造界面
        new MyJFrame2();
    }
}

(3)测试结果

16-JavaSE基础巩固项目:拼图小游戏_第26张图片




(3)KeyListener:键盘监听

方法摘要
返回类型 方法名称 描述
void keyPressed(KeyEvent e) 按下键时调用
void keyReleased(KeyEvent e) 当键已被释放时调用
void keyTyped(KeyEvent e) 键入键时调用


实现
(1)界面类
package cn.edu.gxufe.test;

import javax.swing.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

// 自定义界面类:继承父类JFrame
public class MyJFrame3 extends JFrame implements KeyListener{

    // 提供无参数的构造器
    public MyJFrame3(){
        // 初始化界面
        initJFrame();


        // 给整个界面添加键盘监听事件
        // 调用者this:本类对象,当前的界面对象,表示我要给整个界面添加监听
        // addKeyListener:表示要给本界面添加键盘监听
        // 参数this:当事件被触发之后,会执行本类中的对应代码
        this.addKeyListener(this);


        // 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面
        // 由于界面默认是隐藏起来的,因此需要调用当前界面类的setVisible方法,设置显示界面
        this.setVisible(true);
    }


    // 初始化界面
    private void initJFrame() {
        // 设置界面宽高
        // 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面
        this.setSize(603, 680);
        // 设置界面标题
        this.setTitle("奥利gei拼图单机版 v1.0");
        // 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的
        this.setAlwaysOnTop(true);
        // 设置界面打开时自动居中
        this.setLocationRelativeTo(null);
        // 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行
        // this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能
        this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        // 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件
        // 因为只有这样,添加图片的时候才不会一直处于居中位置
        this.setLayout(null);
    }

    // 这个可以不用管
    @Override
    public void keyTyped(KeyEvent e) {

    }


    // 实现键盘监听接口KeyListener的按下键盘方法
    /*
        细节1:
            如果我们按下键盘的一个键没有松开,那么会重复的去调用keyPressed方法
        细节2:
            键盘里面那么多按键,如果进行区分?
            答:每一个按键都有一个编号与之对应的
     */
    @Override
    public void keyPressed(KeyEvent e) {
        // 获取键盘每个按键的编号
        int keyCode = e.getKeyCode();
        // 判断当前按下的按键的编号是否为65
        if (keyCode == 65) {
            // 是,则说明按下的按键是A
            System.out.println("按下按键A");
        }else if (keyCode == 66){   // 否,则判断当前按下的按键的编号是否为66
            // 是,则说明按下的按键是B
            System.out.println("按下按键B");
        }else {
            // 否,则说明按下的是其他按键
            System.out.println("按下的是其他按键");
        }
    }


    // 实现键盘监听接口KeyListener的释放(松开)键盘方法
    @Override
    public void keyReleased(KeyEvent e) {
        System.out.println("按键已松开");
    }
}

(2)测试类
package cn.edu.gxufe.test;

public class Test5 {
    public static void main(String[] args) {
        // 构造界面
        new MyJFrame3();
    }
}

(3)测试结果

16-JavaSE基础巩固项目:拼图小游戏_第27张图片




七、美化界面

1、需求

  • 按照以下美化后的要求,优化游戏界面

  • 美化之前

    16-JavaSE基础巩固项目:拼图小游戏_第28张图片



  • 美化之后

    16-JavaSE基础巩固项目:拼图小游戏_第29张图片



2、分析

  • 1、将15张小图片的坐标位置调整到中央偏下方
  • 2、在添加完15张小图片的代码后面,添加背景图片:
    • 细节:先加载的图片在上方,后加载的图片会在下方。
  • 3、在每添加完1张小图片的代码后面,给每张小图片添加斜角边框,让图片凹下去。
  • 4、优化文件路径:
    • 从盘符开始的:绝对路径
    • 非盘符开始的:相对路径
    • 使用相对路径优化!


3、实现

(1)游戏主界面类

package cn.edu.gxufe.ui;

import javax.swing.*;

/**
 * 游戏主界面类:
 * 继承父类:JFrame
 */
public class GameJFrame extends JFrame{
    // 表示游戏相关的逻辑代码都写在这!
    
    /*
        创建二维数组:
            目的:管理数据
            加载图片的时候,会根据二位数组中的数据进行加载
     */
    int[][] data = new int[4][4];

    // 定义x、y变量,用于记录空白方块所在的XY的坐标位置
    int x = 0;
    int y = 0;

    // 创建一个成员的随机数对象,用于生成一个随机数
    Random rd = new Random();

    // 定义path变量,用于记录图片的相对路径,方便以后做更换图片、重新游戏等功能的实现
    String path = "puzzle_game\puzzleimages\girl\girl1\";

    // 定义一个正确顺序数据的二维数组
    int[][] win = {
            {1, 2, 3, 4},
            {5, 6, 7, 8},
            {9, 10, 11, 12},
            {13, 14, 15, 0}
    };


    // 定义一个计数器变量,用于记录玩家拼图移动的步数
    int step = 0;


    // c.创建功能菜单下的三个栏目:重新游戏、重新登录、关闭游戏
    // 更换图片栏目放到后面再写,因为比较复杂
    JMenuItem replayGameItem = new JMenuItem("重新游戏");
    JMenuItem replayLoginItem = new JMenuItem("重新登录");
    JMenuItem closeGameItem = new JMenuItem("关闭游戏");

    // d.创建关于我们菜单下的一个栏目:公众号
    JMenuItem accountItem = new JMenuItem("公众号");

    // 创建更换图片菜单下的两个栏目:美女、动物
    JMenuItem belleItem = new JMenuItem("美女");
    JMenuItem animalItem = new JMenuItem("动物");

    /*
        提供无参数的构造器
        需求:初始化一个宽603像素,高680像素的游戏主界面
     */
    public GameJFrame() {
        // 注:如果当前类没有以下这些方法,就会自动调用父类JFrame的
        // 初始化游戏主界面
        initJFrame();
        
        // 初始化游戏主界面里的菜单栏
        initJMenu();
        
        // 初始化数据:打乱
        initData();
        
        // 初始化图片到游戏主界面中
        initImages(path);

        // 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面
        // 由于界面默认是隐藏起来的,因此需要调用当前GameJFrame类的setVisible方法,设置显示界面
        this.setVisible(true);
    }
    
    // 初始化数据:打乱
    private void initData() {
        // 1、创建一个一维数组,用于存储一些数据
        int[] arr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};

        // 2、开始打乱
        // 遍历数组,依次获取到数组中的每个数据
        for (int i = 0; i < arr.length; i++) {
            // 循环每执行一次,就生成一个随机索引
            int rdIndex = rd.nextInt(arr.length);
            // 每遍历到一个数据,就用临时变量存储一下
            int temp = arr[i];
            // 开始交换
            // 将随机索引位置的数据 放到 当前遍历到的数据的位置
            arr[i] = arr[rdIndex];
            // 将当前遍历到的数据 放到 随机索引位置
            arr[rdIndex] = temp;
        }

        /*
            解析:
                比如打乱后的一维数组的数据:[0, 14, 8, 3, 13, 15, 6, 4, 10, 12, 7, 1, 9, 5, 11, 2]
                                  索引: 0  1   2  3  4   5   6  7  8   9   10 11 12 13 14  15
                循环第一次执行:
                    i=0,i<数组长度(16),为true,将0索引的数据:0 添加到二维数组的 i/4=0索引的一维数组的 i%4=0索引,i++
                循环第二次执行:
                    i=1,i<数组长度(16),为true,将1索引的数据:14 添加到二维数组的 i/4=0索引的一维数组的 i%4=1索引,i++
                循环第三次执行:
                    i=2,i<数组长度(16),为true,将2索引的数据:8 添加到二维数组的 i/4=0索引的一维数组的 i%4=2索引,i++
                后面的都是以此类推了!直到i<16,为false,循环结束!
         */
        // 3、遍历arr一维数组,依次得到打乱后的每个数据
        for (int i = 0; i < arr.length; i++) {
            // 判断当前数据是否为0
            if (arr[i] == 0) {
                // 是,则记录空白方块XY的坐标位置
                x = i / 4;
                y = i % 4;
            }
            // 否,则依次将打乱后的数据添加到二维数组中
            data[i / 4][i % 4] = arr[i];
        }
    }
    
    // 初始化游戏主界面
    private void initJFrame() {
        // 设置界面宽高
        // 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面
        this.setSize(603, 680);
        // 设置界面标题
        this.setTitle("奥利gei拼图单机版 v1.0");
        // 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的
        this.setAlwaysOnTop(true);
        // 设置界面打开时自动居中
        this.setLocationRelativeTo(null);
        // 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行
        // this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能
        this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        // 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件
        // 因为只有这样,添加图片的时候才不会一直处于居中位置
        this.setLayout(null);
    }
    
    // 初始化游戏主界面里的菜单栏
    private void initJMenu() {
        // a.创建一个菜单栏对象
        JMenuBar jMenuBar = new JMenuBar();

        // b.创建两个菜单栏下的菜单:功能、关于我们
        JMenu functionJMenu = new JMenu("功能");
        JMenu aboutUSJMenu = new JMenu("关于我们");

        // 创建菜单栏下的菜单:更换图片
        JMenu changeJMenu = new JMenu("更换图片");
        // 将更换图片的菜单嵌套进功能菜单中
        functionJMenu.add(changeJMenu);

        // e.将重新游戏、重新登录、关闭游戏这些栏目放到功能菜单下
        functionJMenu.add(replayGameItem);
        functionJMenu.add(replayLoginItem);
        functionJMenu.add(closeGameItem);

        // 将美女、动物这两个栏目放到更换图片菜单下
        changeJMenu.add(belleItem);
        changeJMenu.add(animalItem);

        // f.将公众号这个栏目放到关于我们菜单下
        aboutUSJMenu.add(accountItem);

        // g.将功能、关于我们这两个菜单放到菜单栏下
        jMenuBar.add(functionJMenu);
        jMenuBar.add(aboutUSJMenu);

        // h.最后将菜单栏放到设置到游戏界面中
        this.setJMenuBar(jMenuBar);
    }
    
    /*
        初始化图片到游戏主界面中
        细节:先加载的图片在上方,后加载的图片会在下方
     */
    private void initImages(String path) {
        // 先清空原本出现的所有图片
        this.getContentPane().removeAll();

        // 判断victory方法的返回结果是否为true
        if (victory()) {
            // 是,则说明玩家胜利了!显示胜利图标!
            // 根据指定图片的相对路径创建图片对象,并添加到管理容器中
            JLabel winJLabel = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\win.jpg"));
            // 设置胜利图片的坐标位置以及宽高
            winJLabel.setBounds(193, 300, 194, 75);
            // 将管理容器添加到游戏主界面中
            this.getContentPane().add(winJLabel);
        }

        // 统计步数
        // 创建一个管理容器对象,用于管理文字(步数: 0)
        JLabel stepJLabel = new JLabel("步数:" + step);
        // 设置管理容器的坐标位置
        stepJLabel.setBounds(40, 20, 400, 50);
        // 将容器添加到游戏主界面中
        this.getContentPane().add(stepJLabel);

        // 再加载图片
        /*
           外循环和内循环执行流程解析:
             外循环第一次执行:
                当 i = 0 时:i<4,为true,表示添加第一行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=1,表示添加第一行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=2,表示添加第一行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=3,表示添加第一行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=4,表示添加第一行的第四张图片
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第二次执行:
                当 i = 1 时:i<4,为true,表示添加第二行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=5,表示添加第二行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=6,表示添加第二行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=7,表示添加第二行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=8,表示添加第二行的第四张图片
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第三次执行:
                当 i = 2 时:i<4,为true,表示添加第三行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=9,表示添加第三行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=10,表示添加第三行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=11,表示添加第三行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=12,表示添加第三行的第四张图片
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第四次执行:
                当 i = 3 时:i<4,为true,表示添加第四行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=13,表示添加第四行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=14,表示添加第四行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=15,表示添加第四行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=16,表示添加第四行的第四张图片(由于文件中没有16这个命名的图片,因此会添加一个空白)
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第五次执行:
                当 i = 4 时:i<4,为false,外循环结束!
        */
        // 外循环:控制添加4行图片
        for (int i = 0; i < data.length; i++) { // Y轴:纵向
            // 内循环:控制每行添加4张图片
            for (int j = 0; j < data[i].length; j++) { // X轴:横向
                /*
                    解析:
                        比如二维数组中的数据:[{3, 9, 7, 5} {8, 10, 14, 11} {4, 2, 12, 13} {0, 15, 1, 6} ]
                   二维数组中的一维数组的索引:  0  1  2  3   0   1  2   3    0  1   2   3   0   1  2  3
                             二维数组的索引:       0             1              2              3
                      (1) 外循环执行第一次:
                        i=0, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (2) 外循环执行第二次:
                        i=1, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (3) 外循环执行第三次:
                        i=2, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (4) 外循环执行第四次:
                        i=3, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (5) 外循环执行第五次:
                        i=0, i<二维数组长度(4), 为false, 外循环结束!!
                 */
                // 接收二维数组i索引的一维数组j索引的数据
                int number = data[i][j];
                // 1、根据指定的文件相对路径创建一个ImageIcon图片对象
                ImageIcon imageIcon = new ImageIcon(path + number + ".jpg");

                // JLabel:管理容器,用于管理图片、文字
                // 2、创建一个管理容器JLabel对象,用于管理图片对象ImageIcon,
                // 并将图片对象imageIcon放到管理容器jLabel中。
                JLabel jLabel = new JLabel(imageIcon);

                // 3、指定图片位置:XY轴
                jLabel.setBounds(105 * j + 80, 105 * i + 130, 105, 105);

                // 4、每添加完一张小图片,给这张小图片添加边框
                /*
                    0: 表示让图片凸起来
                    1: 表示让图片凹下去
                 */
                jLabel.setBorder(new BevelBorder(1));

                // 5、将管理容器JLabel对象放到游戏主界面中
                // getContentPane:获取隐藏容器
                this.getContentPane().add(jLabel);
            }
        }

        // 在所有小图片都添加到界面之后,开始添加背景图片
        // 根据指定的相对路径创建背景图片对象,并添加到管理容器中
        JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));
        // 设置背景图片的坐标位置以及宽高
        background.setBounds(36, 36, 508, 560);
        // 将背景图片管理容器添加到界面中
        this.getContentPane().add(background);

        // 最后刷新一下游戏界面
        this.getContentPane().repaint();
    }
}

(2)程序启动入口类

import cn.edu.gxufe.ui.GameJFrame;
import cn.edu.gxufe.ui.LoginJFrame;
import cn.edu.gxufe.ui.RegisterJFrame;

public class App {
    public static void main(String[] args) {
        // 表示程序启动的入口

        // 如果我们想要开启一个界面,就创建对应界面的对象即可!
        new GameJFrame();       // 游戏主界面
//         new LoginJFrame();      // 登录界面
//         new RegisterJFrame();   // 注册界面
    }
}

(3)测试结果

16-JavaSE基础巩固项目:拼图小游戏_第30张图片




八、上下左右移动图片

1、需求

  • 实现每张小图片可以上下左右移动


2、分析

  • 我们都知道,每张小图片,都有对应的数据存放在二维数组中

    16-JavaSE基础巩固项目:拼图小游戏_第31张图片

  • 1、我们需要先给整个界面添加键盘监听事件,监听键盘的上下左右键就可以了!

  • 2、需要定义两个变量:x、y,用于记录空白方块所在的索引位置,然后记录空白方块的所在索引

  • 3、需要获取每个按键的编号,不需要牢记,只需要在按下键盘按键的时候,输出一下编号就可以知道上下左右按键的编号了!

  • 4、要先清空原本已经出现的所有图片,然后加载完所有图片后,再刷新一下界面就好了!

  • 5、向上移动

    • 其实就是将空白方块下方的图片上移:
      • 也就是将空白方块下方的图片的索引3,1的数据 赋值到 空白方块的索引2,1中。
      • 然后再按照二维数组中的最新数据,来重新加载图片就可以了。
  • 6、向下移动

    • 其实就是将空白方块上方的图片下移:
      • 也就是将空白方块上方的图片的索引1,1的数据 赋值到 空白方块的索引2,1中。
      • 然后再按照二维数组中的最新数据,来重新加载图片就可以了。
  • 7、向左移动

    • 其实就是将空白方块右方的图片左移:
      • 也就是将空白方块右方的图片的索引2,2的数据 赋值到 空白方块的索引2,1中。
      • 然后再按照二维数组中的最新数据,来重新加载图片就可以了。
  • 8、向右移动

    • 其实就是将空白方块左方的图片右移:
      • 也就是将空白方块左方的图片的索引2,0的数据 赋值到 空白方块的索引2,1中。
      • 然后再按照二维数组中的最新数据,来重新加载图片就可以了。


3、实现

(1)游戏主界面类

package cn.edu.gxufe.ui;

import javax.swing.*;

/**
 * 游戏主界面类:
 * 继承父类:JFrame
 */
public class GameJFrame extends JFrame implements KeyListener{
    // 表示游戏相关的逻辑代码都写在这!
    
    /*
        创建二维数组:
            目的:管理数据
            加载图片的时候,会根据二位数组中的数据进行加载
     */
    int[][] data = new int[4][4];

    // 定义x、y变量,用于记录空白方块所在的XY的坐标位置
    int x = 0;
    int y = 0;

    // 创建一个成员的随机数对象,用于生成一个随机数
    Random rd = new Random();

    // 定义path变量,用于记录图片的相对路径,方便以后做更换图片、重新游戏等功能的实现
    String path = "puzzle_game\puzzleimages\girl\girl1\";

    // 定义一个正确顺序数据的二维数组
    int[][] win = {
            {1, 2, 3, 4},
            {5, 6, 7, 8},
            {9, 10, 11, 12},
            {13, 14, 15, 0}
    };


    // 定义一个计数器变量,用于记录玩家拼图移动的步数
    int step = 0;


    // c.创建功能菜单下的三个栏目:重新游戏、重新登录、关闭游戏
    // 更换图片栏目放到后面再写,因为比较复杂
    JMenuItem replayGameItem = new JMenuItem("重新游戏");
    JMenuItem replayLoginItem = new JMenuItem("重新登录");
    JMenuItem closeGameItem = new JMenuItem("关闭游戏");

    // d.创建关于我们菜单下的一个栏目:公众号
    JMenuItem accountItem = new JMenuItem("公众号");

    // 创建更换图片菜单下的两个栏目:美女、动物
    JMenuItem belleItem = new JMenuItem("美女");
    JMenuItem animalItem = new JMenuItem("动物");

    /*
        提供无参数的构造器
        需求:初始化一个宽603像素,高680像素的游戏主界面
     */
    public GameJFrame() {
        // 注:如果当前类没有以下这些方法,就会自动调用父类JFrame的
        // 初始化游戏主界面
        initJFrame();
        
        // 初始化游戏主界面里的菜单栏
        initJMenu();
        
        // 初始化数据:打乱
        initData();
        
        // 初始化图片到游戏主界面中
        initImages(path);

        // 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面
        // 由于界面默认是隐藏起来的,因此需要调用当前GameJFrame类的setVisible方法,设置显示界面
        this.setVisible(true);
    }
    
    // 初始化数据:打乱
    private void initData() {
        // 1、创建一个一维数组,用于存储一些数据
        int[] arr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};

        // 2、开始打乱
        // 遍历数组,依次获取到数组中的每个数据
        for (int i = 0; i < arr.length; i++) {
            // 循环每执行一次,就生成一个随机索引
            int rdIndex = rd.nextInt(arr.length);
            // 每遍历到一个数据,就用临时变量存储一下
            int temp = arr[i];
            // 开始交换
            // 将随机索引位置的数据 放到 当前遍历到的数据的位置
            arr[i] = arr[rdIndex];
            // 将当前遍历到的数据 放到 随机索引位置
            arr[rdIndex] = temp;
        }

        /*
            解析:
                比如打乱后的一维数组的数据:[0, 14, 8, 3, 13, 15, 6, 4, 10, 12, 7, 1, 9, 5, 11, 2]
                                  索引: 0  1   2  3  4   5   6  7  8   9   10 11 12 13 14  15
                循环第一次执行:
                    i=0,i<数组长度(16),为true,将0索引的数据:0 添加到二维数组的 i/4=0索引的一维数组的 i%4=0索引,i++
                循环第二次执行:
                    i=1,i<数组长度(16),为true,将1索引的数据:14 添加到二维数组的 i/4=0索引的一维数组的 i%4=1索引,i++
                循环第三次执行:
                    i=2,i<数组长度(16),为true,将2索引的数据:8 添加到二维数组的 i/4=0索引的一维数组的 i%4=2索引,i++
                后面的都是以此类推了!直到i<16,为false,循环结束!
         */
        // 3、遍历arr一维数组,依次得到打乱后的每个数据
        for (int i = 0; i < arr.length; i++) {
            // 判断当前数据是否为0
            if (arr[i] == 0) {
                // 是,则记录空白方块XY的坐标位置
                x = i / 4;
                y = i % 4;
            }
            // 否,则依次将打乱后的数据添加到二维数组中
            data[i / 4][i % 4] = arr[i];
        }
    }
    
    // 初始化游戏主界面
    private void initJFrame() {
        // 设置界面宽高
        // 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面
        this.setSize(603, 680);
        // 设置界面标题
        this.setTitle("奥利gei拼图单机版 v1.0");
        // 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的
        this.setAlwaysOnTop(true);
        // 设置界面打开时自动居中
        this.setLocationRelativeTo(null);
        // 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行
        // this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能
        this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        // 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件
        // 因为只有这样,添加图片的时候才不会一直处于居中位置
        this.setLayout(null);
        
        // 给整个游戏界面添加键盘监听事件
        this.addKeyListener(this);
    }
    
    // 初始化游戏主界面里的菜单栏
    private void initJMenu() {
        // a.创建一个菜单栏对象
        JMenuBar jMenuBar = new JMenuBar();

        // b.创建两个菜单栏下的菜单:功能、关于我们
        JMenu functionJMenu = new JMenu("功能");
        JMenu aboutUSJMenu = new JMenu("关于我们");

        // 创建菜单栏下的菜单:更换图片
        JMenu changeJMenu = new JMenu("更换图片");
        // 将更换图片的菜单嵌套进功能菜单中
        functionJMenu.add(changeJMenu);

        // e.将重新游戏、重新登录、关闭游戏这些栏目放到功能菜单下
        functionJMenu.add(replayGameItem);
        functionJMenu.add(replayLoginItem);
        functionJMenu.add(closeGameItem);

        // 将美女、动物这两个栏目放到更换图片菜单下
        changeJMenu.add(belleItem);
        changeJMenu.add(animalItem);

        // f.将公众号这个栏目放到关于我们菜单下
        aboutUSJMenu.add(accountItem);

        // g.将功能、关于我们这两个菜单放到菜单栏下
        jMenuBar.add(functionJMenu);
        jMenuBar.add(aboutUSJMenu);

        // h.最后将菜单栏放到设置到游戏界面中
        this.setJMenuBar(jMenuBar);
    }
    
    /*
        初始化图片到游戏主界面中
        细节:先加载的图片在上方,后加载的图片会在下方
     */
    private void initImages(String path) {
        // 先清空原本出现的所有图片
        this.getContentPane().removeAll();

        // 判断victory方法的返回结果是否为true
        if (victory()) {
            // 是,则说明玩家胜利了!显示胜利图标!
            // 根据指定图片的相对路径创建图片对象,并添加到管理容器中
            JLabel winJLabel = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\win.jpg"));
            // 设置胜利图片的坐标位置以及宽高
            winJLabel.setBounds(193, 300, 194, 75);
            // 将管理容器添加到游戏主界面中
            this.getContentPane().add(winJLabel);
        }

        // 统计步数
        // 创建一个管理容器对象,用于管理文字(步数: 0)
        JLabel stepJLabel = new JLabel("步数:" + step);
        // 设置管理容器的坐标位置
        stepJLabel.setBounds(40, 20, 400, 50);
        // 将容器添加到游戏主界面中
        this.getContentPane().add(stepJLabel);

        // 再加载图片
        /*
           外循环和内循环执行流程解析:
             外循环第一次执行:
                当 i = 0 时:i<4,为true,表示添加第一行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=1,表示添加第一行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=2,表示添加第一行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=3,表示添加第一行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=4,表示添加第一行的第四张图片
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第二次执行:
                当 i = 1 时:i<4,为true,表示添加第二行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=5,表示添加第二行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=6,表示添加第二行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=7,表示添加第二行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=8,表示添加第二行的第四张图片
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第三次执行:
                当 i = 2 时:i<4,为true,表示添加第三行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=9,表示添加第三行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=10,表示添加第三行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=11,表示添加第三行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=12,表示添加第三行的第四张图片
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第四次执行:
                当 i = 3 时:i<4,为true,表示添加第四行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=13,表示添加第四行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=14,表示添加第四行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=15,表示添加第四行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=16,表示添加第四行的第四张图片(由于文件中没有16这个命名的图片,因此会添加一个空白)
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第五次执行:
                当 i = 4 时:i<4,为false,外循环结束!
        */
        // 外循环:控制添加4行图片
        for (int i = 0; i < data.length; i++) { // Y轴:纵向
            // 内循环:控制每行添加4张图片
            for (int j = 0; j < data[i].length; j++) { // X轴:横向
                /*
                    解析:
                        比如二维数组中的数据:[{3, 9, 7, 5} {8, 10, 14, 11} {4, 2, 12, 13} {0, 15, 1, 6} ]
                   二维数组中的一维数组的索引:  0  1  2  3   0   1  2   3    0  1   2   3   0   1  2  3
                             二维数组的索引:       0             1              2              3
                      (1) 外循环执行第一次:
                        i=0, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (2) 外循环执行第二次:
                        i=1, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (3) 外循环执行第三次:
                        i=2, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (4) 外循环执行第四次:
                        i=3, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (5) 外循环执行第五次:
                        i=0, i<二维数组长度(4), 为false, 外循环结束!!
                 */
                // 接收二维数组i索引的一维数组j索引的数据
                int number = data[i][j];
                // 1、根据指定的文件相对路径创建一个ImageIcon图片对象
                ImageIcon imageIcon = new ImageIcon(path + number + ".jpg");

                // JLabel:管理容器,用于管理图片、文字
                // 2、创建一个管理容器JLabel对象,用于管理图片对象ImageIcon,
                // 并将图片对象imageIcon放到管理容器jLabel中。
                JLabel jLabel = new JLabel(imageIcon);

                // 3、指定图片位置:XY轴
                jLabel.setBounds(105 * j + 80, 105 * i + 130, 105, 105);

                // 4、每添加完一张小图片,给这张小图片添加边框
                /*
                    0: 表示让图片凸起来
                    1: 表示让图片凹下去
                 */
                jLabel.setBorder(new BevelBorder(1));

                // 5、将管理容器JLabel对象放到游戏主界面中
                // getContentPane:获取隐藏容器
                this.getContentPane().add(jLabel);
            }
        }

        // 在所有小图片都添加到界面之后,开始添加背景图片
        // 根据指定的相对路径创建背景图片对象,并添加到管理容器中
        JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));
        // 设置背景图片的坐标位置以及宽高
        background.setBounds(36, 36, 508, 560);
        // 将背景图片管理容器添加到界面中
        this.getContentPane().add(background);

        // 最后刷新一下游戏界面
        this.getContentPane().repaint();
    }
    
    // 这个可以不用管
    @Override
    public void keyTyped(KeyEvent e) {
    }


    /*
        监听按下键盘按键的事件
        细节:当按下按键不松开,会不断移动图片
     */
    @Override
    public void keyPressed(KeyEvent e) {
        // 获取键盘每个按键的编号
        int keyCode = e.getKeyCode();

        // 判断按下的按键是否为A
        if (keyCode == 65) {
            // 需要先清空原本的所有图片
            this.getContentPane().removeAll();

            // 加载完整图片
            // 根据指定图片的相对路径创建一个图片对象,并添加到管理容器中
            JLabel masterMap = new JLabel(new ImageIcon(path + "all.jpg"));
            // 设置图片的坐标位置以及宽高
            masterMap.setBounds(80, 130, 420, 420);
            // 将管理容器添加到游戏界面中
            this.getContentPane().add(masterMap);

            // 在完整图片下面加载背景图片
            // 根据指定图片的相对路径创建图片对象,并添加到管理容器中
            JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));
            // 设置背景图片的坐标位置以及宽高
            background.setBounds(36, 36, 508, 560);
            // 将管理容器添加到游戏界面中
            this.getContentPane().add(background);

            // 最后刷新一下游戏界面
            this.getContentPane().repaint();
        }
    }


    /*
        监听松开键盘按键的事件:
            当按下按键松开后,会调用该方法
     */
    @Override
    public void keyReleased(KeyEvent e) {
        // 获取键盘每个按键的编号
        int keyCode = e.getKeyCode();

        // 判断玩家是否已经胜利!
        if (victory()) {
            // 是,则说明已经胜利!则直接结束当前方法!不让玩家继续进行拼图操作!
            return;
        }

        /*
            上下左右按键的编号:
                左:37、上:38、右:39、下:40
         */
        // 判断松开的按键是否为上
        if (keyCode == 38) {
            // 判断空白方块的X轴是否为3
            if (x == 3) {
                // 是,说明空白方块下方已没有图片可以向上移动了,则提示!
                System.out.println("下方已没有图片可以向上移动了");
                return; // 结束方法!
            }

            // 否,则图片向上移动
            System.out.println("图片向上移动");
            /*
                图片向上移动逻辑:
                    把空白方块下方的数字向上移动
                    x, y: 表示空白方块
                    x + 1, y: 表示空白方块下方的数字
             */
            // 将空白方块下方图片的数据 赋值给 空白方块处
            data[x][y] = data[x + 1][y];
            // 空白方块下方图片的数据赋值为0
            data[x + 1][y] = 0;
            // 将空白方块往下移动
            x++;
            // 移动后统计步数
            step++;
            // 重新加载一下所有图片
            initImages(path);
        } else if (keyCode == 40) { // 否,则判断松开的按键是否为下
            // 是,则判断空白方块的X轴是否在0位置
            if (x == 0) {
                // 是,说明空白方块的上方已没有图片可以向下移动了,则提示!
                System.out.println("上方已没有图片可以向下移动了");
                return; // 结束方法!
            }

            // 否,则图片向下移动
            System.out.println("图片向下移动");
            /*
                图片向下移动逻辑:
                    把空白方块上方的数字向下移动
                    x, y: 表示空白方块
                    x - 1, y: 表示空白方块上方的数字
             */
            // 将空白方块上方图片的数据 赋值给 空白方块处
            data[x][y] = data[x - 1][y];
            // 空白下方图片的数据赋值为0
            data[x - 1][y] = 0;
            // 将空白方块往上移动
            x--;
            // 移动后统计步数
            step++;
            // 重新加载一下所有图片
            initImages(path);
        } else if (keyCode == 37) { // 否,则判断松开的按键是否为左
            // 是,则判断空白方块的Y轴是否为3
            if (y == 3) {
                // 是,说明空白方块的右方已没有图片可以向左移动了,则提示!
                System.out.println("右方已没有图片可以向左移动了");
                return; // 结束方法!
            }

            // 否,则图片向左移动
            System.out.println("图片向左移动");
            /*
                图片向左移动逻辑:
                    把空白方块右方的数字向左移动
                    x, y: 表示空白方块
                    x, y + 1: 表示空白方块右方的数字
             */
            // 将空表方块右方图片的数据 赋值给 空白方块处
            data[x][y] = data[x][y + 1];
            // 空白右方图片的数据赋值为0
            data[x][y + 1] = 0;
            // 将空白方块往右移动
            y++;
            // 移动后统计步数
            step++;
            // 重新加载一下所有图片
            initImages(path);
        } else if (keyCode == 39) { // 否,则判断松开的按键是否为右
            // 是,则判断空白方块的Y轴是否在0位置
            if (y == 0) {
                // 是,说明空白方块的左方已没有图片可以向右移动了,则提示!
                System.out.println("左方已没有图片可以向右移动了");
                return; // 结束方法!
            }

            // 否,则图片向右移动
            System.out.println("图片向右移动");
            /*
                图片向右移动逻辑:
                    把空白方块左方的数字向右移动
                    x, y: 表示空白方块
                    x, y - 1: 表示空白方块左方的数字
             */
            // 将空表方块左方图片的数据 赋值给 空白方块处
            data[x][y] = data[x][y - 1];
            // 空白左方图片的数据赋值为0
            data[x][y - 1] = 0;
            // 将空白方块往左移动
            y--;
            // 移动后统计步数
            step++;
            // 重新加载一下所有图片
            initImages(path);
        } else if (keyCode == 65) { // 否,则判断松开的按键是否为A
            // 显示随机打乱的图片
            initImages(path);
        } else if (keyCode == 87) { // 否,则判断松开的按键是否为W
            // 是,则显示拼图完成后的效果!
            // 其实就是直接把正确顺序数据的新二维数组 赋值给 乱序数据的旧二维数组
            data = new int[][]{
                    {1, 2, 3, 4},
                    {5, 6, 7, 8},
                    {9, 10, 11, 12},
                    {13, 14, 15, 0}
            };
            // 按照二维数组的最新数据重新加载所有小图片
            initImages(path);
        }
    }
}


(2)程序启动入口类

import cn.edu.gxufe.ui.GameJFrame;
import cn.edu.gxufe.ui.LoginJFrame;
import cn.edu.gxufe.ui.RegisterJFrame;

public class App {
    public static void main(String[] args) {
        // 表示程序启动的入口

        // 如果我们想要开启一个界面,就创建对应界面的对象即可!
        new GameJFrame();       // 游戏主界面
//         new LoginJFrame();      // 登录界面
//         new RegisterJFrame();   // 注册界面
    }
}


(3)测试结果

16-JavaSE基础巩固项目:拼图小游戏_第32张图片



4、移动图片业务小结

  • 1、本类实现键盘监听接口:KeyListener,并重写所有抽象方法
  • 2、给整个界面添加键盘监听事件
  • 3、统计一下空白方块对应的数字0在二维数组中的位置
  • 4、在keyPressed方法当中实现图片移动的逻辑程序代码,当然也可以在keyReleased方法中实现。
    • 注意:
      • keyPressed方法是按下按键触发事件,只要你不松开按键,就会重复触发!
      • keyReleased方法是松开按键触发事件,如果你不松开按键,就不会触发,一松开就触发!
  • 5、Bug修复:
    • 当空白方块在最上方的时候,无法再次进行下移;
    • 当空白方块在最下方的时候,无法再次进行上移;
    • 当空白方块在最左方的时候,无法再次进行右移;
    • 当空白方块在最右方的时候,无法再次进行左移。



九、查看完整图片、作弊码和判断胜利

1、查看完整图片

(1)需求

  • 当按住按键A不松开的时候,显示完整图片
  • 当松开按键A的时候,显示随机打乱的图片

(2)分析

  • 1、先需要给整个界面添加键盘监听事件
  • 2、再需要在按下键盘的方法中实现当按下按键A的时候,显示完整图片的逻辑代码
  • 3、最后只需要在松开键盘的方法中实现当松开按键A的时候,显示随机打乱的图片的逻辑代码
  • 4、优化一下相对路径:
    • 将图片的所有上一层路径定义成一个成员变量path,这样以后要做更换图片的逻辑代码的时候,
    • 就只需要改写path变量里面记录的值就好了!

(3)实现

1、游戏主界面类
package cn.edu.gxufe.ui;

import javax.swing.*;

/**
 * 游戏主界面类:
 * 继承父类:JFrame
 */
public class GameJFrame extends JFrame implements KeyListener{
    // 表示游戏相关的逻辑代码都写在这!
    
    /*
        创建二维数组:
            目的:管理数据
            加载图片的时候,会根据二位数组中的数据进行加载
     */
    int[][] data = new int[4][4];

    // 定义x、y变量,用于记录空白方块所在的XY的坐标位置
    int x = 0;
    int y = 0;

    // 创建一个成员的随机数对象,用于生成一个随机数
    Random rd = new Random();

    // 定义path变量,用于记录图片的相对路径,方便以后做更换图片、重新游戏等功能的实现
    String path = "puzzle_game\puzzleimages\girl\girl1\";

    // 定义一个正确顺序数据的二维数组
    int[][] win = {
            {1, 2, 3, 4},
            {5, 6, 7, 8},
            {9, 10, 11, 12},
            {13, 14, 15, 0}
    };


    // 定义一个计数器变量,用于记录玩家拼图移动的步数
    int step = 0;


    // c.创建功能菜单下的三个栏目:重新游戏、重新登录、关闭游戏
    // 更换图片栏目放到后面再写,因为比较复杂
    JMenuItem replayGameItem = new JMenuItem("重新游戏");
    JMenuItem replayLoginItem = new JMenuItem("重新登录");
    JMenuItem closeGameItem = new JMenuItem("关闭游戏");

    // d.创建关于我们菜单下的一个栏目:公众号
    JMenuItem accountItem = new JMenuItem("公众号");

    // 创建更换图片菜单下的两个栏目:美女、动物
    JMenuItem belleItem = new JMenuItem("美女");
    JMenuItem animalItem = new JMenuItem("动物");

    /*
        提供无参数的构造器
        需求:初始化一个宽603像素,高680像素的游戏主界面
     */
    public GameJFrame() {
        // 注:如果当前类没有以下这些方法,就会自动调用父类JFrame的
        // 初始化游戏主界面
        initJFrame();
        
        // 初始化游戏主界面里的菜单栏
        initJMenu();
        
        // 初始化数据:打乱
        initData();
        
        // 初始化图片到游戏主界面中
        initImages(path);

        // 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面
        // 由于界面默认是隐藏起来的,因此需要调用当前GameJFrame类的setVisible方法,设置显示界面
        this.setVisible(true);
    }
    
    // 初始化数据:打乱
    private void initData() {
        // 1、创建一个一维数组,用于存储一些数据
        int[] arr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};

        // 2、开始打乱
        // 遍历数组,依次获取到数组中的每个数据
        for (int i = 0; i < arr.length; i++) {
            // 循环每执行一次,就生成一个随机索引
            int rdIndex = rd.nextInt(arr.length);
            // 每遍历到一个数据,就用临时变量存储一下
            int temp = arr[i];
            // 开始交换
            // 将随机索引位置的数据 放到 当前遍历到的数据的位置
            arr[i] = arr[rdIndex];
            // 将当前遍历到的数据 放到 随机索引位置
            arr[rdIndex] = temp;
        }

        /*
            解析:
                比如打乱后的一维数组的数据:[0, 14, 8, 3, 13, 15, 6, 4, 10, 12, 7, 1, 9, 5, 11, 2]
                                  索引: 0  1   2  3  4   5   6  7  8   9   10 11 12 13 14  15
                循环第一次执行:
                    i=0,i<数组长度(16),为true,将0索引的数据:0 添加到二维数组的 i/4=0索引的一维数组的 i%4=0索引,i++
                循环第二次执行:
                    i=1,i<数组长度(16),为true,将1索引的数据:14 添加到二维数组的 i/4=0索引的一维数组的 i%4=1索引,i++
                循环第三次执行:
                    i=2,i<数组长度(16),为true,将2索引的数据:8 添加到二维数组的 i/4=0索引的一维数组的 i%4=2索引,i++
                后面的都是以此类推了!直到i<16,为false,循环结束!
         */
        // 3、遍历arr一维数组,依次得到打乱后的每个数据
        for (int i = 0; i < arr.length; i++) {
            // 判断当前数据是否为0
            if (arr[i] == 0) {
                // 是,则记录空白方块XY的坐标位置
                x = i / 4;
                y = i % 4;
            }
            // 否,则依次将打乱后的数据添加到二维数组中
            data[i / 4][i % 4] = arr[i];
        }
    }
    
    // 初始化游戏主界面
    private void initJFrame() {
        // 设置界面宽高
        // 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面
        this.setSize(603, 680);
        // 设置界面标题
        this.setTitle("奥利gei拼图单机版 v1.0");
        // 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的
        this.setAlwaysOnTop(true);
        // 设置界面打开时自动居中
        this.setLocationRelativeTo(null);
        // 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行
        // this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能
        this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        // 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件
        // 因为只有这样,添加图片的时候才不会一直处于居中位置
        this.setLayout(null);
        
        // 给整个游戏界面添加键盘监听事件
        this.addKeyListener(this);
    }
    
    // 初始化游戏主界面里的菜单栏
    private void initJMenu() {
        // a.创建一个菜单栏对象
        JMenuBar jMenuBar = new JMenuBar();

        // b.创建两个菜单栏下的菜单:功能、关于我们
        JMenu functionJMenu = new JMenu("功能");
        JMenu aboutUSJMenu = new JMenu("关于我们");

        // 创建菜单栏下的菜单:更换图片
        JMenu changeJMenu = new JMenu("更换图片");
        // 将更换图片的菜单嵌套进功能菜单中
        functionJMenu.add(changeJMenu);

        // e.将重新游戏、重新登录、关闭游戏这些栏目放到功能菜单下
        functionJMenu.add(replayGameItem);
        functionJMenu.add(replayLoginItem);
        functionJMenu.add(closeGameItem);

        // 将美女、动物这两个栏目放到更换图片菜单下
        changeJMenu.add(belleItem);
        changeJMenu.add(animalItem);

        // f.将公众号这个栏目放到关于我们菜单下
        aboutUSJMenu.add(accountItem);

        // g.将功能、关于我们这两个菜单放到菜单栏下
        jMenuBar.add(functionJMenu);
        jMenuBar.add(aboutUSJMenu);

        // h.最后将菜单栏放到设置到游戏界面中
        this.setJMenuBar(jMenuBar);
    }
    
    /*
        初始化图片到游戏主界面中
        细节:先加载的图片在上方,后加载的图片会在下方
     */
    private void initImages(String path) {
        // 先清空原本出现的所有图片
        this.getContentPane().removeAll();

        // 判断victory方法的返回结果是否为true
        if (victory()) {
            // 是,则说明玩家胜利了!显示胜利图标!
            // 根据指定图片的相对路径创建图片对象,并添加到管理容器中
            JLabel winJLabel = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\win.jpg"));
            // 设置胜利图片的坐标位置以及宽高
            winJLabel.setBounds(193, 300, 194, 75);
            // 将管理容器添加到游戏主界面中
            this.getContentPane().add(winJLabel);
        }

        // 统计步数
        // 创建一个管理容器对象,用于管理文字(步数: 0)
        JLabel stepJLabel = new JLabel("步数:" + step);
        // 设置管理容器的坐标位置
        stepJLabel.setBounds(40, 20, 400, 50);
        // 将容器添加到游戏主界面中
        this.getContentPane().add(stepJLabel);

        // 再加载图片
        /*
           外循环和内循环执行流程解析:
             外循环第一次执行:
                当 i = 0 时:i<4,为true,表示添加第一行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=1,表示添加第一行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=2,表示添加第一行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=3,表示添加第一行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=4,表示添加第一行的第四张图片
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第二次执行:
                当 i = 1 时:i<4,为true,表示添加第二行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=5,表示添加第二行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=6,表示添加第二行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=7,表示添加第二行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=8,表示添加第二行的第四张图片
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第三次执行:
                当 i = 2 时:i<4,为true,表示添加第三行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=9,表示添加第三行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=10,表示添加第三行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=11,表示添加第三行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=12,表示添加第三行的第四张图片
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第四次执行:
                当 i = 3 时:i<4,为true,表示添加第四行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=13,表示添加第四行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=14,表示添加第四行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=15,表示添加第四行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=16,表示添加第四行的第四张图片(由于文件中没有16这个命名的图片,因此会添加一个空白)
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第五次执行:
                当 i = 4 时:i<4,为false,外循环结束!
        */
        // 外循环:控制添加4行图片
        for (int i = 0; i < data.length; i++) { // Y轴:纵向
            // 内循环:控制每行添加4张图片
            for (int j = 0; j < data[i].length; j++) { // X轴:横向
                /*
                    解析:
                        比如二维数组中的数据:[{3, 9, 7, 5} {8, 10, 14, 11} {4, 2, 12, 13} {0, 15, 1, 6} ]
                   二维数组中的一维数组的索引:  0  1  2  3   0   1  2   3    0  1   2   3   0   1  2  3
                             二维数组的索引:       0             1              2              3
                      (1) 外循环执行第一次:
                        i=0, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (2) 外循环执行第二次:
                        i=1, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (3) 外循环执行第三次:
                        i=2, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (4) 外循环执行第四次:
                        i=3, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (5) 外循环执行第五次:
                        i=0, i<二维数组长度(4), 为false, 外循环结束!!
                 */
                // 接收二维数组i索引的一维数组j索引的数据
                int number = data[i][j];
                // 1、根据指定的文件相对路径创建一个ImageIcon图片对象
                ImageIcon imageIcon = new ImageIcon(path + number + ".jpg");

                // JLabel:管理容器,用于管理图片、文字
                // 2、创建一个管理容器JLabel对象,用于管理图片对象ImageIcon,
                // 并将图片对象imageIcon放到管理容器jLabel中。
                JLabel jLabel = new JLabel(imageIcon);

                // 3、指定图片位置:XY轴
                jLabel.setBounds(105 * j + 80, 105 * i + 130, 105, 105);

                // 4、每添加完一张小图片,给这张小图片添加边框
                /*
                    0: 表示让图片凸起来
                    1: 表示让图片凹下去
                 */
                jLabel.setBorder(new BevelBorder(1));

                // 5、将管理容器JLabel对象放到游戏主界面中
                // getContentPane:获取隐藏容器
                this.getContentPane().add(jLabel);
            }
        }

        // 在所有小图片都添加到界面之后,开始添加背景图片
        // 根据指定的相对路径创建背景图片对象,并添加到管理容器中
        JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));
        // 设置背景图片的坐标位置以及宽高
        background.setBounds(36, 36, 508, 560);
        // 将背景图片管理容器添加到界面中
        this.getContentPane().add(background);

        // 最后刷新一下游戏界面
        this.getContentPane().repaint();
    }
    
    // 这个可以不用管
    @Override
    public void keyTyped(KeyEvent e) {
    }


    /*
        监听按下键盘按键的事件
        细节:当按下按键不松开,会不断移动图片
     */
    @Override
    public void keyPressed(KeyEvent e) {
        // 获取键盘每个按键的编号
        int keyCode = e.getKeyCode();

        // 判断按下的按键是否为A
        if (keyCode == 65) {
            // 需要先清空原本的所有图片
            this.getContentPane().removeAll();

            // 加载完整图片
            // 根据指定图片的相对路径创建一个图片对象,并添加到管理容器中
            JLabel masterMap = new JLabel(new ImageIcon(path + "all.jpg"));
            // 设置图片的坐标位置以及宽高
            masterMap.setBounds(80, 130, 420, 420);
            // 将管理容器添加到游戏界面中
            this.getContentPane().add(masterMap);

            // 在完整图片下面加载背景图片
            // 根据指定图片的相对路径创建图片对象,并添加到管理容器中
            JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));
            // 设置背景图片的坐标位置以及宽高
            background.setBounds(36, 36, 508, 560);
            // 将管理容器添加到游戏界面中
            this.getContentPane().add(background);

            // 最后刷新一下游戏界面
            this.getContentPane().repaint();
        }
    }


    /*
        监听松开键盘按键的事件:
            当按下按键松开后,会调用该方法
     */
    @Override
    public void keyReleased(KeyEvent e) {
        // 获取键盘每个按键的编号
        int keyCode = e.getKeyCode();

        // 判断玩家是否已经胜利!
        if (victory()) {
            // 是,则说明已经胜利!则直接结束当前方法!不让玩家继续进行拼图操作!
            return;
        }

        /*
            上下左右按键的编号:
                左:37、上:38、右:39、下:40
         */
        // 判断松开的按键是否为上
        if (keyCode == 38) {
            // 判断空白方块的X轴是否为3
            if (x == 3) {
                // 是,说明空白方块下方已没有图片可以向上移动了,则提示!
                System.out.println("下方已没有图片可以向上移动了");
                return; // 结束方法!
            }

            // 否,则图片向上移动
            System.out.println("图片向上移动");
            /*
                图片向上移动逻辑:
                    把空白方块下方的数字向上移动
                    x, y: 表示空白方块
                    x + 1, y: 表示空白方块下方的数字
             */
            // 将空白方块下方图片的数据 赋值给 空白方块处
            data[x][y] = data[x + 1][y];
            // 空白方块下方图片的数据赋值为0
            data[x + 1][y] = 0;
            // 将空白方块往下移动
            x++;
            // 移动后统计步数
            step++;
            // 重新加载一下所有图片
            initImages(path);
        } else if (keyCode == 40) { // 否,则判断松开的按键是否为下
            // 是,则判断空白方块的X轴是否在0位置
            if (x == 0) {
                // 是,说明空白方块的上方已没有图片可以向下移动了,则提示!
                System.out.println("上方已没有图片可以向下移动了");
                return; // 结束方法!
            }

            // 否,则图片向下移动
            System.out.println("图片向下移动");
            /*
                图片向下移动逻辑:
                    把空白方块上方的数字向下移动
                    x, y: 表示空白方块
                    x - 1, y: 表示空白方块上方的数字
             */
            // 将空白方块上方图片的数据 赋值给 空白方块处
            data[x][y] = data[x - 1][y];
            // 空白下方图片的数据赋值为0
            data[x - 1][y] = 0;
            // 将空白方块往上移动
            x--;
            // 移动后统计步数
            step++;
            // 重新加载一下所有图片
            initImages(path);
        } else if (keyCode == 37) { // 否,则判断松开的按键是否为左
            // 是,则判断空白方块的Y轴是否为3
            if (y == 3) {
                // 是,说明空白方块的右方已没有图片可以向左移动了,则提示!
                System.out.println("右方已没有图片可以向左移动了");
                return; // 结束方法!
            }

            // 否,则图片向左移动
            System.out.println("图片向左移动");
            /*
                图片向左移动逻辑:
                    把空白方块右方的数字向左移动
                    x, y: 表示空白方块
                    x, y + 1: 表示空白方块右方的数字
             */
            // 将空表方块右方图片的数据 赋值给 空白方块处
            data[x][y] = data[x][y + 1];
            // 空白右方图片的数据赋值为0
            data[x][y + 1] = 0;
            // 将空白方块往右移动
            y++;
            // 移动后统计步数
            step++;
            // 重新加载一下所有图片
            initImages(path);
        } else if (keyCode == 39) { // 否,则判断松开的按键是否为右
            // 是,则判断空白方块的Y轴是否在0位置
            if (y == 0) {
                // 是,说明空白方块的左方已没有图片可以向右移动了,则提示!
                System.out.println("左方已没有图片可以向右移动了");
                return; // 结束方法!
            }

            // 否,则图片向右移动
            System.out.println("图片向右移动");
            /*
                图片向右移动逻辑:
                    把空白方块左方的数字向右移动
                    x, y: 表示空白方块
                    x, y - 1: 表示空白方块左方的数字
             */
            // 将空表方块左方图片的数据 赋值给 空白方块处
            data[x][y] = data[x][y - 1];
            // 空白左方图片的数据赋值为0
            data[x][y - 1] = 0;
            // 将空白方块往左移动
            y--;
            // 移动后统计步数
            step++;
            // 重新加载一下所有图片
            initImages(path);
        } else if (keyCode == 65) { // 否,则判断松开的按键是否为A
            // 显示随机打乱的图片
            initImages(path);
        } else if (keyCode == 87) { // 否,则判断松开的按键是否为W
            // 是,则显示拼图完成后的效果!
            // 其实就是直接把正确顺序数据的新二维数组 赋值给 乱序数据的旧二维数组
            data = new int[][]{
                    {1, 2, 3, 4},
                    {5, 6, 7, 8},
                    {9, 10, 11, 12},
                    {13, 14, 15, 0}
            };
            // 按照二维数组的最新数据重新加载所有小图片
            initImages(path);
        }
    }
}

2、程序启动入口类
import cn.edu.gxufe.ui.GameJFrame;
import cn.edu.gxufe.ui.LoginJFrame;
import cn.edu.gxufe.ui.RegisterJFrame;

public class App {
    public static void main(String[] args) {
        // 表示程序启动的入口

        // 如果我们想要开启一个界面,就创建对应界面的对象即可!
        new GameJFrame();       // 游戏主界面
//         new LoginJFrame();      // 登录界面
//         new RegisterJFrame();   // 注册界面
    }
}

3、测试结果

16-JavaSE基础巩固项目:拼图小游戏_第33张图片



2、作弊码(一键通关)

(1)需求

  • 当松开按键W的时候,直接通关,也就是将所有的小图片都拼成与原图一模一样的!

(2)分析

  • 1、先需要给整个游戏主界面添加键盘监听事件
  • 2、然后需要定义一个正确顺序数据的新二维数组,存储正确顺序的数据。
  • 3、最后需要在松开按键的方法中实现当松开按键W的时候,显示与原图一样的拼图效果:
    • 就是把正确顺序数据的新二维数组 赋值给 乱序的旧二维数组,
    • 然后再按照二维数组的最新数据重新初始化所有小图片即可。

(3)实现

1、游戏主界面类
package cn.edu.gxufe.ui;

import javax.swing.*;

/**
 * 游戏主界面类:
 * 继承父类:JFrame
 */
public class GameJFrame extends JFrame implements KeyListener{
    // 表示游戏相关的逻辑代码都写在这!
    
    /*
        创建二维数组:
            目的:管理数据
            加载图片的时候,会根据二位数组中的数据进行加载
     */
    int[][] data = new int[4][4];

    // 定义x、y变量,用于记录空白方块所在的XY的坐标位置
    int x = 0;
    int y = 0;

    // 创建一个成员的随机数对象,用于生成一个随机数
    Random rd = new Random();

    // 定义path变量,用于记录图片的相对路径,方便以后做更换图片、重新游戏等功能的实现
    String path = "puzzle_game\puzzleimages\girl\girl1\";

    // 定义一个正确顺序数据的二维数组
    int[][] win = {
            {1, 2, 3, 4},
            {5, 6, 7, 8},
            {9, 10, 11, 12},
            {13, 14, 15, 0}
    };


    // 定义一个计数器变量,用于记录玩家拼图移动的步数
    int step = 0;


    // c.创建功能菜单下的三个栏目:重新游戏、重新登录、关闭游戏
    // 更换图片栏目放到后面再写,因为比较复杂
    JMenuItem replayGameItem = new JMenuItem("重新游戏");
    JMenuItem replayLoginItem = new JMenuItem("重新登录");
    JMenuItem closeGameItem = new JMenuItem("关闭游戏");

    // d.创建关于我们菜单下的一个栏目:公众号
    JMenuItem accountItem = new JMenuItem("公众号");

    // 创建更换图片菜单下的两个栏目:美女、动物
    JMenuItem belleItem = new JMenuItem("美女");
    JMenuItem animalItem = new JMenuItem("动物");

    /*
        提供无参数的构造器
        需求:初始化一个宽603像素,高680像素的游戏主界面
     */
    public GameJFrame() {
        // 注:如果当前类没有以下这些方法,就会自动调用父类JFrame的
        // 初始化游戏主界面
        initJFrame();
        
        // 初始化游戏主界面里的菜单栏
        initJMenu();
        
        // 初始化数据:打乱
        initData();
        
        // 初始化图片到游戏主界面中
        initImages(path);

        // 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面
        // 由于界面默认是隐藏起来的,因此需要调用当前GameJFrame类的setVisible方法,设置显示界面
        this.setVisible(true);
    }
    
    // 初始化数据:打乱
    private void initData() {
        // 1、创建一个一维数组,用于存储一些数据
        int[] arr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};

        // 2、开始打乱
        // 遍历数组,依次获取到数组中的每个数据
        for (int i = 0; i < arr.length; i++) {
            // 循环每执行一次,就生成一个随机索引
            int rdIndex = rd.nextInt(arr.length);
            // 每遍历到一个数据,就用临时变量存储一下
            int temp = arr[i];
            // 开始交换
            // 将随机索引位置的数据 放到 当前遍历到的数据的位置
            arr[i] = arr[rdIndex];
            // 将当前遍历到的数据 放到 随机索引位置
            arr[rdIndex] = temp;
        }

        /*
            解析:
                比如打乱后的一维数组的数据:[0, 14, 8, 3, 13, 15, 6, 4, 10, 12, 7, 1, 9, 5, 11, 2]
                                  索引: 0  1   2  3  4   5   6  7  8   9   10 11 12 13 14  15
                循环第一次执行:
                    i=0,i<数组长度(16),为true,将0索引的数据:0 添加到二维数组的 i/4=0索引的一维数组的 i%4=0索引,i++
                循环第二次执行:
                    i=1,i<数组长度(16),为true,将1索引的数据:14 添加到二维数组的 i/4=0索引的一维数组的 i%4=1索引,i++
                循环第三次执行:
                    i=2,i<数组长度(16),为true,将2索引的数据:8 添加到二维数组的 i/4=0索引的一维数组的 i%4=2索引,i++
                后面的都是以此类推了!直到i<16,为false,循环结束!
         */
        // 3、遍历arr一维数组,依次得到打乱后的每个数据
        for (int i = 0; i < arr.length; i++) {
            // 判断当前数据是否为0
            if (arr[i] == 0) {
                // 是,则记录空白方块XY的坐标位置
                x = i / 4;
                y = i % 4;
            }
            // 否,则依次将打乱后的数据添加到二维数组中
            data[i / 4][i % 4] = arr[i];
        }
    }
    
    // 初始化游戏主界面
    private void initJFrame() {
        // 设置界面宽高
        // 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面
        this.setSize(603, 680);
        // 设置界面标题
        this.setTitle("奥利gei拼图单机版 v1.0");
        // 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的
        this.setAlwaysOnTop(true);
        // 设置界面打开时自动居中
        this.setLocationRelativeTo(null);
        // 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行
        // this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能
        this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        // 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件
        // 因为只有这样,添加图片的时候才不会一直处于居中位置
        this.setLayout(null);
        
        // 给整个游戏界面添加键盘监听事件
        this.addKeyListener(this);
    }
    
    // 初始化游戏主界面里的菜单栏
    private void initJMenu() {
        // a.创建一个菜单栏对象
        JMenuBar jMenuBar = new JMenuBar();

        // b.创建两个菜单栏下的菜单:功能、关于我们
        JMenu functionJMenu = new JMenu("功能");
        JMenu aboutUSJMenu = new JMenu("关于我们");

        // 创建菜单栏下的菜单:更换图片
        JMenu changeJMenu = new JMenu("更换图片");
        // 将更换图片的菜单嵌套进功能菜单中
        functionJMenu.add(changeJMenu);

        // e.将重新游戏、重新登录、关闭游戏这些栏目放到功能菜单下
        functionJMenu.add(replayGameItem);
        functionJMenu.add(replayLoginItem);
        functionJMenu.add(closeGameItem);

        // 将美女、动物这两个栏目放到更换图片菜单下
        changeJMenu.add(belleItem);
        changeJMenu.add(animalItem);

        // f.将公众号这个栏目放到关于我们菜单下
        aboutUSJMenu.add(accountItem);

        // g.将功能、关于我们这两个菜单放到菜单栏下
        jMenuBar.add(functionJMenu);
        jMenuBar.add(aboutUSJMenu);

        // h.最后将菜单栏放到设置到游戏界面中
        this.setJMenuBar(jMenuBar);
    }
    
    /*
        初始化图片到游戏主界面中
        细节:先加载的图片在上方,后加载的图片会在下方
     */
    private void initImages(String path) {
        // 先清空原本出现的所有图片
        this.getContentPane().removeAll();

        // 判断victory方法的返回结果是否为true
        if (victory()) {
            // 是,则说明玩家胜利了!显示胜利图标!
            // 根据指定图片的相对路径创建图片对象,并添加到管理容器中
            JLabel winJLabel = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\win.jpg"));
            // 设置胜利图片的坐标位置以及宽高
            winJLabel.setBounds(193, 300, 194, 75);
            // 将管理容器添加到游戏主界面中
            this.getContentPane().add(winJLabel);
        }

        // 统计步数
        // 创建一个管理容器对象,用于管理文字(步数: 0)
        JLabel stepJLabel = new JLabel("步数:" + step);
        // 设置管理容器的坐标位置
        stepJLabel.setBounds(40, 20, 400, 50);
        // 将容器添加到游戏主界面中
        this.getContentPane().add(stepJLabel);

        // 再加载图片
        /*
           外循环和内循环执行流程解析:
             外循环第一次执行:
                当 i = 0 时:i<4,为true,表示添加第一行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=1,表示添加第一行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=2,表示添加第一行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=3,表示添加第一行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=4,表示添加第一行的第四张图片
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第二次执行:
                当 i = 1 时:i<4,为true,表示添加第二行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=5,表示添加第二行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=6,表示添加第二行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=7,表示添加第二行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=8,表示添加第二行的第四张图片
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第三次执行:
                当 i = 2 时:i<4,为true,表示添加第三行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=9,表示添加第三行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=10,表示添加第三行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=11,表示添加第三行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=12,表示添加第三行的第四张图片
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第四次执行:
                当 i = 3 时:i<4,为true,表示添加第四行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=13,表示添加第四行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=14,表示添加第四行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=15,表示添加第四行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=16,表示添加第四行的第四张图片(由于文件中没有16这个命名的图片,因此会添加一个空白)
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第五次执行:
                当 i = 4 时:i<4,为false,外循环结束!
        */
        // 外循环:控制添加4行图片
        for (int i = 0; i < data.length; i++) { // Y轴:纵向
            // 内循环:控制每行添加4张图片
            for (int j = 0; j < data[i].length; j++) { // X轴:横向
                /*
                    解析:
                        比如二维数组中的数据:[{3, 9, 7, 5} {8, 10, 14, 11} {4, 2, 12, 13} {0, 15, 1, 6} ]
                   二维数组中的一维数组的索引:  0  1  2  3   0   1  2   3    0  1   2   3   0   1  2  3
                             二维数组的索引:       0             1              2              3
                      (1) 外循环执行第一次:
                        i=0, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (2) 外循环执行第二次:
                        i=1, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (3) 外循环执行第三次:
                        i=2, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (4) 外循环执行第四次:
                        i=3, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (5) 外循环执行第五次:
                        i=0, i<二维数组长度(4), 为false, 外循环结束!!
                 */
                // 接收二维数组i索引的一维数组j索引的数据
                int number = data[i][j];
                // 1、根据指定的文件相对路径创建一个ImageIcon图片对象
                ImageIcon imageIcon = new ImageIcon(path + number + ".jpg");

                // JLabel:管理容器,用于管理图片、文字
                // 2、创建一个管理容器JLabel对象,用于管理图片对象ImageIcon,
                // 并将图片对象imageIcon放到管理容器jLabel中。
                JLabel jLabel = new JLabel(imageIcon);

                // 3、指定图片位置:XY轴
                jLabel.setBounds(105 * j + 80, 105 * i + 130, 105, 105);

                // 4、每添加完一张小图片,给这张小图片添加边框
                /*
                    0: 表示让图片凸起来
                    1: 表示让图片凹下去
                 */
                jLabel.setBorder(new BevelBorder(1));

                // 5、将管理容器JLabel对象放到游戏主界面中
                // getContentPane:获取隐藏容器
                this.getContentPane().add(jLabel);
            }
        }

        // 在所有小图片都添加到界面之后,开始添加背景图片
        // 根据指定的相对路径创建背景图片对象,并添加到管理容器中
        JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));
        // 设置背景图片的坐标位置以及宽高
        background.setBounds(36, 36, 508, 560);
        // 将背景图片管理容器添加到界面中
        this.getContentPane().add(background);

        // 最后刷新一下游戏界面
        this.getContentPane().repaint();
    }
    
    // 这个可以不用管
    @Override
    public void keyTyped(KeyEvent e) {
    }


    /*
        监听按下键盘按键的事件
        细节:当按下按键不松开,会不断移动图片
     */
    @Override
    public void keyPressed(KeyEvent e) {
        // 获取键盘每个按键的编号
        int keyCode = e.getKeyCode();

        // 判断按下的按键是否为A
        if (keyCode == 65) {
            // 需要先清空原本的所有图片
            this.getContentPane().removeAll();

            // 加载完整图片
            // 根据指定图片的相对路径创建一个图片对象,并添加到管理容器中
            JLabel masterMap = new JLabel(new ImageIcon(path + "all.jpg"));
            // 设置图片的坐标位置以及宽高
            masterMap.setBounds(80, 130, 420, 420);
            // 将管理容器添加到游戏界面中
            this.getContentPane().add(masterMap);

            // 在完整图片下面加载背景图片
            // 根据指定图片的相对路径创建图片对象,并添加到管理容器中
            JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));
            // 设置背景图片的坐标位置以及宽高
            background.setBounds(36, 36, 508, 560);
            // 将管理容器添加到游戏界面中
            this.getContentPane().add(background);

            // 最后刷新一下游戏界面
            this.getContentPane().repaint();
        }
    }


    /*
        监听松开键盘按键的事件:
            当按下按键松开后,会调用该方法
     */
    @Override
    public void keyReleased(KeyEvent e) {
        // 获取键盘每个按键的编号
        int keyCode = e.getKeyCode();

        // 判断玩家是否已经胜利!
        if (victory()) {
            // 是,则说明已经胜利!则直接结束当前方法!不让玩家继续进行拼图操作!
            return;
        }

        /*
            上下左右按键的编号:
                左:37、上:38、右:39、下:40
         */
        // 判断松开的按键是否为上
        if (keyCode == 38) {
            // 判断空白方块的X轴是否为3
            if (x == 3) {
                // 是,说明空白方块下方已没有图片可以向上移动了,则提示!
                System.out.println("下方已没有图片可以向上移动了");
                return; // 结束方法!
            }

            // 否,则图片向上移动
            System.out.println("图片向上移动");
            /*
                图片向上移动逻辑:
                    把空白方块下方的数字向上移动
                    x, y: 表示空白方块
                    x + 1, y: 表示空白方块下方的数字
             */
            // 将空白方块下方图片的数据 赋值给 空白方块处
            data[x][y] = data[x + 1][y];
            // 空白方块下方图片的数据赋值为0
            data[x + 1][y] = 0;
            // 将空白方块往下移动
            x++;
            // 移动后统计步数
            step++;
            // 重新加载一下所有图片
            initImages(path);
        } else if (keyCode == 40) { // 否,则判断松开的按键是否为下
            // 是,则判断空白方块的X轴是否在0位置
            if (x == 0) {
                // 是,说明空白方块的上方已没有图片可以向下移动了,则提示!
                System.out.println("上方已没有图片可以向下移动了");
                return; // 结束方法!
            }

            // 否,则图片向下移动
            System.out.println("图片向下移动");
            /*
                图片向下移动逻辑:
                    把空白方块上方的数字向下移动
                    x, y: 表示空白方块
                    x - 1, y: 表示空白方块上方的数字
             */
            // 将空白方块上方图片的数据 赋值给 空白方块处
            data[x][y] = data[x - 1][y];
            // 空白下方图片的数据赋值为0
            data[x - 1][y] = 0;
            // 将空白方块往上移动
            x--;
            // 移动后统计步数
            step++;
            // 重新加载一下所有图片
            initImages(path);
        } else if (keyCode == 37) { // 否,则判断松开的按键是否为左
            // 是,则判断空白方块的Y轴是否为3
            if (y == 3) {
                // 是,说明空白方块的右方已没有图片可以向左移动了,则提示!
                System.out.println("右方已没有图片可以向左移动了");
                return; // 结束方法!
            }

            // 否,则图片向左移动
            System.out.println("图片向左移动");
            /*
                图片向左移动逻辑:
                    把空白方块右方的数字向左移动
                    x, y: 表示空白方块
                    x, y + 1: 表示空白方块右方的数字
             */
            // 将空表方块右方图片的数据 赋值给 空白方块处
            data[x][y] = data[x][y + 1];
            // 空白右方图片的数据赋值为0
            data[x][y + 1] = 0;
            // 将空白方块往右移动
            y++;
            // 移动后统计步数
            step++;
            // 重新加载一下所有图片
            initImages(path);
        } else if (keyCode == 39) { // 否,则判断松开的按键是否为右
            // 是,则判断空白方块的Y轴是否在0位置
            if (y == 0) {
                // 是,说明空白方块的左方已没有图片可以向右移动了,则提示!
                System.out.println("左方已没有图片可以向右移动了");
                return; // 结束方法!
            }

            // 否,则图片向右移动
            System.out.println("图片向右移动");
            /*
                图片向右移动逻辑:
                    把空白方块左方的数字向右移动
                    x, y: 表示空白方块
                    x, y - 1: 表示空白方块左方的数字
             */
            // 将空表方块左方图片的数据 赋值给 空白方块处
            data[x][y] = data[x][y - 1];
            // 空白左方图片的数据赋值为0
            data[x][y - 1] = 0;
            // 将空白方块往左移动
            y--;
            // 移动后统计步数
            step++;
            // 重新加载一下所有图片
            initImages(path);
        } else if (keyCode == 65) { // 否,则判断松开的按键是否为A
            // 显示随机打乱的图片
            initImages(path);
        } else if (keyCode == 87) { // 否,则判断松开的按键是否为W
            // 是,则显示拼图完成后的效果!
            // 其实就是直接把正确顺序数据的新二维数组 赋值给 乱序数据的旧二维数组
            data = new int[][]{
                    {1, 2, 3, 4},
                    {5, 6, 7, 8},
                    {9, 10, 11, 12},
                    {13, 14, 15, 0}
            };
            // 按照二维数组的最新数据重新加载所有小图片
            initImages(path);
        }
    }
}

2、程序启动入口类
import cn.edu.gxufe.ui.GameJFrame;
import cn.edu.gxufe.ui.LoginJFrame;
import cn.edu.gxufe.ui.RegisterJFrame;

public class App {
    public static void main(String[] args) {
        // 表示程序启动的入口

        // 如果我们想要开启一个界面,就创建对应界面的对象即可!
        new GameJFrame();       // 游戏主界面
//         new LoginJFrame();      // 登录界面
//         new RegisterJFrame();   // 注册界面
    }
}

3、测试结果
  • 先玩一下

    16-JavaSE基础巩固项目:拼图小游戏_第34张图片



  • 不想玩了!直接作弊通关吧!

    16-JavaSE基础巩固项目:拼图小游戏_第35张图片



3、判断胜利

(1)需求

  • 当玩家拼图完成了!则显示一个胜利图标!

  • 例如:

    16-JavaSE基础巩固项目:拼图小游戏_第36张图片


(2)分析

  • 1、需要在初始化每张小图片之前判断旧的二维数组中的数据 是否都等于 正确顺序数据的新二维数组中的数据:
    • 是,则说明胜利了!显示胜利图标!
    • 否,则说明还没有胜利!则不显示胜利图标!
  • 2、细节:如果显示胜利图标后,将不能继续使用上下左右键进行拼图了!

(3)实现

1、游戏主界面类
package cn.edu.gxufe.ui;

import javax.swing.*;

/**
 * 游戏主界面类:
 * 继承父类:JFrame
 */
public class GameJFrame extends JFrame implements KeyListener{
    // 表示游戏相关的逻辑代码都写在这!
    
    /*
        创建二维数组:
            目的:管理数据
            加载图片的时候,会根据二位数组中的数据进行加载
     */
    int[][] data = new int[4][4];

    // 定义x、y变量,用于记录空白方块所在的XY的坐标位置
    int x = 0;
    int y = 0;

    // 创建一个成员的随机数对象,用于生成一个随机数
    Random rd = new Random();

    // 定义path变量,用于记录图片的相对路径,方便以后做更换图片、重新游戏等功能的实现
    String path = "puzzle_game\puzzleimages\girl\girl1\";

    // 定义一个正确顺序数据的二维数组
    int[][] win = {
            {1, 2, 3, 4},
            {5, 6, 7, 8},
            {9, 10, 11, 12},
            {13, 14, 15, 0}
    };


    // 定义一个计数器变量,用于记录玩家拼图移动的步数
    int step = 0;


    // c.创建功能菜单下的三个栏目:重新游戏、重新登录、关闭游戏
    // 更换图片栏目放到后面再写,因为比较复杂
    JMenuItem replayGameItem = new JMenuItem("重新游戏");
    JMenuItem replayLoginItem = new JMenuItem("重新登录");
    JMenuItem closeGameItem = new JMenuItem("关闭游戏");

    // d.创建关于我们菜单下的一个栏目:公众号
    JMenuItem accountItem = new JMenuItem("公众号");

    // 创建更换图片菜单下的两个栏目:美女、动物
    JMenuItem belleItem = new JMenuItem("美女");
    JMenuItem animalItem = new JMenuItem("动物");

    /*
        提供无参数的构造器
        需求:初始化一个宽603像素,高680像素的游戏主界面
     */
    public GameJFrame() {
        // 注:如果当前类没有以下这些方法,就会自动调用父类JFrame的
        // 初始化游戏主界面
        initJFrame();
        
        // 初始化游戏主界面里的菜单栏
        initJMenu();
        
        // 初始化数据:打乱
        initData();
        
        // 初始化图片到游戏主界面中
        initImages(path);

        // 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面
        // 由于界面默认是隐藏起来的,因此需要调用当前GameJFrame类的setVisible方法,设置显示界面
        this.setVisible(true);
    }
    
    // 初始化数据:打乱
    private void initData() {
        // 1、创建一个一维数组,用于存储一些数据
        int[] arr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};

        // 2、开始打乱
        // 遍历数组,依次获取到数组中的每个数据
        for (int i = 0; i < arr.length; i++) {
            // 循环每执行一次,就生成一个随机索引
            int rdIndex = rd.nextInt(arr.length);
            // 每遍历到一个数据,就用临时变量存储一下
            int temp = arr[i];
            // 开始交换
            // 将随机索引位置的数据 放到 当前遍历到的数据的位置
            arr[i] = arr[rdIndex];
            // 将当前遍历到的数据 放到 随机索引位置
            arr[rdIndex] = temp;
        }

        /*
            解析:
                比如打乱后的一维数组的数据:[0, 14, 8, 3, 13, 15, 6, 4, 10, 12, 7, 1, 9, 5, 11, 2]
                                  索引: 0  1   2  3  4   5   6  7  8   9   10 11 12 13 14  15
                循环第一次执行:
                    i=0,i<数组长度(16),为true,将0索引的数据:0 添加到二维数组的 i/4=0索引的一维数组的 i%4=0索引,i++
                循环第二次执行:
                    i=1,i<数组长度(16),为true,将1索引的数据:14 添加到二维数组的 i/4=0索引的一维数组的 i%4=1索引,i++
                循环第三次执行:
                    i=2,i<数组长度(16),为true,将2索引的数据:8 添加到二维数组的 i/4=0索引的一维数组的 i%4=2索引,i++
                后面的都是以此类推了!直到i<16,为false,循环结束!
         */
        // 3、遍历arr一维数组,依次得到打乱后的每个数据
        for (int i = 0; i < arr.length; i++) {
            // 判断当前数据是否为0
            if (arr[i] == 0) {
                // 是,则记录空白方块XY的坐标位置
                x = i / 4;
                y = i % 4;
            }
            // 否,则依次将打乱后的数据添加到二维数组中
            data[i / 4][i % 4] = arr[i];
        }
    }
    
    // 初始化游戏主界面
    private void initJFrame() {
        // 设置界面宽高
        // 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面
        this.setSize(603, 680);
        // 设置界面标题
        this.setTitle("奥利gei拼图单机版 v1.0");
        // 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的
        this.setAlwaysOnTop(true);
        // 设置界面打开时自动居中
        this.setLocationRelativeTo(null);
        // 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行
        // this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能
        this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        // 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件
        // 因为只有这样,添加图片的时候才不会一直处于居中位置
        this.setLayout(null);
        
        // 给整个游戏界面添加键盘监听事件
        this.addKeyListener(this);
    }
    
    // 初始化游戏主界面里的菜单栏
    private void initJMenu() {
        // a.创建一个菜单栏对象
        JMenuBar jMenuBar = new JMenuBar();

        // b.创建两个菜单栏下的菜单:功能、关于我们
        JMenu functionJMenu = new JMenu("功能");
        JMenu aboutUSJMenu = new JMenu("关于我们");

        // 创建菜单栏下的菜单:更换图片
        JMenu changeJMenu = new JMenu("更换图片");
        // 将更换图片的菜单嵌套进功能菜单中
        functionJMenu.add(changeJMenu);

        // e.将重新游戏、重新登录、关闭游戏这些栏目放到功能菜单下
        functionJMenu.add(replayGameItem);
        functionJMenu.add(replayLoginItem);
        functionJMenu.add(closeGameItem);

        // 将美女、动物这两个栏目放到更换图片菜单下
        changeJMenu.add(belleItem);
        changeJMenu.add(animalItem);

        // f.将公众号这个栏目放到关于我们菜单下
        aboutUSJMenu.add(accountItem);

        // g.将功能、关于我们这两个菜单放到菜单栏下
        jMenuBar.add(functionJMenu);
        jMenuBar.add(aboutUSJMenu);

        // h.最后将菜单栏放到设置到游戏界面中
        this.setJMenuBar(jMenuBar);
    }
    
    /*
        初始化图片到游戏主界面中
        细节:先加载的图片在上方,后加载的图片会在下方
     */
    private void initImages(String path) {
        // 先清空原本出现的所有图片
        this.getContentPane().removeAll();

        // 判断victory方法的返回结果是否为true
        if (victory()) {
            // 是,则说明玩家胜利了!显示胜利图标!
            // 根据指定图片的相对路径创建图片对象,并添加到管理容器中
            JLabel winJLabel = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\win.jpg"));
            // 设置胜利图片的坐标位置以及宽高
            winJLabel.setBounds(193, 300, 194, 75);
            // 将管理容器添加到游戏主界面中
            this.getContentPane().add(winJLabel);
        }

        // 统计步数
        // 创建一个管理容器对象,用于管理文字(步数: 0)
        JLabel stepJLabel = new JLabel("步数:" + step);
        // 设置管理容器的坐标位置
        stepJLabel.setBounds(40, 20, 400, 50);
        // 将容器添加到游戏主界面中
        this.getContentPane().add(stepJLabel);

        // 再加载图片
        /*
           外循环和内循环执行流程解析:
             外循环第一次执行:
                当 i = 0 时:i<4,为true,表示添加第一行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=1,表示添加第一行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=2,表示添加第一行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=3,表示添加第一行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=4,表示添加第一行的第四张图片
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第二次执行:
                当 i = 1 时:i<4,为true,表示添加第二行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=5,表示添加第二行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=6,表示添加第二行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=7,表示添加第二行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=8,表示添加第二行的第四张图片
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第三次执行:
                当 i = 2 时:i<4,为true,表示添加第三行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=9,表示添加第三行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=10,表示添加第三行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=11,表示添加第三行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=12,表示添加第三行的第四张图片
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第四次执行:
                当 i = 3 时:i<4,为true,表示添加第四行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=13,表示添加第四行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=14,表示添加第四行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=15,表示添加第四行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=16,表示添加第四行的第四张图片(由于文件中没有16这个命名的图片,因此会添加一个空白)
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第五次执行:
                当 i = 4 时:i<4,为false,外循环结束!
        */
        // 外循环:控制添加4行图片
        for (int i = 0; i < data.length; i++) { // Y轴:纵向
            // 内循环:控制每行添加4张图片
            for (int j = 0; j < data[i].length; j++) { // X轴:横向
                /*
                    解析:
                        比如二维数组中的数据:[{3, 9, 7, 5} {8, 10, 14, 11} {4, 2, 12, 13} {0, 15, 1, 6} ]
                   二维数组中的一维数组的索引:  0  1  2  3   0   1  2   3    0  1   2   3   0   1  2  3
                             二维数组的索引:       0             1              2              3
                      (1) 外循环执行第一次:
                        i=0, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (2) 外循环执行第二次:
                        i=1, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (3) 外循环执行第三次:
                        i=2, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (4) 外循环执行第四次:
                        i=3, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (5) 外循环执行第五次:
                        i=0, i<二维数组长度(4), 为false, 外循环结束!!
                 */
                // 接收二维数组i索引的一维数组j索引的数据
                int number = data[i][j];
                // 1、根据指定的文件相对路径创建一个ImageIcon图片对象
                ImageIcon imageIcon = new ImageIcon(path + number + ".jpg");

                // JLabel:管理容器,用于管理图片、文字
                // 2、创建一个管理容器JLabel对象,用于管理图片对象ImageIcon,
                // 并将图片对象imageIcon放到管理容器jLabel中。
                JLabel jLabel = new JLabel(imageIcon);

                // 3、指定图片位置:XY轴
                jLabel.setBounds(105 * j + 80, 105 * i + 130, 105, 105);

                // 4、每添加完一张小图片,给这张小图片添加边框
                /*
                    0: 表示让图片凸起来
                    1: 表示让图片凹下去
                 */
                jLabel.setBorder(new BevelBorder(1));

                // 5、将管理容器JLabel对象放到游戏主界面中
                // getContentPane:获取隐藏容器
                this.getContentPane().add(jLabel);
            }
        }

        // 在所有小图片都添加到界面之后,开始添加背景图片
        // 根据指定的相对路径创建背景图片对象,并添加到管理容器中
        JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));
        // 设置背景图片的坐标位置以及宽高
        background.setBounds(36, 36, 508, 560);
        // 将背景图片管理容器添加到界面中
        this.getContentPane().add(background);

        // 最后刷新一下游戏界面
        this.getContentPane().repaint();
    }
    
    // 这个可以不用管
    @Override
    public void keyTyped(KeyEvent e) {
    }


    /*
        监听按下键盘按键的事件
        细节:当按下按键不松开,会不断移动图片
     */
    @Override
    public void keyPressed(KeyEvent e) {
        // 获取键盘每个按键的编号
        int keyCode = e.getKeyCode();

        // 判断按下的按键是否为A
        if (keyCode == 65) {
            // 需要先清空原本的所有图片
            this.getContentPane().removeAll();

            // 加载完整图片
            // 根据指定图片的相对路径创建一个图片对象,并添加到管理容器中
            JLabel masterMap = new JLabel(new ImageIcon(path + "all.jpg"));
            // 设置图片的坐标位置以及宽高
            masterMap.setBounds(80, 130, 420, 420);
            // 将管理容器添加到游戏界面中
            this.getContentPane().add(masterMap);

            // 在完整图片下面加载背景图片
            // 根据指定图片的相对路径创建图片对象,并添加到管理容器中
            JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));
            // 设置背景图片的坐标位置以及宽高
            background.setBounds(36, 36, 508, 560);
            // 将管理容器添加到游戏界面中
            this.getContentPane().add(background);

            // 最后刷新一下游戏界面
            this.getContentPane().repaint();
        }
    }


    /*
        监听松开键盘按键的事件:
            当按下按键松开后,会调用该方法
     */
    @Override
    public void keyReleased(KeyEvent e) {
        // 获取键盘每个按键的编号
        int keyCode = e.getKeyCode();

        // 判断玩家是否已经胜利!
        if (victory()) {
            // 是,则说明已经胜利!则直接结束当前方法!不让玩家继续进行拼图操作!
            return;
        }

        /*
            上下左右按键的编号:
                左:37、上:38、右:39、下:40
         */
        // 判断松开的按键是否为上
        if (keyCode == 38) {
            // 判断空白方块的X轴是否为3
            if (x == 3) {
                // 是,说明空白方块下方已没有图片可以向上移动了,则提示!
                System.out.println("下方已没有图片可以向上移动了");
                return; // 结束方法!
            }

            // 否,则图片向上移动
            System.out.println("图片向上移动");
            /*
                图片向上移动逻辑:
                    把空白方块下方的数字向上移动
                    x, y: 表示空白方块
                    x + 1, y: 表示空白方块下方的数字
             */
            // 将空白方块下方图片的数据 赋值给 空白方块处
            data[x][y] = data[x + 1][y];
            // 空白方块下方图片的数据赋值为0
            data[x + 1][y] = 0;
            // 将空白方块往下移动
            x++;
            // 移动后统计步数
            step++;
            // 重新加载一下所有图片
            initImages(path);
        } else if (keyCode == 40) { // 否,则判断松开的按键是否为下
            // 是,则判断空白方块的X轴是否在0位置
            if (x == 0) {
                // 是,说明空白方块的上方已没有图片可以向下移动了,则提示!
                System.out.println("上方已没有图片可以向下移动了");
                return; // 结束方法!
            }

            // 否,则图片向下移动
            System.out.println("图片向下移动");
            /*
                图片向下移动逻辑:
                    把空白方块上方的数字向下移动
                    x, y: 表示空白方块
                    x - 1, y: 表示空白方块上方的数字
             */
            // 将空白方块上方图片的数据 赋值给 空白方块处
            data[x][y] = data[x - 1][y];
            // 空白下方图片的数据赋值为0
            data[x - 1][y] = 0;
            // 将空白方块往上移动
            x--;
            // 移动后统计步数
            step++;
            // 重新加载一下所有图片
            initImages(path);
        } else if (keyCode == 37) { // 否,则判断松开的按键是否为左
            // 是,则判断空白方块的Y轴是否为3
            if (y == 3) {
                // 是,说明空白方块的右方已没有图片可以向左移动了,则提示!
                System.out.println("右方已没有图片可以向左移动了");
                return; // 结束方法!
            }

            // 否,则图片向左移动
            System.out.println("图片向左移动");
            /*
                图片向左移动逻辑:
                    把空白方块右方的数字向左移动
                    x, y: 表示空白方块
                    x, y + 1: 表示空白方块右方的数字
             */
            // 将空表方块右方图片的数据 赋值给 空白方块处
            data[x][y] = data[x][y + 1];
            // 空白右方图片的数据赋值为0
            data[x][y + 1] = 0;
            // 将空白方块往右移动
            y++;
            // 移动后统计步数
            step++;
            // 重新加载一下所有图片
            initImages(path);
        } else if (keyCode == 39) { // 否,则判断松开的按键是否为右
            // 是,则判断空白方块的Y轴是否在0位置
            if (y == 0) {
                // 是,说明空白方块的左方已没有图片可以向右移动了,则提示!
                System.out.println("左方已没有图片可以向右移动了");
                return; // 结束方法!
            }

            // 否,则图片向右移动
            System.out.println("图片向右移动");
            /*
                图片向右移动逻辑:
                    把空白方块左方的数字向右移动
                    x, y: 表示空白方块
                    x, y - 1: 表示空白方块左方的数字
             */
            // 将空表方块左方图片的数据 赋值给 空白方块处
            data[x][y] = data[x][y - 1];
            // 空白左方图片的数据赋值为0
            data[x][y - 1] = 0;
            // 将空白方块往左移动
            y--;
            // 移动后统计步数
            step++;
            // 重新加载一下所有图片
            initImages(path);
        } else if (keyCode == 65) { // 否,则判断松开的按键是否为A
            // 显示随机打乱的图片
            initImages(path);
        } else if (keyCode == 87) { // 否,则判断松开的按键是否为W
            // 是,则显示拼图完成后的效果!
            // 其实就是直接把正确顺序数据的新二维数组 赋值给 乱序数据的旧二维数组
            data = new int[][]{
                    {1, 2, 3, 4},
                    {5, 6, 7, 8},
                    {9, 10, 11, 12},
                    {13, 14, 15, 0}
            };
            // 按照二维数组的最新数据重新加载所有小图片
            initImages(path);
        }
    }
}

2、程序启动入口类
import cn.edu.gxufe.ui.GameJFrame;
import cn.edu.gxufe.ui.LoginJFrame;
import cn.edu.gxufe.ui.RegisterJFrame;

public class App {
    public static void main(String[] args) {
        // 表示程序启动的入口

        // 如果我们想要开启一个界面,就创建对应界面的对象即可!
        new GameJFrame();       // 游戏主界面
//         new LoginJFrame();      // 登录界面
//         new RegisterJFrame();   // 注册界面
    }
}

3、测试结果

16-JavaSE基础巩固项目:拼图小游戏_第37张图片




十、统计步数、菜单业务实现

1、统计步数

(1)需求

  • 需要统计玩家一共移动了多少步,并且让数据显示在游戏主界面的左上方

    16-JavaSE基础巩固项目:拼图小游戏_第38张图片


(2)分析

  • 1、需要定义一个计数器变量,用于记录玩家移动的步数
  • 2、需要创建一个管理容器对象,用于管理文字:步数
  • 3、需要将管理容器对象添加到游戏主界面中
  • 4、需要在上下左右监听事件里让步数自增,每移动一次,自增一次

(3)实现

1、游戏主界面类
package cn.edu.gxufe.ui;

import javax.swing.*;

/**
 * 游戏主界面类:
 * 继承父类:JFrame
 */
public class GameJFrame extends JFrame implements KeyListener{
    // 表示游戏相关的逻辑代码都写在这!
    
    /*
        创建二维数组:
            目的:管理数据
            加载图片的时候,会根据二位数组中的数据进行加载
     */
    int[][] data = new int[4][4];

    // 定义x、y变量,用于记录空白方块所在的XY的坐标位置
    int x = 0;
    int y = 0;

    // 创建一个成员的随机数对象,用于生成一个随机数
    Random rd = new Random();

    // 定义path变量,用于记录图片的相对路径,方便以后做更换图片、重新游戏等功能的实现
    String path = "puzzle_game\puzzleimages\girl\girl1\";

    // 定义一个正确顺序数据的二维数组
    int[][] win = {
            {1, 2, 3, 4},
            {5, 6, 7, 8},
            {9, 10, 11, 12},
            {13, 14, 15, 0}
    };


    // 定义一个计数器变量,用于记录玩家拼图移动的步数
    int step = 0;


    // c.创建功能菜单下的三个栏目:重新游戏、重新登录、关闭游戏
    // 更换图片栏目放到后面再写,因为比较复杂
    JMenuItem replayGameItem = new JMenuItem("重新游戏");
    JMenuItem replayLoginItem = new JMenuItem("重新登录");
    JMenuItem closeGameItem = new JMenuItem("关闭游戏");

    // d.创建关于我们菜单下的一个栏目:公众号
    JMenuItem accountItem = new JMenuItem("公众号");

    // 创建更换图片菜单下的两个栏目:美女、动物
    JMenuItem belleItem = new JMenuItem("美女");
    JMenuItem animalItem = new JMenuItem("动物");

    /*
        提供无参数的构造器
        需求:初始化一个宽603像素,高680像素的游戏主界面
     */
    public GameJFrame() {
        // 注:如果当前类没有以下这些方法,就会自动调用父类JFrame的
        // 初始化游戏主界面
        initJFrame();
        
        // 初始化游戏主界面里的菜单栏
        initJMenu();
        
        // 初始化数据:打乱
        initData();
        
        // 初始化图片到游戏主界面中
        initImages(path);

        // 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面
        // 由于界面默认是隐藏起来的,因此需要调用当前GameJFrame类的setVisible方法,设置显示界面
        this.setVisible(true);
    }
    
    // 初始化数据:打乱
    private void initData() {
        // 1、创建一个一维数组,用于存储一些数据
        int[] arr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};

        // 2、开始打乱
        // 遍历数组,依次获取到数组中的每个数据
        for (int i = 0; i < arr.length; i++) {
            // 循环每执行一次,就生成一个随机索引
            int rdIndex = rd.nextInt(arr.length);
            // 每遍历到一个数据,就用临时变量存储一下
            int temp = arr[i];
            // 开始交换
            // 将随机索引位置的数据 放到 当前遍历到的数据的位置
            arr[i] = arr[rdIndex];
            // 将当前遍历到的数据 放到 随机索引位置
            arr[rdIndex] = temp;
        }

        /*
            解析:
                比如打乱后的一维数组的数据:[0, 14, 8, 3, 13, 15, 6, 4, 10, 12, 7, 1, 9, 5, 11, 2]
                                  索引: 0  1   2  3  4   5   6  7  8   9   10 11 12 13 14  15
                循环第一次执行:
                    i=0,i<数组长度(16),为true,将0索引的数据:0 添加到二维数组的 i/4=0索引的一维数组的 i%4=0索引,i++
                循环第二次执行:
                    i=1,i<数组长度(16),为true,将1索引的数据:14 添加到二维数组的 i/4=0索引的一维数组的 i%4=1索引,i++
                循环第三次执行:
                    i=2,i<数组长度(16),为true,将2索引的数据:8 添加到二维数组的 i/4=0索引的一维数组的 i%4=2索引,i++
                后面的都是以此类推了!直到i<16,为false,循环结束!
         */
        // 3、遍历arr一维数组,依次得到打乱后的每个数据
        for (int i = 0; i < arr.length; i++) {
            // 判断当前数据是否为0
            if (arr[i] == 0) {
                // 是,则记录空白方块XY的坐标位置
                x = i / 4;
                y = i % 4;
            }
            // 否,则依次将打乱后的数据添加到二维数组中
            data[i / 4][i % 4] = arr[i];
        }
    }
    
    // 初始化游戏主界面
    private void initJFrame() {
        // 设置界面宽高
        // 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面
        this.setSize(603, 680);
        // 设置界面标题
        this.setTitle("奥利gei拼图单机版 v1.0");
        // 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的
        this.setAlwaysOnTop(true);
        // 设置界面打开时自动居中
        this.setLocationRelativeTo(null);
        // 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行
        // this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能
        this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        // 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件
        // 因为只有这样,添加图片的时候才不会一直处于居中位置
        this.setLayout(null);
        
        // 给整个游戏界面添加键盘监听事件
        this.addKeyListener(this);
    }
    
    // 初始化游戏主界面里的菜单栏
    private void initJMenu() {
        // a.创建一个菜单栏对象
        JMenuBar jMenuBar = new JMenuBar();

        // b.创建两个菜单栏下的菜单:功能、关于我们
        JMenu functionJMenu = new JMenu("功能");
        JMenu aboutUSJMenu = new JMenu("关于我们");

        // 创建菜单栏下的菜单:更换图片
        JMenu changeJMenu = new JMenu("更换图片");
        // 将更换图片的菜单嵌套进功能菜单中
        functionJMenu.add(changeJMenu);

        // e.将重新游戏、重新登录、关闭游戏这些栏目放到功能菜单下
        functionJMenu.add(replayGameItem);
        functionJMenu.add(replayLoginItem);
        functionJMenu.add(closeGameItem);

        // 将美女、动物这两个栏目放到更换图片菜单下
        changeJMenu.add(belleItem);
        changeJMenu.add(animalItem);

        // f.将公众号这个栏目放到关于我们菜单下
        aboutUSJMenu.add(accountItem);

        // g.将功能、关于我们这两个菜单放到菜单栏下
        jMenuBar.add(functionJMenu);
        jMenuBar.add(aboutUSJMenu);

        // h.最后将菜单栏放到设置到游戏界面中
        this.setJMenuBar(jMenuBar);
    }
    
    /*
        初始化图片到游戏主界面中
        细节:先加载的图片在上方,后加载的图片会在下方
     */
    private void initImages(String path) {
        // 先清空原本出现的所有图片
        this.getContentPane().removeAll();

        // 判断victory方法的返回结果是否为true
        if (victory()) {
            // 是,则说明玩家胜利了!显示胜利图标!
            // 根据指定图片的相对路径创建图片对象,并添加到管理容器中
            JLabel winJLabel = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\win.jpg"));
            // 设置胜利图片的坐标位置以及宽高
            winJLabel.setBounds(193, 300, 194, 75);
            // 将管理容器添加到游戏主界面中
            this.getContentPane().add(winJLabel);
        }

        // 统计步数
        // 创建一个管理容器对象,用于管理文字(步数: 0)
        JLabel stepJLabel = new JLabel("步数:" + step);
        // 设置管理容器的坐标位置
        stepJLabel.setBounds(40, 20, 400, 50);
        // 将容器添加到游戏主界面中
        this.getContentPane().add(stepJLabel);

        // 再加载图片
        /*
           外循环和内循环执行流程解析:
             外循环第一次执行:
                当 i = 0 时:i<4,为true,表示添加第一行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=1,表示添加第一行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=2,表示添加第一行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=3,表示添加第一行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=4,表示添加第一行的第四张图片
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第二次执行:
                当 i = 1 时:i<4,为true,表示添加第二行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=5,表示添加第二行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=6,表示添加第二行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=7,表示添加第二行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=8,表示添加第二行的第四张图片
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第三次执行:
                当 i = 2 时:i<4,为true,表示添加第三行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=9,表示添加第三行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=10,表示添加第三行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=11,表示添加第三行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=12,表示添加第三行的第四张图片
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第四次执行:
                当 i = 3 时:i<4,为true,表示添加第四行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=13,表示添加第四行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=14,表示添加第四行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=15,表示添加第四行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=16,表示添加第四行的第四张图片(由于文件中没有16这个命名的图片,因此会添加一个空白)
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第五次执行:
                当 i = 4 时:i<4,为false,外循环结束!
        */
        // 外循环:控制添加4行图片
        for (int i = 0; i < data.length; i++) { // Y轴:纵向
            // 内循环:控制每行添加4张图片
            for (int j = 0; j < data[i].length; j++) { // X轴:横向
                /*
                    解析:
                        比如二维数组中的数据:[{3, 9, 7, 5} {8, 10, 14, 11} {4, 2, 12, 13} {0, 15, 1, 6} ]
                   二维数组中的一维数组的索引:  0  1  2  3   0   1  2   3    0  1   2   3   0   1  2  3
                             二维数组的索引:       0             1              2              3
                      (1) 外循环执行第一次:
                        i=0, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (2) 外循环执行第二次:
                        i=1, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (3) 外循环执行第三次:
                        i=2, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (4) 外循环执行第四次:
                        i=3, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (5) 外循环执行第五次:
                        i=0, i<二维数组长度(4), 为false, 外循环结束!!
                 */
                // 接收二维数组i索引的一维数组j索引的数据
                int number = data[i][j];
                // 1、根据指定的文件相对路径创建一个ImageIcon图片对象
                ImageIcon imageIcon = new ImageIcon(path + number + ".jpg");

                // JLabel:管理容器,用于管理图片、文字
                // 2、创建一个管理容器JLabel对象,用于管理图片对象ImageIcon,
                // 并将图片对象imageIcon放到管理容器jLabel中。
                JLabel jLabel = new JLabel(imageIcon);

                // 3、指定图片位置:XY轴
                jLabel.setBounds(105 * j + 80, 105 * i + 130, 105, 105);

                // 4、每添加完一张小图片,给这张小图片添加边框
                /*
                    0: 表示让图片凸起来
                    1: 表示让图片凹下去
                 */
                jLabel.setBorder(new BevelBorder(1));

                // 5、将管理容器JLabel对象放到游戏主界面中
                // getContentPane:获取隐藏容器
                this.getContentPane().add(jLabel);
            }
        }

        // 在所有小图片都添加到界面之后,开始添加背景图片
        // 根据指定的相对路径创建背景图片对象,并添加到管理容器中
        JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));
        // 设置背景图片的坐标位置以及宽高
        background.setBounds(36, 36, 508, 560);
        // 将背景图片管理容器添加到界面中
        this.getContentPane().add(background);

        // 最后刷新一下游戏界面
        this.getContentPane().repaint();
    }
    
    // 这个可以不用管
    @Override
    public void keyTyped(KeyEvent e) {
    }


    /*
        监听按下键盘按键的事件
        细节:当按下按键不松开,会不断移动图片
     */
    @Override
    public void keyPressed(KeyEvent e) {
        // 获取键盘每个按键的编号
        int keyCode = e.getKeyCode();

        // 判断按下的按键是否为A
        if (keyCode == 65) {
            // 需要先清空原本的所有图片
            this.getContentPane().removeAll();

            // 加载完整图片
            // 根据指定图片的相对路径创建一个图片对象,并添加到管理容器中
            JLabel masterMap = new JLabel(new ImageIcon(path + "all.jpg"));
            // 设置图片的坐标位置以及宽高
            masterMap.setBounds(80, 130, 420, 420);
            // 将管理容器添加到游戏界面中
            this.getContentPane().add(masterMap);

            // 在完整图片下面加载背景图片
            // 根据指定图片的相对路径创建图片对象,并添加到管理容器中
            JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));
            // 设置背景图片的坐标位置以及宽高
            background.setBounds(36, 36, 508, 560);
            // 将管理容器添加到游戏界面中
            this.getContentPane().add(background);

            // 最后刷新一下游戏界面
            this.getContentPane().repaint();
        }
    }


    /*
        监听松开键盘按键的事件:
            当按下按键松开后,会调用该方法
     */
    @Override
    public void keyReleased(KeyEvent e) {
        // 获取键盘每个按键的编号
        int keyCode = e.getKeyCode();

        // 判断玩家是否已经胜利!
        if (victory()) {
            // 是,则说明已经胜利!则直接结束当前方法!不让玩家继续进行拼图操作!
            return;
        }

        /*
            上下左右按键的编号:
                左:37、上:38、右:39、下:40
         */
        // 判断松开的按键是否为上
        if (keyCode == 38) {
            // 判断空白方块的X轴是否为3
            if (x == 3) {
                // 是,说明空白方块下方已没有图片可以向上移动了,则提示!
                System.out.println("下方已没有图片可以向上移动了");
                return; // 结束方法!
            }

            // 否,则图片向上移动
            System.out.println("图片向上移动");
            /*
                图片向上移动逻辑:
                    把空白方块下方的数字向上移动
                    x, y: 表示空白方块
                    x + 1, y: 表示空白方块下方的数字
             */
            // 将空白方块下方图片的数据 赋值给 空白方块处
            data[x][y] = data[x + 1][y];
            // 空白方块下方图片的数据赋值为0
            data[x + 1][y] = 0;
            // 将空白方块往下移动
            x++;
            // 移动后统计步数
            step++;
            // 重新加载一下所有图片
            initImages(path);
        } else if (keyCode == 40) { // 否,则判断松开的按键是否为下
            // 是,则判断空白方块的X轴是否在0位置
            if (x == 0) {
                // 是,说明空白方块的上方已没有图片可以向下移动了,则提示!
                System.out.println("上方已没有图片可以向下移动了");
                return; // 结束方法!
            }

            // 否,则图片向下移动
            System.out.println("图片向下移动");
            /*
                图片向下移动逻辑:
                    把空白方块上方的数字向下移动
                    x, y: 表示空白方块
                    x - 1, y: 表示空白方块上方的数字
             */
            // 将空白方块上方图片的数据 赋值给 空白方块处
            data[x][y] = data[x - 1][y];
            // 空白下方图片的数据赋值为0
            data[x - 1][y] = 0;
            // 将空白方块往上移动
            x--;
            // 移动后统计步数
            step++;
            // 重新加载一下所有图片
            initImages(path);
        } else if (keyCode == 37) { // 否,则判断松开的按键是否为左
            // 是,则判断空白方块的Y轴是否为3
            if (y == 3) {
                // 是,说明空白方块的右方已没有图片可以向左移动了,则提示!
                System.out.println("右方已没有图片可以向左移动了");
                return; // 结束方法!
            }

            // 否,则图片向左移动
            System.out.println("图片向左移动");
            /*
                图片向左移动逻辑:
                    把空白方块右方的数字向左移动
                    x, y: 表示空白方块
                    x, y + 1: 表示空白方块右方的数字
             */
            // 将空表方块右方图片的数据 赋值给 空白方块处
            data[x][y] = data[x][y + 1];
            // 空白右方图片的数据赋值为0
            data[x][y + 1] = 0;
            // 将空白方块往右移动
            y++;
            // 移动后统计步数
            step++;
            // 重新加载一下所有图片
            initImages(path);
        } else if (keyCode == 39) { // 否,则判断松开的按键是否为右
            // 是,则判断空白方块的Y轴是否在0位置
            if (y == 0) {
                // 是,说明空白方块的左方已没有图片可以向右移动了,则提示!
                System.out.println("左方已没有图片可以向右移动了");
                return; // 结束方法!
            }

            // 否,则图片向右移动
            System.out.println("图片向右移动");
            /*
                图片向右移动逻辑:
                    把空白方块左方的数字向右移动
                    x, y: 表示空白方块
                    x, y - 1: 表示空白方块左方的数字
             */
            // 将空表方块左方图片的数据 赋值给 空白方块处
            data[x][y] = data[x][y - 1];
            // 空白左方图片的数据赋值为0
            data[x][y - 1] = 0;
            // 将空白方块往左移动
            y--;
            // 移动后统计步数
            step++;
            // 重新加载一下所有图片
            initImages(path);
        } else if (keyCode == 65) { // 否,则判断松开的按键是否为A
            // 显示随机打乱的图片
            initImages(path);
        } else if (keyCode == 87) { // 否,则判断松开的按键是否为W
            // 是,则显示拼图完成后的效果!
            // 其实就是直接把正确顺序数据的新二维数组 赋值给 乱序数据的旧二维数组
            data = new int[][]{
                    {1, 2, 3, 4},
                    {5, 6, 7, 8},
                    {9, 10, 11, 12},
                    {13, 14, 15, 0}
            };
            // 按照二维数组的最新数据重新加载所有小图片
            initImages(path);
        }
    }
}

2、程序启动入口类
import cn.edu.gxufe.ui.GameJFrame;
import cn.edu.gxufe.ui.LoginJFrame;
import cn.edu.gxufe.ui.RegisterJFrame;

public class App {
    public static void main(String[] args) {
        // 表示程序启动的入口

        // 如果我们想要开启一个界面,就创建对应界面的对象即可!
        new GameJFrame();       // 游戏主界面
//         new LoginJFrame();      // 登录界面
//         new RegisterJFrame();   // 注册界面
    }
}

3、测试结果

16-JavaSE基础巩固项目:拼图小游戏_第39张图片




2、菜单业务实现

(1)需求

  • 当玩家用鼠标单击重新游戏的时候,重新开始游戏。
  • 当玩家用鼠标单击重新登录的时候,关闭游戏主界面,去到登录界面。
  • 当玩家用鼠标单击关闭游戏的时候,游戏主界面就关闭。
  • 当玩家用鼠标单击关于我们的时候,弹出一个彩蛋。

(2)分析

  • 1、先给重新游戏、重新登录、关闭游戏、关于我们的条目绑定动作监听事件
  • 2、根据当前单击的条目进行相应的逻辑操作:
    • 重新游戏
      • 第一步肯定是先将步数清零。
      • 第二步将二维数组中的数据重新打乱。
      • 第三步按照打乱后的数据重新加载所有小图片。
    • 重新登录
      • 第一步是先关闭当前游戏主界面。
      • 第二步是构造登录界面。
    • 关闭游戏
      • 直接结束JVM虚拟机运行。
    • 关于我们
      • 第一步创建对话窗对象
      • 第二步根据相对路径创建图片对象,并添加到管理容器中,并设置图片的坐标和宽高
      • 第三步将管理容器添加到对话窗中
      • 第四步设置对话窗的大小
      • 第五步将对话窗置顶
      • 第六步让对话窗居中
      • 第七步设置对话窗不关闭则无法操作下面的界面
      • 第八步设置对话窗显示出来,因为默认是隐藏的。

(3)实现

1、游戏主界面类
package cn.edu.gxufe.ui;

import javax.swing.*;

/**
 * 游戏主界面类:
 * 继承父类:JFrame
 */
public class GameJFrame extends JFrame implements KeyListener, ActionListener{
    // 表示游戏相关的逻辑代码都写在这!
    
    /*
        创建二维数组:
            目的:管理数据
            加载图片的时候,会根据二位数组中的数据进行加载
     */
    int[][] data = new int[4][4];

    // 定义x、y变量,用于记录空白方块所在的XY的坐标位置
    int x = 0;
    int y = 0;

    // 创建一个成员的随机数对象,用于生成一个随机数
    Random rd = new Random();

    // 定义path变量,用于记录图片的相对路径,方便以后做更换图片、重新游戏等功能的实现
    String path = "puzzle_game\puzzleimages\girl\girl1\";

    // 定义一个正确顺序数据的二维数组
    int[][] win = {
            {1, 2, 3, 4},
            {5, 6, 7, 8},
            {9, 10, 11, 12},
            {13, 14, 15, 0}
    };


    // 定义一个计数器变量,用于记录玩家拼图移动的步数
    int step = 0;


    // c.创建功能菜单下的三个栏目:重新游戏、重新登录、关闭游戏
    // 更换图片栏目放到后面再写,因为比较复杂
    JMenuItem replayGameItem = new JMenuItem("重新游戏");
    JMenuItem replayLoginItem = new JMenuItem("重新登录");
    JMenuItem closeGameItem = new JMenuItem("关闭游戏");

    // d.创建关于我们菜单下的一个栏目:公众号
    JMenuItem accountItem = new JMenuItem("公众号");

    // 创建更换图片菜单下的两个栏目:美女、动物
    JMenuItem belleItem = new JMenuItem("美女");
    JMenuItem animalItem = new JMenuItem("动物");

    /*
        提供无参数的构造器
        需求:初始化一个宽603像素,高680像素的游戏主界面
     */
    public GameJFrame() {
        // 注:如果当前类没有以下这些方法,就会自动调用父类JFrame的
        // 初始化游戏主界面
        initJFrame();
        
        // 初始化游戏主界面里的菜单栏
        initJMenu();
        
        // 初始化数据:打乱
        initData();
        
        // 初始化图片到游戏主界面中
        initImages(path);

        // 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面
        // 由于界面默认是隐藏起来的,因此需要调用当前GameJFrame类的setVisible方法,设置显示界面
        this.setVisible(true);
    }
    
    // 初始化数据:打乱
    private void initData() {
        // 1、创建一个一维数组,用于存储一些数据
        int[] arr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};

        // 2、开始打乱
        // 遍历数组,依次获取到数组中的每个数据
        for (int i = 0; i < arr.length; i++) {
            // 循环每执行一次,就生成一个随机索引
            int rdIndex = rd.nextInt(arr.length);
            // 每遍历到一个数据,就用临时变量存储一下
            int temp = arr[i];
            // 开始交换
            // 将随机索引位置的数据 放到 当前遍历到的数据的位置
            arr[i] = arr[rdIndex];
            // 将当前遍历到的数据 放到 随机索引位置
            arr[rdIndex] = temp;
        }

        /*
            解析:
                比如打乱后的一维数组的数据:[0, 14, 8, 3, 13, 15, 6, 4, 10, 12, 7, 1, 9, 5, 11, 2]
                                  索引: 0  1   2  3  4   5   6  7  8   9   10 11 12 13 14  15
                循环第一次执行:
                    i=0,i<数组长度(16),为true,将0索引的数据:0 添加到二维数组的 i/4=0索引的一维数组的 i%4=0索引,i++
                循环第二次执行:
                    i=1,i<数组长度(16),为true,将1索引的数据:14 添加到二维数组的 i/4=0索引的一维数组的 i%4=1索引,i++
                循环第三次执行:
                    i=2,i<数组长度(16),为true,将2索引的数据:8 添加到二维数组的 i/4=0索引的一维数组的 i%4=2索引,i++
                后面的都是以此类推了!直到i<16,为false,循环结束!
         */
        // 3、遍历arr一维数组,依次得到打乱后的每个数据
        for (int i = 0; i < arr.length; i++) {
            // 判断当前数据是否为0
            if (arr[i] == 0) {
                // 是,则记录空白方块XY的坐标位置
                x = i / 4;
                y = i % 4;
            }
            // 否,则依次将打乱后的数据添加到二维数组中
            data[i / 4][i % 4] = arr[i];
        }
    }
    
    // 初始化游戏主界面
    private void initJFrame() {
        // 设置界面宽高
        // 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面
        this.setSize(603, 680);
        // 设置界面标题
        this.setTitle("奥利gei拼图单机版 v1.0");
        // 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的
        this.setAlwaysOnTop(true);
        // 设置界面打开时自动居中
        this.setLocationRelativeTo(null);
        // 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行
        // this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能
        this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        // 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件
        // 因为只有这样,添加图片的时候才不会一直处于居中位置
        this.setLayout(null);
        
        // 给整个游戏界面添加键盘监听事件
        this.addKeyListener(this);
    }
    
    // 初始化游戏主界面里的菜单栏
    private void initJMenu() {
        // a.创建一个菜单栏对象
        JMenuBar jMenuBar = new JMenuBar();

        // b.创建两个菜单栏下的菜单:功能、关于我们
        JMenu functionJMenu = new JMenu("功能");
        JMenu aboutUSJMenu = new JMenu("关于我们");

        // 创建菜单栏下的菜单:更换图片
        JMenu changeJMenu = new JMenu("更换图片");
        // 将更换图片的菜单嵌套进功能菜单中
        functionJMenu.add(changeJMenu);

        // 给各个栏目绑定动作监听事件
        replayGameItem.addActionListener(this);     // 重新游戏
        replayLoginItem.addActionListener(this);    // 重新登录
        closeGameItem.addActionListener(this);      // 关闭游戏
        accountItem.addActionListener(this);        // 公众号
        belleItem.addActionListener(this);          // 美女
        animalItem.addActionListener(this);         // 动物

        // e.将重新游戏、重新登录、关闭游戏这些栏目放到功能菜单下
        functionJMenu.add(replayGameItem);
        functionJMenu.add(replayLoginItem);
        functionJMenu.add(closeGameItem);

        // 将美女、动物这两个栏目放到更换图片菜单下
        changeJMenu.add(belleItem);
        changeJMenu.add(animalItem);

        // f.将公众号这个栏目放到关于我们菜单下
        aboutUSJMenu.add(accountItem);

        // g.将功能、关于我们这两个菜单放到菜单栏下
        jMenuBar.add(functionJMenu);
        jMenuBar.add(aboutUSJMenu);

        // h.最后将菜单栏放到设置到游戏界面中
        this.setJMenuBar(jMenuBar);
    }
    
    /*
        初始化图片到游戏主界面中
        细节:先加载的图片在上方,后加载的图片会在下方
     */
    private void initImages(String path) {
        // 先清空原本出现的所有图片
        this.getContentPane().removeAll();

        // 判断victory方法的返回结果是否为true
        if (victory()) {
            // 是,则说明玩家胜利了!显示胜利图标!
            // 根据指定图片的相对路径创建图片对象,并添加到管理容器中
            JLabel winJLabel = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\win.jpg"));
            // 设置胜利图片的坐标位置以及宽高
            winJLabel.setBounds(193, 300, 194, 75);
            // 将管理容器添加到游戏主界面中
            this.getContentPane().add(winJLabel);
        }

        // 统计步数
        // 创建一个管理容器对象,用于管理文字(步数: 0)
        JLabel stepJLabel = new JLabel("步数:" + step);
        // 设置管理容器的坐标位置
        stepJLabel.setBounds(40, 20, 400, 50);
        // 将容器添加到游戏主界面中
        this.getContentPane().add(stepJLabel);

        // 再加载图片
        /*
           外循环和内循环执行流程解析:
             外循环第一次执行:
                当 i = 0 时:i<4,为true,表示添加第一行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=1,表示添加第一行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=2,表示添加第一行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=3,表示添加第一行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=4,表示添加第一行的第四张图片
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第二次执行:
                当 i = 1 时:i<4,为true,表示添加第二行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=5,表示添加第二行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=6,表示添加第二行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=7,表示添加第二行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=8,表示添加第二行的第四张图片
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第三次执行:
                当 i = 2 时:i<4,为true,表示添加第三行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=9,表示添加第三行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=10,表示添加第三行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=11,表示添加第三行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=12,表示添加第三行的第四张图片
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第四次执行:
                当 i = 3 时:i<4,为true,表示添加第四行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=13,表示添加第四行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=14,表示添加第四行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=15,表示添加第四行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=16,表示添加第四行的第四张图片(由于文件中没有16这个命名的图片,因此会添加一个空白)
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第五次执行:
                当 i = 4 时:i<4,为false,外循环结束!
        */
        // 外循环:控制添加4行图片
        for (int i = 0; i < data.length; i++) { // Y轴:纵向
            // 内循环:控制每行添加4张图片
            for (int j = 0; j < data[i].length; j++) { // X轴:横向
                /*
                    解析:
                        比如二维数组中的数据:[{3, 9, 7, 5} {8, 10, 14, 11} {4, 2, 12, 13} {0, 15, 1, 6} ]
                   二维数组中的一维数组的索引:  0  1  2  3   0   1  2   3    0  1   2   3   0   1  2  3
                             二维数组的索引:       0             1              2              3
                      (1) 外循环执行第一次:
                        i=0, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (2) 外循环执行第二次:
                        i=1, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (3) 外循环执行第三次:
                        i=2, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (4) 外循环执行第四次:
                        i=3, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (5) 外循环执行第五次:
                        i=0, i<二维数组长度(4), 为false, 外循环结束!!
                 */
                // 接收二维数组i索引的一维数组j索引的数据
                int number = data[i][j];
                // 1、根据指定的文件相对路径创建一个ImageIcon图片对象
                ImageIcon imageIcon = new ImageIcon(path + number + ".jpg");

                // JLabel:管理容器,用于管理图片、文字
                // 2、创建一个管理容器JLabel对象,用于管理图片对象ImageIcon,
                // 并将图片对象imageIcon放到管理容器jLabel中。
                JLabel jLabel = new JLabel(imageIcon);

                // 3、指定图片位置:XY轴
                jLabel.setBounds(105 * j + 80, 105 * i + 130, 105, 105);

                // 4、每添加完一张小图片,给这张小图片添加边框
                /*
                    0: 表示让图片凸起来
                    1: 表示让图片凹下去
                 */
                jLabel.setBorder(new BevelBorder(1));

                // 5、将管理容器JLabel对象放到游戏主界面中
                // getContentPane:获取隐藏容器
                this.getContentPane().add(jLabel);
            }
        }

        // 在所有小图片都添加到界面之后,开始添加背景图片
        // 根据指定的相对路径创建背景图片对象,并添加到管理容器中
        JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));
        // 设置背景图片的坐标位置以及宽高
        background.setBounds(36, 36, 508, 560);
        // 将背景图片管理容器添加到界面中
        this.getContentPane().add(background);

        // 最后刷新一下游戏界面
        this.getContentPane().repaint();
    }
    
    // 这个可以不用管
    @Override
    public void keyTyped(KeyEvent e) {
    }


    /*
        监听按下键盘按键的事件
        细节:当按下按键不松开,会不断移动图片
     */
    @Override
    public void keyPressed(KeyEvent e) {
        // 获取键盘每个按键的编号
        int keyCode = e.getKeyCode();

        // 判断按下的按键是否为A
        if (keyCode == 65) {
            // 需要先清空原本的所有图片
            this.getContentPane().removeAll();

            // 加载完整图片
            // 根据指定图片的相对路径创建一个图片对象,并添加到管理容器中
            JLabel masterMap = new JLabel(new ImageIcon(path + "all.jpg"));
            // 设置图片的坐标位置以及宽高
            masterMap.setBounds(80, 130, 420, 420);
            // 将管理容器添加到游戏界面中
            this.getContentPane().add(masterMap);

            // 在完整图片下面加载背景图片
            // 根据指定图片的相对路径创建图片对象,并添加到管理容器中
            JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));
            // 设置背景图片的坐标位置以及宽高
            background.setBounds(36, 36, 508, 560);
            // 将管理容器添加到游戏界面中
            this.getContentPane().add(background);

            // 最后刷新一下游戏界面
            this.getContentPane().repaint();
        }
    }


    /*
        监听松开键盘按键的事件:
            当按下按键松开后,会调用该方法
     */
    @Override
    public void keyReleased(KeyEvent e) {
        // 获取键盘每个按键的编号
        int keyCode = e.getKeyCode();

        // 判断玩家是否已经胜利!
        if (victory()) {
            // 是,则说明已经胜利!则直接结束当前方法!不让玩家继续进行拼图操作!
            return;
        }

        /*
            上下左右按键的编号:
                左:37、上:38、右:39、下:40
         */
        // 判断松开的按键是否为上
        if (keyCode == 38) {
            // 判断空白方块的X轴是否为3
            if (x == 3) {
                // 是,说明空白方块下方已没有图片可以向上移动了,则提示!
                System.out.println("下方已没有图片可以向上移动了");
                return; // 结束方法!
            }

            // 否,则图片向上移动
            System.out.println("图片向上移动");
            /*
                图片向上移动逻辑:
                    把空白方块下方的数字向上移动
                    x, y: 表示空白方块
                    x + 1, y: 表示空白方块下方的数字
             */
            // 将空白方块下方图片的数据 赋值给 空白方块处
            data[x][y] = data[x + 1][y];
            // 空白方块下方图片的数据赋值为0
            data[x + 1][y] = 0;
            // 将空白方块往下移动
            x++;
            // 移动后统计步数
            step++;
            // 重新加载一下所有图片
            initImages(path);
        } else if (keyCode == 40) { // 否,则判断松开的按键是否为下
            // 是,则判断空白方块的X轴是否在0位置
            if (x == 0) {
                // 是,说明空白方块的上方已没有图片可以向下移动了,则提示!
                System.out.println("上方已没有图片可以向下移动了");
                return; // 结束方法!
            }

            // 否,则图片向下移动
            System.out.println("图片向下移动");
            /*
                图片向下移动逻辑:
                    把空白方块上方的数字向下移动
                    x, y: 表示空白方块
                    x - 1, y: 表示空白方块上方的数字
             */
            // 将空白方块上方图片的数据 赋值给 空白方块处
            data[x][y] = data[x - 1][y];
            // 空白下方图片的数据赋值为0
            data[x - 1][y] = 0;
            // 将空白方块往上移动
            x--;
            // 移动后统计步数
            step++;
            // 重新加载一下所有图片
            initImages(path);
        } else if (keyCode == 37) { // 否,则判断松开的按键是否为左
            // 是,则判断空白方块的Y轴是否为3
            if (y == 3) {
                // 是,说明空白方块的右方已没有图片可以向左移动了,则提示!
                System.out.println("右方已没有图片可以向左移动了");
                return; // 结束方法!
            }

            // 否,则图片向左移动
            System.out.println("图片向左移动");
            /*
                图片向左移动逻辑:
                    把空白方块右方的数字向左移动
                    x, y: 表示空白方块
                    x, y + 1: 表示空白方块右方的数字
             */
            // 将空表方块右方图片的数据 赋值给 空白方块处
            data[x][y] = data[x][y + 1];
            // 空白右方图片的数据赋值为0
            data[x][y + 1] = 0;
            // 将空白方块往右移动
            y++;
            // 移动后统计步数
            step++;
            // 重新加载一下所有图片
            initImages(path);
        } else if (keyCode == 39) { // 否,则判断松开的按键是否为右
            // 是,则判断空白方块的Y轴是否在0位置
            if (y == 0) {
                // 是,说明空白方块的左方已没有图片可以向右移动了,则提示!
                System.out.println("左方已没有图片可以向右移动了");
                return; // 结束方法!
            }

            // 否,则图片向右移动
            System.out.println("图片向右移动");
            /*
                图片向右移动逻辑:
                    把空白方块左方的数字向右移动
                    x, y: 表示空白方块
                    x, y - 1: 表示空白方块左方的数字
             */
            // 将空表方块左方图片的数据 赋值给 空白方块处
            data[x][y] = data[x][y - 1];
            // 空白左方图片的数据赋值为0
            data[x][y - 1] = 0;
            // 将空白方块往左移动
            y--;
            // 移动后统计步数
            step++;
            // 重新加载一下所有图片
            initImages(path);
        } else if (keyCode == 65) { // 否,则判断松开的按键是否为A
            // 显示随机打乱的图片
            initImages(path);
        } else if (keyCode == 87) { // 否,则判断松开的按键是否为W
            // 是,则显示拼图完成后的效果!
            // 其实就是直接把正确顺序数据的新二维数组 赋值给 乱序数据的旧二维数组
            data = new int[][]{
                    {1, 2, 3, 4},
                    {5, 6, 7, 8},
                    {9, 10, 11, 12},
                    {13, 14, 15, 0}
            };
            // 按照二维数组的最新数据重新加载所有小图片
            initImages(path);
        }
    }
    
    
    
    // 动作监听事件:监听鼠标左键单击、键盘空格操作
    @Override
    public void actionPerformed(ActionEvent e) {
        // 获取当前被点击的栏目对象
        Object obj = e.getSource();
        // 判断当前被点击的是否为重新游戏栏目
        if (obj == replayGameItem) {
            System.out.println("重新游戏");
            // 1、先将步数清零
            step = 0;
            // 2、重新打乱二维数组的数据
            initData();
            // 3、重新按照打乱后的数据加载所有小图片
            initImages(path);
        } else if (obj == replayLoginItem) {// 否,则判断当前被点击的是否为重新登录栏目
            System.out.println("重新登录");
            // 1、先关闭当前游戏主界面
            this.setVisible(false); // 隐藏起来
            // 2、构造登录界面
            new LoginJFrame();
        } else if (obj == closeGameItem) {// 否,则判断当前被点击的是否为关闭游戏栏目
            System.out.println("关闭游戏");
            // 直接结束JVM虚拟机运行
            System.exit(0);
        } else if (obj == accountItem) {// 否,则判断当前被点击的是否为公众号栏目
            System.out.println("公众号");
            // 1、创建对话窗对象
            JDialog jDialog = new JDialog();
            // 2、根据相对路径创建图片对象,并添加到管理容器中,并设置图片的坐标和宽高
            JLabel aboutJLabel = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\about.jpg"));
            aboutJLabel.setBounds(0, 0, 220, 220);
            // 3、将管理容器添加到对话窗中
            jDialog.getContentPane().add(aboutJLabel);
            // 4、设置对话窗的大小
            jDialog.setSize(300, 300);
            // 5、将对话窗置顶
            jDialog.setAlwaysOnTop(true);
            // 6、让对话窗居中
            jDialog.setLocationRelativeTo(null);
            // 7、设置对话窗不关闭则无法操作下面的界面
            jDialog.setModal(true);
            // 8、让对话窗显示出来,因为默认是隐藏的
            jDialog.setVisible(true);
        } else if (obj == belleItem) {  // 否,则判断当前被点击的是否为美女栏目
            // 是,则随机更换美女图片中的一张
            System.out.println("更换美女图片");
            // 随机一个1~2之间的数
            int rdNumber = rd.nextInt(2) + 1;
            // 将path的值改为美女图片所在的相对路径
            path = "puzzle_game\puzzleimages\girl\girl" + rdNumber + "\";
            // 重新打乱二维数组中的数据
            initData();
            // 根据美女图片的相对路径,随机初始化一张美女图片
            initImages(path);
        } else if (obj == animalItem) { // 否,则判断当前被点击的是否为动物栏目
            // 是,则随机更换动物图片中的一张
            System.out.println("更换动物图片");
            // 随机一个1~2之间的数
            int rdNumber = rd.nextInt(2) + 1;
            // 将path的值改为美女图片所在的相对路径
            path = "puzzle_game\puzzleimages\animal\animal" + rdNumber + "\";
            // 重新打乱二维数组中的数据
            initData();
            // 根据动物图片的相对路径,随机初始化一张动物图片
            initImages(path);
        }
    }
}

2、程序启动入口类
import cn.edu.gxufe.ui.GameJFrame;
import cn.edu.gxufe.ui.LoginJFrame;
import cn.edu.gxufe.ui.RegisterJFrame;

public class App {
    public static void main(String[] args) {
        // 表示程序启动的入口

        // 如果我们想要开启一个界面,就创建对应界面的对象即可!
        new GameJFrame();       // 游戏主界面
//         new LoginJFrame();      // 登录界面
//         new RegisterJFrame();   // 注册界面
    }
}

3、测试结果
  • 重新游戏:

    16-JavaSE基础巩固项目:拼图小游戏_第40张图片

    16-JavaSE基础巩固项目:拼图小游戏_第41张图片

    16-JavaSE基础巩固项目:拼图小游戏_第42张图片



  • 重新登录

    16-JavaSE基础巩固项目:拼图小游戏_第43张图片



  • 关闭游戏

    16-JavaSE基础巩固项目:拼图小游戏_第44张图片

    16-JavaSE基础巩固项目:拼图小游戏_第45张图片



  • 公众号

    16-JavaSE基础巩固项目:拼图小游戏_第46张图片




十一、更换图片

1、需求

16-JavaSE基础巩固项目:拼图小游戏_第47张图片

  • 需要在菜单栏下的功能菜单下添加一个更换图片的菜单,更换图片的样式有:美女、动物

2、分析

(1)所用技术

  • 菜单栏是:JMenuBar
  • 菜单栏下的菜单是:JMenu
  • 菜单下的栏目是:JMenuItem
  • 动作监听:ActionListener
    • 监听鼠标左键单击、键盘空格的操作

(2)思路分析

  • 功能、关于我们、更换图片,属于菜单栏下的JMenu菜单对象。
  • 更换图片属于嵌套在功能菜单里的菜单对象。
  • 重新游戏、重新登录、关闭游戏、美女、动物,属于菜单下的JMenuItem栏目对象。
  • 1、创建更换图片的JMenu菜单对象 嵌套到 功能菜单中
  • 2、创建美女、动物的JMenuItem栏目对象,并添加到更换图片的菜单下
  • 3、给美女、动物的栏目对象绑定动作监听事件:
    • 只要用鼠标左键单击、按下键盘空格按键,就会触发更换图片的事件。
  • 4、判断当前点击的是否为美女:
    • 是:
      • 则将path的路径改为美女图片的相对路径,然后随机一张美女图片
      • 然后重新打乱二维数组中的数据,最后重新加载所有小图片即可。
    • 否:
      • 则判断当前点击的是否为动物:
        • 是:则将path的路径改为动物图片的相对路径,然后随机一张动物图片
        • 然后重新打乱二维数组中的数据,最后重新加载所有小图片即可。
  • 细节1:选择更换完毕之后,游戏界面中需要加载所有的小图片并且打乱顺序。
  • 细节2:按A、点击重新游戏的时候,显示的是更换之后的图片。

(3)实现

1、游戏主界面类
package cn.edu.gxufe.ui;

import javax.swing.*;
import javax.swing.border.BevelBorder;
import java.awt.event.*;
import java.util.Random;

/**
 * 游戏主界面类:
 * 继承父类:JFrame
 */
public class GameJFrame extends JFrame implements KeyListener, ActionListener {
    // 表示游戏相关的逻辑代码都写在这!

    /*
        创建二维数组:
            目的:管理数据
            加载图片的时候,会根据二位数组中的数据进行加载
     */
    int[][] data = new int[4][4];

    // 定义x、y变量,用于记录空白方块所在的XY的坐标位置
    int x = 0;
    int y = 0;

    // 创建一个成员的随机数对象,用于生成一个随机数
    Random rd = new Random();

    // 定义path变量,用于记录图片的相对路径,方便以后做更换图片、重新游戏等功能的实现
    String path = "puzzle_game\puzzleimages\girl\girl1\";

    // 定义一个正确顺序数据的二维数组
    int[][] win = {
            {1, 2, 3, 4},
            {5, 6, 7, 8},
            {9, 10, 11, 12},
            {13, 14, 15, 0}
    };


    // 定义一个计数器变量,用于记录玩家拼图移动的步数
    int step = 0;


    // c.创建功能菜单下的三个栏目:重新游戏、重新登录、关闭游戏
    // 更换图片栏目放到后面再写,因为比较复杂
    JMenuItem replayGameItem = new JMenuItem("重新游戏");
    JMenuItem replayLoginItem = new JMenuItem("重新登录");
    JMenuItem closeGameItem = new JMenuItem("关闭游戏");

    // d.创建关于我们菜单下的一个栏目:公众号
    JMenuItem accountItem = new JMenuItem("公众号");

    // 创建更换图片菜单下的两个栏目:美女、动物
    JMenuItem belleItem = new JMenuItem("美女");
    JMenuItem animalItem = new JMenuItem("动物");


    /*
        提供无参数的构造器
        需求:初始化一个宽603像素,高680像素的游戏主界面
     */
    public GameJFrame() {
        // 注:如果当前类没有以下这些方法,就会自动调用父类JFrame的
        // 1、初始化游戏主界面
        initJFrame();

        // 2、初始化游戏主界面里的菜单栏
        initJMenu();

        // 3、初始化数据:打乱
        initData();

        // 4、利用打乱后的数据初始化图片到游戏主界面中
        initImages(path);

        // 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面
        // 由于界面默认是隐藏起来的,因此需要调用当前GameJFrame类的setVisible方法,设置显示界面
        this.setVisible(true);
    }


    // 初始化数据:打乱
    private void initData() {
        // 1、创建一个一维数组,用于存储一些数据
        int[] arr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};

        // 2、开始打乱
        // 遍历数组,依次获取到数组中的每个数据
        for (int i = 0; i < arr.length; i++) {
            // 循环每执行一次,就生成一个随机索引
            int rdIndex = rd.nextInt(arr.length);
            // 每遍历到一个数据,就用临时变量存储一下
            int temp = arr[i];
            // 开始交换
            // 将随机索引位置的数据 放到 当前遍历到的数据的位置
            arr[i] = arr[rdIndex];
            // 将当前遍历到的数据 放到 随机索引位置
            arr[rdIndex] = temp;
        }

        /*
            解析:
                比如打乱后的一维数组的数据:[0, 14, 8, 3, 13, 15, 6, 4, 10, 12, 7, 1, 9, 5, 11, 2]
                                  索引: 0  1   2  3  4   5   6  7  8   9   10 11 12 13 14  15
                循环第一次执行:
                    i=0,i<数组长度(16),为true,将0索引的数据:0 添加到二维数组的 i/4=0索引的一维数组的 i%4=0索引,i++
                循环第二次执行:
                    i=1,i<数组长度(16),为true,将1索引的数据:14 添加到二维数组的 i/4=0索引的一维数组的 i%4=1索引,i++
                循环第三次执行:
                    i=2,i<数组长度(16),为true,将2索引的数据:8 添加到二维数组的 i/4=0索引的一维数组的 i%4=2索引,i++
                后面的都是以此类推了!直到i<16,为false,循环结束!
         */
        // 3、遍历arr一维数组,依次得到打乱后的每个数据
        for (int i = 0; i < arr.length; i++) {
            // 判断当前数据是否为0
            /*if (arr[i] == 0) {
                // 是,则记录空白方块XY的坐标位置
                x = i / 4;
                y = i % 4;
            } else {
                // 否,则依次将打乱后的数据添加到二维数组中
                data[i / 4][i % 4] = arr[i];
            }*/

            // 判断当前数据是否为0
            if (arr[i] == 0) {
                // 是,则记录空白方块XY的坐标位置
                x = i / 4;
                y = i % 4;
            }
            // 否,则依次将打乱后的数据添加到二维数组中
            data[i / 4][i % 4] = arr[i];
        }
    }


    /*
        初始化图片到游戏主界面中
        细节:先加载的图片在上方,后加载的图片会在下方
     */
    private void initImages(String path) {
        // 先清空原本出现的所有图片
        this.getContentPane().removeAll();

        // 判断victory方法的返回结果是否为true
        if (victory()) {
            // 是,则说明玩家胜利了!显示胜利图标!
            // 根据指定图片的相对路径创建图片对象,并添加到管理容器中
            JLabel winJLabel = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\win.jpg"));
            // 设置胜利图片的坐标位置以及宽高
            winJLabel.setBounds(193, 300, 194, 75);
            // 将管理容器添加到游戏主界面中
            this.getContentPane().add(winJLabel);
        }

        // 统计步数
        // 创建一个管理容器对象,用于管理文字(步数: 0)
        JLabel stepJLabel = new JLabel("步数:" + step);
        // 设置管理容器的坐标位置
        stepJLabel.setBounds(40, 20, 400, 50);
        // 将容器添加到游戏主界面中
        this.getContentPane().add(stepJLabel);

        // 再加载图片
        /*
           外循环和内循环执行流程解析:
             外循环第一次执行:
                当 i = 0 时:i<4,为true,表示添加第一行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=1,表示添加第一行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=2,表示添加第一行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=3,表示添加第一行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=4,表示添加第一行的第四张图片
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第二次执行:
                当 i = 1 时:i<4,为true,表示添加第二行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=5,表示添加第二行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=6,表示添加第二行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=7,表示添加第二行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=8,表示添加第二行的第四张图片
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第三次执行:
                当 i = 2 时:i<4,为true,表示添加第三行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=9,表示添加第三行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=10,表示添加第三行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=11,表示添加第三行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=12,表示添加第三行的第四张图片
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第四次执行:
                当 i = 3 时:i<4,为true,表示添加第四行的四张图片:
                    当 j = 0 时:j<4,为true,此时number=13,表示添加第四行的第一张图片
                    当 j = 1 时:j<4,为true,此时number=14,表示添加第四行的第二张图片
                    当 j = 2 时:j<4,为true,此时number=15,表示添加第四行的第三张图片
                    当 j = 3 时:j<4,为true,此时number=16,表示添加第四行的第四张图片(由于文件中没有16这个命名的图片,因此会添加一个空白)
                    当 j = 4 时:j<4,为false,内循环结束!
             外循环第五次执行:
                当 i = 4 时:i<4,为false,外循环结束!
        */
        // 外循环:控制添加4行图片
        for (int i = 0; i < data.length; i++) { // Y轴:纵向
            // 内循环:控制每行添加4张图片
            for (int j = 0; j < data[i].length; j++) { // X轴:横向
                /*
                    解析:
                        比如二维数组中的数据:[{3, 9, 7, 5} {8, 10, 14, 11} {4, 2, 12, 13} {0, 15, 1, 6} ]
                   二维数组中的一维数组的索引:  0  1  2  3   0   1  2   3    0  1   2   3   0   1  2  3
                             二维数组的索引:       0             1              2              3
                      (1) 外循环执行第一次:
                        i=0, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (2) 外循环执行第二次:
                        i=1, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (3) 外循环执行第三次:
                        i=2, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (4) 外循环执行第四次:
                        i=3, i<二维数组长度(4), 为true, 进入内循环:
                            a.内循环执行第一次
                                j=0, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][0], j++
                            b.内循环执行第二次
                                j=1, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][1], j++
                            c.内循环执行第三次
                                j=2, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][2], j++
                            d.内循环执行第四次
                                j=3, j<一维数组长度(4), 为true, 此时data[i][j] == data[0][3], j++
                            e.内循环执行第五次
                                j=4, j<一维数组长度(4), 为false,内循环结束
                      (5) 外循环执行第五次:
                        i=0, i<二维数组长度(4), 为false, 外循环结束!!
                 */
                // 接收二维数组i索引的一维数组j索引的数据
                int number = data[i][j];
                // 1、根据指定的文件相对路径创建一个ImageIcon图片对象
                ImageIcon imageIcon = new ImageIcon(path + number + ".jpg");

                // JLabel:管理容器,用于管理图片、文字
                // 2、创建一个管理容器JLabel对象,用于管理图片对象ImageIcon,
                // 并将图片对象imageIcon放到管理容器jLabel中。
                JLabel jLabel = new JLabel(imageIcon);

                // 3、指定图片位置:XY轴
                jLabel.setBounds(105 * j + 80, 105 * i + 130, 105, 105);

                // 4、每添加完一张小图片,给这张小图片添加边框
                /*
                    0: 表示让图片凸起来
                    1: 表示让图片凹下去
                 */
                jLabel.setBorder(new BevelBorder(1));

                // 5、将管理容器JLabel对象放到游戏主界面中
                // getContentPane:获取隐藏容器
                this.getContentPane().add(jLabel);
            }
        }

        // 在所有小图片都添加到界面之后,开始添加背景图片
        // 根据指定的相对路径创建背景图片对象,并添加到管理容器中
        JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));
        // 设置背景图片的坐标位置以及宽高
        background.setBounds(36, 36, 508, 560);
        // 将背景图片管理容器添加到界面中
        this.getContentPane().add(background);

        // 最后刷新一下游戏界面
        this.getContentPane().repaint();
    }


    /**
     * 判断玩家是否胜利!
     *
     * @return 都等于返回true,否则返回false
     */
    private boolean victory() {
        // 外循环:遍历二维数组,依次得到二维数组中的每个一维数组
        for (int i = 0; i < data.length; i++) {
            // 内循环:遍历每个一维数组,依次得到每个一维数组中的每个数据
            for (int j = 0; j < data[i].length; j++) {
                // 判断当前二维数组中的数据 是否不等于 正确二维数组中的数据
                if (data[i][j] != win[i][j]) {
                    // 只要有一个数据不相等,则直接返回false,后面的就没必要判断了!
                    return false;
                }
            }
        }

        // 循环都结束了!说明两个二维数组的数据都相等!则返回true
        return true;
    }


    // 初始化游戏主界面里的菜单栏
    private void initJMenu() {
        // a.创建一个菜单栏对象
        JMenuBar jMenuBar = new JMenuBar();

        // b.创建两个菜单栏下的菜单:功能、关于我们
        JMenu functionJMenu = new JMenu("功能");
        JMenu aboutUSJMenu = new JMenu("关于我们");

        // 创建菜单栏下的菜单:更换图片
        JMenu changeJMenu = new JMenu("更换图片");
        // 将更换图片的菜单嵌套进功能菜单中
        functionJMenu.add(changeJMenu);

        // 给各个栏目绑定动作监听事件
        replayGameItem.addActionListener(this);     // 重新游戏
        replayLoginItem.addActionListener(this);    // 重新登录
        closeGameItem.addActionListener(this);      // 关闭游戏
        accountItem.addActionListener(this);        // 公众号
        belleItem.addActionListener(this);          // 美女
        animalItem.addActionListener(this);         // 动物

        // e.将重新游戏、重新登录、关闭游戏这些栏目放到功能菜单下
        functionJMenu.add(replayGameItem);
        functionJMenu.add(replayLoginItem);
        functionJMenu.add(closeGameItem);

        // 将美女、动物这两个栏目放到更换图片菜单下
        changeJMenu.add(belleItem);
        changeJMenu.add(animalItem);

        // f.将公众号这个栏目放到关于我们菜单下
        aboutUSJMenu.add(accountItem);

        // g.将功能、关于我们这两个菜单放到菜单栏下
        jMenuBar.add(functionJMenu);
        jMenuBar.add(aboutUSJMenu);

        // h.最后将菜单栏放到设置到游戏界面中
        this.setJMenuBar(jMenuBar);
    }


    // 初始化游戏主界面
    private void initJFrame() {
        // 设置界面宽高
        // 调用当前GameJFrame类的setSize方法,初始化一个宽603像素,高680像素大小的界面
        this.setSize(603, 680);
        // 设置界面标题
        this.setTitle("奥利gei拼图单机版 v1.0");
        // 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的
        this.setAlwaysOnTop(true);
        // 设置界面打开时自动居中
        this.setLocationRelativeTo(null);
        // 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行
        // this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能
        this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        // 注:取消默认的居中位置,只有取消了才会按照X、Y轴的形式添加组件
        // 因为只有这样,添加图片的时候才不会一直处于居中位置
        this.setLayout(null);

        // 给整个游戏界面添加键盘监听事件
        this.addKeyListener(this);
    }


    // 这个可以不用管
    @Override
    public void keyTyped(KeyEvent e) {
    }


    /*
        监听按下键盘按键的事件
        细节:当按下按键不松开,会不断移动图片
     */
    @Override
    public void keyPressed(KeyEvent e) {
        // 获取键盘每个按键的编号
        int keyCode = e.getKeyCode();

        // 判断按下的按键是否为A
        if (keyCode == 65) {
            // 需要先清空原本的所有图片
            this.getContentPane().removeAll();

            // 加载完整图片
            // 根据指定图片的相对路径创建一个图片对象,并添加到管理容器中
            JLabel masterMap = new JLabel(new ImageIcon(path + "all.jpg"));
            // 设置图片的坐标位置以及宽高
            masterMap.setBounds(80, 130, 420, 420);
            // 将管理容器添加到游戏界面中
            this.getContentPane().add(masterMap);

            // 在完整图片下面加载背景图片
            // 根据指定图片的相对路径创建图片对象,并添加到管理容器中
            JLabel background = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\background.jpg"));
            // 设置背景图片的坐标位置以及宽高
            background.setBounds(36, 36, 508, 560);
            // 将管理容器添加到游戏界面中
            this.getContentPane().add(background);

            // 最后刷新一下游戏界面
            this.getContentPane().repaint();
        }
    }


    /*
        监听松开键盘按键的事件:
            当按下按键松开后,会调用该方法
     */
    @Override
    public void keyReleased(KeyEvent e) {
        // 获取键盘每个按键的编号
        int keyCode = e.getKeyCode();

        // 判断玩家是否已经胜利!
        if (victory()) {
            // 是,则说明已经胜利!则直接结束当前方法!不让玩家继续进行拼图操作!
            return;
        }

        /*
            上下左右按键的编号:
                左:37、上:38、右:39、下:40
         */
        // 判断松开的按键是否为上
        if (keyCode == 38) {
            // 判断空白方块的X轴是否为3
            if (x == 3) {
                // 是,说明空白方块下方已没有图片可以向上移动了,则提示!
                System.out.println("下方已没有图片可以向上移动了");
                return; // 结束方法!
            }

            // 否,则图片向上移动
            System.out.println("图片向上移动");
            /*
                图片向上移动逻辑:
                    把空白方块下方的数字向上移动
                    x, y: 表示空白方块
                    x + 1, y: 表示空白方块下方的数字
             */
            // 将空白方块下方图片的数据 赋值给 空白方块处
            data[x][y] = data[x + 1][y];
            // 空白方块下方图片的数据赋值为0
            data[x + 1][y] = 0;
            // 将空白方块往下移动
            x++;
            // 移动后统计步数
            step++;
            // 重新加载一下所有图片
            initImages(path);
        } else if (keyCode == 40) { // 否,则判断松开的按键是否为下
            // 是,则判断空白方块的X轴是否在0位置
            if (x == 0) {
                // 是,说明空白方块的上方已没有图片可以向下移动了,则提示!
                System.out.println("上方已没有图片可以向下移动了");
                return; // 结束方法!
            }

            // 否,则图片向下移动
            System.out.println("图片向下移动");
            /*
                图片向下移动逻辑:
                    把空白方块上方的数字向下移动
                    x, y: 表示空白方块
                    x - 1, y: 表示空白方块上方的数字
             */
            // 将空白方块上方图片的数据 赋值给 空白方块处
            data[x][y] = data[x - 1][y];
            // 空白下方图片的数据赋值为0
            data[x - 1][y] = 0;
            // 将空白方块往上移动
            x--;
            // 移动后统计步数
            step++;
            // 重新加载一下所有图片
            initImages(path);
        } else if (keyCode == 37) { // 否,则判断松开的按键是否为左
            // 是,则判断空白方块的Y轴是否为3
            if (y == 3) {
                // 是,说明空白方块的右方已没有图片可以向左移动了,则提示!
                System.out.println("右方已没有图片可以向左移动了");
                return; // 结束方法!
            }

            // 否,则图片向左移动
            System.out.println("图片向左移动");
            /*
                图片向左移动逻辑:
                    把空白方块右方的数字向左移动
                    x, y: 表示空白方块
                    x, y + 1: 表示空白方块右方的数字
             */
            // 将空表方块右方图片的数据 赋值给 空白方块处
            data[x][y] = data[x][y + 1];
            // 空白右方图片的数据赋值为0
            data[x][y + 1] = 0;
            // 将空白方块往右移动
            y++;
            // 移动后统计步数
            step++;
            // 重新加载一下所有图片
            initImages(path);
        } else if (keyCode == 39) { // 否,则判断松开的按键是否为右
            // 是,则判断空白方块的Y轴是否在0位置
            if (y == 0) {
                // 是,说明空白方块的左方已没有图片可以向右移动了,则提示!
                System.out.println("左方已没有图片可以向右移动了");
                return; // 结束方法!
            }

            // 否,则图片向右移动
            System.out.println("图片向右移动");
            /*
                图片向右移动逻辑:
                    把空白方块左方的数字向右移动
                    x, y: 表示空白方块
                    x, y - 1: 表示空白方块左方的数字
             */
            // 将空表方块左方图片的数据 赋值给 空白方块处
            data[x][y] = data[x][y - 1];
            // 空白左方图片的数据赋值为0
            data[x][y - 1] = 0;
            // 将空白方块往左移动
            y--;
            // 移动后统计步数
            step++;
            // 重新加载一下所有图片
            initImages(path);
        } else if (keyCode == 65) { // 否,则判断松开的按键是否为A
            // 显示随机打乱的图片
            initImages(path);
        } else if (keyCode == 87) { // 否,则判断松开的按键是否为W
            // 是,则显示拼图完成后的效果!
            // 其实就是直接把正确顺序数据的新二维数组 赋值给 乱序数据的旧二维数组
            data = new int[][]{
                    {1, 2, 3, 4},
                    {5, 6, 7, 8},
                    {9, 10, 11, 12},
                    {13, 14, 15, 0}
            };
            // 按照二维数组的最新数据重新加载所有小图片
            initImages(path);
        }
    }


    // 动作监听事件:监听鼠标左键单击、键盘空格操作
    @Override
    public void actionPerformed(ActionEvent e) {
        // 获取当前被点击的栏目对象
        Object obj = e.getSource();
        // 判断当前被点击的是否为重新游戏栏目
        if (obj == replayGameItem) {
            System.out.println("重新游戏");
            // 1、先将步数清零
            step = 0;
            // 2、重新打乱二维数组的数据
            initData();
            // 3、重新按照打乱后的数据加载所有小图片
            initImages(path);
        } else if (obj == replayLoginItem) {// 否,则判断当前被点击的是否为重新登录栏目
            System.out.println("重新登录");
            // 1、先关闭当前游戏主界面
            this.setVisible(false); // 隐藏起来
            // 2、构造登录界面
            new LoginJFrame();
        } else if (obj == closeGameItem) {// 否,则判断当前被点击的是否为关闭游戏栏目
            System.out.println("关闭游戏");
            // 直接结束JVM虚拟机运行
            System.exit(0);
        } else if (obj == accountItem) {// 否,则判断当前被点击的是否为公众号栏目
            System.out.println("公众号");
            // 1、创建对话窗对象
            JDialog jDialog = new JDialog();
            // 2、根据相对路径创建图片对象,并添加到管理容器中,并设置图片的坐标和宽高
            JLabel aboutJLabel = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\about.jpg"));
            aboutJLabel.setBounds(0, 0, 220, 220);
            // 3、将管理容器添加到对话窗中
            jDialog.getContentPane().add(aboutJLabel);
            // 4、设置对话窗的大小
            jDialog.setSize(300, 300);
            // 5、将对话窗置顶
            jDialog.setAlwaysOnTop(true);
            // 6、让对话窗居中
            jDialog.setLocationRelativeTo(null);
            // 7、设置对话窗不关闭则无法操作下面的界面
            jDialog.setModal(true);
            // 8、让对话窗显示出来,因为默认是隐藏的
            jDialog.setVisible(true);
        } else if (obj == belleItem) {  // 否,则判断当前被点击的是否为美女栏目
            // 是,则随机更换美女图片中的一张
            System.out.println("更换美女图片");
            // 随机一个1~2之间的数
            int rdNumber = rd.nextInt(2) + 1;
            // 将path的值改为美女图片所在的相对路径
            path = "puzzle_game\puzzleimages\girl\girl" + rdNumber + "\";
            // 重新打乱二维数组中的数据
            initData();
            // 根据美女图片的相对路径,随机初始化一张美女图片
            initImages(path);
        } else if (obj == animalItem) { // 否,则判断当前被点击的是否为动物栏目
            // 是,则随机更换动物图片中的一张
            System.out.println("更换动物图片");
            // 随机一个1~2之间的数
            int rdNumber = rd.nextInt(2) + 1;
            // 将path的值改为美女图片所在的相对路径
            path = "puzzle_game\puzzleimages\animal\animal" + rdNumber + "\";
            // 重新打乱二维数组中的数据
            initData();
            // 根据动物图片的相对路径,随机初始化一张动物图片
            initImages(path);
        }
    }
}

2、程序启动入口类
import cn.edu.gxufe.ui.GameJFrame;
import cn.edu.gxufe.ui.LoginJFrame;
import cn.edu.gxufe.ui.RegisterJFrame;

public class App {
    public static void main(String[] args) {
        // 表示程序启动的入口

        // 如果我们想要开启一个界面,就创建对应界面的对象即可!
        new GameJFrame();       // 游戏主界面
//         new LoginJFrame();      // 登录界面
//         new RegisterJFrame();   // 注册界面
    }
}

3、测试结果
  • 未更换之前:

    16-JavaSE基础巩固项目:拼图小游戏_第48张图片



  • 更换图片为动物

    16-JavaSE基础巩固项目:拼图小游戏_第49张图片

    16-JavaSE基础巩固项目:拼图小游戏_第50张图片



  • 按A的时候

    16-JavaSE基础巩固项目:拼图小游戏_第51张图片



  • 点击重新游戏的时候

    16-JavaSE基础巩固项目:拼图小游戏_第52张图片

    在这里插入图片描述




十二、用户注册功能实现(先有注册才有登录)

1、需求

16-JavaSE基础巩固项目:拼图小游戏_第53张图片

  • 开发用户注册功能:
    • 1、输入注册的用户名:
      • 需要校验用户名的唯一性
    • 2、输入注册的密码:
      • 只能是字母+数字的组合
      • 需要校验两次输入的密码是否一致
    • 3、当用户用鼠标点击注册图标的时候,颜色下沉,并提示注册成功!
    • 4、当用户用鼠标点击重置图标的时候,颜色下沉,并清空输入框的内容!


2、分析

(1)所用技术

  • 字符串、String类的API调用、字符判断
  • for循环、List集合、标准JavaBean
  • JFrame界面对象、JLabel管理容器对象、JButton按钮对象、JTextField明文输入框、ImageIcon图片对象
  • MouseListener鼠标监听

(2)思路分析

界面实现思路:
  • 1、先将注册界面给制作出来:
    • 设置界面宽高、标题、置顶界面、界面打开时居中、界面的关闭模式
    • 取消界面的默认布局:这样才可以让布局按照xy轴的形式添加组件
  • 2、然后初始化信息输入框:
    • 创建管理容器对象,用于管理用户名输入框:
      • 设置管理容器的坐标及宽高
      • 创建文本输入框,用于用户输入用户名
      • 设置输入框的坐标及宽高
      • 将输入框对象添加到管理容器中
      • 将管理容器添加到注册界面中
    • 创建管理容器对象,用于管理用户密码输入框:
      • 与上面一样的操作!
    • 创建管理容器对象,用于管理用户确认密码输入框:
      • 与上面一样的操作!
    • 根据指定注册图片的相对路径创建图片对象,添加到按钮对象中:
      • 设置按钮的坐标及宽高
      • 给按钮绑定鼠标监听
      • 将按钮对象添加到注册界面中
    • 根据指定重置图片的相对路径创建图片对象,添加到按钮对象中:
      • 与上面一样的操作!
  • 3、最后:
    • 根据指定背景图片的相对路径创建图片对象,并添加到管理容器中
    • 设置管理容器的坐标及宽高
    • 将管理容器添加到注册界面中
  • 4、在本类中实现MouseListener鼠标监听接口的所有方法:
    • 然后在mousePressed按下鼠标方法中实现按下鼠标的事件监听:
      • 当用户按下注册、重置按钮时,颜色变暗!
    • 然后在mouseReleased释放鼠标方法中实现松开鼠标的事件监听:
      • 当用户松开注册按钮时,又恢复原样!

注册功能实现思路:
  • 1、先定义用户类,用于封装用户信息为一个用户对象

  • 2、定义一个集合,用于存储用户对象

  • 3、当用户输入注册的用户名时:

    • 先获取用户输入的用户名
  • 4、当用户输入注册的密码时:

    • 先获取输入的密码
  • 5、当用户确认注册的密码时:

    • 先获取输入的确认密码
  • 6、当用户按下注册按钮时:

    • 实现校验用户名唯一性的逻辑代码
    • 实现密码不能是中文的逻辑代码
    • 实现对两次密码一致性的判断
    • 将用户录入的用户名和密码封装成一个用户对象并添加进集合中
  • 7、当用户按下重置按钮时:

    • 将所有输入框的内容清空!


3、实现

(1)用户类

package cn.edu.gxufe.entity;

/**
 * 用户类
 */
public class User {
    // 定义用户属性:用户名、密码
    private String username;
    private String password;

    // 提供无参、有参构造器
    public User(){}
    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    // 提供成员变量全套的get和set方法,方便赋值和取值
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
}

(2)注册界面类

package cn.edu.gxufe.ui;

import cn.edu.gxufe.entity.User;

import javax.swing.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;

/**
 * 注册界面类:
 * 继承父类:JFrame
 */
public class RegisterJFrame extends JFrame implements MouseListener {
    // 表示注册相关的逻辑代码都写在这!

    // 1、创建管理容器:管理用户名输入框
    JLabel nameJLabel = new JLabel("注册用户名");
    // 2、创建管理容器:管理用户密码输入框
    JLabel passwordJLabel = new JLabel("注册密码");
    // 3、创建管理容器:管理用户确认密码输入框
    JLabel okPasswordJLabel = new JLabel("再次确认密码");
    // 4、根据指定注册图片的相对路径创建图片对象,并添加到按钮对象中
    JButton registerJButton = new JButton(new ImageIcon("puzzle_game\puzzleimages\register\register_button.jpg"));
    // 5、根据指定重置图片的相对路径创建图片对象,并添加到按钮对象中
    JButton resetJButton = new JButton(new ImageIcon("puzzle_game\puzzleimages\register\reset_button.jpg"));


    // 定义一个静态的List集合对象常量,用于存储用户对象
    public static final ArrayList<User> userList = new ArrayList<>();

    // b.创建文本输入框:用于用户输入用户名
    JTextField nameJText = new JTextField();
    // b.创建文本输入框:用于用户输入密码
    JTextField passwordJText = new JTextField();
    // b.创建文本输入框:用于用户输入密码
    JTextField okPasswordJText = new JTextField();

    /*
        提供无参数的构造器
        需求:初始化一个宽501像素,高437像素的登录界面
     */
    public RegisterJFrame() {
        // 注:如果当前类没有以下这些方法,就会自动调用父类JFrame的
        // 初始化注册界面
        initJFrame();

        // 初始化注册信息框
        initRegisterMess();

        // 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面
        // 由于界面默认是隐藏起来的,因此需要调用setVisible方法,设置显示界面
        this.setVisible(true);
    }


    // 初始化注册界面
    private void initJFrame() {
        // 设置界面宽高
        // 调用当前GameJFrame类的setSize方法,初始化一个宽501像素,高437像素大小的界面
        this.setSize(501, 437);

        // 设置界面标题
        this.setTitle("奥利gei拼图-注册");
        // 设置界面为置顶:用户在点击其他界面时,该界面一直是置顶的
        this.setAlwaysOnTop(true);
        // 设置界面打开时自动居中
        this.setLocationRelativeTo(null);
        // 设置界面的关闭模式:用户只需要关闭一个界面,其他界面会自动关闭,并且结束JVM虚拟机的运行
        // this.setDefaultCloseOperation(3); // 与下一行代码是一样的功能
        this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        // 取消默认布局,这样子才可以让布局按照x和y轴的形式添加组件
        this.setLayout(null);
    }


    // 初始化注册信息框
    private void initRegisterMess() {
        // a.设置管理容器的坐标位置及宽高
        nameJLabel.setBounds(90, 100, 350, 100);
        // c.设置输入框的坐标位置及宽高
        nameJText.setBounds(80, 35, 200, 30);
        nameJLabel.add(nameJText);
        // 将管理容器对象添加到当前注册界面中
        this.getContentPane().add(nameJLabel);

        // a.设置管理容器的坐标位置及宽高
        passwordJLabel.setBounds(90, 150, 350, 100);
        // c.设置输入框的坐标位置及宽高
        passwordJText.setBounds(80, 35, 200, 30);
        passwordJLabel.add(passwordJText);
        // 将管理容器对象添加到当前注册界面中
        this.getContentPane().add(passwordJLabel);

        // a.设置管理容器的坐标位置及宽高
        okPasswordJLabel.setBounds(90, 200, 350, 100);
        // c.设置输入框的坐标位置及宽高
        okPasswordJText.setBounds(80, 35, 200, 30);
        okPasswordJLabel.add(okPasswordJText);
        // 将管理容器对象添加到当前注册界面中
        this.getContentPane().add(okPasswordJLabel);

        // a.设置按钮的坐标位置及宽高
        registerJButton.setBounds(90, 300, 115, 35);
        // 给按钮对象绑定鼠标监听
        registerJButton.addMouseListener(this);
        // 将按钮对象添加到当前注册界面中
        this.getContentPane().add(registerJButton);


        // a.设置按钮的坐标位置及宽高
        resetJButton.setBounds(256, 300, 115, 35);
        // 给按钮对象绑定鼠标监听
        resetJButton.addMouseListener(this);
        // 将按钮对象添加到当前注册界面中
        this.getContentPane().add(resetJButton);


        // 最后
        // a.根据指定背景图片的相对路径创建图片对象,并添加到管理容器中
        JLabel bgJLabel = new JLabel(new ImageIcon("puzzle_game\puzzleimages\sport\register_login_background.jpg"));
        // b.设置背景图管理容器的坐标位置及宽高
        bgJLabel.setBounds(0, 0, 488, 400);
        // c.将背景图管理容器对象添加到当前注册界面中
        this.getContentPane().add(bgJLabel);
    }

    // 鼠标单击事件
    @Override
    public void mouseClicked(MouseEvent e) {}

    // 按下鼠标事件
    @Override
    public void mousePressed(MouseEvent e) {
        // 获取当前按下的按钮对象
        Object obj = e.getSource();
        // 判断当前按下的按钮是否为注册按钮
        if (obj == registerJButton) {
            // 是
            System.out.println("按下注册按钮");
            // 设置注册按钮图片
            registerJButton.setIcon(new ImageIcon("puzzle_game\puzzleimages\register\register_down.jpg"));
            // 注册用户
            registerUser();
        } else if (obj == resetJButton) { // 否,则判断当前按下的按钮是否为重置按钮
            // 是
            System.out.println("按下重置按钮");
            // 设置重置按钮图片
            resetJButton.setIcon(new ImageIcon("puzzle_game\puzzleimages\register\reset_down.jpg"));
            // 将用户名、密码、确认密码的输入框的内容都清空
            nameJText.setText("");
            passwordJText.setText("");
            okPasswordJText.setText("");
        }
    }

    // 鼠标释放事件
    @Override
    public void mouseReleased(MouseEvent e) {
        // 获取当前按下的按钮对象
        Object obj = e.getSource();
        // 判断当前按下的按钮是否为重置按钮
        if (obj == resetJButton) {
            // 是
            System.out.println("松开重置按钮");
            resetJButton.setIcon(new ImageIcon("puzzle_game\puzzleimages\register\reset_button.jpg"));
        }
    }

    // 鼠标划入事件
    @Override
    public void mouseEntered(MouseEvent e) {
    }

    // 鼠标划出事件
    @Override
    public void mouseExited(MouseEvent e) {
    }


    /**
     * 注册用户功能
     */
    private void registerUser() {
        // 1、校验注册的用户名不能为空字符串
        if (nameJText.getText().equals("")) {
            // 是空字符,创建一个对话窗,提示一下!
            createJDialog(new JLabel("注册的用户名不能为空!"));
        }

        // 2、校验用户名的唯一性
        if (!checkUsername()) {
            // 用户不存在
            // 校验密码不能是空字符串
            if (passwordJText.getText().equals("")) {
                // 是空字符,创建一个对话窗,提示一下!
                createJDialog(new JLabel("注册的密码不能为空!"));
            } else {
                // 否,则校验密码是否不为中文
                if (checkPassword()) {
                    // 是,则说明都不是中文
                    // 判断两次输入的密码是否一致
                    if (passwordJText.getText().equals(okPasswordJText.getText())) {
                        // 一致!封装用户信息为一个用户对象,并添加进集合中
                        userList.add(new User(nameJText.getText(), passwordJText.getText()));
                        // 注册成功!创建一个对话窗,提示一下!
                        createJDialog(new JLabel("注册成功!"));
                        // 注册成功后!关闭注册界面,返回登录界面
                        this.setVisible(false);
                        new LoginJFrame();
                    } else {
                        // 两次输入的密码不一致,创建一个对话窗,提示一下!
                        createJDialog(new JLabel("两次输入的密码不一致!"));
                    }
                } else {
                    // 注册的密码为中文,创建一个对话窗,提示一下!
                    createJDialog(new JLabel("注册的密码不能为中文!"));
                }
            }
        } else {
            // 用户名已存在,创建一个对话窗,提示一下!
            createJDialog(new JLabel("用户名:" + nameJText.getText() + "已注册!请您换一个名字~"));
        }
    }


    /**
     * 创建一个对话窗
     *
     * @param jLabel 管理容器对象
     */
    private void createJDialog(JLabel jLabel) {
        // 1、创建对话窗对象
        JDialog jDialog = new JDialog();
        // 2、设置管理容器的坐标及宽高
        jLabel.setBounds(0, 0, 100, 100);
        // 3、将管理容器添加到对话窗中
        jDialog.getContentPane().add(jLabel);
        // 设置对话窗的大小
        jDialog.setSize(300, 100);
        // 4、将对话窗置顶
        jDialog.setAlwaysOnTop(true);
        // 5、让对话窗居中
        jDialog.setLocationRelativeTo(null);
        // 6、设置对话窗不关闭则无法操作下面的界面
        jDialog.setModal(true);
        // 7、让对话窗显示出来,因为默认是隐藏的
        jDialog.setVisible(true);
        // 8、当用户关闭对话窗时,注册按钮恢复原样!
        registerJButton.setIcon(new ImageIcon("puzzle_game\puzzleimages\register\register_button.jpg"));
    }


    /**
     * 校验密码
     *
     * @return 校验通过返回true,否则返回false
     */
    private boolean checkPassword() {
        // 1、遍历密码字符串,依次得到每个字符
        for (int i = 0; i < passwordJText.getText().length(); i++) {
            // 判断密码的每个字符是否为中文
            char c = passwordJText.getText().charAt(i);
            if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'))) {
                // 有一个是中文,就返回false
                return false;
            }
        }

        // 循环结束,说明都不是中文,返回true
        return true;
    }

    /**
     * 校验用户名
     *
     * @return 用户名不存在返回true,否则返回false
     */
    private boolean checkUsername() {

        // 2、根据注册的用户名得到该用户对象在集合中的索引
        int index = getUserIndex(nameJText.getText());

        // 3、判断索引是否小于0
        if (index < 0) {
            // 该用户不存在!返回null
            return false;
        }

        // 用户存在
        return true;
    }


    /**
     * 根据用户名得到该用户对象在集合中的索引
     *
     * @param username 注册的用户名
     * @return 存在就返回索引,否则返回-1
     */
    public static int getUserIndex(String username) {
        // 1、遍历用户对象集合,依次得到集合中的每个用户对象
        for (int i = 0; i < userList.size(); i++) {
            // 判断当前遍历到的用户对象的用户名 是否匹配 注册的用户名
            if (userList.get(i).getUsername().equals(username)) {
                // 是,则说明匹配成功!返回该用户对象在集合中的索引
                return i;
            }
        }

        // 2、循环结束,仍然找不到与注册的用户名 匹配的用户对象,返回-1
        return -1;
    }
}

(3)程序启动入口类

import cn.edu.gxufe.ui.GameJFrame;
import cn.edu.gxufe.ui.LoginJFrame;
import cn.edu.gxufe.ui.RegisterJFrame;

public class App {
    public static void main(String[] args) {
        // 表示程序启动的入口

        // 如果我们想要开启一个界面,就创建对应界面的对象即可!
//        new GameJFrame();       // 游戏主界面
         new RegisterJFrame();   // 注册界面
//         new LoginJFrame();      // 登录界面
    }
}

(4)测试结果

  • 直接按下注册按钮:

    16-JavaSE基础巩固项目:拼图小游戏_第54张图片


    16-JavaSE基础巩固项目:拼图小游戏_第55张图片



  • 注册一个用户:

    16-JavaSE基础巩固项目:拼图小游戏_第56张图片


    16-JavaSE基础巩固项目:拼图小游戏_第57张图片



  • 当用户关掉注册成功提示框时:

    16-JavaSE基础巩固项目:拼图小游戏_第58张图片




十三、用户登录功能实现(程序启动入口)

1、需求

16-JavaSE基础巩固项目:拼图小游戏_第59张图片

  • 实现用户登陆功能:
    • 1、需要输入用户名、密码、验证码:
      • 校验用户名是否未注册!是,则弹出一个窗口提示用户注册!
      • 校验用户名、密码的正确性!
      • 校验验证码的正确性!
    • 2、点击验证码的时候可以重新生成一个验证码
    • 3、当用户按下登录按钮的时候,颜色下沉,弹出一个窗口提示登录成功!关闭弹窗后按钮恢复原本的样子!
    • 4、当用户按下注册按钮的时候,颜色下沉,并跳转到注册界面!


2、分析

(1)所用技术

  • 字符串比较、String类API调用、随机数
  • for循环、标准JavaBean、List集合
  • 图形化类:
    • JFrame界面、JDialog对话窗、JLabel管理容器、JButton按钮、ImageIcon图片
    • JTextField明文输入框、JPasswordField密文输入框
  • 事件监听:
    • MouseListener鼠标监听


(2)思路分析

登录界面初始化:
  • 1、需要创建一个LoginJFrame类,继承父类JFrame界面类
  • 2、提供无参数的构造器:
    • 设置界面的宽高、标题、置顶、打开时自动居中、关闭模式、取消默认布局(这样才可以让布局按照xy轴的形式添加组件)
    • 最后设置界面为显示的,建议写在所有程序代码的后面。

美化登录界面:
  • 1、创建一个JLabel管理容器,用于管理用户名输入框
  • 2、创建一个JTextField明文输入框,用于输入用户名,放进管理容器中,然后将管理容器放进界面中
  • 3、创建一个JLabel管理容器,用于管理密码输入框
  • 4、创建一个JPasswordField密文输入框,用于输入密码,放进管理容器中,然后将管理容器放进界面中
  • 5、先创建生成验证码的工具类,用于生成一个由4个大小写字母+1个数字的验证码
  • 6、创建一个JLabel管理容器,用于管理验证码输入框
  • 7、创建一个JTextField明文输入框,用于输入验证码,放进管理容器中,然后将管理容器放进界面中
  • 8、创建一个JLabel管理容器,用于管理显示的验证码,然后将管理容器放进界面中
  • 9、创建一个JButton按钮对象,用于管理登录按钮图片,然后将按钮对象放进界面中
  • 10、创建一个JButton按钮对象,用于管理注册按钮图片,然后将按钮对象放进界面中
  • 最后,创建一个JLabel管理容器,用于管理背景图,然后将管理容器放进界面中

登录功能实现:
  • 1、需要得到注册界面类中的List集合

  • 2、需要给显示验证码的管理容器绑定鼠标监听:

    • 在鼠标单击事件的方法中实现:
      • 1、获取当前鼠标单击的事件
      • 2、判断当前鼠标单击的事件是否为验证码管理容器:
        • 是,则重新生成新的验证码
  • 3、需要给登录按钮、注册按钮、眼睛按钮都绑定鼠标监听:

    • 在按下鼠标事件的方法中实现:
      • 1、获取当前按下的事件
      • 2、判断当前按下的事件是否为登录按钮:
          • (1)说明当前按下的是登录按钮,则让按钮变暗
          • (2)校验用户名输入框的用户名是否未注册:
            • 是,则说明该用户名未注册,弹窗提示:“用户名未注册!请先注册!”
            • 否,则说明该用户名已注册,则校验用户名和密码是否正确:
              • 是,则说明正确,则校验验证码是否正确:
                • 是,则说明验证码正确,弹窗提示:“登录成功!”,关闭弹窗后跳转到游戏界面!
                • 否,则说明验证码不正确,弹窗提示:“验证码错误!”
              • 否,则说明不正确,则弹窗提示:“您的用户名或密码有误!”
        • 否,则判断当前按下的事件是否为注册按钮:
            • (1)说明当前按下的是注册按钮,则让按钮变暗
            • (2)关闭登录界面后,跳转到注册界面中


3、实现

(1)登录界面类

package cn.edu.gxufe.ui;

import cn.edu.gxufe.entity.User;
import cn.edu.gxufe.util.VerifyCodeUtil;

import javax.swing.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;

/**
 * 登录界面类:
 * 继承父类:JFrame
 */
public class LoginJFrame extends JFrame implements MouseListener {
    // 表示登录相关的逻辑代码都写在这!

    // 需要得到注册界面类中的List集合
    public static ArrayList<User> userList = RegisterJFrame.userList;

    // 2、创建一个JTextField明文输入框,用于输入用户名
    JTextField usernameText = new JTextField();
    // 4、创建一个JPasswordField密文输入框,用于输入密码
    JPasswordField passwordText = new JPasswordField();
    // 7、创建一个JTextField明文输入框,用于输入验证码
    JTextField verifyCodeText = new JTextField();

    // 8、创建一个JLabel管理容器,用于管理显示的验证码
    JLabel displayVerifyCodeJLabel = new JLabel(VerifyCodeUtil.getVerifyCode());

    // 9、创建一个JButton按钮对象,用于管理登录按钮图片
    JButton loginButton = new JButton();
    // 10、创建一个JButton按钮对象,用于管理注册按钮图片
    JButton registerButton = new JButton();

    /*
        提供无参数的构造器
        需求:初始化一个宽501像素,高437像素的登录界面
     */
    public LoginJFrame() {
        // 初始化界面
        initJFrame();

        // 初始化登录信息框
        initLoginMess();

        // 最后设置界面为显示的:建议写在最后,因为所有设置都弄好了之后,才显示出完整的界面
        // 由于界面默认是隐藏起来的,因此需要调用setVisible方法,设置显示界面
        

你可能感兴趣的:(JavaSE基础巩固练习,java,开发语言,算法)