实验概述

本次实验围绕 Java Swing 图形用户界面编程展开,核心目标是掌握 GUI 组件使用、事件源与监听器绑定、事件接口处理等知识点。实验包含两部分:

  • 必做实验4:开发面向小学生的算术计算练习软件,实现难度自定义、随机出题、答案校验、多类型运算等功能
  • 选做实验2:拓展开发飞机射击游戏,实现图形化界面、难度调节、计时计步、存档读档等完整游戏机制

📥 可执行文件下载

点击下载后,在命令行执行 java -jar 文件名.jar 即可运行。

实验四:小学生算术练习系统(必做)

1. 实验要求

  1. 支持自定义计算难度,可根据功能扩展
  2. 随机生成不同题目,通过按键/按钮触发答案校验
  3. 支持加、减、乘、除四则运算,可扩展
  4. 操作数支持整数、小数、分数等类型,可扩展
  5. 界面美观,程序可正常运行

2. 功能实现对应

实验要求 实现方案
自定义难度 5档难度递进,从10以内整数加减到真分数四则运算,切换自动刷新题目
随机出题+按键判题 每道题随机生成,支持按钮提交与键盘回车提交,即时反馈对错
四则运算 完整实现 + - ,减法保证结果非负,整数除法保证整除
多类型操作数 覆盖整数、一位小数、真分数三种类型,难度逐层提升
扩展功能 实时统计答题总数、正确数、正确率,适配小学生练习场景

3. 核心设计

  • 界面分层:顶部难度选择区、中部题目答题区、底部数据统计区
  • 事件模型:下拉框切换事件、按钮点击事件、输入框回车事件,统一绑定监听器
  • 题目生成:按难度分支生成不同类型题目,保证运算结果符合小学生认知
  • 精度处理:整数模式精确匹配,小数/分数模式允许 0.01 误差

4. 完整源代码

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
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.util.Random;

public class ArithmeticPractice extends JFrame {
// 界面组件
private JComboBox<String> difficultyBox;
private JLabel questionLabel;
private JTextField answerField;
private JButton submitBtn;
private JButton nextBtn;
private JLabel resultLabel;
private JLabel statsLabel;

// 题目与统计变量
private double correctAnswer;
private int totalCount = 0;
private int correctCount = 0;
private final Random random = new Random();

// 难度选项
private final String[] DIFFICULTIES = {
"入门:10以内整数加减法",
"基础:100以内整数加减乘",
"进阶:100以内整数四则运算",
"提高:一位小数四则运算",
"拓展:真分数四则运算"
};

public ArithmeticPractice() {
initFrame();
initComponents();
generateQuestion();
}

/**
* 初始化窗口基础属性
*/
private void initFrame() {
setTitle("小学生算术练习系统");
setSize(500, 380);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLocationRelativeTo(null);
setResizable(false);
}

/**
* 初始化所有界面组件与布局
*/
private void initComponents() {
// 主容器
JPanel mainPanel = new JPanel(new BorderLayout(10, 15));
mainPanel.setBorder(new EmptyBorder(20, 25, 20, 25));
mainPanel.setBackground(new Color(240, 245, 250));
add(mainPanel);

// 顶部:难度选择区
JPanel topPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 0));
topPanel.setOpaque(false);
JLabel diffLabel = new JLabel("选择难度:");
diffLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14));
difficultyBox = new JComboBox<>(DIFFICULTIES);
difficultyBox.setFont(new Font("微软雅黑", Font.PLAIN, 14));
difficultyBox.setPreferredSize(new Dimension(220, 28));
// 难度切换事件:切换后自动生成新题目
difficultyBox.addActionListener(e -> {
generateQuestion();
resultLabel.setText("");
});
topPanel.add(diffLabel);
topPanel.add(difficultyBox);
mainPanel.add(topPanel, BorderLayout.NORTH);

// 中部:题目与答题区
JPanel centerPanel = new JPanel();
centerPanel.setLayout(new BoxLayout(centerPanel, BoxLayout.Y_AXIS));
centerPanel.setOpaque(false);

// 题目显示
questionLabel = new JLabel("", SwingConstants.CENTER);
questionLabel.setFont(new Font("微软雅黑", Font.BOLD, 30));
questionLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
questionLabel.setBorder(new EmptyBorder(15, 0, 20, 0));

