Learned This Week
Overview
This week was primarily focused on the benefits of C++ over blueprint and the performance differential between the two. We had fall break this week (yay!) so the information covered in the lessons this week was less than normal.
In the GameDev.tv course this week, I went over Unreal's implementation of some common C++ features like constructors, arrays, and loops. I started applying those principles to the first part of the level where the player needs to pick up a gargoyle and bring it to a niche in the far wall of the room in order to open the wall to expose a secret passageway leading down into the crypt. While the course implements this using actor tags and loops to identify overlapping actors, I'd certainly not want to implement such a system this way as it's far too performance heavy to constantly be checking tags, but I understand it in the context of demonstrating to the learner how loops work and how tags can be used as identifiers.
In the Balancing Blueprint & C++ course, my learning was more centered around identifying common programming pitfalls/mistakes and comparing the performance and benefits of C++ to blueprint. As some of the mistakes that were identified over the course of this week in the template project are a bit more complicated than meets the eye, I'll be working next week to apply the learning and improve the template project code.
Key Points:
- Constructors
- Much like in standard C++, constructors of classes get called before anything else, whenever an instance of the class is "initialized." In Unreal's case, this is when the world is loaded - constructors get called both in-editor and while playing (before BeginPlay gets called).
- Constructors in C++ are used to set up things like sub-components/objects, default values, etc. and function similarly to the Construction Script in blueprint.
- It's worth noting that while standard C++ supports constructor overloading, Unreal will only ever directly call the simple constructor for the class, as its reflection system doesn't pass any parameters to constructors.
- TArrays
- Unreal's version of a C++ array, TArrays (template arrays) are declared with a type and can differ in size at runtime. They have a variety of helpful functions to operate on and interact with their contents and are recognized by Unreal's reflection system.
- TArrays function the same way as normal C++ arrays in terms of accessing them and changing their contents, but function better inside of Unreal's reflection system.
- Loops
- While, For, and Range-Based For Loops all function the same way in Unreal as they do in standard C++. However, unlike arrays, Unreal doesn't have a special version of them - all you need to do is know how to implement them in C++, and you're good to go!
- C++ Performance
- As a blanket statement, C++ is faster than blueprint. This is due to many factors, but when you get down to it, C++ is simply running closer to the bare metal than blueprint is, and pushes data to Unreal rather than gathering data from it.
- Computationally, C++ is several orders of magnitude faster than blueprint (see the tests performed below) but is not necessarily always applicable. While it certainly is faster, it's uncommon to always need your code to be so fast - often times for common and simple code, the speed difference won't matter.
- Benefits of C++
- To backtrack on my previous sentence, there are a few situations when the speed absolutely does matter. To evaluate whether your code could be implemented in either system or whether it should probably be in C++, here are some of the real benefits of C++:
- Speed/Performance
- Perhaps one of the biggest advantages, C++ excels at running quickly. However, with most simple code, the differential between blueprint and C++ is negligible. Sometimes, blueprint may just be easier.
- Maintenance/Code Readability
- With complex code and large projects, it's often better to use C++ as it is easier to understand and is often less visually complex. That being said, some people just have a better visual understanding of nodes and connectors rather than code.
- Networking & Replication
- Blueprint has a fairly comprehensive network replication system, but it has its drawbacks. Blueprint replication is boolean - it's either on or off for each blueprint. With complex network setups, C++ is not only better, it's almost required in order to keep network speeds snappy, as C++ allows the developer to directly edit and designate what gets replicated and how.
- Complex AI
- Complex AI deals with complex computation. As aforementioned, C++ is orders of magnitude faster than blueprint and when split-second computation is required (like with AI), the ball is firmly in the court of C++.
- Working with Plugins
- C++ has the advantage here when working with code that needs to be transferable into any project - plugins are almost exclusively written in C++ and, while they may contain blueprint content, should mainly stay there.
- Overriding Functionality
- C++ fully supports each principle of Object-Oriented Programming, so its ability to designate overridden (and overloaded) functions is simply unparalleled by blueprint. Additionally, C++ allows the user to override Unreal's function library to be used in ways that you need it to be with your own custom classes.
Nitty-Gritty
- Unreal 5: Learn C++ GameDev.tv Course - Jump to Top
- Constructors
- Functions called whenever a new instance of a class is created, usually used to set the default values and attach any additional required components
- Constructors get called by the engine while it's creating the world (constructors will always be called before BeginPlay)
- While in the editor (and not playing in-editor), constructors are called whenever changes are made to the instance of the class (like placing or moving it in the world)
- Functions similarly to the Construction Script in blueprint
- TArray
- TArrays (template arrays) are declared with a type, and can have a changing number of elements added to/removed from them during runtime
- We're going to use TArrays in order to get information about what actors are inside the trigger component that we set up last week
- To do this, we can use the GetOverlappingActors function on UPrimitiveComponents which returns a TArray (or a TSet depending on its implementation) as an out parameter
- We can use this with a TArray of AActor pointers like so:

