Java阶段项目拼图小游戏


学的黑马的这个视频->阶段项目-10-阶段项目课后练习思路分析_哔哩哔哩_bilibili

没做登录和注册界面

主函数:

import com.itheima.ui.GameJFrame;

public class App {
    public static void main(String[] args){
        new GameJFrame();
    }
}

主要逻辑GameJFrame:

package com.itheima.ui;

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

public class GameJFrame extends JFrame implements KeyListener, ActionListener {
    Random random = new Random();
    
    // 这个数组用来表示一下图片的“位置”
    int[][] arr = new int[4][4];

    // 记录一下空白格在二维数组里面的位置
    int x = 0;
    int y = 0;

    int[][] win = {
            {1,2,3,4},{5,6,7,8},{9,10,11,12},{13,14,15,0}
    };

    String path = "/image/anime/anime3/";

    // 记录一下步数
    int step = 0;

    // 更换图片是JMenu类,作为一级条目
    JMenu changeItem = new JMenu("更换图片");

    // 创建选项里面的分支对象(四个)
    JMenuItem fumo = new JMenuItem("Fumo");
    JMenuItem anime = new JMenuItem("Anime");
    JMenuItem replayItem = new JMenuItem("重新游戏");
//    JMenuItem reloginItem = new JMenuItem("重新登录");
    JMenuItem closeItem = new JMenuItem("关闭游戏");

    JMenuItem accountItem = new JMenuItem("公众号");

    public GameJFrame() {
        // 初始化页面
        initFrame();
        // 初始化菜单
        initJMenuBar();
        // 打乱一下数组顺序,方便生成乱序的图片
        disruptArr();
        // 初始化加载图片
        initImage();

        this.setVisible(true);

    }


    private void initFrame(){
        this.setSize(680,740);

        this.setTitle("拼图单机版 v1.0");

        this.setAlwaysOnTop(true);
        // 设置默认页面居中
        this.setLocationRelativeTo(null);
        // 设置只要关闭窗口,虚拟机就自动停止
        this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        // 取消图片默认的居中放置,方便我们按照XY轴进行操作,否则图片的位置不会变化
        this.setLayout(null);
        // 给整个页面添加键盘监听事件
        this.addKeyListener(this);
    }

    private void initJMenuBar(){
        // 创建整个菜单的对象
        JMenuBar jMenuBar = new JMenuBar();

        // 创建菜单里面选项的对象(两个)
        JMenu functionMenu = new JMenu("功能");
        JMenu aboutMenu = new JMenu("关于");

        // 给菜单的条目绑定一下事件,包括一级条目和二级条目
        replayItem.addActionListener(this);
//        reloginItem.addActionListener(this);
        closeItem.addActionListener(this);
        accountItem.addActionListener(this);
        changeItem.addActionListener(this);
        anime.addActionListener(this);
        fumo.addActionListener(this);

        // 把fumo、anime等二级条目添加到一级条目里面
        changeItem.add(anime);
        changeItem.add(fumo);

        // 把它们都添加进对应的条目
        functionMenu.add(changeItem);
        functionMenu.add(replayItem);
//        functionMenu.add(reloginItem);
        functionMenu.add(closeItem);

        aboutMenu.add(accountItem);

        jMenuBar.add(functionMenu);
        jMenuBar.add(aboutMenu);

        // 把菜单添加到界面中
        this.setJMenuBar(jMenuBar);
    }

    private void initImage(){
        // 清空所有图片

        this.getContentPane().removeAll();

        if(victory()){
            JLabel winJLabel = new JLabel(new ImageIcon(GameJFrame.class.getResource("/image/win.jpg")));
            winJLabel.setBounds(153,153,300,300);
            this.getContentPane().add(winJLabel);
        }

        // 步数统计
        JLabel stepCount = new JLabel("步数" + step);
        stepCount.setBounds(50,30,100,20);
        this.getContentPane().add(stepCount);

        for (int i = 0; i < 4; i++) {
            for (int j = 0; j < 4; j++) {
                if(arr[i][j] == 0){
                    continue;
                }
                ImageIcon icon = new ImageIcon(GameJFrame.class.getResource(path + arr[i][j] + ".png"));
                JLabel jLabel = new JLabel(icon);
                // 指定图片位置
                jLabel.setBounds(170*j,170*i,170,170);
                this.getContentPane().add(jLabel);
            }
        }

        // 刷新一下页面
        this.getContentPane().repaint();
    }

