Java实现项目1——弹射球游戏

项目:弹射球游戏

项目描述:

类似于乒乓球的游戏,游戏可以播放背景音乐,可以更换背景图,当小球碰到下面的挡板后会反弹,当小球碰到方块后会增加分数,当小球掉落会导致游戏失败,按下esc键游戏会暂停,音乐会停止播放,运行时会新建一个music文件夹,文件夹内放入任何音频文件都将作为背景音乐播放

项目代码

package org.example;import com.google.common.base.Throwables;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.common.io.Files;
import com.google.common.io.Resources;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import javazoom.spi.mpeg.sampled.file.MpegAudioFileReader;
import lombok.Getter;
import lombok.Setter;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.URL;
import java.util.*;
import java.util.List;
import java.util.concurrent.*;
import javax.imageio.ImageIO;
import javax.sound.sampled.*;public class BreakoutGame extends JPanel implements KeyListener {// 获取显示器尺寸作为游戏窗口大小private static final Dimension SCREEN_SIZE = Toolkit.getDefaultToolkit().getScreenSize();public static final int WIDTH = SCREEN_SIZE.width;public static final int HEIGHT = SCREEN_SIZE.height;// 游戏常量 - 根据屏幕尺寸动态计算private Clip currentMusicClip;private float volume = 0.7f;private boolean musicEnabled = true;private List<File> musicFiles = new ArrayList<>();private int currentMusicIndex = 0;private ExecutorService musicExecutor;private long pausePosition = 0; // 记录音乐暂停位置private boolean musicPaused = false; // 音乐暂停状态标志static final int PADDLE_WIDTH = WIDTH / 15; // 动态计算挡板宽度static final int PADDLE_HEIGHT = HEIGHT / 60; // 动态计算挡板高度public static final int BALL_SIZE = WIDTH / 80; // 动态计算球的大小private static final int BRICK_COLS = 20; // 增加砖块列数以适应更大屏幕private static final int BRICK_FALL_SPEED = HEIGHT / 200; // 动态计算下落速度// 使用Guava的ImmutableList定义背景列表(不可变集合)private static final ImmutableList<String> BACKGROUND_NAMES = ImmutableList.of("default", "stars", "ocean", "beach", "forest");private int backgroundIndex = 0;// 使用Guava缓存加载背景图片(核心优化)private final LoadingCache<String, BufferedImage> backgroundCache = CacheBuilder.newBuilder().maximumSize(10) // 最多缓存10张背景图.expireAfterAccess(10, TimeUnit.MINUTES) // 10分钟未访问则过期.build(new CacheLoader<>() {@Overridepublic BufferedImage load(String bgName) throws Exception {return loadBackgroundImage(bgName);}});// 游戏状态private int score = 0;private volatile boolean gameRunning = true;private volatile boolean gamePaused = false;// 使用同步集合防止并发修改异常private final List<Brick> bricks = Collections.synchronizedList(new ArrayList<>());private final List<Brick> fallingBricks = Collections.synchronizedList(new ArrayList<>());// 游戏对象private Ball ball;private Paddle paddle;// 纹理资源private BufferedImage backgroundImage;private BufferedImage ballTexture;private BufferedImage paddleTexture;private BufferedImage brickTexture;// 菜单系统private JDialog menuDialog;private JSlider volumeSlider;private JComboBox<String> backgroundSelector;// 线程池private final ExecutorService executor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("game-loop-%d").build());private int brickSpawnCounter = 0;private final Random random = new Random();public BreakoutGame() {setPreferredSize(new Dimension(WIDTH, HEIGHT));loadTextures();initAudio();  // 修复音频加载逻辑initMenu();initGame();startGameLoop();addKeyListener(this);setFocusable(true);requestFocus();addMouseMotionListener(new MouseAdapter() {public void mouseMoved(MouseEvent e) {if (!gamePaused) {paddle.setX(e.getX() - PADDLE_WIDTH / 2);repaint();}}});}// 纹理加载private void loadTextures() {try {// 使用Guava缓存获取背景图backgroundImage = backgroundCache.get(BACKGROUND_NAMES.get(backgroundIndex));ballTexture = loadImageResource("ball_texture.png");paddleTexture = loadImageResource("paddle_texture.png");brickTexture = loadImageResource("brick_texture.png");} catch (Exception e) {System.err.println("纹理加载失败: " + Throwables.getStackTraceAsString(e));createFallbackTextures();}}// 加载背景图片(使用Guava的Files类)private BufferedImage loadBackgroundImage(String bgName) throws IOException {// 优先从resources加载URL resourceUrl = Resources.getResource(bgName + ".jpg");if (resourceUrl != null) {return ImageIO.read(resourceUrl);}// 尝试从文件系统加载File bgFile = new File("backgrounds/" + bgName + ".jpg");if (bgFile.exists()) {return ImageIO.read(bgFile);}// 使用Guava的Files类创建目录Files.createParentDirs(bgFile);throw new IOException("Background image not found: " + bgName);}private BufferedImage loadImageResource(String path) throws IOException {URL url = Resources.getResource(path);return url != null ? ImageIO.read(url) : null;}private void createFallbackTextures() {ballTexture = new BufferedImage(BALL_SIZE, BALL_SIZE, BufferedImage.TYPE_INT_RGB);paddleTexture = new BufferedImage(PADDLE_WIDTH, PADDLE_HEIGHT, BufferedImage.TYPE_INT_RGB);brickTexture = new BufferedImage(WIDTH / 20, HEIGHT / 40, BufferedImage.TYPE_INT_RGB); // 动态计算砖块大小}// 修复音频加载(支持文件夹自动创建和MP3格式)private void initAudio() {// 创建音乐线程池musicExecutor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("music-player-%d").build());File musicDir = new File("music");if (!musicDir.exists()) {boolean created = musicDir.mkdir();System.out.println(created ? "创建music文件夹成功" : "创建music文件夹失败");}// 扫描music文件夹中的音频文件(支持多种格式)File[] files = musicDir.listFiles((dir, name) -> {String lower = name.toLowerCase();return lower.endsWith(".wav") || lower.endsWith(".mp3") ||lower.endsWith(".aiff") || lower.endsWith(".au") ||lower.endsWith(".ogg") || lower.endsWith(".flac");});if (files == null || files.length == 0) {System.err.println("music文件夹为空,请添加音频文件");return;}// 添加到播放列表musicFiles = Arrays.asList(files);System.out.println("找到音频文件: " + musicFiles.size() + " 个");// 开始播放音乐playNextMusic();}// 播放下一首音乐(支持MP3)private void playNextMusic() {if (musicFiles.isEmpty() || !musicEnabled) return;musicExecutor.execute(() -> {try {File musicFile = musicFiles.get(currentMusicIndex);System.out.println("播放音频: " + musicFile.getName());// 1. 加载音频流AudioInputStream audioIn;if (musicFile.getName().toLowerCase().endsWith(".mp3")) {audioIn = new MpegAudioFileReader().getAudioInputStream(musicFile);} else {audioIn = AudioSystem.getAudioInputStream(musicFile);}// 2. 转换为PCM格式(修复兼容性问题)AudioFormat baseFormat = audioIn.getFormat();AudioFormat targetFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,44100,       // 固定采样率16,          // 16位深度baseFormat.getChannels(),baseFormat.getChannels() * 2,44100,       // 帧率false);AudioInputStream pcmStream = AudioSystem.getAudioInputStream(targetFormat, audioIn);// 3. 释放旧资源(关键!)if (currentMusicClip != null) {currentMusicClip.removeLineListener(null); // 移除旧监听器currentMusicClip.close();}// 4. 创建新ClipcurrentMusicClip = AudioSystem.getClip();currentMusicClip.open(pcmStream);setVolume(volume);// 5. 修复监听器逻辑(核心修复点)currentMusicClip.addLineListener(event -> {if (event.getType() == LineEvent.Type.STOP) {// 仅当播放自然结束时切换歌曲(非暂停且播放位置已达末尾)if (!musicPaused && currentMusicClip.getFramePosition() >= currentMusicClip.getFrameLength()) {currentMusicIndex = (currentMusicIndex + 1) % musicFiles.size();playNextMusic();}}});// 6. 开始播放currentMusicClip.start();musicPaused = false; // 重置暂停状态} catch (Exception e) {System.err.println("播放失败: " + Throwables.getStackTraceAsString(e));// 失败时延迟重试try {Thread.sleep(1000);} catch (InterruptedException ex) {throw new RuntimeException(ex);}currentMusicIndex = (currentMusicIndex + 1) % musicFiles.size();playNextMusic();}});}// 暂停音乐private void pauseMusic() {if (currentMusicClip != null && currentMusicClip.isRunning()) {pausePosition = currentMusicClip.getMicrosecondPosition();currentMusicClip.stop();musicPaused = true;System.out.println("音乐已暂停");}}// 恢复音乐private void resumeMusic() {if (currentMusicClip != null && musicPaused && musicEnabled) {currentMusicClip.setMicrosecondPosition(pausePosition);currentMusicClip.start();musicPaused = false;System.out.println("音乐已恢复");} else if (currentMusicClip == null && musicEnabled) {// 如果没有音乐在播放但音乐启用,开始播放playNextMusic();}}// 设置音量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);gainControl.setValue(dB);}}private void initMenu() {menuDialog = new JDialog((Frame)null, "游戏设置", true);menuDialog.setSize(300, 250);menuDialog.setLayout(new GridLayout(5, 1, 10, 10));menuDialog.setLocationRelativeTo(null);// 音量控制JPanel volumePanel = new JPanel();volumePanel.add(new JLabel("音量控制:"));volumeSlider = new JSlider(0, 100, (int)(volume * 100));volumeSlider.addChangeListener(e -> {volume = volumeSlider.getValue() / 100f;setVolume(volume);});volumePanel.add(volumeSlider);menuDialog.add(volumePanel);// 背景选择 - 使用Guava的ImmutableList填充下拉框JPanel bgPanel = new JPanel();bgPanel.add(new JLabel("背景选择:"));backgroundSelector = new JComboBox<>(BACKGROUND_NAMES.toArray(new String[0]));backgroundSelector.setSelectedIndex(backgroundIndex);backgroundSelector.addActionListener(e -> {String selected = (String) backgroundSelector.getSelectedItem();changeBackground(selected);});bgPanel.add(backgroundSelector);menuDialog.add(bgPanel);// 音乐开关JCheckBox musicToggle = new JCheckBox("启用背景音乐", musicEnabled);musicToggle.addActionListener(e -> {musicEnabled = musicToggle.isSelected();if (musicEnabled) {resumeMusic(); // 恢复音乐} else {pauseMusic(); // 暂停音乐}});menuDialog.add(musicToggle);// 返回游戏按钮JButton backButton = new JButton("返回游戏");backButton.addActionListener(e -> {menuDialog.setVisible(false);gamePaused = false;resumeMusic(); // 返回游戏时恢复音乐requestFocus();});menuDialog.add(backButton);// 关闭处理menuDialog.addWindowListener(new WindowAdapter() {public void windowClosing(WindowEvent e) {gamePaused = false;resumeMusic(); // 关闭菜单时恢复音乐}});}private void changeBackground(String bgName) {try {// 使用Guava缓存获取背景图backgroundImage = backgroundCache.get(bgName);backgroundIndex = BACKGROUND_NAMES.indexOf(bgName);repaint(); // 重绘游戏界面System.out.println("更换背景: " + bgName);} catch (ExecutionException e) {System.err.println("背景加载失败: " + Throwables.getStackTraceAsString(e));}}// 游戏初始化private void initGame() {resetGame();}private void resetGame() {score = 0;gameRunning = true;gamePaused = false;bricks.clear();fallingBricks.clear();brickSpawnCounter = 0;resumeMusic(); // 游戏重置时恢复音乐paddle = new Paddle(WIDTH / 2 - PADDLE_WIDTH / 2,HEIGHT - 50,PADDLE_WIDTH,PADDLE_HEIGHT,paddleTexture);ball = new Ball(WIDTH / 2,HEIGHT / 2,WIDTH / 200, // 动态计算球速-HEIGHT / 200,BALL_SIZE,ballTexture);}// 游戏循环private void startGameLoop() {executor.execute(() -> {while (gameRunning && !Thread.currentThread().isInterrupted()) {if (!gamePaused) {try {updateGame();SwingUtilities.invokeLater(this::repaint);} catch (Exception e) {System.err.println("游戏循环错误: " + e.getMessage());}}try {Thread.sleep(16);} catch (InterruptedException e) {Thread.currentThread().interrupt();}}});}// 生成新方块行private void spawnBrickRow() {if (random.nextFloat() > 0.3f) return;int brickWidth = WIDTH / BRICK_COLS;int brickHeight = HEIGHT / 40;for (int col = 0; col < BRICK_COLS; col++) {if (random.nextFloat() > 0.7f) continue;fallingBricks.add(new Brick(col * brickWidth,-30,brickWidth,brickHeight,brickTexture));}}// 修复并发修改问题private void updateGame() {// 生成新方块if (brickSpawnCounter++ >= 60) {spawnBrickRow();brickSpawnCounter = 0;}// 使用同步块保护集合操作synchronized (fallingBricks) {Iterator<Brick> iterator = fallingBricks.iterator();while (iterator.hasNext()) {Brick brick = iterator.next();brick.setY(brick.getY() + BRICK_FALL_SPEED);if (brick.getY() > HEIGHT) {iterator.remove();} else if (brick.getY() > 50) {bricks.add(brick);iterator.remove();}}}// 球移动ball.move();// 边界碰撞检测if (ball.getX() <= 0 || ball.getX() >= WIDTH - ball.getSize())ball.reverseX();if (ball.getY() <= 0)ball.reverseY();// 挡板碰撞检测if (ball.getBounds().intersects(paddle.getBounds()))ball.reverseY();// 游戏结束检测if (ball.getY() > HEIGHT) {gameRunning = false;showGameOverDialog("游戏结束! 得分: " + score);return;}// 砖块碰撞检测synchronized (bricks) {Iterator<Brick> brickIterator = bricks.iterator();while (brickIterator.hasNext()) {Brick brick = brickIterator.next();if (brick.isVisible() && ball.getBounds().intersects(brick.getBounds())) {brick.setVisible(false);ball.reverseY();score += 10;brickIterator.remove();}}}// 检查砖块堆积synchronized (bricks) {for (Brick brick : bricks) {if (brick.getY() + brick.getHeight() >= HEIGHT - 50) {gameRunning = false;showGameOverDialog("砖块堆积到屏幕底部! 得分: " + score);return;}}}}// 游戏结束对话框private void showGameOverDialog(String message) {SwingUtilities.invokeLater(() -> {int option = JOptionPane.showOptionDialog(this, message, "游戏结束",JOptionPane.YES_NO_OPTION, JOptionPane.INFORMATION_MESSAGE,null, new Object[]{"重新开始", "退出"}, "重新开始");if (option == JOptionPane.YES_OPTION) {resetGame();startGameLoop();} else {System.exit(0);}});}@Overrideprotected void paintComponent(Graphics g) {super.paintComponent(g);// 绘制背景if (backgroundImage != null) {// 使用高质量图像缩放g.drawImage(backgroundImage, 0, 0, WIDTH, HEIGHT, this);} else {g.setColor(Color.BLACK);g.fillRect(0, 0, WIDTH, HEIGHT);}// 同步绘制防止并发修改异常List<Brick> fallingCopy;List<Brick> bricksCopy;synchronized (fallingBricks) {fallingCopy = new ArrayList<>(fallingBricks);}synchronized (bricks) {bricksCopy = new ArrayList<>(bricks);}// 绘制下落中的方块for (Brick brick : fallingCopy) {brick.draw(g);}// 绘制砖块for (Brick brick : bricksCopy) {if (brick.isVisible()) brick.draw(g);}// 绘制挡板和小球paddle.draw(g);ball.draw(g);// 绘制分数和状态 - 增大字体以适应高分辨率g.setColor(Color.WHITE);g.setFont(new Font("Msyh", Font.BOLD, WIDTH / 40)); // 动态计算字体大小g.drawString("得分: " + score, WIDTH / 40, HEIGHT / 20); // 动态调整位置if (gamePaused) {g.setColor(new Color(255, 255, 255, 180));int pauseWidth = WIDTH / 4;int pauseHeight = HEIGHT / 10;g.fillRect(WIDTH/2 - pauseWidth/2, HEIGHT/2 - pauseHeight/2, pauseWidth, pauseHeight);g.setColor(Color.BLUE);g.drawString("游戏暂停 - 按ESC返回", WIDTH/2 - pauseWidth/3, HEIGHT/2 + pauseHeight/4);}}@Overridepublic void keyPressed(KeyEvent e) {if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {gamePaused = !gamePaused;if (gamePaused) {pauseMusic(); // 暂停音乐menuDialog.setVisible(true);} else {menuDialog.setVisible(false);resumeMusic(); // 恢复音乐}requestFocus();}}@Override public void keyTyped(KeyEvent e) {}@Override public void keyReleased(KeyEvent e) {}public static void main(String[] args) {SwingUtilities.invokeLater(() -> {JFrame frame = new JFrame("高级打砖块游戏");frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);// 设置全屏模式frame.setUndecorated(true); // 移除窗口边框frame.setExtendedState(JFrame.MAXIMIZED_BOTH); // 最大化窗口frame.setResizable(false); // 禁止调整大小frame.getContentPane().add(new BreakoutGame());frame.pack();frame.setLocationRelativeTo(null);frame.setVisible(true);});}
}// ===== 游戏实体类 =====
@Getter
class Ball {@Setterprivate int x;@Setterprivate int y;private int dx, dy;private final int size;private final BufferedImage texture;public Ball(int x, int y, int dx, int dy, int size, BufferedImage texture) {this.x = x;this.y = y;this.dx = dx;this.dy = dy;this.size = size;this.texture = texture;}public void move() {x += dx;y += dy;}public void reverseX() { dx = -dx; }public void reverseY() { dy = -dy; }public Rectangle getBounds() {return new Rectangle(x, y, size, size);}public void draw(Graphics g) {if (texture != null) {// 使用高质量图像缩放g.drawImage(texture, x, y, size, size, null);} else {g.setColor(Color.RED);g.fillOval(x, y, size, size);}}
}@Getter
class Paddle {private int x;private final int y;private final int width, height;private final BufferedImage texture;public Paddle(int x, int y, int width, int height, BufferedImage texture) {this.x = x;this.y = y;this.width = width;this.height = height;this.texture = texture;}public Rectangle getBounds() {return new Rectangle(x, y, width, height);}public void setX(int x) {this.x = Math.max(0, Math.min(x, BreakoutGame.WIDTH - width));}public void draw(Graphics g) {if (texture != null) {// 使用高质量图像缩放g.drawImage(texture, x, y, width, height, null);} else {g.setColor(Color.GREEN);g.fillRect(x, y, width, height);}}
}@Getter
class Brick {private final int x;@Setterprivate int y;private final int width, height;@Setterprivate boolean visible = true;private final BufferedImage texture;public Brick(int x, int y, int width, int height, BufferedImage texture) {this.x = x;this.y = y;this.width = width;this.height = height;this.texture = texture;}public Rectangle getBounds() {return new Rectangle(x, y, width, height);}public void draw(Graphics g) {if (!visible) return;if (texture != null) {// 使用高质量图像缩放g.drawImage(texture, x, y, width, height, null);} else {g.setColor(new Color((x * 23) % 256,(y * 37) % 256,(x * y) % 256));g.fillRect(x, y, width, height);g.setColor(Color.WHITE);g.drawRect(x, y, width, height);}}
}

代码架构与原理分析

1. 游戏初始化与资源加载
  • 屏幕自适应

