Your footsteps cut each other off, the explosion sound stops when the enemy is destroyed, and when ten coins are collected at once you hear one ding. These are all symptoms of playing sounds from the wrong place. This guide builds a Unity audio manager that plays sound effects without cutting off: PlayOneShot for overlapping sounds, a pool of AudioSources for 3D sounds, random pitch and volume, and ScriptableObject sound definitions so designers can tune everything without code.
Why Sounds Get Cut Off
An AudioSource plays one clip at a time with Play(). Three common problems follow from that:
- Calling Play() again restarts it. Rapid footsteps or gunshots on the same source interrupt each other.
- Destroying or disabling the object stops its sound. An enemy that plays its death sound and is destroyed on the same frame is silent.
- Changing
clipon a playing source stops the old clip.
The solution is to separate “what makes the sound” from “what plays it”. Gameplay objects ask a manager to play a sound; the manager owns the AudioSources.
PlayOneShot vs Play
audioSource.PlayOneShot(clip, volumeScale) plays a clip on top of whatever the source is already playing. Several one-shots can overlap on one source. That makes it perfect for 2D sound effects (UI clicks, pickups, hits in a 2D game).
Its limits:
- You can’t stop one specific one-shot.
Stop()stops everything on the source. - All one-shots share the source’s position, pitch and spatial settings. Changing
pitchbefore each call changes the pitch of ones still playing too.
So: one-shots for non-positional sound, a pool of sources for positional 3D sounds or when you need per-sound pitch.
Sound Definitions as ScriptableObjects
Instead of passing raw AudioClips around, describe each sound once:
using UnityEngine;
using UnityEngine.Audio;
[CreateAssetMenu(menuName = "Audio/Sound")]
public class SoundDef : ScriptableObject
{
public AudioClip[] clips;
[Range(0f, 1f)] public float volume = 1f;
public Vector2 pitchRange = new Vector2(0.95f, 1.05f);
public bool spatial;
public AudioMixerGroup output;
[Tooltip("Minimum seconds between plays. 0 = no limit.")]
public float cooldown = 0.03f;
[System.NonSerialized] public float lastPlayed = -999f;
public AudioClip Pick() => clips.Length == 0 ? null : clips[Random.Range(0, clips.Length)];
public float PickPitch() => Random.Range(pitchRange.x, pitchRange.y);
}
Several clips with random choice plus a little random pitch make repeated sounds (footsteps, hits) far less tiring. Designers tune volume and variation on the asset without touching code. If ScriptableObjects are new to you, the ScriptableObjects guide explains them.
The Audio Manager
using System.Collections.Generic;
using UnityEngine;
public class AudioManager : MonoBehaviour
{
public static AudioManager Instance { get; private set; }
[SerializeField] AudioSource twoDSource; // spatialBlend 0, used for one-shots
[SerializeField] int poolSize = 16;
readonly List<AudioSource> pool = new();
int next;
void Awake()
{
if (Instance != null) { Destroy(gameObject); return; }
Instance = this;
DontDestroyOnLoad(gameObject);
for (int i = 0; i < poolSize; i++)
{
var go = new GameObject("SFX " + i);
go.transform.SetParent(transform);
var src = go.AddComponent<AudioSource>();
src.playOnAwake = false;
src.spatialBlend = 1f;
pool.Add(src);
}
}
public void Play(SoundDef s) => Play(s, Vector3.zero);
public void Play(SoundDef s, Vector3 position)
{
if (s == null || !CanPlay(s)) return;
var clip = s.Pick();
if (clip == null) return;
if (!s.spatial)
{
twoDSource.outputAudioMixerGroup = s.output;
twoDSource.PlayOneShot(clip, s.volume);
return;
}
var src = NextSource();
src.transform.position = position;
src.clip = clip;
src.volume = s.volume;
src.pitch = s.PickPitch();
src.outputAudioMixerGroup = s.output;
src.Play();
}
bool CanPlay(SoundDef s)
{
if (Time.unscaledTime - s.lastPlayed < s.cooldown) return false;
s.lastPlayed = Time.unscaledTime;
return true;
}
AudioSource NextSource()
{
for (int i = 0; i < pool.Count; i++)
{
var src = pool[(next + i) % pool.Count];
if (!src.isPlaying) { next = (next + i + 1) % pool.Count; return src; }
}
var steal = pool[next]; // all busy: reuse the oldest
next = (next + 1) % pool.Count;
return steal;
}
}
Usage from anywhere:
[SerializeField] SoundDef hitSound;
void OnHit(Vector3 point) => AudioManager.Instance.Play(hitSound, point);
Notes:
- The 2D one-shot path doesn’t randomise pitch, since changing the shared source’s pitch affects every overlapping one-shot. Mark sounds that need pitch variation as spatial, or add a second small pool with
spatialBlend = 0. - The manager survives scene loads with
DontDestroyOnLoad, so sounds started just before a scene change still finish. - Setting
outputAudioMixerGroupon the shared 2D source per call is fine for one-shots that use the same group. If your 2D sounds use different groups (UI vs SFX), give each group its own 2D source.
Pooled 3D Sources
The pool gives each 3D sound its own position and pitch. Tune the pooled sources’ 3D settings once, in the loop that creates them:
- Rolloff mode: Logarithmic sounds natural but falls off fast; Linear with a sensible Max Distance is easier to control in games.
- Min/Max Distance: full volume inside Min, silent beyond Max (for Linear).
- Doppler Level: set to 0 unless you want pitch shifting on fast objects, which often sounds odd.
The 3D spatial audio guide covers these settings in depth. For a moving sound that must follow its object (an engine, a burning torch), use an AudioSource on that object instead; pooled sources stay where they were played.
Limiting Spam
When 30 coins are collected in one frame, playing 30 identical sounds is loud and muddy. The cooldown on each SoundDef ignores repeats within a short window. 0.03 to 0.05 seconds is enough to stop stacking while still feeling responsive. For things like rapid-fire weapons, set it to 0 and rely on the pool size.
Also consider Unity’s own limit: Project Settings > Audio > Max Real Voices (default 32). Beyond that, quieter voices are virtualised. Give important sounds a higher Priority (lower number) on their source so they aren’t the ones dropped.
Routing Through the Audio Mixer
Assign every SoundDef an AudioMixerGroup like SFX, UI or Voice. That gives you separate volume sliders, ducking music under dialogue, and effects like low-pass when paused. The Audio Mixer volume slider guide shows how to expose the group volumes and save them.
Music should not go through this manager’s pool. Use a dedicated music player with two sources for crossfades.
🔧 Sound packs and audio tools
Good sound effects lift a game more than almost anything else for the money. Check our pick of Asset Store tools and packs worth buying.
Troubleshooting
Sound stops when the object is destroyed
The AudioSource was on that object. Play through the manager, or use AudioSource.PlayClipAtPoint.
Rapid sounds cut each other off
You’re calling Play() on one source. Use PlayOneShot or a pooled source per sound.
3D sound is silent
The listener is beyond Max Distance, or Spatial Blend is 1 with a very short Min Distance. Check there’s exactly one AudioListener, usually on the camera.
“There are 2 audio listeners in the scene”
Remove the extra AudioListener, often on a second camera.
Sounds keep playing during pause
Set AudioListener.pause = true when pausing, and mark UI sources with ignoreListenerPause = true.
FAQ
How many pooled sources do I need?
16 to 32 covers most indie games. If you hear sounds being stolen, increase it.
Is AudioSource.PlayClipAtPoint good enough?
For a small game, yes. It creates and destroys an object per call and can’t route to a mixer group, so a pool is better as the game grows.
Should I use FMOD or Wwise instead?
They’re great for complex audio and adaptive music. For most small games, Unity’s built-in audio plus a manager like this is enough.
How do I stop a specific pooled sound?
Return the AudioSource from Play and keep a reference, then call Stop on it. Check it hasn’t been reused first.
Can the manager be a ScriptableObject?
A ScriptableObject can’t own AudioSources in the scene, but it can be the API that forwards to a scene component.
Reader questions
Asked by readers of this article and answered by AI. Behavior can differ between Unity versions, so test in yours.




Leave a Reply