Ambient sound system (#4552)
* Ambient sound system Client-side system that plays audio from nearby objects that are randomly sampled. * Decent * Tweaks * Tweaks * Comment this out for now * reduce VM sound * Fix rolloff * Fixes * Volume tweak
This commit is contained in:
178
Content.Client/Audio/AmbientSoundSystem.cs
Normal file
178
Content.Client/Audio/AmbientSoundSystem.cs
Normal file
@@ -0,0 +1,178 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Client.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// Samples nearby <see cref="AmbientSoundComponent"/> and plays audio.
|
||||
/// </summary>
|
||||
public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
|
||||
{
|
||||
[Dependency] private IEntityLookup _lookup = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
|
||||
private int _maxAmbientCount;
|
||||
|
||||
private float _maxAmbientRange;
|
||||
private float _cooldown;
|
||||
private float _accumulator;
|
||||
|
||||
/// <summary>
|
||||
/// How many times we can be playing 1 particular sound at once.
|
||||
/// </summary>
|
||||
private int _maxSingleSound = 3;
|
||||
|
||||
private Dictionary<AmbientSoundComponent, (IPlayingAudioStream? Stream, string Sound)> _playingSounds = new();
|
||||
|
||||
private const float RangeBuffer = 0.5f;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
var configManager = IoCManager.Resolve<IConfigurationManager>();
|
||||
configManager.OnValueChanged(CCVars.AmbientCooldown, SetCooldown, true);
|
||||
configManager.OnValueChanged(CCVars.MaxAmbientSources, SetAmbientCount, true);
|
||||
configManager.OnValueChanged(CCVars.AmbientRange, SetAmbientRange, true);
|
||||
}
|
||||
|
||||
private void SetCooldown(float value) => _cooldown = value;
|
||||
private void SetAmbientCount(int value) => _maxAmbientCount = value;
|
||||
private void SetAmbientRange(float value) => _maxAmbientRange = value;
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
var configManager = IoCManager.Resolve<IConfigurationManager>();
|
||||
configManager.UnsubValueChanged(CCVars.AmbientCooldown, SetCooldown);
|
||||
configManager.UnsubValueChanged(CCVars.MaxAmbientSources, SetAmbientCount);
|
||||
configManager.UnsubValueChanged(CCVars.AmbientRange, SetAmbientRange);
|
||||
}
|
||||
|
||||
private int PlayingCount(string countSound)
|
||||
{
|
||||
var count = 0;
|
||||
|
||||
foreach (var (_, (_, sound)) in _playingSounds)
|
||||
{
|
||||
if (sound.Equals(countSound)) count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
if (_cooldown <= 0f)
|
||||
{
|
||||
_accumulator = 0f;
|
||||
return;
|
||||
}
|
||||
|
||||
_accumulator += frameTime;
|
||||
if (_accumulator < _cooldown) return;
|
||||
_accumulator -= _cooldown;
|
||||
|
||||
var player = _playerManager.LocalPlayer?.ControlledEntity;
|
||||
if (player == null)
|
||||
{
|
||||
ClearSounds();
|
||||
return;
|
||||
}
|
||||
|
||||
var coordinates = player.Transform.Coordinates;
|
||||
|
||||
foreach (var (comp, (stream, _)) in _playingSounds.ToArray())
|
||||
{
|
||||
if (!comp.Deleted && comp.Enabled && comp.Owner.Transform.Coordinates.TryDistance(EntityManager, coordinates, out var range) &&
|
||||
range <= comp.Range)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
stream?.Stop();
|
||||
|
||||
_playingSounds.Remove(comp);
|
||||
}
|
||||
|
||||
if (_playingSounds.Count >= _maxAmbientCount) return;
|
||||
|
||||
SampleNearby(coordinates);
|
||||
}
|
||||
|
||||
private void ClearSounds()
|
||||
{
|
||||
foreach (var (_, (stream, _)) in _playingSounds)
|
||||
{
|
||||
stream?.Stop();
|
||||
}
|
||||
|
||||
_playingSounds.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of ambient components in range and determine which ones to start playing.
|
||||
/// </summary>
|
||||
private void SampleNearby(EntityCoordinates coordinates)
|
||||
{
|
||||
var compsInRange = new List<AmbientSoundComponent>();
|
||||
|
||||
foreach (var entity in _lookup.GetEntitiesInRange(coordinates, _maxAmbientRange,
|
||||
LookupFlags.Approximate | LookupFlags.IncludeAnchored))
|
||||
{
|
||||
if (!entity.TryGetComponent(out AmbientSoundComponent? ambientComp) ||
|
||||
_playingSounds.ContainsKey(ambientComp) ||
|
||||
!ambientComp.Enabled ||
|
||||
// We'll also do this crude distance check because it's what we're doing in the active loop above.
|
||||
!entity.Transform.Coordinates.TryDistance(EntityManager, coordinates, out var range) ||
|
||||
range > ambientComp.Range - RangeBuffer)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
compsInRange.Add(ambientComp);
|
||||
}
|
||||
|
||||
while (_playingSounds.Count < _maxAmbientCount)
|
||||
{
|
||||
if (compsInRange.Count == 0) break;
|
||||
|
||||
var comp = _random.PickAndTake(compsInRange);
|
||||
var sound = comp.Sound.GetSound();
|
||||
|
||||
if (PlayingCount(sound) >= _maxSingleSound) continue;
|
||||
|
||||
var audioParams = AudioHelpers
|
||||
.WithVariation(0.01f)
|
||||
.WithVolume(comp.Volume)
|
||||
.WithLoop(true)
|
||||
.WithAttenuation(Attenuation.LinearDistance)
|
||||
// Randomise start so 2 sources don't increase their volume.
|
||||
.WithPlayOffset(_random.NextFloat())
|
||||
.WithMaxDistance(comp.Range);
|
||||
|
||||
var stream = SoundSystem.Play(
|
||||
Filter.Local(),
|
||||
sound,
|
||||
comp.Owner,
|
||||
audioParams);
|
||||
|
||||
if (stream == null) continue;
|
||||
|
||||
_playingSounds[comp] = (stream, sound);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,8 +29,8 @@ namespace Content.Client.Audio
|
||||
|
||||
private SoundCollectionPrototype _ambientCollection = default!;
|
||||
|
||||
private AudioParams _ambientParams = new(-10f, 1, "Master", 0, 0, true, 0f);
|
||||
private AudioParams _lobbyParams = new(-5f, 1, "Master", 0, 0, true, 0f);
|
||||
private AudioParams _ambientParams = new(-10f, 1, "Master", 0, 0, 0, true, 0f);
|
||||
private AudioParams _lobbyParams = new(-5f, 1, "Master", 0, 0, 0, true, 0f);
|
||||
|
||||
private IPlayingAudioStream? _ambientStream;
|
||||
private IPlayingAudioStream? _lobbyStream;
|
||||
|
||||
@@ -272,7 +272,8 @@ namespace Content.Client.Entry
|
||||
"Advertise",
|
||||
"PowerNetworkBattery",
|
||||
"BatteryCharger",
|
||||
"SpawnItemsOnUse"
|
||||
"SpawnItemsOnUse",
|
||||
"AmbientOnPowered",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
14
Content.Server/Audio/AmbientOnPoweredComponent.cs
Normal file
14
Content.Server/Audio/AmbientOnPoweredComponent.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Content.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Server.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// Toggles <see cref="AmbientSoundComponent"/> on when powered and off when not powered.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public class AmbientOnPoweredComponent : Component
|
||||
{
|
||||
public override string Name => "AmbientOnPowered";
|
||||
}
|
||||
}
|
||||
23
Content.Server/Audio/AmbientSoundSystem.cs
Normal file
23
Content.Server/Audio/AmbientSoundSystem.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Server.Audio
|
||||
{
|
||||
public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<AmbientOnPoweredComponent, PowerChangedEvent>(HandlePowerChange);
|
||||
}
|
||||
|
||||
private void HandlePowerChange(EntityUid uid, AmbientOnPoweredComponent component, PowerChangedEvent args)
|
||||
{
|
||||
if (!ComponentManager.TryGetComponent<AmbientSoundComponent>(uid, out var ambientSound)) return;
|
||||
if (ambientSound.Enabled == args.Powered) return;
|
||||
ambientSound.Enabled = args.Powered;
|
||||
ambientSound.Dirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.UserInterface;
|
||||
using Content.Shared.Acts;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.Gravity;
|
||||
using Content.Shared.Interaction;
|
||||
using Robust.Server.GameObjects;
|
||||
@@ -137,6 +138,7 @@ namespace Content.Server.Gravity
|
||||
private void MakeBroken()
|
||||
{
|
||||
_status = GravityGeneratorStatus.Broken;
|
||||
EntitySystem.Get<SharedAmbientSoundSystem>().SetAmbience(Owner.Uid, false);
|
||||
|
||||
_appearance?.SetData(GravityGeneratorVisuals.State, Status);
|
||||
_appearance?.SetData(GravityGeneratorVisuals.CoreVisible, false);
|
||||
@@ -145,6 +147,7 @@ namespace Content.Server.Gravity
|
||||
private void MakeUnpowered()
|
||||
{
|
||||
_status = GravityGeneratorStatus.Unpowered;
|
||||
EntitySystem.Get<SharedAmbientSoundSystem>().SetAmbience(Owner.Uid, false);
|
||||
|
||||
_appearance?.SetData(GravityGeneratorVisuals.State, Status);
|
||||
_appearance?.SetData(GravityGeneratorVisuals.CoreVisible, false);
|
||||
@@ -153,6 +156,7 @@ namespace Content.Server.Gravity
|
||||
private void MakeOff()
|
||||
{
|
||||
_status = GravityGeneratorStatus.Off;
|
||||
EntitySystem.Get<SharedAmbientSoundSystem>().SetAmbience(Owner.Uid, false);
|
||||
|
||||
_appearance?.SetData(GravityGeneratorVisuals.State, Status);
|
||||
_appearance?.SetData(GravityGeneratorVisuals.CoreVisible, false);
|
||||
@@ -161,6 +165,7 @@ namespace Content.Server.Gravity
|
||||
private void MakeOn()
|
||||
{
|
||||
_status = GravityGeneratorStatus.On;
|
||||
EntitySystem.Get<SharedAmbientSoundSystem>().SetAmbience(Owner.Uid, true);
|
||||
|
||||
_appearance?.SetData(GravityGeneratorVisuals.State, Status);
|
||||
_appearance?.SetData(GravityGeneratorVisuals.CoreVisible, true);
|
||||
|
||||
@@ -4,6 +4,7 @@ using Content.Server.Hands.Components;
|
||||
using Content.Server.Items;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Temperature.Components;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Interaction;
|
||||
@@ -226,7 +227,7 @@ namespace Content.Server.Light.Components
|
||||
|
||||
if (LightBulb == null) // No light bulb.
|
||||
{
|
||||
_currentLit = false;
|
||||
SetLight(false);
|
||||
powerReceiver.Load = 0;
|
||||
_appearance?.SetData(PoweredLightVisuals.BulbState, PoweredLightState.Empty);
|
||||
return;
|
||||
@@ -267,6 +268,7 @@ namespace Content.Server.Light.Components
|
||||
private void SetLight(bool value, Color? color = null)
|
||||
{
|
||||
_currentLit = value;
|
||||
EntitySystem.Get<SharedAmbientSoundSystem>().SetAmbience(Owner.Uid, value);
|
||||
|
||||
if (!Owner.TryGetComponent(out PointLightComponent? pointLight)) return;
|
||||
pointLight.Enabled = value;
|
||||
@@ -327,6 +329,6 @@ namespace Content.Server.Light.Components
|
||||
UpdateLight();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
46
Content.Shared/Audio/AmbientSoundComponent.cs
Normal file
46
Content.Shared/Audio/AmbientSoundComponent.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using Content.Shared.Sound;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Shared.Audio
|
||||
{
|
||||
[RegisterComponent]
|
||||
[NetworkedComponent]
|
||||
public sealed class AmbientSoundComponent : Component
|
||||
{
|
||||
public override string Name => "AmbientSound";
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("enabled")]
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
[DataField("sound")]
|
||||
public SoundSpecifier Sound = default!;
|
||||
|
||||
/// <summary>
|
||||
/// How far away this ambient sound can potentially be heard.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("range")]
|
||||
public float Range = 2f;
|
||||
|
||||
/// <summary>
|
||||
/// Applies this volume to the sound being played.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("volume")]
|
||||
public float Volume = -10f;
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class AmbientSoundComponentState : ComponentState
|
||||
{
|
||||
public bool Enabled { get; init; }
|
||||
public float Range { get; init; }
|
||||
public float Volume { get; init; }
|
||||
}
|
||||
}
|
||||
45
Content.Shared/Audio/SharedAmbientSoundSystem.cs
Normal file
45
Content.Shared/Audio/SharedAmbientSoundSystem.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Audio
|
||||
{
|
||||
public abstract class SharedAmbientSoundSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<AmbientSoundComponent, ComponentGetState>(GetCompState);
|
||||
SubscribeLocalEvent<AmbientSoundComponent, ComponentHandleState>(HandleCompState);
|
||||
}
|
||||
|
||||
public void SetAmbience(EntityUid uid, bool value)
|
||||
{
|
||||
// Reason I didn't make this eventbus for the callers is because it seemed a bit silly
|
||||
// trying to account for damageable + powered + toggle, plus we can't just check if it's powered.
|
||||
// So we'll just call it directly for whatever.
|
||||
if (!ComponentManager.TryGetComponent<AmbientSoundComponent>(uid, out var ambience) ||
|
||||
ambience.Enabled == value) return;
|
||||
|
||||
ambience.Enabled = value;
|
||||
ambience.Dirty();
|
||||
}
|
||||
|
||||
private void HandleCompState(EntityUid uid, AmbientSoundComponent component, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is not AmbientSoundComponentState state) return;
|
||||
component.Enabled = state.Enabled;
|
||||
component.Range = state.Range;
|
||||
component.Volume = state.Volume;
|
||||
}
|
||||
|
||||
private void GetCompState(EntityUid uid, AmbientSoundComponent component, ref ComponentGetState args)
|
||||
{
|
||||
args.State = new AmbientSoundComponentState
|
||||
{
|
||||
Enabled = component.Enabled,
|
||||
Range = component.Range,
|
||||
Volume = component.Volume,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,22 @@ namespace Content.Shared.CCVar
|
||||
[CVarDefs]
|
||||
public sealed class CCVars : CVars
|
||||
{
|
||||
/*
|
||||
* Ambience
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// How long we'll wait until re-sampling nearby objects for ambience.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> AmbientCooldown =
|
||||
CVarDef.Create("ambience.cooldown", 0.5f, CVar.REPLICATED | CVar.SERVER);
|
||||
|
||||
public static readonly CVarDef<float> AmbientRange =
|
||||
CVarDef.Create("ambience.range", 5f, CVar.REPLICATED | CVar.SERVER);
|
||||
|
||||
public static readonly CVarDef<int> MaxAmbientSources =
|
||||
CVarDef.Create("ambience.max_sounds", 6, CVar.REPLICATED | CVar.SERVER);
|
||||
|
||||
/*
|
||||
* Status
|
||||
*/
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace Content.Shared.Light.Component
|
||||
[NetworkedComponent]
|
||||
public abstract class SharedExpendableLightComponent: Robust.Shared.GameObjects.Component
|
||||
{
|
||||
public static readonly AudioParams LoopedSoundParams = new(0, 1, "Master", 62.5f, 1, true, 0.3f);
|
||||
public static readonly AudioParams LoopedSoundParams = new(0, 1, "Master", 62.5f, 1, 1, true, 0.3f);
|
||||
|
||||
public sealed override string Name => "ExpendableLight";
|
||||
|
||||
|
||||
BIN
Resources/Audio/Ambience/Objects/buzzing.ogg
Normal file
BIN
Resources/Audio/Ambience/Objects/buzzing.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Ambience/Objects/emf_buzz.ogg
Normal file
BIN
Resources/Audio/Ambience/Objects/emf_buzz.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Ambience/Objects/engine_hum.ogg
Normal file
BIN
Resources/Audio/Ambience/Objects/engine_hum.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Ambience/Objects/gravity_gen_hum.ogg
Normal file
BIN
Resources/Audio/Ambience/Objects/gravity_gen_hum.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Ambience/Objects/hdd_buzz.ogg
Normal file
BIN
Resources/Audio/Ambience/Objects/hdd_buzz.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Ambience/Objects/light_hum.ogg
Normal file
BIN
Resources/Audio/Ambience/Objects/light_hum.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Ambience/Objects/periodic_beep.ogg
Normal file
BIN
Resources/Audio/Ambience/Objects/periodic_beep.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Ambience/Objects/vending_machine_hum.ogg
Normal file
BIN
Resources/Audio/Ambience/Objects/vending_machine_hum.ogg
Normal file
Binary file not shown.
@@ -6,6 +6,10 @@
|
||||
placement:
|
||||
mode: AlignTileAny
|
||||
components:
|
||||
- type: AmbientSound
|
||||
range: 7
|
||||
sound:
|
||||
path: /Audio/Ambience/Objects/gravity_gen_hum.ogg
|
||||
- type: Sprite
|
||||
netsync: false
|
||||
sprite: Structures/Machines/gravity_generator.rsi
|
||||
|
||||
@@ -5,6 +5,12 @@
|
||||
description: Just add capitalism!
|
||||
abstract: true
|
||||
components:
|
||||
- type: AmbientOnPowered
|
||||
- type: AmbientSound
|
||||
volume: -15
|
||||
range: 3
|
||||
sound:
|
||||
path: /Audio/Ambience/Objects/vending_machine_hum.ogg
|
||||
- type: Sprite
|
||||
sprite: Structures/Machines/VendingMachines/empty.rsi
|
||||
netsync: false
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
placement:
|
||||
mode: SnapgridCenter
|
||||
components:
|
||||
#- type: AmbientSound
|
||||
# range: 5
|
||||
# sound:
|
||||
# path: /Audio/Ambience/Objects/engine_hum.ogg
|
||||
- type: Clickable
|
||||
- type: InteractionOutline
|
||||
- type: Physics
|
||||
|
||||
@@ -46,6 +46,9 @@
|
||||
placement:
|
||||
mode: SnapgridCenter
|
||||
components:
|
||||
- type: AmbientSound
|
||||
sound:
|
||||
path: /Audio/Ambience/Objects/hdd_buzz.ogg
|
||||
- type: Clickable
|
||||
- type: AccessReader
|
||||
access: [["Engineering"]]
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
placement:
|
||||
mode: SnapgridCenter
|
||||
components:
|
||||
- type: AmbientSound
|
||||
range: 3
|
||||
sound:
|
||||
path: /Audio/Ambience/Objects/periodic_beep.ogg
|
||||
- type: Sprite
|
||||
netsync: false
|
||||
sprite: Structures/Power/smes.rsi
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
description: "An unpowered light."
|
||||
suffix: Unpowered
|
||||
components:
|
||||
- type: AmbientSound
|
||||
volume: -12
|
||||
sound:
|
||||
path: /Audio/Ambience/Objects/light_hum.ogg
|
||||
- type: Clickable
|
||||
- type: InteractionOutline
|
||||
- type: Construction
|
||||
|
||||
Reference in New Issue
Block a user