Wasser-Sounds verfeinert
This commit is contained in:
@@ -110,8 +110,13 @@ public class FootstepSystem {
|
||||
* @param gait "walking" / "running" / "sprinting"
|
||||
*/
|
||||
public void play(float[] surfaceWeights, String gait) {
|
||||
play(surfaceWeights, gait, 1f);
|
||||
}
|
||||
|
||||
public void play(float[] surfaceWeights, String gait, float volumeScale) {
|
||||
float baseVol = BASE_VOLUME * gaitVolumeFactor(gait)
|
||||
* (audioSettings != null ? audioSettings.effectiveEffects() : 1f);
|
||||
* (audioSettings != null ? audioSettings.effectiveEffects() : 1f)
|
||||
* volumeScale;
|
||||
float pitch = PITCH_CENTER + (random.nextFloat() - 0.5f) * PITCH_RANGE;
|
||||
|
||||
SurfaceType[] types = SurfaceType.values();
|
||||
|
||||
@@ -65,7 +65,7 @@ public class PlayerInputControl {
|
||||
private static final String BONE_LEFT_FOOT = "mixamorig:LeftFoot";
|
||||
private static final String BONE_RIGHT_FOOT = "mixamorig:RightFoot";
|
||||
/** Wassertiefe (m) ab der Laufen/Sprinten gesperrt ist und Watgeräusch spielt. */
|
||||
public static final float WATER_WADING_DEPTH = 0.5f;
|
||||
public static final float WATER_WADING_DEPTH = 0.75f;
|
||||
|
||||
private de.blight.game.audio.FootstepSystem footstepSystem;
|
||||
private java.util.function.BiFunction<Float, Float, float[]> surfaceQuery;
|
||||
@@ -680,27 +680,23 @@ public class PlayerInputControl {
|
||||
}
|
||||
|
||||
private void pollFootsteps(float tpf) {
|
||||
if (!physicsChar.onGround() || blockingAnimActive || lockedInPlace) {
|
||||
// Wading-Ambient: immer wenn im Wasser, auch im Stand oder blockiert
|
||||
if (currentWaterDepth > 0f) {
|
||||
startWading();
|
||||
} else {
|
||||
stopWading();
|
||||
}
|
||||
|
||||
if (!physicsChar.onGround() || blockingAnimActive || lockedInPlace) {
|
||||
resetFootstepState();
|
||||
return;
|
||||
}
|
||||
float speed = physicsChar.getWalkDirection().length();
|
||||
if (speed < 0.001f) {
|
||||
stopWading();
|
||||
resetFootstepState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentWaterDepth > WATER_WADING_DEPTH) {
|
||||
// Im Wasser: Watgeräusch abspielen, Fußgeräusche unterdrücken
|
||||
startWading();
|
||||
resetFootstepState();
|
||||
return;
|
||||
}
|
||||
|
||||
stopWading();
|
||||
|
||||
if (armature == null || footstepSystem == null || surfaceQuery == null) return;
|
||||
String gait = currentAnimToGait(currentAnim);
|
||||
if (gait == null) return;
|
||||
@@ -754,9 +750,16 @@ public class PlayerInputControl {
|
||||
}
|
||||
|
||||
private void triggerStep(String gait) {
|
||||
Vector3f pos = physicsChar.getPhysicsLocation();
|
||||
float[] weights = surfaceQuery.apply(pos.x, pos.z);
|
||||
footstepSystem.play(weights, gait);
|
||||
if (currentWaterDepth > 0f) {
|
||||
// Im Wasser: Wassergeräusche aus footsteps/water/walking bzw. /running
|
||||
float[] weights = new float[de.blight.game.audio.SurfaceType.values().length];
|
||||
weights[de.blight.game.audio.SurfaceType.WATER.ordinal()] = 1f;
|
||||
footstepSystem.play(weights, gait);
|
||||
} else {
|
||||
Vector3f pos = physicsChar.getPhysicsLocation();
|
||||
float[] weights = surfaceQuery.apply(pos.x, pos.z);
|
||||
footstepSystem.play(weights, gait);
|
||||
}
|
||||
}
|
||||
|
||||
/** Durchsucht den Szenegraphen rekursiv nach dem ersten SkinningControl. */
|
||||
|
||||
@@ -101,6 +101,7 @@ public class WorldScene extends BaseAppState {
|
||||
private float spawnZ = 0f;
|
||||
private float spawnYaw = 0f;
|
||||
private de.blight.game.state.OceanSoundState oceanSound;
|
||||
private de.blight.game.state.RiverSoundState riverSound;
|
||||
private de.blight.game.state.AmbientSoundSystem ambientSounds;
|
||||
private de.blight.game.state.MusicSystem musicSystem;
|
||||
private de.blight.game.audio.FootstepSystem footstepSystem;
|
||||
@@ -189,10 +190,20 @@ public class WorldScene extends BaseAppState {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Liefert die Wassertiefe des Spielers in Metern; 0 wenn über Y=0. */
|
||||
/** Liefert die Wassertiefe (Meer + Fluss) an (worldX, worldZ) in Metern. */
|
||||
public float queryWaterDepth(float worldX, float worldZ) {
|
||||
if (physicsChar == null) return 0f;
|
||||
return Math.max(0f, -physicsChar.getPhysicsLocation().y);
|
||||
float playerY = physicsChar.getPhysicsLocation().y;
|
||||
// Ozean: Terrain unter Meeresspiegel → Tiefe = -terrainH
|
||||
float oceanDepth = 0f;
|
||||
if (terrainChunkState != null) {
|
||||
float h = terrainChunkState.getHeightAt(worldX, worldZ);
|
||||
if (h < 0f) oceanDepth = -h;
|
||||
}
|
||||
// Fluss: echte Tiefe = Wasseroberfläche - Spieler-Y
|
||||
float riverDepth = (riverSound != null)
|
||||
? riverSound.getWaterDepth(worldX, playerY, worldZ) : 0f;
|
||||
return Math.max(oceanDepth, riverDepth);
|
||||
}
|
||||
|
||||
/** Wird von ConfigScreen nach dem Speichern aufgerufen. */
|
||||
@@ -296,12 +307,12 @@ public class WorldScene extends BaseAppState {
|
||||
playerInput.setWaterDepthQuery(this::queryWaterDepth);
|
||||
try {
|
||||
com.jme3.audio.AudioNode wadingNode = new com.jme3.audio.AudioNode(
|
||||
assetManager, "audio/footsteps/water/wading.ogg", com.jme3.audio.AudioData.DataType.Buffer);
|
||||
assetManager, "audio/footsteps/water/wading/wading.ogg", com.jme3.audio.AudioData.DataType.Buffer);
|
||||
wadingNode.setLooping(true);
|
||||
wadingNode.setPositional(false);
|
||||
de.blight.game.state.AudioSettingsState _as =
|
||||
app.getStateManager().getState(de.blight.game.state.AudioSettingsState.class);
|
||||
wadingNode.setVolume(_as != null ? _as.effectiveEffects() : 0.7f);
|
||||
wadingNode.setVolume((_as != null ? _as.effectiveEffects() : 0.7f) * 0.1f);
|
||||
rootNode.attachChild(wadingNode);
|
||||
playerInput.setWadingSound(wadingNode);
|
||||
} catch (Exception e) {
|
||||
@@ -445,6 +456,7 @@ public class WorldScene extends BaseAppState {
|
||||
app.getListener().setLocation(pos);
|
||||
app.getListener().setRotation(app.getCamera().getRotation());
|
||||
if (oceanSound != null) oceanSound.setPlayerPosition(pos);
|
||||
if (riverSound != null) riverSound.setPlayerPosition(pos);
|
||||
if (ambientSounds != null) ambientSounds.setPlayerPosition(pos);
|
||||
if (musicSystem != null) musicSystem.setPlayerPosition(pos);
|
||||
}
|
||||
@@ -966,6 +978,11 @@ public class WorldScene extends BaseAppState {
|
||||
oceanSound = new de.blight.game.state.OceanSoundState(terrainChunkState);
|
||||
app.getStateManager().attach(oceanSound);
|
||||
|
||||
app.getStateManager().attach(new de.blight.game.state.WaterfallSoundState());
|
||||
|
||||
riverSound = new de.blight.game.state.RiverSoundState();
|
||||
app.getStateManager().attach(riverSound);
|
||||
|
||||
ambientSounds = new de.blight.game.state.AmbientSoundSystem();
|
||||
app.getStateManager().attach(ambientSounds);
|
||||
|
||||
|
||||
@@ -25,7 +25,8 @@ public class OceanSoundState extends BaseAppState {
|
||||
private static final float MAX_DIST = 60f;
|
||||
private static final float REF_DIST = 8f;
|
||||
private static final float WIND_THRESHOLD = 20f;
|
||||
private static final float FADE_RATE = 1f / 3f;
|
||||
private static final float FADE_RATE_CALM = 1f / 3f;
|
||||
private static final float FADE_RATE_STORMY = 1f / 10f;
|
||||
private static final float SCAN_INTERVAL = 0.25f;
|
||||
private static final float SCAN_STEP = 5f;
|
||||
/**
|
||||
@@ -98,12 +99,6 @@ public class OceanSoundState extends BaseAppState {
|
||||
}
|
||||
|
||||
boolean playerInWater = terrain.getHeightAt(playerPos.x, playerPos.z) < 0f;
|
||||
if (playerInWater) {
|
||||
applyVolumes(0f, 0f, 0.707f, 0.707f);
|
||||
calmVol = 0f;
|
||||
stormyVol = 0f;
|
||||
return;
|
||||
}
|
||||
|
||||
WeatherState weather = getApplication().getStateManager().getState(WeatherState.class);
|
||||
float wind = weather != null ? weather.getWindSpeed() : 4f;
|
||||
@@ -111,22 +106,29 @@ public class OceanSoundState extends BaseAppState {
|
||||
AudioSettingsState audioSettings = getApplication().getStateManager().getState(AudioSettingsState.class);
|
||||
float scale = audioSettings != null ? audioSettings.effectiveAmbient() : 0.5f;
|
||||
|
||||
// Im Wasser: distScale = 1.0 (scan setzt oceanDist=0), Sound bleibt voll
|
||||
float distScale = oceanInRange
|
||||
? Math.max(0f, 1f - Math.max(0f, oceanDist - REF_DIST) / (MAX_DIST - REF_DIST))
|
||||
: 0f;
|
||||
|
||||
float calmTarget = (oceanInRange && wind < WIND_THRESHOLD) ? scale * distScale : 0f;
|
||||
float stormyTarget = (oceanInRange && wind >= WIND_THRESHOLD) ? scale * distScale : 0f;
|
||||
float stormyTarget = (oceanInRange && wind >= WIND_THRESHOLD) ? scale * distScale * 0.75f : 0f;
|
||||
|
||||
calmVol = approach(calmVol, calmTarget, FADE_RATE * tpf);
|
||||
stormyVol = approach(stormyVol, stormyTarget, FADE_RATE * tpf);
|
||||
calmVol = approach(calmVol, calmTarget, FADE_RATE_CALM * tpf);
|
||||
stormyVol = approach(stormyVol, stormyTarget, FADE_RATE_STORMY * tpf);
|
||||
|
||||
float panL = 0.707f, panR = 0.707f; // default: center
|
||||
if (playing && oceanFound) {
|
||||
float panL, panR;
|
||||
if (playerInWater) {
|
||||
// Im Wasser: omnidirektional — gleiches Panning links/rechts, kein Richtungseffekt
|
||||
panL = panR = 0.707f;
|
||||
updateSourcePositions(); // Quellen nah am Listener halten
|
||||
} else if (playing && oceanFound) {
|
||||
float panValue = computePan(); // -1 = links, 0 = Mitte, +1 = rechts
|
||||
panL = (float) Math.sqrt((1f - panValue) / 2f);
|
||||
panR = (float) Math.sqrt((1f + panValue) / 2f);
|
||||
updateSourcePositions();
|
||||
} else {
|
||||
panL = panR = 0.707f;
|
||||
}
|
||||
|
||||
applyVolumes(calmVol, stormyVol, panL, panR);
|
||||
@@ -148,6 +150,9 @@ public class OceanSoundState extends BaseAppState {
|
||||
if (terrain.getHeightAt(px, pz) < 0f) {
|
||||
oceanInRange = true;
|
||||
oceanDist = 0f;
|
||||
if (!playing) {
|
||||
applyTarget(px, pz); // Sounds starten falls Spieler direkt ins Wasser geht
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
package de.blight.game.state;
|
||||
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.audio.AudioData;
|
||||
import com.jme3.audio.AudioNode;
|
||||
import com.jme3.math.Vector3f;
|
||||
import de.blight.common.PlacedWater;
|
||||
import de.blight.common.RiverIO;
|
||||
import de.blight.common.RiverPoint;
|
||||
import de.blight.common.WaterBodyIO;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Spielt river.ogg in der Nähe von Flüssen und Wasserflächen ab (non-positional,
|
||||
* Stereo-kompatibel). Lautstärke = ambient × 0.5 × distanceScale.
|
||||
* Liefert außerdem isInRiver() für die Wading-Erkennung.
|
||||
*/
|
||||
public class RiverSoundState extends BaseAppState {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(RiverSoundState.class);
|
||||
|
||||
private static final String AUDIO_PATH = "audio/ambient/water/river.ogg";
|
||||
private static final float MAX_DIST = 55f;
|
||||
private static final float REF_DIST = 8f;
|
||||
private static final float FADE_RATE = 1f / 3f;
|
||||
private static final float SCAN_INTERVAL = 0.25f;
|
||||
private static final float VOLUME_SCALE = 0.5f;
|
||||
/** RiverState senkt Quads 0.5m unter die Kontrollpunkte. */
|
||||
private static final float WATER_SINK = 0.5f;
|
||||
/**
|
||||
* Spieler-Y (Kapselmittelpunkt, 0.9m über Boden) darf maximal diese Distanz über
|
||||
* der Wasseroberfläche liegen und gilt noch als "im Fluss".
|
||||
* Kapsel: radius=0.4, halfCyl=0.5 → Mitte = 0.9m über Boden.
|
||||
* 1.1m = 0.9m Kapselmitte + 0.2m Buffer → Erkennung ab erstem Kontakt mit Wasser.
|
||||
*/
|
||||
private static final float WADING_Y_TOL = 1.1f;
|
||||
/** Nominale Minimaltiefe: Terrain liegt meist auf Höhe der Wasseroberfläche → echte Tiefe ≈ 0. */
|
||||
private static final float NOMINAL_RIVER_DEPTH = 0.35f;
|
||||
|
||||
private SimpleApplication app;
|
||||
private AudioNode node;
|
||||
|
||||
private final Vector3f playerPos = new Vector3f();
|
||||
|
||||
private boolean riverInRange = false;
|
||||
private float riverDist = Float.MAX_VALUE;
|
||||
private float scanTimer = 0f;
|
||||
private float vol = 0f;
|
||||
private boolean started = false;
|
||||
|
||||
/** Alle Kandidaten-Punkte für den Distanz-Scan {x, z}. */
|
||||
private final List<float[]> candidates = new ArrayList<>();
|
||||
/**
|
||||
* Fluss-Segmente für den Wading-Check.
|
||||
* Jedes Element: {ax, az, bx, bz, halfWidth, waterSurfaceY}
|
||||
* Segment-Abstand statt Punkt-Abstand → keine Lücken zwischen Kontrollpunkten.
|
||||
*/
|
||||
private final List<float[]> riverSegments = new ArrayList<>();
|
||||
/** Wasserflächen-Polygone: float[3][] = {xs, zs, {waterHeight}}. */
|
||||
private final List<float[][]> waterPolygons = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
protected void initialize(Application application) {
|
||||
app = (SimpleApplication) application;
|
||||
|
||||
try {
|
||||
node = new AudioNode(app.getAssetManager(), AUDIO_PATH, AudioData.DataType.Buffer);
|
||||
node.setLooping(true);
|
||||
node.setVolume(0f);
|
||||
node.setPositional(false);
|
||||
} catch (Exception e) {
|
||||
log.warn("[RiverSound] Audio nicht ladbar: {}", e.getMessage());
|
||||
}
|
||||
|
||||
buildData();
|
||||
log.info("[RiverSound] {} Kandidaten, {} Segmente, {} Polygone.",
|
||||
candidates.size(), riverSegments.size(), waterPolygons.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application application) {
|
||||
if (node != null) {
|
||||
node.stop();
|
||||
if (node.getParent() != null) node.removeFromParent();
|
||||
}
|
||||
}
|
||||
|
||||
@Override protected void onEnable() {}
|
||||
@Override protected void onDisable() {}
|
||||
|
||||
public void setPlayerPosition(Vector3f pos) {
|
||||
playerPos.set(pos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
if (node == null || candidates.isEmpty()) return;
|
||||
|
||||
scanTimer -= tpf;
|
||||
if (scanTimer <= 0f) {
|
||||
scanTimer = SCAN_INTERVAL;
|
||||
scanNearest();
|
||||
}
|
||||
|
||||
AudioSettingsState audioSettings = getApplication().getStateManager()
|
||||
.getState(AudioSettingsState.class);
|
||||
float scale = audioSettings != null ? audioSettings.effectiveAmbient() : 0.5f;
|
||||
|
||||
float distScale = riverInRange
|
||||
? Math.max(0f, 1f - Math.max(0f, riverDist - REF_DIST) / (MAX_DIST - REF_DIST))
|
||||
: 0f;
|
||||
|
||||
float target = scale * VOLUME_SCALE * distScale;
|
||||
vol = approach(vol, target, FADE_RATE * tpf);
|
||||
node.setVolume(vol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert die echte Wassertiefe an (x, playerY, z) in Metern.
|
||||
* Tiefe = Wasseroberfläche − Spieler-Y. 0 wenn der Spieler über dem Wasser ist.
|
||||
* Wird von WorldScene.queryWaterDepth() genutzt.
|
||||
*/
|
||||
public float getWaterDepth(float x, float playerY, float z) {
|
||||
float maxDepth = 0f;
|
||||
for (float[] seg : riverSegments) {
|
||||
if (distToSegmentSq(x, z, seg[0], seg[1], seg[2], seg[3]) > seg[4] * seg[4]) continue;
|
||||
float waterSurfY = seg[5];
|
||||
if (playerY > waterSurfY + WADING_Y_TOL) continue; // Terrain über dem Wasser
|
||||
float depth = Math.max(NOMINAL_RIVER_DEPTH, waterSurfY - playerY);
|
||||
if (depth > maxDepth) maxDepth = depth;
|
||||
}
|
||||
for (float[][] poly : waterPolygons) {
|
||||
if (!pointInPolygon(x, z, poly[0], poly[1])) continue;
|
||||
float waterH = poly[2][0];
|
||||
if (playerY > waterH + WADING_Y_TOL) continue;
|
||||
float depth = Math.max(NOMINAL_RIVER_DEPTH, waterH - playerY);
|
||||
if (depth > maxDepth) maxDepth = depth;
|
||||
}
|
||||
return maxDepth;
|
||||
}
|
||||
|
||||
// ── Scan ──────────────────────────────────────────────────────────────────
|
||||
|
||||
private void scanNearest() {
|
||||
float px = playerPos.x, pz = playerPos.z;
|
||||
float minDist = Float.MAX_VALUE;
|
||||
|
||||
for (float[] c : candidates) {
|
||||
float dx = c[0] - px, dz = c[1] - pz;
|
||||
float d = (float) Math.sqrt(dx * dx + dz * dz);
|
||||
if (d < minDist) minDist = d;
|
||||
}
|
||||
|
||||
if (minDist > MAX_DIST) {
|
||||
riverInRange = false;
|
||||
riverDist = Float.MAX_VALUE;
|
||||
} else {
|
||||
riverInRange = true;
|
||||
riverDist = minDist;
|
||||
if (!started) {
|
||||
app.getRootNode().attachChild(node);
|
||||
node.play();
|
||||
started = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Datenaufbau ───────────────────────────────────────────────────────────
|
||||
|
||||
private void buildData() {
|
||||
try {
|
||||
List<List<RiverPoint>> rivers = RiverIO.load();
|
||||
for (List<RiverPoint> river : rivers) {
|
||||
for (int i = 0; i < river.size() - 1; i++) {
|
||||
RiverPoint a = river.get(i);
|
||||
RiverPoint b = river.get(i + 1);
|
||||
float halfWidth = Math.max(a.width(), b.width()) * 0.5f + 0.3f;
|
||||
float waterSurfY = ((a.y() + b.y()) * 0.5f) - WATER_SINK;
|
||||
riverSegments.add(new float[]{ a.x(), a.z(), b.x(), b.z(), halfWidth, waterSurfY });
|
||||
// Kandidaten: jeden 2. Punkt
|
||||
if (i % 2 == 0) candidates.add(new float[]{ a.x(), a.z() });
|
||||
}
|
||||
// letzten Punkt auch als Kandidat
|
||||
if (!river.isEmpty()) {
|
||||
RiverPoint last = river.get(river.size() - 1);
|
||||
candidates.add(new float[]{ last.x(), last.z() });
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[RiverSound] Flüsse nicht ladbar: {}", e.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
List<PlacedWater> bodies = WaterBodyIO.load();
|
||||
for (PlacedWater b : bodies) {
|
||||
float[] xs = b.pointsX(), zs = b.pointsZ();
|
||||
waterPolygons.add(new float[][]{ xs, zs, { b.waterHeight() } });
|
||||
float cx = 0, cz = 0;
|
||||
for (int i = 0; i < xs.length; i++) { cx += xs[i]; cz += zs[i]; }
|
||||
candidates.add(new float[]{ cx / xs.length, cz / xs.length });
|
||||
for (int i = 0; i < xs.length; i++) candidates.add(new float[]{ xs[i], zs[i] });
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[RiverSound] Wasserflächen nicht ladbar: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Geometrie-Hilfsmethoden ───────────────────────────────────────────────
|
||||
|
||||
/** Quadrierter Abstand von Punkt (px,pz) zum Segment (ax,az)→(bx,bz). */
|
||||
private static float distToSegmentSq(float px, float pz,
|
||||
float ax, float az, float bx, float bz) {
|
||||
float dx = bx - ax, dz = bz - az;
|
||||
float len2 = dx * dx + dz * dz;
|
||||
float t = len2 < 1e-6f ? 0f
|
||||
: Math.max(0f, Math.min(1f, ((px - ax) * dx + (pz - az) * dz) / len2));
|
||||
float cx = ax + t * dx, cz = az + t * dz;
|
||||
float ex = px - cx, ez = pz - cz;
|
||||
return ex * ex + ez * ez;
|
||||
}
|
||||
|
||||
private static boolean pointInPolygon(float px, float pz, float[] xs, float[] zs) {
|
||||
boolean inside = false;
|
||||
int n = xs.length;
|
||||
for (int i = 0, j = n - 1; i < n; j = i++) {
|
||||
if (((zs[i] > pz) != (zs[j] > pz)) &&
|
||||
(px < (xs[j] - xs[i]) * (pz - zs[i]) / (zs[j] - zs[i]) + xs[i]))
|
||||
inside = !inside;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
private static float approach(float cur, float target, float maxStep) {
|
||||
float d = target - cur;
|
||||
return Math.abs(d) <= maxStep ? target : cur + Math.signum(d) * maxStep;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package de.blight.game.state;
|
||||
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.audio.AudioData;
|
||||
import com.jme3.audio.AudioNode;
|
||||
import com.jme3.math.FastMath;
|
||||
import de.blight.common.PlacedWaterfall;
|
||||
import de.blight.common.WaterfallIO;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Spielt waterfall.ogg an jedem Wasserfall ab (non-positional, Stereo-kompatibel).
|
||||
* Lautstärke = ambient × steepness × distanceScale.
|
||||
* Steilheit: 45° → 0, 90° → 1 (linear). Distanz: ref=6m, max=40m.
|
||||
*/
|
||||
public class WaterfallSoundState extends BaseAppState {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(WaterfallSoundState.class);
|
||||
|
||||
private static final String AUDIO_PATH = "audio/ambient/water/waterfall.ogg";
|
||||
private static final float REF_DIST = 6f;
|
||||
private static final float MAX_DIST = 40f;
|
||||
private static final float FADE_RATE = 1f / 2f;
|
||||
|
||||
private record WaterfallEntry(AudioNode node, float cx, float cy, float cz, float steepnessScale) {}
|
||||
|
||||
private final List<WaterfallEntry> entries = new ArrayList<>();
|
||||
private SimpleApplication app;
|
||||
|
||||
@Override
|
||||
protected void initialize(Application application) {
|
||||
app = (SimpleApplication) application;
|
||||
|
||||
List<PlacedWaterfall> waterfalls;
|
||||
try {
|
||||
waterfalls = WaterfallIO.load();
|
||||
} catch (Exception e) {
|
||||
log.error("[WaterfallSound] Wasserfälle nicht ladbar", e);
|
||||
return;
|
||||
}
|
||||
|
||||
for (PlacedWaterfall wf : waterfalls) {
|
||||
try {
|
||||
AudioNode node = new AudioNode(app.getAssetManager(), AUDIO_PATH, AudioData.DataType.Buffer);
|
||||
node.setLooping(true);
|
||||
node.setVolume(0f);
|
||||
node.setPositional(false);
|
||||
app.getRootNode().attachChild(node);
|
||||
node.play();
|
||||
|
||||
float cx = (wf.ax() + wf.bx() + wf.cx() + wf.dx()) * 0.25f;
|
||||
float cy = (wf.ay() + wf.by() + wf.cy() + wf.dy()) * 0.25f;
|
||||
float cz = (wf.az() + wf.bz() + wf.cz() + wf.dz()) * 0.25f;
|
||||
entries.add(new WaterfallEntry(node, cx, cy, cz, computeSteepnessScale(wf)));
|
||||
} catch (Exception e) {
|
||||
log.warn("[WaterfallSound] Audio nicht ladbar: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
log.info("[WaterfallSound] {} Wasserfall-Soundquellen geladen.", entries.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application application) {
|
||||
for (WaterfallEntry e : entries) {
|
||||
e.node().stop();
|
||||
e.node().removeFromParent();
|
||||
}
|
||||
entries.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
if (entries.isEmpty()) return;
|
||||
|
||||
AudioSettingsState audioSettings = getApplication().getStateManager()
|
||||
.getState(AudioSettingsState.class);
|
||||
float ambient = audioSettings != null ? audioSettings.effectiveAmbient() : 0.5f;
|
||||
|
||||
com.jme3.math.Vector3f listenerPos = app.getListener().getLocation();
|
||||
float lx = listenerPos.x, ly = listenerPos.y, lz = listenerPos.z;
|
||||
|
||||
for (WaterfallEntry e : entries) {
|
||||
float dx = e.cx() - lx, dy = e.cy() - ly, dz = e.cz() - lz;
|
||||
float dist = FastMath.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
float distScale = dist <= REF_DIST ? 1f
|
||||
: dist >= MAX_DIST ? 0f
|
||||
: 1f - (dist - REF_DIST) / (MAX_DIST - REF_DIST);
|
||||
|
||||
float target = ambient * e.steepnessScale() * distScale * 1.25f;
|
||||
float next = approach(e.node().getVolume(), target, FADE_RATE * tpf);
|
||||
e.node().setVolume(next);
|
||||
}
|
||||
}
|
||||
|
||||
@Override protected void onEnable() {}
|
||||
@Override protected void onDisable() {}
|
||||
|
||||
private static float computeSteepnessScale(PlacedWaterfall wf) {
|
||||
float topX = (wf.ax() + wf.bx()) * 0.5f, topY = (wf.ay() + wf.by()) * 0.5f, topZ = (wf.az() + wf.bz()) * 0.5f;
|
||||
float botX = (wf.cx() + wf.dx()) * 0.5f, botY = (wf.cy() + wf.dy()) * 0.5f, botZ = (wf.cz() + wf.dz()) * 0.5f;
|
||||
float horiz = FastMath.sqrt((botX - topX) * (botX - topX) + (botZ - topZ) * (botZ - topZ));
|
||||
float vert = FastMath.abs(botY - topY);
|
||||
float angleDeg = FastMath.RAD_TO_DEG * FastMath.atan2(vert, horiz);
|
||||
return FastMath.clamp((angleDeg - 45f) / 45f, 0f, 1f);
|
||||
}
|
||||
|
||||
private static float approach(float cur, float target, float maxStep) {
|
||||
float d = target - cur;
|
||||
return FastMath.abs(d) <= maxStep ? target : cur + FastMath.sign(d) * maxStep;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user