10+1 Mistakes New Unreal Engine C++ Developers Make
Eleven biggest mistakes and time-wasters in my UE development journey I've made in the past.
In today's Devlog, I want to take a break from more technical discussions and talk about more big-picture things, which nevertheless greatly affect our work performance as C++ programmers.
So I have gathered a list of the eleven biggest mistakes and time-wasters in my UE development journey I've made myself in the past. And I've included a few that come up very frequently in game dev circles, too. They all reflect on critical areas of a healthy approach to programming with Unreal Engine that I wish someone had told me about when I first started. I hope my experience will be helpful to you!
Without further ado, here's the eleven:
1. Diving in to C++ head-first
It might seem counter-intuitive. But diving in head-first when we first start working with Unreal Engine is not the approach Epic Games, and I recommend for current C++ developers. It's probably better to get yourself acquainted with UE's capabilities by creating a few prototype games in Blueprint first. And only take up C++ programming when you are comfortable with the game flow and the many object classes already available.
If you are very short on time, the free online Unreal Engine Kickstart for Developers course will give you a broad overlook of the systems in about seven and a half hours of watch time. But do not skip it if you are short on time because debugging and rewriting is the real black hole of time.
2. Not using Blueprints
No Unreal Engine project should be written entirely in C++. It is not only impractical, but it might very well be infeasible. Most game designers and artists have picked up some skills with Blueprints over their time working with UE. Still, if you do not expose your functions to Blueprints or create Blueprint interfaces for your game logic, they will not contribute to your project's development.
Try to look at Blueprints as the game's content and your C++ work as the game's framework. You wouldn't want to "bake in" your gameplay logic into your game's core systems. It's far better for prototyping and iteration if we treat our gameplay logic as content, and everyone skilled in Blueprints can change it. Let alone that there are far more people in the industry skilled at Blueprints than those qualified in C++ for Unreal. Treat your time as a precious commodity, offload scripting work that others could do to other team members, and focus on implementing the core framework and more technical features of your game. That is what only your competency can do.
3. Following tutorials excessively
Unreal Engine's C++ tutorials are plentiful online and sometimes very helpful to get a sense of how things work. But they also have severe drawbacks if we rely on them as a learning tool too much. Firstly, they can be hacky โ some tutorials will not use the engine in the most compatible way. Secondly, while there are many tutorials on doing things at the introductory level, you will find far fewer guides for the advanced aspects of your game. Thirdly, following tutorials too much can lead to an incoherent structure of your project.
Over the long term, it is much faster to learn the core concepts of Unreal Engine initially and cut your dependence on tutorials. Then your classes will have a logical structure, and you will be able to debug any issue that comes along. Especially when you cross the barrier into more advanced topics that are rarely touched upon by very casual enthusiasts for whom online tutorials are written.
One exception to this rule is the Inside Unreal playlist by Epic Games on YouTube. They not only focus on best practices but also dive into some advanced stuff. And they also frequently follow Allar's Gamemakin style guide, which Epic Games use internally to keep their projects structured well. Treat them as overviews of different views toward game development with Unreal and game development in general, and you will learn a lot!
4. Reinventing the wheel
This point touches on good engine knowledge once again. As new programmers, we are prone to doing things our way if we do not know that they are already implemented in the engine. I occasionally see code that doesn't use Unreal Engine's garbage collection and people trying to dynamically link to functions from external libraries that already have their alternatives within UE. Do not try to remake or source parts of the engine yourself; it is much faster to use what is already in place and integrates well with other parts of the game. And even if the functionality is not in the engine yet, it may be available in Marketplace plugins.
Save your time and effort, and let others maintain as much of your functionality as they can. Focus on what unique aspects of your games you bring to the table and maximize your efforts there. There is a lot of wisdom in the "standing on the shoulders of giants" approach when it comes to basic game mechanics that have long been perfected, open source, and made available in the Engine or its Marketplace. And if you aren't quite sure what functionality Unreal Engine has, I would recommend circling back to point #1 and getting some practice hours before diving into C++.
5. Focusing on what you can do instead of shipping
This insight we have made in our company borrows a lot from Eric Ries' marvelous book "The Lean Startup." In it, he claims that a lot of tech entrepreneurs focus on what their companies can make instead of looking at what the customers demand, can they make it, will the customers buy it, and if the customers will buy it from them. It's an approach destined to waste time and effort, because a product that we can make but no one will buy from us is not worth a lot. In the same way, code that we can write instead of code that ships the game is not worth much and does not contribute to the bigger goals of the project, instead mostly contributing to our individual ambitions.
Focus on the big-picture items when you start working on the new project. Ask yourself: do all artists have the tools they need to ship your game? Is the user flow facilitating the game? Is some basic version of the game playable? And many such similar questions. Even if we write the most fantastic and perfect object class, if that object class is written instead of object classes that will ship the game, it is worthless. And in many cases, time spent on non-essential programming is at risk of becoming time wasted. Only code that will ship has value. And the most important parts of your game are most likely to ship.
Excerpt from CIA's Simple Sabotage Field Manual talking about sabotaging production in companies. Don't sabotage yourself on a WWII & Cold War spy scale.
5.1. A note on perfection
An old Italian proverb Il meglio รจ l'inimico del bene teaches us that perfection is the enemy of good. And if there is one thing I'd like us to learn about our performance management, it's that shipped work within a team has value while perfect work does not. Do not overengineer or rewrite your classes to be any more perfect. Deliver and only adjust when necessary to eliminate technical debt or improve performance.
6. Not using assertions from day one
Some of the top things any audience dislikes in a shipped project are bugs and crashes. If we use the many flavors of assertions in Unreal Engine from day one, our team will catch dozens of bugs throughout the development cycle, and less work will be left for the quality assurance phase, or โ perish the thought โ end users.
Going a step further, I highly recommend setting up unit tests for your classes if you work in a team with multiple programmers. Unreal even has a very functional automation system with unit test macros towards this end. But one could argue that even more important than unit tests is asserting a lot in our code. Here's an example of a function with plenty of assertions:
void UTGameInstance::SaveGame(FString& SaveName)
{
// Setup
UWorld* GameWorld = UTCommon::GetGameWorld();
checkf(GameWorld, TEXT("Failed to find Game World when attempting to save game."));
UTSaveGame* SaveGameObject = static_cast<UTSaveGame*>(UGameplayStatics::CreateSaveGameObject(UTSaveGame::StaticClass()));
checkf(SaveGameObject, TEXT("Failed to create a SGO when attempting to save game."));
// Save the (U)World
SaveGameObject->ReadGameState(GameWorld);
// Take a photograph, it is worth 16,384 (64-bit) words
APhotographer* Photographer = GameWorld->SpawnActor<APhotographer>(FVector(), FRotator());
Photographer->MatchDefaultPlayerCamera();
UTexture2D * Screenshot = Photographer->TakeScreenshot();
checkf(Screenshot, TEXT("Failed to take a screenshot when attempting to save game."));
// Save the photograph in the SGO
TEnumAsByte<EExecBranches> Outcome;
SaveGameObject->ReadScreenshotFromTexture(Screenshot, Outcome);
checkf(Outcome == EExecBranches::Success, TEXT("Failed to save screenshot to SGO."));
// Save the SGO to disk
UGameplayStatics::SaveGameToSlot(SaveGameObject, SaveName, 0);
// Leave no witnesses
verifyf(Photographer->Destroy(), TEXT("Failed to destroy photographer object while saving game."));
}
None of these checkf or verifyf assertions will be included in the shipping build, so they only impact your code's performance in the development environment. Use them a lot. Your project will crash in development, but you will not ship bugs to your players.
Alternatively, you can also use the ensureMsgf flavor of assertions in Unreal, that will trigger breakpoints in the attached debuggers but will not crash the Editor.
7. Writing without a style guide
In the words of amazing developers at Gamemakin LLC and their GitHub contributors, "a team without a style guide is no team of mine." That's because, without a style guide, code not only becomes disorganized, but our programming is far less self-documenting. And self-documenting code saves everyone's time, including our own in the distant future.
Luckily, developing a programming style more or less compliant with the industry is easy. Aside from Allar's GitHub article linked above that briefly goes into variable names in the Blueprint section, there is a standard coding reference in the official Unreal Engine Documentation. After that, the teams you will usually work on will have their style guides.
With a robust style guide in place, you will also be confident that whoever takes development over after you will find it easy to read your code, and you won't be anxious about open-sourcing it or showing it off to recruiters, too. In fact, writing self-documenting code is a factor in programmer recruitment โ no company wants to spend resources untangling spaghetti code.
8. Fixating on a Blueprint way of thinking
While it's true that 90% or more of all game code could be made in Blueprints, some algorithms like those that involve memory copy operations, those that rely on hot cache for their performance, or those for interaction with external function libraries are simply impossible without C++. Moreover, Blueprints can be copy-heavy in the way they use variables, and they can be up to 10 times slower than C++ code (not that it matters much in today's graphics-heavy and logic-light games). Still, C++ lets us optimize our performance with references, pointers, and good programming patterns beyond what Blueprint visual programmers will be accommodated to or even what is possible in Blueprints. So it's always a good idea to use these advantages to implement advanced functionality or further optimize our game's performance. Finally, many of the engine's functions aren't exposed to Blueprints, and neither do they have Blueprint alternatives. However, community members like Rama have been working towards extending Blueprint functionality quite a bit with their plugins.
Whenever you feel that something must be impossible because you weren't able to do it with Blueprints, think if there is a part of the engine you can manipulate to achieve your goal. Likely, there's already a C++ function to do it. We can go as far as directly changing memory if we want, even if writing directly to memory isn't a very sustainable way of doing things if we're going to upgrade engine versions during development. Moreover, we can always create our systems in C++. In summation, if a Turing machine can do it, but Blueprints can't โ we can do it!
Just don't reinvent the wheel.
9. Not taking breaks
Science teaches us that varied learning, like interleaved learning, will almost always yield better information retention and fundamental understanding of concepts than spending huge blocks of time attacking a problem or practicing the same thing. That's why it's probably not a great idea to spend a lot of time fixating on an issue you encounter while learning C++ for UE. If you can't solve it, take a break, and come back to it another day. Or perhaps vary your learning by taking an online class.
You might not tackle the problem at hand as fast, but you will learn to program for Unreal Engine much quicker overall. It's science.
10. Thinking about only one set of circumstances
When developing a game, it's easy to fixate on whether it runs well in the development environment or whether it runs well when developers use it in their usual flows and usage patterns. But overall, we should also be asking ourselves:
- Does our project run on all of our target platforms, and what about potential future target platforms?
- Are we using established programming patterns like RAII wherever we can to handle common edge cases?
- Are we using hardware abstraction as much as we can? This is especially important in save games and other filesystem-related things.
- If the hardware abstraction layers in UE are not enough, can we create hardware abstraction layers for our work? Can we hide the per-platform code behind reusable cross-platform libraries?
- Does our project work outside the Editor and compile when Engine code and functions wrapped in #WITH_EDITOR and similar macros are no longer present? What about other pre-processor macros?
- Does our code work within little-endian and big-endian systems?
- Are we not hard-coding asset paths or strings that are meant to be localized?
- Is our communication with all APIs according to their standards and string encoding?
- Are our third-party libraries cross-platform? If not, do they have alternatives for our target platforms?
- Does our code compile on all target platforms as-is, without any need to transmogrify?
- Are we using continuous integration to compile on all target platforms periodically and know as soon as issues arise?
- Do our target platforms have different performance characteristics that we need to take into account?
- Playing the devil's advocate, could we break the game as a user by trying non-standard behaviors and interactions?
With these questions in mind, we can estimate if we will need additional work before our game can ship. The most resource and the time-intensive task being porting to different platforms. It's not recommended to port at the end of development anymore, though. That's because, nowadays, games frequently receive post-launch patches, and I think we would all patch one codebase instead of two or more. Luckily, the games we build with Unreal Engine are mostly platform-agnostic, but only if our code is.
Bonus โ 11. Ignoring encapsulation
Unreal makes it very easy for us to pass data around different classes of objects, call one from the other, and organize our projects in a way where a single class does not encapsulate all its relevant behaviors.
The mistake of ignoring encapsulation is very easy to illustrate with a practical example. Imagine a convenience store with automatic doors and a motion sensor on each side.
Suppose we would like to replicate this arrangement with objects in our Unreal Engine project. There is a very "wrong" way of doing it in terms of encapsulation and a much better way of doing it. Let's dive into the bad practice first.
There have been many times I've seen code architecture like the following in Unreal Engine projects: there are two door objects capable of moving a mesh back and forth that make up the two sides of an automatic door. There are two sensors that have UPROPERTIES for pointers to the two-door actors, on which they call two events โ to open and to close the doors.
This might work in some circumstances, but such a design is incredibly limiting due to how coupled the different classes are. First of all, the sensor object can only open and close two doors each. It cannot turn on a light or trigger a gameplay event. Moreover, it cannot open or close one door, or three doors. Secondly, the sensor object works like an observer or a controller for the door but violating both observer and controller patterns โ there are two sensors (on the back side and the front side of the automatic door) for each of the door actors. Thirdly, if the sensors are triggered out of order, they might leave the door opened or close it too soon, as both of them have timers for closing the door after it has been opened. Finally, if we ever need to modify or extend this architecture once it's already implemented in a part of our project, we may be faced with an almost impossible task. There must be a better way!
And there is. A system like this could be encapsulated better and abstracted more. An automatic door capable of opening and closing both sides could be derived from a base door class rather than an actor class. Now each door, regardless of how many parts make it up, can open and close with the same function calls. The automatic door variety can have a timeout variable, which is reset when the door opens, decremented over time, and once it runs out, the door would close โ the sensor is no longer a controller or an observer. Finally, the sensor should not need to know if it is actuating a door, a light, or a gameplay event. The door should implement an interface such as ITriggerReceiver with a function such as OnTriggerReceived, and the sensor should communicate to it through an interface. Now this interface can be implemented on any actor, and the trigger will work with it just fine!
Okay, these are a lot of changes. Surely we can only know about this after many years of programming, right? Well, not really. There is actually a "silver bullet" rule here โ always ask yourself, "can the class I am building handle more of its own behavior and less behavior of others?". Can the door open and close by itself? Can the sensor worry about what it's doing more than what something else is doing? This one question helps us with encapsulation tremendously and ensures that even your first-year code looks bounds and leaps better designed than much of the code in the industry.
Conclusion
I hope these 10 (+1) bits of advice were helpful to you. Learning each one has led to a small breakthrough in my understanding of Unreal Engine. So I hope at least a few of them do the same to you.
Let me know in the comments below if you have any additional tips for new developers! And if you're in the mood for some more technical articles, feel free to check out some suggestions in this publication.