Sponsored By

The Shoulders of Giants: LensManagers

In which I describe a programming pattern that permits shared control over any sort of game system (a stage curtain, for example) by any number of anonymous game entities.

Brendan Vance, Blogger

May 2, 2016

15 Min Read
Game Developer logo in a gray background | Game Developer

This is part 1 in a hopefully-ongoing series about videogame programming techniques I've acquired over the past several years. Broadly, it's intended to help you solve common problems you encounter in your daily game development life. Yet rather than merely throwing a github or Asset Store link at you, my goal is to present a comprehensive programming style: how to think about the problem, why to think about it this way, and ultimately how to attack similar problems with valour and grace.

I once read a Gamasutra post describing something called 'The Curtain Problem'. Here's the basic idea: you must program a big Broadway-style stage curtain for the videogame you're creating. You'll use this curtain firstly as a loading screen: closing it whenever it's time to unload the previous scene, then reopening it once the new scene has loaded. Yet your level designer is musing about reusing this curtain for dramatic effect at various points during the middle of gameplay. (Your antagonist, it turns out, is an underground R&B idol called 'The Bleaknd' who changes outfits often; it would be cool if each outfit swap caused the curtain to close and reopen around him.)

Had you brought this problem to an entry-level programming class, you'd inevitably find yourself staring at a projector slide with the following text on it:


public class Stage
{
    public bool IsCurtainClosed = false;
}

The very first thing universities teach us about object-oriented programming is that this variety of public member access is, for some reason, sinful. Yet it's difficult to explain to a novice programmer precisely why this is the case. In the land of C# and Unity3d, the first thing your professor would do is show you the ole' getter & setter pattern:


public class Stage
{
    public bool IsCurtainClosed
    {
        get
        {
            return _IsCurtainClosed;
        }
        set
        {
            _IsCurtainClosed = value;
        }
    }

    bool _IsCurtainClosed = false;
}

It's apparent to the student that this code snippet is functionally identical to the previous one, except that it's many lines longer and, therefore, much harder to write correctly on the back of the handwritten exam Your professor is preparing to give you. They explain that this technique entitles you to a fresh and steamy mound of OOP shit™, which smells strongly of a thing called 'encapsulation' (best remember this one for your first job interview). The two of you soon arrive at something like:


public class Stage
{
    public bool IsCurtainClosed
    {
        get
        {
            return Controller.IsLeftCurtainClosed && Controller.IsRightCurtainClosed;
        }
        set
        {
            Controller.SetLeftCurtainClosed(value);
            Controller.SetRightCurtainClosed(value);
        }
    }

    CurtainController Controller = new CurtainController(true, 0.5f, false);
}

This snippet, unlike the previous one, rises above complete pointlessness. It's sort of neat that you can tell 'Stage' to open or close the curtain without needing to know what CurtainController is or what it's doing. It's sort of helpful that CurtainController is inaccessible from outside the Stage, and therefore has only one thing feeding it commands. You could probably reuse CurtainController somewhere else—perhaps as part of some other class—though you'll probably never need to.

Inevitably, the very next problem you'll encounter shall concern the sharing of control. What if your antagonist is frenemies with a local rap superstar (notorious, it turns out, for 'runnin through the Styx with his ghosts')? What if both characters need to open and close the curtain at arbitrary intervals? What if there were four characters, or six, or twenty?

Here we come to realize that the boolean parameter with which we've been working probably needs to be more like a retain counter:


public class Stage
{
    public bool IsCurtainUp
    {
        get
        {
            return _CurtainUpCounter > 0;
        }
    }

    int _CurtainUpCounter;

    public void IncrementCurtainUp()
    {
        ++_CurtainUpCounter;
    }

    public void DecrementCurtainUp()
    {
        --_CurtainUpCounter;
    }
}

Two weeks later, the problem will inevitably become more complicated than we thought. We'll need the curtain to open and close at different speeds based on a variety of different contextual factors; sometimes we'll need it to pause partway, framing one important character on the stage; and so on, and so on. A retain counter is no longer enough; we somehow need to wrangle many potential curtain configurations all at once, appending and removing and making sense of them all on the fly. Here's about as far as we'll make it before collapsing in despair:


// an ever-more complicated Stage
public class ComplicatingStage : MonoBehaviour
{
    List<StageOverride> overrides = new List<StageOverride>();

    // add and remove sets of curtain parameters?
    public void OverrideCurtain(StageOverride ovr)
    {
        overrides.Add(ovr);
    }