// 输入与按钮行
JPanel inputPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 12, 0));
inputPanel.setOpaque(false);
answerField = new JTextField(12);
answerField.setFont(new Font("微软雅黑", Font.PLAIN, 18));
answerField.setHorizontalAlignment(JTextField.CENTER);

submitBtn = new JButton("提交答案");
nextBtn = new JButton("下一题");
submitBtn.setFont(new Font("微软雅黑", Font.PLAIN, 15));
nextBtn.setFont(new Font("微软雅黑", Font.PLAIN, 15));
submitBtn.setPreferredSize(new Dimension(110, 32));
nextBtn.setPreferredSize(new Dimension(110, 32));
submitBtn.setBackground(new Color(90, 140, 220));
submitBtn.setForeground(Color.WHITE);
submitBtn.setFocusPainted(false);
nextBtn.setFocusPainted(false);

// 事件绑定:提交按钮、回车、下一题按钮
submitBtn.addActionListener(e -> checkAnswer());
answerField.addActionListener(e -> checkAnswer());
nextBtn.addActionListener(e -> {
generateQuestion();
resultLabel.setText("");
});

inputPanel.add(answerField);
inputPanel.add(submitBtn);
inputPanel.add(nextBtn);

// 结果提示
resultLabel = new JLabel("", SwingConstants.CENTER);
resultLabel.setFont(new Font("微软雅黑", Font.BOLD, 16));
resultLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
resultLabel.setBorder(new EmptyBorder(18, 0, 10, 0));

centerPanel.add(questionLabel);
centerPanel.add(inputPanel);
centerPanel.add(resultLabel);
mainPanel.add(centerPanel, BorderLayout.CENTER);

// 底部:统计信息
statsLabel = new JLabel("答题总数:0 正确数:0 正确率:0%", SwingConstants.CENTER);
statsLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14));
statsLabel.setForeground(new Color(70, 70, 70));
mainPanel.add(statsLabel, BorderLayout.SOUTH);
}

/**
* 根据当前难度生成随机题目
*/
private void generateQuestion() {
int diffIndex = difficultyBox.getSelectedIndex();
int num1, num2;
double num1d, num2d;
int opIndex;
char op;

switch (diffIndex) {
case 0: // 10以内整数加减
num1 = random.nextInt(10) + 1;
num2 = random.nextInt(10) + 1;
opIndex = random.nextInt(2);
if (opIndex == 1 && num1 < num2) {
int temp = num1;
num1 = num2;
num2 = temp;
}
op = opIndex == 0 ? '+' : '-';
correctAnswer = op == '+' ? num1 + num2 : num1 - num2;
questionLabel.setText(num1 + " " + op + " " + num2 + " = ?");
break;

case 1: // 100以内整数加减乘
num1 = random.nextInt(100) + 1;
num2 = random.nextInt(100) + 1;
opIndex = random.nextInt(3);
if (opIndex == 1 && num1 < num2) {
int temp = num1;
num1 = num2;
num2 = temp;
}
op = opIndex == 0 ? '+' : (opIndex == 1 ? '-' : '');
if (op == '+') correctAnswer = num1 + num2;
else if (op == '-') correctAnswer = num1 - num2;
else correctAnswer = num1 * num2;
questionLabel.setText(num1 + " " + op + " " + num2 + " = ?");
break;

case 2: // 100以内整数四则,除法保证整除
opIndex = random.nextInt(4);
if (opIndex == 3) {
num2 = random.nextInt(20) + 1;
int multiple = random.nextInt(100 / num2) + 1;
num1 = num2 * multiple;
} else {
num1 = random.nextInt(100) + 1;
num2 = random.nextInt(100) + 1;
if (opIndex == 1 && num1 < num2) {
int temp = num1;
num1 = num2;
num2 = temp;
}
}
op = opIndex == 0 ? '+' : (opIndex == 1 ? '-' : (opIndex == 2 ? '' : ''));
if (op == '+') correctAnswer = num1 + num2;
else if (op == '-') correctAnswer = num1 - num2;
else if (op == '') correctAnswer = num1 * num2;
else correctAnswer = num1 / num2;
questionLabel.setText(num1 + " " + op + " " + num2 + " = ?");
break;

case 3: // 一位小数四则运算
num1d = Math.round((random.nextDouble() * 9.9 + 0.1) * 10) / 10.0;
num2d = Math.round((random.nextDouble() * 9.9 + 0.1) * 10) / 10.0;
opIndex = random.nextInt(4);
op = opIndex == 0 ? '+' : (opIndex == 1 ? '-' : (opIndex == 2 ? '' : ''));
if (op == '-' && num1d < num2d) {
double temp = num1d;
num1d = num2d;
num2d = temp;
}
if (op == '+') correctAnswer = num1d + num2d;
else if (op == '-') correctAnswer = num1d - num2d;
else if (op == '') correctAnswer = num1d * num2d;
else correctAnswer = num1d / num2d;
questionLabel.setText(String.format("%.1f %c %.1f = ?", num1d, op, num2d));
break;

case 4: // 真分数四则运算
int a = random.nextInt(9) + 1;
int b = random.nextInt(9) + 2;
if (a >= b) a = random.nextInt(b) + 1;
int c = random.nextInt(9) + 1;
int d = random.nextInt(9) + 2;
if (c >= d) c = random.nextInt(d) + 1;

opIndex = random.nextInt(4);
op = opIndex == 0 ? '+' : (opIndex == 1 ? '-' : (opIndex == 2 ? '' : ''));

double f1 = (double) a / b;
double f2 = (double) c / d;
if (op == '-' && f1 < f2) {
int ta = a, tb = b;
a = c; b = d;
c = ta; d = tb;
f1 = (double) a / b;
f2 = (double) c / d;
}

if (op == '+') correctAnswer = f1 + f2;
else if (op == '-') correctAnswer = f1 - f2;
else if (op == '') correctAnswer = f1 * f2;
else correctAnswer = f1 / f2;
questionLabel.setText(String.format("%d/%d %c %d/%d = ?", a, b, op, c, d));
break;
}
// 清空输入框并聚焦
answerField.setText("");
answerField.requestFocus();
}

