From 8e7a96ad1122efd5cb7d8aca64c408d5254c60d7 Mon Sep 17 00:00:00 2001 From: Martin Prokoph Date: Sun, 29 Jun 2025 13:53:48 +0200 Subject: [PATCH] feat: music & sound!!! --- .../java/eu/midnightdust/yaytris/Tetris.java | 4 + .../eu/midnightdust/yaytris/game/Space.java | 16 ++- .../midnightdust/yaytris/game/Tetromino.java | 3 + .../midnightdust/yaytris/ui/GameCanvas.java | 19 ++- .../midnightdust/yaytris/util/FileUtil.java | 37 ++++++ .../yaytris/util/SoundEffect.java | 14 +++ .../midnightdust/yaytris/util/SoundUtil.java | 108 ++++++++++++++++++ 7 files changed, 183 insertions(+), 18 deletions(-) create mode 100644 src/main/java/eu/midnightdust/yaytris/util/FileUtil.java create mode 100644 src/main/java/eu/midnightdust/yaytris/util/SoundEffect.java create mode 100644 src/main/java/eu/midnightdust/yaytris/util/SoundUtil.java diff --git a/src/main/java/eu/midnightdust/yaytris/Tetris.java b/src/main/java/eu/midnightdust/yaytris/Tetris.java index f41ef2d..344751b 100644 --- a/src/main/java/eu/midnightdust/yaytris/Tetris.java +++ b/src/main/java/eu/midnightdust/yaytris/Tetris.java @@ -3,6 +3,7 @@ package eu.midnightdust.yaytris; import eu.midnightdust.yaytris.game.Space; import eu.midnightdust.yaytris.ui.ScoreMenu; import eu.midnightdust.yaytris.ui.TetrisUI; +import eu.midnightdust.yaytris.util.SoundUtil; import javax.swing.*; import java.util.Random; @@ -36,12 +37,14 @@ public class Tetris { } public static void resetSpace() { + SoundUtil.stopMusic("/music/theme.wav"); if (gravityTask != null) gravityTask.cancel(); timer.purge(); space = new Space(); } public static void startGame() { + SoundUtil.playMusic("/music/theme.wav", true); space.spawnTetromino(); gravityTask = new TimerTask() { @Override @@ -56,6 +59,7 @@ public class Tetris { } public static void stopGame() { + SoundUtil.stopMusic("/music/theme.wav"); if (gravityTask != null) gravityTask.cancel(); timer.purge(); if (ui.getMenuPanel() instanceof ScoreMenu) ((ScoreMenu) ui.getMenuPanel()).gameOver(); diff --git a/src/main/java/eu/midnightdust/yaytris/game/Space.java b/src/main/java/eu/midnightdust/yaytris/game/Space.java index 148a3e5..9ff4e1f 100644 --- a/src/main/java/eu/midnightdust/yaytris/game/Space.java +++ b/src/main/java/eu/midnightdust/yaytris/game/Space.java @@ -24,6 +24,10 @@ public class Space { nextShape = getNextShape(); } + public Tetromino getCurrentTetromino() { + return currentTetromino; + } + public TetrominoShape getNextShape() { return TetrominoShape.values()[random.nextInt(TetrominoShape.values().length)]; } @@ -60,14 +64,14 @@ public class Space { int combo = 0; Set completedLines = new TreeSet<>(); for (int line : lines) { - if (line > getMapHeight()) continue; + if (line >= getMapHeight()) continue; Color[] newBlobs = tetromino.getLine(line); for (int i = 0; i < newBlobs.length; i++) { if (newBlobs[i] == null) continue; gameMap[line][i] = newBlobs[i]; } if (Arrays.stream(gameMap[line]).noneMatch(Objects::isNull)) { // Line completed - combo += 1; + combo += 10; completedLines.add(line); combo *= completedLines.size(); } @@ -78,11 +82,11 @@ public class Space { gameMap[i] = (i-1 < 0) ? new Color[gameMap[i].length] : gameMap[i-1]; } } - this.score += combo; - Tetris.updateScore(this.score); + increaseScore(combo); } - public Tetromino getCurrentTetromino() { - return currentTetromino; + public void increaseScore(int amount) { + this.score += amount; + Tetris.updateScore(this.score); } } diff --git a/src/main/java/eu/midnightdust/yaytris/game/Tetromino.java b/src/main/java/eu/midnightdust/yaytris/game/Tetromino.java index faa1425..f09b66e 100644 --- a/src/main/java/eu/midnightdust/yaytris/game/Tetromino.java +++ b/src/main/java/eu/midnightdust/yaytris/game/Tetromino.java @@ -1,6 +1,7 @@ package eu.midnightdust.yaytris.game; import eu.midnightdust.yaytris.Tetris; +import eu.midnightdust.yaytris.util.SoundEffect; import eu.midnightdust.yaytris.util.Vec2i; import java.awt.*; @@ -20,6 +21,7 @@ public class Tetromino { public void fall(int length) { Vec2i newPos = centerPos.offset(Vec2i.of(0, length)); if (collidesVertically(newPos)) { + SoundEffect.BEEP.play(); int[] affectedLines = new int[this.collision.length]; int line = centerPos.getY(); for (int i = 0; i < this.collision.length; i++) { @@ -27,6 +29,7 @@ public class Tetromino { line++; } Tetris.getSpace().onLinesChanged(this, affectedLines); + Tetris.getSpace().increaseScore(20-fallLength); if (fallLength >= 1) Tetris.getSpace().spawnTetromino(); else Tetris.stopGame(); } diff --git a/src/main/java/eu/midnightdust/yaytris/ui/GameCanvas.java b/src/main/java/eu/midnightdust/yaytris/ui/GameCanvas.java index 81c0da8..c40fd13 100644 --- a/src/main/java/eu/midnightdust/yaytris/ui/GameCanvas.java +++ b/src/main/java/eu/midnightdust/yaytris/ui/GameCanvas.java @@ -1,12 +1,11 @@ package eu.midnightdust.yaytris.ui; import eu.midnightdust.yaytris.Tetris; +import eu.midnightdust.yaytris.util.FileUtil; -import javax.imageio.ImageIO; import javax.swing.*; import java.awt.*; import java.awt.image.BufferedImage; -import java.io.IOException; public class GameCanvas extends JPanel { final TetrisUI ui; @@ -14,11 +13,7 @@ public class GameCanvas extends JPanel { GameCanvas(TetrisUI ui) { this.ui = ui; - try { - this.texture = ImageIO.read(this.getClass().getResourceAsStream("/textures/tetromino.png")); - } catch (IOException | NullPointerException ex) { - throw new RuntimeException(ex); - } + this.texture = FileUtil.loadImage("/textures/tetromino.png"); } public void paintComponent(Graphics graphics) { @@ -27,16 +22,16 @@ public class GameCanvas extends JPanel { for (int y = 0; y < Tetris.getSpace().getMapHeight(); y++) { for (int x = 0; x < Tetris.getSpace().getMapWidth(); x++) { - Color color = Tetris.getSpace().getGameMapWithTetromino()[y][x]; - if (color == null) continue; + Color color = Tetris.getSpace().getGameMapWithTetromino()[y][x]; // Get the color of the blob at these coordinates + if (color == null) continue; // Ignore empty cells int blockSize = (int) Math.ceil((float) (this.getWidth() - this.getInsets().left - this.getInsets().right) / Tetris.getSpace().getMapWidth()); - //graphics.setXORMode(withAlpha(color,0)); - graphics.drawImage(texture, x*blockSize +getInsets().left, y*blockSize + getInsets().top, blockSize, blockSize, color, this); - graphics.setColor(withAlpha(color, 120)); + graphics.drawImage(texture, x*blockSize +getInsets().left, y*blockSize + getInsets().top, blockSize, blockSize, color, this); // Draw out (grayscale) texture + graphics.setColor(withAlpha(color, 120)); // Overlay the texture with the blobs color graphics.fillRect(x*blockSize +getInsets().left, y*blockSize + getInsets().top, blockSize, blockSize); } } } + public static Color withAlpha(Color color, int alpha) { return new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha); } diff --git a/src/main/java/eu/midnightdust/yaytris/util/FileUtil.java b/src/main/java/eu/midnightdust/yaytris/util/FileUtil.java new file mode 100644 index 0000000..77c0674 --- /dev/null +++ b/src/main/java/eu/midnightdust/yaytris/util/FileUtil.java @@ -0,0 +1,37 @@ +package eu.midnightdust.yaytris.util; + +import javax.imageio.ImageIO; +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.UnsupportedAudioFileException; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; + +public class FileUtil { + public static BufferedImage loadImage(String location) { + try (InputStream fileStream = FileUtil.class.getResourceAsStream(location)) { + assert fileStream != null; + return ImageIO.read(fileStream); + } catch (IOException | NullPointerException ex) { + throw new RuntimeException(ex); + } + } + + public static AudioInputStream loadAudio(String location) { + try (InputStream fileStream = FileUtil.class.getResourceAsStream(location)) { + assert fileStream != null; + return AudioSystem.getAudioInputStream(fileStream); + } catch (IOException | NullPointerException | UnsupportedAudioFileException ex) { + throw new RuntimeException(ex); + } + } + + public static InputStream getFileStream(String location) { + try (InputStream fileStream = FileUtil.class.getResourceAsStream(location)) { + return fileStream; + } catch (IOException | NullPointerException ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/src/main/java/eu/midnightdust/yaytris/util/SoundEffect.java b/src/main/java/eu/midnightdust/yaytris/util/SoundEffect.java new file mode 100644 index 0000000..2c93781 --- /dev/null +++ b/src/main/java/eu/midnightdust/yaytris/util/SoundEffect.java @@ -0,0 +1,14 @@ +package eu.midnightdust.yaytris.util; + +public enum SoundEffect { + BEEP("/sounds/beep.wav"); + + final String location; + SoundEffect(String location) { + this.location = location; + } + + public void play() { + SoundUtil.playSoundClip(location); + } +} diff --git a/src/main/java/eu/midnightdust/yaytris/util/SoundUtil.java b/src/main/java/eu/midnightdust/yaytris/util/SoundUtil.java new file mode 100644 index 0000000..b0015c4 --- /dev/null +++ b/src/main/java/eu/midnightdust/yaytris/util/SoundUtil.java @@ -0,0 +1,108 @@ +package eu.midnightdust.yaytris.util; + +import eu.midnightdust.yaytris.Settings; + +import javax.sound.sampled.*; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class SoundUtil { + private static final Map musicThreads = new HashMap<>(); + + public static void playMusic(String fileLocation, boolean looped) { + if (musicThreads.containsKey(fileLocation)) stopMusic(fileLocation); + MusicThread musicThread = new MusicThread(fileLocation, looped); + musicThread.start(); + musicThreads.put(fileLocation, musicThread); + } + + public static void stopMusic(String fileLocation) { + if (musicThreads.containsKey(fileLocation)) musicThreads.get(fileLocation).stopMusic(); + } + + // Adapted from: https://www.baeldung.com/java-play-sound + public static void playSoundClip(String fileLocation) { + try (AudioInputStream stream = AudioSystem.getAudioInputStream(SoundUtil.class.getResource(fileLocation))) { // FIXME: Support audio files from JAR. File streams won't work here for some reason. + AudioFormat format = stream.getFormat(); + DataLine.Info info = new DataLine.Info(Clip.class, format); + + Clip audioClip = (Clip) AudioSystem.getLine(info); + audioClip.addLineListener(new LineUpdateListener()); + audioClip.open(stream); + audioClip.start(); + setVolume(audioClip, Settings.soundVolume); + } catch (LineUnavailableException | IOException | UnsupportedAudioFileException e) { + throw new RuntimeException(e); + } + } + + // Adapted from: https://stackoverflow.com/a/40698149 + private static void setVolume(Line line, int volume) { + if (volume < 0 || volume > 100) + throw new IllegalArgumentException("Volume not valid: " + volume); + FloatControl gainControl = (FloatControl) line.getControl(FloatControl.Type.MASTER_GAIN); + gainControl.setValue(20f * (float) Math.log10(volume/100f)); + } + + public static class MusicThread extends Thread { + private static final int BUFFER_SIZE = 8192; + private final boolean looped; + private final String fileLocation; + private boolean playing; + + MusicThread(String fileLocation, boolean looped) { + this.fileLocation = fileLocation; + this.looped = looped; + this.playing = true; + } + + public void stopMusic() { + this.playing = false; + } + + @Override + public void run() { + do {playMusic(fileLocation);} while (looped && playing); + } + + // Adapted from: https://www.baeldung.com/java-play-sound + private void playMusic(String fileLocation) { + try (AudioInputStream stream = AudioSystem.getAudioInputStream(SoundUtil.class.getResource(fileLocation))) { + AudioFormat format = stream.getFormat(); + DataLine.Info info = new DataLine.Info(SourceDataLine.class, format); + + SourceDataLine sourceDataLine = (SourceDataLine) AudioSystem.getLine(info); + sourceDataLine.open(format); + sourceDataLine.start(); + setVolume(sourceDataLine, Settings.musicVolume); + float fadeOut = 1.0f; + + byte[] bufferBytes = new byte[BUFFER_SIZE]; + int readBytes; + while ((readBytes = stream.read(bufferBytes)) != -1) { + if (!playing) { + fadeOut = (float) Math.sin(fadeOut-0.01f); // Sinus fade-out + setVolume(sourceDataLine, (int) (fadeOut * Settings.musicVolume)); + if (fadeOut <= 0) break; + } + sourceDataLine.write(bufferBytes, 0, readBytes); + } + + sourceDataLine.drain(); + sourceDataLine.close(); + } catch (LineUnavailableException | IOException | UnsupportedAudioFileException e) { + throw new RuntimeException(e); + } + } + } + + public static class LineUpdateListener implements LineListener { + @Override + public void update(LineEvent event) { + if (LineEvent.Type.STOP == event.getType()) { + event.getLine().close(); + } + } + } +}