← All patterns
Structural

Adapter

Wrap an incompatible API so it looks exactly like the interface your game code already expects.

What Is It?

An Adapter sits between two pieces of code that disagree about shape. Your game talks to a target interface it owns, while the adaptee (usually a third-party SDK or a legacy system) exposes a different set of methods. The adapter implements the target and translates every call into the adaptee’s dialect.

In Unity this comes up constantly with input. A PlayerController only wants to ask input.JumpPressed. The legacy Input Manager answers with Input.GetButtonDown("Jump"), the Input System with action.WasPressedThisFrame(), and a vendor gamepad SDK might hand back a bitmask from PollButtons(). Three adapters hide all three behind one IInputProvider.

The key property is that neither side changes. You never edit the vendor SDK, and you never sprinkle #if blocks or SDK-specific calls through gameplay code. The translation lives in one small class per source, and switching sources means handing the controller a different adapter.

When Is It Used?

Reach for it when integrating code you do not control: platform SDKs, analytics providers, ad networks, store APIs, or an Asset Store package whose naming clashes with your conventions.

It is also the cleanest way to migrate gradually. Wrap the old system in an adapter today, write new code against your interface, and later drop in an adapter for the replacement without touching any callers.

Skip it when you own both sides. An adapter around your own code usually means the real fix is a rename or a refactor, not another layer.

Interactive Demo

Click the controls and watch the objects collaborate. The console mirrors what the C# code below would log.

Code

Assets / Scripts / Structural/ Adapter ›PlayerController.cs
using UnityEngine;

namespace Patterns.Structural.Adapter
{
    /// <summary>
    /// Client. Only knows about IInputProvider, so it works with any
    /// input backend that has an adapter.
    /// </summary>
    [RequireComponent(typeof(Rigidbody))]
    public class PlayerController : MonoBehaviour
    {
        [SerializeField] private float moveSpeed = 6f;
        [SerializeField] private float jumpForce = 7f;
        [SerializeField] private bool useGamepadSdk;

        private IInputProvider input;
        private Rigidbody body;

        private void Awake()
        {
            body = GetComponent<Rigidbody>();

            // Pick a backend once; nothing below this line cares which.
            input = useGamepadSdk
                ? new GamepadAdapter(new ThirdPartyGamepad(0))
                : new LegacyInputAdapter();
        }

        public void SetInput(IInputProvider provider) => input = provider;

        private void Update()
        {
            input.Tick();

            Vector2 move = input.Move * moveSpeed;
            body.velocity = new Vector3(move.x, body.velocity.y, move.y);

            if (input.JumpPressed)
                body.AddForce(Vector3.up * jumpForce, ForceMode.VelocityChange);
        }
    }
}
4 files · namespace Patterns.Structural.AdapterC# · UTF-8 · LF

Advantages & Disadvantages

+ Advantages

  • Gameplay depends on your interface rather than a vendor’s, so SDK upgrades are contained to one file.
  • Multiple backends become interchangeable per platform or even at runtime.
  • Testing gets easy: a fake adapter can feed scripted input or canned responses deterministically.

− Disadvantages

  • Adds a class and an indirection per adaptee; trivial wrappers can feel like ceremony.
  • The shared interface drifts toward the lowest common denominator, so features only one backend has are awkward to expose.
  • Semantic mismatches (polled vs. event-driven, sync vs. async) can leak through however clean the signature looks.

Tips

  1. 01Keep the target interface small and game-shaped (JumpPressed, Move) instead of mirroring any single SDK.
  2. 02If the adaptee is event-driven, buffer events inside the adapter so the target can still be polled once per frame.
  3. 03Choose the adapter in a bootstrapper or composition root, not inside the consumer, so PlayerController never names concrete types.
  4. 04Use [SerializeReference] or a ScriptableObject factory if designers need to pick the backend in the Inspector.
  5. 05A ScriptedInputAdapter that replays a recording makes play-mode tests trivial: it is the same pattern pointed at a file.