    private void disruptArr(){
        int[] a = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
        for (int i = 0; i < 16; i++) {
            int index = random.nextInt(16);
            int tmp = a[index];
            a[index] = a[i];
            a[i] = tmp;
        }

        for (int i = 0; i < 4; i++) {
            for (int j = 0; j < 4; j++) {
                if(a[i*4+j] == 0){
                    x = i;
                    y = j;
                }
                arr[i][j] = a[i * 4 + j];
            }
        }
    }

    @Override
    public void keyTyped(KeyEvent e) {

    }

    @Override
    public void keyPressed(KeyEvent e) {
        int code = e.getKeyCode();
        if(code == 65){
            this.getContentPane().removeAll();
            JLabel all = new JLabel(new ImageIcon(GameJFrame.class.getResource(path + "all.jpg")));
            all.setBounds(0,0,680,740);
            this.getContentPane().add(all);
            // 刷新页面
            this.getContentPane().repaint();
        }
    }

    @Override
    public void keyReleased(KeyEvent e) {

        int code = e.getKeyCode();

        // 如果游戏胜利,应该直接结束方法,不能再移动,但要特别放行一下a操作,否则会出bug
        if(victory() && code != 65){
            return;
        }

        switch (code) {
            case 37:
                System.out.println("向左移动");
                if(y == 3){
                    return;
                }
                arr[x][y] = arr[x][y+1];
                arr[x][y+1] = 0;
                y++;
                step++;
                initImage();
                break;

            case 38:
                System.out.println("向上移动");
                if(x == 3){
                    return;
                }
                arr[x][y] = arr[x+1][y];
                arr[x+1][y] = 0;
                x++;
                step++;
                initImage();
                break;

            case 39:
                System.out.println("向右移动");
                if(y == 0){
                    return;
                }
                arr[x][y] = arr[x][y-1];
                arr[x][y-1] = 0;
                y--;
                step++;
                initImage();
                break;

            case 40:
                System.out.println("向下移动");
                if(x == 0){
                    return;
                }
                arr[x][y] = arr[x-1][y];
                arr[x-1][y] = 0;
                x--;
                step++;
                initImage();
                break;

            case 65:
                initImage();
                break;

            case 87:
                arr = new int[][]{
                        {1,2,3,4},{5,6,7,8},{9,10,11,12},{13,14,15,0}
            };
                initImage();
                break;
        }
    }

    // 判断游戏是否胜利
    public boolean victory(){
        for(int i = 0; i < arr.length; i++){
            for(int j = 0; j < arr[i].length; j++){
                if(arr[i][j] != win[i][j]){
                    return false;
                }
            }
        }
        return true;
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        Object obj = e.getSource();
        if(obj == replayItem){
            System.out.println("重新游戏");

            step = 0;
            disruptArr();
            initImage();
        }else if(obj == closeItem){
            System.out.println("关闭游戏");
            System.exit(0);

        } else if(obj == accountItem){
            System.out.println("关于我们");

            // 创建一个弹窗对象
            JDialog jDialog = new JDialog();

            // 创建一个管理图片的容器对象JLabel
            JLabel jLabel = new JLabel(new ImageIcon(GameJFrame.class.getResource("/image/account.jpg")));

            // 设置位置和宽高
            jLabel.setBounds(0,0,258,258);

            // 把图片添加到弹框里面
            jDialog.getContentPane().add(jLabel);

            // 给弹框设置大小
            jDialog.setSize(344,344);

            // 让弹框置顶展示
            jDialog.setAlwaysOnTop(true);

            // 让弹框居中
            jDialog.setLocationRelativeTo(null);

            // 弹框不关闭则无法操作底下的页面
            jDialog.setModal(true);

            // 展示弹框
            jDialog.setVisible(true);

        }else if(obj == anime){
            int index = (random.nextInt(5) + 1);
            path = "/image/anime/anime" + index + "/";
            disruptArr();
            initImage();
        }else if(obj == fumo){
            int index = (random.nextInt(1) + 1);
            path = "/image/fumo/fumo" + index + "/";
            disruptArr();
            initImage();
        }
    }
}

