Java项目(2)——飞机大战游戏

这是一个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.PAUSEstate == 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)由同一操作更新,确保逻辑一致。

六、关键技术选型解析

  1. AtomicBoolean的优势

    • 可见性volatile语义保证多线程即时读取最新值
    • 原子性getAndSet()等操作避免复合操作(如先读后写)的线程中断风险
    • 轻量级:比synchronized锁性能更高,适合高频状态检查
  2. 对象移动的线程安全设计

    • 单线程更新:所有对象位移在EDT单线程中计算,无需同步锁
    • 隔离绘制paintComponent()只读取对象位置,不修改状态
  3. 双缓冲消除闪烁
    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机制相比线程池方案,在资源占用响应速度上具有显著优势。以下从具体维度展开分析,结合游戏音频场景说明实际价值:


⚙️ 一、资源占用优势

  1. 线程数量与内存开销

    • wait/notify方案:仅需1个专用线程,在等待期间(如音乐暂停或播放完成)通过wait()释放CPU资源,线程进入WAITING状态,不消耗CPU周期。内存占用固定(单线程栈空间约1MB)。
    • 线程池方案:需维护线程池实例,即使空闲时核心线程(如Executors.newSingleThreadExecutor())仍占用内存和部分调度资源。若使用缓存线程池,可能因任务波动动态扩缩容,增加线程创建/销毁开销。
  2. 锁与同步效率

    • wait/notify通过对象监视器(Monitor) 实现同步,仅依赖一个轻量级锁(musicLock)。在等待状态时,线程主动让出CPU,资源占用趋近于零。
    • 线程池依赖阻塞队列(如LinkedBlockingQueue),队列本身需维护任务链表和锁,增加额外内存与竞争开销。

二、响应速度优势

  1. 唤醒延迟

    • notify()notifyAll()直接唤醒等待线程,线程从WAITING状态转为RUNNABLE后,操作系统调度器会优先分配CPU(尤其在高优先级线程中)。实测唤醒延迟通常在微秒级
    • 线程池任务需先入队再被工作线程获取,任务调度存在队列操作和锁竞争延迟(约毫秒级),在高并发场景下可能堆积。
  2. 精准事件触发

    • 音乐播放的进度同步(如暂停后恢复)依赖精确计时。wait/notify结合Clip.setMicrosecondPosition()可无缝续播,因唤醒后线程立即处理音频设备I/O。
    • 线程池的通用任务模型难以满足实时性要求。例如,提交“恢复播放”任务需排队,可能被其他任务阻塞。

🎮 三、游戏音频场景的适配性

  1. 避免线程池的“任务化”开销
    音乐播放是连续性任务而非离散短任务。线程池设计针对短生命周期任务(如HTTP请求),频繁提交播放任务会导致:

    • 任务对象(Runnable)的GC压力。
    • 线程切换频繁,破坏音频流的连续性。
  2. 资源隔离保证稳定性

    • 专用音乐线程通过setDaemon(true)设为守护线程,游戏主线程(用户线程)退出时JVM自动终止音乐线程,避免资源泄漏。
    • 线程池若未正确关闭,可能导致线程残留或任务中断异常。

🔍 四、方案对比总结

维度wait/notify 机制线程池方案
线程数量单一线程(守护线程)池化管理(核心线程+队列)
等待时资源占用CPU占用≈0,仅保留线程栈内存核心线程持续轮询,占用CPU
响应延迟微秒级(直接唤醒)毫秒级(任务调度+队列竞争)
适用场景长任务、需精确事件同步(如音频/视频)短任务、高吞吐异步处理(如请求分发)
资源关闭自动随JVM退出(守护线程)需手动shutdown(),否则资源泄漏

💎 结论

在音乐播放等连续性、低延迟要求的任务中,wait/notify机制通过资源休眠式等待精准事件唤醒,实现了更低的资源占用与更快的响应速度。其优势本质在于:

  1. 轻量化:仅需1个线程 + 1个锁,无任务队列开销。
  2. 实时性:避免线程池调度层,唤醒直达操作系统调度器。
  3. 可预测性:与音频设备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;
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lemon_sjdk

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值