Audio
DOTS Survivors supports two independent audio types, music and sound effects, each with adjustable volume settings. This setup uses a simple audio mixer that can be found in the Assets\Settings directory. Each audio category is assigned an audio mixer group to control the volume. While the music group uses default settings, the sound effects group uses a "compressor" effect to normalize the volume when many sound effects are played simultaneously. The base audio mixer has exposed properties for the volume level of each mixer group.
Each scene has an audio controller MonoBehaviour with a direct reference to the AudioMixer, which can be used to set the volume levels for the individual mixer groups. This allows players to set music and SFX volume levels independently.
public class GameAudioController : MonoBehaviour
{
[SerializeField] private AudioMixer _audioMixer;
private const string MUSIC_VOLUME_PARAMETER = "MusicVolume";
public void SetMusicVolume(float newVolume)
{
_audioMixer.SetFloat(MUSIC_VOLUME_PARAMETER, newVolume);
}
}
Sound Effects
As Unity ECS does not have a native audio solution, audio clips still need to play on audio sources that live on GameObjects. At a high level, this is achieved by pooling many GameObjects with audio sources that will play audio clips stored in UnityObjectRef<AudioClip> on a data component. Pooling is used to efficiently manage audio playback by reusing GameObjects rather than constantly creating and destroying them.
There are three types of audio sources that get pooled in the GameAudioController:
- Regular SFX Audio Source - used for one-shot audio clips that should play once
- Looping SFX Audio Source - used for effects that will play looping until the source entity is destroyed
- Pause Resistant Audio Source - plays one-shot audio clips that are not affected by game pause
The pooling consists of spawning many new GameObjects with an audio source component with values set relevant to the audio source type. These GameObjects are then stored in an array. To play an audio clip, the GameAudioController has public methods for each of the audio source types that take an audio clip as a parameter. When called, the next available audio source in that pool will play the audio clip. Keeping track of the next available audio source is done with an incrementing index that will loop to 0 once it exceeds the pool length. The other parameter of these methods is a priority value that is used by Unity to determine which audio clips have a higher priority to play.
From the ECS side, audio clips are stored in UnityObjectRef<AudioClip> in data components and are authored via authoring scripts. Here is an example of a data component that stores an audio clip to play when the entity spawns:
public struct PlayAudioClipOnSpawnData : IComponentData, IEnableableComponent
{
public byte Priority;
public UnityObjectRef<AudioClip> AudioClip;
}
Then here is what would be called to play the audio clip when the entity is spawned:
foreach (var (audioClip, audioClipEnabled) in SystemAPI
.Query<PlayAudioClipOnSpawnData, EnabledRefRW<PlayAudioClipOnSpawnData>>()
.WithNone<ShouldLoopAudioClipTag>())
{
GameAudioController.Instance.PlaySfxAudioClip(audioClip.AudioClip.Value, audioClip.Priority);
audioClipEnabled.ValueRW = false;
}
Note that PlayAudioClipOnSpawnData is an enableable component, so it can be disabled once the audio clip has begun playing.
Looping Sound Effects
Looping sound effects require some additional considerations as their audio isn't "one and done" and they should play through the duration of the entity's lifecycle. To accomplish this I have the return value of GameAudioController.PlayLoopingAudioClip be the index of the audio source in the looping audio clip pool. That index value is then stored in a cleanup component - this type of data component will exist after the entity is destroyed and can be used to perform additional cleanup logic, in this case it will stop the audio clip. Here is what that component looks like:
public struct LoopingAudioSourceIndex : ICleanupComponentData
{
public int AudioClipIndex;
}
For more information on Cleanup Components, see the Unity documentation: Cleanup components introduction | Entities | 1.3.9
Any entity that should play a looping audio clip will have the ShouldLoopAudioClipTag component, which will match the query of a foreach like this:
foreach (var (audioClip, audioClipEnabled, entity) in SystemAPI
.Query<PlayAudioClipOnSpawnData, EnabledRefRW<PlayAudioClipOnSpawnData>>()
.WithAll<ShouldLoopAudioClipTag>().WithNone<LoopingAudioSourceIndex>()
.WithEntityAccess())
{
var audioClipIndex = GameAudioController.Instance.PlayLoopingAudioClip(audioClip.AudioClip.Value, audioClip.Priority);
audioClipEnabled.ValueRW = false;
ecb.AddComponent(entity, new LoopingAudioSourceIndex { AudioClipIndex = audioClipIndex });
}
Then when the entity is destroyed, the ShouldLoopAudioClipTag will no longer exist, however the LoopingAudioSourceIndex component will still exist as it is a special cleanup component. Thus the entity will match this foreach:
foreach (var (loopAudioClip, entity) in SystemAPI
.Query<LoopingAudioSourceIndex>()
.WithNone<ShouldLoopAudioClipTag>()
.WithEntityAccess())
{
GameAudioController.Instance.StopLoopingAudioClip(loopAudioClip.AudioClipIndex);
ecb.RemoveComponent<LoopingAudioSourceIndex>(entity);
}
Pausing Game Audio
When the game pauses, sound effects should also pause and then resume when the game resumes. This is accomplished by iterating all of the audio sources and checking if they are currently playing audio. If they are, pause the audio source and add its index to a list of paused indices. When the game resumes, use the list of paused indices to resume only those audio sources.
The pause resistant sound effects work similarly to the regular, non-looping sound effects, however this pool is not iterated during pause/resume. These types of sound effects are used in rare instances where a sound effect should be played when the game pauses, such as when picking up a loot crate or triggering the game over screen.
Related: Gameplay Pausing
Video Overview
YouTube Tutorial
Over on my YouTube Channel I recently posted a video tutorial where I showcase the step by step process of adding audio sound effects to a game made with Unity ECS. Posting this video here as it is a relevant and helpful resource.