    private static final Dimension SCREEN_SIZE = Toolkit.getDefaultToolkit().getScreenSize();  
    public static final int WIDTH = SCREEN_SIZE.width;  
    public static final int HEIGHT = SCREEN_SIZE.height;  
    

    动态获取屏幕尺寸,确保游戏在不同分辨率下自适应。

  • Guava缓存优化

    private final LoadingCache<String, BufferedImage> backgroundCache = CacheBuilder.newBuilder()  .maximumSize(10)  .expireAfterAccess(10, TimeUnit.MINUTES)  .build(new CacheLoader<>() {  public BufferedImage load(String bgName) throws Exception {  return loadBackgroundImage(bgName);  }  });  
    

    使用Guava缓存背景图,减少重复IO操作,提升性能(maximumSize限制缓存数量,expireAfterAccess自动清理闲置资源)。

  • 纹理加载机制

    • 优先从resources加载图片,失败则尝试文件系统(如backgrounds/目录),最后创建纯色后备纹理。
    • 使用Guava的Resources.getResource()简化资源路径处理。

2. 音频系统设计
  • 多格式支持

    File[] files = musicDir.listFiles((dir, name) -> {  String lower = name.toLowerCase();  return lower.endsWith(".wav") || lower.endsWith(".mp3") || ...;  
    });  
    

