added backroundsound

This commit is contained in:
Filip Szepielewicz 2024-10-13 11:24:13 +02:00
parent cd50358181
commit 98dea36368
7 changed files with 453 additions and 67 deletions

View File

@ -22,8 +22,8 @@ import com.simsilica.lemur.GuiGlobals;
import com.simsilica.lemur.style.BaseStyles; import com.simsilica.lemur.style.BaseStyles;
import pp.battleship.client.gui.BattleAppState; import pp.battleship.client.gui.BattleAppState;
import pp.battleship.client.gui.EditorAppState; import pp.battleship.client.gui.EditorAppState;
import pp.battleship.client.gui.GameMusic;
import pp.battleship.client.gui.SeaAppState; import pp.battleship.client.gui.SeaAppState;
import pp.battleship.client.GameMusic;
import pp.battleship.game.client.BattleshipClient; import pp.battleship.game.client.BattleshipClient;
import pp.battleship.game.client.ClientGameLogic; import pp.battleship.game.client.ClientGameLogic;
import pp.battleship.game.client.ServerConnection; import pp.battleship.game.client.ServerConnection;
@ -269,7 +269,6 @@ public class BattleshipApp extends SimpleApplication implements BattleshipClient
attachGameSound(); attachGameSound();
attachGameMusic(); attachGameMusic();
stateManager.attachAll(new EditorAppState(), new BattleAppState(), new SeaAppState()); stateManager.attachAll(new EditorAppState(), new BattleAppState(), new SeaAppState());
} }
@ -284,12 +283,12 @@ public class BattleshipApp extends SimpleApplication implements BattleshipClient
} }
/** /**
* Attaches the music state and sets its initial enabled state. * Attaches the background music state and sets its initial enabled state.
*/ */
private void attachGameMusic() { private void attachGameMusic() {
final GameMusic gameMusic = new GameMusic(); final GameMusic gameSound = new GameMusic();
gameMusic.setEnabled(GameMusic.enabledInPreferences()); gameSound.setEnabled(GameMusic.enabledInPreferences());
stateManager.attach(gameMusic); stateManager.attach(gameSound);
} }
/** /**
@ -439,4 +438,3 @@ public class BattleshipApp extends SimpleApplication implements BattleshipClient
.open(); .open();
} }
} }

View File

@ -1,5 +1,11 @@
package pp.battleship.client; ////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client;
import com.jme3.app.Application; import com.jme3.app.Application;
import com.jme3.app.state.AbstractAppState; import com.jme3.app.state.AbstractAppState;
@ -8,7 +14,8 @@ import com.jme3.asset.AssetLoadException;
import com.jme3.asset.AssetNotFoundException; import com.jme3.asset.AssetNotFoundException;
import com.jme3.audio.AudioData; import com.jme3.audio.AudioData;
import com.jme3.audio.AudioNode; import com.jme3.audio.AudioNode;
import pp.battleship.notification.GameEventListener;
import pp.battleship.notification.SoundEvent;
import java.lang.System.Logger; import java.lang.System.Logger;
import java.lang.System.Logger.Level; import java.lang.System.Logger.Level;
@ -17,17 +24,16 @@ import java.util.prefs.Preferences;
import static pp.util.PreferencesUtils.getPreferences; import static pp.util.PreferencesUtils.getPreferences;
/** /**
* An application state that plays music. * An application state that plays sounds.
*/ */
public class GameMusic extends AbstractAppState { public class GameSound extends AbstractAppState implements GameEventListener {
private static final Logger LOGGER = System.getLogger(GameMusic.class.getName()); private static final Logger LOGGER = System.getLogger(GameSound.class.getName());
private static final Preferences PREFERENCES = getPreferences(GameMusic.class); private static final Preferences PREFERENCES = getPreferences(GameSound.class);
private static final String ENABLED_PREF = "enabled"; //NON-NLS private static final String ENABLED_PREF = "enabled"; //NON-NLS
private static final String VOLUME_PREF = "volume"; //NON-NLS
private AudioNode battleMusic; private AudioNode splashSound;
private AudioNode menuMusicModern; private AudioNode shipDestroyedSound;
private AudioNode menuMusicTraditional; private AudioNode explosionSound;
/** /**
* Checks if sound is enabled in the preferences. * Checks if sound is enabled in the preferences.
@ -37,17 +43,6 @@ public class GameMusic extends AbstractAppState {
public static boolean enabledInPreferences() { public static boolean enabledInPreferences() {
return PREFERENCES.getBoolean(ENABLED_PREF, true); return PREFERENCES.getBoolean(ENABLED_PREF, true);
} }
/**
* Checks enabled volume in preferences.
*
* @return {@code float} if a volumePreference is set, the volume is set to the Value in PREFERENCES,
* {@code 0.25f} if no volumePreference is set in PREFERENCES, the Volume is set to a default of 0.25f
*
*/
public static float volumePreference() {
return PREFERENCES.getFloat(VOLUME_PREF, 0.25f);
}
/** /**
* Toggles the game sound on or off. * Toggles the game sound on or off.
@ -65,20 +60,14 @@ public class GameMusic extends AbstractAppState {
@Override @Override
public void setEnabled(boolean enabled) { public void setEnabled(boolean enabled) {
if (isEnabled() == enabled) return; if (isEnabled() == enabled) return;
else if (!isEnabled() && enabled) {
if (menuMusicModern != null) menuMusicModern.play();
} else if (isEnabled() && !enabled) {
if (menuMusicModern != null) menuMusicModern.stop();
}
super.setEnabled(enabled); super.setEnabled(enabled);
LOGGER.log(Level.INFO, "Music enabled: {0}", enabled); //NON-NLS LOGGER.log(Level.INFO, "Sound enabled: {0}", enabled); //NON-NLS
PREFERENCES.putBoolean(ENABLED_PREF, enabled); PREFERENCES.putBoolean(ENABLED_PREF, enabled);
} }
/** /**
* Initializes the music. * Initializes the sound effects for the game.
* Overrides {@link AbstractAppState#initialize(AppStateManager, Application)} * Overrides {@link com.jme3.app.state.AbstractAppState#initialize(com.jme3.app.state.AppStateManager, com.jme3.app.Application)}
* *
* @param stateManager The state manager * @param stateManager The state manager
* @param app The application * @param app The application
@ -86,17 +75,13 @@ public class GameMusic extends AbstractAppState {
@Override @Override
public void initialize(AppStateManager stateManager, Application app) { public void initialize(AppStateManager stateManager, Application app) {
super.initialize(stateManager, app); super.initialize(stateManager, app);
menuMusicModern =loadSound(app, "Sound/BackgroundMusic/menu-music-modern.ogg"); shipDestroyedSound = loadSound(app, "Sound/Effects/sunken.wav"); //NON-NLS
setVolume(volumePreference()); splashSound = loadSound(app, "Sound/Effects/splash.wav"); //NON-NLS
menuMusicModern.setLooping(true); explosionSound = loadSound(app, "Sound/Effects/explosion.wav"); //NON-NLS
if (isEnabled() && menuMusicModern != null) {
menuMusicModern.play();
}
} }
/** /**
* Loads the music from the specified file. * Loads a sound from the specified file.
* *
* @param app The application * @param app The application
* @param name The name of the sound file. * @param name The name of the sound file.
@ -116,18 +101,35 @@ public class GameMusic extends AbstractAppState {
} }
/** /**
* Sets the vol param to the level set in PREFERENCES * Plays the splash sound effect.
*
* @param vol Volume level of the music as indicated by the Volume control Slider
*
*/ */
public void setVolume(float vol){ public void splash() {
menuMusicModern.setVolume(vol); if (isEnabled() && splashSound != null)
PREFERENCES.putFloat(VOLUME_PREF, vol); splashSound.playInstance();
} }
/**
* Plays the explosion sound effect.
*/
public void explosion() {
if (isEnabled() && explosionSound != null)
explosionSound.playInstance();
} }
/**
* Plays sound effect when a ship has been destroyed.
*/
public void shipDestroyed() {
if (isEnabled() && shipDestroyedSound != null)
shipDestroyedSound.playInstance();
}
@Override
public void receivedEvent(SoundEvent event) {
switch (event.sound()) {
case EXPLOSION -> explosion();
case SPLASH -> splash();
case DESTROYED_SHIP -> shipDestroyed();
}
}
}