/**
* 校验用户输入的答案
*/
private void checkAnswer() {
String input = answerField.getText().trim();
if (input.isEmpty()) {
resultLabel.setText("请先输入答案");
resultLabel.setForeground(new Color(230, 160, 0));
return;
}

double userAnswer;
try {
userAnswer = Double.parseDouble(input);
} catch (NumberFormatException e) {
resultLabel.setText("请输入有效的数字");
resultLabel.setForeground(new Color(230, 160, 0));
return;
}

totalCount++;
int diffIndex = difficultyBox.getSelectedIndex();
boolean isCorrect;
// 整数模式精确匹配,小数/分数模式允许0.01误差
if (diffIndex <= 2) {
isCorrect = Math.abs(userAnswer - correctAnswer) < 1e-6;
} else {
isCorrect = Math.abs(userAnswer - correctAnswer) < 0.01;
}

if (isCorrect) {
correctCount++;
resultLabel.setText(" 回答正确");
resultLabel.setForeground(new Color(20, 140, 40));
} else {
String answerStr = diffIndex <= 2 ? String.valueOf((int) correctAnswer) : String.format("%.2f", correctAnswer);
resultLabel.setText(" 回答错误,正确答案:" + answerStr);
resultLabel.setForeground(new Color(200, 40, 40));
}

// 更新统计数据
double rate = totalCount == 0 ? 0 : (double) correctCount / totalCount * 100;
statsLabel.setText(String.format("答题总数:%d 正确数:%d 正确率:%.1f%%",
totalCount, correctCount, rate));
}

public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new ArithmeticPractice().setVisible(true));
}
}

程序截图
程序截图

5. 运行与操作说明

将代码保存为 ArithmeticPractice.java
编译:javac ArithmeticPractice.java
运行:java ArithmeticPractice
顶部下拉框选择难度,输入框填写答案,点击「提交答案」或按回车键判题
点击「下一题」手动切换新题目

实验二:飞机射击游戏(选做)

1. 实验要求

良好的界面实现
可调节难度
计时与计步功能
保存 / 读取游戏进度
支持功能扩展

2. 功能实现对应

实验要求实现方案
良好的界面实现自定义绘图面板渲染游戏画面,星空背景 + 三角形战机,顶部状态栏实时显示数据
可调节难度简单 / 中等 / 困难三档,控制敌机速度、生成频率、玩家生命值
计时与计步独立定时器统计存活时长,统计子弹发射总数作为计步
保存 / 读取进度Java 对象序列化,完整保存玩家、敌机、子弹、分数、时间等状态
扩展功能生命值系统、矩形碰撞检测、游戏暂停 / 继续、击杀得分统计

3. 核心架构设计