    支持MP3/WAV等格式,通过MpegAudioFileReader解析MP3(需javazoom库)。

  • 线程安全播放逻辑

    musicExecutor = Executors.newSingleThreadExecutor(...);  
    musicExecutor.execute(() -> {  // 解码、转换PCM格式、播放  
    });  
    

    单线程执行音频任务,避免阻塞主线程。LineListener监听播放结束事件,自动切歌。

  • 音量控制

    FloatControl gainControl = (FloatControl) clip.getControl(FloatControl.Type.MASTER_GAIN);  
    float dB = (float) (Math.log(volume) / Math.log(10.0) * 20.0);  
    gainControl.setValue(dB);  
    

    通过对数转换实现自然音量调节。


3. 游戏逻辑与并发控制
  • 动态对象生成

    private void spawnBrickRow() {  if (random.nextFloat() > 0.3f) return;  // 按概率生成新砖块行  
    }  
    

    砖块生成频率和位置随机化,增加游戏难度。

  • 并发集合操作

    private final List<Brick> bricks = Collections.synchronizedList(new ArrayList<>());  
    synchronized (bricks) {  Iterator<Brick> brickIterator = bricks.iterator();  // 安全删除砖块  
    }  
    

    使用同步集合和显式锁,避免多线程修改冲突。