View File

@ -11,6 +11,8 @@ import com.simsilica.lemur.Button;
import com.simsilica.lemur.Checkbox; import com.simsilica.lemur.Checkbox;
import com.simsilica.lemur.Label; import com.simsilica.lemur.Label;
import com.simsilica.lemur.style.ElementId; import com.simsilica.lemur.style.ElementId;
import pp.battleship.client.gui.GameMusic;
import pp.battleship.client.gui.VolumeSlider;
import pp.dialog.Dialog; import pp.dialog.Dialog;
import pp.dialog.StateCheckboxModel; import pp.dialog.StateCheckboxModel;
import pp.dialog.TextInputDialog; import pp.dialog.TextInputDialog;
@ -33,6 +35,7 @@ class Menu extends Dialog {
private final BattleshipApp app; private final BattleshipApp app;
private final Button loadButton = new Button(lookup("menu.map.load")); private final Button loadButton = new Button(lookup("menu.map.load"));
private final Button saveButton = new Button(lookup("menu.map.save")); private final Button saveButton = new Button(lookup("menu.map.save"));
private final VolumeSlider slider;
/** /**
* Constructs the Menu dialog for the Battleship application. * Constructs the Menu dialog for the Battleship application.
@ -42,17 +45,19 @@ class Menu extends Dialog {
public Menu(BattleshipApp app) { public Menu(BattleshipApp app) {
super(app.getDialogManager()); super(app.getDialogManager());
this.app = app; this.app = app;
slider = new VolumeSlider(app.getStateManager().getState(GameMusic.class));
addChild(new Label(lookup("battleship.name"), new ElementId("header"))); //NON-NLS addChild(new Label(lookup("battleship.name"), new ElementId("header"))); //NON-NLS
addChild(new Checkbox(lookup("menu.sound-enabled"), addChild(new Checkbox(lookup("menu.sound-enabled"), new StateCheckboxModel(app, GameSound.class)));
new StateCheckboxModel(app, GameSound.class)));
addChild(loadButton) addChild(new Checkbox(lookup("menu.background-sound-enabled"), new StateCheckboxModel(app, GameMusic.class)));
.addClickCommands(s -> ifTopDialog(this::loadDialog));
addChild(saveButton) addChild(slider);
.addClickCommands(s -> ifTopDialog(this::saveDialog));
addChild(new Button(lookup("menu.return-to-game"))) addChild(loadButton).addClickCommands(s -> ifTopDialog(this::loadDialog));
.addClickCommands(s -> ifTopDialog(this::close)); addChild(saveButton).addClickCommands(s -> ifTopDialog(this::saveDialog));
addChild(new Button(lookup("menu.quit"))) addChild(new Button(lookup("menu.return-to-game"))).addClickCommands(s -> ifTopDialog(this::close));
.addClickCommands(s -> ifTopDialog(app::closeApp)); addChild(new Button(lookup("menu.quit"))).addClickCommands(s -> ifTopDialog(app::closeApp));
update(); update();
} }
@ -65,6 +70,11 @@ class Menu extends Dialog {
saveButton.setEnabled(app.getGameLogic().maySaveMap()); saveButton.setEnabled(app.getGameLogic().maySaveMap());
} }
@Override
public void update(float delta) {
slider.update();
}
/** /**
* As an escape action, this method closes the menu if it is the top dialog. * As an escape action, this method closes the menu if it is the top dialog.
*/ */
@ -82,7 +92,7 @@ class Menu extends Dialog {
* Executes a file action. * Executes a file action.
* *
* @param file the file to be processed * @param file the file to be processed
* @throws IOException if an I/O error occurs * @throws java.io.IOException if an I/O error occurs
*/ */
void run(File file) throws IOException; void run(File file) throws IOException;
} }

