Skip to main content

Better Blueprints: Efficient Data

··11 mins
Table of Contents

Recently, I’ve extensively covered the career topic. Now it’s time to go back to the actual work ;))

In this new series, I’d like to focus on Blueprints and how they can be improved. This visual scripting language is still software! That’s a domain with decades of accumulated knowledge. I’m going to introduce you to many practical concepts from software engineering that are too often ommited by Unreal-oriented game devs. Let’s begin with… data!

Understanding your data is the first step towards high-performing code. A typical gameplay code juggles lots of values and text all of the time. Rarely will you encounter a purely computation-heavy loop, like you would in ray tracing or fluid simulations - and even there, data layouts matter a lot. In Blueprint, taking care of data means choosing the best variable types for each task. This purpose of this article is to inform your choices but also explain why.

Then I’m going to discuss how to reference other Blueprints - yes, soft references and interfaces rather than hard links. That’s Unreal-specific but it can make or break your game’s final performance. We’re talking of hundreds of MBs (not) wasted per frame. Especially so if you display the player’s character in the main menu 😁

In the article, I refer to the effects on 📦 memory and 🏎️ execution time. And sometimes warn you ⚠️ or provide a smart tip 💡Okay, let’s go

Types of variables in Blueprint
#

While everything in computing is just electricity in the end (0 or 1), the right choice of data structures is crucial at higher abstraction levels. Code needs some fixed assumptions to crunch through data faster. A data type provides a predefined way to interpret the value stored in a variable.

We can roughly categorize the types in Blueprint into:

  • Fixed-size: Their size in bits is always the same per type, regardless of their content. These are flags and numbers, like Boolean, Float, Integer. Their specialization allows the programmer to choose between decimal places, precision or minimal memory usage. More complex yet still fixed-size types like Vector are meant to simplify operations in the 3D space by grouping float values together.
  • Variable-size: String and Text. The engine can’t predict how many characters will they contain. Name is a special case, covered later.
  • Containers: Array, Set and Map. All of them were built with a different kind of data access in mind. I’m explaining them in detail in a dedicated section.
  • Structures (structs): I think of them as a separate category, as they can contain any other types inside. Therefore, their total memory usage can be either fixed or varying, depending on the structure.
  • References: Called Object Types in Unreal’s UI, these are “pointers” onto objects, i.e. instances of classes. It’s just an address, not a copy of the entire thing.

Fixed-size data types
#

  • Boolean: Probably 8 bits (can’t align memory below a full byte). Simple true/false values, used for condition checks.
  • Byte: 8 bits. Just a quarter of integer’s size, it can represents a number between 0 and 255.
  • Integer and Integer64: 32 and 64 bits. Whole numbers for counters, indices, etc. The latter can represent a longer range of values. They are fantastic for storing 32 or 64 booleans too! (It’s called a bitmask.)
  • Float: 64 bits. Decimal numbers for precise calculations. Since UE 5, Float provides twice the usual precision, at the expense of more memory (akin to the type called double in C++).
  • Vector: 3 x 64 bits. Represents 3D coordinates, common in game development and graphics.
  • Rotator: 3 x 64 bits. Used for handling rotations.
  • Transform: My guess is 768 bits (see footnotes). A position, rotation and scale, packed into a single structure.

📦 Have you tried using Byte in Blueprint? How often is the 0-255 range more than enough for your use case? Just be careful with negative indexes or subtraction, because Byte can’t represent anything below zero. It will wrap to 255 😬

🏎️ If you have a big tree of Boolean operations (if that and that but not that), you can probably think it through and combine some of those tests. Make yourself familiar with the boolean logic. Often it also pays off to replace a chain of subsequent Branch function calls with operations on pure booleans first, then feeding the result to a single Branch. Additionally, put checks that are “usually negative” at the beginning. That way you will maximize the chances that huge pieces of code will be skipped early.

🏎️ The Vector type provides you with a lot of built-in functions, like averaging or the dot product. These are written in C++ by Epic, so much faster than whatever you could do in Blueprint nodes! Take advantage of that. Spend a while to learn what each type has to offer.

Text types
#

  • String: Editable text, good for user input.
  • Text: Localizable strings, for multi-language support.
  • Name: Efficient for internal IDs. Comparisons are extremely fast.

📦 🏎️ Be careful with doing too many separate operations on Strings, for example Append. These will create intermediate new Strings in memory, which will hang there for a while. If done in a heavy loop, or on long strings, that can awfully harm memory usage for a few frames. Very often, you can rewrite your Blueprint in a way that does the same logic with fewer function calls.

📦 🏎️ Do you know about the Name type? String comparisons and its Find function are slow, because in the worst case they need to process all of their characters. If the text to compare is short and fixed - for example, item IDs in player’s inventory - use Names instead. These are variables that store their text, ASCII-only, just once in the memory, in a huge global table. Then, a Name interally becomes a simple integer pointing to that entry. The consequence is that Names can’t be edited or appended to (it creates a new one). But it makes any comparisons blazing fast! Just remember to use them as Names, avoiding unnecessary conversions to String or Text.

Choosing the right container
#

Array, Set and Map are all containers. They have surprisingly similar feature sets - providing Add, Find, Remove and such. So where do they differ? Well, in the internal structure and algorithms. The way they’re build is tweaked towards specific use cases:

  • Array: Indexed collections, great for ordered lists. Fast Get by a numeric index. Slow Find by value.
  • Set: Unique items only, no specific order. Fast Find by value.
  • Map: Key-value pairs, useful for lookups. Fast Find by key, which often is a Name (but it supports many types).

When deciding between using an Array, a Set or a Map, think of the simplest, elegant solution to the problem. Consider which operations and features do you need the most. Will the code search in them often? Does the order of the elements matter? That should naturally lead you towards a decently optimal choice for the situation.

