With modulation ranges, AudioSource pooling, playing at world coordinates, and even playing while following a Transform
Version information: Tested in Unity 2018.3, requires scripting runtime set to '.Net 4.x equivalent' (uses C# 7 features)
My take on a light-weight sound manager for Unity3D. Admittedly I haven't looked into any implementations on the Asset Store, etc., because I didn't want to spoil the fun of writing mine from scratch. So this is just a small, relatively simple component with solid core features that should perform reliably and efficiently.
I've been using it in my project for quite some time, and I'm a satisfied customer, so to speak, so I decided to share it.
Pro tip: It's totally not spaghetti code. ;) I tried to generally separate the responsibilities, but without creating a confusing amount of classes/files, so I pretty much nested everything into a single class. I think it's a relatively maintainable/extensible design, so you shouldn't have a very hard time adding new features if you wanted. (I also have some ideas for additional features, but those would require some more book-keeping, plus classes where nesting them is not really justifiable.)
Note that the comments in the code are a bit excessive if you're an experienced developer. I just tried to help others too to understand it better.
Update1: I added the capability of Transform tracking playback in the form of two new public methods. This means that the SoundManager
can now be used for moving objects too. For this addition I refactored the class internally. Short testing shows this new feature to be working well, but please inform me of any bugs.
Update2: I encapsulated all debug log messages into a separate class, and added a logging setting to the Inspector. So you can turn off logging for deployed builds in a way that completely avoids string operations and allocations (since all debug messages are constructed inside this helper class). Available logging settings: None
, LogOnlyInEditor
and LogAlways
.
Update3: I swear I'll stop adding these update sections here. :) Anyhow, I refactored the SoundManager
class again. Plus I changed the public method signatures. The most visible change is that the methods now return the sound type in the callback, so you can route multiple playback finished notifications into a single method, and check there which one was finished. In terms of refactoring, internally the class changed a lot; basically I encapsulated all AudioSource
handling responsibilities into a separate nested class, and cut out all code duplication in the method overloads.
Pretty much everybody uses some sort of audio or sound manager, from what I'm aware of. But if you're not sure what's the point:
-
- If you have an
AudioSource
on aGameObject
, and you destroy it (or ideally, disable for releasing into the pool), you can't play any sounds on it, since sound playback stops instantly.
- If you have an
-
- Sounds sound the best if you slightly modulate the pitch and volume each time you play them, to make them feel natural. It's messy to do this individually everywhere.
-
- You can set up the range of random pitch and volume for each audioclip at a centralized location. Then you just simply play the sound by invoking
PlaySound()
, and the pitch and volume will be automatically modulated each time.
- You can set up the range of random pitch and volume for each audioclip at a centralized location. Then you just simply play the sound by invoking
-
- One given sound type can have multiple entries and
AudioClip
s associated to it in the list. TheSoundManager
automatically creates a list internally from all 'sound variations' of a given sound type, and when you invokePlaySound()
, it selects one randomly. This is also a rather important part of providing rich and diverse audio experiences.
- One given sound type can have multiple entries and
-
- You can define how many simultaneous sounds you want to support at startup. When you invoke
PlaySound()
, theSoundManager
automatically reserves anAudioSource
from the pool to play the requested sound, waits for the playback to finish, and puts theAudioSource
back to the pool. No polling involved whatsoever. Coroutine-based operation. No wasteful use of collections; it uses a simpleStack
the way it's supposed to be used. If it runs out of availableAudioSource
s, it can grow the pool on-demand (if you enable this feature).
- You can define how many simultaneous sounds you want to support at startup. When you invoke
-
-
My current game is 2D, so I have no use for this, but I wanted to add this little extra before sharing it. Basically, you can simply use the
PlaySoundPositioned()
method that accepts aVector3
position defining where to play the sound. So, for example:SoundManager.Instance.PlaySoundPositioned(GameSound.Death, transform.position)
-
When the playback is complete, the
AudioSource
will be instantly put back to its original position. There is no expensive reparenting involved; theSoundManager
simply creates aGameObject
for eachAudioSource
in the pool at startup, so it can later position them anywhere.
-
-
-
After some pondering I decided to add this too. This means being able to use the
SoundManager
in cases when you need a movingAudioSource
(for a movingGameObject
). I added two new overloads of thePlaySoundPositioned()
method which take aTransform
, and they follow the position of this transform for the entire duration of the playback (and then jump back to origin). Invoking these methods are similarly simple, for example:SoundManager.Instance.PlaySoundPositioned(GameSound.RocketLaunch, transform)
-
-
- There is an overload of the
PlaySound()
andPlaySoundPositioned()
methods that accept two floats which serve as multipliers to pitch and volume. So if you find yourself wanting to play a faster/slower or louder/quieter sound than normal, or play it reverse by using a negative pitch, you can. These multipliers are applied on top of the already randomized pitch and volume, so the sound variation is kept intact.
- There is an overload of the
-
- All versions of the
PlaySound()
andPlaySoundPositioned()
methods accept an optionalcallback
parameter, in case you want to be notified when the playback finishes. - Additionally, all methods return the
AudioSource
playing your requested sound, so you can monitor it yourself if you want. But don't mess with the playback settings on the returnedAudioSource
, because then theSoundManager
won't be able to predict the end time of the playback (to release theAudioSource
to the pool). (It does have built-in safety mechanism for additional waiting, though.) - If the playback fails for whatever reason, the return value is
null
, so you can actually check if playback was successful.
- All versions of the
-
- I added standard XML documentation tags to all public methods, so Visual Studio's IntelliSense can help you understand what do the methods and parameters do.
- Also, the code contains lots of comments, including on all private methods and everywhere where something might not be obvious. I think I went a bit overboard, because I know that many Unity3D users are not that well-versed in programming.
-
- There are a lot of checks internally for various error cases, and they log intelligible warning messages to the console. Of course you might want to strip out this debug logging, integrate some switchable or injected logging system, or whatever.
-
- I tried to avoid allocations and losing performance for no good reason. The code properly uses a
Dictionary
for lookups andLists
for indexed access, basically doesn'tnew
up and throw away anything, etc. - One thing you might want to look into is the infamous allocation when you use
enum
asDictionary
keys. I have no idea if this still happens these days; if so, you can supposedly avoid it by providing a custom comparer for theenum
.
- I tried to avoid allocations and losing performance for no good reason. The code properly uses a
Just for illustrative purposes, because it's really obvious and straightforward to use.
-
Playing a 'sound type' the simplest way possible (no 3D positioning, perfect for mobile games):
SoundManager.Instance.PlaySound(GameSound.Rocket);
-
Playing a 'sound type' at a given world position:
SoundManager.Instance.PlaySoundPositioned(GameSound.Rocket, transform.position);
-
Playing a 'sound type' at a given world position, by overriding pitch and volume with a multiplier (obviously you can leave out the parameter names :) ):
SoundManager.Instance.PlaySoundPositioned(GameSound.Rocket, volumeMultiplier: 0.5f, pitchMultiplier: 0.6f, transform.position);
-
Playing a 'sound type' at a given world position, by overriding pitch and volume with a multiplier, and setting a callback to be invoked when the sound playback finishes (where DoAfterPlayback is a void parameterless method):
SoundManager.Instance.PlaySoundPositioned(GameSound.Rocket, volumeMultiplier: 0.5f, pitchMultiplier: 0.6f, transform.position, DoAfterPlayback);
(The new
Transform
tracking playback is similarly simple, so I'm not including those examples.)
(Note that the callback shown is obviously available as an optional parameter on all of the method overloads, not just on the longest. Also note that there is an overload for setting pitch and volume override for the simple PlaySound()
method too.)
-
Specify your
sound types
by editing theGameSound
enum. This means that all the sounds you want to be able to play need to be named in this enum. This obviously has some drawbacks, for example you can't just nilly-willy delete values from the enum and shift the rest of the values, because Unity actually saves the enum values as aninteger
. But using the enums is very comfortable, so I personally like this approach. If you want, you can look into associating explicit integer values to your enum values, and then it's safer to modify it. -
Add the
SoundManager
script as a component to aGameObject
. This hardly needs an explanation, if you have ever seen a computer before. -
Add entries to the
SoundManager
component'sSound List
array in Inspector. Change the 'None' sound type to an actual sound type, select yourAudioClip
, and set your pitch and volume ranges. Note that - obviously - Unity stores everything you add here with the given instance of the component, so you'd probably want to make a prefab from it, and use that in your scenes, or set it not to destroy on load.(I have a
ScriptableObject
-based architecture that decouples configuration data from components, and possibly I'll convert mySoundManager
to use that, but I felt it would complicate matters too much if I included it here. I intended to share this just as a simple but powerful component.) -
You're ready to call
SoundManager
'sPlaySound()
andPlaySoundPositioned()
methods.(Note that
SoundManager
, as it is provided here, uses a very simple singleton implementation that exposes a static instance of itself, calledInstance
. This instance is created when the class is first instantiated, and any further instantiation (which shouldn't happen to begin with) will destroy itself. You can just change this to however you prefer to access your service components. For example you can just inject the SoundManager as a dependency into your classes through their constructor. Just kidding, since you can't use constructors at all in your Unity MonoBehaviour classes. ;P )
- There is no Editor customization provided. The display of arrays in the Unity Inspector is hideous, so I did include some trickery (with platform-specific compile directives, restricting it to the Unity Editor) to at least replace the default
'Element'
names with the value of the enums. But if you use some generic/universal Editor script package to prettify arrays, possibly even this is superfluous. - One functionality that would be nice to have is a 'Play' button right besides the sound settings, so you could listen to how does the AudioClip sound with the given ranges. Maybe I'll look into implementing this.
Let me know if you happen to find any bugs, or spot any sort of weirdness with the code. I'm coming from normal .Net development, so I can't rule out the possibility that I'm unaware of some weirdness in how Unity handles lifecycle of objects, coroutines, or who knows what.
Since it seems some people are using this, and actually someone asked me earlier about the licence... I added a permissive MIT licence for your peace of mind.