Store ambient sound entities on a component tree. (#13110)

This commit is contained in:
Leon Friedrich
2022-12-30 17:10:14 +13:00
committed by GitHub
parent 860ff9ee38
commit ae58bb1f1b
6 changed files with 145 additions and 63 deletions

View File

@@ -3,8 +3,10 @@ using Content.Shared.CCVar;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using Robust.Client.Player; using Robust.Client.Player;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Log;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Robust.Shared.Utility; using Robust.Shared.Utility;
@@ -20,13 +22,16 @@ namespace Content.Client.Audio
/// </summary> /// </summary>
public sealed class AmbientSoundSystem : SharedAmbientSoundSystem public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
{ {
[Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly AmbientSoundTreeSystem _treeSys = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
protected override void QueueUpdate(EntityUid uid, AmbientSoundComponent ambience)
=> _treeSys.QueueTreeUpdate(uid, ambience);
private AmbientSoundOverlay? _overlay; private AmbientSoundOverlay? _overlay;
private int _maxAmbientCount; private int _maxAmbientCount;
private bool _overlayEnabled; private bool _overlayEnabled;
@@ -35,13 +40,6 @@ namespace Content.Client.Audio
private TimeSpan _targetTime = TimeSpan.Zero; private TimeSpan _targetTime = TimeSpan.Zero;
private float _ambienceVolume = 0.0f; private float _ambienceVolume = 0.0f;
// Note that except for some rare exceptions, every ambient sound source appears to be static. So:
// TODO AMBIENT SOUND Use only static queries
// This would make all the lookups significantly faster. There are some rare exceptions, like flies, vehicles,
// and the singularity. But those can just play sounds via some other system. Alternatively: give ambient sound
// its own client-side tree to avoid this issue altogether.
private static LookupFlags _flags = LookupFlags.Static | LookupFlags.Dynamic | LookupFlags.Sundries;
private static AudioParams _params = AudioParams.Default.WithVariation(0.01f).WithLoop(true).WithAttenuation(Attenuation.LinearDistance); private static AudioParams _params = AudioParams.Default.WithVariation(0.01f).WithLoop(true).WithAttenuation(Attenuation.LinearDistance);
/// <summary> /// <summary>
@@ -172,40 +170,48 @@ namespace Content.Client.Audio
_playingCount.Clear(); _playingCount.Clear();
} }
private Dictionary<string, List<(float Importance, AmbientSoundComponent)>> GetNearbySources(TransformComponent playerXform, MapCoordinates coords, EntityQuery<TransformComponent> xformQuery) private readonly struct QueryState
{ {
var sourceDict = new Dictionary<string, List<(float, AmbientSoundComponent)>>(16); public readonly Dictionary<string, List<(float Importance, AmbientSoundComponent)>> SourceDict = new();
var ambientQuery = GetEntityQuery<AmbientSoundComponent>(); public readonly Vector2 MapPos;
public readonly TransformComponent Player;
public readonly EntityQuery<TransformComponent> Query;
// TODO add variant of GetComponentsInRange that also returns the transform component. public QueryState(Vector2 mapPos, TransformComponent player, EntityQuery<TransformComponent> query)
foreach (var entity in _lookup.GetEntitiesInRange(coords, _maxAmbientRange, flags: _flags))
{ {
if (!ambientQuery.TryGetComponent(entity, out var ambientComp) || !ambientComp.Enabled) MapPos = mapPos;
continue; Player = player;
Query = query;
var xform = xformQuery.GetComponent(entity);
var delta = xform.ParentUid == playerXform.ParentUid
? xform.LocalPosition - playerXform.LocalPosition
: xform.WorldPosition - coords.Position;
var range = delta.Length;
if (range >= ambientComp.Range)
continue;
string key;
if (ambientComp.Sound is SoundPathSpecifier path)
key = path.Path?.ToString() ?? string.Empty;
else
key = ((SoundCollectionSpecifier) ambientComp.Sound).Collection ?? string.Empty;
var list = sourceDict.GetOrNew(key);
// Prioritize far away & loud sounds.
list.Add((range * (ambientComp.Volume + 32), ambientComp));
} }
}
return sourceDict; private static bool Callback(
ref QueryState state,
in ComponentTreeEntry<AmbientSoundComponent> value)
{
var (ambientComp, xform) = value;
DebugTools.Assert(ambientComp.Enabled);
var delta = xform.ParentUid == state.Player.ParentUid
? xform.LocalPosition - state.Player.LocalPosition
: xform.WorldPosition - state.MapPos;
var range = delta.Length;
if (range >= ambientComp.Range)
return true;
string key;
if (ambientComp.Sound is SoundPathSpecifier path)
key = path.Path?.ToString() ?? string.Empty;
else
key = ((SoundCollectionSpecifier) ambientComp.Sound).Collection ?? string.Empty;
// Prioritize far away & loud sounds.
var importance = range * (ambientComp.Volume + 32);
state.SourceDict.GetOrNew(key).Add((importance, ambientComp));
return true;
} }
/// <summary> /// <summary>
@@ -240,8 +246,13 @@ namespace Content.Client.Audio
if (_playingSounds.Count >= _maxAmbientCount) if (_playingSounds.Count >= _maxAmbientCount)
return; return;
var pos = mapPos.Position;
var state = new QueryState(pos, playerXform, query);
var worldAabb = new Box2(pos - _maxAmbientRange, pos + _maxAmbientRange);
_treeSys.QueryAabb(ref state, Callback, mapPos.MapId, worldAabb);
// Add in range ambiences // Add in range ambiences
foreach (var (key, sources) in GetNearbySources(playerXform, mapPos, query)) foreach (var (key, sources) in state.SourceDict)
{ {
if (_playingSounds.Count >= _maxAmbientCount) if (_playingSounds.Count >= _maxAmbientCount)
break; break;

View File

@@ -0,0 +1,14 @@
using Content.Shared.Audio;
using Robust.Shared.ComponentTrees;
using Robust.Shared.Physics;
namespace Content.Client.Audio;
/// <summary>
/// Samples nearby <see cref="AmbientSoundComponent"/> and plays audio.
/// </summary>
[RegisterComponent]
public sealed class AmbientSoundTreeComponent : Component, IComponentTreeComponent<AmbientSoundComponent>
{
public DynamicTree<ComponentTreeEntry<AmbientSoundComponent>> Tree { get; set; } = default!;
}

View File

@@ -0,0 +1,31 @@
using Content.Shared.Audio;
using Robust.Shared.ComponentTrees;
using Robust.Shared.Physics;
namespace Content.Client.Audio;
public sealed class AmbientSoundTreeSystem : ComponentTreeSystem<AmbientSoundTreeComponent, AmbientSoundComponent>
{
#region Component Tree Overrides
protected override bool DoFrameUpdate => false;
protected override bool DoTickUpdate => true;
protected override int InitialCapacity => 256;
protected override bool Recursive => true;
protected override Box2 ExtractAabb(in ComponentTreeEntry<AmbientSoundComponent> entry, Vector2 pos, Angle rot)
=> new Box2(pos - entry.Component.Range, pos + entry.Component.Range);
protected override Box2 ExtractAabb(in ComponentTreeEntry<AmbientSoundComponent> entry)
{
if (entry.Component.TreeUid == null)
return default;
var pos = XformSystem.GetRelativePosition(
entry.Transform,
entry.Component.TreeUid.Value,
GetEntityQuery<TransformComponent>());
return ExtractAabb(in entry, pos, default);
}
#endregion
}

View File

@@ -1,4 +1,4 @@
using System.Linq; using System.Linq;
using Content.Server.Construction; using Content.Server.Construction;
using Content.Server.MachineLinking.Components; using Content.Server.MachineLinking.Components;
using Content.Server.MachineLinking.Events; using Content.Server.MachineLinking.Events;
@@ -32,6 +32,7 @@ public sealed class ArtifactAnalyzerSystem : EntitySystem
{ {
[Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedAmbientSoundSystem _ambienntSound = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!; [Dependency] private readonly UserInterfaceSystem _ui = default!;
[Dependency] private readonly ArtifactSystem _artifact = default!; [Dependency] private readonly ArtifactSystem _artifact = default!;
@@ -475,11 +476,7 @@ public sealed class ArtifactAnalyzerSystem : EntitySystem
if (TryComp<ApcPowerReceiverComponent>(uid, out var powa)) if (TryComp<ApcPowerReceiverComponent>(uid, out var powa))
powa.NeedsPower = true; powa.NeedsPower = true;
if (TryComp<AmbientSoundComponent>(uid, out var ambientSound)) _ambienntSound.SetAmbience(uid, true);
{
ambientSound.Enabled = true;
Dirty(ambientSound);
}
} }
private void OnAnalyzeEnd(EntityUid uid, ActiveArtifactAnalyzerComponent component, ComponentShutdown args) private void OnAnalyzeEnd(EntityUid uid, ActiveArtifactAnalyzerComponent component, ComponentShutdown args)
@@ -487,11 +484,7 @@ public sealed class ArtifactAnalyzerSystem : EntitySystem
if (TryComp<ApcPowerReceiverComponent>(uid, out var powa)) if (TryComp<ApcPowerReceiverComponent>(uid, out var powa))
powa.NeedsPower = false; powa.NeedsPower = false;
if (TryComp<AmbientSoundComponent>(uid, out var ambientSound)) _ambienntSound.SetAmbience(uid, false);
{
ambientSound.Enabled = false;
Dirty(ambientSound);
}
} }
private void OnPowerChanged(EntityUid uid, ActiveArtifactAnalyzerComponent component, ref PowerChangedEvent args) private void OnPowerChanged(EntityUid uid, ActiveArtifactAnalyzerComponent component, ref PowerChangedEvent args)

View File

@@ -1,33 +1,44 @@
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.ComponentTrees;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Physics;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
namespace Content.Shared.Audio namespace Content.Shared.Audio
{ {
[RegisterComponent] [RegisterComponent]
[NetworkedComponent] [NetworkedComponent]
public sealed class AmbientSoundComponent : Component [Access(typeof(SharedAmbientSoundSystem))]
public sealed class AmbientSoundComponent : Component, IComponentTreeEntry<AmbientSoundComponent>
{ {
[ViewVariables(VVAccess.ReadWrite)]
[DataField("enabled")] [DataField("enabled")]
[ViewVariables(VVAccess.ReadWrite)] // only for map editing
public bool Enabled { get; set; } = true; public bool Enabled { get; set; } = true;
[DataField("sound", required: true), ViewVariables(VVAccess.ReadWrite)] [DataField("sound", required: true), ViewVariables(VVAccess.ReadWrite)] // only for map editing
public SoundSpecifier Sound = default!; public SoundSpecifier Sound = default!;
/// <summary> /// <summary>
/// How far away this ambient sound can potentially be heard. /// How far away this ambient sound can potentially be heard.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)] // only for map editing
[DataField("range")] [DataField("range")]
public float Range = 2f; public float Range = 2f;
/// <summary> /// <summary>
/// Applies this volume to the sound being played. /// Applies this volume to the sound being played.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)] // only for map editing
[DataField("volume")] [DataField("volume")]
public float Volume = -10f; public float Volume = -10f;
public EntityUid? TreeUid { get; set; }
public DynamicTree<ComponentTreeEntry<AmbientSoundComponent>>? Tree { get; set; }
public bool AddToTree => Enabled;
public bool TreeUpdateQueued { get; set; }
} }
[Serializable, NetSerializable] [Serializable, NetSerializable]

View File

@@ -11,24 +11,46 @@ namespace Content.Shared.Audio
SubscribeLocalEvent<AmbientSoundComponent, ComponentHandleState>(HandleCompState); SubscribeLocalEvent<AmbientSoundComponent, ComponentHandleState>(HandleCompState);
} }
public void SetAmbience(EntityUid uid, bool value) public virtual void SetAmbience(EntityUid uid, bool value, AmbientSoundComponent? ambience = null)
{ {
// Reason I didn't make this eventbus for the callers is because it seemed a bit silly if (!Resolve(uid, ref ambience, false) || ambience.Enabled == value)
// trying to account for damageable + powered + toggle, plus we can't just check if it's powered. return;
// So we'll just call it directly for whatever.
if (!EntityManager.TryGetComponent<AmbientSoundComponent>(uid, out var ambience) ||
ambience.Enabled == value) return;
ambience.Enabled = value; ambience.Enabled = value;
QueueUpdate(uid, ambience);
Dirty(ambience);
}
public virtual void SetRange(EntityUid uid, float value, AmbientSoundComponent? ambience = null)
{
if (!Resolve(uid, ref ambience, false) || MathHelper.CloseToPercent(ambience.Range, value))
return;
ambience.Range = value;
QueueUpdate(uid, ambience);
Dirty(ambience);
}
protected virtual void QueueUpdate(EntityUid uid, AmbientSoundComponent ambience)
{
// client side tree
}
public virtual void SetVolume(EntityUid uid, float value, AmbientSoundComponent? ambience = null)
{
if (!Resolve(uid, ref ambience, false) || MathHelper.CloseToPercent(ambience.Volume, value))
return;
ambience.Volume = value;
Dirty(ambience); Dirty(ambience);
} }
private void HandleCompState(EntityUid uid, AmbientSoundComponent component, ref ComponentHandleState args) private void HandleCompState(EntityUid uid, AmbientSoundComponent component, ref ComponentHandleState args)
{ {
if (args.Current is not AmbientSoundComponentState state) return; if (args.Current is not AmbientSoundComponentState state) return;
component.Enabled = state.Enabled; SetAmbience(uid, state.Enabled, component);
component.Range = state.Range; SetRange(uid, state.Range, component);
component.Volume = state.Volume; SetVolume(uid, state.Volume, component);
} }
private void GetCompState(EntityUid uid, AmbientSoundComponent component, ref ComponentGetState args) private void GetCompState(EntityUid uid, AmbientSoundComponent component, ref ComponentGetState args)