This repository has 2 things that help with multi-version support in Unreal code plugins:
- The
VersionMacros.h
header file - Prebuild scripts that work around preprocessor limitations in UnrealHeaderTool
Both are optional and independent of one another. Installing this to your own plugins involves copying the relevant files over and modifying them. The VersionMacros plugin itself is for testing/documentation purposes.
I use these macros and prebuild scripts on my own plugins across various versions of UE4 and UE5. Sometimes I needed to backport modern UE features, which is usually not possible with preprocessor macros alone.
VersionMacros.h
provides the following macros for engine version comparisons.
UE_VERSION_BELOW(major, minor)
UE_VERSION_ABOVE(major, minor)
UE_VERSION_EQUAL(major, minor)
UE_VERSION_MINIMUM(major, minor)
UE_VERSION_MAXIMUM(major, minor)
UE_VERSION_WITHIN(major_min, minor_min, major_max, minor_max)
My all-time favorites are UE_VERSION_MINIMUM
and UE_VERSION_MAXIMUM
, but I've used all of them at some point or another.
A notable limitation of preprocessor macros in Unreal is you can't wrap "magic" macros (UCLASS
, USTRUCT
, UPROPERTY
, UFUNCTION
) in preprocessor logic. However, UnrealHeaderTool allows #if 1
and #if 0
to wrap their "magic" macros. Prebuild scripts take advantage of that to work around those limitations.
Another common compatibility issue between UE4/UE5 is TObjectPtr
. The prebuild scripts have a feature to automatically convert those to raw pointers in UE4 builds.
When configured in your .uplugin
file, prebuild scripts will automatically execute whenever you compile.
PrebuildConfig.py
gives you options to take advantage of this, plus some other goodies:
MacroReplacements
is a dictionary where you can configure "fake" version macros of the form#if <0 or 1> // MY_CUSTOM_MACRO
. The prebuild script will automatically change matching code lines between0
and1
depending on your engine version.CustomPrebuildHeaders
is a list of header file paths to auto-generateMacroReplacements
for. It will only consider simple#define
directives that use theUE_VERSION_*
macros seen inVersionMacros.h
. It does not actually compile the header file, so complex macros that use arithmatic or logical operators will be ignored.AllowDynamicVersionMacroReplacements
will interpret lines of the form#if <0 or 1> // UE_VERSION_*(major,minor)
to match the macros defined inVersionMacros.h
and change between1
and0
according to your engine version.AllowObjectPtrReplacements
provides backward/forward compatibility withTObjectPtr
, which is a common issue for UE4/UE5 cross-compatibility. In UE4 it will replace allTObjectPtr<T>
withT* /* TObjectPtr */
. In UE5 it will replace allT* /* TObjectPtr */
withTObjectPtr<T>
.
By default, all features are enabled. Disable the ones you don't need to speed up the prebuild phase.
Here's how it works:
- When you start a build, Unreal parses your
.uplugin
file and execute its"PreBuildSteps"
in your host platform shell. PreBuildSteps
exports variables from Unreal to the host shell environment so scripts can access them. The relevant variables areEngineDir
andPluginDir
.PreBuildSteps
executes the shim script contained inResources/BuildScripts/<HostPlatform>/
. On Windows this is a Powershell script. On Mac/Linux it's a Bash script.- The shim script first deduces your Unreal Engine version using the
Build.version
file in your engine directory. - The shim script then deduces a reliable Python executable location. On Windows, it will use the
python.exe
that's bundled with Unreal according to your engine version. On Mac/Linux, it will search for an executable namedpython3
orpython
using your environmentPATH
. - The shim script then executes
Prebuild.py
. Prebuild.py
performs text replacements in your plugin source files according to your engine version and your settings inPrebuildConfig.py
.
The benefit of using PreBuildSteps
is your plugin can safely be copy/pasted from a newer version of Unreal to an older one (and vice versa) and still compile! At least as long as you're diligent about #if
ing out newer dependency references in your .Build.cs
files and using the Optional
field for newer dependencies in your .uplugin
file "Plugins"
section.
The VersionMacros plugin is not meant to be installed in your project. Instead, you should copy the parts you need to the plugins you wish to use it on. This is explained in greater detail below.
The VersionMacros.h
header file is located in Source/VersionMacros/Public/
and is meant to be copied to your plugin Source/<PluginName>/Public/
folder.
To add the prebuild scripts to your own plugin:
- Copy the
Resources/BuildScripts/
folder to your pluginResources/
folder. - Copy the
"PreBuildSteps"
section fromVersionMacros.uplugin
to your.uplugin
file. - Modify the prebuild scripts as needed for your plugin. Use the
PrebuildConfig.py
file to customize to your project needs. Disable the features you don't need to improve prebuild performance. - (Optional) Add a
Prebuild.h
file to your plugin and updatePrebuildConfig.py
to point to it in theCustomPrebuildHeaders
list. This plugin comes with a samplePrebuildTemplate.h
file.
Starting in UE 5.4, the declaration of FHitResult
was moved to a new header file.
#if UE_VERSION_MINIMUM(5,4)
#include "Engine/HitResult.h"
#endif
Starting in UE 5.3, FMessageDialog::Open
had a signature change that broke my build.
#if UE_VERSION_MINIMUM(5,3)
const EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, Message, Title);
#else
const EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, Message, &Title);
#endif
Before UE 4.24, bReplicates
was the way to inform a component/actor that it should replicate. This was later replaced with SetIsReplicatedByDefault
.
#if UE_VERSION_BELOW(4,24)
bReplicates = true;
#else
SetIsReplicatedByDefault(true);
#endif
There was a concatenation operator missing for FString
/FStringView
until UE 4.27, and the necessary implementation for it also differed in UE 4.24. FStringView
didn't exist until 4.24 so I was able to build this overload into my backported implementation in 4.23 and lower.
#if UE_VERSION_WITHIN(4,24, 4,27)
FString operator+(const FStringView& Lhs, const FString& RHS)
{
#if UE_VERSION_EQUAL(4,24)
return Lhs.ToString() + RHS;
#else
return FString(Lhs.GetData()) + RHS;
#endif
}
#endif
UE 4.24 was the last version you could access APlayerState::bIsABot
directly, after which it was deprecated and replaced with APlayerState::IsABot()
, so I made this utility function to wrap that.
static bool IsABot(APlayerState* PS)
{
#if UE_VERSION_MAXIMUM(4,24)
return IsValid(PS) && PS->bIsABot;
#else
return IsValid(PS) && PS->IsABot();
#endif
}
Let's say I've defined SHOULD_MY_PROPERTY_EXIST
as UE_VERSION_MINIMUM(5,5)
and want to hide a specific UPROPERTY
on earlier engine versions.
The following would give a compile error:
#if SHOULD_MY_PROPERTY_EXIST
UPROPERTY()
bool MyProperty = true;
#endif
Sadly, UnrealHeaderTool forbids it. I'm not sure why exactly, but I'm sure Epic had their reasons.
To overcome this limitation when supporting plugins that span many versions of Unreal, I do something like this instead:
#if 1 // SHOULD_MY_PROPERTY_EXIST
UPROPERTY()
bool MyProperty = true;
#endif
The above code is allowed! But for it to have a meaningful effect, you need to use a prebuild script to ensure it changes that 1
to a 0
and vice versa depending on your version of Unreal.
Example MacroReplacements
in PrebuildConfig.py
:
MacroReplacements = {
"SHOULD_MY_PROPERTY_EXIST": {
"Version": "5.5",
"Compare": '>='
},
# More of these "fake" macro configurations can be added
}
The above configuration will do the following:
- Automatically replace
#if 0 // SHOULD_MY_PROPERTY_EXIST
with#if 1 // SHOULD_MY_PROPERTY_EXIST
inside header files anytime I start a build on Unreal 5.5 and later. - Automatically replace
#if 1 // SHOULD_MY_PROPERTY_EXIST
with#if 0 // SHOULD_MY_PROPERTY_EXIST
inside header files anytime I start a build on Unreal 5.4 and earlier.
UTickableWorldSubsystem
didn't get introduced until UE 4.27, so I created a backport of it for earlier versions of Unreal. The problem is UObject
types need a UCLASS()
macro, so I couldn't do the following:
#define COMPAT_TICKABLE_WORLD_SUBSYSTEM UE_VERSION_MAXIMUM(4,26)
#if COMPAT_TICKABLE_WORLD_SUBSYSTEM
UCLASS()
class UTickableWorldSubsystem : public UDynamicSubsystem
{
// backported declarations
};
#endif
Prebuild scripts helped me work around this.
Example WorldSubsystemCompat.h
file:
#define COMPAT_TICKABLE_WORLD_SUBSYSTEM UE_VERSION_MAXIMUM(4,26)
#if 0 // COMPAT_TICKABLE_WORLD_SUBSYSTEM
UCLASS()
class UTickableWorldSubsystem : public UDynamicSubsystem
{
// backported declarations
};
#endif
Example MacroReplacements
in PrebuildConfig.py
:
MacroReplacements = {
"COMPAT_TICKABLE_WORLD_SUBSYSTEM": {
"Version": "4.26",
"Compare": '<=',
"MatchFiles": [r'.*/WorldSubsystemCompat.h']
},
# More of these "fake" macro configurations can be added
}
The above configuration will do the following:
- Automatically replace
#if 0 // COMPAT_TICKABLE_WORLD_SUBSYSTEM
with#if 1 // COMPAT_TICKABLE_WORLD_SUBSYSTEM
inside theWorldSubsystemCompat.h
file anytime I start a build on Unreal 4.26 and lower. - Automatically replace
#if 1 // COMPAT_TICKABLE_WORLD_SUBSYSTEM
with#if 0 // COMPAT_TICKABLE_WORLD_SUBSYSTEM
inside theWorldSubsystemCompat.h
file anytime I start a build on Unreal 4.27 and higher.
Some other possible use-cases:
- Adding version-specific
UFUNCTION
/USTRUCT
declarations - Changing how a
UPROPERTY
is declared, such as whether to useTObjectPtr<>
(introduced in UE 5.0) vs a raw pointer
File Handling:
SourceFileEncoding
is passed toio.open
as theencoding
option when reading/writing source files.EncodingErrorHandling
is passed toio.open
as theerrors
option when reading/writing source files.ProcessDirs
is a list of directories to recursively perform replacements in. The more specific you are here, the faster the prebuild script will complete. By default, it does replacements in every file under the pluginSource
directory. It's not a bad idea to replace that with more specific directories with files you care about.MatchHeaderFiles
is a regex pattern list for header files (.h
). These are used to determine which files to perform "fake" macro replacements in by default.MatchImplementationFiles
is a regex pattern list for implementation files (.cpp
). These are used in conjunction withMatchHeaderFiles
to determine which files to performTObjectPtr
replacements in.MatchAllSourceFiles
is the combination ofMatchHeaderFiles
andMatchImplementationFiles
.
VersionMacros.h
should work on all platforms.
Prebuild script platforms tested so far:
- Win64
- Mac
- Linux
Please file an issue if you run into a platform where this doesn't work.
I've tested this in UE versions 4.12 to 5.x, but it should work in lower versions as well.
Support Notes:
- UE 4.14 sometimes has problems with wrapping entire
UCLASS
declarations in#if 0
/#if 1
preprocessor blocks, but can usually be resolved with a clean rebuild. - UE 4.12 and 4.13 wouldn't compile any
UCLASS
/USTRUCT
declarations for me, so I have those test cases disabled on those versions of Unreal until I find out why. - I'm unable to test UE 4.10 and 4.11 until I can find a Visual Studio 2015 installer that doesn't include Update 3, which breaks builds on those versions of UE.
- I'm unable to test UE 4.9 and lower. I was unable to install Visual Studio 2013 on my system due to an unspecified conflict.
- UE 4.8 and lower do not bundle a Python executable on Windows, so you'll need Python installed and in your environment
PATH
in order to run the prebuild scripts.
Please file an issue if you run into a version of Unreal where this doesn't work.
If this was helpful for one of your projects, consider leaving a thank-you note in the issues. :)
My website: sbseltzer.net