View File

@ -0,0 +1,119 @@
package pp.battleship.client.gui;
import static pp.util.PreferencesUtils.getPreferences;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.util.prefs.Preferences;
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;
import com.jme3.audio.AudioNode;
public class GameMusic extends AbstractAppState{
private static final Logger LOGGER = System.getLogger(GameMusic.class.getName());
private static final Preferences PREFERENCES = getPreferences(GameMusic.class);
private static final String ENABLED_PREF = "enabled"; //NON-NLS
private static final String VOLUME_PREF = "volume"; //NON-NLS
private AudioNode music;
/**
* Checks if sound is enabled in the preferences.
*
* @return {@code true} if sound is enabled, {@code false} otherwise.
*/
public static boolean enabledInPreferences() {
return PREFERENCES.getBoolean(ENABLED_PREF, true);
}
/**
* Checks if sound is enabled in the preferences.
*
* @return float to which the volume is set
*/
public static float volumeInPreferences() {
return PREFERENCES.getFloat(VOLUME_PREF, 0.5f);
}
/**
* Initializes the sound effects 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) {
super.initialize(stateManager, app);
music = loadSound(app, "Sound/background.ogg");
setVolume(volumeInPreferences());
music.setLooping(true);
if (isEnabled() && music != null) {
music.play();
}
}
/**
* Loads a sound from the specified file.
*
* @param app The application
* @param name The name of the sound file.
* @return The loaded AudioNode.
*/
private AudioNode loadSound(Application app, String name) {
try {
final AudioNode sound = new AudioNode(app.getAssetManager(), name, AudioData.DataType.Buffer);
sound.setLooping(false);
sound.setPositional(false);
return sound;
}
catch (AssetLoadException | AssetNotFoundException ex) {
LOGGER.log(Level.ERROR, ex.getMessage(), ex);
}
return null;
}
/**
* 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;
if (music != null) {
if (enabled) {
music.play();
} else {
music.stop();
}
}
super.setEnabled(enabled);
LOGGER.log(Level.INFO, "Sound enabled: {0}", enabled); //NON-NLS
PREFERENCES.putBoolean(ENABLED_PREF, enabled);
}
/**
* Toggles the game sound on or off.
*/
public void toggleSound() {
setEnabled(!isEnabled());
}
/**
* Sets the volume of music
* @param vol the volume to which the music should be set
*/
public void setVolume(float vol){
music.setVolume(vol);
PREFERENCES.putFloat(VOLUME_PREF, vol);
}
}

