uss audio

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:

  1. 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.
  2. 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?

1 Like