- TArrays function the same way as a normal array access-wise (i.e. .Num() returns the number of elements, the elements count up from 0, and elements can be accessed directly using angle bracket notation [#])
- While & For Loops
- Loops are helpful to use with sets of data that change in order to adapt your code to still work with variable data
- Unreal implementations of while & for loops doesn't differ from generic C++ implementations
- For example, one way to access all elements in the array and do the same thing for each one of them (in this case, printing them) would be to stick the print statement within a while or a for loop
- While loop implementation

- For loop implementation

- Range-Based For Loops
- While continuously testing booleans does what we need for looping through an array, we can use range-based for loops to make it cleaner
- As with while and for, Unreal doesn't have any different implementation of the range-based for loop than C++, so our loop would look like this:

- Actor Tags
- We want our trigger component to be able to identify the gargoyle when it is placed within its bounds, and one of the ways (note: one of the ways is crucial here, because this is certainly not the most efficient way to do this) is to add a tag to the actor and have the trigger test off that tag

- Once added, we can query in our range-based loop to see if the actor that's overlapping has the tag we're testing against which is pretty simple to do:

- We can also make the actor tag the trigger is checking for extensible by making it a UPROPERTY so that the TriggerComponent can be reused for other locations with different tags to do the same thing
- Early Returns
- Just like in blueprint and Python and regular C++, early return statements can be used to skip code blocks and exit out of the function if a certain condition is met/not met
- By shifting the code we've created into a separate function, we can create an early return once we've found an actor that matches the tag we want to check:

- We can then use this in our component's tick function to print out whether it has been found or not:

- Balancing Blueprint and C++ in Game Development Course - Jump to Top
- Referencing Assets
- Officially Recommended Development Approaches
- Unreal has a great reference page for common game features and when to implement them with blueprint vs. C++ here
- The documentation also includes things to avoid and be careful of, some of which is important to highlight because the template project currently does them and we're going to go fix them:
- Avoid referencing assets by string/name: soft object is much more secure and string references are completely untracked by the cooker, and can cause issues is packaged games
- Avoid referencing assets from C++ classes: you can use FObjectFinder and FClassFinder in the constructor of a C++ class, but assets loaded this way are done so on project startup and can cause memory issues if the references are not really needed
- Currently, the pawn in our template is referencing an asset for the static mesh for the ship in its constructor using a string which points to the file location:

- Additionally, the projectile class does the exact same thing for its static mesh:

- Our problem now is that those references rely entirely upon the naming structure of every file and folder in that path staying exactly the same always, all the way through packaging the game, and that almost never happens due to the way that teams work together, optimize, and create new assets/versions of assets (and our code should support a good working environment!)
- To avoid this, we should use soft object paths/pointers - something which gets complex quickly, so we'll go over that next week
- Flexible GameModes
- In Unreal, GameModes determine the basic rules of the game you're creating along with simple information about it such as the classes that are spawned and used for player input, spectator input, etc.
- The GameMode that's currently in the project only sets one thing - a hard reference to the pawn we're using for the project as its DefaultPawnClass (meaning the pawn that will be spawned for the player at the PlayerStart):

- The problem with this is that it doesn't allow for easy editing/changing and flexibility in the GameMode, as any changes would need to be made in C++ and cannot be done from the editor
- We can derive a Blueprint class from the C++ class in order to make it editable in the editor (and add blueprint code to it)
- C++ Performance
- In general, C++ is more performant than blueprint - that's just a fact of how the editor runs and the way that blueprint is compiled
- However, it's not always extremely out-performant - we can test the two with the same test (implemented in purely blueprint and purely C++) in order to see the difference
- A good test for this is a common one in programming; running a loop that gets the total resulting sum of every number plus the number before it (i.e. for loop number 5, it runs through 5 loops 5 times), so it's running two different loop functions (the C++ code is easiest to understand for this, its code is below)

- The reason this is a good test is because it scales exponentially, and at higher values, the difference between the performance of each implementation should be more visible
- The tests displayed in the course are split across one with fully C++ code, one with fully blueprint code, and one mixed (using blueprint for the outer loop (the GetTotalSum function) and running C++ BlueprintCallable functions for the Sum function)
- The results were as such:
- At lower values, all three implementations took about the same time, as the calculations are simple and the number of loops is low
- Once the test reaches about 40 or 50 loops, blueprint starts to lag behind with ~1 millisecond of calculation time as opposed to ~0ms for both the C++ and mixed implementations
- When dealing with much larger numbers (around 200 loops), it becomes very clear that C++ is faster, with C++ and the mixed implementations still hovering around ~0ms and the blueprint implementation at about 19ms
- When pushing the envelope of what blueprint can handle (around about 500 loops), the pure C++ implementation is still going strong around about 0ms, while the mixed hit about 2ms, with the blueprint crashing into 117ms - the kind of time strain which will cause a performance hit for the engine
- Going up to 1000 loops, C++ finally hits the 1ms mark, with the mixed coming in around about 3ms, and the blueprint implementation crashing the game because it classifies the performance impact as too high and "detects" an infinite loop
- (In fact, when running the code in pure C++ outside of the editor, you can push it up to about 10000 and it still only hits about 3-4ms)
- The results themselves don't show that C++ is always better, but rather that it's important to know when to use blueprint and when to use C++ - for some things, blueprint is simply easier, and for others, C++ is better
- Additionally, it's not always required by a program to do this level of calculation (in fact, if you're running through 1000 loops 1000 times, you've probably coded something wrong) so it's good to keep these sorts of tests in mind, but not use them as a catch-all argument for why one is better
- You can take advantage of mixing blueprint and C++ by creating BlueprintCallable functions for any function which could be simply created in C++ to make your blueprints faster
- Real Benefits of C++
- Performance
- There's no real argument - C++ will outperform blueprint in most things, but the question that comes into play is whether that is actually relevant for what your code needs to do
- Maintenance & Code Readability
- When working in larger teams, it's usually better to use C++ for complex code as it's easier to read, bring others up to speed on, conceptualize, and maintain in a code base
- With blueprint functions that call other blueprint functions, it can often take a while to conceptualize what the outermost function does as a hole, and this cannot be done at a glance
- C++ often (not always) takes up less space in terms of the code it requires (for example, some functions that take a large space in blueprint can be simplified to about one or two lines in C++) which allows readers to identify what's going on and read through it at a glance
- Networking and Code Replication
- While blueprint provides a lot of functionality when working with basic networking, the level of control that C++ provides over specifically what is replicated is just not there, and on larger projects is vital to making things run quickly
- Complex AI
- Large-scale AI calculations are going to hit that wall of blueprint performance, they will need to be in C++ in order to run quickly and efficiently
- Working with Plugins
- C++ is a great way to export and share logic across projects, and plugins need to be able to adapt to whatever project they're used with
- Overriding Default Class Functionality
- The principles of Object-Oriented Programming are fully supported by C++, and deriving, overriding, and overloading are only going to be able to be taken full advantage of if you're using C++