    public void RemoveCurtainOverride(StageOverride ovr)
    {
        var index = overrides.IndexOf(ovr);
        if (index >= 0) {
            overrides.RemoveAt(index);
        }
    }

    // interpret all the overlapping sets of curtain parameters we might have??
    bool TryGetCurtainMoveRate(out float rate)
    {
        if (overrides.Count == 0) {
            rate = float.NaN;
            return false;
        }

        rate = overrides[0].CurtainMoveRate;
        for(var i = 1; i < overrides.Count; ++i) {
            rate = Mathf.Max(overrides[i].CurtainMoveRate, rate);
        }
        return true;
    }

    bool TryGetFocalBounds(out Bounds focalBounds)
    {
        if (overrides.Count == 0) {
            focalBounds = new Bounds();
            return false;
        }

        focalBounds = new Bounds(overrides[0].focalPoint, Vector3.zero);
        for (var i = 1; i < overrides.Count; ++i) {
            focalBounds.Encapsulate(overrides[i].focalPoint);
        }

        return true;
    }

    void Update()
    {
        // TODO: Somehow make all this shit animate correctly??? O_O
    }
}

As the arrival of each new feature brings ruin down upon our treasured OOP shit™, we shall be forced into endless refactoring. As professional programmers we are destined to encounter problems of this same essential form over and over again: no less than twice a week, for as many years as we can bear it.

Rudimentary object-oriented design is mostly incapable of addressing problems of this variety. It's all about hierarchies of control: everything must have a boss, and everything's boss must have a boss. Objects within objects within objects, pushing instructions down: wrappers upon wrappers upon wrappers, pulling information up. This is a passable approach for business transactions, maybe, but it's next to useless for creating videogames. In games our requirements remain fluid, and our solutions are always temporary; any class hierarchy we could devise will inevitably crumble before an onslaught of incremental design shifts. And furthermore, the vast majority of our medium's traditions call for parallelism rather than vertical chains of command: the more everything in the game world is able to affect everything else, the more space we create for doing design work. Software like ours demands a different kind of programming style. Regrettably, it's a style few institutions ever teach.

In my experience game programming is not so much about encapsulation, data hiding or any of that rudimentary OOP shit™; those things can be nice, but they scarcely scratch the surface of the problem. More than anything, game programming is about distributing control such that the software won't implode the moment anything unexpected happens. To achieve this we must do away with all the hidden assumptions underlying traditional programming dogma. We typically assume that our objects should have bosses within bosses; that to call a function is to issue marching orders down a chain of command. We typically assume that the objects on and around our Broadway stage (our antagonist, for example, or our scene changing subroutine) possess a reference to the curtain and, therefore, must tell it precisely how to behave. But what if we treated the curtain as a collaborator, rather than a possession? What if we told it what we wanted, rather than issuing commands for it to follow? What if we decoupled the notion of an outcome from the notion of a request?

Suppose any game entity, anywhere in the world, could notify the stage a) when it is interested in the curtain closing itself and b) when it is no longer interested in the curtain at all. We phrase this as any number of entities requesting and subsequently disposing any number of 'Lenses' from a centralized 'LensManager' located within the stage. 'Lenses', for our purposes, are small tokens that each exert a temporary (in that they can be requested, held, then disposed) and anonymous (in that they do not need to know about or reason with one another) influence on our stage's behaviour:


using System;
using System.Collections.Generic;

public class Lens
{
    public bool IsFinished { get; private set; }

    Queue<Action> Callbacks = new Queue<Action>();

    public void OnFinished(Action callback)
    {
        EnqueueCallback(callback);
    }

    internal void EnqueueCallback(Action c)
    {
        Callbacks.Enqueue(c);

        if(IsFinished) {
            FlushCallbacks();
        }
    }

    public void Finish()
    {
        if(!IsFinished) {
            IsFinished = true;
            FlushCallbacks();
        }
    }

    protected virtual void FlushCallbacks()
    {
        while(Callbacks.Count > 0) {
            var a = Callbacks.Dequeue();
            a();
        }
    }
}

public class LensManager<T> where T : Lens, new()
{
    List<T> LensList = new List<T>();
    IList<T> ReadOnlyLenses;

    public IList Lenses
    {
        get
        {
            return ReadOnlyLenses;
        }
    }