  • 碰撞检测优化

    if (ball.getBounds().intersects(paddle.getBounds()))  ball.reverseY();  
    

    Rectangle.intersects()快速检测矩形碰撞,效率高于像素级检测。


4. 菜单与用户交互
  • 模态对话框

    private void initMenu() {  menuDialog = new JDialog((Frame)null, "游戏设置", true);  // 添加音量滑块、背景选择器等组件  
    }  
    

    模态对话框确保焦点锁定,防止游戏后台运行。

  • 背景热切换

    backgroundSelector.addActionListener(e -> {  String selected = (String) backgroundSelector.getSelectedItem();  changeBackground(selected);  
    });  
    

    通过Guava缓存即时切换背景,无需重启游戏。


5. 渲染与性能优化
  • 双缓冲技术

    protected void paintComponent(Graphics g) {  super.paintComponent(g);  // 先绘制到内存缓冲,再一次性渲染到屏幕  
    }  
    

    避免画面闪烁(Swing默认双缓冲,此处显式优化)。

  • 动态字体与布局

    g.setFont(new Font("Msyh", Font.BOLD, WIDTH / 40));  
    g.drawString("得分: " + score, WIDTH / 40, HEIGHT / 20);  
    

    基于屏幕尺寸计算字体大小和位置,确保高分辨率下可读性。


6. 实体类设计
  • 统一绘制接口