实体类:Player(玩家)、Enemy(敌机)、Bullet(子弹),各自维护坐标、速度、绘制方法
多定时器:游戏主循环(50ms 刷新)、敌机生成、计时三个定时器并行
碰撞检测:基于 Rectangle.intersects 的矩形碰撞判定
存档机制:GameSave 可序列化类,打包所有游戏状态进行文件读写

4. 完整源代码

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
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.ArrayList;
import java.util.Random;

public class ShootingGame extends JFrame {
// 游戏核心参数
private static final int PANEL_WIDTH = 480;
private static final int PANEL_HEIGHT = 600;
private int difficulty = 1; // 0简单 1中等 2困难
private final int[] PLAYER_HP = {3, 2, 1};
private final int[] ENEMY_SPEED = {2, 3, 5};
private final int[] SPAWN_INTERVAL = {1500, 1000, 700};

// 游戏状态
private Player player;
private ArrayList<Enemy> enemies;
private ArrayList<Bullet> bullets;
private int score = 0;
private int shootCount = 0; // 计步:子弹发射数
private int killCount = 0;
private int gameTime = 0;
private boolean isRunning = false;
private boolean isPaused = false;
private boolean gameOver = false;

// 定时器
private Timer gameTimer;
private Timer spawnTimer;
private Timer timeTimer;

// 界面组件
private GamePanel gamePanel;
private JLabel scoreLabel;
private JLabel hpLabel;
private JLabel timeLabel;
private JLabel stepLabel;

private final Random random = new Random();

public ShootingGame() {
setTitle("飞机射击游戏");
setDefaultCloseOperation(EXIT_ON_CLOSE);
setLocationRelativeTo(null);
setResizable(false);

initMenuBar();
initStatusBar();
initGamePanel();
pack();
startNewGame();
}

/**
* 初始化顶部菜单栏
*/
private void initMenuBar() {
JMenuBar menuBar = new JMenuBar();

// 游戏菜单
JMenu gameMenu = new JMenu("游戏");
JMenuItem newGame = new JMenuItem("新游戏");
JMenuItem pauseItem = new JMenuItem("暂停/继续");
JMenuItem saveItem = new JMenuItem("保存进度");
JMenuItem loadItem = new JMenuItem("读取进度");
JMenuItem exitItem = new JMenuItem("退出");

newGame.addActionListener(e -> startNewGame());
pauseItem.addActionListener(e -> togglePause());
saveItem.addActionListener(e -> saveGame());
loadItem.addActionListener(e -> loadGame());
exitItem.addActionListener(e -> System.exit(0));

gameMenu.add(newGame);
gameMenu.add(pauseItem);
gameMenu.addSeparator();
gameMenu.add(saveItem);
gameMenu.add(loadItem);
gameMenu.addSeparator();
gameMenu.add(exitItem);

// 难度菜单
JMenu diffMenu = new JMenu("难度");
JRadioButtonMenuItem easy = new JRadioButtonMenuItem("简单");
JRadioButtonMenuItem normal = new JRadioButtonMenuItem("中等", true);
JRadioButtonMenuItem hard = new JRadioButtonMenuItem("困难");
ButtonGroup diffGroup = new ButtonGroup();
diffGroup.add(easy); diffGroup.add(normal); diffGroup.add(hard);

easy.addActionListener(e -> setDifficulty(0));
normal.addActionListener(e -> setDifficulty(1));
hard.addActionListener(e -> setDifficulty(2));

diffMenu.add(easy);
diffMenu.add(normal);
diffMenu.add(hard);

// 帮助菜单
JMenu helpMenu = new JMenu("帮助");
JMenuItem ruleItem = new JMenuItem("操作说明");
ruleItem.addActionListener(e -> JOptionPane.showMessageDialog(this,
" 方向键:控制飞机左右移动\n空格键:发射子弹\nP键:暂停/继续游戏\n击毁敌机获得分数,撞到敌机扣血",
"操作说明", JOptionPane.INFORMATION_MESSAGE));
helpMenu.add(ruleItem);

menuBar.add(gameMenu);
menuBar.add(diffMenu);
menuBar.add(helpMenu);
setJMenuBar(menuBar);
}

/**
* 初始化状态栏
*/
private void initStatusBar() {
JPanel statusPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 25, 5));
statusPanel.setBackground(new Color(230, 240, 250));

