While working on the game Front Line Zero with the METATEK game developement studio, I’ve used the Unreal Engine 4 game engine for some years now.
I’d like to share some knowledge about the pitfalls and neat tricks I got to discover under the form of short, easily-readable blog posts.
Today’s topic : some tips on how to migrate a project from an older version of the engine to a newer one (and what were the encountered issues).
We recently ported the project of our game from the antiquated 4.17 to the newer 4.26 version of Unreal Engine. And although I always strived to use forward-compatible and avoid deprecated APIs, there were much more things to fix than we expected.
Here is a quick « whirlwind tour » of everything we had to fix so far. If you’re reading this in hope of fixing a problem that you’re having yourself, I also recommend you to check the UE 4.24 transition guide on the Epic forums – lots of good information to find there too.
Slate
With new versions, include paths have changed. Which means a lot of the includes we were doing in our code had to change because the include path didn’t compile anymore.
There is a flag that you can set in the Build.cs of your project if you want to keep using the « legacy » way of using include paths if you don’t wanna have to change your whole codebase. For this one, I decided to take a look at it and changed everything I could. It mostly boils down to also put the subdirectory in which a header file is now. Here are some examples of include paths that I had to fix (what follows is the correct include path for default settings in 4.26) :
« Framework/Docking/TabManager.h »
« Widgets/Docking/SDockTab.h »
« Widgets/Input/SCheckBox.h »
« Widgets/Layout/SScrollBox.h »
« Widgets/Text/STextBlock.h »
« Widgets/Docking/SDockTab.h »
« Widgets/Docking/SDockableTab.h »
« Widgets/Docking/SDockTabStack.h »
« Framework/Application/SlateApplication.h »
« Styling/SlateTypes.h »
« Modules/ModuleManager.h »
If you’re using any of those headers, try changing your include path to one of those and it should fix your error.
C# Build Scripts
Another big source of trouble during the migration are the build scripts. There was a lot of minor but annoying changes that led to change scripts here and there and it’s actually very time-consuming. So here is a list of everything that had to change in our build scripts.
- The global « UEBuildConfiguration » doesn’t exist anymore and most of its properties have been switched to the Target argument of the module script (cf. this link) .
if (UEBuildConfiguration.bBuildEditor == true)
had to change to :
if (Target.bBuildEditor == true)
Path.GetFullPath(UEBuildConfiguration.RelativeEnginePath)
had to change to :
Path.GetFullPath(Target.RelativeEnginePath)
- As well, it’s not possible to switch on the Target.Platform value anymore (see this link). We then had to resort to a big list of if… else if… conditions where there was a switch case previously.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
switch (Target.Platform) { //... has become : if (Target.Platform == UnrealTargetPlatform.Win32) { linkExtension = "_vc.lib"; dllExtension = ".dll"; bAddDelayLoad = true; } else if (Target.Platform == UnrealTargetPlatform.Win64) { // blabla.... |
- We also had to change or remove some deprecated modules that we depend on : for example, ClothingSystemRuntime became ClothingSystemRuntimeCommon, and the ShaderCore module completely disappeared (was probably merged in another one).
- Some settings changed in the new versions. If you want to use the new settings by default in your project, you have to manually add a line in your project’s Target.cs:
- DefaultBuildSettings = BuildSettingsVersion.V2;
- These settings are mostly good for performance. Namely, three main ones have changed default value :
ShadowVariableWarningLevel = WarningLevel.Error : This one is to make the build fail when you're shadowing the name of an already existing variable in your code with a new variable. This is a good thing as it can be bug prone.
- PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs : for IWYU-style PCH usage, so that files that don’t need PCHs don’t use them.
- bLegacyPublicIncludePaths : now set to false, not using it allows to reduce the compiler command-line length. However, in our main project we decided to keep it to true as we didn’t want to have the whole project’s include paths everywhere… Maybe someday…
Plugins
- Some things changed with Plugins setup scripts too, sometimes requiring us to fix broken scripts or warnings :
- RuntimeDependencies now only need the path and not an object to construct. So for example,
1 |
RuntimeDependencies.Add(new RuntimeDependency(fmodDllPath)); |
becomes :
1 |
RuntimeDependencies.Add(fmodDllPath); |
- Android Receipts have the same problem. So,
1 |
AdditionalPropertiesForReceipt.Add(new ReceiptProperty("AndroidPlugin", RelAPLPath)) |
- becomes:
1 |
AdditionalPropertiesForReceipt.Add("AndroidPlugin", RelAPLPath); |
- We had to rename the bFasterWithoutUnity boolean to bUseUnity.
- Similarly, the Definitions variable became PublicDefinitions, PublicLibraryPaths became PublicSystemLibraryPaths.
- A plugin previously labelled as a « Developer » plugin now has to be labelled as a « DeveloperTool » in the plugin file. You also have « UncookedOnly » if it should be usable by uncooked targets only.
In editor
We didn’t have so much problems in editor as of now, except some annoying problems, like some of our maps disappeared from the Content Browser. The umap file is still there, but it seems Unreal Engine just kinda silently failed at opening them (no warning, error or assert of any kind).
Fortunately, the fix was simple : it was enough to just run in the console the same command that the editor does every time you double click on a map asset in the content browser, replacing the file path by the one going to the faulty umap.
Then, the map just loads normally, and if you save it in the editor, it’s going to fix it up. A bit weird as it happened to only specific maps, but I don’t know why. In case you’re curious, here’s the command line :
1 |
MAP LOAD FILE="path\to\umap\file..." TEMPLATE=0 SHOWPROGRESS=1 FEATURELEVEL=3 |
On the other hand, a problem that I noticed but I wasn’t able to fix yet is that our custom level templates are gone. I used the approach from this article to add some of our custom made maps to the « New Level » menu but for some reason, it doesn’t work anymore.
I don’t think the editor changed the way it handles these, so it’s really weird. I’ll update the post if I find a solution.
In C++ Code
Then, we also had a bunch of things to fix in the C++ code of the game. Here is a rather long list of everything we had to change, some of them because the codebase of our game relied on a really old version of the engine previously (4.17 !) :
- A SequencePlayer Initialize method now takes an explicit FLevelSequenceCameraSettings parameter, and not the World but the associated ULevel now, so you have to write :
1 |
SequencePlayer->Initialize(sequenceDesc.Sequence, GetWorld()->GetCurrentLevel(), FMovieSceneSequencePlaybackSettings(), FLevelSequenceCameraSettings()); |
- The ForceGarbageCollection method is now in the global GEngine :
1 |
GetOwner()->GetWorld()->ForceGarbageCollection(true); becomes : GEngine->ForceGarbageCollection(true); |
- I had to change all of our calls to the Actor’s GetCompnentsByClass function to GetComponents because it seems now UE prefers to take the array of components as an out parameter of the function. Sad, because I preferred the version that returned the array.
So
1 |
auto const SceneComps = Actor->GetComponentsByClass(USceneComponent::StaticClass()); |
became :
1 |
TArray SceneComps;<br>Actor->GetComponents(USceneComponent::StaticClass(), SceneComps); |
- The bGenerateOverlapEvents boolean of scene components is not public anymore. You have to use the getter GetGenerateOverlapEvents(). Similarly, you cannot access the absolute transform (bAbsoluteLocation/Rotation/Scale) directly anymore. There is a getter for them, so now you should write something like :
mySpline->SetAbsolute(ownerSpline->IsUsingAbsoluteLocation(), ownerSpline->IsUsingAbsoluteRotation(), ownerSpline->IsUsingAbsoluteScale());
Similarly, you cannot get directly the ActionName of an FInputActionBinding directly. You have to use GetActionName() now.
Similarly, it’s impossible now to directly add a level to the array of a StreamingLevels in a UWorld now. You have to call AddStreamingLevel now, which is a bit cumbersome for me because it does a bit of machinery every time you add a level. The old approach had the advantage that you could add your new levels and do the notify new level stuff etc. once at the end. Now it runs on every add.
FComponentReference now store an ActorComponent instead of a SceneComponent. This is a really good thing because it now allows you to reference a component that may not have a position in space. Yet it also means that every time you fetched a SceneComponent from it, you now have to make an explicit cast, like this :
USceneComponent* laserBeamCpnt = Cast<USceneComponent>(LaserParticleRef.GetComponent(this));
The Pawn virtual function Possess is now deprecated in favor of OnPossess.
Conclusion
I hope this short list (and I’m pretty sure I’m forgetting some stuff) of everything we had to do to migrate our project to a more recent version of the engine may be of help for some readers. 🙂