    public void draw(Graphics g) {  if (texture != null) {  g.drawImage(texture, x, y, width, height, null);  } else {  // 绘制纯色图形  }  
    }  
    

    Ball/Paddle/Brick均实现draw()方法,封装渲染细节。

  • 运动逻辑解耦

    public void move() {  x += dx;  y += dy;  
    }  
    

    实体类独立处理移动,游戏循环仅调用接口。


关键设计思想总结

  1. 性能优先

    • Guava缓存减少IO开销
    • 并发集合保证线程安全
    • 矩形碰撞检测替代复杂计算
  2. 扩展性设计

    • 背景/音频资源动态加载
    • 实体类与游戏逻辑分离
  3. 用户体验优化

    • 全屏自适应布局
    • 音量/背景实时切换
    • 暂停菜单防止误操作
  4. 健壮性保障

    • 资源加载失败时生成后备纹理
    • 音频格式转换兼容不同系统
    • 显式同步避免多线程冲突

此架构通过分层设计(资源层、逻辑层、渲染层)实现高内聚低耦合,适合扩展为更复杂游戏。完整代码可参考https://www.528045.com/article/b05051cd36.html和http://mp.weixin.qq.com/s?__biz=MzAxNjE2MTcyNQ==&mid=2450448537&idx=2&sn=8ab7f51ee62e382e0eaa9bb175c024cf。

1. dx 和 dy 的含义与小球的移动控制

dxdy 是球体运动的核心控制变量:

private int dx, dy; // Ball类中的运动速度分量public void move() {x += dx; // 每帧水平方向移动dx个像素y += dy; // 每帧垂直方向移动dy个像素
}
工作原理:
  1. dx 控制水平运动方向

