Guidance / Cookbook
Music Systems
Recipe: Background Music (Intro + Loop)
📋 What: Music with intro that transitions to seamless loop
🎯 Use Case: Level music, menu music
Asset Setup
1. Create Containers:
Intro:
- Routing Container: “Music_MainTheme_Intro_RC”
- Audio Clips: [Intro clip]
- Loop: ☐ (no loop)
- Volume: 1.0
- Is3D: ☐ (2D)
Loop:
- Routing Container: “Music_MainTheme_Loop_RC”
- Audio Clips: [Loop clip]
- Loop: ✓ (loop enabled)
- Volume: 1.0
- Is3D: ☐ (2D)
2. Create Event:
- Event: “Music_Start_MainTheme”
- Priority: Medium
- Max Instances: 1
- Actions → Size: 2
Action [0]:
- Type: Play
- Container: Music_MainTheme_Intro_RC
- Target Bus: Music_Bus
- Delay: 0
Action [1]:
- Type: Play
- Container: Music_MainTheme_Loop_RC
- Target Bus: Music_Bus
- Delay: 8.5 ← SET TO YOUR INTRO LENGTH IN SECONDS
3. Stopping it: keep the handle Post returns and call Stop on it (see the code below). Don’t make a separate “Music_Stop” event with a Stop action: a Stop action only stops sounds its own event started, so it can’t stop this music.
Code
using UnityEngine;
using AudioSystem;
public class MusicManager : MonoBehaviour
{
[SerializeField] private AudioEvent startMusicEvent;
[SerializeField] private bool playOnStart = true;
private AudioHandle musicHandle;
void Start()
{
if (playOnStart)
{
StartMusic();
}
}
public void StartMusic()
{
if (startMusicEvent != null)
{
musicHandle = startMusicEvent.Post();
}
}
public void StopMusic(float fadeTime = 2f)
{
if (musicHandle != null)
{
// Also cancels the loop if the intro is still playing.
musicHandle.Stop(fadeTime);
musicHandle = null;
}
}
void OnDestroy()
{
StopMusic(1f);
}
}
Setup:
- Create empty GameObject: “MusicManager”
- Attach MusicManager script
- Assign the start event
- Mark as DontDestroyOnLoad if needed
✅ Result: Intro plays once, then the loop takes over and repeats.
💡 How close is the join? A delayed action waits on a frame timer, so the loop starts within about one frame after the intro ends (≈16 ms at 60 fps; more if that frame is slow). With the event’s Time on Scaled (the default), slow motion or a pause also stretches the wait, so set Time = Unscaled (§5.4) for music. On a hard musical cut that small gap can be heard. For a sample-accurate join, make two events (intro and loop, each with Delay 0) and start both on the audio clock yourself:
[SerializeField] private AudioEvent introEvent, loopEvent;
[SerializeField] private AudioClip introClip; // the intro's clip, for its exact length
private AudioHandle introHandle, loopHandle;
public void StartMusic()
{
double start = AudioSettings.dspTime + 0.1; // a little ahead, so both are scheduled in time
double introLength = (double)introClip.samples / introClip.frequency; // exact, in seconds
introHandle = introEvent.Post(gameObject, transform.position, start);
loopHandle = loopEvent.Post(gameObject, transform.position, start + introLength);
}
// To stop, Stop both handles.
This needs the intro’s container to have no pitch randomization (a pitch change changes its length).
💡 Switching tracks: keep the handle Post returns and crossfade from it: musicHandle = AudioManager.Instance.CrossFade(musicHandle, nextTrackEvent, 2f);. That also cancels the delayed loop if the intro is still playing. A container crossfade from the intro would let the loop start anyway (see §10.3).
Recipe: Dynamic Combat Music (3 Layers)
📋 What: Music that responds to combat intensity with layered crossfading
🎯 Use Case: Action games, dynamic gameplay music
Asset Setup
1. Create Layer Containers:
Ambient Layer:
- Routing Container: “Music_Ambient_RC”
- Clip: Calm ambient music loop
- Loop: ✓
- Volume: 1.0
Tension Layer:
- Routing Container: “Music_Tension_RC”
- Clip: Tense percussion/strings loop
- Loop: ✓
- Volume: 1.0
Combat Layer:
- Routing Container: “Music_Combat_RC”
- Clip: Full combat music loop
- Loop: ✓
- Volume: 1.0
2. Create Blend Container:
- Blend Container: “Music_Dynamic_Combat_BC”
- Blend Parameter Name: “CombatIntensity”
- Volume: 1.0
- Loop: ✓
Blend Entries:
Entry [0] - Ambient:
- Container: Music_Ambient_RC
- Volume Curve:
- Key (0.0, 1.0) ← Full volume at calm
- Key (1.0, 0.0) ← Silent at combat
Entry [1] - Tension:
- Container: Music_Tension_RC
- Volume Curve:
- Key (0.0, 0.0) ← Silent at calm
- Key (0.5, 1.0) ← Peak at medium intensity
- Key (1.0, 0.0) ← Silent at full combat
Entry [2] - Combat:
- Container: Music_Combat_RC
- Volume Curve:
- Key (0.0, 0.0) ← Silent at calm
- Key (1.0, 1.0) ← Full at combat
3. Create Event:
- Event: “Music_Start_DynamicCombat”
- Actions: Play Music_Dynamic_Combat_BC → Music_Bus
Code
using UnityEngine;
using AudioSystem;
public class DynamicCombatMusic : MonoBehaviour
{
[Header("Audio")]
[SerializeField] private AudioEvent startMusicEvent;
[Header("Settings")]
[SerializeField] private float transitionSpeed = 0.5f;
[SerializeField] private float enemyDetectionRadius = 20f;
[SerializeField] private LayerMask enemyLayer;
[Header("Intensity Thresholds")]
[SerializeField] private int calmEnemyCount = 0;
[SerializeField] private int tensionEnemyCount = 2;
[SerializeField] private int combatEnemyCount = 4;
private float currentIntensity = 0f;
private float targetIntensity = 0f;
void Start()
{
StartMusic();
}
void StartMusic()
{
if (startMusicEvent != null)
{
startMusicEvent.Post();
}
AudioManager.Instance.SetRTPC("CombatIntensity", 0f);
}
void Update()
{
UpdateMusicIntensity();
}
void UpdateMusicIntensity()
{
// Calculate target intensity based on enemy count
int enemyCount = CountNearbyEnemies();
targetIntensity = CalculateIntensity(enemyCount);
// Smooth transition
if (Mathf.Abs(currentIntensity - targetIntensity) > 0.01f)
{
currentIntensity = Mathf.MoveTowards(
currentIntensity,
targetIntensity,
transitionSpeed * Time.deltaTime
);
AudioManager.Instance.SetRTPC("CombatIntensity", currentIntensity);
}
}
int CountNearbyEnemies()
{
Collider[] enemies = Physics.OverlapSphere(
transform.position,
enemyDetectionRadius,
enemyLayer
);
return enemies.Length;
}
float CalculateIntensity(int enemyCount)
{
if (enemyCount <= calmEnemyCount)
return 0f; // Ambient only
if (enemyCount <= tensionEnemyCount)
return 0.5f; // Tension layer
if (enemyCount <= combatEnemyCount)
return 0.75f; // Ramping up
return 1f; // Full combat
}
void OnDrawGizmos()
{
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, enemyDetectionRadius);
// Show intensity
#if UNITY_EDITOR
UnityEditor.Handles.Label(transform.position + Vector3.up * 2f,
$"Music Intensity: {currentIntensity:F2}");
#endif
}
}
Setup:
- Attach to player or game manager
- Configure detection radius and thresholds
- Set enemy layer mask
- Tune transition speed (0.3-0.8 recommended)
✅ Result: Music dynamically responds to combat!
From the Pepper.SFX 3.1 guides (USER_GUIDE.md). Something wrong or unclear? Tell me.