DEV505 / CODE RECIPE
Compare Stop and Detach.
Destroy a temporary sound owner and hear whether its departure cue ends or stays at the last position.
Set up the scene
Section titled “Set up the scene”Start with Your first sound if you have not set up a sound scene before. Use a separate test scene with one enabled Sound Manager, generated Sound Keys, and one Audio Listener near the sound object. Add PortalEcho and PortalCut from 09-Portals to the manager's Direct events list; these are the supplied .soundgraph assets under Assets/DEV505/SoundManager/Samples.
Save the complete code below as PortalOwnerComparison.cs in Assets/Scripts. Wait for Unity to compile it, create an empty GameObject, and drag the script onto it. The filename must match the class name. Test one recipe component at a time so another demo controller does not compete for the same events.
No Inspector references are needed. Place the component a few units to one side of the listener. It creates its own temporary sound owner for each comparison.
In Play Mode, wait until the Sound Manager is ready. To run a listed test action, open the component's three-dot menu in the Inspector and choose that action. These are the script's ContextMenu methods. Your own code or a Unity UI Button can call its public methods too. Inspector edits made during Play Mode are temporary.
Try the component
Section titled “Try the component”Choose Try Detach. After 0.35 seconds, the temporary owner object is destroyed, but the echo should keep sounding from its original location. Let it finish. Choose Try Stop: the same recording should end when that owner is destroyed. Disable the component during an echo to check that final cleanup also reaches detached plays.
The test coroutine uses a short real-time wait only to destroy the object. It is not the source of the echo’s playback or audio delay. The graph defines the sound, and the event’s Owner loss setting decides what happens after destruction.
Follow target and owner
Section titled “Follow target and owner”For each comparison, the script creates a GameObject, gets an owner token for it, and supplies both to Following. OwnerOf binds lifetime to the GameObject; Following reads its moving position while it exists. PortalEcho has Detach, while PortalCut has Stop.
On loss, Detach keeps the last known position and allows the play to finish. It does not keep the GameObject alive or move the echo to the listener. Stop ends the play according to the event’s stopping behavior.
Do not stop the sound before testing owner loss
Section titled “Do not stop the sound before testing owner loss”Destroying the rig is the action under test. Calling StopOwned first explicitly ends those plays regardless of the intended comparison. The script deliberately leaves that call out when removing the rig.
Final cleanup is different. OnDisable stops every retained handle, including any detached plays, then removes remaining rigs and stops their coroutines. The full Portals demo follows this pattern while also switching world ambience and moving the player. It all takes place within one Unity scene.
Complete component
Section titled “Complete component”using System.Collections;using System.Collections.Generic;using DEV505.SoundManager;using UnityEngine;
public sealed class PortalOwnerComparison : MonoBehaviour{ private readonly List<GameObject> rigs = new(); private readonly List<SoundHandle> plays = new(); public SoundHandle LastDeparture { get; private set; }
[ContextMenu("Try Detach")] public void TryDetach() => Begin(true); [ContextMenu("Try Stop")] public void TryStop() => Begin(false);
private void Begin(bool detach) { if (!Application.isPlaying || !isActiveAndEnabled) return; var service = SoundServices.Current; if (service == null || !service.Ready) return; rigs.RemoveAll(rig => !rig); plays.RemoveAll(play => !play.IsAlive()); var rig = new GameObject("Temporary portal sound owner"); rigs.Add(rig); rig.transform.position = transform.position; var context = SoundPlayContext.Following(rig.transform, service.OwnerOf(rig)); var result = SoundBus.Play(detach ? Sounds.PortalEcho : Sounds.PortalCut, context); if (!result.Accepted) { Debug.LogWarning($"Departure could not play: {result.Status}", this); Destroy(rig); return; } LastDeparture = result.Handle; plays.Add(result.Handle); StartCoroutine(RemoveOwner(rig)); }
private IEnumerator RemoveOwner(GameObject rig) { yield return new WaitForSecondsRealtime(0.35f); // Deliberately no StopOwned: owner loss must apply this event's Stop or Detach policy. if (rig) Destroy(rig); }
private void OnDisable() { StopAllCoroutines(); foreach (var play in plays) SoundBus.Stop(play, 0); // Includes detached plays. foreach (var rig in rigs) if (rig) Destroy(rig); plays.Clear(); rigs.Clear(); LastDeparture = default; }}Keep exploring
Section titled “Keep exploring”Read the demo walkthrough for the graph, parameters, controls, and Unity screenshots. The related API reference explains the calls used here. Return to all twelve recipes to choose another interaction.
This page imports the checked C# file directly. It was compiled against the documented Unity revision; its runtime checks and source hash are recorded in the documentation repository’s validation files.