I am trying to add sfx to my UI using USS. For example, I want all buttons to make a sound when clicked, my menu to make a sound when I hover over them, play a whoosh sound when windows fly in, etc.
So I added this in USS.
.unity-button:active {
--audio: URL("program://....")
}
then I created a manipulator like this:
public class AudioManipulator : Manipulator
{
internal static readonly CustomStyleProperty<AudioClip> k_audioProperty = new("--audio");
private readonly AudioSource audioSource;
private bool played = false;
public AudioManipulator(AudioSource audioSource)
{
this.audioSource = audioSource;
}
protected override void RegisterCallbacksOnTarget()
{
target.RegisterCallback<CustomStyleResolvedEvent>(OnCustomStyleResolved);
}
protected override void UnregisterCallbacksFromTarget()
{
target.UnregisterCallback<CustomStyleResolvedEvent>(OnCustomStyleResolved);
}
private void OnCustomStyleResolved(CustomStyleResolvedEvent evt)
{
if (evt.customStyle.TryGetValue(k_audioProperty, out var audio))
{
if (audio != null) {
if (!played)
{
audioSource.PlayOneShot(audio);
// make sure we only play once when
// the property is set.
played = true;
}
} else {
played = false;
}
}
else
{
played = false;
}
}
}
Lastly, I have to add that manipulator to everything that could play audio. My current approach is adding a monobehavior in the gameobject with the UI document like this:
[RequireComponent(typeof(AudioSource))]
[RequireComponent(typeof(UIDocument))]
public class AudioManager : MonoBehaviour
{
public void OnEnable() {
var uiDocument = GetComponent<UIDocument>();
var audioSource = GetComponent<AudioSource>();
if (uiDocument == null || audioSource == null) {
Debug.LogError("AudioManager requires UIDocument and AudioSource components");
return;
}
// add a manipulator to everything that has audio custom property
uiDocument.rootVisualElement.Query().ForEach((element) => {
element.AddManipulator(new AudioManipulator(audioSource));
});
}
}
This works reasonably well:
I can hear a sound when I click on any button.
I don’t have to change every button, radio, tab, dropdown, window, etc in my uxml documents. I can simply add sfx as part of their style in a global theme.
I can extend this to add delays, loops, etc if I want to, without changing all my uxml documents.
However, I have some challenges:
- It allocates a Manipulator for every visual element in the hierarchy. It seems wasteful. It would be great to limit this Manipulator to only things that have --audio. I can’t check if the element has the property, since they are not calculated until later.
- It does not work with visual elements that are added dynamically, since they are added after the manager has added all manipulators. I would prefer not to have to sprinkle code everywhere where I do dynamic content, I see it as a cross-cutting concern. Maybe there is some way to track when the tree changes?
Does anyone have suggestions on how to address these problems?