The choice of the container type will have a visible impact when dealing with hundreds or thousands of items. Don’t spend too much energy if the problem involves just a few dozen of numeric items. Still, understranding these types is beneficial - you’ll naturally choose the best one without much thinking.

Imagine (or count) how many times the code will need to run a certain function. “Unoptimal” is a very contextual statement - an Array of 10 elements is trivial no matter which operation is performed on it. On the contrary, collecting 1000s of paths (Strings) while scanning the game’s depot, then checking if certain assets exists there, may see a tenfold speed improvement when a Set is used instead of an Array.

That said, don’t jump in to hastily replace every accidental Array with a Map. You may break things - and spend time on stuff that didn’t matter. You’ll just do better next time ;)

Don’t overengineer some elaborate, counterintuitive schemes in Blueprint. Such complexity will only slow you down when maintaining the system later. You may also want to reconsider the game design of the specific mechanic, or the scope of the problem (“Does it need to test against all NPCs in the world?”). If you are really blocked perf-wise by data management, and are certain it’s a good design, you would be better off moving that part of the code to C++.

The one place that is worth the effort of a refactor are the “hot paths”. These are the slowest, or most frequently run, parts of your code. Usually these are some loops iterating over thousands of entries. That could be often a search for a specific item in a big container. The best way to seek hot paths - and verify the improvement - is to measure it. The process is called profiling. UE5 provides a dedicated profiler called Unreal Insights. Search for the docs or watch the Unreal Fest session by Ari Arnbjörnsson.

Now let’s go type by type in detail.

Array
#

Array is an indexed collection of items. They are efficient for indexed access (the Get function) and iteration. An Array automatically assigns subsequent numerical indices to items, starting from 0. There can be no gap between indices. They are ideal for scenarios where the order of items is crucial and the operations mostly involve adding or accessing items by index. However, they can be slow for search operations (Find).

Arrays allow duplicates, while Sets and Maps do not. This property can be useful when you need to manage collections of items where duplicates are either likely or necessary, such as logging events or storing user actions.

🏎️ Adding an item to an array is fast. The new item is simply appended to the end.

🏎️ Getting an item by index is straightforward and quick. It’s just a matter of accessing the item at the specified index, which involves a trivial integer-based address shift.

🐢 Finding an item is expensive as hell. That’s because it requires searching through the array and comparing each item against the key until a match is found. This means all items must be read and compared, which can be computationally expensive if the array is large or the item type is complex.

Searching in an array can be cheap if the array is short or the elements are of a simple type (e.g. an Integer).

Set
#

Sets are optimal for managing collections of unique items where the order of items is not important. Their hashing mechanism makes them excellent for fast lookup operations.

🏎️ They use a simple hashing mechanism for each element (generating a short, unique key, based on the item’s content). The hashes make the search operations very fast - because for every item, it only needs perform a basic comparison of integers. An Array would need to compare the full, potentially complex item.

This efficiency makes Sets ideal for collections where uniqueness is required (or not an issue) and lookup speed is critical. For example, they are good for storing unique file paths scanned from a directory to ensure no duplicates. That’s because comparing long strings character by character would be computationally expensive.

🏎️ Additionally, adding items to a Set is inherently an Add Unique operation. Unlike Arrays, which must iterate through each element to check for duplicates, adding an item to a Set is a straightforward operation.

📦 The fact that every item need to store a hash too means that there’s an extra cost in memory, compared to Arrays. Therefore, an Array is often a better choice when the value type is simple (e.g. an Integer or a Float).

Map
#

Maps are ideal when you need to associate unique keys with values and fast retrieval based on those keys is required. They are particularly useful for data structures like player inventories in games, where each item (key) is associated with its quantity or properties (value).

Maps allow for retrieval of the original key data. While the hash in a Set is hidden (internal only), a Map provides the programmer with a choice of an almost arbitrary type of the key. Some practical ones include:

  • Name - when storing game balance data. Example: Map called UnitStats and keys "Engineer", "Sniper", "Medic"
  • String - a similar case. Less efficient than Name but useful when dealing with user-generated or dynamic data.
  • Integer - for situation where they may be huge gaps between indexes. In that situation an Array would waste too many unused items in between. Example: Map called NamePerID, with key-value pairs: 1001: "Olivia", 2054: "Ye-ji", 31006: "Timothy"
  • Int Point - it’s an easy (if a bit hacky) way to build a 3D grid. Example: Map called DamagePerCell, with key-value pairs like: {2, 0, -1}: 0.9932

📦 🏎️ The feature of having arbitary keys makes it heavier then Set - both for the time it takes to add a new element, as well for the storage. That, however, depends on the size (complexity) of the key type. The extra memory may not matter at all, while providing benefits for code simplicity.

Data: continued
#

There’s still a lot to cover in the topic. We’ve got the basics, so then I need you to understand the dangers of Cast-ing and hard references. We’ll mitigate that with a Soft Reference mechanism. That’s even before we head off to other BP aspects beyond data!


Footnotes
#

1 - Types of variables in Blueprint

  • Blueprint’s Float used to be C++ float (32-bit). However, that was changed due to implementation of Large World Coordinates (LWC) around UE 5.1
  • FVector is a union of 3 components using double. See Engine\Source\Runtime\Core\Public\Math\MathFwd.h
  • TTransform is composed of Translation, Rotation (as a quaternion) and Scale3D. All three fields seem to use the same vector type, despite “wasting” the 4th component except the Rotation. I guess it improves bit alignment. See Engine\Source\Core\Public\Math\TransformVectorized.h. TTransform is using double in the main type (see MathFwd.h as above)
  • TRotator has 3 fields (pitch, yaw and roll), all of them double