Java Swing 图形用户界面实验 ------ 从算术练习到游戏开发的完整实践
一、项目一:小学生算术练习系统
1.1 需求分析
- 支持 + − × ÷ 四种运算,操作数涵盖整数、小数、分数
- 四种难度梯度:简单 → 中等 → 困难 → 专家
- 随机出题、按键提交、实时反馈、错题回顾
- 计时计分,进度条显示
1.2 架构设计
采用 CardLayout 实现三页切换(欢迎页 → 游戏页 → 结果页),避免多窗口跳转带来的状态同步问题。
核心类职责:
Fraction:封装分数的加减乘除与约分,支持带分数解析Question:策略模式生成不同难度的题目GameUI:界面与逻辑解耦,专注事件分发ArithmeticGame:程序入口
1.3 程序界面
1.4 完整代码
Fraction.java
java
import java.util.Objects;
/**
* 分数类:支持加减乘除、约分、带分数/假分数转换
*/
public class Fraction {
private final int numerator;
private final int denominator;
public Fraction(int numerator, int denominator) {
if (denominator == 0) {
throw new IllegalArgumentException("分母不能为零");
}
if (denominator < 0) {
numerator = -numerator;
denominator = -denominator;
}
int gcd = gcd(Math.abs(numerator), Math.abs(denominator));
this.numerator = numerator / gcd;
this.denominator = denominator / gcd;
}
public Fraction(int whole) {
this(whole, 1);
}
private int gcd(int a, int b) {
while (b != 0) {
int temp = b;
b = a % b;
a = temp;
}
return a;
}
public Fraction add(Fraction other) {
int num = this.numerator * other.denominator + other.numerator * this.denominator;
int den = this.denominator * other.denominator;
return new Fraction(num, den);
}
public Fraction subtract(Fraction other) {
int num = this.numerator * other.denominator - other.numerator * this.denominator;
int den = this.denominator * other.denominator;
return new Fraction(num, den);
}
public Fraction multiply(Fraction other) {
return new Fraction(this.numerator * other.numerator, this.denominator * other.denominator);
}
public Fraction divide(Fraction other) {
if (other.numerator == 0) throw new ArithmeticException("除以零");
return new Fraction(this.numerator * other.denominator, this.denominator * other.numerator);
}
public double toDouble() {
return (double) numerator / denominator;
}
@Override
public String toString() {
if (denominator == 1) return String.valueOf(numerator);
if (Math.abs(numerator) > denominator) {
int whole = numerator / denominator;
int rem = Math.abs(numerator % denominator);
if (rem == 0) return String.valueOf(whole);
return whole + "'" + rem + "/" + denominator;
}
return numerator + "/" + denominator;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Fraction)) return false;
Fraction other = (Fraction) o;
return this.numerator == other.numerator && this.denominator == other.denominator;
}
@Override
public int hashCode() {
return Objects.hash(numerator, denominator);
}
public int getNumerator() { return numerator; }
public int getDenominator() { return denominator; }
}
Question.java
java
import java.util.Random;
/**
* 题目类:根据难度和运算符随机生成算术题
*/
public class Question {
public enum Operator {
ADD("+"), SUBTRACT("-"), MULTIPLY("×"), DIVIDE("÷");
private final String symbol;
Operator(String symbol) { this.symbol = symbol; }
public String getSymbol() { return symbol; }
}
public enum Difficulty {
EASY("简单"), MEDIUM("中等"), HARD("困难"), EXPERT("专家");
private final String label;
Difficulty(String label) { this.label = label; }
public String getLabel() { return label; }
}
private final String leftStr;
private final String rightStr;
private final Operator operator;
private final String answerStr;
private final double answerValue;
private final String questionText;
private Question(String leftStr, String rightStr, Operator operator,
String answerStr, double answerValue, String questionText) {
this.leftStr = leftStr;
this.rightStr = rightStr;
this.operator = operator;
this.answerStr = answerStr;
this.answerValue = answerValue;
this.questionText = questionText;
}
public static Question generate(Difficulty difficulty, Operator[] operators, Random random) {
Operator op = operators[random.nextInt(operators.length)];
switch (difficulty) {
case EASY: return generateEasy(op, random);
case MEDIUM: return generateMedium(op, random);
case HARD: return generateHard(op, random);
case EXPERT: return generateExpert(op, random);
default: return generateEasy(op, random);
}
}
private static Question generateEasy(Operator op, Random random) {
int a = random.nextInt(20) + 1;
int b = random.nextInt(20) + 1;
if (op == Operator.SUBTRACT && a < b) { int t = a; a = b; b = t; }
String ans; double val;
switch (op) {
case ADD: ans = String.valueOf(a + b); val = a + b; break;
case SUBTRACT: ans = String.valueOf(a - b); val = a - b; break;
case MULTIPLY: ans = String.valueOf(a * b); val = a * b; break;
case DIVIDE: a = a * b; ans = String.valueOf(a / b); val = a / b; break;
default: ans = "0"; val = 0;
}
return new Question(String.valueOf(a), String.valueOf(b), op, ans, val,
a + " " + op.getSymbol() + " " + b + " = ?");
}
private static Question generateMedium(Operator op, Random random) {
int a = random.nextInt(50) + 1;
int b = random.nextInt(50) + 1;
if (op == Operator.SUBTRACT && a < b) { int t = a; a = b; b = t; }
if (op == Operator.DIVIDE && b == 0) b = 1;
String ans; double val;
switch (op) {
case ADD: ans = String.valueOf(a + b); val = a + b; break;
case SUBTRACT: ans = String.valueOf(a - b); val = a - b; break;
case MULTIPLY: ans = String.valueOf(a * b); val = a * b; break;
case DIVIDE: int f = random.nextInt(20) + 1; a = b * f; ans = String.valueOf(a / b); val = a / b; break;
default: ans = "0"; val = 0;
}
return new Question(String.valueOf(a), String.valueOf(b), op, ans, val,
a + " " + op.getSymbol() + " " + b + " = ?");
}
private static Question generateHard(Operator op, Random random) {
double a = Math.round((random.nextInt(100) + 1) / 10.0 * 10) / 10.0;
double b = Math.round((random.nextInt(100) + 1) / 10.0 * 10) / 10.0;
if (op == Operator.SUBTRACT && a < b) { double t = a; a = b; b = t; }
if (op == Operator.DIVIDE && b == 0) b = 0.1;
String ans; double val;
switch (op) {
case ADD: val = a + b; ans = String.format("%.1f", val); break;
case SUBTRACT: val = a - b; ans = String.format("%.1f", val); break;
case MULTIPLY: val = a * b; ans = String.format("%.1f", val); break;
case DIVIDE: val = a / b; ans = String.format("%.2f", val); break;
default: ans = "0"; val = 0;
}
return new Question(String.valueOf(a), String.valueOf(b), op, ans, val,
a + " " + op.getSymbol() + " " + b + " = ?");
}
private static Question generateExpert(Operator op, Random random) {
int n1 = random.nextInt(10) + 1, d1 = random.nextInt(10) + 2;
int n2 = random.nextInt(10) + 1, d2 = random.nextInt(10) + 2;
Fraction f1 = new Fraction(n1, d1), f2 = new Fraction(n2, d2);
if (op == Operator.SUBTRACT && f1.toDouble() < f2.toDouble()) {
Fraction t = f1; f1 = f2; f2 = t;
}
Fraction result;
switch (op) {
case ADD: result = f1.add(f2); break;
case SUBTRACT: result = f1.subtract(f2); break;
case MULTIPLY: result = f1.multiply(f2); break;
case DIVIDE: if (f2.toDouble() == 0) f2 = new Fraction(1); result = f1.divide(f2); break;
default: result = new Fraction(0);
}
return new Question(f1.toString(), f2.toString(), op, result.toString(), result.toDouble(),
f1 + " " + op.getSymbol() + " " + f2 + " = ?");
}
public String getQuestionText() { return questionText; }
public String getAnswerStr() { return answerStr; }
public double getAnswerValue() { return answerValue; }
public Operator getOperator() { return operator; }
public boolean checkAnswer(String userInput) {
if (userInput == null || userInput.trim().isEmpty()) return false;
try {
String input = userInput.trim();
if (input.contains("/")) {
if (input.contains("'")) {
String[] parts = input.split("'");
int whole = Integer.parseInt(parts[0]);
String[] frac = parts[1].split("/");
int num = Integer.parseInt(frac[0]), den = Integer.parseInt(frac[1]);
Fraction f = new Fraction(whole * den + (whole < 0 ? -num : num), den);
return Math.abs(f.toDouble() - answerValue) < 0.05;
} else {
String[] frac = input.split("/");
int num = Integer.parseInt(frac[0]), den = Integer.parseInt(frac[1]);
return Math.abs(new Fraction(num, den).toDouble() - answerValue) < 0.05;
}
} else {
double val = Double.parseDouble(input);
return Math.abs(val - answerValue) < 0.05;
}
} catch (Exception e) {
return false;
}
}
}
GameUI.java
java
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.plaf.basic.BasicButtonUI;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* 优化版游戏界面:修复按钮文字颜色问题
*/
public class GameUI extends JFrame {
private CardLayout cardLayout;
private JPanel mainPanel;
private Question.Difficulty selectedDifficulty = Question.Difficulty.EASY;
private boolean addSelected = true, subSelected = true, mulSelected = false, divSelected = false;
private int questionCount = 10;
private List<Question> questions;
private List<String> userAnswers;
private List<Boolean> results;
private int currentQuestionIndex = 0;
private int score = 0;
private long startTime;
private Timer gameTimer;
private Random random = new Random();
private JLabel timerLabel, scoreLabel, questionLabel, feedbackLabel;
private JTextField answerField;
private JButton submitBtn, nextBtn;
private JProgressBar progressBar;
private JTextArea resultArea;
private final Color PRIMARY_BLUE = new Color(30, 100, 180);
private final Color PRIMARY_GREEN = new Color(50, 150, 50);
private final Color PRIMARY_RED = new Color(200, 50, 50);
private final Color PRIMARY_ORANGE = new Color(220, 140, 0);
private final Color BG_WELCOME = new Color(240, 248, 255);
private final Color BG_GAME = new Color(255, 250, 240);
private final Color BG_RESULT = new Color(240, 255, 240);
private final Color BTN_BLUE = new Color(50, 130, 220);
private final Color BTN_GREEN = new Color(50, 170, 80);
private final Color BTN_HOVER_BLUE = new Color(40, 110, 200);
private final Color BTN_HOVER_GREEN = new Color(40, 150, 70);
public GameUI() {
setTitle("小学生算术练习系统");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(650, 520);
setLocationRelativeTo(null);
cardLayout = new CardLayout();
mainPanel = new JPanel(cardLayout);
mainPanel.add(createWelcomePanel(), "WELCOME");
mainPanel.add(createGamePanel(), "GAME");
mainPanel.add(createResultPanel(), "RESULT");
add(mainPanel);
cardLayout.show(mainPanel, "WELCOME");
}
private JPanel createWelcomePanel() {
JPanel panel = new JPanel(new BorderLayout());
panel.setBackground(BG_WELCOME);
panel.setBorder(new EmptyBorder(40, 60, 40, 60));
JLabel title = new JLabel("小学生算术练习系统", JLabel.CENTER);
title.setFont(new Font("微软雅黑", Font.BOLD, 36));
title.setForeground(PRIMARY_BLUE);
panel.add(title, BorderLayout.NORTH);
JPanel center = new JPanel(new GridBagLayout());
center.setOpaque(false);
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(14, 15, 14, 15);
gbc.anchor = GridBagConstraints.WEST;
Font labelFont = new Font("微软雅黑", Font.BOLD, 16);
Font controlFont = new Font("微软雅黑", Font.PLAIN, 14);
gbc.gridx = 0; gbc.gridy = 0;
JLabel diffLabel = new JLabel("选择难度:");
diffLabel.setFont(labelFont);
center.add(diffLabel, gbc);
gbc.gridx = 1;
String[] diffs = {"简单(整数 + -)", "中等(整数 × ÷)", "困难(小数)", "专家(分数)"};
JComboBox<String> diffCombo = new JComboBox<>(diffs);
diffCombo.setFont(controlFont);
diffCombo.setPreferredSize(new Dimension(220, 34));
diffCombo.addActionListener(e -> selectedDifficulty = Question.Difficulty.values()[diffCombo.getSelectedIndex()]);
center.add(diffCombo, gbc);
gbc.gridx = 0; gbc.gridy = 1;
JLabel opLabel = new JLabel("运算类型:");
opLabel.setFont(labelFont);
center.add(opLabel, gbc);
gbc.gridx = 1;
JPanel opPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 15, 0));
opPanel.setOpaque(false);
JCheckBox addBox = new JCheckBox("加法", true);
JCheckBox subBox = new JCheckBox("减法", true);
JCheckBox mulBox = new JCheckBox("乘法", false);
JCheckBox divBox = new JCheckBox("除法", false);
for (JCheckBox box : new JCheckBox[]{addBox, subBox, mulBox, divBox}) {
box.setFont(controlFont);
box.setOpaque(false);
opPanel.add(box);
}
addBox.addActionListener(e -> addSelected = addBox.isSelected());
subBox.addActionListener(e -> subSelected = subBox.isSelected());
mulBox.addActionListener(e -> mulSelected = mulBox.isSelected());
divBox.addActionListener(e -> divSelected = divBox.isSelected());
center.add(opPanel, gbc);
gbc.gridx = 0; gbc.gridy = 2;
JLabel countLabel = new JLabel("题目数量:");
countLabel.setFont(labelFont);
center.add(countLabel, gbc);
gbc.gridx = 1;
SpinnerNumberModel model = new SpinnerNumberModel(10, 5, 50, 5);
JSpinner countSpinner = new JSpinner(model);
countSpinner.setFont(controlFont);
countSpinner.setPreferredSize(new Dimension(220, 34));
countSpinner.addChangeListener(e -> questionCount = (Integer) countSpinner.getValue());
center.add(countSpinner, gbc);
panel.add(center, BorderLayout.CENTER);
JButton startBtn = createStyledButton("开始练习", BTN_BLUE, BTN_HOVER_BLUE, 220, 55, 20);
startBtn.addActionListener(e -> startGame());
JPanel btnPanel = new JPanel();
btnPanel.setOpaque(false);
btnPanel.add(startBtn);
panel.add(btnPanel, BorderLayout.SOUTH);
return panel;
}
private JPanel createGamePanel() {
JPanel panel = new JPanel(new BorderLayout());
panel.setBackground(BG_GAME);
panel.setBorder(new EmptyBorder(20, 40, 20, 40));
JPanel topContainer = new JPanel(new BorderLayout());
topContainer.setOpaque(false);
JPanel top = new JPanel(new BorderLayout());
top.setOpaque(false);
timerLabel = new JLabel("时间: 0秒", JLabel.LEFT);
timerLabel.setFont(new Font("微软雅黑", Font.BOLD, 16));
scoreLabel = new JLabel("得分: 0", JLabel.RIGHT);
scoreLabel.setFont(new Font("微软雅黑", Font.BOLD, 16));
top.add(timerLabel, BorderLayout.WEST);
top.add(scoreLabel, BorderLayout.EAST);
progressBar = new JProgressBar(0, 100);
progressBar.setStringPainted(true);
progressBar.setFont(new Font("微软雅黑", Font.PLAIN, 12));
progressBar.setForeground(new Color(50, 180, 80));
progressBar.setBackground(new Color(220, 220, 220));
progressBar.setBorder(BorderFactory.createEmptyBorder(5, 0, 5, 0));
topContainer.add(top, BorderLayout.NORTH);
topContainer.add(progressBar, BorderLayout.SOUTH);
panel.add(topContainer, BorderLayout.NORTH);
JPanel center = new JPanel(new GridBagLayout());
center.setOpaque(false);
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0; gbc.gridy = 0;
gbc.insets = new Insets(25, 10, 25, 10);
questionLabel = new JLabel("题目区域", JLabel.CENTER);
questionLabel.setFont(new Font("微软雅黑", Font.BOLD, 42));
questionLabel.setForeground(new Color(50, 50, 50));
center.add(questionLabel, gbc);
gbc.gridy = 1;
answerField = new JTextField(12);
answerField.setFont(new Font("微软雅黑", Font.BOLD, 28));
answerField.setHorizontalAlignment(JTextField.CENTER);
answerField.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(new Color(100, 150, 220), 2, true),
BorderFactory.createEmptyBorder(8, 15, 8, 15)
));
answerField.setPreferredSize(new Dimension(280, 55));
answerField.addActionListener(e -> checkAnswer());
center.add(answerField, gbc);
gbc.gridy = 2;
feedbackLabel = new JLabel(" ", JLabel.CENTER);
feedbackLabel.setFont(new Font("微软雅黑", Font.BOLD, 18));
center.add(feedbackLabel, gbc);
panel.add(center, BorderLayout.CENTER);
JPanel bottom = new JPanel(new FlowLayout(FlowLayout.CENTER, 25, 10));
bottom.setOpaque(false);
submitBtn = createStyledButton("提交答案", BTN_GREEN, BTN_HOVER_GREEN, 150, 48, 16);
submitBtn.addActionListener(e -> checkAnswer());
nextBtn = createStyledButton("下一题", BTN_BLUE, BTN_HOVER_BLUE, 150, 48, 16);
nextBtn.setEnabled(false);
nextBtn.addActionListener(e -> nextQuestion());
bottom.add(submitBtn);
bottom.add(nextBtn);
panel.add(bottom, BorderLayout.SOUTH);
return panel;
}
private JPanel createResultPanel() {
JPanel panel = new JPanel(new BorderLayout());
panel.setBackground(BG_RESULT);
panel.setBorder(new EmptyBorder(30, 50, 30, 50));
JLabel title = new JLabel("练习完成!", JLabel.CENTER);
title.setFont(new Font("微软雅黑", Font.BOLD, 32));
title.setForeground(PRIMARY_GREEN);
panel.add(title, BorderLayout.NORTH);
resultArea = new JTextArea();
resultArea.setFont(new Font("微软雅黑", Font.PLAIN, 15));
resultArea.setEditable(false);
resultArea.setOpaque(false);
resultArea.setLineWrap(true);
resultArea.setWrapStyleWord(true);
JScrollPane scroll = new JScrollPane(resultArea);
scroll.setOpaque(false);
scroll.getViewport().setOpaque(false);
scroll.setBorder(BorderFactory.createEmptyBorder(15, 10, 15, 10));
panel.add(scroll, BorderLayout.CENTER);
JButton restartBtn = createStyledButton("再来一次", BTN_BLUE, BTN_HOVER_BLUE, 200, 50, 18);
restartBtn.addActionListener(e -> cardLayout.show(mainPanel, "WELCOME"));
JPanel btnPanel = new JPanel();
btnPanel.setOpaque(false);
btnPanel.add(restartBtn);
panel.add(btnPanel, BorderLayout.SOUTH);
return panel;
}
private JButton createStyledButton(String text, Color bgColor, Color hoverColor, int width, int height, int fontSize) {
JButton btn = new JButton(text);
btn.setFont(new Font("微软雅黑", Font.BOLD, fontSize));
btn.setUI(new BasicButtonUI());
btn.setBackground(bgColor);
btn.setForeground(Color.BLACK);
btn.setOpaque(true);
btn.setContentAreaFilled(true);
btn.setFocusPainted(false);
btn.setPreferredSize(new Dimension(width, height));
btn.setCursor(new Cursor(Cursor.HAND_CURSOR));
btn.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(bgColor.darker(), 2, true),
BorderFactory.createEmptyBorder(5, 15, 5, 15)
));
btn.addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(MouseEvent e) {
btn.setBackground(hoverColor);
}
@Override
public void mouseExited(MouseEvent e) {
btn.setBackground(bgColor);
}
});
return btn;
}
private void startGame() {
if (!addSelected && !subSelected && !mulSelected && !divSelected) {
JOptionPane.showMessageDialog(this, "请至少选择一种运算类型!", "提示", JOptionPane.WARNING_MESSAGE);
return;
}
List<Question.Operator> ops = new ArrayList<>();
if (addSelected) ops.add(Question.Operator.ADD);
if (subSelected) ops.add(Question.Operator.SUBTRACT);
if (mulSelected) ops.add(Question.Operator.MULTIPLY);
if (divSelected) ops.add(Question.Operator.DIVIDE);
questions = new ArrayList<>();
userAnswers = new ArrayList<>();
results = new ArrayList<>();
for (int i = 0; i < questionCount; i++) {
questions.add(Question.generate(selectedDifficulty, ops.toArray(new Question.Operator[0]), random));
}
currentQuestionIndex = 0;
score = 0;
startTime = System.currentTimeMillis();
if (gameTimer != null) gameTimer.stop();
gameTimer = new Timer(1000, e -> updateTimer());
gameTimer.start();
showQuestion();
cardLayout.show(mainPanel, "GAME");
}
private void updateTimer() {
long elapsed = (System.currentTimeMillis() - startTime) / 1000;
timerLabel.setText("时间: " + elapsed + "秒");
}
private void showQuestion() {
Question q = questions.get(currentQuestionIndex);
questionLabel.setText(q.getQuestionText());
answerField.setText("");
answerField.setEnabled(true);
feedbackLabel.setText(" ");
feedbackLabel.setForeground(Color.BLACK);
submitBtn.setEnabled(true);
nextBtn.setEnabled(false);
progressBar.setValue((currentQuestionIndex * 100) / questionCount);
progressBar.setString("第 " + (currentQuestionIndex + 1) + " / " + questionCount + " 题");
answerField.requestFocus();
}
private void checkAnswer() {
String input = answerField.getText().trim();
if (input.isEmpty()) {
feedbackLabel.setText("请输入答案!");
feedbackLabel.setForeground(PRIMARY_ORANGE);
return;
}
Question q = questions.get(currentQuestionIndex);
boolean correct = q.checkAnswer(input);
userAnswers.add(input);
results.add(correct);
if (correct) {
score += 10;
feedbackLabel.setText("回答正确!");
feedbackLabel.setForeground(PRIMARY_GREEN);
} else {
feedbackLabel.setText("错误!正确答案是: " + q.getAnswerStr());
feedbackLabel.setForeground(PRIMARY_RED);
}
scoreLabel.setText("得分: " + score);
answerField.setEnabled(false);
submitBtn.setEnabled(false);
nextBtn.setEnabled(true);
nextBtn.requestFocus();
if (currentQuestionIndex == questionCount - 1) {
nextBtn.setText("查看结果");
}
}
private void nextQuestion() {
currentQuestionIndex++;
if (currentQuestionIndex < questionCount) {
showQuestion();
nextBtn.setText("下一题");
} else {
endGame();
}
}
private void endGame() {
if (gameTimer != null) gameTimer.stop();
long totalTime = (System.currentTimeMillis() - startTime) / 1000;
int correctCount = 0;
for (boolean r : results) if (r) correctCount++;
StringBuilder sb = new StringBuilder();
sb.append("━━━━━━━━━━━━━━━━━━━━\n");
sb.append(" 难度: ").append(selectedDifficulty.getLabel()).append("\n");
sb.append(" 总题数: ").append(questionCount).append("\n");
sb.append(" 正确数: ").append(correctCount).append("\n");
sb.append(" 得分: ").append(score).append(" / ").append(questionCount * 10).append("\n");
sb.append(" 用时: ").append(totalTime).append(" 秒\n");
sb.append(" 正确率: ").append(String.format("%.1f", 100.0 * correctCount / questionCount)).append("%\n");
sb.append("━━━━━━━━━━━━━━━━━━━━\n\n");
if (correctCount < questionCount) {
sb.append("【错题回顾】\n");
for (int i = 0; i < questionCount; i++) {
if (!results.get(i)) {
sb.append("第 ").append(i + 1).append(" 题: ").append(questions.get(i).getQuestionText())
.append(" 你的答案: ").append(userAnswers.get(i))
.append(" 正确答案: ").append(questions.get(i).getAnswerStr()).append("\n");
}
}
} else {
sb.append("太棒了!全部正确!");
}
resultArea.setText(sb.toString());
cardLayout.show(mainPanel, "RESULT");
}
}
ArithmeticGame.java
java
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.plaf.metal.MetalLookAndFeel;
/**
* 程序入口:使用 Metal LookAndFeel 确保按钮颜色正常显示
*/
public class ArithmeticGame {
public static void main(String[] args) {
try {
UIManager.setLookAndFeel(new MetalLookAndFeel());
UIManager.put("Button.foreground", java.awt.Color.WHITE);
UIManager.put("Button.background", new java.awt.Color(50, 130, 220));
UIManager.put("Button.select", new java.awt.Color(40, 110, 200));
UIManager.put("Button.focus", new java.awt.Color(50, 130, 220));
} catch (Exception e) {
e.printStackTrace();
}
SwingUtilities.invokeLater(() -> new GameUI().setVisible(true));
}
}
二、项目二:扫雷游戏
2.1 需求分析
复刻经典扫雷:左键揭开、右键标记(旗/问号)、笑脸重置、LED 计数器、难度分级、存档读档。
2.2 架构设计
MinesweeperGame:纯逻辑核心,不依赖 Swing,方便单元测试与序列化MinesweeperUI:负责绘制与事件转发GameState:DTO 对象,实现Serializable用于存档MinesweeperApp:程序入口
2.3 程序界面
2.4 完整代码
MinesweeperGame.java
java
import java.io.Serializable;
import java.util.Random;
/**
* 扫雷游戏核心类:管理雷区、揭开、标记、计时、胜负判断
*/
public class MinesweeperGame implements Serializable {
private static final long serialVersionUID = 1L;
public enum CellState { COVERED, REVEALED, FLAGGED, QUESTIONED }
public static class Cell implements Serializable {
private static final long serialVersionUID = 1L;
boolean isMine;
int neighborMines;
CellState state = CellState.COVERED;
}
private Cell[][] board;
private int rows, cols, mineCount;
private int revealedCount;
private int flaggedCount;
private boolean gameOver;
private boolean win;
private boolean started;
private transient long startTimeMillis;
private int elapsedTime;
private int steps;
private String difficultyName;
public void init(int rows, int cols, int mineCount, String difficultyName) {
this.rows = rows;
this.cols = cols;
this.mineCount = mineCount;
this.difficultyName = difficultyName;
board = new Cell[rows][cols];
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
board[r][c] = new Cell();
}
}
revealedCount = 0;
flaggedCount = 0;
gameOver = false;
win = false;
started = false;
elapsedTime = 0;
steps = 0;
}
private void placeMines(int firstRow, int firstCol) {
Random rand = new Random();
int placed = 0;
while (placed < mineCount) {
int r = rand.nextInt(rows);
int c = rand.nextInt(cols);
if (r == firstRow && c == firstCol) continue;
if (!board[r][c].isMine) {
board[r][c].isMine = true;
placed++;
}
}
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
if (board[r][c].isMine) continue;
int count = 0;
for (int dr = -1; dr <= 1; dr++) {
for (int dc = -1; dc <= 1; dc++) {
if (dr == 0 && dc == 0) continue;
int nr = r + dr, nc = c + dc;
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && board[nr][nc].isMine) {
count++;
}
}
}
board[r][c].neighborMines = count;
}
}
started = true;
startTimeMillis = System.currentTimeMillis();
}
public boolean reveal(int r, int c) {
return reveal(r, c, true);
}
private boolean reveal(int r, int c, boolean countStep) {
if (gameOver || r < 0 || r >= rows || c < 0 || c >= cols) return false;
Cell cell = board[r][c];
if (cell.state != CellState.COVERED) return false;
if (!started) placeMines(r, c);
cell.state = CellState.REVEALED;
revealedCount++;
if (countStep) steps++;
if (cell.isMine) {
gameOver = true;
win = false;
return true;
}
if (cell.neighborMines == 0) {
for (int dr = -1; dr <= 1; dr++) {
for (int dc = -1; dc <= 1; dc++) {
if (dr == 0 && dc == 0) continue;
reveal(r + dr, c + dc, false);
}
}
}
if (revealedCount == rows * cols - mineCount) {
gameOver = true;
win = true;
}
return true;
}
public void toggleFlag(int r, int c) {
if (gameOver || r < 0 || r >= rows || c < 0 || c >= cols) return;
Cell cell = board[r][c];
if (cell.state == CellState.REVEALED) return;
steps++;
switch (cell.state) {
case COVERED:
cell.state = CellState.FLAGGED;
flaggedCount++;
break;
case FLAGGED:
cell.state = CellState.QUESTIONED;
flaggedCount--;
break;
case QUESTIONED:
cell.state = CellState.COVERED;
break;
}
}
public int getElapsedTime() {
if (started && !gameOver) {
elapsedTime = (int) ((System.currentTimeMillis() - startTimeMillis) / 1000);
}
return elapsedTime;
}
public void setElapsedTime(int elapsedTime) {
this.elapsedTime = elapsedTime;
if (started && !gameOver) {
this.startTimeMillis = System.currentTimeMillis() - elapsedTime * 1000L;
}
}
public Cell[][] getBoard() { return board; }
public int getRows() { return rows; }
public int getCols() { return cols; }
public int getMineCount() { return mineCount; }
public int getRevealedCount() { return revealedCount; }
public int getFlaggedCount() { return flaggedCount; }
public boolean isGameOver() { return gameOver; }
public boolean isWin() { return win; }
public boolean isStarted() { return started; }
public int getSteps() { return steps; }
public String getDifficultyName() { return difficultyName; }
public void setGameOver(boolean gameOver) { this.gameOver = gameOver; }
public void setWin(boolean win) { this.win = win; }
public void setStarted(boolean started) { this.started = started; }
public void setRevealedCount(int revealedCount) { this.revealedCount = revealedCount; }
public void setFlaggedCount(int flaggedCount) { this.flaggedCount = flaggedCount; }
public void setSteps(int steps) { this.steps = steps; }
public void setBoard(Cell[][] board) { this.board = board; }
public void setRows(int rows) { this.rows = rows; }
public void setCols(int cols) { this.cols = cols; }
public void setMineCount(int mineCount) { this.mineCount = mineCount; }
public void setDifficultyName(String difficultyName) { this.difficultyName = difficultyName; }
}
GameState.java(扫雷)
java
import java.io.Serializable;
/**
* 游戏状态封装:实现 Serializable,用于保存/读取进度
*/
public class GameState implements Serializable {
private static final long serialVersionUID = 1L;
private MinesweeperGame.Cell[][] board;
private int rows, cols, mineCount;
private int revealedCount, flaggedCount;
private boolean gameOver, win, started;
private int elapsedTime, steps;
private String difficultyName;
public GameState(MinesweeperGame game) {
this.board = game.getBoard();
this.rows = game.getRows();
this.cols = game.getCols();
this.mineCount = game.getMineCount();
this.revealedCount = game.getRevealedCount();
this.flaggedCount = game.getFlaggedCount();
this.gameOver = game.isGameOver();
this.win = game.isWin();
this.started = game.isStarted();
this.elapsedTime = game.getElapsedTime();
this.steps = game.getSteps();
this.difficultyName = game.getDifficultyName();
}
public MinesweeperGame.Cell[][] getBoard() { return board; }
public int getRows() { return rows; }
public int getCols() { return cols; }
public int getMineCount() { return mineCount; }
public int getRevealedCount() { return revealedCount; }
public int getFlaggedCount() { return flaggedCount; }
public boolean isGameOver() { return gameOver; }
public boolean isWin() { return win; }
public boolean isStarted() { return started; }
public int getElapsedTime() { return elapsedTime; }
public int getSteps() { return steps; }
public String getDifficultyName() { return difficultyName; }
}
MinesweeperUI.java
java
import javax.swing.*;
import javax.swing.border.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
/**
* 优化版扫雷图形界面:解决 Emoji 显示问题,提升视觉体验
*/
public class MinesweeperUI extends JFrame {
private MinesweeperGame game;
private JButton[][] buttons;
private JPanel boardPanel;
private JLabel mineLabel, timeLabel, stepLabel;
private JButton faceButton;
private Timer timer;
private int currentRows = 9, currentCols = 9, currentMines = 10;
private String currentDifficulty = "初级";
private final Color[] NUMBER_COLORS = {
Color.BLACK, new Color(0, 0, 255), new Color(0, 128, 0),
new Color(255, 0, 0), new Color(0, 0, 128), new Color(128, 0, 0),
new Color(0, 128, 128), new Color(0, 0, 0), new Color(128, 128, 128)
};
private final Color BG_COLOR = new Color(192, 192, 192);
private final Color BOARD_BG = new Color(128, 128, 128);
private final Color CELL_COVERED = new Color(192, 192, 192);
private final Color CELL_REVEALED = new Color(220, 220, 220);
private final Color MINE_BG = new Color(255, 100, 100);
private final Color FLAG_COLOR = new Color(220, 0, 0);
private final Color QUESTION_COLOR = new Color(0, 0, 0);
private final Color LED_BG = new Color(0, 0, 0);
private final Color LED_FG = new Color(255, 50, 50);
public MinesweeperUI() {
setTitle("扫雷游戏");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setResizable(false);
initMenu();
initComponents();
newGame(currentRows, currentCols, currentMines, currentDifficulty);
}
private void initMenu() {
JMenuBar menuBar = new JMenuBar();
menuBar.setBackground(new Color(220, 220, 220));
JMenu gameMenu = new JMenu("游戏(G)");
gameMenu.setMnemonic(KeyEvent.VK_G);
gameMenu.setFont(new Font("微软雅黑", Font.PLAIN, 13));
JMenuItem newItem = new JMenuItem("新游戏(N)", KeyEvent.VK_N);
newItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, InputEvent.CTRL_DOWN_MASK));
newItem.setFont(new Font("微软雅黑", Font.PLAIN, 13));
newItem.addActionListener(e -> newGame(currentRows, currentCols, currentMines, currentDifficulty));
JMenuItem saveItem = new JMenuItem("保存进度(S)", KeyEvent.VK_S);
saveItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK));
saveItem.setFont(new Font("微软雅黑", Font.PLAIN, 13));
saveItem.addActionListener(e -> saveGame());
JMenuItem loadItem = new JMenuItem("读取进度(L)", KeyEvent.VK_L);
loadItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.CTRL_DOWN_MASK));
loadItem.setFont(new Font("微软雅黑", Font.PLAIN, 13));
loadItem.addActionListener(e -> loadGame());
JMenu diffMenu = new JMenu("难度");
diffMenu.setFont(new Font("微软雅黑", Font.PLAIN, 13));
JMenuItem begItem = new JMenuItem("初级 (9x9, 10雷)");
JMenuItem intItem = new JMenuItem("中级 (16x16, 40雷)");
JMenuItem expItem = new JMenuItem("高级 (16x30, 99雷)");
JMenuItem custItem = new JMenuItem("自定义...");
for (JMenuItem item : new JMenuItem[]{begItem, intItem, expItem, custItem}) {
item.setFont(new Font("微软雅黑", Font.PLAIN, 13));
}
begItem.addActionListener(e -> setDifficulty(9, 9, 10, "初级"));
intItem.addActionListener(e -> setDifficulty(16, 16, 40, "中级"));
expItem.addActionListener(e -> setDifficulty(16, 30, 99, "高级"));
custItem.addActionListener(e -> showCustomDialog());
diffMenu.add(begItem); diffMenu.add(intItem); diffMenu.add(expItem); diffMenu.add(custItem);
JMenuItem exitItem = new JMenuItem("退出(Q)");
exitItem.setFont(new Font("微软雅黑", Font.PLAIN, 13));
exitItem.addActionListener(e -> System.exit(0));
gameMenu.add(newItem); gameMenu.add(saveItem); gameMenu.add(loadItem);
gameMenu.addSeparator(); gameMenu.add(diffMenu); gameMenu.addSeparator(); gameMenu.add(exitItem);
JMenu helpMenu = new JMenu("帮助(H)");
helpMenu.setMnemonic(KeyEvent.VK_H);
helpMenu.setFont(new Font("微软雅黑", Font.PLAIN, 13));
JMenuItem aboutItem = new JMenuItem("关于");
aboutItem.setFont(new Font("微软雅黑", Font.PLAIN, 13));
aboutItem.addActionListener(e -> JOptionPane.showMessageDialog(this,
"Java Swing 扫雷游戏 v2.0\n\n"
+ "【操作说明】\n"
+ "• 左键单击:揭开格子\n"
+ "• 右键单击:标记/取消标记\n"
+ "• 笑脸按钮:重新开始\n\n"
+ "【功能特性】\n"
+ "• 四种难度(含自定义)\n"
+ "• 实时计时与计步\n"
+ "• 保存/读取游戏进度",
"关于扫雷", JOptionPane.INFORMATION_MESSAGE));
helpMenu.add(aboutItem);
menuBar.add(gameMenu); menuBar.add(helpMenu);
setJMenuBar(menuBar);
}
private void initComponents() {
setLayout(new BorderLayout());
JPanel topPanel = new JPanel(new BorderLayout(10, 0));
topPanel.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createEmptyBorder(10, 12, 10, 12),
BorderFactory.createLoweredBevelBorder()
));
topPanel.setBackground(BG_COLOR);
JPanel leftPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0));
leftPanel.setOpaque(false);
mineLabel = createLedLabel("010");
stepLabel = createLedLabel("000");
JLabel mineText = new JLabel("剩余雷数");
mineText.setFont(new Font("微软雅黑", Font.PLAIN, 11));
mineText.setForeground(Color.DARK_GRAY);
JLabel stepText = new JLabel("步数");
stepText.setFont(new Font("微软雅黑", Font.PLAIN, 11));
stepText.setForeground(Color.DARK_GRAY);
JPanel minePanel = new JPanel(new GridLayout(2, 1, 0, 2));
minePanel.setOpaque(false);
minePanel.add(mineLabel);
minePanel.add(mineText);
JPanel stepPanel = new JPanel(new GridLayout(2, 1, 0, 2));
stepPanel.setOpaque(false);
stepPanel.add(stepLabel);
stepPanel.add(stepText);
leftPanel.add(minePanel);
leftPanel.add(Box.createHorizontalStrut(10));
leftPanel.add(stepPanel);
faceButton = createFaceButton();
JPanel rightPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 0));
rightPanel.setOpaque(false);
timeLabel = createLedLabel("000");
JLabel timeText = new JLabel("时间(秒)");
timeText.setFont(new Font("微软雅黑", Font.PLAIN, 11));
timeText.setForeground(Color.DARK_GRAY);
JPanel timePanel = new JPanel(new GridLayout(2, 1, 0, 2));
timePanel.setOpaque(false);
timePanel.add(timeLabel);
timePanel.add(timeText);
rightPanel.add(timePanel);
topPanel.add(leftPanel, BorderLayout.WEST);
topPanel.add(faceButton, BorderLayout.CENTER);
topPanel.add(rightPanel, BorderLayout.EAST);
add(topPanel, BorderLayout.NORTH);
boardPanel = new JPanel();
boardPanel.setBackground(BOARD_BG);
boardPanel.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createEmptyBorder(8, 8, 8, 8),
BorderFactory.createLoweredBevelBorder()
));
add(boardPanel, BorderLayout.CENTER);
JPanel statusPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 20, 5));
statusPanel.setBackground(BG_COLOR);
statusPanel.setBorder(BorderFactory.createEmptyBorder(3, 0, 3, 0));
JLabel statusLabel = new JLabel("左键揭开 | 右键标记 | Ctrl+N 新游戏 | Ctrl+S 保存 | Ctrl+O 读取");
statusLabel.setFont(new Font("微软雅黑", Font.PLAIN, 11));
statusLabel.setForeground(Color.DARK_GRAY);
statusPanel.add(statusLabel);
add(statusPanel, BorderLayout.SOUTH);
timer = new Timer(100, e -> updateTimer());
}
private JLabel createLedLabel(String text) {
JLabel label = new JLabel(text, SwingConstants.CENTER);
label.setFont(new Font("Consolas", Font.BOLD, 24));
label.setForeground(LED_FG);
label.setBackground(LED_BG);
label.setOpaque(true);
label.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(Color.DARK_GRAY, 1),
BorderFactory.createEmptyBorder(4, 8, 4, 8)
));
label.setPreferredSize(new Dimension(70, 40));
return label;
}
private JButton createFaceButton() {
JButton btn = new JButton("☺");
btn.setFont(new Font("Segoe UI Symbol", Font.BOLD, 28));
btn.setFocusPainted(false);
btn.setPreferredSize(new Dimension(55, 55));
btn.setBackground(BG_COLOR);
btn.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createRaisedBevelBorder(),
BorderFactory.createEmptyBorder(2, 2, 2, 2)
));
btn.setCursor(new Cursor(Cursor.HAND_CURSOR));
btn.addActionListener(e -> newGame(currentRows, currentCols, currentMines, currentDifficulty));
return btn;
}
private void setDifficulty(int r, int c, int m, String name) {
currentRows = r; currentCols = c; currentMines = m; currentDifficulty = name;
newGame(r, c, m, name);
}
private void showCustomDialog() {
JPanel panel = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(8, 10, 8, 10);
gbc.anchor = GridBagConstraints.WEST;
JTextField rowField = new JTextField("16", 8);
JTextField colField = new JTextField("16", 8);
JTextField mineField = new JTextField("40", 8);
Font fieldFont = new Font("微软雅黑", Font.PLAIN, 14);
for (JTextField f : new JTextField[]{rowField, colField, mineField}) {
f.setFont(fieldFont);
}
gbc.gridx = 0; gbc.gridy = 0;
panel.add(new JLabel("行数:"), gbc);
gbc.gridx = 1;
panel.add(rowField, gbc);
gbc.gridx = 0; gbc.gridy = 1;
panel.add(new JLabel("列数:"), gbc);
gbc.gridx = 1;
panel.add(colField, gbc);
gbc.gridx = 0; gbc.gridy = 2;
panel.add(new JLabel("雷数:"), gbc);
gbc.gridx = 1;
panel.add(mineField, gbc);
int result = JOptionPane.showConfirmDialog(this, panel, "自定义难度", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
if (result == JOptionPane.OK_OPTION) {
try {
int r = Integer.parseInt(rowField.getText().trim());
int c = Integer.parseInt(colField.getText().trim());
int m = Integer.parseInt(mineField.getText().trim());
if (r < 5 || c < 5 || m < 1 || m >= r * c) {
JOptionPane.showMessageDialog(this, "参数无效!\n行/列 ≥ 5,雷数 ≥ 1 且小于总格数", "错误", JOptionPane.ERROR_MESSAGE);
return;
}
setDifficulty(r, c, m, "自定义");
} catch (NumberFormatException ex) {
JOptionPane.showMessageDialog(this, "请输入有效的数字!", "错误", JOptionPane.ERROR_MESSAGE);
}
}
}
private void newGame(int rows, int cols, int mines, String diffName) {
if (timer != null) timer.stop();
game = new MinesweeperGame();
game.init(rows, cols, mines, diffName);
boardPanel.removeAll();
boardPanel.setLayout(new GridLayout(rows, cols, 1, 1));
buttons = new JButton[rows][cols];
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
JButton btn = new JButton();
btn.setPreferredSize(new Dimension(30, 30));
btn.setFont(new Font("微软雅黑", Font.BOLD, 16));
btn.setFocusPainted(false);
btn.setBackground(CELL_COVERED);
btn.setBorder(BorderFactory.createRaisedBevelBorder());
btn.setCursor(new Cursor(Cursor.HAND_CURSOR));
final int rr = r, cc = c;
btn.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
if (game.isGameOver()) return;
if (SwingUtilities.isLeftMouseButton(e)) {
handleLeftClick(rr, cc);
} else if (SwingUtilities.isRightMouseButton(e)) {
handleRightClick(rr, cc);
}
}
});
buttons[r][c] = btn;
boardPanel.add(btn);
}
}
updateBoard();
updateInfo();
faceButton.setText("☺");
faceButton.setBackground(BG_COLOR);
pack();
setLocationRelativeTo(null);
boardPanel.revalidate();
boardPanel.repaint();
}
private void handleLeftClick(int r, int c) {
if (game.reveal(r, c)) {
updateBoard();
updateInfo();
if (game.isGameOver()) {
timer.stop();
if (game.isWin()) {
faceButton.setText("☻");
faceButton.setBackground(new Color(180, 255, 180));
JOptionPane.showMessageDialog(this,
"恭喜你赢了!\n\n"
+ "难度: " + game.getDifficultyName() +
"\n用时: " + game.getElapsedTime() + " 秒\n步数: " + game.getSteps(),
"胜利", JOptionPane.INFORMATION_MESSAGE);
} else {
faceButton.setText("☹");
faceButton.setBackground(new Color(255, 180, 180));
revealAllMines();
JOptionPane.showMessageDialog(this,
"踩到地雷了!游戏结束。\n用时: " + game.getElapsedTime() + " 秒",
"失败", JOptionPane.ERROR_MESSAGE);
}
} else if (!timer.isRunning() && game.isStarted()) {
timer.start();
}
}
}
private void handleRightClick(int r, int c) {
game.toggleFlag(r, c);
updateBoard();
updateInfo();
}
private void updateBoard() {
MinesweeperGame.Cell[][] board = game.getBoard();
for (int r = 0; r < game.getRows(); r++) {
for (int c = 0; c < game.getCols(); c++) {
JButton btn = buttons[r][c];
MinesweeperGame.Cell cell = board[r][c];
if (cell.state == MinesweeperGame.CellState.REVEALED) {
btn.setBackground(CELL_REVEALED);
btn.setBorder(BorderFactory.createLoweredBevelBorder());
if (cell.isMine) {
btn.setText("✦");
btn.setFont(new Font("Segoe UI Symbol", Font.BOLD, 18));
btn.setForeground(Color.BLACK);
btn.setBackground(MINE_BG);
} else if (cell.neighborMines > 0) {
btn.setText(String.valueOf(cell.neighborMines));
btn.setFont(new Font("微软雅黑", Font.BOLD, 16));
btn.setForeground(NUMBER_COLORS[cell.neighborMines]);
} else {
btn.setText("");
}
} else if (cell.state == MinesweeperGame.CellState.FLAGGED) {
btn.setText("▶");
btn.setFont(new Font("Segoe UI Symbol", Font.BOLD, 14));
btn.setForeground(FLAG_COLOR);
btn.setBackground(CELL_COVERED);
btn.setBorder(BorderFactory.createRaisedBevelBorder());
} else if (cell.state == MinesweeperGame.CellState.QUESTIONED) {
btn.setText("?");
btn.setFont(new Font("微软雅黑", Font.BOLD, 16));
btn.setForeground(QUESTION_COLOR);
btn.setBackground(CELL_COVERED);
btn.setBorder(BorderFactory.createRaisedBevelBorder());
} else {
btn.setText("");
btn.setBackground(CELL_COVERED);
btn.setBorder(BorderFactory.createRaisedBevelBorder());
}
}
}
}
private void revealAllMines() {
MinesweeperGame.Cell[][] board = game.getBoard();
for (int r = 0; r < game.getRows(); r++) {
for (int c = 0; c < game.getCols(); c++) {
if (board[r][c].isMine) {
buttons[r][c].setText("✦");
buttons[r][c].setFont(new Font("Segoe UI Symbol", Font.BOLD, 18));
buttons[r][c].setForeground(Color.BLACK);
buttons[r][c].setBackground(MINE_BG);
buttons[r][c].setBorder(BorderFactory.createLoweredBevelBorder());
}
}
}
}
private void updateInfo() {
int remaining = game.getMineCount() - game.getFlaggedCount();
mineLabel.setText(String.format("%03d", Math.max(0, remaining)));
timeLabel.setText(String.format("%03d", game.getElapsedTime()));
stepLabel.setText(String.format("%03d", game.getSteps()));
}
private void updateTimer() {
if (game != null && game.isStarted() && !game.isGameOver()) {
timeLabel.setText(String.format("%03d", game.getElapsedTime()));
}
}
private void saveGame() {
if (game == null || !game.isStarted()) {
JOptionPane.showMessageDialog(this, "游戏尚未开始,无法保存!", "提示", JOptionPane.WARNING_MESSAGE);
return;
}
JFileChooser chooser = new JFileChooser();
chooser.setSelectedFile(new File("minesweeper.sav"));
if (chooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
File file = chooser.getSelectedFile();
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file))) {
oos.writeObject(new GameState(game));
JOptionPane.showMessageDialog(this, "保存成功!", "提示", JOptionPane.INFORMATION_MESSAGE);
} catch (IOException ex) {
JOptionPane.showMessageDialog(this, "保存失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
}
}
}
private void loadGame() {
JFileChooser chooser = new JFileChooser();
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
File file = chooser.getSelectedFile();
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
GameState state = (GameState) ois.readObject();
restoreGame(state);
JOptionPane.showMessageDialog(this, "读取成功!", "提示", JOptionPane.INFORMATION_MESSAGE);
} catch (IOException | ClassNotFoundException ex) {
JOptionPane.showMessageDialog(this, "读取失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
}
}
}
private void restoreGame(GameState state) {
if (timer != null) timer.stop();
currentRows = state.getRows();
currentCols = state.getCols();
currentMines = state.getMineCount();
currentDifficulty = state.getDifficultyName();
game = new MinesweeperGame();
game.init(currentRows, currentCols, currentMines, currentDifficulty);
game.setBoard(state.getBoard());
game.setRevealedCount(state.getRevealedCount());
game.setFlaggedCount(state.getFlaggedCount());
game.setGameOver(state.isGameOver());
game.setWin(state.isWin());
game.setStarted(state.isStarted());
game.setElapsedTime(state.getElapsedTime());
game.setSteps(state.getSteps());
boardPanel.removeAll();
boardPanel.setLayout(new GridLayout(currentRows, currentCols, 1, 1));
buttons = new JButton[currentRows][currentCols];
for (int r = 0; r < currentRows; r++) {
for (int c = 0; c < currentCols; c++) {
JButton btn = new JButton();
btn.setPreferredSize(new Dimension(30, 30));
btn.setFont(new Font("微软雅黑", Font.BOLD, 16));
btn.setFocusPainted(false);
btn.setBackground(CELL_COVERED);
btn.setBorder(BorderFactory.createRaisedBevelBorder());
btn.setCursor(new Cursor(Cursor.HAND_CURSOR));
final int rr = r, cc = c;
btn.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
if (game.isGameOver()) return;
if (SwingUtilities.isLeftMouseButton(e)) {
handleLeftClick(rr, cc);
} else if (SwingUtilities.isRightMouseButton(e)) {
handleRightClick(rr, cc);
}
}
});
buttons[r][c] = btn;
boardPanel.add(btn);
}
}
updateBoard();
updateInfo();
if (game.isStarted() && !game.isGameOver()) {
timer.start();
faceButton.setText("☺");
faceButton.setBackground(BG_COLOR);
} else if (game.isGameOver()) {
if (game.isWin()) {
faceButton.setText("☻");
faceButton.setBackground(new Color(180, 255, 180));
} else {
faceButton.setText("☹");
faceButton.setBackground(new Color(255, 180, 180));
revealAllMines();
}
} else {
faceButton.setText("☺");
faceButton.setBackground(BG_COLOR);
}
pack();
setLocationRelativeTo(null);
boardPanel.revalidate();
boardPanel.repaint();
}
}
MinesweeperApp.java
java
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.plaf.metal.MetalLookAndFeel;
/**
* 程序入口:设置系统外观并启动扫雷界面
*/
public class MinesweeperApp {
public static void main(String[] args) {
try {
UIManager.setLookAndFeel(new MetalLookAndFeel());
} catch (Exception e) {
e.printStackTrace();
}
SwingUtilities.invokeLater(() -> new MinesweeperUI().setVisible(true));
}
}
三、项目三:星际射击游戏
3.1 需求分析
在扫雷的回合制基础上,升级为 实时动作游戏:玩家控制飞船,自动/手动发射子弹,消灭下落的敌人,支持粒子爆炸特效、暂停、难度梯度。
3.2 架构设计
采用 双 Timer 架构:
- 逻辑 Timer(16ms):更新实体位置、碰撞检测、生成敌人
- 渲染 Timer (16ms):触发
repaint(),与逻辑解耦
实体采用 抽象基类 + 匿名内部类 实现多态绘制。
3.3 程序界面
3.4 完整代码
GameEntity.java
java
import java.awt.*;
import java.io.Serializable;
/**
* 游戏实体基类:玩家、敌人、子弹都继承此类
*/
public abstract class GameEntity implements Serializable {
private static final long serialVersionUID = 1L;
protected double x, y;
protected int width, height;
protected double speedX, speedY;
protected boolean alive = true;
protected Color color;
protected String type;
public GameEntity(double x, double y, int width, int height, double speedX, double speedY, Color color, String type) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.speedX = speedX;
this.speedY = speedY;
this.color = color;
this.type = type;
}
public void update() {
x += speedX;
y += speedY;
}
public Rectangle getBounds() {
return new Rectangle((int)x, (int)y, width, height);
}
public boolean intersects(GameEntity other) {
return getBounds().intersects(other.getBounds());
}
public abstract void draw(Graphics2D g2d);
public double getX() { return x; }
public double getY() { return y; }
public int getWidth() { return width; }
public int getHeight() { return height; }
public boolean isAlive() { return alive; }
public void setAlive(boolean alive) { this.alive = alive; }
public String getType() { return type; }
public void setX(double x) { this.x = x; }
public void setY(double y) { this.y = y; }
public void setSpeedX(double speedX) { this.speedX = speedX; }
public void setSpeedY(double speedY) { this.speedY = speedY; }
}
ShootingGame.java
java
import java.awt.*;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
/**
* 射击游戏核心:管理所有实体、碰撞检测、分数、难度、存档
*/
public class ShootingGame implements Serializable {
private static final long serialVersionUID = 1L;
public enum Difficulty { EASY, MEDIUM, HARD }
private GameEntity player;
private int playerLives = 3;
private int maxLives = 3;
private List<GameEntity> enemies;
private List<GameEntity> bullets;
private List<GameEntity> particles;
private boolean running = false;
private boolean paused = false;
private boolean gameOver = false;
private int score = 0;
private int killCount = 0;
private int shotCount = 0;
private int difficulty = 1;
private String difficultyName = "简单";
private transient long startTime;
private int elapsedTime = 0;
private int enemySpawnTimer = 0;
private int enemySpawnInterval = 60;
private int enemySpeedBase = 2;
private Random random = new Random();
private int width = 600;
private int height = 700;
public void init(int width, int height, Difficulty diff) {
this.width = width;
this.height = height;
this.difficulty = diff.ordinal() + 1;
this.difficultyName = diff.name().equals("EASY") ? "简单" : diff.name().equals("MEDIUM") ? "中等" : "困难";
switch (diff) {
case EASY:
enemySpawnInterval = 80;
enemySpeedBase = 1;
maxLives = 5;
break;
case MEDIUM:
enemySpawnInterval = 50;
enemySpeedBase = 2;
maxLives = 3;
break;
case HARD:
enemySpawnInterval = 30;
enemySpeedBase = 3;
maxLives = 2;
break;
}
playerLives = maxLives;
score = 0;
killCount = 0;
shotCount = 0;
elapsedTime = 0;
gameOver = false;
paused = false;
player = new GameEntity(width / 2 - 20, height - 80, 40, 40, 0, 0, new Color(50, 130, 220), "player") {
@Override
public void draw(Graphics2D g2d) {
g2d.setColor(color);
int[] xs = {(int)x + width/2, (int)x, (int)x + width};
int[] ys = {(int)y, (int)y + height, (int)y + height};
g2d.fillPolygon(xs, ys, 3);
g2d.setColor(Color.CYAN);
g2d.fillOval((int)x + 15, (int)y + 20, 10, 10);
}
};
enemies = new ArrayList<>();
bullets = new ArrayList<>();
particles = new ArrayList<>();
running = true;
startTime = System.currentTimeMillis();
}
public void update() {
if (!running || paused || gameOver) return;
elapsedTime = (int)((System.currentTimeMillis() - startTime) / 1000);
player.update();
if (player.getX() < 0) player.setX(0);
if (player.getX() > width - player.getWidth()) player.setX(width - player.getWidth());
enemySpawnTimer++;
if (enemySpawnTimer >= enemySpawnInterval) {
enemySpawnTimer = 0;
spawnEnemy();
}
Iterator<GameEntity> bit = bullets.iterator();
while (bit.hasNext()) {
GameEntity b = bit.next();
b.update();
if (b.getY() < -10) b.setAlive(false);
}
Iterator<GameEntity> eit = enemies.iterator();
while (eit.hasNext()) {
GameEntity e = eit.next();
e.update();
if (e.getY() > height) {
e.setAlive(false);
playerLives--;
createExplosion(e.getX() + e.getWidth()/2, e.getY() + e.getHeight(), Color.RED);
if (playerLives <= 0) {
gameOver = true;
running = false;
}
}
}
Iterator<GameEntity> pit = particles.iterator();
while (pit.hasNext()) {
GameEntity p = pit.next();
p.update();
if (p.getY() > p.getY() + 50 || !p.isAlive()) pit.remove();
}
for (GameEntity b : bullets) {
if (!b.isAlive()) continue;
for (GameEntity e : enemies) {
if (!e.isAlive()) continue;
if (b.intersects(e)) {
b.setAlive(false);
e.setAlive(false);
score += 10;
killCount++;
createExplosion(e.getX() + e.getWidth()/2, e.getY() + e.getHeight()/2, e.color);
break;
}
}
}
for (GameEntity e : enemies) {
if (!e.isAlive()) continue;
if (e.intersects(player)) {
e.setAlive(false);
playerLives--;
createExplosion(player.getX() + 20, player.getY(), Color.ORANGE);
if (playerLives <= 0) {
gameOver = true;
running = false;
}
}
}
bullets.removeIf(b -> !b.isAlive());
enemies.removeIf(e -> !e.isAlive());
}
private void spawnEnemy() {
int w = 30 + random.nextInt(20);
int h = 30 + random.nextInt(20);
int ex = random.nextInt(width - w);
int speed = enemySpeedBase + random.nextInt(2);
Color[] colors = {Color.RED, Color.MAGENTA, Color.ORANGE, new Color(200, 50, 50)};
Color c = colors[random.nextInt(colors.length)];
GameEntity enemy = new GameEntity(ex, -h, w, h, 0, speed, c, "enemy") {
@Override
public void draw(Graphics2D g2d) {
g2d.setColor(color);
g2d.fillOval((int)x, (int)y, width, height);
g2d.setColor(Color.WHITE);
g2d.fillOval((int)x + 5, (int)y + 5, width - 10, height / 3);
}
};
enemies.add(enemy);
}
public void shoot() {
if (!running || paused || gameOver) return;
shotCount++;
GameEntity bullet = new GameEntity(
player.getX() + player.getWidth()/2 - 3,
player.getY() - 10,
6, 12, 0, -8, new Color(255, 220, 50), "bullet"
) {
@Override
public void draw(Graphics2D g2d) {
g2d.setColor(color);
g2d.fillRect((int)x, (int)y, width, height);
g2d.setColor(Color.WHITE);
g2d.fillRect((int)x + 2, (int)y, 2, 4);
}
};
bullets.add(bullet);
}
public void movePlayer(int dx) {
if (player != null) {
player.setX(player.getX() + dx);
}
}
public void setPlayerX(double x) {
if (player != null) player.setX(x - player.getWidth()/2);
}
private void createExplosion(double cx, double cy, Color c) {
for (int i = 0; i < 8; i++) {
double angle = Math.random() * Math.PI * 2;
double speed = 1 + Math.random() * 3;
final double sx = Math.cos(angle) * speed;
final double sy = Math.sin(angle) * speed;
final Color pc = c;
GameEntity p = new GameEntity(cx, cy, 4, 4, sx, sy, pc, "particle") {
private int life = 20;
@Override
public void update() {
super.update();
life--;
if (life <= 0) setAlive(false);
}
@Override
public void draw(Graphics2D g2d) {
g2d.setColor(pc);
g2d.fillOval((int)x, (int)y, width, height);
}
};
particles.add(p);
}
}
public void draw(Graphics2D g2d) {
g2d.setColor(new Color(20, 20, 40));
g2d.fillRect(0, 0, width, height);
g2d.setColor(new Color(255, 255, 255, 80));
for (int i = 0; i < 50; i++) {
int sx = (i * 37 + 13) % width;
int sy = (i * 23 + 7) % height;
g2d.fillOval(sx, (sy + elapsedTime * 10) % height, 2, 2);
}
for (GameEntity p : particles) p.draw(g2d);
for (GameEntity e : enemies) e.draw(g2d);
for (GameEntity b : bullets) b.draw(g2d);
if (player != null && playerLives > 0) player.draw(g2d);
g2d.setColor(Color.WHITE);
g2d.setFont(new Font("微软雅黑", Font.BOLD, 16));
g2d.drawString("分数: " + score, 15, 25);
g2d.drawString("时间: " + elapsedTime + "秒", 150, 25);
g2d.drawString("击杀: " + killCount, 280, 25);
g2d.drawString("发射: " + shotCount, 400, 25);
g2d.drawString("生命: ", 15, 50);
for (int i = 0; i < maxLives; i++) {
if (i < playerLives) {
g2d.setColor(Color.RED);
g2d.fillOval(60 + i * 20, 38, 12, 12);
} else {
g2d.setColor(Color.GRAY);
g2d.drawOval(60 + i * 20, 38, 12, 12);
}
}
g2d.setColor(Color.YELLOW);
g2d.drawString("难度: " + difficultyName, width - 100, 25);
if (gameOver) {
g2d.setColor(new Color(0, 0, 0, 180));
g2d.fillRect(0, 0, width, height);
g2d.setColor(Color.WHITE);
g2d.setFont(new Font("微软雅黑", Font.BOLD, 40));
String msg = playerLives <= 0 ? "游戏结束" : "胜利";
int msgW = g2d.getFontMetrics().stringWidth(msg);
g2d.drawString(msg, (width - msgW) / 2, height / 2 - 40);
g2d.setFont(new Font("微软雅黑", Font.PLAIN, 20));
String info = "最终得分: " + score + " 击杀: " + killCount + " 用时: " + elapsedTime + "秒";
int infoW = g2d.getFontMetrics().stringWidth(info);
g2d.drawString(info, (width - infoW) / 2, height / 2 + 10);
g2d.setFont(new Font("微软雅黑", Font.PLAIN, 16));
String hint = "按 R 重新开始 或 空格 发射";
int hintW = g2d.getFontMetrics().stringWidth(hint);
g2d.drawString(hint, (width - hintW) / 2, height / 2 + 50);
}
if (paused && !gameOver) {
g2d.setColor(new Color(0, 0, 0, 120));
g2d.fillRect(0, 0, width, height);
g2d.setColor(Color.YELLOW);
g2d.setFont(new Font("微软雅黑", Font.BOLD, 36));
String msg = "暂 停";
int msgW = g2d.getFontMetrics().stringWidth(msg);
g2d.drawString(msg, (width - msgW) / 2, height / 2);
g2d.setFont(new Font("微软雅黑", Font.PLAIN, 16));
g2d.drawString("按 P 继续", (width - 60) / 2, height / 2 + 40);
}
}
public boolean isRunning() { return running; }
public boolean isPaused() { return paused; }
public boolean isGameOver() { return gameOver; }
public int getScore() { return score; }
public int getKillCount() { return killCount; }
public int getShotCount() { return shotCount; }
public int getElapsedTime() { return elapsedTime; }
public int getPlayerLives() { return playerLives; }
public int getDifficulty() { return difficulty; }
public String getDifficultyName() { return difficultyName; }
public int getWidth() { return width; }
public int getHeight() { return height; }
public void setPaused(boolean paused) {
this.paused = paused;
if (!paused) {
startTime = System.currentTimeMillis() - elapsedTime * 1000L;
}
}
public void setRunning(boolean running) { this.running = running; }
public void setGameOver(boolean gameOver) { this.gameOver = gameOver; }
public void setScore(int score) { this.score = score; }
public void setKillCount(int killCount) { this.killCount = killCount; }
public void setShotCount(int shotCount) { this.shotCount = shotCount; }
public void setElapsedTime(int elapsedTime) { this.elapsedTime = elapsedTime; }
public void setPlayerLives(int playerLives) { this.playerLives = playerLives; }
public void setDifficulty(int difficulty) { this.difficulty = difficulty; }
public void setDifficultyName(String difficultyName) { this.difficultyName = difficultyName; }
public void setStartTime(long startTime) { this.startTime = startTime; }
public List<GameEntity> getEnemies() { return enemies; }
public List<GameEntity> getBullets() { return bullets; }
public List<GameEntity> getParticles() { return particles; }
public GameEntity getPlayer() { return player; }
public void setPlayer(GameEntity player) { this.player = player; }
public void setEnemies(List<GameEntity> enemies) { this.enemies = enemies; }
public void setBullets(List<GameEntity> bullets) { this.bullets = bullets; }
public void setParticles(List<GameEntity> particles) { this.particles = particles; }
public void setMaxLives(int maxLives) { this.maxLives = maxLives; }
public int getMaxLives() { return maxLives; }
public void setEnemySpawnInterval(int interval) { this.enemySpawnInterval = interval; }
public void setEnemySpeedBase(int speed) { this.enemySpeedBase = speed; }
public int getEnemySpawnInterval() { return enemySpawnInterval; }
public int getEnemySpeedBase() { return enemySpeedBase; }
}
GameState.java(射击)
java
import java.io.Serializable;
import java.util.List;
/**
* 射击游戏存档状态
*/
public class GameState implements Serializable {
private static final long serialVersionUID = 1L;
private List<GameEntity> enemies, bullets, particles;
private GameEntity player;
private int playerLives, maxLives;
private int score, killCount, shotCount, elapsedTime;
private boolean running, paused, gameOver;
private int difficulty, enemySpawnInterval, enemySpeedBase;
private String difficultyName;
private int width, height;
public GameState(ShootingGame game) {
this.enemies = game.getEnemies();
this.bullets = game.getBullets();
this.particles = game.getParticles();
this.player = game.getPlayer();
this.playerLives = game.getPlayerLives();
this.maxLives = game.getMaxLives();
this.score = game.getScore();
this.killCount = game.getKillCount();
this.shotCount = game.getShotCount();
this.elapsedTime = game.getElapsedTime();
this.running = game.isRunning();
this.paused = game.isPaused();
this.gameOver = game.isGameOver();
this.difficulty = game.getDifficulty();
this.difficultyName = game.getDifficultyName();
this.enemySpawnInterval = game.getEnemySpawnInterval();
this.enemySpeedBase = game.getEnemySpeedBase();
this.width = game.getWidth();
this.height = game.getHeight();
}
public List<GameEntity> getEnemies() { return enemies; }
public List<GameEntity> getBullets() { return bullets; }
public List<GameEntity> getParticles() { return particles; }
public GameEntity getPlayer() { return player; }
public int getPlayerLives() { return playerLives; }
public int getMaxLives() { return maxLives; }
public int getScore() { return score; }
public int getKillCount() { return killCount; }
public int getShotCount() { return shotCount; }
public int getElapsedTime() { return elapsedTime; }
public boolean isRunning() { return running; }
public boolean isPaused() { return paused; }
public boolean isGameOver() { return gameOver; }
public int getDifficulty() { return difficulty; }
public String getDifficultyName() { return difficultyName; }
public int getEnemySpawnInterval() { return enemySpawnInterval; }
public int getEnemySpeedBase() { return enemySpeedBase; }
public int getWidth() { return width; }
public int getHeight() { return height; }
}
ShootingGameUI.java
java
import javax.swing.*;
import javax.swing.border.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
/**
* 射击游戏 Swing 界面:游戏面板、菜单、难度选择、存档
*/
public class ShootingGameUI extends JFrame {
private ShootingGame game;
private GamePanel gamePanel;
private Timer gameTimer;
private Timer renderTimer;
private int currentDifficulty = 1;
public ShootingGameUI() {
setTitle("星际射击");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setResizable(false);
initMenu();
initComponents();
showWelcome();
}
private void initMenu() {
JMenuBar menuBar = new JMenuBar();
JMenu gameMenu = new JMenu("游戏(G)");
gameMenu.setMnemonic(KeyEvent.VK_G);
gameMenu.setFont(new Font("微软雅黑", Font.PLAIN, 13));
JMenuItem newItem = new JMenuItem("新游戏(N)", KeyEvent.VK_N);
newItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, InputEvent.CTRL_DOWN_MASK));
newItem.setFont(new Font("微软雅黑", Font.PLAIN, 13));
newItem.addActionListener(e -> showWelcome());
JMenuItem pauseItem = new JMenuItem("暂停/继续(P)", KeyEvent.VK_P);
pauseItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_P, 0));
pauseItem.setFont(new Font("微软雅黑", Font.PLAIN, 13));
pauseItem.addActionListener(e -> togglePause());
JMenuItem saveItem = new JMenuItem("保存进度(S)", KeyEvent.VK_S);
saveItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK));
saveItem.setFont(new Font("微软雅黑", Font.PLAIN, 13));
saveItem.addActionListener(e -> saveGame());
JMenuItem loadItem = new JMenuItem("读取进度(L)", KeyEvent.VK_L);
loadItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.CTRL_DOWN_MASK));
loadItem.setFont(new Font("微软雅黑", Font.PLAIN, 13));
loadItem.addActionListener(e -> loadGame());
JMenu diffMenu = new JMenu("难度");
diffMenu.setFont(new Font("微软雅黑", Font.PLAIN, 13));
JMenuItem easyItem = new JMenuItem("简单");
JMenuItem medItem = new JMenuItem("中等");
JMenuItem hardItem = new JMenuItem("困难");
for (JMenuItem item : new JMenuItem[]{easyItem, medItem, hardItem}) {
item.setFont(new Font("微软雅黑", Font.PLAIN, 13));
}
easyItem.addActionListener(e -> startNewGame(ShootingGame.Difficulty.EASY));
medItem.addActionListener(e -> startNewGame(ShootingGame.Difficulty.MEDIUM));
hardItem.addActionListener(e -> startNewGame(ShootingGame.Difficulty.HARD));
diffMenu.add(easyItem); diffMenu.add(medItem); diffMenu.add(hardItem);
JMenuItem exitItem = new JMenuItem("退出(Q)");
exitItem.setFont(new Font("微软雅黑", Font.PLAIN, 13));
exitItem.addActionListener(e -> System.exit(0));
gameMenu.add(newItem); gameMenu.add(pauseItem);
gameMenu.addSeparator(); gameMenu.add(saveItem); gameMenu.add(loadItem);
gameMenu.addSeparator(); gameMenu.add(diffMenu); gameMenu.addSeparator(); gameMenu.add(exitItem);
JMenu helpMenu = new JMenu("帮助(H)");
helpMenu.setMnemonic(KeyEvent.VK_H);
helpMenu.setFont(new Font("微软雅黑", Font.PLAIN, 13));
JMenuItem aboutItem = new JMenuItem("关于");
aboutItem.setFont(new Font("微软雅黑", Font.PLAIN, 13));
aboutItem.addActionListener(e -> JOptionPane.showMessageDialog(this,
"星际射击游戏 v1.0\n\n"
+ "【操作说明】\n"
+ "• 鼠标移动:控制飞船左右移动\n"
+ "• 空格键:发射子弹\n"
+ "• P 键:暂停/继续\n"
+ "• R 键:重新开始\n\n"
+ "【功能特性】\n"
+ "• 三种难度(简单/中等/困难)\n"
+ "• 实时计时、计分、计击杀\n"
+ "• 粒子爆炸特效\n"
+ "• 保存/读取游戏进度",
"关于", JOptionPane.INFORMATION_MESSAGE));
helpMenu.add(aboutItem);
menuBar.add(gameMenu); menuBar.add(helpMenu);
setJMenuBar(menuBar);
}
private void initComponents() {
setLayout(new BorderLayout());
gamePanel = new GamePanel();
add(gamePanel, BorderLayout.CENTER);
pack();
setLocationRelativeTo(null);
}
private void showWelcome() {
if (gameTimer != null) gameTimer.stop();
if (renderTimer != null) renderTimer.stop();
String[] options = {"简单(5条命,敌人慢)", "中等(3条命,正常)", "困难(2条命,敌人快)"};
int choice = JOptionPane.showOptionDialog(this,
"选择游戏难度", "星际射击",
JOptionPane.DEFAULT_OPTION, JOptionPane.PLAIN_MESSAGE,
null, options, options[0]);
if (choice == JOptionPane.CLOSED_OPTION) {
System.exit(0);
}
ShootingGame.Difficulty diff = choice == 0 ? ShootingGame.Difficulty.EASY :
choice == 1 ? ShootingGame.Difficulty.MEDIUM : ShootingGame.Difficulty.HARD;
startNewGame(diff);
}
private void startNewGame(ShootingGame.Difficulty diff) {
if (gameTimer != null) gameTimer.stop();
if (renderTimer != null) renderTimer.stop();
game = new ShootingGame();
game.init(600, 700, diff);
currentDifficulty = diff.ordinal() + 1;
gameTimer = new Timer(16, e -> {
game.update();
gamePanel.repaint();
if (game.isGameOver()) {
gameTimer.stop();
}
});
gameTimer.start();
renderTimer = new Timer(16, e -> gamePanel.repaint());
renderTimer.start();
requestFocus();
}
private void togglePause() {
if (game == null || game.isGameOver()) return;
game.setPaused(!game.isPaused());
gamePanel.repaint();
}
private void saveGame() {
if (game == null || !game.isRunning()) {
JOptionPane.showMessageDialog(this, "游戏尚未开始,无法保存!", "提示", JOptionPane.WARNING_MESSAGE);
return;
}
boolean wasPaused = game.isPaused();
game.setPaused(true);
JFileChooser chooser = new JFileChooser();
chooser.setSelectedFile(new File("shooting.sav"));
if (chooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
File file = chooser.getSelectedFile();
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file))) {
oos.writeObject(new GameState(game));
JOptionPane.showMessageDialog(this, "保存成功!", "提示", JOptionPane.INFORMATION_MESSAGE);
} catch (IOException ex) {
JOptionPane.showMessageDialog(this, "保存失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
}
}
game.setPaused(wasPaused);
}
private void loadGame() {
JFileChooser chooser = new JFileChooser();
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
File file = chooser.getSelectedFile();
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
GameState state = (GameState) ois.readObject();
restoreGame(state);
JOptionPane.showMessageDialog(this, "读取成功!", "提示", JOptionPane.INFORMATION_MESSAGE);
} catch (IOException | ClassNotFoundException ex) {
JOptionPane.showMessageDialog(this, "读取失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
}
}
}
private void restoreGame(GameState state) {
if (gameTimer != null) gameTimer.stop();
if (renderTimer != null) renderTimer.stop();
game = new ShootingGame();
game.init(state.getWidth(), state.getHeight(),
state.getDifficulty() == 1 ? ShootingGame.Difficulty.EASY :
state.getDifficulty() == 2 ? ShootingGame.Difficulty.MEDIUM : ShootingGame.Difficulty.HARD);
game.setPlayer(state.getPlayer());
game.setEnemies(state.getEnemies());
game.setBullets(state.getBullets());
game.setParticles(state.getParticles());
game.setPlayerLives(state.getPlayerLives());
game.setMaxLives(state.getMaxLives());
game.setScore(state.getScore());
game.setKillCount(state.getKillCount());
game.setShotCount(state.getShotCount());
game.setElapsedTime(state.getElapsedTime());
game.setRunning(state.isRunning());
game.setPaused(true);
game.setGameOver(state.isGameOver());
game.setDifficulty(state.getDifficulty());
game.setDifficultyName(state.getDifficultyName());
game.setEnemySpawnInterval(state.getEnemySpawnInterval());
game.setEnemySpeedBase(state.getEnemySpeedBase());
game.setStartTime(System.currentTimeMillis() - state.getElapsedTime() * 1000L);
gameTimer = new Timer(16, e -> {
game.update();
gamePanel.repaint();
if (game.isGameOver()) gameTimer.stop();
});
gameTimer.start();
renderTimer = new Timer(16, e -> gamePanel.repaint());
renderTimer.start();
gamePanel.repaint();
requestFocus();
}
private class GamePanel extends JPanel {
public GamePanel() {
setPreferredSize(new Dimension(600, 700));
setBackground(Color.BLACK);
setFocusable(true);
addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseMoved(MouseEvent e) {
if (game != null && !game.isPaused() && !game.isGameOver()) {
game.setPlayerX(e.getX());
}
}
});
addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
int key = e.getKeyCode();
if (key == KeyEvent.VK_SPACE) {
if (game != null && !game.isGameOver()) {
game.shoot();
}
} else if (key == KeyEvent.VK_P) {
togglePause();
} else if (key == KeyEvent.VK_R) {
if (game != null && game.isGameOver()) {
startNewGame(ShootingGame.Difficulty.values()[currentDifficulty - 1]);
}
} else if (key == KeyEvent.VK_LEFT || key == KeyEvent.VK_A) {
if (game != null) game.movePlayer(-20);
} else if (key == KeyEvent.VK_RIGHT || key == KeyEvent.VK_D) {
if (game != null) game.movePlayer(20);
}
}
});
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
if (game != null) {
game.draw(g2d);
} else {
g2d.setColor(Color.BLACK);
g2d.fillRect(0, 0, getWidth(), getHeight());
g2d.setColor(Color.WHITE);
g2d.setFont(new Font("微软雅黑", Font.BOLD, 24));
g2d.drawString("按 游戏->新游戏 开始", 180, 350);
}
}
}
}
ShootingGameApp.java
java
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.plaf.metal.MetalLookAndFeel;
/**
* 射击游戏入口
*/
public class ShootingGameApp {
public static void main(String[] args) {
try {
UIManager.setLookAndFeel(new MetalLookAndFeel());
} catch (Exception e) {
e.printStackTrace();
}
SwingUtilities.invokeLater(() -> new ShootingGameUI().setVisible(true));
}
}
四、运行指南
环境要求
- JDK 8 及以上
- IntelliJ IDEA 或 Eclipse
编译运行
bash
# 算术系统
cd ArithmeticGame
javac *.java
java ArithmeticGame
# 扫雷
cd Minesweeper
javac *.java
java MinesweeperApp
# 射击游戏
cd ShootingGame
javac *.java
java ShootingGameApp
操作速查表
| 系统 | 操作 |
|---|---|
| 算术 | 回车提交答案;按钮开始/提交/下一题 |
| 扫雷 | 左键揭开;右键标记;笑脸重置;Ctrl+S/O 存读档 |
| 射击 | 鼠标移动飞船;空格发射;P暂停;R重启;Ctrl+S/O 存读档 |
五、结语
本文提供的三个项目均可在标准 JDK 环境下直接编译运行,代码结构清晰、注释完整,适合作为 Java Swing 课程的实验参考。欢迎在此基础上继续扩展:为算术系统增加排行榜,为扫雷添加音效,为射击游戏加入 Boss 战与道具系统。
完整源码:三个系统的核心设计思路与全部代码已在文中尽数呈现,按章节组织即可直接复现。