    public T CreateLens()
    {
        var lens = new T();

        LensList.Add(lens);
        lens.OnFinished(() => LensList.Remove(lens));

        return lens;
    }

    public bool AnyLensesExist
    {
        get
        {
            return LensList.Count > 0;
        }
    }

    public LensManager()
    {
        ReadOnlyLenses = LensList.AsReadOnly();
    }
}

The only entity that needs to know about every currently-existing Lens is the stage, which remains free to decide for itself how these Lenses should influence its behaviour. The simplest case we can imagine, analogous to our retain counter code, is: 'If any Lenses currently exist, the curtain's state is closed':


public class Stage
{
    LensManager<Lens> LensManager = new LensManager<Lens>();

    public Lens RequestLowerCurtainLens()
    {
        return LensManager.CreateLens();
    }

    public bool CurtainIsDown
    {
        get
        {
            return LensManager.AnyLensesExist;
        }
    }
}

But the beauty of LensManagers is that we can easily extend them to represent any arbitrary information the problem might require. I've prepared an example in Unity, featuring a big Broadway Stage and several instances of our favourite underground R&B Star. This is what the code looks like:


using UnityEngine;

// A lens for spotlighting a particular segment of the stage!
public class SpotlightCurtainLens : Lens
{
    public Vector3 WorldCenter;
    public float HorizontalRadius;
    public float CurtainRate = 1.0f;
}

// A lens for just closing the damn curtain!
public class CloseCurtainLens : Lens
{
    public float CurtainRate = 1.0f;
}

// A relatively small class that uses the above lenses to solve several really complicated problems!
[RequireComponent(typeof(RectTransform))]
public class ComplicatedStage : MonoBehaviour
{
    // our lens managers!
    public LensManager<SpotlightCurtainLens> SpotlightLensManager = new LensManager<SpotlightCurtainLens>();
    public LensManager<CloseCurtainLens> CloseLensManager = new LensManager<CloseCurtainLens>();

    public SpotlightCurtainLens RequestSpotlightCurtainLens()
    {
        return SpotlightLensManager.CreateLens();
    }

    public CloseCurtainLens RequestCloseCurtainLens()
    {
        return CloseLensManager.CreateLens();
    }

    // how to tell whether the curtain fully closed
    public bool CurtainIsClosed
    {
        get
        {
            ThisRect.GetLocalCorners(CornersCache);
            return CurrentLeftCurtainWidth + CurrentRightCurtainWidth + 0.5f > CornersCache[2].x - CornersCache[0].x;
        }
    }

    // the parameters we need to do fancy animation work
    public RectTransform LeftCurtain;
    public RectTransform RightCurtain;

    public float BaseCurtainSmoothTime = 0.1f;
    public float MinCurtainRate = 0.1f;
    public float MaxCurtainVelocity = 800f;

    RectTransform ThisRect;

    float CurrentLeftCurtainWidth;
    float CurrentLeftCurtainVelocity;

    float CurrentRightCurtainWidth;
    float CurrentRightCurtainVelocity;

    Vector3[] CornersCache = new Vector3[4];

    void Awake()
    {
        ThisRect = GetComponent();
    }

    void Start()
    {
        DontDestroyOnLoad(gameObject);

        ThisRect.GetLocalCorners(CornersCache);

        CurrentLeftCurtainWidth = CurrentRightCurtainWidth = (CornersCache[2].x - CornersCache[0].x) * 0.5f;
    }

