Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ECS wrapper for the sound system #590

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions engine/src/main/java/org/destinationsol/SolApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -272,7 +274,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_.*"));
Expand All @@ -286,13 +288,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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -33,6 +34,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;
Expand Down Expand Up @@ -69,7 +71,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<SolObject, Map<OggSound, Float>> loopedSoundMap;
private final Map<SolObject, Map<OggSound, Float>> 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<EntityRef, Map<OggSound, Float>> loopedSoundMapOfEntities;
/**
* Used for drawing debug hints when {@link DebugOptions#SOUND_INFO} flag is set. See
* {@link #drawDebug(GameDrawer, SolCam)} for more info.
Expand All @@ -90,7 +99,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);
this.solCam = context.get(SolCam.class);
Expand Down Expand Up @@ -192,6 +202,63 @@ 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.
* <p>
* {@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.
*
* @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
Expand Down Expand Up @@ -246,10 +313,43 @@ private boolean skipLooped(SolObject source, OggSound sound, float time) {
return false;
}

Map<OggSound, Float> looped = loopedSoundMap.get(source);
Map<OggSound, Float> 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.
* <p>
* 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<OggSound, Float> looped = loopedSoundMapOfEntities.get(source);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is all duplicated code. Can you really not unify this?

if (looped == null) {
looped = new HashMap<>();
loopedSoundMap.put(source, looped);
loopedSoundMapOfEntities.put(source, looped);
return false;
} else {
Float endTime = looped.get(sound);
Expand Down Expand Up @@ -293,14 +393,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.
* <p>
* (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());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe return an Optional instead then for these two methods?
UPDATE: In light of later review comments, you may not even want this method at all

*
* @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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this deserve to exist if hitSound(metal, dmgType) is already a thing?

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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
1 change: 1 addition & 0 deletions engine/src/main/java/org/destinationsol/game/SolGame.java
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ public SolGame(String shipName, boolean isTutorial, boolean isNewGame, CommonDra

//TODO this no longer needs to be instantiated in SolGame
soundManager = solApplication.getSoundManager();
context.put(OggSoundManager.class, soundManager);
SpecialSounds specialSounds = new SpecialSounds(soundManager);
context.put(SpecialSounds.class, specialSounds);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -35,8 +37,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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this would be appropriate as an overload so you don't have to send through arbitrary null values

myOwner = owner;
this.entity = entity;
this.position = new Vector2(position);
myMsgs = new HashMap<>();
}
Expand Down Expand Up @@ -66,6 +71,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<Map.Entry<String, Long>> it = myMsgs.entrySet().iterator();
Expand Down
Loading