scoreLabel = new JLabel("得分:0");
hpLabel = new JLabel("生命:2");
timeLabel = new JLabel("计时:0秒");
stepLabel = new JLabel("计步:0发");

Font font = new Font("微软雅黑", Font.PLAIN, 14);
scoreLabel.setFont(font);
hpLabel.setFont(font);
timeLabel.setFont(font);
stepLabel.setFont(font);

statusPanel.add(scoreLabel);
statusPanel.add(hpLabel);
statusPanel.add(timeLabel);
statusPanel.add(stepLabel);
add(statusPanel, BorderLayout.NORTH);
}

/**
* 初始化游戏绘图面板
*/
private void initGamePanel() {
gamePanel = new GamePanel();
gamePanel.setPreferredSize(new Dimension(PANEL_WIDTH, PANEL_HEIGHT));
gamePanel.setFocusable(true);
gamePanel.setBackground(new Color(20, 30, 50));

// 键盘事件监听
gamePanel.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (gameOver) return;
int key = e.getKeyCode();
if (key == KeyEvent.VK_LEFT) player.moveLeft();
else if (key == KeyEvent.VK_RIGHT) player.moveRight();
else if (key == KeyEvent.VK_SPACE) shoot();
else if (key == KeyEvent.VK_P) togglePause();
gamePanel.repaint();
}
});

add(gamePanel, BorderLayout.CENTER);
}

/**
* 发射子弹
*/
private void shoot() {
if (isPaused) return;
Bullet bullet = new Bullet(player.getX() + player.getWidth()/2 - 2, player.getY());
bullets.add(bullet);
shootCount++;
stepLabel.setText("计步:" + shootCount + "发");
}

/**
* 游戏主循环
*/
private void gameLoop() {
if (isPaused || gameOver) return;

// 移动所有子弹
for (int i = bullets.size() - 1; i >= 0; i--) {
Bullet b = bullets.get(i);
b.moveUp();
if (b.getY() < 0) bullets.remove(i);
}

// 移动所有敌机
for (int i = enemies.size() - 1; i >= 0; i--) {
Enemy e = enemies.get(i);
e.moveDown();
if (e.getY() > PANEL_HEIGHT) {
enemies.remove(i);
continue;
}
// 检测与玩家碰撞
if (e.getRect().intersects(player.getRect())) {
enemies.remove(i);
player.hurt();
hpLabel.setText("生命:" + player.getHp());
if (player.getHp() <= 0) {
endGame();
}
}
}

// 子弹与敌机碰撞检测
for (int i = bullets.size() - 1; i >= 0; i--) {
Bullet b = bullets.get(i);
for (int j = enemies.size() - 1; j >= 0; j--) {
Enemy e = enemies.get(j);
if (b.getRect().intersects(e.getRect())) {
bullets.remove(i);
enemies.remove(j);
score += 10;
killCount++;
scoreLabel.setText("得分:" + score);
break;
}
}
}

gamePanel.repaint();
}

/**
* 生成敌机
*/
private void spawnEnemy() {
if (isPaused || gameOver) return;
int x = random.nextInt(PANEL_WIDTH - 30);
int speed = ENEMY_SPEED[difficulty];
enemies.add(new Enemy(x, 0, speed));
}

/**
* 开始新游戏
*/
private void startNewGame() {
stopAllTimers();

player = new Player(PANEL_WIDTH/2 - 20, PANEL_HEIGHT - 60, PLAYER_HP[difficulty]);
enemies = new ArrayList<>();
bullets = new ArrayList<>();
score = 0;
shootCount = 0;
killCount = 0;
gameTime = 0;
gameOver = false;
isPaused = false;
isRunning = true;

updateStatus();

// 游戏刷新定时器 50ms
gameTimer = new Timer(50, e -> gameLoop());
gameTimer.start();

// 敌机生成定时器
spawnTimer = new Timer(SPAWN_INTERVAL[difficulty], e -> spawnEnemy());
spawnTimer.start();

// 计时定时器
timeTimer = new Timer(1000, e -> {
if (!isPaused && !gameOver) {
gameTime++;
timeLabel.setText("计时:" + gameTime + "秒");
}
});
timeTimer.start();

gamePanel.requestFocus();
}

/**
* 暂停/继续切换
*/
private void togglePause() {
if (gameOver) return;
isPaused = !isPaused;
gamePanel.repaint();
}

