added background music to the game

- added a class BackgroundMusic: is an AbstractAppState and GameEventListener that handles the backgroundmusic
- attached the BackgroundMusic to the stateManager  in the BattleshipApp
- added to the Menu a CheckBox and Slider to manipulate the volume of the backgroundmusic
- added four different music files (for different states of the game)
- edited the WaitState and BattleState to play different music files when chaing to that state
- added to ClientGameLogic a new method playMusic(Music) to play the right music (depends on the current state)
- added a new method receivedEvent(MusicEvent) to handle the music events
- added a new enum Music, that represents different types of music
- added a new record MusicEvent(Music), that decides which music shall play
This commit is contained in:
Daniel Grigencha
2024-10-09 02:14:46 +02:00
parent ec80dd40ce
commit a44cbf2a72
17 changed files with 421 additions and 35 deletions

View File

@@ -0,0 +1,264 @@
package pp.battleship.client;
import com.jme3.app.Application;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.asset.AssetLoadException;
import com.jme3.asset.AssetNotFoundException;
import com.jme3.audio.AudioData.DataType;
import com.jme3.audio.AudioNode;
import pp.battleship.notification.GameEventListener;
import pp.battleship.notification.MusicEvent;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.util.prefs.Preferences;
import static pp.util.PreferencesUtils.getPreferences;
/**
* The BackgroundMusic class represents the background music in the Battleship game application.
* It extends the AbstractAppState class and provides functionalities for playing the menu music,
* game music, victory music, and defeat music.
*/
public class BackgroundMusic extends AbstractAppState implements GameEventListener {
/**
* Logger for the BackgroundMusic class.
*/
private static final Logger LOGGER = System.getLogger(BackgroundMusic.class.getName());
/**
* Preferences for storing music settings.
*/
private static final Preferences PREFERENCES = getPreferences(BackgroundMusic.class);
/**
* Preference key for enabling/disabling music.
*/
private static final String ENABLED_PREF = "enabled"; //NON-NLS
/**
* Preference key for storing the volume level.
*/
private static final String VOLUME_PREF = "volume"; //NON-NLS
/**
* Path to the menu music file.
*/
private static final String MENU_MUSIC_PATH = "Sound/Music/menu_music.ogg";
/**
* Path to the game music file.
*/
private static final String GAME_MUSIC_PATH = "Sound/Music/pirates.ogg";
/**
* Path to the victory music file.
*/
private static final String VICTORY_MUSIC_PATH = "Sound/Music/win_the_game.ogg";
/**
* Path to the defeat music file.
*/
private static final String DEFEAT_MUSIC_PATH = "Sound/Music/defeat.ogg";
/**
* AudioNode for the menu music.
*/
private AudioNode menuMusic;
/**
* AudioNode for the game music.
*/
private AudioNode gameMusic;
/**
* AudioNode for the victory music.
*/
private AudioNode victoryMusic;
/**
* AudioNode for the defeat music.
*/
private AudioNode defeatMusic;
/**
* The currently playing AudioNode.
*/
private AudioNode currentMusic;
/**
* The volume level for the background music.
*/
private float volume;
/**
* Checks if music is enabled in the preferences.
*
* @return {@code true} if music is enabled, {@code false} otherwise.
*/
public static boolean enabledInPreferences() {
return PREFERENCES.getBoolean(ENABLED_PREF, true);
}
/**
* Sets the enabled state of this AppState.
* Overrides {@link com.jme3.app.state.AbstractAppState#setEnabled(boolean)}
*
* @param enabled {@code true} to enable the AppState, {@code false} to disable it.
*/
@Override
public void setEnabled(boolean enabled) {
if (isEnabled() == enabled) return;
super.setEnabled(enabled);
LOGGER.log(Level.INFO, "Music enabled: {0}", enabled); //NON-NLS
PREFERENCES.putBoolean(ENABLED_PREF, enabled);
playCurrentMusic();
}
/**
* Initializes the music for the game.
* Overrides {@link AbstractAppState#initialize(AppStateManager, Application)}
*
* @param stateManager The state manager
* @param app The application
*/
@Override
public void initialize(AppStateManager stateManager, Application app) {
LOGGER.log(Level.INFO, "Initializing background music"); //NON-NLS
super.initialize(stateManager, app);
menuMusic = loadMusic(app, MENU_MUSIC_PATH);
gameMusic = loadMusic(app, GAME_MUSIC_PATH);
victoryMusic = loadMusic(app, VICTORY_MUSIC_PATH);
defeatMusic = loadMusic(app, DEFEAT_MUSIC_PATH);
currentMusic = menuMusic;
playCurrentMusic();
}
/**
* Loads a music file and initializes an AudioNode with the specified settings.
*
* @param app The application instance.
* @param name The name of the music file to load.
* @return The initialized AudioNode, or {@code null} if the file could not be loaded.
*/
private AudioNode loadMusic(Application app, String name) {
try {
this.volume = PREFERENCES.getFloat(VOLUME_PREF, 0.5f);
final AudioNode music = new AudioNode(app.getAssetManager(), name, DataType.Stream);
music.setLooping(true);
music.setVolume(volume);
music.setPositional(false);
music.setDirectional(false);
return music;
} catch (AssetLoadException | AssetNotFoundException ex) {
LOGGER.log(Level.ERROR, ex.getMessage(), ex);
}
return null;
}
/**
* Plays the current music if the music is enabled.
* Stops the current music if the music is disabled.
*/
private void playCurrentMusic() {
if (isEnabled()) {
if (currentMusic != null) {
LOGGER.log(Level.INFO, "Playing current music"); //NON-NLS
currentMusic.play();
}
} else {
if (currentMusic != null) {
currentMusic.stop();
}
}
}
/**
* Plays the game music.
*/
private void gameMusic() {
if (isEnabled() && gameMusic != null) {
stopAll();
LOGGER.log(Level.INFO, "Playing game music"); //NON-NLS
gameMusic.play();
}
}
/**
* Plays the victory music.
*/
private void victoryMusic() {
if (isEnabled() && victoryMusic != null) {
stopAll();
LOGGER.log(Level.INFO, "Playing victory music"); //NON-NLS
victoryMusic.play();
}
}
/**
* Plays the defeat music.
*/
private void defeatMusic() {
if (isEnabled() && defeatMusic != null) {
stopAll();
LOGGER.log(Level.INFO, "Playing defeat music"); //NON-NLS
defeatMusic.play();
}
}
/**
* Stops all music.
*/
private void stopAll() {
if (menuMusic != null) menuMusic.stop();
if (gameMusic != null) gameMusic.stop();
if (victoryMusic != null) victoryMusic.stop();
if (defeatMusic != null) defeatMusic.stop();
}
/**
* Handles the received music event and plays the corresponding music.
*
* @param event The music event to handle.
*/
@Override
public void receivedEvent(MusicEvent event) {
switch (event.music()) {
case GAME_MUSIC -> {
gameMusic();
currentMusic = gameMusic;
}
case VICTORY_MUSIC -> {
victoryMusic();
currentMusic = victoryMusic;
}
case DEFEAT_MUSIC -> {
defeatMusic();
currentMusic = defeatMusic;
}
}
}
/**
* Sets the volume for the background music and updates the preferences.
*
* @param volume The volume level to set.
*/
public void setVolume(float volume) {
LOGGER.log(Level.INFO, "Setting volume to {0}", volume); //NON-NLS
this.volume = volume;
currentMusic.setVolume(volume);
PREFERENCES.putFloat(VOLUME_PREF, volume);
}
/**
* Returns the volume level for the background music.
*
* @return The volume level as a float.
*/
public float getVolume() {
return volume;
}
}

