From ca02e427e81912ea560ef7f193e47c2cd43c187e Mon Sep 17 00:00:00 2001 From: Isaac Lichter Date: Fri, 5 Mar 2021 17:22:38 -0500 Subject: [PATCH 1/7] feat: added entity support to sound manager --- .../assets/sound/OggSoundManager.java | 100 ++++++++++++++++-- .../destinationsol/game/sound/DebugHint.java | 17 ++- .../game/sound/DebugHintDrawer.java | 33 ++++-- 3 files changed, 132 insertions(+), 18 deletions(-) diff --git a/engine/src/main/java/org/destinationsol/assets/sound/OggSoundManager.java b/engine/src/main/java/org/destinationsol/assets/sound/OggSoundManager.java index 48969842d..22fbc91bf 100644 --- a/engine/src/main/java/org/destinationsol/assets/sound/OggSoundManager.java +++ b/engine/src/main/java/org/destinationsol/assets/sound/OggSoundManager.java @@ -20,6 +20,7 @@ import org.destinationsol.Const; import org.destinationsol.SolApplication; import org.destinationsol.assets.Assets; +import org.destinationsol.common.NotNull; import org.destinationsol.common.Nullable; import org.destinationsol.common.SolMath; import org.destinationsol.common.SolRandom; @@ -32,6 +33,7 @@ import org.destinationsol.game.context.Context; import org.destinationsol.game.planet.Planet; import org.destinationsol.game.sound.DebugHintDrawer; +import org.terasology.gestalt.entitysystem.entity.EntityRef; import java.util.HashMap; import java.util.Map; @@ -68,7 +70,14 @@ public class OggSoundManager implements UpdateAwareSystem { * {@code SolObject} is the object the sound belongs to, inner map's {@code OggSound} is the sound in question, * {@code Float} is an absolute time the sound will stop playing. (Absolute as in not relative to the current time) */ - private final Map> loopedSoundMap; + private final Map> loopedSoundMapOfSolObjects; + /** + * A container for working with looping sounds. Looped sounds are stored here per-entity, and this map is every so often + * cleared, on the basis provided by calling each entity's {@link EntityRef#exists()} method. + * {@code EntityRef} is the object the sound belongs to, inner map's {@code OggSound} is the sound in question, + * {@code Float} is an absolute time the sound will stop playing. (Absolute as in not relative to the current time) + */ + private final Map> loopedSoundMapOfEntities; /** * Used for drawing debug hints when {@link DebugOptions#SOUND_INFO} flag is set. See * {@link #drawDebug(GameDrawer, SolGame)} for more info. @@ -86,7 +95,8 @@ public class OggSoundManager implements UpdateAwareSystem { public OggSoundManager(Context context) { soundMap = new HashMap<>(); - loopedSoundMap = new HashMap<>(); + loopedSoundMapOfSolObjects = new HashMap<>(); + loopedSoundMapOfEntities = new HashMap<>(); debugHintDrawer = new DebugHintDrawer(); solApplication = context.get(SolApplication.class); @@ -188,6 +198,47 @@ public void play(SolGame game, PlayableSound playableSound, @Nullable Vector2 po gdxSound.play(volume, pitch, 0); } + /** + * Plays a sound at a particular position. If the sound has an associated loop, this will loop the sound, coming + * from the entity. + * + * @param game Game to play the sound in + * @param playableSound The sound to play + * @param position Position to play the sound at + * @param soundSource Bearer of a sound. Must not be null for looped sounds. + * @param volumeMultiplier Multiplier for sound volume + */ + public void play(SolGame game, PlayableSound playableSound, @NotNull Vector2 position, @NotNull EntityRef soundSource, float volumeMultiplier) { + + if (playableSound == null) { + return; + } + if (soundSource == null || position == null) { + throw new AssertionError("Position and source must be non-null"); + } + + OggSound sound = playableSound.getOggSound(); + + float volume = getVolume(game, position, volumeMultiplier, sound); + if (volume <= 0) { + return; + } + + // Calculate the pitch for the sound + float pitch = SolRandom.randomFloat(.97f, 1.03f) * game.getTimeFactor() * playableSound.getBasePitch(); + + if (skipLooped(soundSource, sound, game.getTime())) { + return; + } + + if (DebugOptions.SOUND_INFO) { + debugHintDrawer.add(soundSource, position, sound.toString()); + } + + Sound gdxSound = sound.getSound(); + gdxSound.play(volume, pitch, 0); + } + /** * Calculates the volume a sound should be played at. * This method takes several factors in account, more exactly: global game's volume, spreading of sound in vacuum @@ -242,10 +293,43 @@ private boolean skipLooped(SolObject source, OggSound sound, float time) { return false; } - Map looped = loopedSoundMap.get(source); + Map looped = loopedSoundMapOfSolObjects.get(source); + if (looped == null) { + looped = new HashMap<>(); + loopedSoundMapOfSolObjects.put(source, looped); + return false; + } else { + Float endTime = looped.get(sound); + if (endTime == null || endTime <= time) { + looped.put(sound, time + sound.getLoopTime()); // argh, performance loss + return false; + } else { + return true; + } + } + } + + /** + * Returns true when sound should not be played because of loop, false otherwise. + *

+ * Sound should not be played when its {@code loopTime > 0} and {@code loopTime} time units have not yet passed + * since it was last played on the object. + * TODO: now handles even adding the sound to the list of looping sounds. Possibly extract that? + * + * @param source Object playing this sound. + * @param sound Sound to be played. + * @param time Game's current time. + * @return true when sound should not be played because of loop, false otherwise. + */ + private boolean skipLooped(EntityRef source, OggSound sound, float time) { + if (sound.getLoopTime() == 0) { + return false; + } + + Map looped = loopedSoundMapOfEntities.get(source); if (looped == null) { looped = new HashMap<>(); - loopedSoundMap.put(source, looped); + loopedSoundMapOfEntities .put(source, looped); return false; } else { Float endTime = looped.get(sound); @@ -289,14 +373,16 @@ public void update(SolGame game, float timeStep) { } /** - * Iterates {@link #loopedSoundMap} and removes any entries that are no longer in the game. + * Iterates {@link #loopedSoundMapOfSolObjects} and {@link #loopedSoundMapOfEntities} and removes any entries that + * are no longer in the game. *

- * (See {@link SolObject#shouldBeRemoved(SolGame)}) + * (See {@link SolObject#shouldBeRemoved(SolGame)} and {@link EntityRef#exists()}) * * @param game Game currently in progress. */ private void cleanLooped(SolGame game) { - loopedSoundMap.keySet().removeIf(o -> o.shouldBeRemoved(game)); + loopedSoundMapOfSolObjects.keySet().removeIf(o -> o.shouldBeRemoved(game)); + loopedSoundMapOfEntities.keySet().removeIf(entity -> !entity.exists()); } /** diff --git a/engine/src/main/java/org/destinationsol/game/sound/DebugHint.java b/engine/src/main/java/org/destinationsol/game/sound/DebugHint.java index fa9032948..4541acdff 100644 --- a/engine/src/main/java/org/destinationsol/game/sound/DebugHint.java +++ b/engine/src/main/java/org/destinationsol/game/sound/DebugHint.java @@ -21,6 +21,8 @@ import org.destinationsol.game.GameDrawer; import org.destinationsol.game.SolGame; import org.destinationsol.game.SolObject; +import org.destinationsol.location.components.Position; +import org.terasology.gestalt.entitysystem.entity.EntityRef; import java.util.HashMap; import java.util.Iterator; @@ -34,8 +36,11 @@ public class DebugHint { private SolObject myOwner; private String myMsg; - public DebugHint(SolObject owner, Vector2 position) { + private EntityRef entity; + + public DebugHint(SolObject owner, EntityRef entity, Vector2 position) { myOwner = owner; + this.entity = entity; this.position = new Vector2(position); myMsgs = new HashMap<>(); } @@ -65,6 +70,16 @@ public void update(SolGame game) { } } + if (entity != null) { + if (!entity.exists()) { + entity = null; + } else { + entity.getComponent(Position.class).ifPresent(entityPosition -> { + position.set(entityPosition.position); + }); + } + } + long now = TimeUtils.millis(); boolean needsRebuild = false; Iterator> it = myMsgs.entrySet().iterator(); diff --git a/engine/src/main/java/org/destinationsol/game/sound/DebugHintDrawer.java b/engine/src/main/java/org/destinationsol/game/sound/DebugHintDrawer.java index 4a1958ee7..150363852 100644 --- a/engine/src/main/java/org/destinationsol/game/sound/DebugHintDrawer.java +++ b/engine/src/main/java/org/destinationsol/game/sound/DebugHintDrawer.java @@ -20,33 +20,43 @@ import org.destinationsol.game.GameDrawer; import org.destinationsol.game.SolGame; import org.destinationsol.game.SolObject; +import org.terasology.gestalt.entitysystem.entity.EntityRef; import java.util.HashMap; import java.util.Iterator; import java.util.Map; public class DebugHintDrawer { - private final Map myTracedNotes; - private final Map myFreeNotes; + private final Map tracedSolObjectNotes; + private final Map freeNotes; + private final Map tracedEntityNotes; public DebugHintDrawer() { - myTracedNotes = new HashMap<>(); - myFreeNotes = new HashMap<>(); + tracedSolObjectNotes = new HashMap<>(); + freeNotes = new HashMap<>(); + tracedEntityNotes = new HashMap<>(); } public void add(@Nullable SolObject owner, Vector2 position, String value) { DebugHint dh; if (owner == null) { - dh = myFreeNotes.computeIfAbsent(position, p -> new DebugHint(null, p)); + dh = freeNotes.computeIfAbsent(position, p -> new DebugHint(null, null, p)); } else { - dh = myTracedNotes.computeIfAbsent(owner, o -> new DebugHint(o, o.getPosition())); + dh = tracedSolObjectNotes.computeIfAbsent(owner, o -> new DebugHint(o, null, o.getPosition())); } dh.add(value); } + public void add(EntityRef entity, Vector2 position, String value) { + DebugHint debugHint; + debugHint = tracedEntityNotes.computeIfAbsent(entity, entityRef -> new DebugHint(null, entityRef, position)); + debugHint.add(value); + } + public void update(SolGame game) { - updateEach(game, myTracedNotes.values().iterator()); - updateEach(game, myFreeNotes.values().iterator()); + updateEach(game, tracedSolObjectNotes.values().iterator()); + updateEach(game, freeNotes.values().iterator()); + updateEach(game, tracedEntityNotes.values().iterator()); } private void updateEach(SolGame game, Iterator it) { @@ -60,10 +70,13 @@ private void updateEach(SolGame game, Iterator it) { } public void draw(GameDrawer drawer, SolGame game) { - for (DebugHint n : myTracedNotes.values()) { + for (DebugHint n : tracedSolObjectNotes.values()) { + n.draw(drawer, game); + } + for (DebugHint n : freeNotes.values()) { n.draw(drawer, game); } - for (DebugHint n : myFreeNotes.values()) { + for (DebugHint n : tracedEntityNotes.values()) { n.draw(drawer, game); } } From dd523cae4f44dbc6ca7fd67dfc3aa676a86b0e09 Mon Sep 17 00:00:00 2001 From: Isaac Lichter Date: Fri, 5 Mar 2021 17:29:13 -0500 Subject: [PATCH 2/7] feat: ECS-based sound playing system --- .../sound/events/SoundEvent.java | 33 +++++++++++++ .../sound/systems/SoundPlayingSystem.java | 49 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 engine/src/main/java/org/destinationsol/sound/events/SoundEvent.java create mode 100644 engine/src/main/java/org/destinationsol/sound/systems/SoundPlayingSystem.java diff --git a/engine/src/main/java/org/destinationsol/sound/events/SoundEvent.java b/engine/src/main/java/org/destinationsol/sound/events/SoundEvent.java new file mode 100644 index 000000000..ef7583b37 --- /dev/null +++ b/engine/src/main/java/org/destinationsol/sound/events/SoundEvent.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020 The Terasology Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.destinationsol.sound.events; + +import org.destinationsol.assets.sound.PlayableSound; +import org.terasology.gestalt.entitysystem.event.Event; + +/** + * Plays a sound emitting from an entity. + */ +public class SoundEvent implements Event { + + public final PlayableSound playableSound; + public final int volumeMultplier; + + public SoundEvent(PlayableSound playableSound, int volumeMultplier) { + this.playableSound = playableSound; + this.volumeMultplier = volumeMultplier; + } +} diff --git a/engine/src/main/java/org/destinationsol/sound/systems/SoundPlayingSystem.java b/engine/src/main/java/org/destinationsol/sound/systems/SoundPlayingSystem.java new file mode 100644 index 000000000..9ab205047 --- /dev/null +++ b/engine/src/main/java/org/destinationsol/sound/systems/SoundPlayingSystem.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020 The Terasology Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.destinationsol.sound.systems; + +import com.badlogic.gdx.math.Vector2; +import org.destinationsol.assets.sound.OggSoundManager; +import org.destinationsol.common.In; +import org.destinationsol.entitysystem.EventReceiver; +import org.destinationsol.game.SolGame; +import org.destinationsol.location.components.Position; +import org.destinationsol.sound.events.SoundEvent; +import org.terasology.gestalt.entitysystem.entity.EntityRef; +import org.terasology.gestalt.entitysystem.event.EventResult; +import org.terasology.gestalt.entitysystem.event.ReceiveEvent; + +/** + * This system plays sounds emitting from entities with a {@link Position} component, using the {@link OggSoundManager}. + */ +public class SoundPlayingSystem implements EventReceiver { + + @In + private SolGame game; + + @In + private OggSoundManager soundManager; + + /** + * Plays a given sound emitting from an entity, at that entity's {@link Position}. + */ + @ReceiveEvent(components = Position.class) + public EventResult playSound(SoundEvent event, EntityRef entity) { + Vector2 position = entity.getComponent(Position.class).get().position; + soundManager.play(game, event.playableSound, null, entity, event.volumeMultplier); + return EventResult.CONTINUE; + } +} From 55ffcc6a58e35a35cf90cd8b45503710b5a1f9ff Mon Sep 17 00:00:00 2001 From: Isaac Lichter Date: Mon, 8 Mar 2021 21:39:18 -0500 Subject: [PATCH 3/7] feat: implemented ECS collision/damage sounds --- .../org/destinationsol/SolApplication.java | 11 +++- .../assets/sound/OggSoundManager.java | 18 +++++- .../assets/sound/SpecialSounds.java | 45 ++++++++++++++ .../systems/AsteroidSoundSystem.java | 59 +++++++++++++++++++ .../java/org/destinationsol/game/SolGame.java | 2 + .../game/projectile/Projectile.java | 4 +- .../health/events/DamageEvent.java | 11 ++++ .../destinationsol/material/MaterialType.java | 23 ++++++++ .../material/components/Material.java | 32 ++++++++++ .../sound/events/SoundEvent.java | 4 +- .../sound/systems/SoundPlayingSystem.java | 48 ++++++++++++++- 11 files changed, 248 insertions(+), 9 deletions(-) create mode 100644 engine/src/main/java/org/destinationsol/asteroids/systems/AsteroidSoundSystem.java create mode 100644 engine/src/main/java/org/destinationsol/material/MaterialType.java create mode 100644 engine/src/main/java/org/destinationsol/material/components/Material.java diff --git a/engine/src/main/java/org/destinationsol/SolApplication.java b/engine/src/main/java/org/destinationsol/SolApplication.java index a33d65c22..96fb1e005 100644 --- a/engine/src/main/java/org/destinationsol/SolApplication.java +++ b/engine/src/main/java/org/destinationsol/SolApplication.java @@ -41,6 +41,8 @@ import org.destinationsol.health.components.Health; import org.destinationsol.location.components.Angle; import org.destinationsol.location.components.Velocity; +import org.destinationsol.material.MaterialType; +import org.destinationsol.material.components.Material; import org.destinationsol.moneyDropping.components.DropsMoneyOnDestruction; import org.destinationsol.rendering.RenderableElement; import org.destinationsol.rendering.components.Renderable; @@ -262,7 +264,7 @@ private void draw() { if (!entityCreated) { Size size = new Size(); - size.size = 2; + size.size = 1; RenderableElement element = new RenderableElement(); element.texture = SolRandom.randomElement(Assets.listTexturesMatching("engine:asteroid_.*")); @@ -276,13 +278,16 @@ private void draw() { Position position = new Position(); position.position = solGame.getHero().getShip().getPosition().cpy(); - position.position.y += 3; + position.position.y += 1; Health health = new Health(); health.currentHealth = 1; + Material material = new Material(); + material.materialType = MaterialType.ROCK; EntityRef entityRef = entitySystemManager.getEntityManager().createEntity(graphicsComponent, position, size, - new Angle(), new Velocity(), new AsteroidMesh(), health, new DropsMoneyOnDestruction(), new CreatesRubbleOnDestruction()); + new Angle(), new Velocity(), new AsteroidMesh(), health, new DropsMoneyOnDestruction(), + new CreatesRubbleOnDestruction(), material); entityRef.setComponent(new BodyLinked()); entityCreated = true; diff --git a/engine/src/main/java/org/destinationsol/assets/sound/OggSoundManager.java b/engine/src/main/java/org/destinationsol/assets/sound/OggSoundManager.java index 22fbc91bf..06942cddb 100644 --- a/engine/src/main/java/org/destinationsol/assets/sound/OggSoundManager.java +++ b/engine/src/main/java/org/destinationsol/assets/sound/OggSoundManager.java @@ -198,6 +198,22 @@ public void play(SolGame game, PlayableSound playableSound, @Nullable Vector2 po gdxSound.play(volume, pitch, 0); } + /** + * Plays a sound at a particular position. If the sound has an associated loop, this will loop the sound, coming + * from the entity. + *

+ * {@code source} must not be null if the sound is specified to loop, and at least one of {@code source} or + * {@code position} must be specified. + * + * @param game Game to play the sound in + * @param playableSound The sound to play + * @param position Position to play the sound at + * @param soundSource Bearer of a sound. Must not be null for looped sounds. + */ + public void play(SolGame game, PlayableSound playableSound, @NotNull Vector2 position, @NotNull EntityRef soundSource) { + play(game, playableSound, position, soundSource, 1f); + } + /** * Plays a sound at a particular position. If the sound has an associated loop, this will loop the sound, coming * from the entity. @@ -329,7 +345,7 @@ private boolean skipLooped(EntityRef source, OggSound sound, float time) { Map looped = loopedSoundMapOfEntities.get(source); if (looped == null) { looped = new HashMap<>(); - loopedSoundMapOfEntities .put(source, looped); + loopedSoundMapOfEntities.put(source, looped); return false; } else { Float endTime = looped.get(sound); diff --git a/engine/src/main/java/org/destinationsol/assets/sound/SpecialSounds.java b/engine/src/main/java/org/destinationsol/assets/sound/SpecialSounds.java index c9638d5d1..f06ef5204 100644 --- a/engine/src/main/java/org/destinationsol/assets/sound/SpecialSounds.java +++ b/engine/src/main/java/org/destinationsol/assets/sound/SpecialSounds.java @@ -20,6 +20,7 @@ import org.destinationsol.game.DmgType; import org.destinationsol.game.SolGame; import org.destinationsol.game.SolObject; +import org.destinationsol.material.MaterialType; import java.util.Arrays; @@ -106,4 +107,48 @@ public void playColl(SolGame game, float absImpulse, SolObject o, Vector2 positi } game.getSoundManager().play(game, metal ? metalColl : rockColl, position, o, absImpulse * Const.IMPULSE_TO_COLL_VOL); } + + /** + * Gets the damage sound associated with the given {@link MaterialType} and {@link DmgType}. If no sound is defined, + * null is returned. + * + * @param materialType the material type of the damaged entity + * @param damageType the type of damage done + * @return the sound of the damage + */ + public PlayableSound getHitSound(MaterialType materialType, DmgType damageType) { + if (damageType == DmgType.ENERGY) { + if (materialType == MaterialType.METAL) { + return metalEnergyHit; + } + if (materialType == MaterialType.ROCK) { + return rockEnergyHit; + } + } + if (damageType == DmgType.BULLET) { + if (materialType == MaterialType.METAL) { + return metalBulletHit; + } + if (materialType == MaterialType.ROCK) { + return rockBulletHit; + } + } + return null; + } + + /** + * Gets the collision sound associated with the given {@link MaterialType}. If no sound is defined, null is returned. + * + * @param materialType the material type of the entity + * @return the sound of the collision + */ + public PlayableSound getCollisionSound(MaterialType materialType) { + if (materialType == MaterialType.METAL) { + return metalColl; + } + if (materialType == MaterialType.ROCK) { + return rockColl; + } + return null; + } } diff --git a/engine/src/main/java/org/destinationsol/asteroids/systems/AsteroidSoundSystem.java b/engine/src/main/java/org/destinationsol/asteroids/systems/AsteroidSoundSystem.java new file mode 100644 index 000000000..024afe393 --- /dev/null +++ b/engine/src/main/java/org/destinationsol/asteroids/systems/AsteroidSoundSystem.java @@ -0,0 +1,59 @@ +/* + * Copyright 2020 The Terasology Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.destinationsol.asteroids.systems; + +import org.destinationsol.assets.sound.SpecialSounds; +import org.destinationsol.asteroids.components.AsteroidMesh; +import org.destinationsol.common.In; +import org.destinationsol.common.SolMath; +import org.destinationsol.entitysystem.EntitySystemManager; +import org.destinationsol.entitysystem.EventReceiver; +import org.destinationsol.location.components.Position; +import org.destinationsol.removal.events.DeletionEvent; +import org.destinationsol.removal.systems.DestructionSystem; +import org.destinationsol.size.components.Size; +import org.destinationsol.sound.events.SoundEvent; +import org.terasology.gestalt.entitysystem.entity.EntityRef; +import org.terasology.gestalt.entitysystem.event.Before; +import org.terasology.gestalt.entitysystem.event.EventResult; +import org.terasology.gestalt.entitysystem.event.ReceiveEvent; + +/** + * This system plays asteroid-specific sounds. + */ +public class AsteroidSoundSystem implements EventReceiver { + + @In + private EntitySystemManager entitySystemManager; + + @In + private SpecialSounds specialSounds; + + /** + * When an asteroid is destroyed, this plays the asteroid destruction sound. + */ + @ReceiveEvent(components = {AsteroidMesh.class, Position.class}) + @Before(DestructionSystem.class) + public EventResult playDeathSound(DeletionEvent event, EntityRef entity) { + float volumeMultiplier = 1; + if (entity.hasComponent(Size.class)) { + float size = entity.getComponent(Size.class).get().size; + volumeMultiplier = SolMath.clamp(size / .5f); + } + entitySystemManager.sendEvent(new SoundEvent(specialSounds.asteroidCrack, volumeMultiplier), entity); + return EventResult.CONTINUE; + } +} diff --git a/engine/src/main/java/org/destinationsol/game/SolGame.java b/engine/src/main/java/org/destinationsol/game/SolGame.java index 153483357..ef7626453 100644 --- a/engine/src/main/java/org/destinationsol/game/SolGame.java +++ b/engine/src/main/java/org/destinationsol/game/SolGame.java @@ -124,7 +124,9 @@ public SolGame(String shipName, boolean isTutorial, boolean isNewGame, CommonDra context.put(GameDrawer.class, drawer); gameColors = new GameColors(); soundManager = solApplication.getSoundManager(); + context.put(OggSoundManager.class, soundManager); specialSounds = new SpecialSounds(soundManager); + context.put(SpecialSounds.class, specialSounds); drawableManager = new DrawableManager(drawer); camera = new SolCam(); gameScreens = new GameScreens(solApplication, context); diff --git a/engine/src/main/java/org/destinationsol/game/projectile/Projectile.java b/engine/src/main/java/org/destinationsol/game/projectile/Projectile.java index d855798a5..185563203 100644 --- a/engine/src/main/java/org/destinationsol/game/projectile/Projectile.java +++ b/engine/src/main/java/org/destinationsol/game/projectile/Projectile.java @@ -132,12 +132,12 @@ public void update(SolGame game) { while (iterator.next()) { Vector2 entityPosition = iterator.getEntity().getComponent(Position.class).get().position; if (getPosition().dst2(entityPosition) <= config.aoeRadius) { - game.getEntitySystemManager().sendEvent(new DamageEvent(config.dmg), entity); + game.getEntitySystemManager().sendEvent(new DamageEvent(config.dmg, config.dmgType), entity); } } } else { - game.getEntitySystemManager().sendEvent(new DamageEvent(config.dmg), entity); + game.getEntitySystemManager().sendEvent(new DamageEvent(config.dmg, config.dmgType), entity); } } diff --git a/engine/src/main/java/org/destinationsol/health/events/DamageEvent.java b/engine/src/main/java/org/destinationsol/health/events/DamageEvent.java index c46719829..ae5ed57ae 100644 --- a/engine/src/main/java/org/destinationsol/health/events/DamageEvent.java +++ b/engine/src/main/java/org/destinationsol/health/events/DamageEvent.java @@ -15,6 +15,7 @@ */ package org.destinationsol.health.events; +import org.destinationsol.game.DmgType; import org.terasology.gestalt.entitysystem.event.Event; /** @@ -23,12 +24,22 @@ public class DamageEvent implements Event { private float damage; + private DmgType damageType; public DamageEvent(float damage) { this.damage = damage; } + public DamageEvent(float damage, DmgType damageType) { + this.damage = damage; + this.damageType = damageType; + } + public float getDamage() { return damage; } + + public DmgType getDamageType() { + return damageType; + } } diff --git a/engine/src/main/java/org/destinationsol/material/MaterialType.java b/engine/src/main/java/org/destinationsol/material/MaterialType.java new file mode 100644 index 000000000..adae0b62b --- /dev/null +++ b/engine/src/main/java/org/destinationsol/material/MaterialType.java @@ -0,0 +1,23 @@ +/* + * Copyright 2020 The Terasology Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.destinationsol.material; + +/** + * The types of materials that an entity can be composed of. + */ +public enum MaterialType { + METAL, ROCK +} diff --git a/engine/src/main/java/org/destinationsol/material/components/Material.java b/engine/src/main/java/org/destinationsol/material/components/Material.java new file mode 100644 index 000000000..61f664ab3 --- /dev/null +++ b/engine/src/main/java/org/destinationsol/material/components/Material.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020 The Terasology Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.destinationsol.material.components; + +import org.destinationsol.material.MaterialType; +import org.terasology.gestalt.entitysystem.component.Component; + +/** + * Indicates what type of material the entity is made of. + */ +public class Material implements Component { + + public MaterialType materialType; + + @Override + public void copy(Material other) { + this.materialType = other.materialType; + } +} diff --git a/engine/src/main/java/org/destinationsol/sound/events/SoundEvent.java b/engine/src/main/java/org/destinationsol/sound/events/SoundEvent.java index ef7583b37..f429f8427 100644 --- a/engine/src/main/java/org/destinationsol/sound/events/SoundEvent.java +++ b/engine/src/main/java/org/destinationsol/sound/events/SoundEvent.java @@ -24,9 +24,9 @@ public class SoundEvent implements Event { public final PlayableSound playableSound; - public final int volumeMultplier; + public final float volumeMultplier; - public SoundEvent(PlayableSound playableSound, int volumeMultplier) { + public SoundEvent(PlayableSound playableSound, float volumeMultplier) { this.playableSound = playableSound; this.volumeMultplier = volumeMultplier; } diff --git a/engine/src/main/java/org/destinationsol/sound/systems/SoundPlayingSystem.java b/engine/src/main/java/org/destinationsol/sound/systems/SoundPlayingSystem.java index 9ab205047..44c1cfb59 100644 --- a/engine/src/main/java/org/destinationsol/sound/systems/SoundPlayingSystem.java +++ b/engine/src/main/java/org/destinationsol/sound/systems/SoundPlayingSystem.java @@ -16,11 +16,19 @@ package org.destinationsol.sound.systems; import com.badlogic.gdx.math.Vector2; +import org.destinationsol.Const; import org.destinationsol.assets.sound.OggSoundManager; +import org.destinationsol.assets.sound.PlayableSound; +import org.destinationsol.assets.sound.SpecialSounds; import org.destinationsol.common.In; import org.destinationsol.entitysystem.EventReceiver; +import org.destinationsol.force.events.ImpulseEvent; +import org.destinationsol.game.DmgType; import org.destinationsol.game.SolGame; +import org.destinationsol.health.events.DamageEvent; import org.destinationsol.location.components.Position; +import org.destinationsol.material.MaterialType; +import org.destinationsol.material.components.Material; import org.destinationsol.sound.events.SoundEvent; import org.terasology.gestalt.entitysystem.entity.EntityRef; import org.terasology.gestalt.entitysystem.event.EventResult; @@ -37,13 +45,51 @@ public class SoundPlayingSystem implements EventReceiver { @In private OggSoundManager soundManager; + @In + private SpecialSounds specialSounds; + /** * Plays a given sound emitting from an entity, at that entity's {@link Position}. */ @ReceiveEvent(components = Position.class) public EventResult playSound(SoundEvent event, EntityRef entity) { Vector2 position = entity.getComponent(Position.class).get().position; - soundManager.play(game, event.playableSound, null, entity, event.volumeMultplier); + soundManager.play(game, event.playableSound, position, entity, event.volumeMultplier); + return EventResult.CONTINUE; + } + + /** + * When an entity takes damage, this plays a sound based on the type of damage taken and the type of material that + * the entity is. No sound will be played if there is no defined sound for the {@link DmgType}/{@link MaterialType} + * combination, or if either the {@link DmgType} or {@link MaterialType} is null. + */ + @ReceiveEvent(components = {Position.class, Material.class}) + public EventResult playDamageSound(DamageEvent event, EntityRef entity) { + MaterialType materialType = entity.getComponent(Material.class).get().materialType; + PlayableSound sound = specialSounds.getHitSound(materialType, event.getDamageType()); + if (sound != null) { + Vector2 position = entity.getComponent(Position.class).get().position; + soundManager.play(game, sound, position, entity); + } + return EventResult.CONTINUE; + } + + /** + * When an entity experiences a collision, this plays a sound based on the type of material that the entity is. No + * sound will be played if there is no defined collision sound for the {@link MaterialType}, or if the + * {@link MaterialType} is null. + */ + @ReceiveEvent(components = {Position.class, Material.class}) + public EventResult playCollisionSound(ImpulseEvent event, EntityRef entity) { + float magnitude = event.getMagnitude(); + if (magnitude >= .1f) { + Vector2 position = entity.getComponent(Position.class).get().position; + MaterialType materialType = entity.getComponent(Material.class).get().materialType; + PlayableSound collisionSound = specialSounds.getCollisionSound(materialType); + if (collisionSound != null) { + soundManager.play(game, collisionSound, position, entity, magnitude * Const.IMPULSE_TO_COLL_VOL); + } + } return EventResult.CONTINUE; } } From 163bf1b9b5f56b86cfa4727b2121a7a828a170aa Mon Sep 17 00:00:00 2001 From: Isaac Lichter Date: Mon, 8 Mar 2021 23:26:22 -0500 Subject: [PATCH 4/7] fix: corrected method signature --- .../java/org/destinationsol/game/sound/DebugHintDrawer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/src/main/java/org/destinationsol/game/sound/DebugHintDrawer.java b/engine/src/main/java/org/destinationsol/game/sound/DebugHintDrawer.java index 40570ed11..8521f8801 100644 --- a/engine/src/main/java/org/destinationsol/game/sound/DebugHintDrawer.java +++ b/engine/src/main/java/org/destinationsol/game/sound/DebugHintDrawer.java @@ -70,7 +70,7 @@ private void updateEach(SolGame game, Iterator it) { } } - public void draw(GameDrawer drawer, SolGame solCam) { + public void draw(GameDrawer drawer, SolCam solCam) { for (DebugHint n : tracedSolObjectNotes.values()) { n.draw(drawer, solCam); } From c90790ffdd1e275f8084883a4ab65a36587f4eb5 Mon Sep 17 00:00:00 2001 From: Isaac Lichter Date: Tue, 9 Mar 2021 11:34:38 -0500 Subject: [PATCH 5/7] fix: removed duplicate classes --- .../game/sound/OggSoundManager.java | 199 ------------------ .../game/sound/OggSoundSet.java | 54 ----- .../game/sound/SpecialSounds.java | 110 ---------- 3 files changed, 363 deletions(-) delete mode 100644 engine/src/main/java/org/destinationsol/game/sound/OggSoundManager.java delete mode 100644 engine/src/main/java/org/destinationsol/game/sound/OggSoundSet.java delete mode 100644 engine/src/main/java/org/destinationsol/game/sound/SpecialSounds.java diff --git a/engine/src/main/java/org/destinationsol/game/sound/OggSoundManager.java b/engine/src/main/java/org/destinationsol/game/sound/OggSoundManager.java deleted file mode 100644 index 2034c0d1a..000000000 --- a/engine/src/main/java/org/destinationsol/game/sound/OggSoundManager.java +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Copyright 2016 MovingBlocks - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.destinationsol.game.sound; - -import com.badlogic.gdx.audio.Sound; -import com.badlogic.gdx.math.Vector2; -import org.destinationsol.Const; -import org.destinationsol.assets.Assets; -import org.destinationsol.assets.sound.OggSound; -import org.destinationsol.assets.sound.PlayableSound; -import org.destinationsol.common.Nullable; -import org.destinationsol.common.SolMath; -import org.destinationsol.common.SolRandom; -import org.destinationsol.game.DebugOptions; -import org.destinationsol.game.GameDrawer; -import org.destinationsol.game.Hero; -import org.destinationsol.game.SolCam; -import org.destinationsol.game.SolGame; -import org.destinationsol.game.SolObject; -import org.destinationsol.game.planet.Planet; - -import java.util.HashMap; -import java.util.Map; - -public class OggSoundManager { - // private static Logger logger = LoggerFactory.getLogger(OggSoundManager.class); - private final Map soundMap; - private final Map> loopedSoundMap; - private final DebugHintDrawer debugHintDrawer; - - private float myLoopAwait; - - public OggSoundManager() { - this.soundMap = new HashMap<>(); - this.loopedSoundMap = new HashMap<>(); - this.debugHintDrawer = new DebugHintDrawer(); - } - - public OggSound getSound(String path) { - return getSound(path, 1.0f); - } - - public OggSound getSound(String path, float basePitch) { - if (soundMap.containsKey(path)) { - return soundMap.get(path); - } - - OggSound sound = Assets.getSound(path); - sound.setBasePitch(basePitch); - soundMap.put(path, sound); - return sound; - } - - /** - * Plays a sound. Source must not be null. - * - * @param position position of a sound. If null, source.getPosition() will be used - * @param source bearer of a sound. Must not be null for looped sounds - * @param volumeMultiplier multiplier for sound volume - */ - public void play(SolGame game, PlayableSound playableSound, @Nullable Vector2 position, @Nullable SolObject source, float volumeMultiplier) { - if (playableSound == null) { - return; - } - - OggSound sound = playableSound.getOggSound(); - // logger.debug("Playing sound: {}", sound.getUrn().toString()); - - // Perform some initial argument validation - if (source == null && position == null) { - throw new AssertionError("Either position or source must be non-null"); - } - if (source == null && sound.getLoopTime() > 0) { - throw new AssertionError("Attempted to loop a sound without a parent object: " + sound.getUrn()); - } - if (position == null) { - position = source.getPosition(); - } - - // Calculate the volume multiplier for the sound - float globalVolumeMultiplier = game.getSolApplication().getOptions().sfxVolume.getVolume(); - if (globalVolumeMultiplier == 0) { - return; - } - - Vector2 cameraPosition = game.getCam().getPosition(); - Planet nearestPlanet = game.getPlanetManager().getNearestPlanet(); - - float airPercentage = 0; - if (nearestPlanet.getConfig().skyConfig != null) { - float distanceToAtmosphere = cameraPosition.dst(nearestPlanet.getPosition()) - nearestPlanet.getGroundHeight() - Const.ATM_HEIGHT / 2; - airPercentage = SolMath.clamp(1 - distanceToAtmosphere / (Const.ATM_HEIGHT / 2)); - } - if (DebugOptions.SOUND_IN_SPACE) { - airPercentage = 1; - } - - float maxSoundDist = 1 + 1.5f * Const.CAM_VIEW_DIST_GROUND * airPercentage; - - Hero hero = game.getHero(); - float soundRadius = hero.isTranscendent() ? 0 : hero.getHull().config.getApproxRadius(); - float distance = position.dst(cameraPosition) - soundRadius; - float distanceMultiplier = SolMath.clamp(1 - distance / maxSoundDist); - - float volume = sound.getBaseVolume() * volumeMultiplier * distanceMultiplier * globalVolumeMultiplier; - - if (volume <= 0) { - return; - } - - // Calculate the pitch for the sound - float pitch = SolRandom.randomFloat(.97f, 1.03f) * game.getTimeFactor() * playableSound.getBasePitch(); - - if (skipLooped(source, sound, game.getTime())) { - return; - } - - if (DebugOptions.SOUND_INFO) { - debugHintDrawer.add(source, position, sound.toString()); - } - - Sound gdxSound = sound.getSound(); - gdxSound.play(volume, pitch, 0); - } - - /** - * Plays a sound. Source must not be null. - * - * @param position position of a sound. If null, source.getPosition() will be used - * @param source bearer of a sound. Must not be null for looped sounds - */ - public void play(SolGame game, PlayableSound sound, @Nullable Vector2 position, @Nullable SolObject source) { - this.play(game, sound, position, source, 1f); - } - - private boolean skipLooped(SolObject source, OggSound sound, float time) { - if (sound.getLoopTime() == 0) { - return false; - } - - boolean playing; - Map looped = loopedSoundMap.get(source); - if (looped == null) { - looped = new HashMap<>(); - loopedSoundMap.put(source, looped); - playing = false; - } else { - Float endTime = looped.get(sound); - if (endTime == null || endTime <= time) { - looped.put(sound, time + sound.getLoopTime()); // argh, performance loss - playing = false; - } else { - playing = time < endTime; - } - } - return playing; - } - - public void drawDebug(GameDrawer drawer, SolCam solCam) { - if (DebugOptions.SOUND_INFO) { - debugHintDrawer.draw(drawer, solCam); - } - } - - public void update(SolGame game) { - if (DebugOptions.SOUND_INFO) { - debugHintDrawer.update(game); - } - - myLoopAwait -= game.getTimeStep(); - if (myLoopAwait <= 0) { - myLoopAwait = 30; - cleanLooped(game); - } - } - - private void cleanLooped(SolGame game) { - loopedSoundMap.keySet().removeIf(o -> o.shouldBeRemoved(game)); - } - - public void dispose() { - for (OggSound sound : soundMap.values()) { - sound.doDispose(); - } - } -} diff --git a/engine/src/main/java/org/destinationsol/game/sound/OggSoundSet.java b/engine/src/main/java/org/destinationsol/game/sound/OggSoundSet.java deleted file mode 100644 index 93be81378..000000000 --- a/engine/src/main/java/org/destinationsol/game/sound/OggSoundSet.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2016 MovingBlocks - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.destinationsol.game.sound; - -import org.destinationsol.assets.sound.OggSound; -import org.destinationsol.assets.sound.PlayableSound; -import org.destinationsol.common.SolRandom; - -import java.util.List; - -/** - * Represents a set of random OggSound urns with a single basePitch assigned to every of them. - *

- * This is an alternative to sounds being randomly fetched from a specified folder - - * a workflow that isn't viable with gestalt. - */ -public class OggSoundSet implements PlayableSound { - private final OggSoundManager oggSoundManager; - private final List urnList; - private final float basePitch; - - public OggSoundSet(OggSoundManager oggSoundManager, List urnList, float basePitch) { - this.oggSoundManager = oggSoundManager; - this.urnList = urnList; - this.basePitch = basePitch; - } - - public OggSoundSet(OggSoundManager oggSoundManager, List urnList) { - this(oggSoundManager, urnList, 1.0f); - } - - @Override - public OggSound getOggSound() { - return oggSoundManager.getSound(SolRandom.randomElement(urnList)); - } - - @Override - public float getBasePitch() { - return basePitch; - } -} \ No newline at end of file diff --git a/engine/src/main/java/org/destinationsol/game/sound/SpecialSounds.java b/engine/src/main/java/org/destinationsol/game/sound/SpecialSounds.java deleted file mode 100644 index ef2aa2cfd..000000000 --- a/engine/src/main/java/org/destinationsol/game/sound/SpecialSounds.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2017 MovingBlocks - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.destinationsol.game.sound; - -import com.badlogic.gdx.math.Vector2; -import org.destinationsol.Const; -import org.destinationsol.assets.sound.PlayableSound; -import org.destinationsol.game.DmgType; -import org.destinationsol.game.SolGame; -import org.destinationsol.game.SolObject; - -import java.util.Arrays; - -public class SpecialSounds { - - public final PlayableSound metalColl; - public final PlayableSound metalEnergyHit; - public final PlayableSound rockColl; - public final PlayableSound rockEnergyHit; - public final PlayableSound asteroidCrack; - public final PlayableSound shipExplosion; - public final PlayableSound forceBeaconWork; - public final PlayableSound doorMove; - public final PlayableSound abilityRecharged; - public final PlayableSound abilityRefused; - public final PlayableSound controlDisabled; - public final PlayableSound controlEnabled; - public final PlayableSound lootThrow; - public final PlayableSound transcendentCreated; - public final PlayableSound transcendentFinished; - - public final PlayableSound metalBulletHit; - public final PlayableSound rockBulletHit; - public final PlayableSound burning; - public final PlayableSound transcendentMove; - - public SpecialSounds(OggSoundManager soundManager) { - // OggSound - metalColl = soundManager.getSound("core:metalCollision"); - metalEnergyHit = soundManager.getSound("core:empty"); - rockColl = soundManager.getSound("core:rockCollision"); - rockEnergyHit = soundManager.getSound("core:empty"); - asteroidCrack = soundManager.getSound("core:asteroidCrack"); - shipExplosion = soundManager.getSound("core:shipExplosion"); - forceBeaconWork = soundManager.getSound("core:forceBeaconWork"); - doorMove = soundManager.getSound("core:controlEnabled"); - abilityRecharged = soundManager.getSound("core:abilityRecharged"); - abilityRefused = soundManager.getSound("core:abilityRefused"); - controlDisabled = soundManager.getSound("core:controlDisabled"); - controlEnabled = soundManager.getSound("core:controlEnabled"); - lootThrow = soundManager.getSound("core:rocketLauncherShoot"); - transcendentCreated = soundManager.getSound("core:teleport"); - transcendentFinished = soundManager.getSound("core:teleport"); - - // OggSoundSet - metalBulletHit = new OggSoundSet(soundManager, Arrays.asList("core:metalBulletHit0", "core:metalBulletHit1", "core:metalBulletHit2"), 1.1f); - rockBulletHit = new OggSoundSet(soundManager, Arrays.asList("core:rockBulletHit0", "core:rockBulletHit1")); - burning = new OggSoundSet(soundManager, Arrays.asList("core:burning2", "core:burning3", "core:burning4")); - transcendentMove = new OggSoundSet(soundManager, Arrays.asList("core:transcendentMove", "core:transcendentMove2", "core:transcendentMove3", "core:transcendentMove4")); - } - - public PlayableSound hitSound(boolean forMetal, DmgType dmgType) { - if (dmgType == DmgType.ENERGY) { - return forMetal ? metalEnergyHit : rockEnergyHit; - } - if (dmgType == DmgType.BULLET) { - return forMetal ? metalBulletHit : rockBulletHit; - } - return null; - } - - public void playHit(SolGame game, SolObject o, Vector2 position, DmgType dmgType) { - if (o == null) { - return; - } - Boolean metal = o.isMetal(); - if (metal == null) { - return; - } - PlayableSound sound = hitSound(metal, dmgType); - if (sound == null) { - return; - } - game.getSoundManager().play(game, sound, position, o); - } - - public void playColl(SolGame game, float absImpulse, SolObject o, Vector2 position) { - if (o == null || absImpulse < .1f) { - return; - } - Boolean metal = o.isMetal(); - if (metal == null) { - return; - } - game.getSoundManager().play(game, metal ? metalColl : rockColl, position, o, absImpulse * Const.IMPULSE_TO_COLL_VOL); - } -} From 0576ebfdd767cdcdde1a5cfbc3b9fedde120859b Mon Sep 17 00:00:00 2001 From: Nicholas Bates <19882546+NicholasBatesNZ@users.noreply.github.com> Date: Sat, 15 Aug 2026 10:18:57 +1200 Subject: [PATCH 6/7] fix: unify ECS sound handling with the existing sound API Addresses the review on #590. The ECS sound path re-implemented sound selection and playback alongside the existing SolObject path instead of reusing it. It now delegates: - SpecialSounds.hitSound(MaterialType, DmgType) is the single hit-sound selector, replacing both hitSound(boolean, DmgType) and the added getHitSound(MaterialType, DmgType). collisionSound(MaterialType) likewise. Both return Optional rather than null. - SpecialSounds gains playHit and playColl overloads that take an EntityRef. They share selection, thresholds and volume scaling with the SolObject versions; only the position and material sourcing differ, since an entity carries those as components. - SoundPlayingSystem calls those instead of selecting and playing sounds itself, and uses the impulse's contact position for collisions, as SolContactListener already does for SolObjects. - OggSoundManager's two play(...) paths and two skipLooped(...) methods collapse into one generic implementation each, parameterised by the loop-bookkeeping map. The resolved OggSound is returned from the shared method so that a PlayableSound backed by an OggSoundSet is not sampled twice for one playback. - DamageEvent.getDamageType() returns Optional. - DebugHint takes a SolObject, an EntityRef, or neither, through separate constructors instead of arbitrary nulls. SoundPlayingSystem and AsteroidSoundSystem used @In field injection, which the gestalt DI container that instantiates EventReceivers does not populate; they would have thrown NullPointerException at runtime. Both now use javax.inject.Inject, matching the other systems. Also fixes Component.copy -> copyFrom on the new Material component, the getVolume signature change, and a typo in SoundEvent.volumeMultiplier. Co-authored-by: IsaacLic Co-Authored-By: Claude Opus 5 --- .../assets/sound/OggSoundManager.java | 112 ++++++--------- .../assets/sound/SpecialSounds.java | 136 +++++++++++------- .../systems/AsteroidSoundSystem.java | 16 ++- .../destinationsol/game/sound/DebugHint.java | 23 ++- .../game/sound/DebugHintDrawer.java | 7 +- .../health/events/DamageEvent.java | 19 ++- .../material/components/Material.java | 2 +- .../sound/events/SoundEvent.java | 6 +- .../sound/systems/SoundPlayingSystem.java | 51 ++++--- 9 files changed, 204 insertions(+), 168 deletions(-) diff --git a/engine/src/main/java/org/destinationsol/assets/sound/OggSoundManager.java b/engine/src/main/java/org/destinationsol/assets/sound/OggSoundManager.java index d980fb409..48e481588 100644 --- a/engine/src/main/java/org/destinationsol/assets/sound/OggSoundManager.java +++ b/engine/src/main/java/org/destinationsol/assets/sound/OggSoundManager.java @@ -182,33 +182,15 @@ public void play(SolGame game, PlayableSound playableSound, @Nullable Vector2 po position = source.getPosition(); } - float volume = getVolume(game, position, volumeMultiplier, sound, game.getCam()); - - if (volume <= 0) { - return; - } - - // Calculate the pitch for the sound - float pitch = SolRandom.randomFloat(.97f, 1.03f) * game.getTimeFactor() * playableSound.getBasePitch(); - - if (skipLooped(source, sound, game.getTime())) { - return; - } - - if (DebugOptions.SOUND_INFO) { + if (play(game, sound, playableSound.getBasePitch(), position, volumeMultiplier, loopedSoundMapOfSolObjects, source) + && DebugOptions.SOUND_INFO) { debugHintDrawer.add(source, position, sound.toString()); } - - Sound gdxSound = sound.getSound(); - gdxSound.play(volume, pitch, 0); } /** * Plays a sound at a particular position. If the sound has an associated loop, this will loop the sound, coming * from the entity. - *

- * {@code source} must not be null if the sound is specified to loop, and at least one of {@code source} or - * {@code position} must be specified. * * @param game Game to play the sound in * @param playableSound The sound to play @@ -230,7 +212,6 @@ public void play(SolGame game, PlayableSound playableSound, @NotNull Vector2 pos * @param volumeMultiplier Multiplier for sound volume */ public void play(SolGame game, PlayableSound playableSound, @NotNull Vector2 position, @NotNull EntityRef soundSource, float volumeMultiplier) { - if (playableSound == null) { return; } @@ -240,24 +221,48 @@ public void play(SolGame game, PlayableSound playableSound, @NotNull Vector2 pos OggSound sound = playableSound.getOggSound(); - float volume = getVolume(game, position, volumeMultiplier, sound); + if (play(game, sound, playableSound.getBasePitch(), position, volumeMultiplier, loopedSoundMapOfEntities, soundSource) + && DebugOptions.SOUND_INFO) { + debugHintDrawer.add(soundSource, position, sound.toString()); + } + } + + /** + * The shared body of the public {@code play} overloads. Everything about playing a sound is identical between a + * {@link SolObject} source and an {@link EntityRef} source, except for which map the loop bookkeeping lives in - + * so the caller supplies that map along with the key to use within it. + *

+ * The already-resolved {@link OggSound} is taken rather than a {@link PlayableSound}, because a + * {@link PlayableSound} may pick a different sound on every call (see {@link OggSoundSet}) and so must only be + * resolved once per playback. + * + * @param game Game to play the sound in. + * @param sound The sound to play. + * @param basePitch The base pitch of the {@link PlayableSound} the sound was resolved from. + * @param position Position to play the sound at. Must not be null. + * @param volumeMultiplier Multiplier for sound volume. + * @param loopedSounds The loop bookkeeping map appropriate for {@code source}. + * @param source Bearer of the sound, used as the key into {@code loopedSounds}. + * @param The type of the sound's bearer. + * @return true if the sound was played, false if it was inaudible or suppressed by its loop. + */ + private boolean play(SolGame game, OggSound sound, float basePitch, Vector2 position, float volumeMultiplier, + Map> loopedSounds, @Nullable T source) { + float volume = getVolume(game, position, volumeMultiplier, sound, game.getCam()); if (volume <= 0) { - return; + return false; } // Calculate the pitch for the sound - float pitch = SolRandom.randomFloat(.97f, 1.03f) * game.getTimeFactor() * playableSound.getBasePitch(); + float pitch = SolRandom.randomFloat(.97f, 1.03f) * game.getTimeFactor() * basePitch; - if (skipLooped(soundSource, sound, game.getTime())) { - return; - } - - if (DebugOptions.SOUND_INFO) { - debugHintDrawer.add(soundSource, position, sound.toString()); + if (skipLooped(loopedSounds, source, sound, game.getTime())) { + return false; } Sound gdxSound = sound.getSound(); gdxSound.play(volume, pitch, 0); + return true; } /** @@ -304,53 +309,22 @@ private float getVolume(SolGame game, Vector2 position, float volumeMultiplier, * since it was last played on the object. * TODO: now handles even adding the sound to the list of looping sounds. Possibly extract that? * - * @param source Object playing this sound. - * @param sound Sound to be played. - * @param time Game's current time. - * @return true when sound should not be played because of loop, false otherwise. - */ - private boolean skipLooped(SolObject source, OggSound sound, float time) { - if (sound.getLoopTime() == 0) { - return false; - } - - Map looped = loopedSoundMapOfSolObjects.get(source); - if (looped == null) { - looped = new HashMap<>(); - loopedSoundMapOfSolObjects.put(source, looped); - return false; - } else { - Float endTime = looped.get(sound); - if (endTime == null || endTime <= time) { - looped.put(sound, time + sound.getLoopTime()); // argh, performance loss - return false; - } else { - return true; - } - } - } - - /** - * Returns true when sound should not be played because of loop, false otherwise. - *

- * Sound should not be played when its {@code loopTime > 0} and {@code loopTime} time units have not yet passed - * since it was last played on the object. - * TODO: now handles even adding the sound to the list of looping sounds. Possibly extract that? - * - * @param source Object playing this sound. - * @param sound Sound to be played. - * @param time Game's current time. + * @param loopedSounds The loop bookkeeping map appropriate for {@code source}. + * @param source Object or entity playing this sound. + * @param sound Sound to be played. + * @param time Game's current time. + * @param The type of the sound's bearer. * @return true when sound should not be played because of loop, false otherwise. */ - private boolean skipLooped(EntityRef source, OggSound sound, float time) { + private boolean skipLooped(Map> loopedSounds, T source, OggSound sound, float time) { if (sound.getLoopTime() == 0) { return false; } - Map looped = loopedSoundMapOfEntities.get(source); + Map looped = loopedSounds.get(source); if (looped == null) { looped = new HashMap<>(); - loopedSoundMapOfEntities.put(source, looped); + loopedSounds.put(source, looped); return false; } else { Float endTime = looped.get(sound); diff --git a/engine/src/main/java/org/destinationsol/assets/sound/SpecialSounds.java b/engine/src/main/java/org/destinationsol/assets/sound/SpecialSounds.java index 5c007930e..1b1811776 100644 --- a/engine/src/main/java/org/destinationsol/assets/sound/SpecialSounds.java +++ b/engine/src/main/java/org/destinationsol/assets/sound/SpecialSounds.java @@ -17,16 +17,24 @@ import com.badlogic.gdx.math.Vector2; import org.destinationsol.Const; +import org.destinationsol.common.Nullable; import org.destinationsol.game.DmgType; import org.destinationsol.game.SolGame; import org.destinationsol.game.SolObject; import org.destinationsol.material.MaterialType; +import org.terasology.gestalt.entitysystem.entity.EntityRef; import javax.inject.Inject; import java.util.Arrays; +import java.util.Optional; public class SpecialSounds { + /** + * Collisions gentler than this do not make a sound. + */ + private static final float MIN_COLLISION_IMPULSE = .1f; + public final PlayableSound metalColl; public final PlayableSound metalEnergyHit; public final PlayableSound rockColl; @@ -74,83 +82,109 @@ public SpecialSounds(OggSoundManager soundManager) { transcendentMove = new OggSoundSet(soundManager, Arrays.asList("core:transcendentMove", "core:transcendentMove2", "core:transcendentMove3", "core:transcendentMove4")); } - public PlayableSound hitSound(boolean forMetal, DmgType dmgType) { + /** + * The sound made when something of the given material is hit by the given kind of damage. + *

+ * This is the single place where hit sounds are selected: both the {@link SolObject} and the entity code paths + * resolve their sound through it. + * + * @param materialType the material of the thing being hit, or null if it is not made of a known material + * @param dmgType the kind of damage being dealt, or null if it is not known + * @return the sound to play, or empty if no sound is defined for that combination + */ + public Optional hitSound(@Nullable MaterialType materialType, @Nullable DmgType dmgType) { + if (materialType == null || dmgType == null) { + return Optional.empty(); + } + boolean metal = materialType == MaterialType.METAL; if (dmgType == DmgType.ENERGY) { - return forMetal ? metalEnergyHit : rockEnergyHit; + return Optional.of(metal ? metalEnergyHit : rockEnergyHit); } if (dmgType == DmgType.BULLET) { - return forMetal ? metalBulletHit : rockBulletHit; + return Optional.of(metal ? metalBulletHit : rockBulletHit); } - return null; + return Optional.empty(); + } + + /** + * The sound made when something of the given material collides with something else. + *

+ * This is the single place where collision sounds are selected: both the {@link SolObject} and the entity code + * paths resolve their sound through it. + * + * @param materialType the material of the colliding thing, or null if it is not made of a known material + * @return the sound to play, or empty if no sound is defined for that material + */ + public Optional collisionSound(@Nullable MaterialType materialType) { + if (materialType == null) { + return Optional.empty(); + } + return Optional.of(materialType == MaterialType.METAL ? metalColl : rockColl); } public void playHit(SolGame game, SolObject o, Vector2 position, DmgType dmgType) { if (o == null) { return; } - Boolean metal = o.isMetal(); - if (metal == null) { - return; - } - PlayableSound sound = hitSound(metal, dmgType); - if (sound == null) { - return; - } - game.getSoundManager().play(game, sound, position, o); + hitSound(materialTypeOf(o), dmgType) + .ifPresent(sound -> game.getSoundManager().play(game, sound, position, o)); + } + + /** + * The entity-based counterpart of {@link #playHit(SolGame, SolObject, Vector2, DmgType)}. An entity carries its + * material as a component rather than through {@link SolObject#isMetal()}, and its position is not derivable from + * the sound's bearer, so both are passed in; sound selection is shared. + * + * @param game Game to play the sound in. + * @param entity The entity that was hit; the sound is attached to it for looping and debug purposes. + * @param position Where the hit happened. + * @param dmgType The kind of damage dealt. + * @param materialType The material the entity is made of. + */ + public void playHit(SolGame game, EntityRef entity, Vector2 position, @Nullable DmgType dmgType, @Nullable MaterialType materialType) { + hitSound(materialType, dmgType) + .ifPresent(sound -> game.getSoundManager().play(game, sound, position, entity)); } public void playColl(SolGame game, float absImpulse, SolObject o, Vector2 position) { - if (o == null || absImpulse < .1f) { + if (o == null || absImpulse < MIN_COLLISION_IMPULSE) { return; } - Boolean metal = o.isMetal(); - if (metal == null) { - return; - } - game.getSoundManager().play(game, metal ? metalColl : rockColl, position, o, absImpulse * Const.IMPULSE_TO_COLL_VOL); + collisionSound(materialTypeOf(o)) + .ifPresent(sound -> game.getSoundManager().play(game, sound, position, o, absImpulse * Const.IMPULSE_TO_COLL_VOL)); } /** - * Gets the damage sound associated with the given {@link MaterialType} and {@link DmgType}. If no sound is defined, - * null is returned. + * The entity-based counterpart of {@link #playColl(SolGame, float, SolObject, Vector2)}. See + * {@link #playHit(SolGame, EntityRef, Vector2, DmgType, MaterialType)} for why the material and position are + * passed in rather than read off the sound's bearer. * - * @param materialType the material type of the damaged entity - * @param damageType the type of damage done - * @return the sound of the damage + * @param game Game to play the sound in. + * @param absImpulse The magnitude of the impulse of the collision. + * @param entity The entity that collided; the sound is attached to it for looping and debug purposes. + * @param position Where the collision happened. + * @param materialType The material the entity is made of. */ - public PlayableSound getHitSound(MaterialType materialType, DmgType damageType) { - if (damageType == DmgType.ENERGY) { - if (materialType == MaterialType.METAL) { - return metalEnergyHit; - } - if (materialType == MaterialType.ROCK) { - return rockEnergyHit; - } - } - if (damageType == DmgType.BULLET) { - if (materialType == MaterialType.METAL) { - return metalBulletHit; - } - if (materialType == MaterialType.ROCK) { - return rockBulletHit; - } + public void playColl(SolGame game, float absImpulse, EntityRef entity, Vector2 position, @Nullable MaterialType materialType) { + if (absImpulse < MIN_COLLISION_IMPULSE) { + return; } - return null; + collisionSound(materialType) + .ifPresent(sound -> game.getSoundManager().play(game, sound, position, entity, absImpulse * Const.IMPULSE_TO_COLL_VOL)); } /** - * Gets the collision sound associated with the given {@link MaterialType}. If no sound is defined, null is returned. + * Bridges the {@link SolObject} representation of a material - a nullable {@link Boolean} "is it metal?" - to the + * {@link MaterialType} used by entities. * - * @param materialType the material type of the entity - * @return the sound of the collision + * @return the object's material, or null if the object does not declare one */ - public PlayableSound getCollisionSound(MaterialType materialType) { - if (materialType == MaterialType.METAL) { - return metalColl; - } - if (materialType == MaterialType.ROCK) { - return rockColl; + @Nullable + private static MaterialType materialTypeOf(SolObject solObject) { + Boolean metal = solObject.isMetal(); + if (metal == null) { + return null; } - return null; + return metal ? MaterialType.METAL : MaterialType.ROCK; } } diff --git a/engine/src/main/java/org/destinationsol/asteroids/systems/AsteroidSoundSystem.java b/engine/src/main/java/org/destinationsol/asteroids/systems/AsteroidSoundSystem.java index 024afe393..12ad7f618 100644 --- a/engine/src/main/java/org/destinationsol/asteroids/systems/AsteroidSoundSystem.java +++ b/engine/src/main/java/org/destinationsol/asteroids/systems/AsteroidSoundSystem.java @@ -17,7 +17,6 @@ import org.destinationsol.assets.sound.SpecialSounds; import org.destinationsol.asteroids.components.AsteroidMesh; -import org.destinationsol.common.In; import org.destinationsol.common.SolMath; import org.destinationsol.entitysystem.EntitySystemManager; import org.destinationsol.entitysystem.EventReceiver; @@ -31,16 +30,23 @@ import org.terasology.gestalt.entitysystem.event.EventResult; import org.terasology.gestalt.entitysystem.event.ReceiveEvent; +import javax.inject.Inject; + /** * This system plays asteroid-specific sounds. */ public class AsteroidSoundSystem implements EventReceiver { - @In - private EntitySystemManager entitySystemManager; + @Inject + EntitySystemManager entitySystemManager; + + @Inject + SpecialSounds specialSounds; - @In - private SpecialSounds specialSounds; + @Inject + public AsteroidSoundSystem() { + + } /** * When an asteroid is destroyed, this plays the asteroid destruction sound. diff --git a/engine/src/main/java/org/destinationsol/game/sound/DebugHint.java b/engine/src/main/java/org/destinationsol/game/sound/DebugHint.java index 1a9b75b84..21300f839 100644 --- a/engine/src/main/java/org/destinationsol/game/sound/DebugHint.java +++ b/engine/src/main/java/org/destinationsol/game/sound/DebugHint.java @@ -39,13 +39,30 @@ public class DebugHint { private EntityRef entity; - public DebugHint(SolObject owner, EntityRef entity, Vector2 position) { - myOwner = owner; - this.entity = entity; + /** + * A hint pinned to a fixed position, belonging to nothing in particular. + */ + public DebugHint(Vector2 position) { this.position = new Vector2(position); myMsgs = new HashMap<>(); } + /** + * A hint that follows a {@link SolObject}, and disappears when that object does. + */ + public DebugHint(SolObject owner, Vector2 position) { + this(position); + myOwner = owner; + } + + /** + * A hint that follows an entity, and disappears when that entity does. + */ + public DebugHint(EntityRef entity, Vector2 position) { + this(position); + this.entity = entity; + } + public void add(String value) { boolean needsRebuild = !myMsgs.containsKey(value); myMsgs.put(value, TimeUtils.millis() + MAX_AWAIT); diff --git a/engine/src/main/java/org/destinationsol/game/sound/DebugHintDrawer.java b/engine/src/main/java/org/destinationsol/game/sound/DebugHintDrawer.java index 8521f8801..a097ed73c 100644 --- a/engine/src/main/java/org/destinationsol/game/sound/DebugHintDrawer.java +++ b/engine/src/main/java/org/destinationsol/game/sound/DebugHintDrawer.java @@ -41,16 +41,15 @@ public DebugHintDrawer() { public void add(@Nullable SolObject owner, Vector2 position, String value) { DebugHint dh; if (owner == null) { - dh = freeNotes.computeIfAbsent(position, p -> new DebugHint(null, null, p)); + dh = freeNotes.computeIfAbsent(position, DebugHint::new); } else { - dh = tracedSolObjectNotes.computeIfAbsent(owner, o -> new DebugHint(o, null, o.getPosition())); + dh = tracedSolObjectNotes.computeIfAbsent(owner, o -> new DebugHint(o, o.getPosition())); } dh.add(value); } public void add(EntityRef entity, Vector2 position, String value) { - DebugHint debugHint; - debugHint = tracedEntityNotes.computeIfAbsent(entity, entityRef -> new DebugHint(null, entityRef, position)); + DebugHint debugHint = tracedEntityNotes.computeIfAbsent(entity, entityRef -> new DebugHint(entityRef, position)); debugHint.add(value); } diff --git a/engine/src/main/java/org/destinationsol/health/events/DamageEvent.java b/engine/src/main/java/org/destinationsol/health/events/DamageEvent.java index ae5ed57ae..4973e95d9 100644 --- a/engine/src/main/java/org/destinationsol/health/events/DamageEvent.java +++ b/engine/src/main/java/org/destinationsol/health/events/DamageEvent.java @@ -18,16 +18,22 @@ import org.destinationsol.game.DmgType; import org.terasology.gestalt.entitysystem.event.Event; +import java.util.Optional; + /** * Event that contains information about the damage an entity receives. */ public class DamageEvent implements Event { - private float damage; - private DmgType damageType; + private final float damage; + private final DmgType damageType; + /** + * Damage of no particular kind. Use {@link #DamageEvent(float, DmgType)} where the kind of damage is known, so + * that systems which care about it (such as sound) can react to it. + */ public DamageEvent(float damage) { - this.damage = damage; + this(damage, null); } public DamageEvent(float damage, DmgType damageType) { @@ -39,7 +45,10 @@ public float getDamage() { return damage; } - public DmgType getDamageType() { - return damageType; + /** + * The kind of damage dealt, if the source of the damage specified one. + */ + public Optional getDamageType() { + return Optional.ofNullable(damageType); } } diff --git a/engine/src/main/java/org/destinationsol/material/components/Material.java b/engine/src/main/java/org/destinationsol/material/components/Material.java index 61f664ab3..3c53ac25e 100644 --- a/engine/src/main/java/org/destinationsol/material/components/Material.java +++ b/engine/src/main/java/org/destinationsol/material/components/Material.java @@ -26,7 +26,7 @@ public class Material implements Component { public MaterialType materialType; @Override - public void copy(Material other) { + public void copyFrom(Material other) { this.materialType = other.materialType; } } diff --git a/engine/src/main/java/org/destinationsol/sound/events/SoundEvent.java b/engine/src/main/java/org/destinationsol/sound/events/SoundEvent.java index f429f8427..c0510a91f 100644 --- a/engine/src/main/java/org/destinationsol/sound/events/SoundEvent.java +++ b/engine/src/main/java/org/destinationsol/sound/events/SoundEvent.java @@ -24,10 +24,10 @@ public class SoundEvent implements Event { public final PlayableSound playableSound; - public final float volumeMultplier; + public final float volumeMultiplier; - public SoundEvent(PlayableSound playableSound, float volumeMultplier) { + public SoundEvent(PlayableSound playableSound, float volumeMultiplier) { this.playableSound = playableSound; - this.volumeMultplier = volumeMultplier; + this.volumeMultiplier = volumeMultiplier; } } diff --git a/engine/src/main/java/org/destinationsol/sound/systems/SoundPlayingSystem.java b/engine/src/main/java/org/destinationsol/sound/systems/SoundPlayingSystem.java index 44c1cfb59..4eeeee86c 100644 --- a/engine/src/main/java/org/destinationsol/sound/systems/SoundPlayingSystem.java +++ b/engine/src/main/java/org/destinationsol/sound/systems/SoundPlayingSystem.java @@ -16,11 +16,8 @@ package org.destinationsol.sound.systems; import com.badlogic.gdx.math.Vector2; -import org.destinationsol.Const; import org.destinationsol.assets.sound.OggSoundManager; -import org.destinationsol.assets.sound.PlayableSound; import org.destinationsol.assets.sound.SpecialSounds; -import org.destinationsol.common.In; import org.destinationsol.entitysystem.EventReceiver; import org.destinationsol.force.events.ImpulseEvent; import org.destinationsol.game.DmgType; @@ -34,19 +31,29 @@ import org.terasology.gestalt.entitysystem.event.EventResult; import org.terasology.gestalt.entitysystem.event.ReceiveEvent; +import javax.inject.Inject; + /** * This system plays sounds emitting from entities with a {@link Position} component, using the {@link OggSoundManager}. + *

+ * Hit and collision sounds are selected and played by {@link SpecialSounds}, which is also what the non-entity + * ({@link org.destinationsol.game.SolObject}) code path uses, so that both paths stay in step. */ public class SoundPlayingSystem implements EventReceiver { - @In - private SolGame game; + @Inject + SolGame game; + + @Inject + OggSoundManager soundManager; + + @Inject + SpecialSounds specialSounds; - @In - private OggSoundManager soundManager; + @Inject + public SoundPlayingSystem() { - @In - private SpecialSounds specialSounds; + } /** * Plays a given sound emitting from an entity, at that entity's {@link Position}. @@ -54,42 +61,32 @@ public class SoundPlayingSystem implements EventReceiver { @ReceiveEvent(components = Position.class) public EventResult playSound(SoundEvent event, EntityRef entity) { Vector2 position = entity.getComponent(Position.class).get().position; - soundManager.play(game, event.playableSound, position, entity, event.volumeMultplier); + soundManager.play(game, event.playableSound, position, entity, event.volumeMultiplier); return EventResult.CONTINUE; } /** * When an entity takes damage, this plays a sound based on the type of damage taken and the type of material that * the entity is. No sound will be played if there is no defined sound for the {@link DmgType}/{@link MaterialType} - * combination, or if either the {@link DmgType} or {@link MaterialType} is null. + * combination, or if either the {@link DmgType} or {@link MaterialType} is unknown. */ @ReceiveEvent(components = {Position.class, Material.class}) public EventResult playDamageSound(DamageEvent event, EntityRef entity) { MaterialType materialType = entity.getComponent(Material.class).get().materialType; - PlayableSound sound = specialSounds.getHitSound(materialType, event.getDamageType()); - if (sound != null) { - Vector2 position = entity.getComponent(Position.class).get().position; - soundManager.play(game, sound, position, entity); - } + Vector2 position = entity.getComponent(Position.class).get().position; + specialSounds.playHit(game, entity, position, event.getDamageType().orElse(null), materialType); return EventResult.CONTINUE; } /** * When an entity experiences a collision, this plays a sound based on the type of material that the entity is. No - * sound will be played if there is no defined collision sound for the {@link MaterialType}, or if the - * {@link MaterialType} is null. + * sound will be played if there is no defined collision sound for the {@link MaterialType}, if the + * {@link MaterialType} is unknown, or if the collision was too gentle to be heard. */ @ReceiveEvent(components = {Position.class, Material.class}) public EventResult playCollisionSound(ImpulseEvent event, EntityRef entity) { - float magnitude = event.getMagnitude(); - if (magnitude >= .1f) { - Vector2 position = entity.getComponent(Position.class).get().position; - MaterialType materialType = entity.getComponent(Material.class).get().materialType; - PlayableSound collisionSound = specialSounds.getCollisionSound(materialType); - if (collisionSound != null) { - soundManager.play(game, collisionSound, position, entity, magnitude * Const.IMPULSE_TO_COLL_VOL); - } - } + MaterialType materialType = entity.getComponent(Material.class).get().materialType; + specialSounds.playColl(game, event.getMagnitude(), entity, event.getContactPosition(), materialType); return EventResult.CONTINUE; } } From c6a4e74c9b566ce5a98de276b71c1f01b561f00e Mon Sep 17 00:00:00 2001 From: Nicholas Bates <19882546+NicholasBatesNZ@users.noreply.github.com> Date: Sat, 15 Aug 2026 10:22:21 +1200 Subject: [PATCH 7/7] refactor: remove unused duplicate sound classes OggSoundManager, OggSoundSet and SpecialSounds existed in both org.destinationsol.game.sound and org.destinationsol.assets.sound. Only the assets.sound copies are wired up: every consumer in engine, desktop and the tests imports org.destinationsol.assets.sound.*, and the game.sound copies were referenced only by each other. They have been dead since the assets.sound versions were introduced. The rest of the game.sound package is untouched. DebugHint and DebugHintDrawer are still live -- assets.sound.OggSoundManager imports DebugHintDrawer -- so the package and its @API export remain. Originally spotted by IsaacLic in #590. Co-authored-by: IsaacLic Co-Authored-By: Claude Opus 5 --- .../game/sound/OggSoundManager.java | 199 ------------------ .../game/sound/OggSoundSet.java | 54 ----- .../game/sound/SpecialSounds.java | 110 ---------- 3 files changed, 363 deletions(-) delete mode 100644 engine/src/main/java/org/destinationsol/game/sound/OggSoundManager.java delete mode 100644 engine/src/main/java/org/destinationsol/game/sound/OggSoundSet.java delete mode 100644 engine/src/main/java/org/destinationsol/game/sound/SpecialSounds.java diff --git a/engine/src/main/java/org/destinationsol/game/sound/OggSoundManager.java b/engine/src/main/java/org/destinationsol/game/sound/OggSoundManager.java deleted file mode 100644 index 2034c0d1a..000000000 --- a/engine/src/main/java/org/destinationsol/game/sound/OggSoundManager.java +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Copyright 2016 MovingBlocks - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.destinationsol.game.sound; - -import com.badlogic.gdx.audio.Sound; -import com.badlogic.gdx.math.Vector2; -import org.destinationsol.Const; -import org.destinationsol.assets.Assets; -import org.destinationsol.assets.sound.OggSound; -import org.destinationsol.assets.sound.PlayableSound; -import org.destinationsol.common.Nullable; -import org.destinationsol.common.SolMath; -import org.destinationsol.common.SolRandom; -import org.destinationsol.game.DebugOptions; -import org.destinationsol.game.GameDrawer; -import org.destinationsol.game.Hero; -import org.destinationsol.game.SolCam; -import org.destinationsol.game.SolGame; -import org.destinationsol.game.SolObject; -import org.destinationsol.game.planet.Planet; - -import java.util.HashMap; -import java.util.Map; - -public class OggSoundManager { - // private static Logger logger = LoggerFactory.getLogger(OggSoundManager.class); - private final Map soundMap; - private final Map> loopedSoundMap; - private final DebugHintDrawer debugHintDrawer; - - private float myLoopAwait; - - public OggSoundManager() { - this.soundMap = new HashMap<>(); - this.loopedSoundMap = new HashMap<>(); - this.debugHintDrawer = new DebugHintDrawer(); - } - - public OggSound getSound(String path) { - return getSound(path, 1.0f); - } - - public OggSound getSound(String path, float basePitch) { - if (soundMap.containsKey(path)) { - return soundMap.get(path); - } - - OggSound sound = Assets.getSound(path); - sound.setBasePitch(basePitch); - soundMap.put(path, sound); - return sound; - } - - /** - * Plays a sound. Source must not be null. - * - * @param position position of a sound. If null, source.getPosition() will be used - * @param source bearer of a sound. Must not be null for looped sounds - * @param volumeMultiplier multiplier for sound volume - */ - public void play(SolGame game, PlayableSound playableSound, @Nullable Vector2 position, @Nullable SolObject source, float volumeMultiplier) { - if (playableSound == null) { - return; - } - - OggSound sound = playableSound.getOggSound(); - // logger.debug("Playing sound: {}", sound.getUrn().toString()); - - // Perform some initial argument validation - if (source == null && position == null) { - throw new AssertionError("Either position or source must be non-null"); - } - if (source == null && sound.getLoopTime() > 0) { - throw new AssertionError("Attempted to loop a sound without a parent object: " + sound.getUrn()); - } - if (position == null) { - position = source.getPosition(); - } - - // Calculate the volume multiplier for the sound - float globalVolumeMultiplier = game.getSolApplication().getOptions().sfxVolume.getVolume(); - if (globalVolumeMultiplier == 0) { - return; - } - - Vector2 cameraPosition = game.getCam().getPosition(); - Planet nearestPlanet = game.getPlanetManager().getNearestPlanet(); - - float airPercentage = 0; - if (nearestPlanet.getConfig().skyConfig != null) { - float distanceToAtmosphere = cameraPosition.dst(nearestPlanet.getPosition()) - nearestPlanet.getGroundHeight() - Const.ATM_HEIGHT / 2; - airPercentage = SolMath.clamp(1 - distanceToAtmosphere / (Const.ATM_HEIGHT / 2)); - } - if (DebugOptions.SOUND_IN_SPACE) { - airPercentage = 1; - } - - float maxSoundDist = 1 + 1.5f * Const.CAM_VIEW_DIST_GROUND * airPercentage; - - Hero hero = game.getHero(); - float soundRadius = hero.isTranscendent() ? 0 : hero.getHull().config.getApproxRadius(); - float distance = position.dst(cameraPosition) - soundRadius; - float distanceMultiplier = SolMath.clamp(1 - distance / maxSoundDist); - - float volume = sound.getBaseVolume() * volumeMultiplier * distanceMultiplier * globalVolumeMultiplier; - - if (volume <= 0) { - return; - } - - // Calculate the pitch for the sound - float pitch = SolRandom.randomFloat(.97f, 1.03f) * game.getTimeFactor() * playableSound.getBasePitch(); - - if (skipLooped(source, sound, game.getTime())) { - return; - } - - if (DebugOptions.SOUND_INFO) { - debugHintDrawer.add(source, position, sound.toString()); - } - - Sound gdxSound = sound.getSound(); - gdxSound.play(volume, pitch, 0); - } - - /** - * Plays a sound. Source must not be null. - * - * @param position position of a sound. If null, source.getPosition() will be used - * @param source bearer of a sound. Must not be null for looped sounds - */ - public void play(SolGame game, PlayableSound sound, @Nullable Vector2 position, @Nullable SolObject source) { - this.play(game, sound, position, source, 1f); - } - - private boolean skipLooped(SolObject source, OggSound sound, float time) { - if (sound.getLoopTime() == 0) { - return false; - } - - boolean playing; - Map looped = loopedSoundMap.get(source); - if (looped == null) { - looped = new HashMap<>(); - loopedSoundMap.put(source, looped); - playing = false; - } else { - Float endTime = looped.get(sound); - if (endTime == null || endTime <= time) { - looped.put(sound, time + sound.getLoopTime()); // argh, performance loss - playing = false; - } else { - playing = time < endTime; - } - } - return playing; - } - - public void drawDebug(GameDrawer drawer, SolCam solCam) { - if (DebugOptions.SOUND_INFO) { - debugHintDrawer.draw(drawer, solCam); - } - } - - public void update(SolGame game) { - if (DebugOptions.SOUND_INFO) { - debugHintDrawer.update(game); - } - - myLoopAwait -= game.getTimeStep(); - if (myLoopAwait <= 0) { - myLoopAwait = 30; - cleanLooped(game); - } - } - - private void cleanLooped(SolGame game) { - loopedSoundMap.keySet().removeIf(o -> o.shouldBeRemoved(game)); - } - - public void dispose() { - for (OggSound sound : soundMap.values()) { - sound.doDispose(); - } - } -} diff --git a/engine/src/main/java/org/destinationsol/game/sound/OggSoundSet.java b/engine/src/main/java/org/destinationsol/game/sound/OggSoundSet.java deleted file mode 100644 index 93be81378..000000000 --- a/engine/src/main/java/org/destinationsol/game/sound/OggSoundSet.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2016 MovingBlocks - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.destinationsol.game.sound; - -import org.destinationsol.assets.sound.OggSound; -import org.destinationsol.assets.sound.PlayableSound; -import org.destinationsol.common.SolRandom; - -import java.util.List; - -/** - * Represents a set of random OggSound urns with a single basePitch assigned to every of them. - *

- * This is an alternative to sounds being randomly fetched from a specified folder - - * a workflow that isn't viable with gestalt. - */ -public class OggSoundSet implements PlayableSound { - private final OggSoundManager oggSoundManager; - private final List urnList; - private final float basePitch; - - public OggSoundSet(OggSoundManager oggSoundManager, List urnList, float basePitch) { - this.oggSoundManager = oggSoundManager; - this.urnList = urnList; - this.basePitch = basePitch; - } - - public OggSoundSet(OggSoundManager oggSoundManager, List urnList) { - this(oggSoundManager, urnList, 1.0f); - } - - @Override - public OggSound getOggSound() { - return oggSoundManager.getSound(SolRandom.randomElement(urnList)); - } - - @Override - public float getBasePitch() { - return basePitch; - } -} \ No newline at end of file diff --git a/engine/src/main/java/org/destinationsol/game/sound/SpecialSounds.java b/engine/src/main/java/org/destinationsol/game/sound/SpecialSounds.java deleted file mode 100644 index ef2aa2cfd..000000000 --- a/engine/src/main/java/org/destinationsol/game/sound/SpecialSounds.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2017 MovingBlocks - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.destinationsol.game.sound; - -import com.badlogic.gdx.math.Vector2; -import org.destinationsol.Const; -import org.destinationsol.assets.sound.PlayableSound; -import org.destinationsol.game.DmgType; -import org.destinationsol.game.SolGame; -import org.destinationsol.game.SolObject; - -import java.util.Arrays; - -public class SpecialSounds { - - public final PlayableSound metalColl; - public final PlayableSound metalEnergyHit; - public final PlayableSound rockColl; - public final PlayableSound rockEnergyHit; - public final PlayableSound asteroidCrack; - public final PlayableSound shipExplosion; - public final PlayableSound forceBeaconWork; - public final PlayableSound doorMove; - public final PlayableSound abilityRecharged; - public final PlayableSound abilityRefused; - public final PlayableSound controlDisabled; - public final PlayableSound controlEnabled; - public final PlayableSound lootThrow; - public final PlayableSound transcendentCreated; - public final PlayableSound transcendentFinished; - - public final PlayableSound metalBulletHit; - public final PlayableSound rockBulletHit; - public final PlayableSound burning; - public final PlayableSound transcendentMove; - - public SpecialSounds(OggSoundManager soundManager) { - // OggSound - metalColl = soundManager.getSound("core:metalCollision"); - metalEnergyHit = soundManager.getSound("core:empty"); - rockColl = soundManager.getSound("core:rockCollision"); - rockEnergyHit = soundManager.getSound("core:empty"); - asteroidCrack = soundManager.getSound("core:asteroidCrack"); - shipExplosion = soundManager.getSound("core:shipExplosion"); - forceBeaconWork = soundManager.getSound("core:forceBeaconWork"); - doorMove = soundManager.getSound("core:controlEnabled"); - abilityRecharged = soundManager.getSound("core:abilityRecharged"); - abilityRefused = soundManager.getSound("core:abilityRefused"); - controlDisabled = soundManager.getSound("core:controlDisabled"); - controlEnabled = soundManager.getSound("core:controlEnabled"); - lootThrow = soundManager.getSound("core:rocketLauncherShoot"); - transcendentCreated = soundManager.getSound("core:teleport"); - transcendentFinished = soundManager.getSound("core:teleport"); - - // OggSoundSet - metalBulletHit = new OggSoundSet(soundManager, Arrays.asList("core:metalBulletHit0", "core:metalBulletHit1", "core:metalBulletHit2"), 1.1f); - rockBulletHit = new OggSoundSet(soundManager, Arrays.asList("core:rockBulletHit0", "core:rockBulletHit1")); - burning = new OggSoundSet(soundManager, Arrays.asList("core:burning2", "core:burning3", "core:burning4")); - transcendentMove = new OggSoundSet(soundManager, Arrays.asList("core:transcendentMove", "core:transcendentMove2", "core:transcendentMove3", "core:transcendentMove4")); - } - - public PlayableSound hitSound(boolean forMetal, DmgType dmgType) { - if (dmgType == DmgType.ENERGY) { - return forMetal ? metalEnergyHit : rockEnergyHit; - } - if (dmgType == DmgType.BULLET) { - return forMetal ? metalBulletHit : rockBulletHit; - } - return null; - } - - public void playHit(SolGame game, SolObject o, Vector2 position, DmgType dmgType) { - if (o == null) { - return; - } - Boolean metal = o.isMetal(); - if (metal == null) { - return; - } - PlayableSound sound = hitSound(metal, dmgType); - if (sound == null) { - return; - } - game.getSoundManager().play(game, sound, position, o); - } - - public void playColl(SolGame game, float absImpulse, SolObject o, Vector2 position) { - if (o == null || absImpulse < .1f) { - return; - } - Boolean metal = o.isMetal(); - if (metal == null) { - return; - } - game.getSoundManager().play(game, metal ? metalColl : rockColl, position, o, absImpulse * Const.IMPULSE_TO_COLL_VOL); - } -}