/**
* 游戏结束
*/
private void endGame() {
gameOver = true;
isRunning = false;
stopAllTimers();
JOptionPane.showMessageDialog(this,
"游戏结束!\n最终得分:" + score +
"\n击杀敌机:" + killCount +
"\n发射子弹:" + shootCount + "发" +
"\n存活时间:" + gameTime + "秒",
"游戏结束", JOptionPane.INFORMATION_MESSAGE);
}

/**
* 停止所有定时器
*/
private void stopAllTimers() {
if (gameTimer != null) gameTimer.stop();
if (spawnTimer != null) spawnTimer.stop();
if (timeTimer != null) timeTimer.stop();
}

/**
* 设置难度
*/
private void setDifficulty(int diff) {
difficulty = diff;
startNewGame();
}

/**
* 更新状态栏
*/
private void updateStatus() {
scoreLabel.setText("得分:" + score);
hpLabel.setText("生命:" + player.getHp());
timeLabel.setText("计时:" + gameTime + "秒");
stepLabel.setText("计步:" + shootCount + "发");
}

/**
* 保存游戏进度
*/
private void saveGame() {
if (!isRunning || gameOver) {
JOptionPane.showMessageDialog(this, "当前无有效游戏进度可保存", "提示", JOptionPane.WARNING_MESSAGE);
return;
}
JFileChooser chooser = new JFileChooser();
chooser.setDialogTitle("保存游戏进度");
if (chooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
File file = chooser.getSelectedFile();
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file))) {
GameSave save = new GameSave();
save.difficulty = difficulty;
save.playerX = player.getX();
save.playerY = player.getY();
save.playerHp = player.getHp();
save.score = score;
save.shootCount = shootCount;
save.killCount = killCount;
save.gameTime = gameTime;
save.isPaused = isPaused;
// 保存敌机和子弹数据
save.enemyList = new ArrayList<>();
for (Enemy e : enemies) {
save.enemyList.add(new int[]{e.getX(), e.getY(), e.getSpeed()});
}
save.bulletList = new ArrayList<>();
for (Bullet b : bullets) {
save.bulletList.add(new int[]{b.getX(), b.getY()});
}
oos.writeObject(save);
JOptionPane.showMessageDialog(this, "保存成功", "提示", JOptionPane.INFORMATION_MESSAGE);
} catch (IOException e) {
JOptionPane.showMessageDialog(this, "保存失败:" + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
}
}
}

/**
* 读取游戏进度
*/
private void loadGame() {
JFileChooser chooser = new JFileChooser();
chooser.setDialogTitle("读取游戏进度");
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
File file = chooser.getSelectedFile();
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
GameSave save = (GameSave) ois.readObject();
stopAllTimers();

difficulty = save.difficulty;
player = new Player(save.playerX, save.playerY, save.playerHp);
enemies = new ArrayList<>();
for (int[] arr : save.enemyList) {
enemies.add(new Enemy(arr[0], arr[1], arr[2]));
}
bullets = new ArrayList<>();
for (int[] arr : save.bulletList) {
bullets.add(new Bullet(arr[0], arr[1]));
}
score = save.score;
shootCount = save.shootCount;
killCount = save.killCount;
gameTime = save.gameTime;
isPaused = save.isPaused;
gameOver = false;
isRunning = true;

updateStatus();

// 重启定时器
gameTimer = new Timer(50, e -> gameLoop());
gameTimer.start();
spawnTimer = new Timer(SPAWN_INTERVAL[difficulty], e -> spawnEnemy());
spawnTimer.start();
timeTimer = new Timer(1000, e -> {
if (!isPaused && !gameOver) {
gameTime++;
timeLabel.setText("计时:" + gameTime + "秒");
}
});
timeTimer.start();

gamePanel.repaint();
gamePanel.requestFocus();
JOptionPane.showMessageDialog(this, "读取成功", "提示", JOptionPane.INFORMATION_MESSAGE);

} catch (IOException | ClassNotFoundException e) {
JOptionPane.showMessageDialog(this, "读取失败:" + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
}
}
}

// ===================== 游戏实体类 =====================