    // a den of stinking evil...
    void LateUpdate()
    {
        ThisRect.GetLocalCorners(CornersCache);

        var totalWidth = CornersCache[2].x - CornersCache[0].x;

        var targetFocalX = Mathf.Lerp(CornersCache[2].x, CornersCache[0].x, 0.5f);
        var targetRate = 1.0f;
        var targetFocalWidth = totalWidth;

        if (CloseLensManager.AnyLensesExist) {
            foreach(var l in CloseLensManager.Lenses) {
                targetRate *= l.CurtainRate;
            }

            targetFocalX = Mathf.Lerp(CornersCache[0].x + CurrentLeftCurtainWidth, CornersCache[2].x - CurrentRightCurtainWidth, 0.5f);
            targetFocalWidth = 0;
        } else if(SpotlightLensManager.AnyLensesExist) {
            var minX = 1.0f;
            var maxX = 0.0f;

            var anyInView = false;
            foreach(var l in SpotlightLensManager.Lenses) {
                var centerViewport = Camera.main.WorldToViewportPoint(l.WorldCenter);
                if(centerViewport.x >= 0 && centerViewport.x <= 1 && centerViewport.y >= 0 && centerViewport.y <= 1) {
                    minX = Mathf.Max(0, Mathf.Min(minX, Camera.main.WorldToViewportPoint(l.WorldCenter - l.HorizontalRadius * Camera.main.transform.right).x));
                    maxX = Mathf.Min(1, Mathf.Max(maxX, Camera.main.WorldToViewportPoint(l.WorldCenter + l.HorizontalRadius * Camera.main.transform.right).x));

                    targetRate *= l.CurtainRate;

                    anyInView = true;
                }
            }

            if(anyInView) {
                Vector2 leftPos;
                Vector2 rightPos;
                if(RectTransformUtility.ScreenPointToLocalPointInRectangle(ThisRect, Camera.main.ViewportToScreenPoint(new Vector3(minX, 0, 0)), null, out leftPos) &&
                   RectTransformUtility.ScreenPointToLocalPointInRectangle(ThisRect, Camera.main.ViewportToScreenPoint(new Vector3(maxX, 0, 0)), null, out rightPos)) {

                    targetFocalX = Mathf.Lerp(leftPos.x, rightPos.x, 0.5f);
                    targetFocalWidth = rightPos.x - leftPos.x;
                }
            }
        }

        targetRate = Mathf.Max(MinCurtainRate, targetRate);

        CurrentLeftCurtainWidth = Mathf.SmoothDamp(CurrentLeftCurtainWidth, Mathf.Min(totalWidth, Mathf.Max(0, (targetFocalX - targetFocalWidth / 2.0f) - CornersCache[0].x)), ref CurrentLeftCurtainVelocity, BaseCurtainSmoothTime / targetRate, MaxCurtainVelocity);
        CurrentRightCurtainWidth = Mathf.SmoothDamp(CurrentRightCurtainWidth, Mathf.Min(totalWidth, Mathf.Max(0, CornersCache[2].x - (targetFocalX + targetFocalWidth / 2.0f))), ref CurrentRightCurtainVelocity, BaseCurtainSmoothTime / targetRate, MaxCurtainVelocity);

        LeftCurtain.sizeDelta = new Vector2(CurrentLeftCurtainWidth, LeftCurtain.sizeDelta.y);
        RightCurtain.sizeDelta = new Vector2(CurrentRightCurtainWidth, RightCurtain.sizeDelta.y);
    }

    // a weird little singleton-type thing
    public static ComplicatedStage GetOrCreateInstance()
    {
        var stage = FindObjectOfType<ComplicatedStage>();
        if(stage == null) {
            stage = Instantiate(Resources.Load<ComplicatedStage>("Stage"));
        }
        return stage;
    }
}

Here is, in my opinion, the apotheosis of what we began with at the top of this post. It permits any number of game elements to interact with it using a simple, easily-adjustable ruleset that handles all potential collisions between all our game elements. It's easy to extend or contract, should we require more or less from it. It is as loosely-coupled as I can imagine; we can drop it directly into some other game project and immediately start doing very complicated things without having to change a line of code. It works well across update loops, coroutines and high-efficiency event-driven systems. It works well for creating and destroying game objects, or on the enabling/disabling of individual components. Level designers love having the ability to layer complicated mechanical effects atop one another; gameplay programmers love having tangible references to these complicated effects.

I've come to use LensManagers for almost any game feature that requires shared control. Imagine a camera system where any number of game elements may ask to frame something and the camera automatically tweens towards the most important one. Imagine a stats controller that buffs and/or debuffs the player's attributes any time you enable/disable any number of Unity components anywhere in your scene. Imagine a physics controller that stacks intensifiers and weakeners of the player's frictional constants as she travels through any number of overlapping trigger areas. I've written all these things using lenses. (The physics one took me ten minutes flat.)

I believe this is the very best way to permit volatile state mutations between loosely-coupled software systems (which, in videogames, is one of thte great Holy Grails). LensManagers will save you time! They will preserve the hairs on your scalp! They will improve your game development life! Give them a shot.

You can download my example unity project here.

(This post was informed by the aforementioned 'Curtain Problem' piece by Jamie Fristrom, as well as this brilliant blog post by Craig Gidney laying out the fundamental programming technique.)

Read more about:

Featured Blogs
Daily news, dev blogs, and stories from Game Developer straight to your inbox

You May Also Like