Merge work #7

Merged
fkoppe merged 31 commits from dev/client_koppe into dev/client 2024-11-20 13:50:01 +01:00
13 changed files with 130 additions and 62 deletions
Showing only changes of commit b15dd96a86 - Show all commits

View File

@@ -15,17 +15,19 @@ public class AcousticHandler {
private ArrayList<MusicAsset> gameTracks = new ArrayList<>(); private ArrayList<MusicAsset> gameTracks = new ArrayList<>();
private NanoTimer trackTimer = new NanoTimer(); private NanoTimer trackTimer = new NanoTimer();
private boolean fading = false; private boolean fading = false; // Indicates if a fade is in progress
private NanoTimer fadeTimer = new NanoTimer(); private boolean outFadingFinished = false; // Tracks the completion of the outfade
private static final float FADE_DURATION = 3.0f; private NanoTimer fadeTimer = new NanoTimer(); // Timer to track fade progress
private static final float CROSSFADE_DURATION = 1.5f; private static final float FADE_DURATION = 3.0f; // Duration for outfade
private static final float CROSSFADE_DURATION = 1.5f; // Duration for infade
private GameMusic playing = null; // Currently playing track
private GameMusic scheduled = null; // Scheduled track to play next
private GameMusic old = null; // Old track being faded out
private float mainVolume = 1.0f; private float mainVolume = 1.0f;
private float musicVolume = 1.0f; private float musicVolume = 1.0f;
private float soundVolume = 1.0f; private float soundVolume = 1.0f;
private GameMusic scheduled = null;
private GameMusic playing = null;
private ArrayList<GameSound> sounds = new ArrayList<>(); private ArrayList<GameSound> sounds = new ArrayList<>();
public AcousticHandler(MdgaApp app) { public AcousticHandler(MdgaApp app) {
@@ -103,8 +105,8 @@ public void playState(MdgaState state) {
case GAME: case GAME:
addGameTracks(); addGameTracks();
playGame = true; playGame = true;
assert (gameTracks.size() > 0) : "no more game music available"; assert (!gameTracks.isEmpty()) : "no more game music available";
asset = gameTracks.remove(0); asset = gameTracks.removeFirst();
break; break;
case CEREMONY: case CEREMONY:
playGame = false; playGame = false;
@@ -116,7 +118,7 @@ public void playState(MdgaState state) {
assert (null != asset) : "music sceduling went wrong"; assert (null != asset) : "music sceduling went wrong";
scheduled = new GameMusic(app, asset, getMusicVolumeTotal(), asset.getSubVolume(), asset.getLoop()); scheduled = new GameMusic(app, asset, getMusicVolumeTotal(), asset.getSubVolume(), asset.getLoop(), 0.0f);
} }
/** /**
@@ -132,56 +134,119 @@ private float lerp(float start, float end, float t) {
} }
/** /**
* Updates the current volume and handles track crossfading logic. * Updates the music playback state, handling transitions between tracks with fades.
* This method is responsible for fading out the currently playing track, *
* fading in the scheduled track, and handling crossfade between the two tracks. * This function ensures smooth transitions between tracks, including fading out the
* current track, pausing, and fading in the next scheduled track. It also starts
* music instantly if nothing is currently playing and a track is scheduled.
*
* Function Behavior:
* 1. If no track is playing and a track is scheduled, it starts instantly.
* 2. If a track is playing and a new track is scheduled, it initiates a fade-out
* of the current track, followed by a pause (if applicable), and a fade-in of the new track.
* 3. If a fade process is ongoing, it respects the fade durations and smoothly transitions
* between tracks.
* 4. Tracks that finish their fade-out are paused, and their volume is set to 0.
* 5. During regular playback, the current track's volume is updated to match the overall
* music volume setting.
*
* Special Cases:
* - If a new track is scheduled during an ongoing fade, it interrupts the current fade
* and starts the new fade sequence.
* - Handles null checks for playing and scheduled tracks to prevent errors.
* - Instant start of music is prioritized if nothing is playing.
*
* Preconditions:
* - `playing`, `scheduled`, and `old` may be null, and the function gracefully handles these states.
* - Fade durations and pause durations are configurable via `FADE_DURATION` and `CROSSFADE_DURATION`.
*
* Dependencies:
* - `getMusicVolumeTotal()`: Provides the total volume scaling factor for music.
* - `GameMusic` class methods: `play()`, `pause()`, `update(float volume)`, and `getPause()` for managing track playback.
* - `NanoTimer`: Used to track the elapsed time during fades.
*
* Exceptions:
* - Ensures no NullPointerException occurs by validating track states before invoking methods.
*/ */
private void updateVolumeAndTrack() { private void updateVolumeAndTrack() {
if (playing == null && scheduled != null && !fading) { if (scheduled == null && !fading && playing == null) {
// Nothing to play or fade, early exit
return;
}
if (scheduled != null && playing == null && !fading) {
// Start the scheduled music instantly if nothing is currently playing
playing = scheduled; playing = scheduled;
scheduled = null; scheduled = null;
playing.play(); playing.play();
playing.update(getMusicVolumeTotal()); // Start at full volume
return; return;
} }
if (scheduled != null && !fading) { if (scheduled != null && !fading) {
// Start a new fade process if no fade is ongoing
fading = true; fading = true;
fadeTimer.reset(); fadeTimer.reset();
old = playing;
playing = null; // Clear current track until fade-in begins
outFadingFinished = false;
} }
if (fading) { if (fading) {
float time = fadeTimer.getTimeInSeconds(); float time = fadeTimer.getTimeInSeconds();
if (time <= FADE_DURATION) { if (old != null && time <= FADE_DURATION) {
// Handle outfade for the old track
float t = Math.min(time / FADE_DURATION, 1.0f); float t = Math.min(time / FADE_DURATION, 1.0f);
float oldVolume = lerp(1.0f, 0.0f, t); float oldVolume = lerp(1.0f, 0.0f, t);
if (playing != null) { old.update(getMusicVolumeTotal() * oldVolume);
playing.update(getMusicVolumeTotal() * oldVolume);
}
} }
if (time > FADE_DURATION && time <= FADE_DURATION + CROSSFADE_DURATION) { if (old != null && time > FADE_DURATION) {
float t = Math.min((time - FADE_DURATION) / CROSSFADE_DURATION, 1.0f); // Complete the outfade for the old track
old.pause();
old = null;
outFadingFinished = true;
}
// Respect the pause duration between outfade and infade
float pause = (scheduled != null) ? scheduled.getPause() : 0.0f;
if (time > FADE_DURATION + pause) {
// Start infade for the scheduled track
float infadeTime = time - FADE_DURATION - pause;
if (scheduled != null && infadeTime <= CROSSFADE_DURATION) {
float t = Math.min(infadeTime / CROSSFADE_DURATION, 1.0f);
float newVolume = lerp(0.0f, 1.0f, t); float newVolume = lerp(0.0f, 1.0f, t);
if (!scheduled.isPlaying()) { if (playing != scheduled) {
scheduled.play();
}
scheduled.update(getMusicVolumeTotal() * newVolume);
}
if (time > FADE_DURATION + CROSSFADE_DURATION) {
if (playing != null) {
playing.pause();
}
playing = scheduled; playing = scheduled;
scheduled = null; playing.play();
}
playing.update(getMusicVolumeTotal() * newVolume);
}
if (infadeTime > CROSSFADE_DURATION) {
// Infade is complete, finalize state
fading = false; fading = false;
if (playing != null) {
playing.update(getMusicVolumeTotal()); // Ensure playing track is at full volume
}
scheduled = null;
} }
} }
else if (playing != null) { } else {
// Regular playback state, ensure playing track is updated
if (playing != null) {
playing.update(getMusicVolumeTotal()); playing.update(getMusicVolumeTotal());
} else if (scheduled != null) {
// Handle immediate track switch
fading = true;
fadeTimer.reset();
old = playing;
playing = null; // Clear current track to start fade process
outFadingFinished = false;
}
} }
} }
@@ -203,6 +268,10 @@ private void addGameTracks() {
* a new track will be scheduled to play. If the list of game tracks is empty, it will be refreshed. * a new track will be scheduled to play. If the list of game tracks is empty, it will be refreshed.
*/ */
private void updateGameTracks() { private void updateGameTracks() {
if(null == playing) {
return;
}
if (playing.nearEnd(10)) { if (playing.nearEnd(10)) {
if (gameTracks.isEmpty()) { if (gameTracks.isEmpty()) {
addGameTracks(); addGameTracks();
@@ -214,7 +283,7 @@ private void updateGameTracks() {
MusicAsset nextTrack = gameTracks.remove(0); MusicAsset nextTrack = gameTracks.remove(0);
scheduled = new GameMusic(app, nextTrack, getMusicVolumeTotal(), nextTrack.getSubVolume(), nextTrack.getLoop()); scheduled = new GameMusic(app, nextTrack, getMusicVolumeTotal(), nextTrack.getSubVolume(), nextTrack.getLoop(), 0.0f);
} }
} }

View File

@@ -14,6 +14,7 @@ class GameMusic {
private float volume; private float volume;
private final float subVolume; private final float subVolume;
private final AudioNode music; private final AudioNode music;
private float pause;
/** /**
* Constructs a new GameMusic object. * Constructs a new GameMusic object.
@@ -24,9 +25,10 @@ class GameMusic {
* @param subVolume A relative volume that modifies the base music volume, typically a percentage. * @param subVolume A relative volume that modifies the base music volume, typically a percentage.
* @param loop A flag indicating whether the music should loop once it finishes. * @param loop A flag indicating whether the music should loop once it finishes.
*/ */
GameMusic(MdgaApp app, MusicAsset asset, float volume, float subVolume, boolean loop) { GameMusic(MdgaApp app, MusicAsset asset, float volume, float subVolume, boolean loop, float pause) {
this.volume = volume; this.volume = volume;
this.subVolume = subVolume; this.subVolume = subVolume;
this.pause = pause;
music = new AudioNode(app.getAssetManager(), asset.getPath(), AudioData.DataType.Stream); music = new AudioNode(app.getAssetManager(), asset.getPath(), AudioData.DataType.Stream);
music.setPositional(false); music.setPositional(false);
@@ -107,4 +109,8 @@ void update(float newVolume) {
music.setVolume(volume * subVolume); music.setVolume(volume * subVolume);
} }
} }
float getPause() {
return pause;
}
} }

View File

@@ -7,15 +7,15 @@
* These music assets are used to control the music that plays in different parts of the game, such as menus and in-game music. * These music assets are used to control the music that plays in different parts of the game, such as menus and in-game music.
*/ */
enum MusicAsset { enum MusicAsset {
MAIN_MENU("Spaceship.wav", 1.0f), MAIN_MENU("Spaceship.wav", true, 1.0f),
LOBBY("DeadPlanet.wav", 1.0f), LOBBY("DeadPlanet.wav", true, 1.0f),
CEREMONY("80s,Disco,Life.wav", 1.0f), CEREMONY("80s,Disco,Life.wav", true, 1.0f),
GAME_1("NeonRoadTrip.wav", false, 1.0f), GAME_1("NeonRoadTrip.wav", 1.0f),
GAME_2("NoPressureTrance.wav", false, 1.0f), GAME_2("NoPressureTrance.wav", 1.0f),
GAME_3("TheSynthRave.wav", false, 1.0f), GAME_3("TheSynthRave.wav", 1.0f),
GAME_4("LaserParty.wav", false, 1.0f), GAME_4("LaserParty.wav", 1.0f),
GAME_5("RetroNoir.wav", false, 1.0f), GAME_5("RetroNoir.wav", 1.0f),
GAME_6("SpaceInvaders.wav", false, 1.0f); GAME_6("SpaceInvaders.wav", 1.0f);
private final String path; private final String path;
private final boolean loop; private final boolean loop;

View File

@@ -49,7 +49,7 @@ private void setupAwardCeremony() {
awardCeremonyNode = new Node("AwardCeremonyNode"); awardCeremonyNode = new Node("AwardCeremonyNode");
// Add a background for the award ceremony // Add a background for the award ceremony
Geometry background = createBackground("b1.jpg"); Geometry background = createBackground("b1.png");
awardCeremonyNode.attachChild(background); awardCeremonyNode.attachChild(background);
// Create a container for the UI elements // Create a container for the UI elements
@@ -74,7 +74,7 @@ private void setupStatistics() {
statisticsNode = new Node("StatisticsNode"); statisticsNode = new Node("StatisticsNode");
// Add a background for the statistics // Add a background for the statistics
Geometry background = createBackground("b2.jpg"); Geometry background = createBackground("b2.png");
statisticsNode.attachChild(background); statisticsNode.attachChild(background);
// Create a container for the statistics UI // Create a container for the statistics UI

View File

@@ -1,12 +1,5 @@
package pp.mdga.client; package pp.mdga.client;
import com.jme3.material.Material;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.shape.Quad;
import com.simsilica.lemur.Button;
import com.simsilica.lemur.Container;
import com.simsilica.lemur.Label;
import pp.mdga.client.Board.BoardHandler; import pp.mdga.client.Board.BoardHandler;
public class GameView extends MdgaView { public class GameView extends MdgaView {

View File

@@ -17,18 +17,18 @@ public LobbyView(MdgaApp app) {
@Override @Override
public void enter() { public void enter() {
// Add a background // Add a background
Geometry background = createBackground("sky.jpg"); Geometry background = createBackground("lobby.png");
rootNode.attachChild(background); rootNode.attachChild(background);
// Add color selection boxes // Add color selection boxes
float boxSize = 100; float boxSize = 200;
float spacing = 20; float spacing = 60;
float totalWidth = 4 * boxSize + 3 * spacing; float totalWidth = 4 * boxSize + 3 * spacing;
float startX = (app.getCamera().getWidth() - totalWidth) / 2; float startX = (app.getCamera().getWidth() - totalWidth) / 2;
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
Geometry colorBox = createColorBox(startX + i * (boxSize + spacing), app.getCamera().getHeight() / 2 - boxSize / 2, boxSize); //Geometry colorBox = createColorBox(startX + i * (boxSize + spacing), app.getCamera().getHeight() / 2 - boxSize / 2, boxSize);
rootNode.attachChild(colorBox); //rootNode.attachChild(colorBox);
} }
app.getGuiNode().attachChild(rootNode); app.getGuiNode().attachChild(rootNode);
@@ -68,7 +68,7 @@ private Geometry createColorBox(float x, float y, float size) {
Quad quad = new Quad(size, size); Quad quad = new Quad(size, size);
Geometry geom = new Geometry("ColorBox", quad); Geometry geom = new Geometry("ColorBox", quad);
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", ColorRGBA.White); // Example texture/color mat.setColor("Color", ColorRGBA.Gray); // Example texture/color
geom.setMaterial(mat); geom.setMaterial(mat);
geom.setLocalTranslation(x, y, 0); geom.setLocalTranslation(x, y, 0);
return geom; return geom;

View File

@@ -47,7 +47,7 @@ public void simpleInitApp() {
public void simpleUpdate(float tpf) { public void simpleUpdate(float tpf) {
acousticHandler.update(); acousticHandler.update();
if(test.getTimeInSeconds() < 10) { if(test.getTimeInSeconds() < 8) {
return; return;
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 KiB