What Is It?
A Proxy implements the same interface as a real object and sits in front of it. Clients think they are talking to the real thing, but the proxy decides what happens: it might delay creating the object, cache its results, check permissions, or forward the call across a network.
A virtual proxy is the most common flavour in games. A TextureProxy hands out a low-res placeholder immediately, kicks off the real load (from Addressables, disk or a CDN) only the first time someone asks, and then caches the result so every later request is instant.
A protection proxy uses the same shape to guard access. The proxy checks whether the caller is allowed, say whether a DLC skin is owned, before letting the request reach the real asset. The client code is identical in both cases; only the proxy’s rules differ.
When Is It Used?
Use a virtual proxy for anything heavy that may never be needed: high-res textures in an inventory grid, audio banks for areas the player has not reached, or remote profile pictures.
Use a protection proxy when the same API should behave differently depending on entitlement, platform, or debug state, without scattering checks through every caller.
Avoid it when the object is cheap or always needed right away. Lazy loading in that case just moves the hitch to a worse moment, in the middle of gameplay instead of a loading screen.
Interactive Demo
Click the controls and watch the objects collaborate. The console mirrors what the C# code below would log.
Code
using UnityEngine;
using UnityEngine.UI;
namespace Patterns.Structural.Proxy
{
/// <summary>
/// Client. Asks its provider for a texture every time it is shown.
/// It cannot tell whether it holds a proxy or the real thing.
/// </summary>
public class InventorySlot : MonoBehaviour
{
[SerializeField] private RawImage icon;
[SerializeField] private string assetKey = "Icons/DragonSword_4K";
[SerializeField] private Texture2D placeholder;
private ITextureProvider provider;
private void Awake()
{
provider = new TextureProxy(assetKey, placeholder);
}
private void OnEnable()
{
icon.texture = provider.GetTexture();
provider.Loaded += OnLoaded;
}
private void OnDisable()
{
provider.Loaded -= OnLoaded;
}
private void OnLoaded(Texture2D texture) => icon.texture = texture;
}
}using System;
using UnityEngine;
namespace Patterns.Structural.Proxy
{
/// <summary>
/// Subject interface shared by the real texture and its proxy.
/// </summary>
public interface ITextureProvider
{
/// <summary>
/// Returns the best texture available right now. For a proxy this
/// may be a placeholder until the real asset finishes loading.
/// </summary>
Texture2D GetTexture();
/// <summary>Raised once the full-quality texture is available.</summary>
event Action<Texture2D> Loaded;
}
}using System;
using UnityEngine;
namespace Patterns.Structural.Proxy
{
/// <summary>
/// Real subject: owns a heavy, fully loaded texture. Constructing one
/// is expensive, which is exactly why we hide it behind a proxy.
/// </summary>
public class RealTexture : ITextureProvider
{
private readonly Texture2D texture;
public RealTexture(Texture2D loaded)
{
texture = loaded != null ? loaded : throw new ArgumentNullException(nameof(loaded));
Debug.Log($"RealTexture ready: {texture.name} ({texture.width}x{texture.height})");
}
public Texture2D GetTexture() => texture;
// Already loaded, so subscribers are notified immediately.
public event Action<Texture2D> Loaded
{
add => value?.Invoke(texture);
remove { }
}
}
}using System;
using UnityEngine;
namespace Patterns.Structural.Proxy
{
/// <summary>
/// Virtual proxy: returns a placeholder at once, loads the real
/// texture lazily on first request, then caches it for everyone.
/// </summary>
public class TextureProxy : ITextureProvider
{
private readonly string key;
private readonly Texture2D placeholder;
private RealTexture real;
private ResourceRequest pending;
public TextureProxy(string key, Texture2D placeholder)
{
this.key = key;
this.placeholder = placeholder;
}
public event Action<Texture2D> Loaded;
public bool IsLoaded => real != null;
public Texture2D GetTexture()
{
if (real != null) return real.GetTexture(); // cache hit: instant
if (pending == null) // first request only
{
pending = Resources.LoadAsync<Texture2D>(key);
pending.completed += OnCompleted;
}
return placeholder;
}
private void OnCompleted(AsyncOperation _)
{
real = new RealTexture((Texture2D)pending.asset);
pending = null;
Loaded?.Invoke(real.GetTexture());
}
}
/// <summary>
/// Protection proxy: same interface, but only forwards to the inner
/// provider when the player owns the item.
/// </summary>
public class ProtectedTextureProxy : ITextureProvider
{
private readonly ITextureProvider inner;
private readonly Func<bool> isOwned;
private readonly Texture2D locked;
public ProtectedTextureProxy(ITextureProvider inner, Func<bool> isOwned, Texture2D locked)
{
this.inner = inner;
this.isOwned = isOwned;
this.locked = locked;
}
public event Action<Texture2D> Loaded
{
add => inner.Loaded += value;
remove => inner.Loaded -= value;
}
public Texture2D GetTexture() => isOwned() ? inner.GetTexture() : locked;
}
}Advantages & Disadvantages
+ Advantages
- Defers expensive work until it is actually needed, and caches it afterwards.
- Access rules, logging or metrics live in one place instead of in every client.
- Clients stay unchanged because the proxy shares the real object’s interface.
− Disadvantages
- The first real access can cause a visible hitch unless loading is asynchronous.
- Adds a layer of indirection that hides when work really happens, which can confuse profiling.
- Cached objects need an explicit release strategy or memory quietly grows.
Tips
- 01Load asynchronously (
Addressables.LoadAssetAsync,Resources.LoadAsync) and serve a placeholder while the task runs. - 02Track an
IsLoadedflag and a pending task so simultaneous requests share one load instead of starting several. - 03Give the proxy a
Release()method that callsAddressables.Releaseso memory can be reclaimed. - 04Keep protection checks in a separate proxy from caching; stacking two proxies is cleaner than one that does both.
- 05Unity’s own
AssetReferenceis effectively a proxy: a lightweight handle that loads the real asset on demand.