View File

@@ -10,6 +10,7 @@
import com.jme3.app.DebugKeysAppState;
import com.jme3.app.SimpleApplication;
import com.jme3.app.StatsAppState;
import com.jme3.audio.AudioNode;
import com.jme3.font.BitmapFont;
import com.jme3.font.BitmapText;
import com.jme3.input.KeyInput;
@@ -128,8 +129,7 @@ public class BattleshipApp extends SimpleApplication implements BattleshipClient
try {
manager.readConfiguration(new FileInputStream("logging.properties"));
LOGGER.log(Level.INFO, "Successfully read logging properties"); //NON-NLS
}
catch (IOException e) {
} catch (IOException e) {
LOGGER.log(Level.INFO, e.getMessage());
}
}
@@ -267,6 +267,7 @@ private void setupStates() {
stateManager.detach(stateManager.getState(DebugKeysAppState.class));
attachGameSound();
attachBackgroundSound();
stateManager.attachAll(new EditorAppState(), new BattleAppState(), new SeaAppState());
}
@@ -280,6 +281,19 @@ private void attachGameSound() {
stateManager.attach(gameSound);
}
/**
* Attaches the background music state and sets its initial enabled state.
* The background music state is responsible for managing the background music
* playback in the game. It listens to the game logic for any changes in the
* background music settings.
*/
private void attachBackgroundSound() {
final BackgroundMusic backgroundMusic = new BackgroundMusic();
logic.addListener(backgroundMusic);
backgroundMusic.setEnabled(BackgroundMusic.enabledInPreferences());
stateManager.attach(backgroundMusic);
}
/**
* Updates the application state every frame.
* This method is called once per frame during the game loop.
@@ -405,12 +419,12 @@ public void stop(boolean waitFor) {
*/
void confirmDialog(String question, Runnable yesAction) {
DialogBuilder.simple(dialogManager)
.setTitle(lookup("dialog.question"))
.setText(question)
.setOkButton(lookup("button.yes"), yesAction)
.setNoButton(lookup("button.no"))
.build()
.open();
.setTitle(lookup("dialog.question"))
.setText(question)
.setOkButton(lookup("button.yes"), yesAction)
.setNoButton(lookup("button.no"))
.build()
.open();
}
/**
@@ -420,10 +434,10 @@ void confirmDialog(String question, Runnable yesAction) {
*/
void errorDialog(String errorMessage) {
DialogBuilder.simple(dialogManager)
.setTitle(lookup("dialog.error"))
.setText(errorMessage)
.setOkButton(lookup("button.ok"))
.build()
.open();
.setTitle(lookup("dialog.error"))
.setText(errorMessage)
.setOkButton(lookup("button.ok"))
.build()
.open();
}
}