View File

@ -0,0 +1,35 @@
package pp.battleship.client.gui;
import com.simsilica.lemur.Slider;
/**
* The VolumeSlider class represents the Volume Slider in the Menu.
* It extends the Slider class and provides functionalities for setting the music volume,
* with the help of the Slider in the GUI
*/
public class VolumeSlider extends Slider {
private final GameMusic music;
private double vol;
/**
* Constructs the Volume Slider for the Menu dialog
* @param music the music instance
*/
public VolumeSlider(GameMusic music) {
super();
this.music = music;
vol = GameMusic.volumeInPreferences();
getModel().setPercent(vol);
}
/**
* when triggered it updates the volume to the value set with the slider
*/
public void update() {
if (vol != getModel().getPercent()) {
vol = getModel().getPercent();
music.setVolume( (float) vol);
}
}
}

View File

@ -0,0 +1,205 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client.server;
import com.jme3.network.ConnectionListener;
import com.jme3.network.HostedConnection;
import com.jme3.network.Message;
import com.jme3.network.MessageListener;
import com.jme3.network.Network;
import com.jme3.network.Server;
import com.jme3.network.serializing.Serializer;
import pp.battleship.BattleshipConfig;
import pp.battleship.game.server.Player;
import pp.battleship.game.server.ServerGameLogic;
import pp.battleship.game.server.ServerSender;
import pp.battleship.message.client.ClientMessage;
import pp.battleship.message.client.MapMessage;
import pp.battleship.message.client.ShootMessage;
import pp.battleship.message.server.EffectMessage;
import pp.battleship.message.server.GameDetails;
import pp.battleship.message.server.ServerMessage;
import pp.battleship.message.server.StartBattleMessage;
import pp.battleship.model.Battleship;
import pp.battleship.model.IntPoint;
import pp.battleship.model.Shot;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.logging.LogManager;
/**
* Server implementing the visitor pattern as MessageReceiver for ClientMessages
*/
public class BattleshipServer implements MessageListener<HostedConnection>, ConnectionListener, ServerSender {
private static final Logger LOGGER = System.getLogger(BattleshipServer.class.getName());
private static final File CONFIG_FILE = new File("server.properties");
private final BattleshipConfig config = new BattleshipConfig();
private Server myServer;
private final ServerGameLogic logic;
private final BlockingQueue<ReceivedMessage> pendingMessages = new LinkedBlockingQueue<>();
private boolean running = true; // Condition for stopping the server
static {
// Configure logging
LogManager manager = LogManager.getLogManager();
try {
manager.readConfiguration(new FileInputStream("logging.properties"));
LOGGER.log(Level.INFO, "Successfully read logging properties"); //NON-NLS
}
catch (IOException e) {
LOGGER.log(Level.INFO, e.getMessage());
}
}
/**
* Starts the Battleships server.
*/
public static void main(String[] args) {
new BattleshipServer().run();
}
/**
* Creates the server.
*/
BattleshipServer() {
config.readFromIfExists(CONFIG_FILE);
LOGGER.log(Level.INFO, "Configuration: {0}", config); //NON-NLS
logic = new ServerGameLogic(this, config);
}
public void run() {
startServer();
try {
while (running) {
processNextMessage();
}
} finally {
stopServer(); // Stop the server when the loop exits
}
}
/**
* Starts the server and initializes required settings.
*/
private void startServer() {
try {
LOGGER.log(Level.INFO, "Starting server..."); //NON-NLS
myServer = Network.createServer(config.getPort());
initializeSerializables();
myServer.start();
registerListeners();
LOGGER.log(Level.INFO, "Server started: {0}", myServer.isRunning()); //NON-NLS
} catch (IOException e) {
LOGGER.log(Level.ERROR, "Couldn't start server: {0}", e.getMessage()); //NON-NLS
exit(1);
}
}
/**
* Stops the server and closes all connections.
*/
private void stopServer() {
LOGGER.log(Level.INFO, "Stopping server..."); //NON-NLS
if (myServer != null) {
for (HostedConnection client : myServer.getConnections()) {
if (client != null) client.close("Game over");
}
myServer.close();
LOGGER.log(Level.INFO, "Server stopped."); //NON-NLS
}
}
/**
* Gracefully stops the server loop by setting the running flag to false.
*/
public void stop() {
running = false;
}
private void processNextMessage() {
try {
pendingMessages.take().process(logic);
} catch (InterruptedException ex) {
LOGGER.log(Level.INFO, "Interrupted while waiting for messages"); //NON-NLS
Thread.currentThread().interrupt();
}
}
private void initializeSerializables() {
Serializer.registerClass(GameDetails.class);
Serializer.registerClass(StartBattleMessage.class);
Serializer.registerClass(MapMessage.class);
Serializer.registerClass(ShootMessage.class);
Serializer.registerClass(EffectMessage.class);
Serializer.registerClass(Battleship.class);
Serializer.registerClass(IntPoint.class);
Serializer.registerClass(Shot.class);
}
private void registerListeners() {
myServer.addMessageListener(this, MapMessage.class);
myServer.addMessageListener(this, ShootMessage.class);
myServer.addConnectionListener(this);
}
@Override
public void messageReceived(HostedConnection source, Message message) {
LOGGER.log(Level.INFO, "message received from {0}: {1}", source.getId(), message); //NON-NLS
if (message instanceof ClientMessage clientMessage)
pendingMessages.add(new ReceivedMessage(clientMessage, source.getId()));
}
@Override
public void connectionAdded(Server server, HostedConnection hostedConnection) {
LOGGER.log(Level.INFO, "new connection {0}", hostedConnection); //NON-NLS
logic.addPlayer(hostedConnection.getId());
}
@Override
public void connectionRemoved(Server server, HostedConnection hostedConnection) {
LOGGER.log(Level.INFO, "connection closed: {0}", hostedConnection); //NON-NLS
final Player player = logic.getPlayerById(hostedConnection.getId());
if (player == null)
LOGGER.log(Level.INFO, "closed connection does not belong to an active player"); //NON-NLS
else {
LOGGER.log(Level.INFO, "closed connection belongs to {0}", player); //NON-NLS
exit(0);
}
}
private void exit(int exitValue) {
LOGGER.log(Level.INFO, "close request"); //NON-NLS
stop();
System.exit(exitValue);
}
/**
* Send the specified message to the specified connection.
*
* @param id the connection id
* @param message the message
*/
public void send(int id, ServerMessage message) {
if (myServer == null || !myServer.isRunning()) {
LOGGER.log(Level.ERROR, "no server running when trying to send {0}", message); //NON-NLS
return;
}
final HostedConnection connection = myServer.getConnection(id);
if (connection != null)
connection.send(message);
else
LOGGER.log(Level.ERROR, "there is no connection with id={0}", id); //NON-NLS
}
}

View File

@ -0,0 +1,17 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client.server;
import pp.battleship.message.client.ClientInterpreter;
import pp.battleship.message.client.ClientMessage;
record ReceivedMessage(ClientMessage message, int from) {
void process(ClientInterpreter interpreter) {
message.accept(interpreter, from);
}
}