Guidance / Cookbook
States & RTPCs
Recipe: Underwater State
📋 What: Complete underwater audio effect
🎯 Use Case: Swimming, diving, underwater sections
Asset Setup
1. Create States:
Normal State:
- Audio State: “State_Normal”
- Save to: Assets/Resources/Audio/States/
- Inspector:
- State Name: “Normal”
- State Group: “Location”
- (Leave all properties empty/default)
Underwater State:
- Audio State: “State_Underwater”
- Save to: Assets/Resources/Audio/States/
- Inspector:
- State Name: “Underwater”
- State Group: “Location”
Bus Volumes:
- [0] Bus: SFX_Bus, Volume Db: -9
- [1] Bus: Music_Bus, Volume Db: -12
- [2] Bus: Dialogue_Bus, Volume Db: -6
Switch Values:
- [0] Switch Group: “Location”, Value: “Underwater”
RTPC Values:
- [0] Parameter: “Underwater”, Value: 1.0
A state sets the global value of a parameter, by name. To give it a range and a default, create a Parameter asset whose Parameter Name is “Underwater”: the state and the asset share one value. Underwater is the same for every sound, so a global value is right here; per-object values go on an Audio Emitter (Manual §3.4).
2. Create Underwater Ambience:
- Routing Container: “Ambience_Underwater_RC”
- Clip: Underwater bubbles/muffled sound
- Loop: ✓
- Volume: 0.8
- Is3D: ☐ (2D)
3. Create Event:
- Event: “Play_Ambience_Underwater”
- Actions: Play Ambience_Underwater_RC → Ambience_Bus
Code
using UnityEngine;
using AudioSystem;
public class UnderwaterVolume : MonoBehaviour
{
[Header("Audio")]
[SerializeField] private AudioEvent underwaterAmbienceEvent;
[SerializeField] private float transitionTime = 1.5f;
[Header("Optional VFX")]
[SerializeField] private GameObject underwaterVFX;
private AudioHandle ambienceHandle;
private bool isUnderwater = false;
void OnTriggerEnter(Collider other)
{
if (!other.CompareTag("Player")) return;
if (!isUnderwater)
{
EnterUnderwater();
}
}
void OnTriggerExit(Collider other)
{
if (!other.CompareTag("Player")) return;
if (isUnderwater)
{
ExitUnderwater();
}
}
void EnterUnderwater()
{
isUnderwater = true;
// Activate underwater state
AudioManager.Instance.SetState("Underwater", transitionTime);
// Set location switch
AudioManager.Instance.SetSwitch("Location", "Underwater");
// Play underwater ambience
if (underwaterAmbienceEvent != null)
{
ambienceHandle = underwaterAmbienceEvent.Post();
}
// Enable VFX
if (underwaterVFX != null)
{
underwaterVFX.SetActive(true);
}
Debug.Log("Entered underwater");
}
void ExitUnderwater()
{
isUnderwater = false;
// Return to normal state
AudioManager.Instance.SetState("Normal", transitionTime);
// Reset location switch
AudioManager.Instance.SetSwitch("Location", "Normal");
// Stop underwater ambience
if (ambienceHandle != null)
{
ambienceHandle.Stop(transitionTime);
ambienceHandle = null;
}
// Disable VFX
if (underwaterVFX != null)
{
underwaterVFX.SetActive(false);
}
Debug.Log("Exited underwater");
}
void OnDrawGizmos()
{
Gizmos.color = new Color(0, 0, 1, 0.3f);
Gizmos.DrawCube(transform.position, transform.localScale);
}
}
Setup:
- Create BoxCollider trigger volume for water
- Attach UnderwaterVolume script
- Assign underwater ambience event
- Tag player as “Player”
✅ Result: Smooth transition to muffled underwater audio!
Recipe: Vehicle Engine RPM
📋 What: Engine sound that changes with speed/RPM
🎯 Use Case: Cars, motorcycles, aircraft
Asset Setup
1. Create Layer Containers:
- “Engine_Idle_RC” (idle loop)
- “Engine_Load_RC” (medium RPM loop)
- “Engine_Redline_RC” (high RPM loop)
- Tick Loop on each of these, or set the Blend’s Loop Mode to Force Loop (step 3).
- For a 3D engine, set Doppler to Pepper on the Blend (its 3D Settings; or Doppler Level 0 for none) and tick Uses Parent Positioning on each layer. Layers that go virtual then come back in step with the others; Unity’s Doppler is the one pitch change Pepper can’t follow (Manual §3.2, Voice Virtualization; §4.1 Doppler).
2. Create a Parameter:
- Parameter: “EngineRPM” (Create > Audio System > Parameter)
- Unit Label: “0-1”, Min: 0, Max: 1, Default: 0.2 (idle)
- Smoothing: Ease, Rise 0.3 s, Fall 0.5 s. The engine follows the car’s speed gently, the same at any frame rate, so the script below doesn’t smooth anything itself.
3. Create Blend Container:
- Blend Container: “Vehicle_Engine_BC”
- Blend Parameter: the EngineRPM asset
- Loop Mode: Force Loop
- Silent Layers: Virtualize. Layers that are silent at the current RPM go virtual and free their real voices, coming back in step when the RPM reaches them (Manual §4.6).
- 3D Settings: Is 3D ticked and Doppler Pepper (or Doppler Level 0). The layers take both through Uses Parent Positioning (step 1); without Is 3D on the Blend the engine plays 2D, in the listener’s head.
Each car has its own RPM. The car posts its engine on its own Audio Emitter and sets the value there, so two cars playing this Blend each follow their own RPM. A value set globally (
SetRTPC("EngineRPM", ...)orAudioManager.SetParameter) reaches only engines whose emitter has no value of its own. See Manual §3.4.
Engine loops recorded at known RPMs? Use the parameter in real RPM (Min 0, Max 8000, Unit “RPM”), set each layer’s Root Value to the RPM it was recorded at, switch the Blend’s Layout to Regions, press Auto Layout, and set Pitch Follow Parameter to the same parameter. Every layer then crossfades at the right RPM and is pitched by RPM ÷ its recording, so the layers stay in tune with each other. In the script below, write the car’s real RPM instead of the 0–1 value (
targetRPMfrom the engine model,idleRPMin RPM, say 900) and untick Modulate Pitch: Pitch Follow does the pitching. See Manual §4.6. The Engine Builder (Manual §10.8) builds this whole tree, with on-load and off-load sets, from the selected loops, and Detect Root Values (§10.7) measures each loop’s RPM.
Entry [0] - Idle:
- Container: Engine_Idle_RC
- Curve: (0.0, 1.0), (0.3, 0.5), (1.0, 0.0)
Entry [1] - Load:
- Container: Engine_Load_RC
- Curve: (0.0, 0.0), (0.5, 1.0), (1.0, 0.3)
Entry [2] - Redline:
- Container: Engine_Redline_RC
- Curve: (0.0, 0.0), (0.7, 0.2), (1.0, 1.0)
4. Create Event:
- Event: “Start_Vehicle_Engine”
- Actions: Play Vehicle_Engine_BC → SFX_Bus
- Persistent: ticked. The engine is never stolen to make room for other sounds (Manual §5.3).
5. On the car: add an Audio Emitter component (Add Component > Audio System > Audio Emitter) next to the script below, and set its On Disable to Stop: when the car is destroyed or switched off, its engine fades out over Stop Fade Seconds where it is and its voices go back to the pool (Manual §3.4). Left at Keep, the engine’s voices sit under the car and are cut with it.
Code
using UnityEngine;
using AudioSystem;
[RequireComponent(typeof(Rigidbody), typeof(AudioEmitter))]
public class VehicleEngineAudio : MonoBehaviour
{
[Header("Audio")]
[SerializeField] private AudioEvent engineStartEvent;
[SerializeField] private AudioParameter engineRpm; // the EngineRPM asset
[Header("RPM Settings")]
[SerializeField] private float maxSpeed = 30f;
[SerializeField] private float idleRPM = 0.2f;
[Header("Pitch Modulation")]
[SerializeField] private bool modulatePitch = true;
[SerializeField] private float minPitch = 0.8f;
[SerializeField] private float maxPitch = 1.4f;
private Rigidbody rb;
private AudioEmitter emitter;
private AudioHandle engineHandle;
void Start()
{
rb = GetComponent<Rigidbody>();
emitter = GetComponent<AudioEmitter>();
StartEngine();
}
// Call this when the car is reset or teleported, so the engine doesn't glide across it.
public void OnCarReset() => emitter.SnapAllParameters();
void StartEngine()
{
if (engineStartEvent != null)
{
// Posted on this car's emitter: the engine reads this car's RPM.
engineHandle = emitter.Post(engineStartEvent);
}
}
void Update()
{
UpdateEngineRPM();
}
void UpdateEngineRPM()
{
// Calculate target RPM from speed
float speed = rb.linearVelocity.magnitude;
float targetRPM = Mathf.Clamp01(speed / maxSpeed);
// Keep minimum idle RPM when not moving
if (targetRPM < idleRPM)
targetRPM = idleRPM;
// This car's RPM, raw. The parameter's Smoothing eases what the engine hears; the Blend
// picks it up after LateUpdate, before the frame's audio.
emitter.SetParameter(engineRpm, targetRPM);
// Optional: Modulate pitch by the smoothed RPM the engine is playing at
float currentRPM = emitter.GetEffectiveParameter(engineRpm);
if (modulatePitch && engineHandle != null)
{
// SetPitchOffset takes semitones. A playback-rate ratio r is 12 * log2(r) semitones:
// 0.8 -> -3.9 st, 1.4 -> +5.8 st. It shifts on top of the layers' own pitch
// randomization, if any, rather than replacing it.
float pitchSemitones = Mathf.Lerp(
12f * Mathf.Log(minPitch, 2f),
12f * Mathf.Log(maxPitch, 2f),
currentRPM
);
engineHandle.SetPitchOffset(pitchSemitones);
}
}
// Engine off while the car stays (a stall, the key turned). Removing the car needs nothing here: its Audio
// Emitter, set to On Disable = Stop, fades the engine out where it is.
public void StopEngine()
{
engineHandle?.Stop(0.5f);
engineHandle = null;
}
}
✅ Result: Realistic engine sound that responds to speed!
Why no
OnDestroy: an earlier version posted a stop event there. A Stop action only stops its own event’s sounds, so a separate stop event never reached the engine, and aStop(0.5f)fromOnDestroycan’t fade voices that are destroyed with the car on the same frame. The emitter’s On Disable = Stop does both jobs.
Tuning by ear: open Window > Audio System > Audition with the engine Blend selected, press Play and Sweep the RPM (say 900 → 7000 over 4 s, back and forth) while you adjust regions and root values in the Inspector; the Layers table shows each band’s level and pitch (§10.5).
From the Pepper.SFX 3.1 guides (USER_GUIDE.md). Something wrong or unclear? Tell me.