用构造函数作为程序入口,把程序的加载分为了三步:

1、初始化页面

2、初始化菜单

3、打乱数组顺序,然后根据数组的顺序去添加图片以达到一种“打乱”的效果

用了swingGUI库开发

根据这三步来逐一分析回顾一下

初始化页面部分

代码为initFrame部分,通过继承JFrame类达到强耦合,让我这个类本身就是一个窗口,直接用this就能调用窗口的方法,不用再new一个JFrame对象,这样写比较简洁

单独运行initFrame如下:

初始化菜单部分

JMenuBar:整个菜单栏的容器,显示在窗口顶部

JMenu:可以添加到JMenuBar里面,作为一级菜单。同时JMenu自身也可以添加到JMenu中,作为二级菜单(如更换图片->功能)

JMenuItem:作为真正的选项,绑定点击事件来触发某些功能

运行后如下:

加载&打乱图片部分

这里用数组arr表示每个小图片的摆放位置,先用disruptArr方法打乱arr数组,然后遍历被打乱的数组,用它作为索引来依次存入图片到窗口中

这里用到ImageIcon和JLabel两个swing类

JLabel:一个swing组件,可以包装文字或者图片或者文字和图片,作为显示用的载体把它们加载到JFrame窗口中

ImageIcon:是一个swing用来加载和保存图片的类,同样是一个载体,用来包装图片。可以用文件路径、URL或者getResource()加载,但它不是和JLabel一样的组件,没法直接显示,仅仅起到包装作用

基础功能

图片移动

实现KeyListener接口来监听一下键盘松开事件keyReleased(表现为按一下按键即触发)

e.getKeyCode()可以获取按下的按键,不同的按键要执行不同的操作,用switch来实现上下左右移动的逻辑

这里用全局变量x、y记录空白格的位置,利用空白格来完成移动操作,每次移动要修改arr数组的内容,然后重新用initImage方法加载图片

判断游戏是否胜利

写了一个victory方法,先用win数组储存一下正确图片的顺序,若最后arr数组与win数组相等,则返回true

这个方法的返回值要在initImage里面接收,若游戏已经胜利,则再使用JLabel组件弹出一个提示胜利的图片,同时要在keyReleased里面添加if判断,若游戏已经胜利则应该禁止再移动图片

扩展功能

查看完整图片

搭配keyPressed和keyReleased来使用,当按下a不松开的时候,keyPressed监听事件会触发,先清空当前窗口的所有图片,然后使用JLabel组件直接添加完整的原图进去

松开a的时候应该恢复原状,所以写在了keyReleased里面。当松开a的时候触发case65,把原来的图片再加载回去

一键通关

按下w,触发case87,先把arr数组摆正,然后直接initImage加载图片,这样直接通过victory校验,触发胜利逻辑

菜单功能

更换图片

该条目下有两个二级条目,当点击它们的时候会先获取一个随机数(在图片数量范围之内),然后改变一下path的值,最后调用disruptArr和initImage两个方法重新加载图片即可

重新游戏

把步数设置为0然后重开就行,一样通过disruptArr和initImage这两个方法

关闭游戏

直接exit,简单

关于&公众号

这里用到JDialog类,它是一个对话框窗口,和JFrame同级(但需要依附于JFrame存在)

打包成exe

这里我想把它像pyinstaller打包python程序那样打包,使得我写的这个小游戏在一个没有JRE的电脑上也能正常运行,具体操作如下(我的JDK为23.0.1):
先把代码编译成jar,备用

然后

jre的作用就是一个简单java虚拟机,让java程序在没有JRE的电脑上面也能运行

最后用jpackage打包:

jpackage --name PuzzleGame --input E:\edge --main-jar PuzzleGame.jar --type app-image --runtime-image E:\edge\jre

这里我直接把它打包成了一个可以直接运行的exe程序(在一个有简单JRE的文件夹里面),也可以把它打包成一个安装程序