Introduction
Welcome to the documentation for Audio Dispatcher for DOTS.
Triggering standard Unity AudioSource components from pure ECS is notoriously difficult because managed objects cannot be accessed inside Burst-compiled jobs.
Audio Dispatcher solves this problem elegantly. It is a production-ready, Data-Oriented audio bridging system that allows your high-frequency ECS systems to trigger, move, and stop sounds without ever touching the Managed Heap or stalling the Main Thread.
Key Features
- ⚡ 100% Burst Compatible: Trigger sounds directly from
IJobEntityorISystem. - 🚀 Zero Main Thread Stalls: Uses a Double Buffering Command Architecture.
- 🗑️ Zero GC Allocations: Pre-allocated object pools for One-Shot and Looping sounds.
- 🧠 Smart Voice Stealing: Automatically replaces the quietest/furthest sound when pools are full.
- 🔗 Shadow Tracking: Looping sounds follow entities without modifying your core archetypes.
- 💎 Fluent API: Clean, readable, and chainable syntax for triggering sounds.
- 🛠️ Stable Hash IDs: Auto-generates C# constants using stable hashes. Reordering your audio database will never break your code.
Let's get started!
Installation
Prerequisites
Ensure your project is using Unity 2022.3 LTS or higher and has the following packages installed:
com.unity.entities(1.0.0+)com.unity.burstcom.unity.collections
Method 1: Unity Asset Store (Recommended)
- Open your project in Unity.
- Go to Window > Package Manager.
- Select My Assets from the dropdown menu.
- Search for Audio Dispatcher, click Download, and then Import.
Method 2: Install via Git URL
Open Window > Package Manager.
Click the + button in the top-left corner and select Add package from git URL...
Paste the following URL and click Add:
https://github.com/sniveler-code/com.snivelercode.audio-dispatcher
Quick Start
Get your first sound playing in 4 simple steps.
1. Create an Audio Database
Right-click in your Project view and select Create > SnivelerCode > Audio Database. Add an AudioClip (e.g., an explosion sound) to the list.
2. Generate IDs
Select your new Audio Database asset. In the Inspector, click the green "Generate C# Constants" button. This creates a file containing your Audio IDs.
3. Scene Setup
Create an empty GameObject in your sub-scene. Add the AudioSettingsAuthoring component to it and assign your Audio Database asset.
4. Play a Sound from Code
Use the Fluent API inside any Burst-compiled system:
using SnivelerCode.AudioDispatcher.Runtime;
using Unity.Burst;
using Unity.Entities;
[BurstCompile]
public partial struct CombatSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
// 1. Get the Audio Writer
var audioSingleton = SystemAPI.GetSingleton<NativeAudioSystem.Singleton>();
state.Dependency = new CombatJob
{
AudioWriter = audioSingleton.Writer
}.ScheduleParallel(state.Dependency);
}
}
[BurstCompile]
public partial struct CombatJob : IJobEntity
{
public NativeQueue<AudioEvent>.ParallelWriter AudioWriter;
private void Execute(in LocalTransform transform)
{
// 2. Play the sound!
AudioIDs.EXPLOSION.Shot(transform.Position).Apply(AudioWriter);
}
}
Audio Database & IDs
The AudioDatabase ScriptableObject is the heart of your audio configuration.
Pool Settings
- Pool Size: The maximum number of One-Shot sounds (explosions, gunshots) that can play simultaneously.
- Loop Pool Size: The maximum number of Looping sounds (engines, auras) that can play simultaneously.
💡 Tip: All
AudioSourceGameObjects are instantiated and pooled at startup. There are zeroInstantiateorDestroycalls during gameplay.
Sound Definitions
For each sound, you can configure:
- Mixer Group: Route audio to UI, SFX, or Music buses.
- Spatial Blend: Set to
0for 2D sounds (UI) or1for 3D spatialized sounds. - Min/Max Distance & Rolloff: Standard Unity 3D audio settings.
Stable Hash IDs
Instead of using strings (which allocate memory) or Enums (which break if you reorder them), Audio Dispatcher uses Stable Hash IDs.
When you click "Generate C# Constants", the system hashes the name of your AudioClip (e.g., math.hash("Explosion")) and generates a static class:
public static class AudioIDs
{
public const int EXPLOSION = -211360833;
public const int SHOT = 188032901;
}
Playing Sounds (Fluent API)
Audio Dispatcher features a zero-allocation Fluent Builder API. It makes writing audio code inside Jobs incredibly clean and readable.
The Syntax
Every generated Audio ID gets extension methods. You start by calling .Shot() or .Loop(), chain your modifiers, and finish with .Apply(writer).
Basic One-Shot
AudioIDs.SHOT.Shot(transform.Position).Apply(AudioWriter);
Modifying Volume and Pitch
You can chain modifiers to randomize sounds. This is fully Burst-compatible:
AudioIDs.EXPLOSION.Shot(transform.Position)
.Volume(0.8f)
.Pitch(random.NextFloat(0.9f, 1.1f))
.Apply(AudioWriter);
How it works under the hood
The Fluent API does not allocate any classes. It builds a 32-byte AudioEvent struct on the stack and pushes it directly into the lock-free NativeQueue.
Looping Sounds (Shadow Tracking)
Playing a looping sound (like a car engine) in ECS is tricky because the sound needs to follow the moving entity, and it needs to stop when the entity is destroyed.
Audio Dispatcher handles this automatically using a technique called Shadow Tracking.
How to start a Loop
Instead of passing a float3 Position, you pass the Entity itself using the .Loop() method:
AudioIDs.ENGINE_LOOP.Loop(entity)
.Volume(0.5f)
.Apply(AudioWriter);
What happens next?
- No Archetype Changes: The system does not add any components to your vehicle entity. Your gameplay data remains tightly packed in memory.
- Shadow Entity: The Audio Dispatcher creates a hidden "Shadow Entity" that tracks your vehicle using a fast
ComponentLookup<LocalTransform>. - Automatic Movement: The
AudioSourcewill automatically update its position every frame to match your vehicle. - Automatic Cleanup: If your vehicle entity is destroyed (e.g.,
ecb.DestroyEntity(entity)), the Shadow Tracker detects it, stops theAudioSource, returns it to the pool, and destroys itself. You don't need to write any cleanup code!
Architecture & Performance
Audio Dispatcher was built with strict Data-Oriented Design (DOD) principles to ensure it scales to AAA workloads.
1. Double Buffering (No Main Thread Stalls)
In a naive implementation, the Main Thread must wait for ECS Jobs to finish before it can read the audio queue (Dependency.Complete()). This causes massive frame drops.
Audio Dispatcher uses Double Buffering:
- Frame 1: Your Jobs write to
Queue A. The Main Thread reads fromQueue B(which is empty). - Frame 2: Your Jobs write to
Queue B. The Main Thread reads fromQueue A. Because the Main Thread is always reading the queue from the previous frame, it never has to wait for worker threads. The 1-frame audio delay (~16ms) is imperceptible to players.
2. 32-Byte Cache Alignment
The AudioEvent struct uses [StructLayout(LayoutKind.Explicit)] to overlap the Entity (used for loops) and float3 Position (used for one-shots) in memory.
This compresses the struct to exactly 32 bytes. This is a magic number in CPU architecture: exactly two events fit perfectly into a standard 64-byte L1 CPU cache line, resulting in lightning-fast queue processing.
3. Priority-Based Voice Stealing
What happens if you have a pool of 32 sounds, but 40 explosions happen at once? Instead of a naive Round-Robin approach, the Burst-compiled AudioBrainJob evaluates the priority (Volume) of all currently playing sounds. It will automatically find the quietest sound and overwrite it with the new explosion.
4. TimeScale Independent
The system calculates sound lifetimes using AudioSettings.dspTime instead of Time.time. If you use slow-motion effects (Time.timeScale = 0.1f), your sounds will not be cut off prematurely.