Learn the difference between Object and Class Adapters
Recognize when to use Adapter vs Facade patterns
Implement adapters for third-party inventory systems
Avoid merge conflicts when updating vendor code
Create flexible, maintainable integration layers
Developer's Note: "Never Panic Early"
โ ๏ธ What to Expect During Implementation
As we implement this pattern, we'll be creating multiple files in a specific order.
You will see errors in Unity and your IDE until all files are complete.
How to Handle Development Errors
Don't Ignore: Note the errors - they're telling you something
Don't Panic: These are expected until all files are created
Stay Calm: Follow the implementation order and errors will resolve
As Apollo 13's Fred Haise said: "Never Panic Early"
This is a critical skill for software development. Note problems, but stay focused on the implementation path.
๐ Real-World Analogy
Power Adapters & Cable Converters
Think about traveling internationally with your phone:
Your Phone: Expects US-style plug (Client)
European Outlet: Provides European socket (Adaptee)
Travel Adapter: Converts between them (Adapter)
Key Properties:
Adapter doesn't modify the phone or the outlet
Phone doesn't know it's using an adapter
Outlet doesn't know an adapter is involved
You can swap adapters without changing either end
The Pattern: Bridge incompatible interfaces without modifying either side
๐ Understanding the Adapter Pattern
Core Concept
Convert the interface of a class into another interface clients expect, allowing incompatible classes to work together.
When You Need Adapters:
Integrating third-party libraries with different APIs
Using legacy code with modern systems
Working with vendor code you can't modify
Avoiding merge conflicts during library updates
Creating a consistent interface across different implementations
The Problem It Solves
You have two systems that need to work together, but their interfaces are incompatible. You can't modify one or both systems directly.
โ๏ธ Two Adapter Approaches
1. Object Adapter (Composition)
Uses composition - "has-a" relationship
Adapter wraps an instance of the Adaptee
More flexible - can adapt entire class hierarchies
Preferred in most scenarios
2. Class Adapter (Inheritance)
Uses inheritance - "is-a" relationship
Adapter inherits from the Adaptee
More direct but less flexible
Can override Adaptee behavior if needed
This Lecture: We'll focus on Class Adapter using inheritance, as it's more challenging to understand. Once you grasp this, Object Adapter is straightforward.
Client calls methods on the interface, Adapter translates those calls to the Adaptee's methods.
Pattern Type: Structural - focuses on composing classes and objects
๐ค Adapter vs Facade Pattern
Aspect
Adapter Pattern
Facade Pattern
Intent
Convert interface to match client expectations
Simplify complex subsystem
Number of Classes
Usually adapts one class
Wraps multiple classes
Interface
Matches existing target interface
Creates new simplified interface
Goal
Compatibility between systems
Ease of use
Remember: Adapter makes incompatible interfaces work together. Facade makes complex interfaces easier to use.
โ Benefits of Adapter Pattern
Non-Invasive Integration:
No modifications to existing code
Vendor code stays pristine
Easy to pull library updates
Reusability and Flexibility:
Continue using legacy code with new systems
Swap implementations without changing client code
Immediate return on investment
Single Responsibility:
Separates interface conversion from business logic
Each class has one reason to change
Open/Closed Principle:
Add new adapters without modifying existing code
Extend functionality through composition
โ ๏ธ Potential Drawbacks
Persisting Legacy Code:
Old code might limit upgrade options
Can become deprecated over time
May conflict with new Unity versions
Performance Overhead:
Additional layer of indirection
Extra method calls for adaptation
Usually negligible, but worth noting
Complexity:
More classes to manage
Can be confusing if overused
Need clear documentation
Mitigation: Plan eventual migration away from legacy systems. Use adapters as a temporary bridge, not a permanent crutch.
๐ฏ When to Use Adapter Pattern
Use Adapter When:
โ Integrating third-party libraries from Unity Asset Store
โ Working with legacy code you can't refactor
โ Adding features to vendor code without modification
โ Avoiding merge conflicts on library updates
โ Multiple systems need the same interface
Don't Use Adapter When:
โ You can modify the source code directly
โ The interface mismatch is simple (just wrap it)
โ You're trying to hide bad design (refactor instead)
Common Use Cases:
Cloud save + local save integration
Multiple analytics providers with unified interface
Cross-platform input systems
๐ Use Case: Inventory System
The Problem:
You've downloaded an excellent inventory system from the Unity Asset Store. It saves player items to a secure cloud backend, but it only supports cloud saves.
Your Requirements:
Need both cloud AND local disk saves for redundancy
Want to sync between local and cloud inventories
Can't modify vendor code (merge hell on updates)
The Solution:
Use the Adapter pattern to create InventorySystemAdapter that:
Inherits from vendor's InventorySystem
Adds local save functionality
Provides sync methods
Exposes consistent IInventorySystem interface
๐ป Third-Party InventorySystem
using UnityEngine;
using System.Collections.Generic;
// This is the third-party class we CANNOT modifypublicclass InventorySystem
{
publicvoidAddItem(InventoryItem item)
{
Debug.Log("Adding item to the cloud");
}
publicvoidRemoveItem(InventoryItem item)
{
Debug.Log("Removing item from the cloud");
}
public ListGetInventory()
{
Debug.Log(
"Returning inventory list stored in the cloud");
returnnew List();
}
}
Problem: No local save support, can't add it without modifying vendor code
๐ป SaveLocation Enum
๐ File Structure Note - PRODUCTION CODE
Create a new file: Assets/Scripts/Enums/SaveLocation.cs
This enum defines where inventory items can be saved. โ ๏ธ This code goes into your Unity project for Blade Racer.
publicenum SaveLocation
{
Local, // Save to local disk
Cloud, // Save to cloud backend
Both // Save to both locations
}
}
Why an Enum?
Provides clear options to client code
Type-safe selection of save destinations
Easy to extend with new locations (e.g., RemoteServer)
Self-documenting code
๐ป IInventorySystem Interface
๐ File Structure Note - PRODUCTION CODE
Create a new file: Assets/Scripts/Patterns/IInventorySystem.cs
Interface that defines our desired inventory system contract. โ ๏ธ This code goes into your Unity project for Blade Racer.
using System.Collections.Generic;
// This is the interface our client expectspublicinterface IInventorySystem
{
voidSyncInventories();
voidAddItem(
InventoryItem item, SaveLocation location);
voidRemoveItem(
InventoryItem item, SaveLocation location);
ListGetInventory(
SaveLocation location);
}
Key Differences from Vendor Interface:
Methods accept SaveLocation parameter
Adds SyncInventories() method
Client can specify where to save/load
๐ป InventorySystemAdapter (1/3)
๐ File Structure Note - PRODUCTION CODE
Create a new file: Assets/Scripts/Patterns/InventorySystemAdapter.cs
Adapter that bridges InventorySystem and IInventorySystem. โ ๏ธ This code goes into your Unity project for Blade Racer.
using UnityEngine;
using System.Collections.Generic;
// Inherits from InventorySystem AND implements IInventorySystempublicclass InventorySystemAdapter :
InventorySystem, IInventorySystem
{
private List _cloudInventory;
// NEW functionality: Sync local and cloudpublicvoidSyncInventories()
{
var _cloudInventory = GetInventory();
Debug.Log(
"Synchronizing local drive and cloud inventories");
}
Notice: We inherit from the Adaptee AND implement the Target interface. This is the Class Adapter approach.
๐ป InventorySystemAdapter (2/3)
// ADAPTED method: AddItem with location choicepublicvoidAddItem(
InventoryItem item, SaveLocation location)
{
if (location == SaveLocation.Cloud)
AddItem(item); // Calls parent method!
if (location == SaveLocation.Local)
Debug.Log("Adding item to local drive");
if (location == SaveLocation.Both)
Debug.Log(
"Adding item to local drive and cloud");
}
The Adaptation Magic:
AddItem(item) calls the inherited method
Local save logic is added here
Client gets unified interface for both
๐ป InventorySystemAdapter (3/3)
publicvoidRemoveItem(
InventoryItem item, SaveLocation location)
{
Debug.Log(
"Remove item from local/cloud/both");
}
public ListGetInventory(
SaveLocation location)
{
Debug.Log(
"Get inventory from local/cloud/both");
returnnew List();
}
}
Result: We've added local save support without touching the vendor's InventorySystem class!
๐ป InventoryItem Class
๐ File Structure Note - PRODUCTION CODE
Create a new file: Assets/Scripts/Items/InventoryItem.cs
ScriptableObject for defining inventory items (placeholder for now). โ ๏ธ This code goes into your Unity project for Blade Racer.
using UnityEngine;
[CreateAssetMenu(
fileName = "New Item",
menuName = "Inventory")]
publicclass InventoryItem : ScriptableObject
{
// Placeholder class for inventory items// In production, add:// - Item name, description, icon// - Stack size, rarity, value// - Equipment type, stats, etc.
}
ScriptableObject Benefits:
Designer-friendly asset creation
Shareable across scenes
No runtime instantiation overhead
๐ป ClientAdapter - Setup
๐ File Structure Note - TESTING CODE
Create a new file: Assets/Scripts/Testing/ClientAdapter.cs
This script tests your Adapter pattern implementation. โ ๏ธ This is temporary testing code - you can remove it after testing your implementation.
using UnityEngine;
publicclass ClientAdapter : MonoBehaviour
{
public InventoryItem item;
private InventorySystem _inventorySystem;
private IInventorySystem _inventorySystemAdapter;
voidStart()
{
// Old vendor system
_inventorySystem = newInventorySystem();
// Our adapted system
_inventorySystemAdapter =
newInventorySystemAdapter();
}
Key: Client can use either the old system OR the adapted system
๐ป ClientAdapter - Testing Interface
voidOnGUI()
{
if (GUILayout.Button("Add item (no adapter)"))
_inventorySystem.AddItem(item);
if (GUILayout.Button("Add item (with adapter)"))
_inventorySystemAdapter.AddItem(
item, SaveLocation.Both);
}
}
}
๐ก Alternative: When you have multiple test scripts with overlapping GUI buttons,
consider using TestPanel.cs to combine all test controls into one draggable window.
See the Event Bus lecture for the unified TestPanel implementation.
What This Demonstrates:
First button: Uses old cloud-only system
Second button: Uses adapter with local+cloud saves
Client decides which to use
Both options available simultaneously
๐ฎ Updating TestPanel for Adapter Pattern
๐ Evolving TestPanel.cs
Add Adapter Pattern keyboard shortcuts and section to your TestPanel.cs.
// Add to TestPanel fieldsprivate bool _adapterExpanded = true;
public InventoryItem testItem;
private InventorySystem _inventorySystem;
private IInventorySystem _inventoryAdapter;
voidStart()
{
// ... existing code ...
_inventorySystem = new InventorySystem();
_inventoryAdapter = new InventorySystemAdapter();
}
// Add to Update() - Adapter Pattern shortcutsif (Input.GetKeyDown(KeyCode.J)) // J = Just cloud
_inventorySystem.AddItem(testItem);
if (Input.GetKeyDown(KeyCode.X)) // X = eXtended (adapted)
_inventoryAdapter.AddItem(testItem, SaveLocation.Both);
// Add DrawAdapterSection() methodvoidDrawAdapterSection()
{
GUI.backgroundColor = new Color(0f, 0.5f, 0.5f); // Teal
_adapterExpanded = GUILayout.Toggle(
_adapterExpanded, "โผ Adapter Pattern", "button");
GUI.backgroundColor = Color.white;
if (_adapterExpanded)
{
GUILayout.BeginVertical("box");
if (GUILayout.Button("Add Item - No Adapter (J)"))
_inventorySystem.AddItem(testItem);
if (GUILayout.Button("Add Item - With Adapter (X)"))
_inventoryAdapter.AddItem(testItem, SaveLocation.Both);
GUILayout.EndVertical();
}
}
Keymap Update: Add to DrawKeymapWindow(): J = Add Item (Just cloud), X = Add Item with Adapter (eXtended)
๐งช Testing the Implementation
Steps:
Create new Unity scene
Create InventoryItem ScriptableObject asset:
Assets โ Create โ Inventory โ New Item
Name it "Test Sword" or similar
Create empty GameObject
Attach ClientAdapter script
Drag Test Sword asset into the Item field
Press Play
Click both buttons and observe console logs
Expected Output: First button logs cloud save only. Second button logs both local and cloud saves.
No vendor modification: InventorySystem stays pristine
๐ Object Adapter Approach
Alternative Implementation (Composition):
publicclass InventorySystemAdapter : IInventorySystem
{
// Wrap instead of inherit!private InventorySystem _inventorySystem;
publicInventorySystemAdapter()
{
_inventorySystem = newInventorySystem();
}
publicvoidAddItem(InventoryItem item, SaveLocation location)
{
if (location == SaveLocation.Cloud)
_inventorySystem.AddItem(item); // Delegate call
if (location == SaveLocation.Local)
Debug.Log("Adding item to local drive");
// ... rest of implementation
}
}
Object Adapter: More flexible, can adapt multiple classes, no inheritance constraints
๐ค Class vs Object Adapter
Use Class Adapter When:
โ You need to override Adaptee methods
โ Single inheritance is acceptable (C# supports one parent class)
โ Adaptee has protected members you need to access
Use Object Adapter When:
โ You need to adapt multiple classes
โ You want more flexibility (can swap Adaptee at runtime)
โ Inheritance hierarchy is already complex
โ You want to adapt an entire class hierarchy
General Recommendation: Prefer Object Adapter (composition over inheritance) unless you have a specific reason to inherit
๐พ Implementing Real Save Logic
using System.IO;
using UnityEngine;
publicvoidAddItem(InventoryItem item, SaveLocation location)
{
if (location == SaveLocation.Cloud || location == SaveLocation.Both)
{
AddItem(item); // Cloud save via parent
}
if (location == SaveLocation.Local || location == SaveLocation.Both)
{
// Real local save implementationstring path = Application.persistentDataPath + "/inventory.json";
var inventory = LoadLocalInventory(path);
inventory.Add(item);
string json = JsonUtility.ToJson(newInventoryList(inventory));
File.WriteAllText(path, json);
Debug.Log($"Saved {item.name} to {path}");
}
}
Production Tip: Use JsonUtility or JSON.NET for serialization, handle exceptions, validate data
๐ Implementing SyncInventories
publicvoidSyncInventories()
{
// Get inventories from both sources
List cloudInventory = GetInventory();
List localInventory = LoadLocalInventory();
// Merge strategy: Cloud takes precedencevar merged = new HashSet(cloudInventory);
foreach (var item in localInventory)
{
if (!merged.Contains(item))
{
// Item exists locally but not in cloud - upload itAddItem(item, SaveLocation.Cloud);
merged.Add(item);
}
}
// Save merged inventory locallySaveLocalInventory(merged.ToList());
Debug.Log($"Synced {merged.Count} items between local and cloud");
}
โ ๏ธ Error Handling Best Practices
publicvoidAddItem(InventoryItem item, SaveLocation location)
{
if (item == null)
{
Debug.LogError("Cannot add null item to inventory");
return;
}
try
{
if (location == SaveLocation.Cloud || location == SaveLocation.Both)
{
AddItem(item);
}
if (location == SaveLocation.Local || location == SaveLocation.Both)
{
SaveToLocalDisk(item);
}
}
catch (System.Exception e)
{
Debug.LogError($"Failed to add item: {e.Message}");
// Implement rollback if needed
}
}
๐ Multiple Adapters Pattern
Adapting Multiple Vendors:
// Adapter for Vendor A's systempublicclass VendorAInventoryAdapter : IInventorySystem
{
private VendorAInventory _vendorA;
// ... implement IInventorySystem
}
// Adapter for Vendor B's systempublicclass VendorBInventoryAdapter : IInventorySystem
{
private VendorBInventory _vendorB;
// ... implement IInventorySystem
}
// Client uses either adapter through same interface
IInventorySystem inventory;
if (useVendorA)
inventory = newVendorAInventoryAdapter();
else
inventory = newVendorBInventoryAdapter();
inventory.AddItem(item, SaveLocation.Both); // Works with both!
Benefit: Swap vendors without changing client code!
The early 2000s saw three radically different platforms competing: Sony's PlayStation 2 (DVD-based, Emotion Engine CPU), Microsoft's Xbox (x86 PC architecture, DirectX), and Nintendo's GameCube (mini-DVD, PowerPC). Each console had completely incompatible architectures, APIs, and development tools.
Game studios creating multi-platform titles faced a nightmare: the same game logic needed to run on three incompatible systems. They couldn't rewrite their entire codebase for each platform. The solution? Adapter layers. Studios created abstraction layers - unified rendering APIs, input handlers, and audio systems that translated calls to each platform's native format. Your game code called RenderSprite(), and platform-specific adapters converted it to PS2's GS, Xbox's DirectX, or GameCube's GX commands.
Connection to Adapter Pattern
Cross-platform game development IS the Adapter Pattern at industrial scale! Just like our UnityAnalyticsAdapter wraps incompatible vendor code, multi-platform engines wrap incompatible console APIs. Your game (client) uses a standard interface (IRenderer), and platform-specific adapters (PS2Adapter, XboxAdapter, GameCubeAdapter) translate to native calls. Modern engines like Unity and Unreal still use this exact pattern to support 20+ platforms from one codebase!