Null Coalescing Operator for Blueprints

Fluffy Intro

Thanks to encouragement and example provided by my dad, I've been a DIY enthusiast for as long as I can remember. So, naturally, over the years, I've accumulated a collection of tools: A power drill, a staple gun, a nice heavy pickaxe, etc. With the addition of each tool to my arsenal, DIY-ing has become more fun: more possibilities, faster and more professional-looking results, and fewer (literally) painful mistakes and dead-ends. It strikes me that the very first introduction of these tools must have been game-changing, and mind-blowingly gratifying, for professional builders, electricians, and so on.

One of the nice things about software is that game-changing power tools often come in the form of a download. The C# programming language is a particularly good example of this - in the time I've used it, since only about 8 years ago, it's advanced 5 major versions, each providing new foundational grammar for building digital structures more expressively and performantly than was possible before.

After learning new language features, it's easy to get "spoiled". It can be difficult to go back to an old codebase, or framework, that doesn't support them. I've sawed through steel pipes by hand before, but now that I've experienced how much easier it is to do with a rotary tool, I'd be hard-pressed to settle for picking up the hacksaw again. If my rotary tool broke, I'd probably rather postpone the project and wait until I could get it fixed. It's hard to believe that, for my first Unity game, every single getter needed to be explicitly implemented with an explicit backing field declaration, because we couldn't do public Foo Bar { get; protected set; }. And yet, even rotary tools are small-fry in the grand scheme of things - async/await feels more like the introduction of the electric motor itself.

Nowadays, I work mostly in Unreal Engine, both in C++ and Blueprints. C++ has its own set of unique tools that make my work more enjoyable, especially when enhanced with a JetBrains Rider subscription. Blueprints, on the other hand, while surprisingly complete for a visual node-based programming language, lacks a lot of the luxuries that I've become accustomed to. One of these that recently nagged at me was the lack of a null-coalescing operator.

What is Null Coalescing?

The null-coalescing operator and its companion, the null-coalescing assignment operator, found in C# 8.0, Kotlin, Swift, and some other languages, fall squarely in the "syntax sugar" category. They allow you to express common patterns much more concisely than you could without them:

// Without null coalescing nor ternary if (foo != null) 	return foo; return bar;  // Without null coalescing return foo != null ? foo : bar;  // With null coalescing return foo ?? bar;   // Without null-coalescing assignment nor ternary if (foo != null) 	return foo; foo = new Foo(); return foo;  // Without null-coalescing assignment return foo != null ? foo : (foo = new Foo());  // With null-coalescing assignment return foo ??= new Foo();

The null-coalescing operator (expressed as ?? in C#) effectively says "evaluate and use the statement on the left, but if it resolves to null, then evaluate and use the statement on the right instead". In short, it translates to "... if viable, otherwise...". It's particularly nice when you have even more than two options to prefer or fall back to: return Foo ?? Bar ?? Baz.

How Would I Use Null Coalescing in a Game?

Surely you can find some examples in your source code where you test a value, and produce a different value if the first turns out to be null. But if you need a bit of inspiration, here are some use-cases that I've come across:

  • Any and all lazy-initialization (spawn an object instance no sooner than when it's first requested, and re-use this instance whenever it's requested again).

    • Pooling object factories are a special subset of this scenario.

  • Override/Default Chains: Decide which material to apply to a player's or NPC's mesh, based on the presence or lack of a status effect. return (CurrentStatusEffect ?? TeamStatusEffect ?? Skin).GetMaterialFor(this);

  • Interception: Route damage, boosts, or any other sort of message to the most prominent "layer" that is actually present. ApplyDamage(Power, Shield ?? Armor ?? Body);.

  • Null Representation: When drawing a UI that shows players' character selections before a match, use a default "none" character-object when no proper character has yet been selected. DrawCharacterStats(SelectedCharacter ?? PlaceholderCharacterAsset);.

Null Coalescing in Blueprints

In Blueprints, where logic is composed with bulky boxes that make much less efficient use of screen-space than written code, the benefit of this kind of syntax sugar (and the ugly unwieldiness of the alternative) is far more dramatic.

if (Shield) { ApplyDamage(Shield, Power); }
else if (Armor) { ApplyDamage(Armor, Power); }
else { ApplyDamage(Body, Power); }

Blueprint vs code - which requires more scrolling to see the big picture?

So, while working on a hobby project that happened to require frequent use of the "if A isn't null, then A, else B" pattern, I decided it was worth attempting to make my own null-coalescing operator.

The (next) closest thing to null coalescing in many languages is a ternary expression, and Blueprints "Select" node is very similar to ternary, so I threw together a macro like this:

And I made a null-coalescing assignment macro like this:

However, these easy-to-throw-together macros turned out to be substantially stunted.

  • The Select node causes evaluation of all incoming pins. In fact, this is true for any native blueprint node. If your B parameter is a pure function, then it will be wastefully executed even when your A parameter isn't null.

This is particularly bad if your B parameter creates a new object.

  • The null-coalescing assignment macro simply wasn't possible without using execution pins.

... Which some may argue is a good thing, as assignment isn't a pure operation. But it wouldn't be the first "pure" Blueprint node to have side-effects. Personally, I'm a big proponent of declarative programming in general, so I prefer to take every opportunity to remove the visual noise of execution pins, leaving behind only the connections truly necessary to describe the intended relationships between the present logic nuggets. Isn't it clear enough that A should occur before B, when A's right-hand side is wired up to B's left-hand side?

Having run into a wall with Blueprints' available features, I decided to pursue creating a new Blueprint node in C++ that would do what I wanted. And by "a new Blueprint node in C++", I don't mean a reflected static function a la UFUNCTION(BlueprintCallable) or UFUNCTION(BlueprintPure), as this approach would be even more restrictive than the macro approach, and give me no greater control over the execution of incoming pure functions. Considering that Blueprints' most fundamental building blocks are all defined in UE4's source code, I figured just about any conceivable language feature ought to be possible when creating a new node in the same manner as, for example, the built-in Branch node. This may seem like extreme lengths to go for such a small convenience, but I saw it as an opportunity to learn the fundamentals that might be necessary for me to work through my growing wish-list of additional Blueprint language features that I'd like to have on hand.

Making a Custom Blueprint Node

It turns out that there is very little information on the internet about creating custom Blueprint nodes. I searched online for the names of what appeared to be (and turned out to be) fundamentally important functions used in nodes like Select, only to find that there were only two remotely relevant results in the English language (both of them translated from Chinese sources): This incredibly terse and meandering list of relevant function & class summaries, and this more highly detailed blog series which did a decent job of explaining some useful fundamentals, but failed to answer many of my burning questions. It appears that there is a third source on the web as of a few months ago, which hadn't been published when I started this journey, but doesn't provide much more information anyhow. Important topics not covered by any of the above include "how do the VM instructions behave, in actual detail?", "what actually is a net and how should I be thinking about them when implementing the RegisterNets function?", and (importantly for my purposes) "what controls the invocation of pure nodes?". In the end, as is typical with UE4, understanding the art of custom Blueprints node creation came down to reading through the relevant engine source code.

Unexpected Potential

In the course of developing this null-coalescing operator, I found that there is the potential to do a lot of things in a plugin-housed Blueprint node that I had expected to only be possible through modifying core engine modules. Having learned this, it's actually sort of a shame that some of the built-in nodes aren't as versatile as they could be...

Wildcard Pins + Covariance and Contravariance

I knew that my null-coalescing node would need to be "generic", or in Blueprint terminology, to have wildcard pins. For example, if I wanted to use null-coalescing to choose between two potential Camera Actor references, then ideally I could attach said references to a null-coalescing node's inputs, and expect the output pin to then take on the appearance and behavior of a Camera Actor reference. Without wildcard pins, I would have to use "Object reference" as the type of every pin, requiring the use of a Cast node every time, or else flood the catalog with thousands of type-specific versions of the node.

We would have to settle for this without wildcard pins. Fragile, tedious, not ideal.

Requiring casts would have been wasteful (in terms of performance), and tedious - simply not acceptable. Type-specific versions would have been disappointingly inflexible, and would take significantly more time and effort to conjure up from the node-creation menu than a universal version would. Further, I hoped to support Class, Soft-Object, and Soft-Class pins as well, considering that those meta-types also support null or null-like values.

So, I set out to understand exactly how wildcard pins work, and how they're made. I was a bit disappointed to discover that the behavior of UE4's wildcard nodes, including wildcard Blueprint macros that are user-defined, depends on order-of-operations in a way that makes them unnecessarily inconvenient in certain situations. Consider the following scenario:

  1. Create a generic node in an Actor-derived Blueprint

  2. Attach one of the node's inputs to a Self pin

  3. Attach the other input to an Actor pin

As soon as you perform step 2, the wildcard will become associated with the specific class of the containing Blueprint. When you try to perform step 3, the editor won't let you, complaining that Actor is less specific than Foo Actor (or whatever you decided to call it). Shame on you for not connecting the output pin first, I guess.

It turns out that a lot of the behavior of generic pins is entirely up to the node's implementation, and it's a matter of coincidence that all of UE4's generic nodes behave this same way. I can understand why the UE4 team settled for this behavior, though - the alternative is surprisingly complex. But in the end, I was able to create a node that will allow you to attach pins in the above-described order, by leveraging the fact that the Blueprints editor will "ask" each affected node if a connection is supported, in addition to applying the fundamental rules like "string outputs can't connect to object inputs", or "object outputs can't connect to more-specific object inputs". My null-coalescing node uses the least-specific pin types necessary, applies additional rules on top of the fundamentals, and determines input vs output types separately.

The type of the input pins is automatically determined to be the most concrete (i.e. specific) of all of the output connections. The type of the output pin is determined to be the most concrete common parent of all of the input connections. This is enough to prevent connections that won't work: As soon as an Actor is fed into one of the inputs, it would no longer allow you to attach the output to something more specific, such as Camera Actor.

In the grand scheme of Blueprinting, this is a small nuance that might not matter on a frequent basis. But I couldn't swallow the idea of publishing a tool that even occasionally inconvenienced its users by forcing them to do something as perplexing as temporarily removing perfectly valid connections, just because some type-inference mechanism isn't as intelligent as it could have been.

Execution Control of Pure Nodes

Earlier, I mentioned that UE4's Select node, macros, and reflected-function-based Blueprint nodes will cause all pure inputs to be invoked. This was, of course, the primary problem that led me to pursue this project in the first place. Something I learned along the way is that pure node clusters are compiled once for every non-pure node that they're connected to. Double-dipping is not allowed. That is to say, if you put a pure cast node in your graph, and connect it to three non-pure nodes that are sequenced together, then the cast will be executed three times.

This holds true for the null-coalescing node: If you don't want it to be executed more often than necessary, then don't connect its output to more than one non-pure node in the same event or function. Use a (local) variable if necessary.

However, it turns out that this behavior isn't inevitable within the constraints of the Blueprint language framework. True to UE4's traditional (and precarious) lack of encapsulation, each Blueprint node, upon compilation, technically has the ability to re-wire and monkey-patch just about any part of the graph within which it resides, without having to resort to "hacks" like const-casting or private/protected-circumvention.

Of course, Blueprint authors expect a certain level of consistency from their visual scripting language, so while it was tempting, I didn't follow through on anything like trying to automatically cache the coalesce result into a local variable. But I did implement a minimal amount of in-place "post-processing" - I was able to "hide" the null-coalescing node's inputs from the compiler's eager hoisting + inlining behavior, and ensure that the bytecode generated by those nodes ends up interleaved between conditional jump instructions that guard their execution. UE4's CompileDisplaysBinaryBackend setting, which causes Blueprint compilation results to be spit out into the output log in the form of an Assembly-like syntax, was incredibly valuable in verifying that the end-result of this post-processing was exactly as intended.

More to Come

Null coalescing really seems simple on the outside - it's a single operator that does, like, a simple if-then type thing, right? But something as narrow as that can go surprisingly deep. Beyond the functionality described above, I've identified opportunities to make this lil nugget even more super than it already is, and you can expect these new features to become available in the coming months:

  • Per-node option to produce even more optimized bytecode, with fewer instructions and thunks, for use in scenarios where you can safely assume that non-null references are always fully valid (i.e. not pending destruction).

  • Ability to make the node pure or non-pure, and to toggle any given null-coalescing node between the two (similar to the built-in Cast node).

  • Project-wide option to change the display text on the node. Maybe some users would prefer "coalesce" or "?:" over "??".

  • Support for String (empty is like null), Name (None is like null), Int (-1 is like null, when used as an index), and Float (NaN is like null).

How To Get The Null Coalescing Node

My Null Coalescing node is available for you to use via the Unreal Marketplace. You can read detailed documentation, and find example use-cases, in the GitHub Wiki. I hope this is a helpful addition to your visual-scripting tool bag!

 

Max Pixel • Freeform Labs

Freeform Labs, Inc. is a games and software development team with a commitment to providing the highest quality of digital craftsmanship, and a mission to inspire learning and creativity through cutting-edge experience design. The team is centrally located in Los Angeles, and maintains a network of trusted partners across the globe.
With specialized experience in VR, AR, Game AI, and more, Freeform Labs provides an array of services including consultation, work-for-hire software development, and on-site "ship it" assistance. The company's portfolio includes work for Microsoft, Disney, and Starbreeze - as well as award-winning original content.
| hello@freeformlabs.xyz