diff --git a/engine/src/main/java/org/destinationsol/SolApplication.java b/engine/src/main/java/org/destinationsol/SolApplication.java index 50b9f58e4..65d56ac07 100644 --- a/engine/src/main/java/org/destinationsol/SolApplication.java +++ b/engine/src/main/java/org/destinationsol/SolApplication.java @@ -47,6 +47,8 @@ import org.destinationsol.location.components.Angle; import org.destinationsol.location.components.Position; import org.destinationsol.location.components.Velocity; +import org.destinationsol.material.MaterialType; +import org.destinationsol.material.components.Material; import org.destinationsol.menu.MenuScreens; import org.destinationsol.menu.background.MenuBackgroundManager; import org.destinationsol.modules.ModuleManager; @@ -284,7 +286,7 @@ private void draw() { if (DebugOptions.SPAWN_ECS_ASTEROID && !entityCreated) { Size size = new Size(); - size.size = 2; + size.size = 1; RenderableElement element = new RenderableElement(); element.texture = SolRandom.randomElement(Assets.listTexturesMatching("engine:asteroid_.*")); @@ -298,13 +300,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 f58568119..48e481588 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.UpdateAwareSystem; import org.destinationsol.game.planet.Planet; import org.destinationsol.game.sound.DebugHintDrawer; +import org.terasology.gestalt.entitysystem.entity.EntityRef; import javax.inject.Inject; import javax.inject.Provider; @@ -72,7 +74,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, SolCam)} for more info. @@ -92,7 +101,8 @@ public class OggSoundManager implements UpdateAwareSystem { @Inject public OggSoundManager(Provider applicationProvider) { soundMap = new HashMap<>(); - loopedSoundMap = new HashMap<>(); + loopedSoundMapOfSolObjects = new HashMap<>(); + loopedSoundMapOfEntities = new HashMap<>(); debugHintDrawer = new DebugHintDrawer(); this.applicationProvider = applicationProvider; } @@ -172,25 +182,87 @@ public void play(SolGame game, PlayableSound playableSound, @Nullable Vector2 po position = source.getPosition(); } - float volume = getVolume(game, position, volumeMultiplier, sound, game.getCam()); + if (play(game, sound, playableSound.getBasePitch(), position, volumeMultiplier, loopedSoundMapOfSolObjects, source) + && DebugOptions.SOUND_INFO) { + debugHintDrawer.add(source, position, sound.toString()); + } + } - if (volume <= 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. + */ + 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. + * + * @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"); + } - // Calculate the pitch for the sound - float pitch = SolRandom.randomFloat(.97f, 1.03f) * game.getTimeFactor() * playableSound.getBasePitch(); + OggSound sound = playableSound.getOggSound(); - if (skipLooped(source, sound, game.getTime())) { - return; + if (play(game, sound, playableSound.getBasePitch(), position, volumeMultiplier, loopedSoundMapOfEntities, soundSource) + && DebugOptions.SOUND_INFO) { + debugHintDrawer.add(soundSource, position, sound.toString()); } + } - if (DebugOptions.SOUND_INFO) { - debugHintDrawer.add(source, 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 false; + } + + // Calculate the pitch for the sound + float pitch = SolRandom.randomFloat(.97f, 1.03f) * game.getTimeFactor() * basePitch; + + if (skipLooped(loopedSounds, source, sound, game.getTime())) { + return false; } Sound gdxSound = sound.getSound(); gdxSound.play(volume, pitch, 0); + return true; } /** @@ -237,20 +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. + * @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(SolObject source, OggSound sound, float time) { + private boolean skipLooped(Map> loopedSounds, T source, OggSound sound, float time) { if (sound.getLoopTime() == 0) { return false; } - Map looped = loopedSoundMap.get(source); + Map looped = loopedSounds.get(source); if (looped == null) { looped = new HashMap<>(); - loopedSoundMap.put(source, looped); + loopedSounds.put(source, looped); return false; } else { Float endTime = looped.get(sound); @@ -294,14 +368,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/assets/sound/SpecialSounds.java b/engine/src/main/java/org/destinationsol/assets/sound/SpecialSounds.java index 0508b2a73..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,15 +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; @@ -73,39 +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 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 null; + 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) { + 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 < MIN_COLLISION_IMPULSE) { return; } - game.getSoundManager().play(game, sound, position, o); + collisionSound(materialTypeOf(o)) + .ifPresent(sound -> game.getSoundManager().play(game, sound, position, o, absImpulse * Const.IMPULSE_TO_COLL_VOL)); } - public void playColl(SolGame game, float absImpulse, SolObject o, Vector2 position) { - if (o == null || absImpulse < .1f) { + /** + * 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 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 void playColl(SolGame game, float absImpulse, EntityRef entity, Vector2 position, @Nullable MaterialType materialType) { + if (absImpulse < MIN_COLLISION_IMPULSE) { return; } - Boolean metal = o.isMetal(); + collisionSound(materialType) + .ifPresent(sound -> game.getSoundManager().play(game, sound, position, entity, absImpulse * Const.IMPULSE_TO_COLL_VOL)); + } + + /** + * Bridges the {@link SolObject} representation of a material - a nullable {@link Boolean} "is it metal?" - to the + * {@link MaterialType} used by entities. + * + * @return the object's material, or null if the object does not declare one + */ + @Nullable + private static MaterialType materialTypeOf(SolObject solObject) { + Boolean metal = solObject.isMetal(); if (metal == null) { - return; + return null; } - game.getSoundManager().play(game, metal ? metalColl : rockColl, position, o, absImpulse * Const.IMPULSE_TO_COLL_VOL); + 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 new file mode 100644 index 000000000..12ad7f618 --- /dev/null +++ b/engine/src/main/java/org/destinationsol/asteroids/systems/AsteroidSoundSystem.java @@ -0,0 +1,65 @@ +/* + * 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.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; + +import javax.inject.Inject; + +/** + * This system plays asteroid-specific sounds. + */ +public class AsteroidSoundSystem implements EventReceiver { + + @Inject + EntitySystemManager entitySystemManager; + + @Inject + SpecialSounds specialSounds; + + @Inject + public AsteroidSoundSystem() { + + } + + /** + * 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/projectile/Projectile.java b/engine/src/main/java/org/destinationsol/game/projectile/Projectile.java index 6493ba69b..1385d8105 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/game/sound/DebugHint.java b/engine/src/main/java/org/destinationsol/game/sound/DebugHint.java index 11aa15896..21300f839 100644 --- a/engine/src/main/java/org/destinationsol/game/sound/DebugHint.java +++ b/engine/src/main/java/org/destinationsol/game/sound/DebugHint.java @@ -22,6 +22,8 @@ import org.destinationsol.game.SolCam; 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; @@ -35,12 +37,32 @@ public class DebugHint { private SolObject myOwner; private String myMsg; - public DebugHint(SolObject owner, Vector2 position) { - myOwner = owner; + private EntityRef 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); @@ -66,6 +88,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 0b4d2703c..a097ed73c 100644 --- a/engine/src/main/java/org/destinationsol/game/sound/DebugHintDrawer.java +++ b/engine/src/main/java/org/destinationsol/game/sound/DebugHintDrawer.java @@ -21,33 +21,42 @@ import org.destinationsol.game.SolCam; 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, DebugHint::new); } else { - dh = myTracedNotes.computeIfAbsent(owner, o -> new DebugHint(o, 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 = tracedEntityNotes.computeIfAbsent(entity, entityRef -> new DebugHint(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) { @@ -61,10 +70,13 @@ private void updateEach(SolGame game, Iterator it) { } public void draw(GameDrawer drawer, SolCam solCam) { - for (DebugHint n : myTracedNotes.values()) { + for (DebugHint n : tracedSolObjectNotes.values()) { + n.draw(drawer, solCam); + } + for (DebugHint n : freeNotes.values()) { n.draw(drawer, solCam); } - for (DebugHint n : myFreeNotes.values()) { + for (DebugHint n : tracedEntityNotes.values()) { n.draw(drawer, solCam); } } 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); - } -} 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..4973e95d9 100644 --- a/engine/src/main/java/org/destinationsol/health/events/DamageEvent.java +++ b/engine/src/main/java/org/destinationsol/health/events/DamageEvent.java @@ -15,20 +15,40 @@ */ package org.destinationsol.health.events; +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 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, null); + } + + public DamageEvent(float damage, DmgType damageType) { this.damage = damage; + this.damageType = damageType; } public float getDamage() { return damage; } + + /** + * 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/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..3c53ac25e --- /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 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 new file mode 100644 index 000000000..c0510a91f --- /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 float volumeMultiplier; + + public SoundEvent(PlayableSound playableSound, float volumeMultiplier) { + this.playableSound = playableSound; + 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 new file mode 100644 index 000000000..4eeeee86c --- /dev/null +++ b/engine/src/main/java/org/destinationsol/sound/systems/SoundPlayingSystem.java @@ -0,0 +1,92 @@ +/* + * 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.assets.sound.SpecialSounds; +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; +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 { + + @Inject + SolGame game; + + @Inject + OggSoundManager soundManager; + + @Inject + SpecialSounds specialSounds; + + @Inject + public SoundPlayingSystem() { + + } + + /** + * 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, 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 unknown. + */ + @ReceiveEvent(components = {Position.class, Material.class}) + public EventResult playDamageSound(DamageEvent event, EntityRef entity) { + MaterialType materialType = entity.getComponent(Material.class).get().materialType; + 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}, 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) { + MaterialType materialType = entity.getComponent(Material.class).get().materialType; + specialSounds.playColl(game, event.getMagnitude(), entity, event.getContactPosition(), materialType); + return EventResult.CONTINUE; + } +}