What Is It?
A Facade is a single class that offers a small, task-oriented API on top of several lower-level systems. It does not hide those systems from everyone; it just means ordinary callers no longer need to know how they fit together.
Think about what a convincing explosion needs: grab a free AudioSource from a pool, route it through the SFX group of an AudioMixer, briefly duck the music, and rumble the controller. Without a facade, every grenade, barrel and rocket script repeats that choreography. With an AudioFacade, they all call PlayExplosion(position) and move on.
The subsystems stay fully usable on their own. The facade simply bundles the common recipes, so the knowledge of "how we do an explosion here" lives in exactly one place.
When Is It Used?
Use it when many clients repeat the same multi-step sequence across several systems: audio and feedback, saving and loading, scene transitions, analytics plus achievements plus cloud sync.
It is a good boundary around a third-party package or a large internal module, so most of the codebase only touches a few well-named methods.
Avoid letting it grow into a god object that every system routes through. If the facade starts accumulating unrelated responsibilities, split it into several focused facades.
Interactive Demo
Click the controls and watch the objects collaborate. The console mirrors what the C# code below would log.
Code
using UnityEngine;
namespace Patterns.Structural.Facade
{
/// <summary>
/// Client. Knows nothing about pools, mixers or haptics: it asks the
/// facade for an explosion and gets the whole package.
/// </summary>
public class Grenade : MonoBehaviour
{
[SerializeField] private AudioFacade audioFacade;
[SerializeField] private float fuseSeconds = 2.5f;
private float timer;
private void OnEnable() => timer = fuseSeconds;
private void Update()
{
timer -= Time.deltaTime;
if (timer > 0f) return;
audioFacade.PlayExplosion(transform.position);
gameObject.SetActive(false);
}
}
}using System.Collections;
using UnityEngine;
namespace Patterns.Structural.Facade
{
/// <summary>
/// Facade: one intent-named entry point that orchestrates several
/// audio and feedback subsystems in the right order.
/// </summary>
public class AudioFacade : MonoBehaviour
{
[SerializeField] private AudioSourcePool pool = new();
[SerializeField] private MixerRouter mixer = new();
[SerializeField] private MusicDucker music = new();
[SerializeField] private Haptics haptics = new();
[SerializeField] private AudioClip explosionClip;
private void Awake() => pool.Init(transform);
public void PlayExplosion(Vector3 position)
{
AudioSource source = pool.Rent(position);
mixer.RouteToSfx(source);
source.PlayOneShot(explosionClip);
music.Duck();
haptics.Rumble(low: 0.8f, high: 0.4f);
StartCoroutine(Cleanup(source, explosionClip.length));
}
private IEnumerator Cleanup(AudioSource source, float clipLength)
{
yield return new WaitForSeconds(0.35f);
haptics.Stop();
yield return new WaitForSeconds(Mathf.Max(0f, clipLength - 0.35f));
music.Restore();
pool.Return(source);
}
}
}using System;
using UnityEngine;
using UnityEngine.Audio;
using UnityEngine.InputSystem;
using UnityEngine.Pool;
namespace Patterns.Structural.Facade
{
/// <summary>Subsystem: recycles AudioSources instead of instantiating them.</summary>
[Serializable]
public class AudioSourcePool
{
[SerializeField] private AudioSource prefab;
private ObjectPool<AudioSource> pool;
public void Init(Transform parent) => pool = new ObjectPool<AudioSource>(
() => UnityEngine.Object.Instantiate(prefab, parent),
s => s.gameObject.SetActive(true),
s => s.gameObject.SetActive(false));
public AudioSource Rent(Vector3 position)
{
AudioSource s = pool.Get();
s.transform.position = position;
return s;
}
public void Return(AudioSource s) => pool.Release(s);
}
/// <summary>Subsystem: assigns sources to the right mixer group.</summary>
[Serializable]
public class MixerRouter
{
[SerializeField] private AudioMixerGroup sfx;
[SerializeField] private AudioMixerGroup ui;
public void RouteToSfx(AudioSource s) => s.outputAudioMixerGroup = sfx;
public void RouteToUi(AudioSource s) => s.outputAudioMixerGroup = ui;
}
/// <summary>Subsystem: ducks background music using mixer snapshots.</summary>
[Serializable]
public class MusicDucker
{
[SerializeField] private AudioMixerSnapshot normal;
[SerializeField] private AudioMixerSnapshot ducked;
public void Duck() => ducked.TransitionTo(0.05f);
public void Restore() => normal.TransitionTo(0.5f);
}
/// <summary>Subsystem: controller rumble via the Input System.</summary>
[Serializable]
public class Haptics
{
public void Rumble(float low, float high) => Gamepad.current?.SetMotorSpeeds(low, high);
public void Stop() => Gamepad.current?.ResetHaptics();
}
}Advantages & Disadvantages
+ Advantages
- Clients become simple and decoupled from the details of each subsystem.
- Shared recipes are defined once, so tuning an explosion is a one-file change.
- Subsystems can be refactored or swapped behind the facade without touching callers.
− Disadvantages
- Can turn into a god class if every new feature gets bolted on.
- Callers that need fine control still have to go around it, so you maintain two paths.
- Often implemented as a global singleton, which brings its own testing and coupling issues.
Tips
- 01Name facade methods after game intents (
PlayExplosion,EnterCombat), not after the subsystems they touch. - 02Inject the subsystems into the facade via
[SerializeField]references so they remain visible and swappable in the Inspector. - 03Keep the facade thin: it should orchestrate, not reimplement. Real logic belongs in the subsystems.
- 04Pair it with a Service Locator or DI container rather than a static
Instanceif you want testable callers. - 05Use
AudioMixer.FindSnapshotandTransitionToinside the facade for ducking instead of manually lerping volumes.