Initial Thoughts on Using Unreal Engine’s Blueprints - 2024-10-31 02:41
This is a follow-up to my last game dev thread where I wanted to talk a bit more about how I’ve found working with Unreal Engine’s Blueprints, which is an entire visual programming system that is no doubt the most ambitious and complete visual programming system I’ve used. But how does it compare to good old-fashioned textual programming?
Well before I answer that question, let me describe how Blueprints work in a bit more detail (sorry for no pictures; I haven’t really implemented that yet on this website...). I think the best comparison I can make for how Blueprints works is very similar to the recent “Nodes” interface that Blender added to make it easier to create materials or more analog, modular sound synthesizers like Eurorack.
Code in Blueprints is made up from different nodes, which have input and output pins that can be connected to each other. So for example, the addition operator node has two input pins for each operand and one output pin for the result. By comparison, a node to get a variable value has only the single output pin. To add a variable to another, you would connect the output pins of the variable nodes to the input pins of the addition operator node. Alternatively, you could add a variable to a constant by only connecting the first input pin of the addition operator, which will reveal a text field next to the other input pin where a value can be typed in. Pins are color-coded to represent what type they are.
In addition to these input and output pins, the state-changing nodes have “execution” pins, which need to be threaded together to represent the flow of the program. So a common example might be that you have the Event Tick node that represents the start of work done on each frame, which feeds from its out execution pin to the in execution pin of a Set node that sets a value, which then feeds into another Set node, and so on. Nodes that don’t change state tend not to require execution pins. In our example, all the nodes to retrieve what property to set and what value to set it to will likely not require execution pins, but will just feed into the input pins of the Set nodes. Execution pins are drawn with a different shape from normal pins and have white “cables” connecting them.
Probably a bit hard to really get without an image, but I’m sure y’all can find one.
Anyway, what I was first surprised by is how complete this all was. I’ve found no reason yet to write an explicit C++ class. Blueprints have most features you’d expect of classes such as member variables, methods, inheritance, and interfaces. Pretty much every class or component or function you might use in C++ has a Blueprint node equivalent. I haven’t used it enough to judge performance, but from hearing others discuss it, it may actually be more performant to use Blueprints in a lot of cases. I really do feel like from what I’ve seen, you could do an entire game with never writing a line of C++.
(Editor’s note, 2024-11-05: So I found out later that what I say here about not being able to recombine branches just isn’t true. You can indeed connect two different execution wires to the same pin. I’m leaving what I wrote here though for posterity, though.)
But I do think there are a lot of issues with the coding in Blueprints. I think a good example is how you have to do
if-else
statements. There is a Branch node that takes in an execution pin and a Boolean condition. It has two output
execution pins, one for true and one for false. So that means if you want to do something in both cases, your execution
literally “splits” into two paths. This is all just the fallout of the visual system, but rather surprising to me is
that there is not really a way to combine these paths back together and continue on execution from there.
That is, for example, if you just want to do one thing or the other based on a condition and then exit, you need to have two exit nodes, most likely. An alternative is to use a Sequence node. A Sequence node first follows one execution path and when that’s done follows another. But you have to enter the Sequence node before you enter the Branch node, and then basically walk back to the Sequence node to see where it continues.
Compare this to this incredibly common pattern:
virtual void Tick(float deltaTime) override
{
if (playerVelocity > 0.0f)
{
// Do one thing
}
else
{
// Do another
}
return;
}
This is a much clearer flow to me, where you can start at the top and work your way down. The Blueprint method pretty much requires you to double back. It’s not unsurmountable but then consider something like adding a print debug statement before some code runs.
In traditional textual code, you can just add one line like:
GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, "DEBUG: Hit my Debug message!")
// Rest of code here...
and then delete it when you’re done. With Blueprints, you need to actually disconnect your existing execution pins to go off to the Print Text node and then back to your normal execution path. And then reconnect them when you’re done. This isn’t so bad if you are instead adding a message after the code in question, but do keep in mind what I mentioned before about possibly having multiple paths due to a Branch node. The first time I needed to add such a debug statement, I had to add it twice, at the end of each path. I eventually instead put my code into its own function so I would only have the single output, and then later on discovered the Sequence node workaround much like I describe above.
This is all small potatoes, but to me the most killer one is that because Blueprint files are binary files, they are completely opaque to traditional source control diffing. I did find a link to a different version control tool, Diversion, that has features for working with this, but like, I don’t want to use a new tool just for Unreal Engine. It’s honestly disappointing when I compare that this is not an issue in Unity or Godot. Yeah, files like textures or models will be binary files, but most of the important code and game objects will have some form of source control friendly representation.
I’m definitely intending on keeping on with only using Blueprints for now just to continue to get a taste for them, but I think I would find myself being more desirous of keeping most of my code in C++ classes in a bigger project. There are some videos further discussing what is best practice that I haven’t looked at yet, but that’s my initial leaning. Regardless, I do find it a fascinating system, and I do wager that a lot of non-programmers would be more productive in it quicker, with way less headaches.