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
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);
}
}
}using UnityEngine;
namespace Patterns.Structural.Adapter
{
/// <summary>
/// Target interface. Game-shaped, deliberately small, and owned by
/// our project rather than by any input SDK.
/// </summary>
public interface IInputProvider
{
/// <summary>Called once per frame before values are read.</summary>
void Tick();
/// <summary>Movement on the XZ plane, each axis in [-1, 1].</summary>
Vector2 Move { get; }
/// <summary>True only on the frame jump was pressed.</summary>
bool JumpPressed { get; }
}
}using UnityEngine;
namespace Patterns.Structural.Adapter
{
/// <summary>
/// Adapter for the built-in Input Manager. Translates our interface
/// into Input.GetAxis / Input.GetButtonDown calls.
/// </summary>
public class LegacyInputAdapter : IInputProvider
{
private readonly string horizontalAxis;
private readonly string verticalAxis;
private readonly string jumpButton;
public LegacyInputAdapter(string horizontal = "Horizontal",
string vertical = "Vertical",
string jump = "Jump")
{
horizontalAxis = horizontal;
verticalAxis = vertical;
jumpButton = jump;
}
public Vector2 Move { get; private set; }
public bool JumpPressed { get; private set; }
public void Tick()
{
var raw = new Vector2(Input.GetAxis(horizontalAxis), Input.GetAxis(verticalAxis));
Move = Vector2.ClampMagnitude(raw, 1f);
JumpPressed = Input.GetButtonDown(jumpButton);
}
}
}using UnityEngine;
namespace Patterns.Structural.Adapter
{
/// <summary>
/// Adaptee: a vendor SDK we cannot modify. It reports sticks as raw
/// shorts and buttons as a bitmask.
/// </summary>
public class ThirdPartyGamepad
{
public const uint ButtonA = 1u << 0;
public ThirdPartyGamepad(int slot) => Slot = slot;
public int Slot { get; }
public uint PollButtons() => 0u; // native call in the real SDK
public short ReadStick(int axis) => 0; // -32768..32767
}
/// <summary>
/// Adapter: makes ThirdPartyGamepad look like an IInputProvider by
/// normalising sticks and turning the bitmask into a "pressed this frame" edge.
/// </summary>
public class GamepadAdapter : IInputProvider
{
private readonly ThirdPartyGamepad pad;
private uint previousButtons;
public GamepadAdapter(ThirdPartyGamepad pad) => this.pad = pad;
public Vector2 Move { get; private set; }
public bool JumpPressed { get; private set; }
public void Tick()
{
Move = new Vector2(pad.ReadStick(0) / 32767f, pad.ReadStick(1) / 32767f);
uint buttons = pad.PollButtons();
bool down = (buttons & ThirdPartyGamepad.ButtonA) != 0;
bool wasDown = (previousButtons & ThirdPartyGamepad.ButtonA) != 0;
JumpPressed = down && !wasDown;
previousButtons = buttons;
}
}
}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
- 01Keep the target interface small and game-shaped (
JumpPressed,Move) instead of mirroring any single SDK. - 02If the adaptee is event-driven, buffer events inside the adapter so the target can still be polled once per frame.
- 03Choose the adapter in a bootstrapper or composition root, not inside the consumer, so
PlayerControllernever names concrete types. - 04Use
[SerializeReference]or a ScriptableObject factory if designers need to pick the backend in the Inspector. - 05A
ScriptedInputAdapterthat replays a recording makes play-mode tests trivial: it is the same pattern pointed at a file.