How the Undertale Changer Template Audio System Manages Sound Effects and Music

The Undertale Changer Template implements a dual-layer audio architecture that separates background music handling through a persistent singleton AudioSource from short sound-effect playback via an object-pooled component system, all routed through Unity's AudioMixer for dynamic group control.

The arch-aik/undertale-changer-template repository provides a purpose-built audio subsystem designed for RPG-style games. This system distinguishes between long-running background tracks and transient sound effects while maintaining clean separation of concerns through ScriptableObject configuration and singleton pattern management.

Core Audio Architecture Components

The architecture relies on five interconnected components that separate configuration data from runtime execution.

AudioControl ScriptableObject

In Assets/Scripts/UCT/Control/AudioControl.cs, the AudioControl class stores the global AudioMixer asset and organizes sound effects into three functional lists. The important members include globalAudioMixer, fxClipUI, fxClipBattle, and fxClipWalk. This ScriptableObject acts as the central configuration hub that runtime controllers reference for mixer groups and clip libraries.

AudioController Singleton

Assets/Scripts/UCT/Audio/AudioController.cs implements the primary runtime coordinator as a singleton pattern. It owns the main BGM AudioSource attached to its own GameObject and manages an object pool of AudioPlayer components for FX playback. Key methods include PlayFx(...), PlayFxInternal(...), and GetClipFromCharacterSpriteManager(...). The singleton instance is accessible via AudioController.Instance.

AudioPlayer Pool Objects

Assets/Scripts/UCT/Audio/AudioPlayer.cs defines a lightweight MonoBehaviour that attaches to pooled GameObjects. Each instance contains its own AudioSource and exposes a Playing(AudioClip) method to start playback. The Update() method monitors clip completion and automatically returns the object to the controller's pool via AudioController.Instance.ReturnPool(gameObject, this).

MusicData Assets

Assets/Scripts/UCT/Control/MusicData.cs defines the ScriptableObject structure for music tracks. Each asset contains an AudioClip (clip) alongside metadata fields: musicDataName, authorDataName, informationDataName, and cover. These assets populate the music library loaded by the UI controller.

MusicRoomController UI Manager

Assets/Scripts/UCT/Scene/MusicRoomController.cs handles user-facing music selection and playback control. It exposes methods like SpawnMusicData() to load resources from Resources/Audios, SetMusic(bool) to toggle playback state, and SetMusicProgressUI to synchronize visual feedback with playback position.

How Sound Effects Are Managed

The FX pipeline uses object pooling to minimize garbage collection during gameplay.

Request and Validation

Any script initiates playback by calling AudioController.Instance.PlayFx(index, clipList). The method validates the index against the provided List<AudioClip> bounds and ensures the clip exists before proceeding.

Mixer Group Routing

If the caller does not specify an AudioMixerGroup, PlayFxInternal inspects the calling context against the three FX lists defined in AudioControl. It determines the appropriate channel—"FX/UI", "FX/Walk", or "FX/Battle"—and looks up the matching group in globalAudioMixer.FindMatchingGroups(groupName)[0].

Pool Lifecycle Execution

The controller retrieves a pooled AudioPlayer via GetFromPool<AudioPlayer>(), configures its AudioSource with the selected mixer group, volume, and pitch, then invokes Playing(clip). The AudioPlayer component monitors playback in its Update() loop and self-recycles when audioSource.isPlaying becomes false.

Example: Playing a UI click sound

// Play the first UI FX stored in the AudioControl asset
AudioController.Instance.PlayFx(0, MainControl.Instance.AudioControl.fxClipUI);

Example: Random footstep sound

int idx = UnityEngine.Random.Range(0, MainControl.Instance.AudioControl.fxClipWalk.Count);
AudioController.Instance.PlayFx(idx, MainControl.Instance.AudioControl.fxClipWalk);

How Background Music Is Managed

BGM handling differs fundamentally from FX by using a single persistent source rather than pooled objects.

BGM Source Initialization

In AudioController.Start(), the script creates a dedicated AudioSource on the same GameObject and forces it to the "BGM" mixer group:

audioSource.outputAudioMixerGroup =
    MainControl.Instance.AudioControl.globalAudioMixer.FindMatchingGroups("BGM")[0];

Asset Loading and Selection

MusicRoomController.SpawnMusicData() loads all MusicData assets from Resources/Audios into the musicData list. When the player selects a track via UI input, the controller updates currentMusicDataIndex and prepares the clip for playback.

Playback Control

The controller assigns the selected clip to the singleton source and manages transport state:

var audioSrc = AudioController.Instance.audioSource;
audioSrc.clip = MusicRoomController.Instance.musicData[musicDataIndex].clip;
audioSrc.Play();  // Begins BGM playback

For pausing and resuming, scripts can check audioSrc.isPlaying and call Pause() or Play() accordingly. Seeking is achieved by modifying audioSrc.time within the bounds of audioSrc.clip.length.

UI Synchronization

MusicRoomController.SetMusicProgressUI reads audioSource.time and audioSource.clip.length each frame to calculate normalized progress. It updates a material property (_progressBar.material.SetFloat(Crop, normalized)) and repositions a point sprite to reflect the current playback position.

Summary

  • Configuration Layer: AudioControl stores the global mixer and categorizes FX into UI, battle, and walk lists.
  • FX Pipeline: AudioController.PlayFx() routes requests through object-pooled AudioPlayer components with automatic mixer group selection and recycle-on-completion behavior.
  • BGM Pipeline: A single persistent AudioSource managed by the AudioController singleton handles music playback, driven by MusicData assets and controlled through MusicRoomController.
  • Mixer Routing: All audio routes through Unity's AudioMixer using named groups ("BGM", "FX/UI", "FX/Walk", "FX/Battle") for independent volume and effect control.
  • Resource Management: Sound effects use object pooling to prevent runtime allocation, while music tracks load as MusicData ScriptableObjects from the Resources/Audios folder.

Frequently Asked Questions

How do I add a new sound effect category beyond UI, battle, and walk?

Create a new List<AudioClip> field in AudioControl.cs following the pattern of fxClipUI. Then modify AudioController.PlayFxInternal() to check against your new list and route to a corresponding mixer group (e.g., "FX/Ambient") looked up via globalAudioMixer.FindMatchingGroups().

Why does the audio system use object pooling for sound effects instead of creating AudioSources on demand?

The AudioPlayer pool prevents runtime garbage collection stutter during combat or rapid UI interactions. By recycling GameObjects with pre-configured AudioSource components, the system maintains consistent performance without allocating new Unity engine objects per sound trigger.

Can I play multiple background music tracks simultaneously?

The current architecture supports only one BGM source via AudioController.Instance.audioSource. To implement layered music (e.g., ambient plus combat layers), you would need to extend AudioController to manage additional persistent AudioSource instances, each routed through the mixer with independent volume control.

How do I programmatically seek to a specific timestamp in a music track?

Access the singleton audio source and set the time property directly, clamping to the clip duration:

var src = AudioController.Instance.audioSource;
src.time = Mathf.Clamp(targetTime, 0, src.clip.length);

The MusicRoomController demonstrates this pattern in its input handling for fast-forward and rewind functionality.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →