Ramblings of an aging IT geek
← Ramblings of an aging IT geek
gamedev

drawing the line between c++ and blueprint

How I decided what to expose from C++ to Blueprint in Unreal, and the UFUNCTION and UPROPERTY habits that kept the boundary clean instead of a mess of spaghetti nodes.

A game development workstation with an engine editor on screen

The mistake I made early on with Unreal was treating Blueprint exposure as a default. Slap BlueprintCallable on everything, tick every box, and let the designer wire it up however they like. Six months later you have a graph that looks like someone dropped a plate of spaghetti, logic split unpredictably between C++ and nodes, and no way to reason about where a bug actually lives. Over the holidays I went back through a project and got disciplined about the boundary, and it's the difference between a codebase I dread opening and one I don't.

The principle I settled on is simple: C++ owns behaviour, Blueprint owns tuning and composition. The expensive, order-sensitive, performance-critical logic lives in C++ and stays there. What I expose to Blueprint is the knobs and the events, not the machinery.

In practice that means UPROPERTY(EditAnywhere) on the values a designer should tweak, the speeds and thresholds and curves, so they show up in the editor without anyone touching code:

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float DashSpeed = 1200.f;

UPROPERTY(EditAnywhere, Category = "Movement")
float DashCooldown = 0.75f;

And it means BlueprintImplementableEvent for the moments where I want C++ to call into Blueprint rather than the other way round. The C++ decides when the dash happens; the Blueprint decides what it looks and sounds like.

UFUNCTION(BlueprintImplementableEvent, Category = "Movement")
void OnDashStarted();

That keeps the cosmetic, iterate-a-hundred-times-a-day stuff in Blueprint where it's fast to change, and the "this must be correct" stuff in C++ where it's typed and testable.

Source code on a developer's monitor

What I am much more careful about now is BlueprintCallable. Every function I expose with it is a function I've promised to keep stable, because the moment a Blueprint calls it, renaming or removing it breaks assets that don't show up in a compiler error. C++ refactors are safe; the compiler shouts. Break a Blueprint reference and you find out when something silently stops working in play mode, often much later. So BlueprintCallable is now a deliberate decision per function, not a reflex, and the function has to be one I actually want designers calling.

The other habit worth its weight: BlueprintPure only for things that are genuinely cheap and have no side effects, because a pure node gets re-evaluated every time its output is read, not once. I got caught by that early, a "pure" getter that did real work, called from three places in a tick, running three times a frame for no reason. If it costs anything, it's BlueprintCallable, not BlueprintPure, so it executes once and caches.

None of this is exotic. It's the standard Unreal reflection macros used with a bit of restraint. But the restraint is the whole point. The engine will happily let you expose everything, and the regret comes later, when the boundary between native code and visual scripting has dissolved and you can no longer say with confidence where any given piece of behaviour actually lives. Decide the line up front, C++ for behaviour, Blueprint for tuning and presentation, and the project stays legible. I wish I'd drawn it on day one.