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的文件夹里面),也可以把它打包成一个安装程序