View File

@@ -9,7 +9,10 @@
import com.simsilica.lemur.Button;
import com.simsilica.lemur.Checkbox;
import com.simsilica.lemur.DefaultRangedValueModel;
import com.simsilica.lemur.Label;
import com.simsilica.lemur.Slider;
import com.simsilica.lemur.core.VersionedReference;
import com.simsilica.lemur.style.ElementId;
import pp.dialog.Dialog;
import pp.dialog.StateCheckboxModel;
@@ -19,6 +22,8 @@
import java.io.IOException;
import java.util.prefs.Preferences;
import java.lang.System.Logger;
import static pp.battleship.Resources.lookup;
import static pp.util.PreferencesUtils.getPreferences;
@@ -28,11 +33,13 @@
* returning to the game, and quitting the application.
*/
class Menu extends Dialog {
private static final Logger LOGGER = System.getLogger(Menu.class.getName());
private static final Preferences PREFERENCES = getPreferences(Menu.class);
private static final String LAST_PATH = "last.file.path";
private final BattleshipApp app;
private final Button loadButton = new Button(lookup("menu.map.load"));
private final Button saveButton = new Button(lookup("menu.map.save"));
private final VersionedReference<Double> volumeRef;
/**
* Constructs the Menu dialog for the Battleship application.
@@ -43,8 +50,19 @@ public Menu(BattleshipApp app) {
super(app.getDialogManager());
this.app = app;
addChild(new Label(lookup("battleship.name"), new ElementId("header"))); //NON-NLS
addChild(new Checkbox(lookup("menu.sound-enabled"),
new StateCheckboxModel(app, GameSound.class)));
new StateCheckboxModel(app, GameSound.class)));
addChild(new Checkbox(lookup("menu.music-toggle"),
new StateCheckboxModel(app, BackgroundMusic.class)));
Slider volumeSlider = new Slider();
volumeSlider.setModel(new DefaultRangedValueModel(0.0, 2.0, app.getStateManager().getState(BackgroundMusic.class).getVolume()));
volumeSlider.setDelta(0.1f);
addChild(volumeSlider);
volumeRef = volumeSlider.getModel().createReference();
addChild(loadButton)
.addClickCommands(s -> ifTopDialog(this::loadDialog));
addChild(saveButton)
@@ -65,6 +83,28 @@ public void update() {
saveButton.setEnabled(app.getGameLogic().maySaveMap());
}
/**
* Updates the menu state based on the time per frame (tpf).
* If the volume reference has been updated, adjusts the volume accordingly.
*
* @param tpf the time per frame
*/
@Override
public void update(float tpf) {
if (volumeRef.update()) {
adjustVolume(volumeRef.get());
}
}
/**
* Adjusts the volume of the background music.
*
* @param volume the new volume level to set, as a double
*/
private void adjustVolume(double volume) {
app.getStateManager().getState(BackgroundMusic.class).setVolume((float) volume);
}
/**
* As an escape action, this method closes the menu if it is the top dialog.
*/
@@ -99,8 +139,7 @@ private void handle(FileAction fileAction, TextInputDialog dialog) {
PREFERENCES.put(LAST_PATH, path);
fileAction.run(new File(path));
dialog.close();
}
catch (IOException e) {
} catch (IOException e) {
app.errorDialog(e.getLocalizedMessage());
}
}
@@ -114,13 +153,13 @@ private void handle(FileAction fileAction, TextInputDialog dialog) {
private void fileDialog(FileAction fileAction, String label) {
final TextInputDialog dialog =
TextInputDialog.builder(app.getDialogManager())
.setLabel(lookup("label.file"))
.setFocus(TextInputDialog::getInput)
.setTitle(label)
.setOkButton(lookup("button.ok"), d -> handle(fileAction, d))
.setNoButton(lookup("button.cancel"))
.setOkClose(false)
.build();
.setLabel(lookup("label.file"))
.setFocus(TextInputDialog::getInput)
.setTitle(label)
.setOkButton(lookup("button.ok"), d -> handle(fileAction, d))
.setNoButton(lookup("button.cancel"))
.setOkClose(false)
.build();
final String path = PREFERENCES.get(LAST_PATH, null);
if (path != null)
dialog.getInput().setText(path.trim());

View File

@@ -0,0 +1,10 @@
Personal-use only.
menu_music.ogg
https://pixabay.com/de/music/szenen-aufbauen-demolition-outline-science-fiction-trailer-music-191960/
pirates.ogg
https://pixabay.com/de/music/epische-klassik-pirates-163389/
win_the_game.gg
https://pixabay.com/de/users/enrico_dering-31760131/
defeat.ogg
https://pixabay.com/de/music/dramaszene-defeat-charles-michel-140604/

View File

@@ -11,6 +11,7 @@
import pp.battleship.message.server.EffectMessage;
import pp.battleship.model.IntPoint;
import pp.battleship.model.ShipMap;
import pp.battleship.notification.Music;
import pp.battleship.notification.Sound;
import java.lang.System.Logger.Level;
@@ -62,6 +63,10 @@ public void receivedEffect(EffectMessage msg) {
if (msg.isGameOver()) {
msg.getRemainingOpponentShips().forEach(logic.getOpponentMap()::add);
logic.setState(new GameOverState(logic));
if (msg.isOwnShot())
logic.playMusic(Music.VICTORY_MUSIC);
else
logic.playMusic(Music.DEFEAT_MUSIC);
}
}

View File

@@ -15,13 +15,7 @@
import pp.battleship.model.IntPoint;
import pp.battleship.model.ShipMap;
import pp.battleship.model.dto.ShipMapDTO;
import pp.battleship.notification.ClientStateEvent;
import pp.battleship.notification.GameEvent;
import pp.battleship.notification.GameEventBroker;
import pp.battleship.notification.GameEventListener;
import pp.battleship.notification.InfoTextEvent;
import pp.battleship.notification.Sound;
import pp.battleship.notification.SoundEvent;
import pp.battleship.notification.*;
import java.io.File;
import java.io.IOException;
@@ -258,6 +252,15 @@ public void playSound(Sound sound) {
notifyListeners(new SoundEvent(sound));
}
/**
* Emits an event to play the specified music.
*
* @param music the music to be played.
*/
public void playMusic(Music music) {
notifyListeners(new MusicEvent(music));
}
/**
* Loads a map from the specified file.
*

View File

@@ -10,7 +10,6 @@
import pp.battleship.message.client.MapMessage;
import pp.battleship.model.Battleship;
import pp.battleship.model.IntPoint;
import pp.battleship.model.Rotation;
import pp.battleship.model.ShipMap;
import pp.battleship.model.dto.ShipMapDTO;
@@ -113,8 +112,7 @@ private void placeShip(IntPoint cursor) {
harbor().remove(selectedInHarbor);
preview = null;
selectedInHarbor = null;
}
else {
} else {
preview.setStatus(INVALID_PREVIEW);
ownMap().add(preview);
}
@@ -137,8 +135,7 @@ public void clickHarbor(IntPoint pos) {
harbor().add(selectedInHarbor);
preview = null;
selectedInHarbor = null;
}
else if (shipAtCursor != null) {
} else if (shipAtCursor != null) {
selectedInHarbor = shipAtCursor;
selectedInHarbor.setStatus(VALID_PREVIEW);
harbor().remove(selectedInHarbor);

View File

@@ -9,6 +9,7 @@
import pp.battleship.message.server.GameDetails;
import pp.battleship.message.server.StartBattleMessage;
import pp.battleship.notification.Music;
import java.lang.System.Logger.Level;
@@ -38,6 +39,7 @@ public void receivedStartBattle(StartBattleMessage msg) {
ClientGameLogic.LOGGER.log(Level.INFO, "start battle, {0} turn", msg.isMyTurn() ? "my" : "other's"); //NON-NLS
logic.setInfoText(msg.getInfoTextKey());
logic.setState(new BattleState(logic, msg.isMyTurn()));
logic.playMusic(Music.GAME_MUSIC);
}
/**

View File

@@ -39,6 +39,13 @@ default void receivedEvent(InfoTextEvent event) { /* do nothing */ }
*/
default void receivedEvent(SoundEvent event) { /* do nothing */ }
/**
* Indicates that music shall be played.
*
* @param event the received event
*/
default void receivedEvent(MusicEvent event) { /* do nothing */ }
/**
* Indicates that the client's state has changed.
*

View File

@@ -0,0 +1,23 @@
package pp.battleship.notification;
/**
* Enumeration representing different types of music used in the game.
*/
public enum Music {
/**
* Music for the game.
*/
GAME_MUSIC,
/**
* Music for the menu.
*/
MENU_MUSIC,
/**
* Music for victory.
*/
VICTORY_MUSIC,
/**
* Music for defeat.
*/
DEFEAT_MUSIC
}

View File

@@ -0,0 +1,20 @@
package pp.battleship.notification;
/**
* Event when music is played in the game.
*
* @param music the music to be played
*/
public record MusicEvent(Music music) implements GameEvent {
/**
* Notifies the game event listener of this event.
*
* @param listener the game event listener
*/
@Override
public void notifyListener(GameEventListener listener) {
listener.receivedEvent(this);
}
}

View File

@@ -31,6 +31,7 @@ menu.return-to-game=Return to game
menu.sound-enabled=Sound switched on
menu.map.load=Load map from file...
menu.map.save=Save map in file...
menu.music-toggle=Music on/off
label.file=File:
label.connecting=Connecting...
dialog.error=Error

View File

@@ -31,6 +31,7 @@ menu.return-to-game=Zur
menu.sound-enabled=Sound eingeschaltet
menu.map.load=Karte von Datei laden...
menu.map.save=Karte in Datei speichern...
menu.music-toggle=Musik an/aus
label.file=Datei:
label.connecting=Verbindung wird aufgebaut...
dialog.error=Fehler