项目:弹射球游戏
项目描述:
类似于乒乓球的游戏,游戏可以播放背景音乐,可以更换背景图,当小球碰到下面的挡板后会反弹,当小球碰到方块后会增加分数,当小球掉落会导致游戏失败,按下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; }
实体类独立处理移动,游戏循环仅调用接口。
关键设计思想总结
-
性能优先:
- Guava缓存减少IO开销
- 并发集合保证线程安全
- 矩形碰撞检测替代复杂计算
-
扩展性设计:
- 背景/音频资源动态加载
- 实体类与游戏逻辑分离
-
用户体验优化:
- 全屏自适应布局
- 音量/背景实时切换
- 暂停菜单防止误操作
-
健壮性保障:
- 资源加载失败时生成后备纹理
- 音频格式转换兼容不同系统
- 显式同步避免多线程冲突
此架构通过分层设计(资源层、逻辑层、渲染层)实现高内聚低耦合,适合扩展为更复杂游戏。完整代码可参考https://www.528045.com/article/b05051cd36.html和http://mp.weixin.qq.com/s?__biz=MzAxNjE2MTcyNQ==&mid=2450448537&idx=2&sn=8ab7f51ee62e382e0eaa9bb175c024cf。
1. dx 和 dy 的含义与小球的移动控制
dx
和 dy
是球体运动的核心控制变量:
private int dx, dy; // Ball类中的运动速度分量public void move() {x += dx; // 每帧水平方向移动dx个像素y += dy; // 每帧垂直方向移动dy个像素
}
工作原理:
-
dx 控制水平运动方向
dx > 0
:球向右移动dx < 0
:球向左移动dx = 0
:球水平静止
-
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; // 暂停位置记录
控制流程:
-
播放控制:
private void playNextMusic() {musicExecutor.execute(() -> {// 加载音频文件currentMusicClip.open(pcmStream);currentMusicClip.start(); // 开始播放}); }
-
暂停与恢复:
private void pauseMusic() {pausePosition = currentMusicClip.getMicrosecondPosition();currentMusicClip.stop(); }private void resumeMusic() {currentMusicClip.setMicrosecondPosition(pausePosition);currentMusicClip.start(); }
-
音量控制:
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); }
-
自动切歌:
currentMusicClip.addLineListener(event -> {if (event.getType() == LineEvent.Type.STOP) {// 自然播放结束后切换到下一首currentMusicIndex = (currentMusicIndex + 1) % musicFiles.size();playNextMusic();} });
3. 通过窗口更换背景图
实现流程:
关键代码:
-
背景选择器设置:
// 使用Guava的ImmutableList填充下拉框 backgroundSelector = new JComboBox<>(BACKGROUND_NAMES.toArray(new String[0]));// 添加选择事件监听器 backgroundSelector.addActionListener(e -> {String selected = (String) backgroundSelector.getSelectedItem();changeBackground(selected); // 更换背景 });
-
背景更换实现:
private void changeBackground(String bgName) {try {// 从Guava缓存获取背景图backgroundImage = backgroundCache.get(bgName);backgroundIndex = BACKGROUND_NAMES.indexOf(bgName);repaint(); // 触发界面重绘} catch (ExecutionException e) {// 异常处理...} }
-
背景缓存加载:
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("背景图不存在");}});
-
最终渲染:
protected void paintComponent(Graphics g) {// 绘制缓存中的背景图g.drawImage(backgroundImage, 0, 0, WIDTH, HEIGHT, this);// ...其他渲染逻辑 }
总结特点
-
小球移动控制:
- 基于增量运动模型(dx/dy)
- 物理模拟通过速度分量反转实现
- 速度值根据屏幕尺寸动态计算
-
音乐控制系统:
- 使用独立线程播放避免阻塞
- 通过Clip位置记录实现精确暂停/继续
- 对数计算实现自然音量调节
-
背景切换机制:
- Guava缓存实现高效资源管理
- 多来源加载策略(resources → 文件系统)
- 实时刷新机制(repaint触发即时更新)
这些机制共同实现了游戏的核心交互功能,同时保证了代码的可维护性和性能优化。