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

没做登录和注册界面

主函数:

1
2
3
4
5
6
7
import com.itheima.ui.GameJFrame;

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

主要逻辑 GameJFrame:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
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 打包:

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

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