/**
* 玩家飞机类
*/
class Player {
private int x, y;
private final int width = 40, height = 50;
private int hp;
private final int speed = 8;

public Player(int x, int y, int hp) {
this.x = x;
this.y = y;
this.hp = hp;
}

public void moveLeft() {
if (x > 0) x -= speed;
}

public void moveRight() {
if (x + width < PANEL_WIDTH) x += speed;
}

public void hurt() { hp--; }

public int getX() { return x; }
public int getY() { return y; }
public int getWidth() { return width; }
public int getHp() { return hp; }

public Rectangle getRect() {
return new Rectangle(x, y, width, height);
}

public void draw(Graphics g) {
g.setColor(new Color(100, 200, 255));
int[] xPoints = {x + width/2, x, x + width};
int[] yPoints = {y, y + height, y + height};
g.fillPolygon(xPoints, yPoints, 3);
// 机身装饰
g.setColor(new Color(50, 150, 220));
g.fillRect(x + width/2 - 4, y + 15, 8, 25);
}
}

/**
* 敌机类
*/
class Enemy {
private int x, y;
private final int width = 30, height = 30;
private int speed;

public Enemy(int x, int y, int speed) {
this.x = x;
this.y = y;
this.speed = speed;
}

public void moveDown() { y += speed; }

public int getX() { return x; }
public int getY() { return y; }
public int getSpeed() { return speed; }

public Rectangle getRect() {
return new Rectangle(x, y, width, height);
}

public void draw(Graphics g) {
g.setColor(new Color(255, 100, 100));
int[] xPoints = {x + width/2, x, x + width};
int[] yPoints = {y + height, y, y};
g.fillPolygon(xPoints, yPoints, 3);
}
}

/**
* 子弹类
*/
class Bullet {
private int x, y;
private final int width = 4, height = 12;
private final int speed = 10;

public Bullet(int x, int y) {
this.x = x;
this.y = y;
}

public void moveUp() { y -= speed; }

public int getX() { return x; }
public int getY() { return y; }

public Rectangle getRect() {
return new Rectangle(x, y, width, height);
}

public void draw(Graphics g) {
g.setColor(Color.YELLOW);
g.fillRect(x, y, width, height);
}
}

/**
* 存档数据类
*/
static class GameSave implements Serializable {
int difficulty;
int playerX, playerY;
int playerHp;
int score;
int shootCount;
int killCount;
int gameTime;
boolean isPaused;
ArrayList<int[]> enemyList;
ArrayList<int[]> bulletList;
}

/**
* 游戏绘图面板
*/
class GamePanel extends JPanel {
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);

// 绘制星空背景
g.setColor(new Color(20, 30, 50));
g.fillRect(0, 0, PANEL_WIDTH, PANEL_HEIGHT);
g.setColor(Color.WHITE);
Random starRand = new Random(123);
for (int i = 0; i < 50; i++) {
int sx = starRand.nextInt(PANEL_WIDTH);
int sy = starRand.nextInt(PANEL_HEIGHT);
g.fillOval(sx, sy, 2, 2);
}

// 绘制子弹
for (Bullet b : bullets) b.draw(g);
// 绘制敌机
for (Enemy e : enemies) e.draw(g);
// 绘制玩家
player.draw(g);

// 暂停遮罩
if (isPaused) {
g.setColor(new Color(0, 0, 0, 150));
g.fillRect(0, 0, PANEL_WIDTH, PANEL_HEIGHT);
g.setColor(Color.WHITE);
g.setFont(new Font("微软雅黑", Font.BOLD, 30));
g.drawString("游戏暂停", PANEL_WIDTH/2 - 70, PANEL_HEIGHT/2);
}
}
}

public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new ShootingGame().setVisible(true));
}
}

程序截图

5. 运行与操作说明

保存为 ShootingGame.java,编译:javac ShootingGame.java
运行:java ShootingGame
操作方式
方向键:控制飞机左右移动
空格键:发射子弹
P键:暂停 / 继续游戏
顶部菜单可切换难度、存档读档、开启新游戏

实验总结

本次实验完整覆盖了 Java Swing 图形编程的核心知识点:
GUI 组件使用:JFrame、JPanel、JButton、JLabel、JComboBox、JMenuBar 等常用组件的属性设置与布局管理
事件处理模型:理解事件源、监听器、事件接口的关系,熟练使用 ActionListener、KeyListener 实现交互
自定义绘图:重写 paintComponent 方法,使用 Graphics 类绘制自定义图形
多任务调度:使用 javax.swing.Timer 实现定时任务,处理游戏循环与计时
文件 IO 与序列化:通过对象序列化实现复杂数据的持久化存储与读取
两个程序均具备完整的交互逻辑和美观的界面,既满足了实验的基础要求,也在功能扩展上做了合理的设计。