    • dx > 0:球向右移动
    • dx < 0:球向左移动
    • dx = 0:球水平静止
  2. dy 控制垂直运动方向

    • dy > 0:球向下移动
    • dy < 0:球向上移动
    • dy = 0:球垂直静止
方向控制逻辑:
// 碰到左右边界时反转水平方向
public void reverseX() { dx = -dx; 
}// 碰到上下边界或挡板时反转垂直方向
public void reverseY() { dy = -dy; 
}
动态速度计算:
// 根据屏幕尺寸动态计算初始速度
ball = new Ball(WIDTH / 2,HEIGHT / 2,WIDTH / 200, // 水平速度 (dx)-HEIGHT / 200, // 垂直速度 (dy) 负值表示向上BALL_SIZE,ballTexture
);

2. 音乐播放控制机制

核心实现:
private Clip currentMusicClip; // 当前播放的音乐剪辑
private float volume = 0.7f; // 音量值 (0.0~1.0)
private boolean musicEnabled = true; // 音乐开关
private List<File> musicFiles; // 音乐文件列表
private long pausePosition; // 暂停位置记录
控制流程:
  1. 播放控制

    private void playNextMusic() {musicExecutor.execute(() -> {// 加载音频文件currentMusicClip.open(pcmStream);currentMusicClip.start(); // 开始播放});
    }
    
  2. 暂停与恢复

    private void pauseMusic() {pausePosition = currentMusicClip.getMicrosecondPosition();currentMusicClip.stop();
    }private void resumeMusic() {currentMusicClip.setMicrosecondPosition(pausePosition);currentMusicClip.start();
    }
    
  3. 音量控制

    private void setVolume(float volume) {FloatControl gainControl = (FloatControl) currentMusicClip.getControl(FloatControl.Type.MASTER_GAIN);float dB = (float) (Math.log(volume) / Math.log(10.0) * 20.0);gainControl.setValue(dB);
    }
    
  4. 自动切歌

    currentMusicClip.addLineListener(event -> {if (event.getType() == LineEvent.Type.STOP) {// 自然播放结束后切换到下一首currentMusicIndex = (currentMusicIndex + 1) % musicFiles.size();playNextMusic();}
    });
    

3. 通过窗口更换背景图

实现流程:
用户 背景选择器 游戏主类 Guava缓存 资源加载器 界面渲染 paintComponent 选择背景名称 触发changeBackground事件 获取背景图 检查背景是否存在 返回BufferedImage 返回缓存图片 调用repaint() 重绘背景 用户 背景选择器 游戏主类 Guava缓存 资源加载器 界面渲染 paintComponent
关键代码:
  1. 背景选择器设置

    // 使用Guava的ImmutableList填充下拉框
    backgroundSelector = new JComboBox<>(BACKGROUND_NAMES.toArray(new String[0]));// 添加选择事件监听器
    backgroundSelector.addActionListener(e -> {String selected = (String) backgroundSelector.getSelectedItem();changeBackground(selected); // 更换背景
    });
    
  2. 背景更换实现

    private void changeBackground(String bgName) {try {// 从Guava缓存获取背景图backgroundImage = backgroundCache.get(bgName);backgroundIndex = BACKGROUND_NAMES.indexOf(bgName);repaint(); // 触发界面重绘} catch (ExecutionException e) {// 异常处理...}
    }
    
  3. 背景缓存加载

    private final LoadingCache<String, BufferedImage> backgroundCache = CacheBuilder.newBuilder().maximumSize(10).build(new CacheLoader<>() {public BufferedImage load(String bgName) throws Exception {// 尝试从多种来源加载背景URL resourceUrl = Resources.getResource(bgName + ".jpg");if (resourceUrl != null) return ImageIO.read(resourceUrl);File bgFile = new File("backgrounds/" + bgName + ".jpg");if (bgFile.exists()) return ImageIO.read(bgFile);throw new IOException("背景图不存在");}});
    
  4. 最终渲染

    protected void paintComponent(Graphics g) {// 绘制缓存中的背景图g.drawImage(backgroundImage, 0, 0, WIDTH, HEIGHT, this);// ...其他渲染逻辑
    }
    

总结特点

  1. 小球移动控制

    • 基于增量运动模型(dx/dy)
    • 物理模拟通过速度分量反转实现
    • 速度值根据屏幕尺寸动态计算
  2. 音乐控制系统

    • 使用独立线程播放避免阻塞
    • 通过Clip位置记录实现精确暂停/继续
    • 对数计算实现自然音量调节
  3. 背景切换机制

    • Guava缓存实现高效资源管理
    • 多来源加载策略(resources → 文件系统)
    • 实时刷新机制(repaint触发即时更新)

这些机制共同实现了游戏的核心交互功能,同时保证了代码的可维护性和性能优化。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/913488.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/913488.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

(十八)深入了解 AVFoundation-编辑:添加背景音乐与音量控制(下)——实战篇

一、功能目标回顾在理论篇中&#xff0c;我们系统地介绍了如何使用 AVFoundation 添加背景音乐音轨&#xff0c;并通过 AVMutableAudioMix 与 AVMutableAudioMixInputParameters 实现多音轨混音与音量控制。我们了解了诸如淡入淡出、静音控制、动态音量曲线等核心技术细节。本篇…

如何在新机器上设置github完成内容git push

如果你在一台新的机器上git pull 仓库&#xff0c;完成修改&#xff0c;然后git push&#xff0c;会发现下面错误&#xff1a; Username for https://github.com: xiaomaolv Password for https://xiaomaolvgithub.com: remote: Support for password authentication was rem…

Rust 注释

Rust 注释 引言 Rust 编程语言以其内存安全、并发支持和高性能等特点在软件开发领域获得了广泛的关注。在Rust编程中&#xff0c;注释是一种非常重要的元素&#xff0c;它不仅可以帮助程序员理解代码&#xff0c;还可以提高代码的可维护性和可读性。本文将详细介绍Rust中的注释…

Flink Oracle CDC 环境配置与验证

一、Oracle 数据库核心配置详解 1. 启用归档日志&#xff08;Archiving Log&#xff09; Oracle CDC 依赖归档日志获取增量变更数据&#xff0c;需按以下步骤启用&#xff1a; 非CDB数据库配置&#xff1a; -- 以DBA身份连接数据库 CONNECT sys/password AS SYSDBA; -- …

ssh: Could not resolve hostname d: Temporary failure in name resolution

关于不能本机上传文件夹到服务器上的一个问题的记录。 scp -r "D:\***\datasets" usernamexxxxxx:接收文件夹名 一直报错&#xff1a;ssh: Could not resolve hostname d: Temporary failure in name resolution 反复尝试发现无果之后想起来&#xff0c;在传输的时候…

2025年的前后端一体化CMS框架优选方案

以下是结合技术生态、开发效率和商业落地验证&#xff0c;整理的2025年前后端一体化CMS框架优选方案&#xff1a;一、‌主流成熟框架组合‌1. ‌React Node.js (Express/Next.js)‌‌前端‌&#xff1a;React生态成熟&#xff0c;配合Redux状态管理&#xff0c;适合复杂后台界…

《声音的变形记:Web Audio API的实时特效法则》

用户期待更丰富、更具沉浸感的听觉体验时&#xff0c;基于Web Audio API实现的实时音频特效&#xff0c;就像是为这片森林注入了灵动的精灵&#xff0c;让简单的声音蜕变为震撼人心的听觉盛宴。回声特效带来空间的深邃回响&#xff0c;变声效果赋予声音全新的个性面貌。接下来&…

LLM场景下的强化学习【PPO】

适合本身对强化学习有基本了解 一、什么是强化学习 一句话&#xff1a;在当前状态(State)下&#xff0c;智能体(Agent)与环境(Environment)交互&#xff0c;并采取动作(Action)进入下一状态&#xff0c;过程中获得奖励(Reward&#xff0c;有正向有负向)&#xff0c;从而实现从…

Python爬虫实战:研究chardet库相关技术

1. 引言 1.1 研究背景与意义 在互联网信息爆炸的时代,网络数据采集技术已成为信息获取、数据分析和知识发现的重要手段。Python 作为一种高效的编程语言,凭借其丰富的第三方库和简洁的语法,成为爬虫开发的首选语言之一。然而,在网络数据采集中,文本编码的多样性和不确定…

回溯题解——全排列【LeetCode】

46. 全排列 一、算法逻辑&#xff08;逐步通顺讲解每一步思路&#xff09; 该算法使用了典型的 回溯&#xff08;backtracking&#xff09; 状态数组 思路&#xff0c;逐层递归生成排列。 题目目标&#xff1a;给定一个无重复整数数组 nums&#xff0c;返回其所有可能的全排…

RICE模型或KANO模型在具体UI评审时的运用经验

模型是抽象的产物,结合场景才好说明(数据为非精确实际数据,仅供参考,勿照搬)。 ​​案例一:RICE模型解决「支付流程优化」vs「首页动效升级」优先级争议​​ ​​背景​​:APP电商模块在迭代中面临两个需求冲突——支付团队主张优化支付失败提示(减少用户流失),设计…

缓存中间件

缓存与分布式锁 即时性、数据一致要求不高的 访问量大且更新频率不高的数据 &#xff08;读多&#xff0c;写少&#xff09; 常用缓存中间件 redis Spring 如果用spring的情况下&#xff0c;由于redis没有受spring的管理&#xff0c; 则我们需要自己先写一个redis的配置类&…

大语言模型全方位解析:从基础认知到RESTful API应用

文章目录 前言一、初见大模型1.1 大语言模型基本知识了解&#xff08;一&#xff09;日常可能用到的大语言模型&#xff08;二&#xff09;大模型的作用&#xff08;三&#xff09;核心价值 1.2 大模型与人工智能关系1.3 大语言模型的“前世今生”与发展1.3.1 大语言模型的发展…

网安系列【11】之目录穿越与文件包含漏洞详解

文章目录 前言一 目录穿越漏洞1.1 什么是目录穿越&#xff1f;1.2 目录穿越的原理1.3 目录穿越的常见形式1.3.1 基本形式1.3.2 编码绕过1.3.3 绝对路径攻击 1.4 实战案例解析1.4.1 案例1&#xff1a;简单的目录穿越1.4.2 案例2&#xff1a;编码绕过 1.5 目录穿越的危害 二、文件…

uri-url-HttpServletRequest

1. 使用HttpServletRequest UrlPathHelper 解析 出 url路径 org.springframework.web.util.UrlPathHelper 是 Spring 框架中用于处理 HTTP 请求路径的一个工具类&#xff0c;它帮助解析和处理与请求路径相关的细节。特别是 getLookupPathForRequest(HttpServletRequest request…

Ubuntu22.04安装p4显卡 nvidia-utils-570-server 570.133.20驱动CUDA Version: 12.8

Ubuntu22.04安装p4显卡 nvidia-utils-570-server 570.133.20驱动CUDA Version: 12.8专业显卡就是专业显卡&#xff0c;尽管p4已经掉到了白菜价&#xff0c;官方的支持却一直都保持&#xff0c;比如它可以装上cuda12.8,这真的出乎我意料。NVIDIA Tesla P4显卡的主要情况Pascal架…

工业日志AI大模型智能分析系统-前端实现

目录 主要架构 前端项目结构 1. 核心实现代码 1.1 API服务封装 (src/api/log.ts) 1.2 TS类型定义 (src/types/api.ts) 1.3 Pinia状态管理 (src/stores/logStore.ts) 1.4 日志分析页面 (src/views/LogAnalysis.vue) 1.5 日志详情组件 (src/components/LogDetail.vue) 2…

C++内存泄漏排查

引言 C内存泄漏问题的普遍性与危害内存泄漏排查大赛的背景与目标文章结构和主要内容概述 内存泄漏的基本概念 内存泄漏的定义与类型&#xff08;显式、隐式、循环引用等&#xff09;C中常见的内存泄漏场景&#xff08;指针管理不当、资源未释放等&#xff09;内存泄漏对程序性能…

20250706-4-Docker 快速入门(上)-常用容器管理命令_笔记

一、常用管理命令1. 选项&#xfeff;&#xfeff;1&#xff09;ls&#xfeff;功能&#xff1a;列出容器常用参数&#xff1a;-a&#xff1a;查看所有容器包含退出的-q&#xff1a;列出所有容器ID-l&#xff1a;列出最新创建的容器状态使用技巧&#xff1a;容器很多时使用dock…

基于 Camunda BPM 的工作流引擎示例项目

项目介绍 这是一个基于 Camunda BPM 的工作流引擎示例项目&#xff0c;包含完整的后台接口和前端页面&#xff0c;实现了流程的设计、部署、执行等核心功能。 技术栈 后端 Spring Boot 2.7.9Camunda BPM 7.18.0MySQL 8.0JDK 1.8 前端 Vue 3Element PlusBpmn.jsVite 功能…