这是一个Java项目,经典的飞机大战游戏,可以播放音乐。
项目代码
1. 首先新建一个主类
package org.example;
import com.google.common.collect.Lists;
import com.google.common.io.Resources;
import javax.sound.sampled.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.*;
import java.awt.image.BufferedImage;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import javax.imageio.ImageIO;
import javax.swing.Timer;
/**
* 优化后的游戏面板(支持GIF动画、静态图片、背景音乐和暂停功能)
*/
public class GamePanel extends JPanel {
// 常量定义
public static final int WIDTH = 360;
public static final int HEIGHT = 600;
private GameState state = GameState.START;
private int scores = 0;
private long musicPosition = 0;
// 图像资源
public static BufferedImage startImage, backgroundImage, enemyImage;
public static BufferedImage airdropImage, ammoImage, gameoverImage;
public static ImageIcon playerGif; // GIF动画使用ImageIcon
// 游戏对象集合
private final List<FlyModel> flyModels = Lists.newArrayList();
private final List<Ammo> ammos = Lists.newArrayList();
private Player player = new Player();
private final Object musicLock = new Object(); // 音乐线程同步锁
private Clip currentMusicClip;
private final AtomicBoolean paused = new AtomicBoolean(false);
private float volume = 0.5f; // 默认音量50%
private final List<URL> musicUrls = new ArrayList<>();
private int currentMusicIndex = 0;
// 图像加载
static {
try {
startImage = loadImageResource("start");
backgroundImage = loadImageResource("background");
enemyImage = loadImageResource("enemy");
airdropImage = loadImageResource("airdrop");
ammoImage = loadImageResource("ammo");
gameoverImage = loadImageResource("gameover");
// 加载GIF动图
playerGif = loadGifImage("player_airplane.gif");
} catch (IOException e) {
JOptionPane.showMessageDialog(null, "资源加载失败: " + e.getMessage());
System.exit(1);
}
}
private static BufferedImage loadImageResource(String n) throws IOException {
String name = n + ".png";
URL url = Resources.getResource(name);
return ImageIO.read(url);
}
/**
* 加载GIF动画
*/
private static ImageIcon loadGifImage(String name) throws IOException {
URL res = Resources.getResource(name);
return new ImageIcon(res);
}
public GamePanel() {
setDoubleBuffered(true); // 启用双缓冲减少闪烁
setFocusable(true); // 允许键盘焦点
initAudio(); // 初始化音频系统
}
private void findAudioFiles() {
try {
// 获取sounds目录的URL(开发环境或JAR环境)
Enumeration<URL> soundsDirs = GamePanel.class.getClassLoader().getResources("sounds");
while (soundsDirs.hasMoreElements()) {
URL soundsDirUrl = soundsDirs.nextElement();
if ("jar".equals(soundsDirUrl.getProtocol())) {
// 解析JAR文件路径
String jarPath = soundsDirUrl.getPath().split("!")[0].replace("file:", "");
try (JarFile jar = new JarFile(jarPath)) {
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String name = entry.getName();
// 过滤sounds目录下的音频文件
if (name.startsWith("sounds/") && !entry.isDirectory() &&
(name.endsWith(".mp3") || name.endsWith(".wav"))) {
// 使用类加载器获取资源URL
URL audioUrl = GamePanel.class.getClassLoader().getResource(name);
if (audioUrl != null) {
musicUrls.add(audioUrl);
}
}
}
}
} else if ("file".equals(soundsDirUrl.getProtocol())) {
// 开发环境处理(保持不变)
File dir = new File(soundsDirUrl.toURI());
File[] files = dir.listFiles((f) -> f.getName().endsWith(".mp3") || f.getName().endsWith(".wav"));
if (files != null) {
for (File file : files) {
musicUrls.add(file.toURI().toURL());
}
}
}
}
} catch (Exception e) {
System.err.println("加载音频失败: " + e.getMessage());
}
}
/**
* 初始化音频系统(使用专用线程替代线程池)
*/
private void initAudio() {
findAudioFiles();
if (musicUrls.isEmpty()) {
System.err.println("❌ 未找到音频文件. 请检查:");
System.err.println("1. 文件是否在 src/main/resources/sounds/ 目录下");
System.err.println("2. 文件扩展名是否为 .mp3 或 .wav");
System.err.println("3. Maven/Gradle 是否将资源打包进JAR");
// 打印类路径下所有sounds资源
try {
Enumeration<URL> urls = getClass().getClassLoader().getResources("sounds");
while (urls.hasMoreElements()) {
System.err.println("找到资源目录: " + urls.nextElement());
}
} catch (IOException e) {
e.printStackTrace();
}
return;
}
// 创建专用音乐线程(避免递归调用栈溢出)
Thread musicThread = new Thread(this::playMusicLoop, "music-player");
musicThread.setDaemon(true); // 设置为守护线程
musicThread.start();
}
/**
* 音乐循环播放核心逻辑(避免递归调用)
*/
private void playMusicLoop() {
while (!Thread.currentThread().isInterrupted()) {
if (musicUrls.isEmpty()) {
throw new RuntimeException("音乐列表为空");
}
try {
synchronized (musicLock) {
playCurrentMusic();
// 等待当前音乐播放完成
musicLock.wait();
}
} catch (Exception e) {
System.err.println("音乐播放异常: " + e.getMessage());
}
// 切换到下一首
currentMusicIndex = (currentMusicIndex + 1) % musicUrls.size();
}
}
private void playCurrentMusic() throws Exception {
URL musicUrl = musicUrls.get(currentMusicIndex);
System.out.println("播放音频: " + musicUrl.getFile());
try (InputStream audioStream = musicUrl.openStream()) {
AudioInputStream audioInput = AudioSystem.getAudioInputStream(audioStream);
AudioFormat baseFormat = audioInput.getFormat();
AudioFormat targetFormat = new AudioFormat(
AudioFormat.Encoding.PCM_SIGNED,
baseFormat.getSampleRate(),
16,
baseFormat.getChannels(),
baseFormat.getChannels() * 2,
baseFormat.getSampleRate(),
false
);
try (AudioInputStream pcmStream = AudioSystem.getAudioInputStream(targetFormat, audioInput)) {
// 关闭旧资源
if (currentMusicClip != null) {
currentMusicClip.close();
}
currentMusicClip = AudioSystem.getClip();
currentMusicClip.open(pcmStream);
setVolume(volume);
currentMusicClip.addLineListener(event -> {
if (event.getType() == LineEvent.Type.STOP && !paused.get()) {
synchronized (musicLock) { musicLock.notify(); }
}
});
currentMusicClip.start();
}
}
}
private void setVolume(float volume) {
this.volume = volume;
if (currentMusicClip != null && currentMusicClip.isControlSupported(FloatControl.Type.MASTER_GAIN)) {
FloatControl gainControl = (FloatControl) currentMusicClip.getControl(FloatControl.Type.MASTER_GAIN);
float dB = (float) (Math.log(volume) / Math.log(10.0) * 20.0);
dB = Math.max(gainControl.getMinimum(), Math.min(gainControl.getMaximum(), dB));
gainControl.setValue(dB);
}
}
private void togglePause() {
boolean nowPaused = paused.getAndSet(!paused.get());
state = nowPaused ? GameState.RUNNING : GameState.PAUSE;
if (currentMusicClip != null) {
if (nowPaused) {
// 恢复播放:设置位置后启动
currentMusicClip.setMicrosecondPosition(musicPosition); // 关键修复点
currentMusicClip.start();
} else {
// 暂停:保存位置再停止
musicPosition = currentMusicClip.getMicrosecondPosition(); // 关键修复点
currentMusicClip.stop();
}
}
requestFocus();
}
// 绘制逻辑优化
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawImage(backgroundImage, 0, 0, null);
// 仅在游戏运行或暂停时绘制游戏元素
if (state == GameState.RUNNING || state == GameState.PAUSE) {
paintPlayer(g);
paintAmmo(g);
paintFlyModel(g);
paintScores(g);
}
// 绘制游戏状态界面
paintGameState(g);
// 绘制暂停界面
if (state == GameState.PAUSE) {
paintPauseScreen(g);
}
}
/**
* 绘制暂停界面
*/
private void paintPauseScreen(Graphics g) {
// 半透明遮罩
g.setColor(new Color(0, 0, 0, 150));
g.fillRect(0, 0, WIDTH, HEIGHT);
// 暂停提示文字
g.setColor(Color.YELLOW);
g.setFont(new Font("Microsoft YaHei", Font.BOLD, 36));
String text = "游戏暂停";
int textWidth = g.getFontMetrics().stringWidth(text);
g.drawString(text, (WIDTH - textWidth) / 2, HEIGHT / 2 - 20);
g.setFont(new Font("Microsoft YaHei", Font.PLAIN, 18));
g.drawString("按ESC继续游戏", (WIDTH - 150) / 2, HEIGHT / 2 + 20);
// 设置按钮
g.setColor(Color.CYAN);
g.drawRect(WIDTH - 100, 20, 80, 30);
g.setFont(new Font("Microsoft YaHei", Font.PLAIN, 16));
g.drawString("设置", WIDTH - 85, 40);
}
private void paintPlayer(Graphics g) {
// 直接绘制GIF动画
Image playerImage = playerGif.getImage();
g.drawImage(playerImage, player.getX(), player.getY(), this);
}
private void paintAmmo(Graphics g) {
for (Ammo a : ammos) {
if (a != null && ammoImage != null) {
g.drawImage(ammoImage, a.getX() - a.getWidth() / 2, a.getY(), null);
}
}
}
private void paintFlyModel(Graphics g) {
for (FlyModel f : flyModels) {
if (f != null && f.getImage() != null) {
g.drawImage(f.getImage(), f.getX(), f.getY(), null);
}
}
}
private void paintScores(Graphics g) {
g.setColor(Color.YELLOW);
g.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 14));
g.drawString("SCORE:" + scores, 10, 25);
g.drawString("LIFE:" + player.getLifeNumbers(), 10, 45);
}
private void paintGameState(Graphics g) {
if (state == GameState.START) {
g.drawImage(startImage, 0, 0, null);
// 绘制设置按钮
g.setColor(Color.CYAN);
g.drawRect(WIDTH - 100, 20, 80, 30);
g.setFont(new Font("Microsoft YaHei", Font.PLAIN, 16));
g.drawString("设置", WIDTH - 85, 40);
}
else if (state == GameState.OVER) {
g.drawImage(gameoverImage, 0, 0, null);
}
}
/**
* 显示设置菜单(音量调节)
*/
private void showSettingsMenu() {
JDialog settingsDialog = new JDialog((Frame)SwingUtilities.getWindowAncestor(this), "游戏设置", true);
settingsDialog.setLayout(new BorderLayout());
settingsDialog.setSize(300, 200);
settingsDialog.setLocationRelativeTo(this);
// 音量控制滑块
JPanel volumePanel = new JPanel();
volumePanel.add(new JLabel("音量:"));
JSlider volumeSlider = new JSlider(0, 100, (int)(volume * 100));
volumeSlider.setPreferredSize(new Dimension(200, 40));
volumeSlider.addChangeListener(e -> setVolume(volumeSlider.getValue() / 100f));
volumePanel.add(volumeSlider);
// 确认按钮
JButton confirmBtn = new JButton("确认");
confirmBtn.addActionListener(e -> settingsDialog.dispose());
settingsDialog.add(volumePanel, BorderLayout.CENTER);
settingsDialog.add(confirmBtn, BorderLayout.SOUTH);
settingsDialog.setVisible(true);
}
/** 初始化游戏 */
public void load() {
// 鼠标监听
MouseAdapter adapter = new MouseAdapter() {
@Override
public void mouseMoved(MouseEvent e) {
if (state == GameState.RUNNING) player.updateXY(e.getX(), e.getY());
}
@Override
public void mouseClicked(MouseEvent e) {
if (state == GameState.START) {
// 检测是否点击设置按钮
if (e.getX() > WIDTH - 100 && e.getX() < WIDTH - 20 &&
e.getY() > 20 && e.getY() < 50) {
showSettingsMenu();
return;
}
state = GameState.RUNNING;
}
else if (state == GameState.OVER) {
resetGame();
}
// 在暂停界面点击设置按钮
else if (state == GameState.PAUSE &&
e.getX() > WIDTH - 100 && e.getX() < WIDTH - 20 &&
e.getY() > 20 && e.getY() < 50) {
showSettingsMenu();
}
}
};
addMouseListener(adapter);
addMouseMotionListener(adapter);
// 键盘监听(添加ESC键暂停功能)
addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
togglePause();
}
}
});
// 使用Swing Timer保证线程安全
// 60 FPS
int interval = 1000 / 60;
new Timer(interval, e -> {
if (state == GameState.RUNNING) {
updateGame();
}
repaint();
}).start();
}
private void resetGame() {
flyModels.clear();
ammos.clear();
player = new Player();
scores = 0;
state = GameState.START;
paused.getAndSet(false);
}
private void updateGame() {
flyModelsEnter();
step();
fire();
hitFlyModel();
delete();
overOrNot();
}
/** 敌机/空投生成逻辑 */
private int flyModelsIndex = 0;
private void flyModelsEnter() {
if (++flyModelsIndex % 40 == 0) {
flyModels.add(nextOne());
}
}
public static FlyModel nextOne() {
return (new Random().nextInt(20) == 0) ? new Airdrop() : new Enemy();
}
/** 游戏对象移动 */
private void step() {
flyModels.forEach(FlyModel::move);
ammos.forEach(Ammo::move);
player.move();
}
/** 导弹发射 */
private int fireIndex = 0;
private void fire() {
if (++fireIndex % 30 == 0) {
ammos.addAll(Arrays.asList(player.fireAmmo()));
}
}
/** 碰撞检测 */
private void hitFlyModel() {
Iterator<Ammo> ammoIter = ammos.iterator();
while (ammoIter.hasNext()) {
Ammo ammo = ammoIter.next();
Iterator<FlyModel> flyIter = flyModels.iterator();
while (flyIter.hasNext()) {
FlyModel obj = flyIter.next();
if (obj.shootBy(ammo)) {
flyIter.remove();
ammoIter.remove();
if (obj instanceof Enemy) {
scores += ((Enemy) obj).getScores();
} else if (obj instanceof Airdrop) {
player.fireDoubleAmmos();
}
break;
}
}
}
}
/** 删除越界对象 */
private void delete() {
flyModels.removeIf(FlyModel::outOfPanel);
ammos.removeIf(Ammo::outOfPanel);
}
/** 游戏结束检测 */
private void overOrNot() {
if (isOver()) state = GameState.OVER;
}
private boolean isOver() {
Iterator<FlyModel> iter = flyModels.iterator();
while (iter.hasNext()) {
FlyModel obj = iter.next();
if (player.hit(obj)) {
player.loseLifeNumbers();
iter.remove();
}
}
return player.getLifeNumbers() <= 0;
}
/** 主入口 */
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame("飞机大战");
GamePanel panel = new GamePanel();
frame.add(panel);
frame.setSize(WIDTH, HEIGHT);
frame.setResizable(false);
frame.setLocationRelativeTo(null);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
panel.load();
});
}
}
以下是对代码的详细技术解析,涵盖线程管理、状态控制、音频同步等关键设计:
一、游戏线程与Timer机制
new Timer(interval, e -> {
if (state == GameState.RUNNING) updateGame();
repaint();
}).start();
-
Timer优势
使用javax.swing.Timer
(而非java.util.Timer
)的核心优势在于其自动运行在事件调度线程(EDT) 上。游戏循环中所有UI操作(如repaint()
)必须通过EDT执行,否则会导致渲染异常。60FPS(16ms/帧)的稳定刷新率通过1000/60
实现,比独立线程+Thread.sleep()
更精准避免帧率波动。 -
避免线程冲突
Swing组件非线程安全,Timer保证updateGame()
和repaint()
在EDT顺序执行,消除多线程同步问题。
二、音乐线程设计
1. 专用线程替代线程池
Thread musicThread = new Thread(this::playMusicLoop, "music-player");
musicThread.setDaemon(true);
-
资源隔离
音乐解码/播放涉及I/O和硬件资源(声卡),独立线程避免与游戏逻辑争抢CPU。守护线程确保JVM退出时自动终止。 -
防止递归溢出
原线程池方案中playNextMusic()
递归调用可能引发栈溢出。新方案通过wait/notify
实现循环:synchronized (musicLock) { playCurrentMusic(); musicLock.wait(); // 阻塞至播放结束 }
2. 音频进度同步
private void togglePause() {
if (nowPaused) {
currentMusicClip.setMicrosecondPosition(musicPosition);
currentMusicClip.start();
} else {
musicPosition = currentMusicClip.getMicrosecondPosition();
currentMusicClip.stop();
}
}
- 精准定位
setMicrosecondPosition()
记录暂停时的微秒级位置,恢复时精准续播。 - 格式兼容性
MP3需转换为PCM格式(AudioInputStream
转换)才能支持定位操作。
三、游戏状态管理
枚举状态机
public enum GameState { START, RUNNING, PAUSE, OVER }
private GameState state = GameState.START;
- 编译时检查
避免数字常量(如state=5
)的非法赋值,编译器直接报错。 - 可读性提升
state == GameState.PAUSE
比state == 2
更清晰表达意图。 - 扩展性
新增状态(如BOSS_FIGHT
)无需全局查找替换数字常量。
状态驱动渲染
protected void paintComponent(Graphics g) {
if (state == RUNNING || state == PAUSE) paintGameElements(g);
if (state == PAUSE) paintPauseScreen(g);
if (state == START) paintStartScreen(g);
}
- 高效分支处理
根据状态枚举值选择性渲染,避免冗余绘制操作。
四、游戏元素移动控制
private void updateGame() {
flyModelsEnter(); // 生成敌机
step(); // 移动所有对象
fire(); // 发射子弹
hitFlyModel(); // 碰撞检测
delete(); // 移除越界对象
overOrNot(); // 结束判断
}
private void step() {
flyModels.forEach(FlyModel::move);
ammos.forEach(Ammo::move);
player.move();
}
- 统一帧更新
所有对象的move()
在单次updateGame()
中调用,保证位移计算原子性,避免多线程位置不一致。 - 迭代器安全删除
delete()
使用removeIf
实现快速失败(fail-fast)的线程安全删除。
五、音乐播放与同步
1. 播放流程
private void playCurrentMusic() throws Exception {
AudioInputStream pcmStream = AudioSystem.getAudioInputStream(targetFormat, audioInput);
currentMusicClip.open(pcmStream);
currentMusicClip.addLineListener(event -> {
if (event.getType() == LineEvent.Type.STOP && !paused.get()) {
synchronized (musicLock) { musicLock.notify(); }
}
});
currentMusicClip.start();
}
- 格式转换
MP3→PCM转换解决跨平台兼容性问题。 - 事件驱动切歌
LineListener
在音乐自然结束时触发notify()
,唤醒线程播下一首。
2. 游戏与音乐暂停同步
private final AtomicBoolean paused = new AtomicBoolean(false);
private void togglePause() {
boolean nowPaused = paused.getAndSet(!paused.get());
state = nowPaused ? GameState.RUNNING : GameState.PAUSE;
// 同步操作音乐...
}
- 原子状态切换
AtomicBoolean.getAndSet()
保证状态翻转的原子性,避免多线程竞态条件。 - 单一状态源
游戏状态(state
)和暂停标志(paused
)由同一操作更新,确保逻辑一致。
六、关键技术选型解析
-
AtomicBoolean的优势
- 可见性:
volatile
语义保证多线程即时读取最新值 - 原子性:
getAndSet()
等操作避免复合操作(如先读后写)的线程中断风险 - 轻量级:比
synchronized
锁性能更高,适合高频状态检查
- 可见性:
-
对象移动的线程安全设计
- 单线程更新:所有对象位移在EDT单线程中计算,无需同步锁
- 隔离绘制:
paintComponent()
只读取对象位置,不修改状态
-
双缓冲消除闪烁
setDoubleBuffered(true)
启用离屏缓冲区,避免画面撕裂。
七、关键代码段解析
// 敌机生成逻辑(每40帧生成一架)
private void flyModelsEnter() {
if (++flyModelsIndex % 40 == 0) {
flyModels.add(nextOne());
}
}
// 碰撞检测(迭代器安全删除)
Iterator<FlyModel> flyIter = flyModels.iterator();
while (flyIter.hasNext()) {
FlyModel obj = flyIter.next();
if (obj.shootBy(ammo)) {
flyIter.remove(); // 安全删除当前元素
ammoIter.remove();
break;
}
}
// 音量控制(对数转换)
float dB = (float)(Math.log(volume)/Math.log(10.0)*20.0);
gainControl.setValue(dB); // 人耳感知的音量变化更线性
该设计通过状态驱动+EDT单线程更新实现线程安全,专用音乐线程+原子状态保障音游同步,枚举+双缓冲提升可维护性与视觉效果。完整技术方案平衡性能、安全性与扩展性。
在Java并发编程中,音乐线程采用wait/notify
机制相比线程池方案,在资源占用和响应速度上具有显著优势。以下从具体维度展开分析,结合游戏音频场景说明实际价值:
⚙️ 一、资源占用优势
-
线程数量与内存开销
wait/notify
方案:仅需1个专用线程,在等待期间(如音乐暂停或播放完成)通过wait()
释放CPU资源,线程进入WAITING
状态,不消耗CPU周期。内存占用固定(单线程栈空间约1MB)。- 线程池方案:需维护线程池实例,即使空闲时核心线程(如
Executors.newSingleThreadExecutor()
)仍占用内存和部分调度资源。若使用缓存线程池,可能因任务波动动态扩缩容,增加线程创建/销毁开销。
-
锁与同步效率
wait/notify
通过对象监视器(Monitor) 实现同步,仅依赖一个轻量级锁(musicLock
)。在等待状态时,线程主动让出CPU,资源占用趋近于零。- 线程池依赖阻塞队列(如
LinkedBlockingQueue
),队列本身需维护任务链表和锁,增加额外内存与竞争开销。
⚡ 二、响应速度优势
-
唤醒延迟
notify()
或notifyAll()
可直接唤醒等待线程,线程从WAITING
状态转为RUNNABLE
后,操作系统调度器会优先分配CPU(尤其在高优先级线程中)。实测唤醒延迟通常在微秒级。- 线程池任务需先入队再被工作线程获取,任务调度存在队列操作和锁竞争延迟(约毫秒级),在高并发场景下可能堆积。
-
精准事件触发
- 音乐播放的进度同步(如暂停后恢复)依赖精确计时。
wait/notify
结合Clip.setMicrosecondPosition()
可无缝续播,因唤醒后线程立即处理音频设备I/O。 - 线程池的通用任务模型难以满足实时性要求。例如,提交“恢复播放”任务需排队,可能被其他任务阻塞。
- 音乐播放的进度同步(如暂停后恢复)依赖精确计时。
🎮 三、游戏音频场景的适配性
-
避免线程池的“任务化”开销
音乐播放是连续性任务而非离散短任务。线程池设计针对短生命周期任务(如HTTP请求),频繁提交播放任务会导致:- 任务对象(
Runnable
)的GC压力。 - 线程切换频繁,破坏音频流的连续性。
- 任务对象(
-
资源隔离保证稳定性
- 专用音乐线程通过
setDaemon(true)
设为守护线程,游戏主线程(用户线程)退出时JVM自动终止音乐线程,避免资源泄漏。 - 线程池若未正确关闭,可能导致线程残留或任务中断异常。
- 专用音乐线程通过
🔍 四、方案对比总结
维度 | wait/notify 机制 | 线程池方案 |
---|---|---|
线程数量 | 单一线程(守护线程) | 池化管理(核心线程+队列) |
等待时资源占用 | CPU占用≈0,仅保留线程栈内存 | 核心线程持续轮询,占用CPU |
响应延迟 | 微秒级(直接唤醒) | 毫秒级(任务调度+队列竞争) |
适用场景 | 长任务、需精确事件同步(如音频/视频) | 短任务、高吞吐异步处理(如请求分发) |
资源关闭 | 自动随JVM退出(守护线程) | 需手动shutdown() ,否则资源泄漏 |
💎 结论
在音乐播放等连续性、低延迟要求的任务中,wait/notify
机制通过资源休眠式等待和精准事件唤醒,实现了更低的资源占用与更快的响应速度。其优势本质在于:
- 轻量化:仅需1个线程 + 1个锁,无任务队列开销。
- 实时性:避免线程池调度层,唤醒直达操作系统调度器。
- 可预测性:与音频设备I/O操作无缝衔接,进度控制更精确。
若需进一步优化,可结合
java.util.concurrent.locks.LockSupport
(如park()
/unpark()
),其响应速度更快且无需同步块。但对于多数音频场景,wait/notify
已在资源与效率间达到最佳平衡。
2. 创建游戏状态枚举
package org.example;
public enum GameState {
START, // 游戏开始界面
RUNNING, // 游戏运行中
PAUSE, // 游戏暂停
OVER // 游戏结束
}
3.新建flymodel类
package org.example;
import lombok.Getter;
import lombok.Setter;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
/**
* 会飞的模型类(玩家飞机、导弹、敌机、空投物资的父类)
*/
@Setter
@Getter
public abstract class FlyModel {
protected BufferedImage staticImage; // 静态图片
protected ImageIcon gifImage; // GIF动画
protected int x; // 图片左上角的x坐标
protected int y; // 图片左上角的y坐标
protected int width; // 图片的宽度
protected int height; // 图片的高度
/**
* 获取当前图像(支持静态图片和GIF)
* 优先返回GIF动画的当前帧,否则返回静态图片
*/
public Image getImage() {
if (gifImage != null) {
return gifImage.getImage(); // 返回GIF当前帧[6,8](@ref)
}
return staticImage; // 返回静态图片
}
/**
* 会飞的模型的移动方法
*/
public abstract void move();
/**
* 会飞的模型是否移动到游戏面板外
*/
public abstract boolean outOfPanel();
/**
* 检查当前会飞的模型是否被导弹击中
* @param ammo 导弹对象
*/
public boolean shootBy(Ammo ammo) {
int x = ammo.x; //导弹图片左上角的x坐标
int y = ammo.y; //导弹图片左上角的y坐标
return this.x < x && x < this.x + width && this.y < y && y < this.y + height;
}
}
4.新建player类
package org.example;
import lombok.Getter;
import javax.swing.*;
/**
* 玩家飞机类(继承会飞的模型类)
*/
public class Player extends FlyModel {
private final ImageIcon gifIcon;
private int doubleAmmos; // 玩家飞机同时发射两枚导弹
/**
* -- GETTER --
* 获得生命数
*/
@Getter
private int lifeNumbers; // 玩家飞机剩余的生命数
/**
* 在构造方法中,初始化玩家飞机类中数据
*/
public Player() {
lifeNumbers = 1; // 游戏开始时玩家飞机有1条命
doubleAmmos = 1; // 设置游戏开始时,玩家飞机只能在同一时间内发射一枚导弹
gifIcon = GamePanel.playerGif;
width = gifIcon.getIconWidth();
height = gifIcon.getIconHeight();
x = 145; // 设置游戏开始时,玩家飞机图片左上角的x坐标
y = 450; // 设置游戏开始时,玩家飞机图片左上角的y坐标
}
/**
* 玩家飞机同时发射两枚导弹
*/
public void fireDoubleAmmos() {
doubleAmmos = 2;
}
/**
* 减少生命数
*/
public void loseLifeNumbers() {
lifeNumbers--;
}
/**
* 更新玩家飞机移动后的中心点坐标
* @param mouseX 鼠标所处位置的x坐标
* @param mouseY 鼠标所处位置的y坐标
*/
public void updateXY(int mouseX, int mouseY) {
this.x = mouseX - width/2;
this.y = mouseY - height/2;
}
/**
* 玩家飞机的图片不能移动到游戏面板外
*/
public boolean outOfPanel() {
return false;
}
/**
* 玩家飞机发射导弹
* @return 发射的导弹对象
*/
public Ammo[] fireAmmo() {
int xStep = width / 4; // 把玩家飞机图片的宽度平均分为4份
int yStep = 20; // 游戏开始时,第一枚导弹与玩家飞机的距离
if (doubleAmmos == 1) { // 发射一枚导弹
Ammo[] ammos = new Ammo[1]; // 一枚导弹
// x + 2 * xStep(导弹相对玩家飞机的x坐标),y-yStep(导弹相对玩家飞机的y坐标)
ammos[0] = new Ammo(x + 2 * xStep, y - yStep);
return ammos;
} else { // 发射两枚导弹
Ammo[] ammos = new Ammo[2]; // 两枚导弹
ammos[0] = new Ammo(x + xStep, y - yStep);
ammos[1] = new Ammo(x + 3 * xStep, y - yStep);
return ammos;
}
}
/**
* 玩家飞机图片的移动方法
*/
public void move() {
}
/**
* 判断玩家飞机是否发生碰撞
*/
public boolean hit(FlyModel model) {
int x1 = model.x - this.width / 2; // 距离玩家飞机最小的x坐标
int x2 = model.x + this.width / 2 + model.width; // 距离玩家飞机最大的x坐标
int y1 = model.y - this.height / 2; // 距离玩家飞机最小的y坐标
int y2 = model.y + this.height / 2 + model.height; // 距离玩家飞机最大的y坐标
int playerx = this.x + this.width / 2; // 表示玩家飞机中心点的x坐标
int playery = this.y + this.height / 2; // 表示玩家飞机中心点的y坐标
return playerx > x1 && playerx < x2 && playery > y1 && playery < y2; // 区间范围内发生碰撞
}
}
5.新建hit接口
package org.example;
/**
* 敌机被击中,玩家飞机获得分数
*/
public interface Hit {
int getScores(); // 获得分数
}
6.新建各种类
package org.example;
import java.util.Random;
/**
* 敌机,继承会飞的模型类
*/
public class Enemy extends FlyModel implements Hit {
/**
* 初始化数据
*/
public Enemy(){
this.staticImage = GamePanel.enemyImage; // 敌机图片
width = staticImage.getWidth(); // 敌机图片的宽度
height = staticImage.getHeight(); // 敌机图片的高度
y = -height; // 游戏开始时,敌机图片左上角的y坐标
Random rand = new Random(); // 创建随机数对象
x = rand.nextInt(GamePanel.WIDTH - width); // 游戏开始时,敌机图片左上角的x坐标(随机)
}
/**
* 获得分数
*/
public int getScores() {
return 5; // 击落一架敌机得5分
}
/**
* 敌机图片移动
*/
public void move() {
// 敌机图片的移动速度
int speed = 3;
y += speed;
}
/**
* 敌机图片是否移动到游戏面板外
*/
public boolean outOfPanel() {
return y > GamePanel.HEIGHT;
}
}
package org.example;
/**
* 导弹类(继承会飞的模型类)
*/
public class Ammo extends FlyModel {
private int speed = 3; // 导弹的移动速度
/** 初始化数据 */
public Ammo(int x,int y){
this.x = x;
this.y = y;
this.staticImage = GamePanel.ammoImage;
}
/**
* 导弹图片的移动方法
*/
public void move(){
y -= speed;
}
/**
* 导弹图片是否移动到游戏面板外
*/
public boolean outOfPanel() {
return y <- height;
}
}
package org.example;
import java.util.Random;
/**
* 空投物资(继承会飞的模型类)
*/
public class Airdrop extends FlyModel {
private int xSpeed = 1; // 空投物资图片x坐标的移动速度
private int ySpeed = 2; // 空投物资图片y坐标的移动速度
/**
* 初始化数据
*/
public Airdrop(){
this.staticImage = GamePanel.airdropImage; // 空投物资的图片
width = staticImage .getWidth(); // 空投物资图片的宽度
height = staticImage .getHeight(); // 空投物资图片的高度
y = -height; // 游戏开始时,空投物资图片左上角的y坐标
Random rand = new Random(); // 创建随机数对象
x = rand.nextInt(GamePanel.WIDTH - width); // 初始时,空投物资图片左上角的x坐标(随机)
}
/**
* 空投物资的图片是否移动到游戏面板外
*/
public boolean outOfPanel() {
return y > GamePanel.HEIGHT;
}
/**
* 空投物资图片的移动方法
*/
public void move() {
x += xSpeed;
y += ySpeed;
if(x > GamePanel.WIDTH-width){
xSpeed = -1;
}
if(x < 0){
xSpeed = 1;
}
}
}