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

Let's get started!


Installation

Prerequisites

Ensure your project is using Unity 2022.3 LTS or higher and has the following packages installed:

Method 1: Unity Asset Store (Recommended)

  1. Open your project in Unity.
  2. Go to Window > Package Manager.
  3. Select My Assets from the dropdown menu.
  4. Search for Audio Dispatcher, click Download, and then Import.

Method 2: Install via Git URL

  1. Open Window > Package Manager.

  2. Click the + button in the top-left corner and select Add package from git URL...

  3. 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

💡 Tip: All AudioSource GameObjects are instantiated and pooled at startup. There are zero Instantiate or Destroy calls during gameplay.

Sound Definitions

For each sound, you can configure:

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?

  1. No Archetype Changes: The system does not add any components to your vehicle entity. Your gameplay data remains tightly packed in memory.
  2. Shadow Entity: The Audio Dispatcher creates a hidden "Shadow Entity" that tracks your vehicle using a fast ComponentLookup<LocalTransform>.
  3. Automatic Movement: The AudioSource will automatically update its position every frame to match your vehicle.
  4. Automatic Cleanup: If your vehicle entity is destroyed (e.g., ecb.DestroyEntity(entity)), the Shadow Tracker detects it, stops the AudioSource, 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:

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.


Edit on GitHub v1.0.0