UE和VR开发技术笔记

Some technical notes and related materials on UE4 and VR development that I’ve casually written down, previously scattered around imzlp.com/notes. Today, I’ve organized them, and future notes will be added to this article.

Engine Documents

UE4 Online Documents

UE GamePlay Framework Tutorial

VR Devices

UE Adaptation to Different VR Devices’ Height Issues

When adapting UE for Oculus Rift and HTC Vive, make sure to change TrackingOrigin (EHMDTrackingOrigin::Type) to Floor. Otherwise, the default tracking origin is at eye level, causing the headset height to be incorrect in the game scene.

Another issue to be aware of is that when placing with PlayerStart, PlayerStart has a height, which may lead to incorrect height (depending on how your Pawn is written). A solution is to override AGameModeBase‘s SpawnDefaultPawnAtTransform to specify the height.

VR Headset Initialization When Starting UE

1
2
Stereo On/Off
Notice: Enables or Disables stereo rendering for Head Mounted Display (HMD) devices.

Resetting VR Headset Rotation and Position

Since the default orientation of the VR headset along the X-axis is related to the room’s setup, if we start off misaligned with the intended orientation during setup, entering the game will cause it to deviate from what we want the player to see. Therefore, adjustments are needed.
You can use the following function:

1
2
3
4
5
6
7
8
9
/**
* Resets orientation by setting roll and pitch to 0, assuming that current yaw is forward direction and assuming
* current position as a 'zero-point' (for positional tracking).
*
* @param Yaw (in) the desired yaw to be set after orientation reset.
* @param Options (in) specifies either position, orientation or both should be reset.
*/
UFUNCTION(BlueprintCallable, Category="Input|HeadMountedDisplay")
static void ResetOrientationAndPosition(float Yaw = 0.f, EOrientPositionSelector::Type Options = EOrientPositionSelector::OrientationAndPosition);

There is also a node with the same name in Blueprints.

Using Oculus Rift in UE

After installing the Oculus Rift device, you need to enable Unknown Source in the Oculus Store; otherwise, it cannot be previewed in UE.

The relevant page in Unreal’s documentation for adapting Oculus Rift: Developing for Oculus Rift.

Oculus official key operation introduction:

And the key mapping for Oculus Rift in UE:

HTC Vive Key Mapping

For comparison with the above Oculus Rift, here is an HTC Vive key mapping:

HTC VIVE Device Setup



SteamVR 2.0 Base Station Positioning

The HTC Vive Pro supports SteamVR 2.0, which allows tracking to cover 150° and 7 meters, and supports daisy chaining of base stations (up to 16) for large space positioning solutions.

Note: The second-generation base stations do not support the first-generation HTC Vive headsets.

Using Proxifier to Route Oculus Traffic through SS Proxy

Yesterday, the company bought an Oculus Rift device, and during the installation, a global proxy was needed. On Windows, I used Proxifier to route the Oculus-related software through the proxy.
First, add a proxy in Proxifier:

Once the connection test is successful, you can proceed with the following operations.

After a successful connection, an additional proxy rule can be added. The relevant processes for the Oculus programs are as follows; what we need to do is route these processes through the SS proxy:

1
OculusSetup.exe;OculusClient.exe;OVRServiceLauncher.exe;OVRServer_x64.exe;OculusVR.exe;OculusCompatCheck.exe;CompatToolCommandLine.exe;OculusLogGatherer.exe;OVRLibrarian.exe;oculus-driver.exe;OVRLibraryService.exe;oculus-overlays.exe;OVRRedistributableInstaller.exe;

Fill the above content into the Application text box and select the local proxy port for SS.

For other programs (like foreign games, Steam, etc.), if they need to be forced through the SS proxy, the same method applies.

VR Screen Blur Issue

Since the default Screen Percentage value in UE projects is quite low, the visuals may appear blurry. However, increasing it directly can lead to significant frame rate drops.
Typically, a setting under 200 will provide good visual quality, and below is a list of recommended values (ideal values) for UE. Art designers should optimize the scene based on these to ensure frames are close to maximum.

Before UE 4.19 (excluding 4.19), the ideal suggested values for VR HMD (r.ScreenPercentage) are:

DeviceType r.ScreenPercentage
Oculus Rift 133
HTC Vive 140
PSVR 140
GearVR 120
GoogleVR 120

After UE 4.19, vr.PixelDensity was added. When r.ScreenPercentage is set to 100, standardized values can be used across different platform devices:

DeviceType r.ScreenPercentage vr.PixelDensity
Oculus Rift 100 1
HTC Vive 100 1
PSVR 100 1
GearVR 100 1
GoogleVR 100 1

Lower values will perform faster but will be undersampled (more blurry), while values over 1 will perform slower and will supersample (extra sharp).

This way, you can control the display quality through the standardized value of vr.PixelDensity.
For further details, see the link: Significant Changes Coming To VR Resolution Settings in 4.19.

Solution for HTC VIVE Base Station Fault (03)

If the indicator on the base station flashes red and SteamVR displays Base Station Fault (03), try manually flashing the base station firmware. The steps are as follows:
The firmware path is under the Steam installation path: Steam\steamapps\common\SteamVR\tools\lighthouse\firmware\lighthouse_tx\archive\htc_2.0, find the following two files:

1
2
* lighthouse_tx_htc_2_0-calibration-rescue-244.bin
* lighthouse_tx_htc_2_0-244-2016-03-12.bin
  1. Ensure the base station is powered off, then connect it to the computer using a micro-B USB cable.
  2. Press and hold the mode button on the back of the base station and plug in the power cable.
  3. Release the mode button once the computer confirms a USB mass storage device.
  4. The connected base station’s storage device is named “CRP_DISABLED”; open it and delete the file “firmware.bin.”
  5. Copy the file “lighthouse_tx_htc_2_0-calibration-rescue-244.bin” to the storage space of the base station.
  6. After copying, unplug the power cable.
  7. Wait a few seconds, then plug the power back in. Do not press the mode button during this process. The base station should now rapidly blink green or red after a few seconds. A green light indicates successful recovery.
  8. If it blinks red, this indicates it cannot auto-repair; you need to send it for repair.
  9. Unplug the power again.
  10. Repeat steps 1 to 7, but in step 5, copy the file “lighthouse_tx_htc_2_0-244-2016-03-12.bin.”
  11. Once complete, the base station should return to normal operation. Set its channel to “A” and track it independently (keep the other base station powered off) to verify functionality. Once confirmed working, power on the other base station.

If the green light is blinking but cannot function properly, repeat steps 1 to 7, but in step 5 clear all files in “CRP_DISABLED” and only copy “lighthouse_tx_htc_2_0-244-2016-03-12.bin.”

  • If manually flashing the base station firmware and recalibration does not work, use a phone to take a picture to ensure the two laser points are functioning normally (do not use iPhone/iPad as they cannot capture the laser points); if either of these laser points does not display, it indicates laser damage and should be sent to customer service for inspection.


Reference link: Base Station Fault (03)

Engine Analysis

Starting a UE Game

The game’s initialization starts from UGameInstance::StartGameInstance. It is called by FEngineLoop::Init() through polymorphic calls to GEngine->Start(), which invokes UGameEngine::Start() that calls GameInstance->StartGameInstance();:

Actor Initialization in UE

AActor’s PreInitializeComponents/InitializeComponents/PostInitializeComponents and DispatchBeginPlay are all invoked in ULevel::RouteActorInitialize.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
void ULevel::RouteActorInitialize()
{
// Send PreInitializeComponents and collect volumes.
for( int32 Index = 0; Index < Actors.Num(); ++Index )
{
AActor* const Actor = Actors[Index];
if( Actor && !Actor->IsActorInitialized() )
{
Actor->PreInitializeComponents();
}
}

const bool bCallBeginPlay = OwningWorld->HasBegunPlay();
TArray<AActor *> ActorsToBeginPlay;

// Send InitializeComponents on components and PostInitializeComponents.
for( int32 Index = 0; Index < Actors.Num(); ++Index )
{
AActor* const Actor = Actors[Index];
if( Actor )
{
if( !Actor->IsActorInitialized() )
{
// Call Initialize on Components.
Actor->InitializeComponents();

Actor->PostInitializeComponents(); // should set Actor->bActorInitialized = true
if (!Actor->IsActorInitialized() && !Actor->IsPendingKill())
{
UE_LOG(LogActor, Fatal, TEXT("%s failed to route PostInitializeComponents. Please call Super::PostInitializeComponents() in your <className>::PostInitializeComponents() function. "), *Actor->GetFullName() );
}

if (bCallBeginPlay && !Actor->IsChildActor())
{
ActorsToBeginPlay.Add(Actor);
}
}

// Components are all set up, init touching state.
// Note: Not doing notifies here since loading or streaming in isn't actually conceptually beginning a touch.
// Rather, it was always touching and the mechanics of loading is just an implementation detail.
Actor->UpdateOverlaps(Actor->bGenerateOverlapEventsDuringLevelStreaming);
}
}

// Do this in a second pass to make sure they're all initialized before begin play starts
for (int32 ActorIndex = 0; ActorIndex < ActorsToBeginPlay.Num(); ActorIndex++)
{
AActor* Actor = ActorsToBeginPlay[ActorIndex];
SCOPE_CYCLE_COUNTER(STAT_ActorBeginPlay);
Actor->DispatchBeginPlay();
}
}

UE4: Engine Start Order

UE Engine Tick Call Stack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
FEngineLoop_Tick=>start: void FEngineLoop::Tick()
UGameEngine_Tick=>operation: void UGameEngine::Tick( float DeltaSeconds, bool bIdleMode )
UWorld_Tick=>operation: void UWorld::Tick( ELevelTick TickType, float DeltaSeconds)
UWorld_RunTickGroup=>operation: void UWorld::RunTickGroup(ETickingGroup Group, bool bBlockTillComplete = true)
FTickTaskManager_RunTickGroup=>operation: void FTickTaskManager::RunTickGroup(ETickingGroup Group, bool bBlockTillComplete )
FTickTaskSequencer_ReleaseTickGroup=>operation: void FTickTaskSequencer::ReleaseTickGroup(ETickingGroup WorldTickGroup, bool bBlockTillComplete)
FTaskGraphImplementation_WaitUntilTaskComplete=>operation: FTaskGraphImplementation::WaitUntilTaskComplete()
FNamedTaskThread_ProcessTaskUntilQuit=>operation: FNamedTaskThread::ProcessTaskUntilQuit()
FNameTaskThread_ProcessTaskNamedThread=>operation: FNameTaskThread::ProcessTaskNamedThread()
FTaskGraphInterfaces_ExecuteTask=>operation: FTaskGraphInterfaces::ExecuteTask()
FTickFunction_DoTask=>operation: FTickFunction::DoTask()
DynamicExecFTickFunctionExecuteTick=>condition: Polymorphism execute FTickFunction::ExecuteTask()
AActor_Tick=>subroutine: AActor
AActor_Tick_FActorTickFunction_ExecuteTick=>operation: FActorTickFunction::ExecuteTick()
AActor_Tick_AActor_TickActor=>operation: AActor::TickActor()
AActor_Tick_AActor_Tick=>operation: AActor::Tick()
AActor_Tick_AActor_ReceiveTick=>operation: AActor::ReceiveTick() // BpTick

ActorComponent_Tick=>subroutine: ActorComponent
ActorComponent_Tick_FActorComponentTickFunction_ExecuteTick=>operation: FActorComponentTickFunction::ExecuteTick()
ActorComponent_Tick_FActorComponentTickFunction_ExecuteTickHelper=>operation: FActorComponentTickFunction::ExecuteTickHelper()
ActorComponent_Tick_UActorComponent_TickComponent=>operation: UActorComponent::TickComponent()
ActorComponent_Tick_ReceiveTick=>operation: UActorComponent::ReceiveTick(DeltaTime) // BpTick
end=>end: End

FEngineLoop_Tick->UGameEngine_Tick
UGameEngine_Tick->UWorld_Tick
UWorld_Tick->UWorld_RunTickGroup
UWorld_RunTickGroup->FTickTaskManager_RunTickGroup
FTickTaskManager_RunTickGroup->FTickTaskSequencer_ReleaseTickGroup
FTickTaskSequencer_ReleaseTickGroup->FTaskGraphImplementation_WaitUntilTaskComplete
FTaskGraphImplementation_WaitUntilTaskComplete->FNamedTaskThread_ProcessTaskUntilQuit
FNamedTaskThread_ProcessTaskUntilQuit->FNameTaskThread_ProcessTaskNamedThread
FNameTaskThread_ProcessTaskNamedThread->FTaskGraphInterfaces_ExecuteTask
FTaskGraphInterfaces_ExecuteTask->FTickFunction_DoTask
FTickFunction_DoTask->DynamicExecFTickFunctionExecuteTick
DynamicExecFTickFunctionExecuteTick(yes,left)->AActor_Tick
AActor_Tick->AActor_Tick_FActorTickFunction_ExecuteTick
AActor_Tick_FActorTickFunction_ExecuteTick->AActor_Tick_AActor_TickActor
AActor_Tick_AActor_TickActor->AActor_Tick_AActor_Tick
AActor_Tick_AActor_Tick->AActor_Tick_AActor_ReceiveTick
AActor_Tick_AActor_ReceiveTick->end
DynamicExecFTickFunctionExecuteTick(no,right)->ActorComponent_Tick
ActorComponent_Tick->ActorComponent_Tick_FActorComponentTickFunction_ExecuteTick
ActorComponent_Tick_FActorComponentTickFunction_ExecuteTick->ActorComponent_Tick_FActorComponentTickFunction_ExecuteTickHelper
ActorComponent_Tick_FActorComponentTickFunction_ExecuteTickHelper->ActorComponent_Tick_UActorComponent_TickComponent
ActorComponent_Tick_UActorComponent_TickComponent->ActorComponent_Tick_ReceiveTick
ActorComponent_Tick_ReceiveTick->end

Game Flow on UE4

Game Flow Overview on UE4

Actor LifeCycle

Actor Lifecycle on Unreal Engine

UE Log Macro Defined

Some Log definitions in the UE source code:
The logs defined in EngineLogs.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ENGINE_API DECLARE_LOG_CATEGORY_EXTERN(LogPath, Warning, All);
ENGINE_API DECLARE_LOG_CATEGORY_EXTERN(LogPhysics, Warning, All);
ENGINE_API DECLARE_LOG_CATEGORY_EXTERN(LogBlueprint, Warning, All);
ENGINE_API DECLARE_LOG_CATEGORY_EXTERN(LogBlueprintUserMessages, Log, All);
ENGINE_API DECLARE_LOG_CATEGORY_EXTERN(LogAnimation, Warning, All);
ENGINE_API DECLARE_LOG_CATEGORY_EXTERN(LogRootMotion, Warning, All);
ENGINE_API DECLARE_LOG_CATEGORY_EXTERN(LogLevel, Log, All);
ENGINE_API DECLARE_LOG_CATEGORY_EXTERN(LogSkeletalMesh, Log, All);
ENGINE_API DECLARE_LOG_CATEGORY_EXTERN(LogStaticMesh, Log, All);
ENGINE_API DECLARE_LOG_CATEGORY_EXTERN(LogNet, Log, All);
ENGINE_API DECLARE_LOG_CATEGORY_EXTERN(LogRep, Log, All);
ENGINE_API DECLARE_LOG_CATEGORY_EXTERN(LogNetPlayerMovement, Warning, All);
ENGINE_API DECLARE_LOG_CATEGORY_EXTERN(LogNetTraffic, Warning, All);
ENGINE_API DECLARE_LOG_CATEGORY_EXTERN(LogRepTraffic, Warning, All);
ENGINE_API DECLARE_LOG_CATEGORY_EXTERN(LogNetFastTArray, Warning, All);
ENGINE_API DECLARE_LOG_CATEGORY_EXTERN(LogNetDormancy, Warning, All);
ENGINE_API DECLARE_LOG_CATEGORY_EXTERN(LogSkeletalControl, Warning, All);
ENGINE_API DECLARE_LOG_CATEGORY_EXTERN(LogSubtitle, Log, All);
ENGINE_API DECLARE_LOG_CATEGORY_EXTERN(LogTexture, Log, All);
ENGINE_API DECLARE_LOG_CATEGORY_EXTERN(LogPlayerManagement, Error, All);
ENGINE_API DECLARE_LOG_CATEGORY_EXTERN(LogSecurity, Warning, All);
ENGINE_API DECLARE_LOG_CATEGORY_EXTERN(LogEngineSessionManager, Log, All);

Defined in CoreGlobals.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogHAL, Log, All);
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogMac, Log, All);
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogLinux, Log, All);
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogIOS, Log, All);
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogAndroid, Log, All);
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogPS4, Log, All);
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogXboxOne, Log, All);
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogWindows, Log, All);
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogSwitch, Log, All);
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogQuail, Log, All);
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogSerialization, Log, All);
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogUnrealMath, Log, All);
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogUnrealMatrix, Log, All);
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogContentComparisonCommandlet, Log, All);
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogNetPackageMap, Warning, All);
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogNetSerialization, Warning, All);
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogMemory, Log, All);
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogProfilingDebugging, Log, All);
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogCore, Log, All);
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogOutputDevice, Log, All);

CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogSHA, Warning, All);
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogStats, Log, All);
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogStreaming, Display, All);
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogInit, Log, All);
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogExit, Log, All);
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogExec, Warning, All);
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogScript, Warning, All);
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogLocalization, Error, All);
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogLongPackageNames, Log, All);
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogProcess, Log, All);
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogLoad, Log, All);

// Temporary log category, generally you should not check things in that use this
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogTemp, Log, All);

Here are some recently used log definitions:

1
2
3
// Module: Json
// File: Runtime/Json/Public/JsonGlobals.h
JSON_API DECLARE_LOG_CATEGORY_EXTERN(LogJson, Log, All);

Method to Detect if it is an Installed Engine Version

The engine version installed via EpicGameLauncher cannot create a Target Program project using UnrealBuildTool.exe -ProjectFiles "ProgramProjectName", and will prompt the following error:

1
ERROR: UnrealBuildTool Exception: A game project path was not specified, which is required when generating project files using an installed build or passing -game on the command line

This exception in the related code in UBT is located in ProjectFileGenerator.cs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// ProjectFileGenerator.cs

protected virtual void ConfigureProjectFileGeneration( String[] Arguments, ref bool IncludeAllPlatforms )
{
// ...
else switch( CurArgument.ToUpperInvariant() )
{
// ...
case "-GAME":
// Generates project files for a single game
bGeneratingGameProjectFiles = true;
break;
// ...
}
// ...

if( bGeneratingGameProjectFiles || UnrealBuildTool.IsEngineInstalled() )
{
if (OnlyGameProject == null)
{
throw new BuildException("A game project path was not specified, which is required when generating project files using an installed build or passing -game on the command line");
}

GameProjectName = OnlyGameProject.GetFileNameWithoutExtension();
if (String.IsNullOrEmpty(GameProjectName))
{
throw new BuildException("A valid game project was not found in the specified location (" + OnlyGameProject.Directory.FullName + ")");
}

bool bInstalledEngineWithSource = UnrealBuildTool.IsEngineInstalled() && DirectoryReference.Exists(UnrealBuildTool.EngineSourceDirectory);

bIncludeEngineSource = bAlwaysIncludeEngineModules || bInstalledEngineWithSource;
bIncludeDocumentation = false;
bIncludeBuildSystemFiles = false;
bIncludeShaderSource = true;
bIncludeTemplateFiles = false;
bIncludeConfigFiles = true;
IncludeEnginePrograms = bAlwaysIncludeEngineModules;
}
}

As seen, if the -game parameter is passed to UBT and it is an installed engine version (EpicGameLauncher), it will check if the project parameter has been passed. If not, it will throw an exception.
A normal UBT call command:

1
UnrealBuildTool.exe -ProjectFiles -project="D:\UnrealProjects\UEProject.uproject" -game

This ordinary Game parameter won’t throw an exception in UBT. However, calling UBT with the command to generate the Program will throw an exception for the installed engine version:

1
UnrealBuildTool.exe -ProjectFiles ProgramName

According to the detection code in ProjectFileGenerator.cs above, what we need to do is make the result from UnrealBuildTool.IsEngineInstalled() return false.
Continuing with the code: UnrealBuildTool.IsEngineInstalled() gets a boolean variable bIsEngineInstalled in UnrealBuildTool:

1
2
3
4
5
6
7
8
9
// UnrealBuildTool.cs
static public bool IsEngineInstalled()
{
if (!bIsEngineInstalled.HasValue)
{
throw new BuildException("IsEngineInstalled() called before being initialized.");
}
return bIsEngineInstalled.Value;
}

Searching for references reveals that there is a place where bIsEngineInstalled is set in GuardedMain:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// UnrealBuildTool.cs
private static int GuardedMain(string[] Arguments)
{
// ...
try{
// ...
foreach (string Argument in Arguments)
{
string LowercaseArg = Argument.ToLowerInvariant();
if (LowercaseArg == "-installed" || LowercaseArg == "-installedengine")
{
bIsEngineInstalled = true;
}
else if (LowercaseArg == "-notinstalledengine")
{
bIsEngineInstalled = false;
}
}
if (!bIsEngineInstalled.HasValue)
{
bIsEngineInstalled = FileReference.Exists(FileReference.Combine(RootDirectory, "Engine", "Build", "InstalledBuild.txt"));
}
// ...
}
}

From the code, it’s clear that UBT detects whether it is an installed engine version using three methods:

  1. Check if the parameters include -installled and -installedengine
  2. Check if the parameters include -notinstallengine
  3. Check if the engine path Engine\Build has the InstalledBuild.txt file

(Technically, it’s two methods; even if -notinstallengine is specified, it still needs to check for the existence of Engine\Build\InstalledBuild.txt).
The detection order is as follows:

1
-installled > -installedengine > -notinstalledengine > Engine\Build\InstalledBuild.txt

Thus, when I want UBT to consider my engine version as not installed, there are two approaches:

  1. Call UBT with the -notinstalledengine parameter and delete the InstalledBuild.txt file in the Engine\Build directory.
  2. Call UBT without passing the -installled and -installedengine parameters and delete the InstalledBuild.txt file in the Engine\Build directory.

Note: UBT also has the following two detections:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// UnrealBuildTools.cs
static public bool IsEnterpriseInstalled()
{
if(!bIsEnterpriseInstalled.HasValue)
{
bIsEnterpriseInstalled = FileReference.Exists(FileReference.Combine(EnterpriseDirectory, "Build", "InstalledBuild.txt"));
}
return bIsEnterpriseInstalled.Value;
}
static public bool IsProjectInstalled()
{
if (!bIsProjectInstalled.HasValue)
{
bIsProjectInstalled = FileReference.Exists(FileReference.Combine(RootDirectory, "Engine", "Build", "InstalledProjectBuild.txt"));
}
return bIsProjectInstalled.Value;
}

UE: Get All Registered Engines

You can use IDesktopPlatform::EnumerateEngineInstallations to get all the engine versions installed via the launcher and those registered by the user using UnrealVersionSelector.

1
2
TMap<FString,FString> Installations;
FDesktopPlatformModule::Get()->EnumerateEngineInstallations(Installations)

UnrealVersionSelector Registered Engine Path

When registering the engine using UnrealVersionSelector, the registered engine path will be written to the registry:

1
HKEY_CURRENT_USER\SOFTWARE\Epic Games\Unreal Engine\Builds

The registry entry lpValueName will be a generated GUID:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// DesktopPlatformWindows.cpp
bool FDesktopPlatformWindows::RegisterEngineInstallation(const FString &RootDir, FString &OutIdentifier)
{
bool bRes = false;
if(IsValidRootDirectory(RootDir))
{
HKEY hRootKey;
if(RegCreateKeyEx(HKEY_CURRENT_USER, InstallationsSubKey, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, NULL, &hRootKey, NULL) == ERROR_SUCCESS)
{
FString NewIdentifier = FGuid::NewGuid().ToString(EGuidFormats::DigitsWithHyphensInBraces);
LRESULT SetResult = RegSetValueEx(hRootKey, *NewIdentifier, 0, REG_SZ, (const BYTE*)*RootDir, (RootDir.Len() + 1) * sizeof(TCHAR));
RegCloseKey(hRootKey);

if(SetResult == ERROR_SUCCESS)
{
OutIdentifier = NewIdentifier;
bRes = true;
}
}
}
return bRes;
}

UE does not directly provide a separate way in IDesktopPlatform to get the list of engines registered by users using UnrealVersionSelector.
However, it includes functionality in EnumerateEngineInstallations to fetch per-user installations engines from the registry:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
void FDesktopPlatformWindows::EnumerateEngineInstallations(TMap<FString, FString> &OutInstallations)
{
// Enumerate the binary installations
EnumerateLauncherEngineInstallations(OutInstallations);

// Enumerate the per-user installations
HKEY hKey;
if (RegOpenKeyEx(HKEY_CURRENT_USER, InstallationsSubKey, 0, KEY_ALL_ACCESS, &hKey) == ERROR_SUCCESS)
{
// Get a list of all the directories
TArray<FString> UniqueDirectories;
OutInstallations.GenerateValueArray(UniqueDirectories);

// Enumerate all the installations
TArray<FString> InvalidKeys;
for (::DWORD Index = 0;; Index++)
{
TCHAR ValueName[256];
TCHAR ValueData[MAX_PATH];
::DWORD ValueType = 0;
::DWORD ValueNameLength = sizeof(ValueName) / sizeof(ValueName[0]);
::DWORD ValueDataSize = sizeof(ValueData);

LRESULT Result = RegEnumValue(hKey, Index, ValueName, &ValueNameLength, NULL, &ValueType, (BYTE*)&ValueData[0], &ValueDataSize);
if(Result == ERROR_SUCCESS)
{
int32 ValueDataLength = ValueDataSize / sizeof(TCHAR);
if(ValueDataLength > 0 && ValueData[ValueDataLength - 1] == 0) ValueDataLength--;

FString NormalizedInstalledDirectory(ValueDataLength, ValueData);
FPaths::NormalizeDirectoryName(NormalizedInstalledDirectory);
FPaths::CollapseRelativeDirectories(NormalizedInstalledDirectory);

if(IsValidRootDirectory(NormalizedInstalledDirectory) && !UniqueDirectories.Contains(NormalizedInstalledDirectory))
{
OutInstallations.Add(ValueName, NormalizedInstalledDirectory);
UniqueDirectories.Add(NormalizedInstalledDirectory);
}
else
{
InvalidKeys.Add(ValueName);
}
}
else if(Result == ERROR_NO_MORE_ITEMS)
{
break;
}
}

// Remove all the keys which weren't valid
for(const FString InvalidKey: InvalidKeys)
{
RegDeleteValue(hKey, *InvalidKey);
}

RegCloseKey(hKey);
}
}

EpicGameLauncher Registered Engine Registry Path

As mentioned before, if it is a non-source version engine, when registering using UnrealVersionSelector, it will write to the registry:

1
HKEY_CURRENT_USER\SOFTWARE\Epic Games\Unreal Engine\Builds

Whereas from the version installed by EpicGameLauncher, it will write to another registry path:

1
HKEY_LOCAL_MACHINE\SOFTWARE\EpicGames\Unreal Engine

Its value is (from the exported registry):

1
2
3
4
Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\EpicGames\Unreal Engine\4.18]
"InstalledDirectory"="C:\\Program Files\\Epic Games\\UE_4.18"

UE provides a method to obtain engines installed from EpicGameLauncher (via the IDesktopPlatform interface):

1
2
TMap<FString,FString> Installations;
FDesktopPlatformModule::Get()->EnumerateLauncherEngineInstallations(OutInstallations);

UnrealVersionSelector Registry File Associations

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\.uproject]
@="Unreal.ProjectFile"

[HKEY_CLASSES_ROOT\Unreal.ProjectFile]
@="Unreal Engine Project File"

[HKEY_CLASSES_ROOT\Unreal.ProjectFile\DefaultIcon]
@="\"C:\\Program Files (x86)\\Epic Games\\Launcher\\Engine\\Binaries\\Win64\\UnrealVersionSelector.exe\""

[HKEY_CLASSES_ROOT\Unreal.ProjectFile\shell]

[HKEY_CLASSES_ROOT\Unreal.ProjectFile\shell\open]
@="Open"

[HKEY_CLASSES_ROOT\Unreal.ProjectFile\shell\open\command]
@="\"C:\\Program Files (x86)\\Epic Games\\Launcher\\Engine\\Binaries\\Win64\\UnrealVersionSelector.exe\" /editor \"%1\""

[HKEY_CLASSES_ROOT\Unreal.ProjectFile\shell\run]
@="Launch game"
"Icon"="\"C:\\Program Files (x86)\\Epic Games\\Launcher\\Engine\\Binaries\\Win64\\UnrealVersionSelector.exe\""

[HKEY_CLASSES_ROOT\Unreal.ProjectFile\shell\run\command]
@="\"C:\\Program Files (x86)\\Epic Games\\Launcher\\Engine\\Binaries\\Win64\\UnrealVersionSelector.exe\" /game \"%1\""

[HKEY_CLASSES_ROOT\Unreal.ProjectFile\shell\rungenproj]
@="Generate Visual Studio project files"
"Icon"="\"C:\\Program Files (x86)\\Epic Games\\Launcher\\Engine\\Binaries\\Win64\\UnrealVersionSelector.exe\""

[HKEY_CLASSES_ROOT\Unreal.ProjectFile\shell\rungenproj\command]
@="\"C:\\Program Files (x86)\\Epic Games\\Launcher\\Engine\\Binaries\\Win64\\UnrealVersionSelector.exe\" /projectfiles \"%1\""

[HKEY_CLASSES_ROOT\Unreal.ProjectFile\shell\switchversion]
@="Switch Unreal Engine version..."
"Icon"="\"C:\\Program Files (x86)\\Epic Games\\Launcher\\Engine\\Binaries\\Win64\\UnrealVersionSelector.exe\""

[HKEY_CLASSES_ROOT\Unreal.ProjectFile\shell\switchversion\command]
@="\"C:\\Program Files (x86)\\Epic Games\\Launcher\\Engine\\Binaries\\Win64\\UnrealVersionSelector.exe\" /switchversion \"%1\""

Note: If you want to define a startup method for your own files, you can use this method.

UE Registered Compilation Version Engine Installation Path

UE provides the UnrealVersionSelector tool to select engine versions/generate sln, and it can also be used to register local engines (non-Launcher installed engines such as self-compiled source engines) into the project’s right-click Select Unreal Engine Version... list.
After analyzing the UnrealVersionSelector code, its registration process is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
WinMain_Func=>start: WinMain
Main_Func=>operation: Main(Arguments)
RegisterCurrentEngineDirectoryWithPrompt_Func=>operation: RegisterCurrentEngineDirectoryWithPrompt()
RegisterCurrentEngineDirectory_Func=>operation: RegisterCurrentEngineDirectory(false)
GetEngineIdentifierFromRootDir_Func=>operation: FDesktopPlatformModule::Get()->GetEngineIdentifierFromRootDir(EngineRootDir, Identifier)
RegisterEngineInstallation_Func=>operation: RegisterEngineInstallation(RootDir, OutIdentifier)
end=>end: End

WinMain_Func->Main_Func
Main_Func->RegisterCurrentEngineDirectoryWithPrompt_Func
RegisterCurrentEngineDirectoryWithPrompt_Func->RegisterCurrentEngineDirectory_Func
RegisterCurrentEngineDirectory_Func->GetEngineIdentifierFromRootDir_Func
GetEngineIdentifierFromRootDir_Func->RegisterEngineInstallation_Func
RegisterEngineInstallation_Func->end

The operation to write to the registry is in the FDesktopPlatformWindows::RegisterEngineInstallation function.
The process involves writing the registered engine directory into a string entry in HKEY_CURRENT_USER\Software\Epic Games\Unreal Engine\Builds.
When right-clicking Switch Unreal Engine Version..., it will modify the EngineAssociation value in the .uproject file:

The call flow is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
WinMain_Func=>start: WinMain
Main_Func=>operation: Main(Arguments)
SwitchVersion_Func=>operation: SwitchVersion(const FString& ProjectFileName)
GetEngineIdentifierForProject_fUNC=>operation: FDesktopPlatformModule::Get()->GetEngineIdentifierForProject(ProjectFileName, Identifier) // Get the currently selected engine
SelectEngineInstallation_Func=>operation: FPlatformInstallation::SelectEngineInstallation(Identifier) // Pop up the engine selection list, current selection is default
SetEngineIdentifierForProject_func=>operation: FDesktopPlatformModule::Get()->SetEngineIdentifierForProject(ProjectFileName, Identifier) // Set the selected engine and write to the uproject file
ContentOnly=>condition: Is Content Only Project?
GenerateProjectFiles_func=>operation: GenerateProjectFiles(ProjectFileName)
end=>end: End


WinMain_Func->Main_Func
Main_Func->SwitchVersion_Func
SwitchVersion_Func->GetEngineIdentifierForProject_fUNC
GetEngineIdentifierForProject_fUNC->SelectEngineInstallation_Func
SelectEngineInstallation_Func->SetEngineIdentifierForProject_func
SetEngineIdentifierForProject_func->ContentOnly
ContentOnly(yes)->end
ContentOnly(no)->GenerateProjectFiles_func
GenerateProjectFiles_func->end

The operation for writing project files is in FDesktopPlatformBase::SetEngineIdentifierForProject:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
bool FDesktopPlatformBase::SetEngineIdentifierForProject(const FString &ProjectFileName, const FString &InIdentifier)
{
// Load the project file
TSharedPtr<FJsonObject> ProjectFile = LoadProjectFile(ProjectFileName);
if (!ProjectFile.IsValid())
{
return false;
}

// Check if the project is a non-foreign project of the given engine installation. If so, blank the identifier
// string to allow portability between source control databases. GetEngineIdentifierForProject will translate
// the association back into a local identifier on other machines or syncs.
FString Identifier = InIdentifier;
if(Identifier.Len() > 0)
{
FString RootDir;
if(GetEngineRootDirFromIdentifier(Identifier, RootDir))
{
const FUProjectDictionary &Dictionary = GetCachedProjectDictionary(RootDir);
if(!Dictionary.IsForeignProject(ProjectFileName))
{
Identifier.Empty();
}
}
}

// Set the association on the project and save it
ProjectFile->SetStringField(TEXT("EngineAssociation"), Identifier);
return SaveProjectFile(ProjectFileName, ProjectFile);
}

Thus, if you choose a project using a compiled version engine, you can open the .uproject file with a text editor and see that the value of the EngineAssociation field corresponds to the engine version value in the registry.

Note: Command-Line Parameters of UnrealVersionSelector

The command-line parameters supported by UnrealVersionSelector and their uses can be found in UnrealVersionSelector.cpp#L224:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
int Main(const TArray<FString>& Arguments)
{
bool bRes = false;
if (Arguments.Num() == 0)
{
// Add the current directory to the list of installations
bRes = RegisterCurrentEngineDirectoryWithPrompt();
}
else if (Arguments.Num() == 1 && Arguments[0] == TEXT("-register"))
{
// Add the current directory to the list of installations
bRes = RegisterCurrentEngineDirectory(true);
}
else if (Arguments.Num() == 1 && Arguments[0] == TEXT("-fileassociations"))
{
// Update all the settings.
bRes = UpdateFileAssociations();
}
else if (Arguments.Num() == 2 && Arguments[0] == TEXT("-switchversion"))
{
// Associate with an engine label
bRes = SwitchVersion(Arguments[1]);
}
else if (Arguments.Num() == 3 && Arguments[0] == TEXT("-switchversionsilent"))
{
// Associate with a specific engine label
bRes = SwitchVersionSilent(Arguments[1], Arguments[2]);
}
else if (Arguments.Num() == 2 && Arguments[0] == TEXT("-editor"))
{
// Open a project with the editor
bRes = LaunchEditor(Arguments[1], L"");
}
else if (Arguments.Num() == 2 && Arguments[0] == TEXT("-game"))
{
// Play a game using the editor executable
bRes = LaunchEditor(Arguments[1], L"-game");
}
else if (Arguments.Num() == 2 && Arguments[0] == TEXT("-projectfiles"))
{
// Generate Visual Studio project files
bRes = GenerateProjectFiles(Arguments[1]);
}
else
{
// Invalid command line
FPlatformMisc::MessageBoxExt(EAppMsgType::Ok, TEXT("Invalid command line"), NULL);
}
return bRes ? 0 : 1;
}

Resolving .uproject Right-Click Generation Failure

First, check the path of UnrealVersionSelector.exe used in the right-click menu by opening the registry:

1
HKEY_CLASSES_ROOT\Unreal.ProjectFile

Then find the shell\open\command entry to see where UnrealVersionSelector.exe is located (if you haven’t installed the source version of the engine, it defaults to the Binaries path of EpicGamesLauncher), for example:

1
"C:\Program Files (x86)\Epic Games\Launcher\Engine\Binaries\Win64\UnrealVersionSelector.exe" /editor "%1"

Next, open C:\Program Files (x86)\Epic Games\Launcher\Engine\Binaries\Win64 and set UnrealVersionSelector.exe to run with administrator privileges (right-click - properties - compatibility - run this program as an administrator).

GamePlay

Issue with UE Macro Library

When using a macro library in UE Blueprints, simply saving the macro library after editing is not sufficient; you must compile the blueprint using the macro library for changes to take effect. In essence, the compilation in the blueprint will directly expand the macros; otherwise, it will revert to the previously unmodified version. This issue exists in UE4.18.3, but I haven’t tested other versions.

Saved Directory of Project Packaged in Shipping Mode

The Saved directory for UE projects packaged under Development or DebugGame modes is present in either the project directory or the target packed directory and includes directories such as Autosaves, Backup, Config, Logs, Crashs, SaveGames, and so on.
However, for games packaged in Shipping mode, the Saved directory is not located in the target packed directory. Instead, it is found in C:/User/%username%/AppData/Local/ProjectName, where ProjectName is replaced by your project name.

For more information about UE packing, you can refer to UE’s documentation:

RVO Obstacle Avoidance in CharacterMovement

The CharacterMovementComponent has automatic obstacle avoidance functionality, and you just need to enable UseRVOAvoidance.
However, remember to disable RVO avoidance after the character’s death to avoid issues with monsters attempting to avoid obstacles in open spaces.
You can use SetAvoidanceEnable to disable it.

VR Bow Model Standard

The bow required for VR should be a SkeleMesh, with the root bone located at the grabbing position (ideally at the grip center of the bow). Additionally, for easier calculations, the model’s orientation is required to be as shown below:

This orientation is for simplifying the rotation calculations when a VR player grabs the bow:

When drawing the bow, the hand holding the bow provides the Roll rotation, while Pitch and Yaw are provided by the rotation required for the hand holding the bow to look at the hand pulling the bow.

All bows should adhere to the same model rotation standards, which is easily implemented in art, but can be cumbersome for different programs to adjust.

Furthermore, the model origin of the arrow should be at the tail of the fletching:

Creating a well-experienced bow is quite complex, with attention to detail being key.

An Implementation of Clothing Change in UE

In UE’s USkinnedMeshComponent, there exists a function:

1
2
3
4
5
6
7
8
9
10
11
class ENGINE_API USkinnedMeshComponent : public UMeshComponent
{
public:
/**
* Set MasterPoseComponent for this component
*
* @param NewMasterBoneComponent New MasterPoseComponent
*/
UFUNCTION(BlueprintCallable, Category="Components|SkinnedMesh")
void SetMasterPoseComponent(USkinnedMeshComponent* NewMasterBoneComponent);
};

The gist is to make the current SkinnedMeshComponent move with the pose of the NewMasterBoneComponent.
We can use a set of models that share the same skeleton to implement the clothing change functionality, such as head, chest, legs, feet, and hands, allowing a character to have these five separate USkinnedMeshComponent model components to represent different parts.
Have a body with an animated USkinnedMeshComponent as the torso, and set the MasterPose of the five USkinnedMeshComponent parts to it.
At this point, all parts will move with the body’s animations, and to change clothing, you can simply modify the models of each part.

Compiling Blueprints to C++ Code in UE

UE supports compiling from Blueprints to C++ code. In the Project Setting - Packaging - Blueprints, select either Exclusive (only selected Blueprint resources will be generated in C++) or Inclusive (all Blueprint resources will be generated in C++).
After making a selection, package the project; upon successful packaging, you can find the .cpp and .h files prefixed with the Blueprint name in the Intermediate\Plugins\NativizedAssets\Windows\Game\Source\NativizedAssets path under the public/private directories.

UE Blueprint Function Library Cannot Be Called in UObject Objects

Creating a Blueprint Function Library in the editor and creating a function will yield an error if it is called in a blueprint Object, stating:

1
Function Requires A World Context.

Writing a test Blueprint Function Library generates C++ code:

The generated C++ code is as follows (removing excess parts, all the code can be viewed here):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// TestFuncLib_pf1448227310.h
UFUNCTION(BlueprintCallable, BlueprintPure, meta=(WorldContext="__WorldContext", Category, OverrideNativeName="GetComponents"))
static TArray<UActorComponent*> bpf__GetComponents__pf(AActor* bpp__pActor__pf, UClass* bpp__pCompClass__pf, UObject* bpp____WorldContext__pf);

// TestFuncLib_pf1448227310.cpp
TArray<UActorComponent*> UTestFuncLib_C__pf1448227310::bpf__GetComponents__pf(AActor* bpp__pActor__pf, UClass* bpp__pCompClass__pf, UObject* bpp____WorldContext__pf)
{
TArray<UActorComponent*> bpp__ReturnValue__pf{};
TArray<UActorComponent*> bpfv__CallFunc_GetComponentsByClass_ReturnValue__pf{};
FString bpfv__CallFunc_GetDisplayName_ReturnValue__pf{};
bpfv__CallFunc_GetDisplayName_ReturnValue__pf = UKismetSystemLibrary::GetDisplayName(bpp____WorldContext__pf);
UKismetSystemLibrary::PrintString(bpp____WorldContext__pf, bpfv__CallFunc_GetDisplayName_ReturnValue__pf, true, true, FLinearColor(0.000000,0.660000,1.000000,1.000000), 2.000000);
if(IsValid(bpp__pActor__pf))
{
bpfv__CallFunc_GetComponentsByClass_ReturnValue__pf = bpp__pActor__pf->AActor::GetComponentsByClass(bpp__pCompClass__pf);
}
bpp__ReturnValue__pf = bpfv__CallFunc_GetComponentsByClass_ReturnValue__pf;
return bpp__ReturnValue__pf;
}

Directly copying the above code into a C++ blueprint function library allows it to be called in UObject objects… this is quite strange.

Placing Components into Bone Sockets in UE

In Blueprints, you can directly set the parent socket property in the Blueprint Editor to place a component into a bone socket. The same applies in C++, although it’s slightly more complicated. Let’s look at an example from ShooterGame:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// ShooterGame\Private\Weapons\ShooterWeapon.cpp
void AShooterWeapon::AttachMeshToPawn()
{
if (MyPawn)
{
// Remove and hide both first and third person meshes
DetachMeshFromPawn();

// For locally controlled players we attach both weapons and let the bOnlyOwnerSee, bOwnerNoSee flags deal with visibility.
FName AttachPoint = MyPawn->GetWeaponAttachPoint();
if( MyPawn->IsLocallyControlled() == true )
{
USkeletalMeshComponent* PawnMesh1p = MyPawn->GetSpecifcPawnMesh(true);
USkeletalMeshComponent* PawnMesh3p = MyPawn->GetSpecifcPawnMesh(false);
Mesh1P->SetHiddenInGame( false );
Mesh3P->SetHiddenInGame( false );
Mesh1P->AttachToComponent(PawnMesh1p, FAttachmentTransformRules::KeepRelativeTransform, AttachPoint);
Mesh3P->AttachToComponent(PawnMesh3p, FAttachmentTransformRules::KeepRelativeTransform, AttachPoint);
}
else
{
USkeletalMeshComponent* UseWeaponMesh = GetWeaponMesh();
USkeletalMeshComponent* UsePawnMesh = MyPawn->GetPawnMesh();
UseWeaponMesh->AttachToComponent(UsePawnMesh, FAttachmentTransformRules::KeepRelativeTransform, AttachPoint);
UseWeaponMesh->SetHiddenInGame( false );
}
}
}

The same AttachToComponent is used here, and a Blueprint node with the same name can also be used to attach to bone sockets.

Getting OpenLevel Options in Unreal

When loading a level in UE, you can pass a FString of options:

In the loaded level, you can retrieve it via GetGameMode:

The C++ code looks like this:

1
2
AGameModeBase* OurGameMode=UGameplayStatics::GetGameMode(this);
FString LevelOptionString=OurGameMode->OptionsString;

Note: After switching levels, objects created in the original level will be destroyed. To solve this, use the GameMode’s GetSeamlessTravelActorList to preserve specified Actors during level transitions.

Converting 3D Positions in Game Scene to Screen Positions

Setting Mouse Cursor’s Relative Position within Viewport

First, install the LowEntry-ExtendedStandardLibrary plugin. You can use the ULowEntryExtendedStandardLibrary::SetMousePosition function to set the mouse cursor’s relative position within the viewport. The viewport size can be obtained via UWidgetLayoutLibrary::GetViewportSize.

Mouse Focus is Not in the Window After Starting the Game in UE

You can use SetInputModeGameAndUI to set the mouse focus within the window, and set InMourseLockMode to LockAlways. Using this action when starting the game will ensure the mouse focus is within the game window from the start, avoiding the need to click once on the game window to interact with UI.

Determining if a Particle Effect is Looping

If an Emitter from a UParticleSystem has Emitter Loops set to 0, it indicates a looping effect:

UE provides a method to determine if a particle effect is looping:

1
bool UParticleSystem::IsLooping()const{ /*...*/ }

If there is at least one looping Emitter in the effect, then the effect is considered looping. However, this method is not exposed to Blueprints, so a function library can be written to encapsulate it for use in Blueprints.

Caution with Exported Symbols When Using Classes Across Modules

When inheriting/accessing classes within the UE engine, be cautious about whether the class has exported symbols. Classes without exported symbols cannot be linked to their definitions. For instance, the FParticleSpriteEmitterInstance class in the UE Engine module does not have exported symbols.

Creating Async Nodes in Blueprints

In projects with some asynchronous operation needs, such as downloading files, a download command waits for completion before callback execution, minimizing excessive event binding and unbinding operations. Some async operations like PlayMontage and DownloadImage have multiple output nodes:

Can we create our own such async operation node? Absolutely.
You can check the implementation of the DownloadImage node in Engine\Source\Runtime\UMG\Public\Blueprint\AsyncTaskDownloadImage.h (C++API).
Here’s how to write one:

  1. The key is to inherit UBlueprintAsyncActionBase, which is mandatory.
  2. Then write a static function that returns a pointer to the class, marked with UFUNCTION as BlueprintInternalUseOnly="true".
  3. Declare and define several dispatcher members, which are the async nodes (the exec nodes on the right side of the Blueprint node).

Let’s look at the declaration of DownloadImage:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class UTexture2DDynamic;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDownloadImageDelegate, UTexture2DDynamic*, Texture);

UCLASS()
class UMG_API UAsyncTaskDownloadImage : public UBlueprintAsyncActionBase
{
GENERATED_UCLASS_BODY()
public:
UFUNCTION(BlueprintCallable, meta=( BlueprintInternalUseOnly="true" ))
static UAsyncTaskDownloadImage* DownloadImage(FString URL);

public:
UPROPERTY(BlueprintAssignable)
FDownloadImageDelegate OnSuccess;
UPROPERTY(BlueprintAssignable)
FDownloadImageDelegate OnFail;

public:
void Start(FString URL);

private:
/** Handles image requests coming from the web */
void HandleImageRequest(FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded);
};

Comparing with the above DownloadImage node, we can see:
0. First, a dynamic multicast delegate FDownloadImageDelegate is declared with one parameter.

  1. The UAsyncTaskDownloadImage class declares two event dispatchers OnSuccess and OnFail, akin to the Blueprint node’s right-side exec and parameter Texture, fundamentally being dispatchers (dynamic multicast delegates).
  2. The static function DownloadImage receives a FString parameter and returns a UAsyncTaskDownloadImage*, which displays the dispatcher execution nodes in the Blueprint.

In essence, the declared dynamic multicast members and their parameters will show on the right side of the Blueprint node.

I implemented an async action operation: first, look at the Blueprint operation:

The operation involves creating an AsyncActionObject object to trigger the async operation and then executing CreateAsyncTask to bind events to the previously created AsyncActionObject.
You can then call operations like OnActionStart on the ActionObj to trigger the relevant nodes on the right side of CreateAsyncTask.
Note: I imposed a restriction that one object corresponds to one task. If the currently passed AsyncActionObject is already bound to another task, creating the task will fail.
Now the code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// AsyncTask.h
// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved.

#pragma once

#include "AsyncActionObject.h"

// unreal header
#include "Array.h"
#include "Kismet/KismetSystemLibrary.h"
#include "CoreMinimal.h"
#include "UObject/ObjectMacros.h"
#include "Kismet/BlueprintAsyncActionBase.h"
#include "AsyncTask.generated.h"

#ifndef PRINT_LOG
#define PRINT_LOG(lOG_TEXT) UKismetSystemLibrary::PrintString(this,lOG_TEXT,true,true);
#endif

#ifndef PRINT_ASYNC_ACTION_TOOLS_DEBUG_INFO
#define PRINT_ASYNC_ACTION_TOOLS_DEBUG_INFO 0
#endif

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAsyncTaskDelegate,UAsyncActionObject*,ActionObj);

UCLASS(BlueprintType)
class UAsyncTask : public UBlueprintAsyncActionBase
{
GENERATED_UCLASS_BODY()

public:
UFUNCTION(BlueprintCallable, meta=( BlueprintInternalUseOnly="true" ))
static UAsyncTask* CreateAsyncTask(UAsyncActionObject* ActionObj);

public:

UPROPERTY(BlueprintAssignable)
FAsyncTaskDelegate OnStart;

UPROPERTY(BlueprintAssignable)
FAsyncTaskDelegate OnAbort;

UPROPERTY(BlueprintAssignable)
FAsyncTaskDelegate OnUpdate;

UPROPERTY(BlueprintAssignable)
FAsyncTaskDelegate OnFinishd;

protected:
void StartTask(UAsyncActionObject* ActionObj);
virtual void OnActionStart(UAsyncActionObject* ActionObj);
virtual void OnActionAbort(UAsyncActionObject* ActionObj);
virtual void OnActionUpdate(UAsyncActionObject* ActionObj);
virtual void OnActionFinishd(UAsyncActionObject* ActionObj);
};

Next is the implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// AsyncTask.cpp
// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved.

#include "AsyncTask.h"
#include "Modules/ModuleManager.h"

//----------------------------------------------------------------------//
// UAsyncTask
//----------------------------------------------------------------------//

UAsyncTask::UAsyncTask(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
if ( HasAnyFlags(RF_ClassDefaultObject) == false )
{
AddToRoot();
}
}

UAsyncTask* UAsyncTask::CreateAsyncTask(UAsyncActionObject* ActionObj)
{
UAsyncTask* AsyncActionTask = NewObject<UAsyncTask>();
AsyncActionTask->StartTask(ActionObj);
return AsyncActionTask;
}

void UAsyncTask::StartTask(UAsyncActionObject* ActionObj)
{
if(ActionObj && !ActionObj->Action_IsRunning())
{
#if PRINT_ASYNC_ACTION_TOOLS_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncTask::StartTask Bind Event"));
#endif
(ActionObj->OnStart).BindUObject(this,&UAsyncTask::OnActionStart);
(ActionObj->OnAbort).BindUObject(this,&UAsyncTask::OnActionAbort);
(ActionObj->OnUpdate).BindUObject(this,&UAsyncTask::OnActionUpdate);
(ActionObj->OnFinishd).BindUObject(this,&UAsyncTask::OnActionFinishd);
}
}

void UAsyncTask::OnActionStart(UAsyncActionObject* ActionObj)
{
#if PRINT_ASYNC_ACTION_TOOLS_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncTask::OnStart"));
#endif
OnStart.Broadcast(ActionObj);
}
void UAsyncTask::OnActionAbort(UAsyncActionObject* ActionObj)
{
#if PRINT_ASYNC_ACTION_TOOLS_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncTask::OnActionAbort"));
#endif
OnAbort.Broadcast(ActionObj);
}
void UAsyncTask::OnActionUpdate(UAsyncActionObject* ActionObj)
{
#if PRINT_ASYNC_ACTION_TOOLS_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncTask::OnActionUpdate"));
#endif
OnUpdate.Broadcast(ActionObj);
}
void UAsyncTask::OnActionFinishd(UAsyncActionObject* ActionObj)
{
#if PRINT_ASYNC_ACTION_TOOLS_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncTask::OnActionFinishd"));
#endif
OnFinishd.Broadcast(ActionObj);
}

Declaration for the AsyncActionObject class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// AsyncActionObject.h
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

// unreal header
#include "Array.h"
#include "Kismet/KismetSystemLibrary.h"
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "AsyncActionObject.generated.h"

#ifndef PRINT_LOG
#define PRINT_LOG(lOG_TEXT) UKismetSystemLibrary::PrintString(this,lOG_TEXT,true,true);
#endif

#ifndef PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
#define PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO 0
#endif

class UAsyncActionObject;

DECLARE_DELEGATE_OneParam(FAsyncActionDelegate,UAsyncActionObject*);

UCLASS(BlueprintType,Blueprintable)
class UAsyncActionObject : public UObject
{
GENERATED_BODY()
public:
UAsyncActionObject(const FObjectInitializer& objectInitializer);
FAsyncActionDelegate OnStart;
FAsyncActionDelegate OnAbort;
FAsyncActionDelegate OnUpdate;
FAsyncActionDelegate OnFinishd;

public:
UFUNCTION(BlueprintCallable)
virtual bool OnActionStart(FString& rReason);
UFUNCTION(BlueprintCallable)
virtual void OnActionAbort();
UFUNCTION(BlueprintCallable)
virtual void OnActionUpdate();
UFUNCTION(BlueprintCallable)
virtual void OnActionFinishd();

public:
// Action status (BP Executable function)
UFUNCTION(BlueprintCallable)
virtual bool Action_IsRunning()const;
UFUNCTION(BlueprintCallable)
virtual bool Action_ExecutableStart()const;

protected:
// End Action
virtual void EndAction();
virtual void UnBindAll();

virtual void InitDelegateList();
virtual bool Action_IsStarted()const;
virtual bool Action_EventIsBinded()const;

protected:
bool mActionStarted=false;
TArray<FAsyncActionDelegate*> DelegateList;
};

And its implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
// AsyncActionObject.cpp
// Fill out your copyright notice in the Description page of Project Settings.
// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved.

#include "AsyncActionObject.h"

UAsyncActionObject::UAsyncActionObject(const FObjectInitializer& objectInitializer)
{
#if PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncActionObject::UAsyncActionObject"));
#endif
InitDelegateList();
}

bool UAsyncActionObject::OnActionStart(FString& rReason)
{
rReason.Reset();
bool local_bResault=false;

if(Action_ExecutableStart())
{
#if PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncActionObject::OnActionStart"));
#endif
mActionStarted=true;
OnStart.ExecuteIfBound(this);

local_bResault=true;
}else{

#if PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
PRINT_LOG(TEXT("call UAsyncActionObject::OnActionStart Faild."));
#endif

local_bResault=false;
if(Action_IsStarted())
{

#if PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
PRINT_LOG(TEXT("StartFaild: Action is Started."));
#endif

rReason.Append(FString(TEXT("Action is Started.\n")));
}
if(!Action_EventIsBinded())
{
#if PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
PRINT_LOG(TEXT("StartFaild: Action is not bind anything event."));
#endif
rReason.Append(FString(TEXT("Action is not bind to anything Task")));
}
}
return local_bResault;
}

void UAsyncActionObject::OnActionAbort()
{
if(Action_IsRunning())
{
#if PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncActionObject::OnActionAbort"));
#endif
OnAbort.ExecuteIfBound(this);
EndAction();
}
}

void UAsyncActionObject::OnActionUpdate()
{
if(Action_IsRunning())
{
#if PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncActionObject::OnActionUpdate"));
#endif
OnUpdate.ExecuteIfBound(this);
}
}
void UAsyncActionObject::OnActionFinishd()
{
if(Action_IsRunning())
{
#if PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncActionObject::OnActionFinishd"));
#endif
OnFinishd.ExecuteIfBound(this);
EndAction();
}
}

void UAsyncActionObject::EndAction()
{
if(Action_IsRunning())
{
#if PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncActionObject::EndAction"));
#endif
UnBindAll();
mActionStarted=false;
}
}

void UAsyncActionObject::UnBindAll()
{
#if PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncActionObject::UnBindAll"));
#endif
for(auto& DeleIndex:DelegateList)
{
if(DeleIndex->IsBound())
{
DeleIndex->Unbind();
}
}
}

void UAsyncActionObject::InitDelegateList()
{
if(!Action_IsStarted())
{
DelegateList.AddUnique(&OnStart);
DelegateList.AddUnique(&OnAbort);
DelegateList.AddUnique(&OnUpdate);
DelegateList.AddUnique(&OnFinishd);
}
}

bool UAsyncActionObject::Action_EventIsBinded()const
{
bool EventIsBinded=true;
for(auto& DeleIndex:DelegateList)
{
if(!DeleIndex->IsBound())
EventIsBinded=false;
}
return EventIsBinded;
}

bool UAsyncActionObject::Action_IsStarted()const
{
return mActionStarted;
}

bool UAsyncActionObject::Action_IsRunning()const
{
return Action_IsStarted() && Action_EventIsBinded();
}

bool UAsyncActionObject::Action_ExecutableStart()const
{
return !Action_IsStarted() && Action_EventIsBinded();
}

Essentially, it wraps two layers of dispatchers…
For example, it allows you to listen in a behavior tree for an animation being terminated or completed before executing other actions, thereby solving coupling between different modules.

Warnings/Errors When Referencing Windows Header Files in UE

If you’ve used windows.h in UE, you might encounter errors like 4668 in VS. Previously, this was only a warning, but in UE4.18 it became an error. UE has encapsulated the Windows API header files, allowing you to include the appropriate ones:

1
2
3
#include <windows.h>
// In UE
#include "Windows/MinWindows.h"

Alternatively, you could use:

1
#pragma warning(disable:4668)

Additionally, if you use Windows API functions such as windows.h, you may encounter errors like this:

1
2
3
4
1>C:\Program Files\Epic Games\UE_4.15\Engine\Source\Runtime\Core\Public\Async/TaskGraphInterfaces.h(892): error C2039: '__faststorefence': is not a member of 'FWindowsPlatformMisc'
1>c:\program files\epic games\ue_4.15\engine\source\runtime\core\public\Windows/WindowsPlatformMisc.h(33): note: see declaration of 'FWindowsPlatformMisc'
1>C:\Program Files\Epic Games\UE_4.15\Engine\Source\Runtime\Core\Public\Async/TaskGraphInterfaces.h(868): note: while compiling class template member function 'void TGraphTask<FFunctionGraphTask>::ExecuteTask(TArray<FBaseGraphTask *,FDefaultAllocator> &,ENamedThreads::Type)'
1>C:\Program Files\Epic Games\UE_4.15\Engine\Source\Runtime\Core\Public\Async/TaskGraphInterfaces.h(1379): note: see reference to class template instantiation 'TGraphTask<FFunctionGraphTask>' being compiled

To resolve this, avoid including these Windows API header files in UE header files; instead, move them to the corresponding .cpp files.

Getting the Cursor’s Relative Proportion in the Window in UE

First, obtain the current window size through UGameViewportClient to get the SWindow, which allows you to retrieve the window size with SWindow::GetSizeInScreen:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void GetWindowSize(bool& Success, int32& Width, int32& Height)
{
Success = false;
Width = 0;
Height = 0;

if(GEngine == nullptr)
{
return;
}

UGameViewportClient* ViewportClient = GEngine->GameViewport;
if(ViewportClient == nullptr)
{
return;
}

TSharedPtr<SWindow> Window = ViewportClient->GetWindow();
if(!Window.IsValid())
{
return;
}

FVector2D Size = Window->GetSizeInScreen();

Success = true;
Width = Size.X;
Height = Size.Y;
}

Next, to get the cursor’s position in the current window, you can use APlayerController::GetMousePosition (it also retrieves position via the viewport):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool APlayerController::GetMousePosition(float& LocationX, float& LocationY) const
{
bool bGotMousePosition = false;
ULocalPlayer* LocalPlayer = Cast<ULocalPlayer>(Player);

if (LocalPlayer && LocalPlayer->ViewportClient)
{
FVector2D MousePosition;

bGotMousePosition = LocalPlayer->ViewportClient->GetMousePosition(MousePosition);

if (bGotMousePosition)
{
LocationX = MousePosition.X;
LocationY = MousePosition.Y;
}
}

return bGotMousePosition;
}

Then dividing the two results yields the relative position of the cursor within the current window.

Gamma Value of SceneCapture2D in UE

When using USceneCapture2D to capture the scene to a RenderTarget for display on the screen via Spectator Screen, you may find the colors are incorrect:

To solve this:

  1. Set the SceneCapture2D’s CaptureSource to Final Color (LDR) in RGB
  2. Set the RenderTarget resource’s gamma value to 2.2 to match the original image closely.

A Trick with Event Parameters in Blueprints

In some plugins, you might see operations like this:

In reality, parameters of event nodes in Unreal Blueprints are class member variables (data members of the class), allowing their usage in other places directly.

Other similar issues include delay operations:

Collision Detection Issues with UProjectileMovementComponent in UE

When using the UProjectileMovementComponent for functionalities like arrows, collision might not trigger due to excessive speed. It’s noted as a point for further research.

Config and Editor

Changing the Esc Key Shortcut to Exit the Editor Mode in UE

Open Editor Preferences - Keyboard Shortcuts to modify the key for Play World(PIE/SIE).

Displaying Plugin Content in UE Editor

Modifying the Parallel Build Count in UE

Edit the following files:

1
2
3
Engine/Saved/UnrealBuildTool/BuildConfiguration.xml
User Folder/AppData/Roaming/Unreal Engine/UnrealBuildTool/BuildConfiguration.xml
My Documents/Unreal Engine/UnrealBuildTool/BuildConfiguration.xml

As shown:

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<Configuration xmlns="https://www.unrealengine.com/BuildConfiguration">
<BuildConfiguration>
<MaxProcessorCount>1</MaxProcessorCount>
</BuildConfiguration>
</Configuration>

MaxProcessorCount specifies the maximum number of processors for local execution. More configuration parameters can be found in the UBT configuration.

Packaging Android Data in OBB Files in UE

Stuttering when the Editor Window is Out of Focus in UE

When running UE on the Windows platform, if you play a game within the editor while the current focus is outside of the editor/VR app, CPU usage decreases, leading to stuttering (this issue doesn’t occur in packaged builds). This is due to UE performing checks on focus in the editor environment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// Runtime/ApplicationCore/Private/Windows/WindowsPlatformApplicationMisc.cpp
void FWindowsPlatformApplicationMisc::PumpMessages(bool bFromMainLoop)
{
if (!bFromMainLoop)
{
TGuardValue<bool> PumpMessageGuard( GPumpingMessagesOutsideOfMainLoop, true );
WinPumpSentMessages();
return;
}

GPumpingMessagesOutsideOfMainLoop = false;
WinPumpMessages();

// Determine if application has focus
bool HasFocus = FApp::UseVRFocus() ? FApp::HasVRFocus() : FWindowsPlatformApplicationMisc::IsThisApplicationForeground();

// If editor thread doesn't have the focus, don't suck up too much CPU time.
if( GIsEditor )
{
static bool HadFocus=1;
if( HadFocus && !HasFocus )
{
// Drop our priority to speed up whatever is in the foreground.
SetThreadPriority( GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL );
}
else if( HasFocus && !HadFocus )
{
// Boost our priority back to normal.
SetThreadPriority( GetCurrentThread(), THREAD_PRIORITY_NORMAL );
}
if( !HasFocus )
{
// Sleep for a bit to not eat up all CPU time.
FPlatformProcess::Sleep(0.005f);
}
HadFocus = HasFocus;
}
// ...
}

Simply comment out FPlatformProcess::Sleep(0.005f); to resolve the issue.

Fixing Chinese Character Encoding Issues in UE Editor’s Compile Logs on Win10

Note: Some software might have encoding problems (like GBK encoding showing garbled text).

Package

ObservedKeyNames.Num()>0

For detailed solutions, look at: UE Package Error: ObservedKeyNames.Num()>0.

UE Package Error: RenderDocPlugin

RenderDocPlugin: Error: unable to initialize the plugin because no RenderDoc library has been located.


The solution: Disable the RenderDocPlugin before packaging.

UE4 Packaging 32-bit Alignment Error

When selecting the 32-bit platform for packaging in the engine, the following error may occur:

1
error C2719: 'SpawnTransform': formal parameter with requested alignment of 16 won't be aligned

This is due to using pass-by-value to transfer FTransform in the code:

1
AProps* SpawnProps(FTransform SpawnTransform);

Because FTransform requires 16-byte alignment: Math/TransformVectorized.h#L36

1
2
3
4
MS_ALIGN(16) struct FTransform
{
//...
}

The MS_ALIGN macro is defined in Core/Public/Windows/WindowsPlatform.h#L131:

1
2
3
4
5
6
7
8
9
10
// Alignment.
#if defined(__clang__)
#define GCC_PACK(n) __attribute__((packed,aligned(n)))
#define GCC_ALIGN(n) __attribute__((aligned(n)))
#if defined(_MSC_VER)
#define MS_ALIGN(n) __declspec(align(n)) // With -fms-extensions, Clang will accept either alignment attribute
#endif
#else
#define MS_ALIGN(n) __declspec(align(n))
#endif

So FTransform requires 16-byte alignment.

FTransform is implemented using vector intrinsics, and instances of it need to have 16-byte alignment. I think the stack is guaranteed to be aligned to 16-bytes on Win64 so the compiler can pass it on the stack correctly, but there’s no such guarantee on Win32 (which is what we use for shipping builds).

Therefore, changing to pass a pointer or reference resolves the issue (both passing by reference and passing by pointer are sizeof(void*) in size):

1
AProps* SpawnProps(const FTransform& SpawnTransform);

Related issues:

UE Packaging Error Indicating File In Use

  • Engine Version: 4.18.3
  • Packaging Platform: Windows
  • Running Mode: VR
  • BuildConfiguration: Shipping

Error log during packaging:

1
UATHelper: Packaging (Windows (64-bit)): Program.Main: ERROR: AutomationTool terminated with exception: System.IO.IOException: The process cannot access the file** ‘C:\YourProjectPath\Saved\StagedBuilds\WindowsNoEditor\Engine\Extras\Redist\en-us\UE4PrereqSetup_x64.exe’ **because it is being used by another process**.

The UE packaging error message indicates is being used by another process because the EpicGameLauncher is also running in the process, so closing it before packaging resolves the issue.

Online


UE Actor Replication Config

Replication

OnlyRelevantToServer (default false)
If true, this actor is only relevant to its owner; if this flag is changed during play, all non-owner channels would need to be explicitly closed.

Always Relevant (default false)

Always relevant for the network (overrides bOnlyRelevantToOwner).

ReplicateMovement (default true)

If true, replicate movement/location related properties.
Actor must also be set to replicate.

  1. see SetReplicates()
  2. see https://doc.unrealengine.com/latest/INT/Gameplay/Networking/Replication/

NetLoadOnClient (default true)

This actor will be loaded on network clients during map load.

NetUseOwnerRelevancy (default false)

If actor has a valid Owner, call Owner’s IsNetRelevantFor and GetNetPriority.

Replicates (default true)

If true, this actor will replicate to remote machines.
see SetReplicates()

NetDormancy (default DORM_Awake)

Dormancy setting for actor to take itself off the replication list without being destroyed on clients.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// This actor can go dormant, but is not currently dormant. Game code will tell it when it goes dormant.
enum ENetDormancy
{
DORM_Never,
DORM_Awake,
DORM_DormantAll,
DORM_DormantPartial,
DORM_Initial,
DORM_MAX,
}
DORM_Never: This actor can never go network dormant.
DORM_Awake: This actor can go dormant, but is not currently dormant. Game code will tell it when it goes dormant.
DORM_DormantAll: This actor wants to go fully dormant for all connections.
DORM_DormantPartial: This actor may want to go dormant for some connections; GetNetDormancy() will be called to find out which.
DORM_Initial: This actor is initially dormant for all connections if it was placed in the map.
DORM_MAX

NeuCullDistanceSquared (default 225000000.0)

Square of the max distance from the client’s viewpoint that this actor is relevant and will be replicated.

NetUpdateFrequency (default 100.0)

How often (per second) this actor will be considered for replication, used to determine NetUpdateTime.

MinNetUpdateFrequency (default 2.0)

Used to determine what rate to throttle down to when replicated properties are changing infrequently.

NetPriority (default 3.0)

Priority for this actor when checking for replication in a low bandwidth or saturated situation; higher priority means it is more likely to replicate.

Replicated Movement

LocationQuantization Level: (default EVectorQuantization::RoundTwoDecimals)

1
2
3
4
5
6
7
8
9
enum EVectorQuantization
{
RoundWholeNumber,
RoundOneDecimal,
RoundTwoDecimals,
}
RoundWholeNumber: Each vector component will be rounded to the nearest whole number.
RoundOneDecimal: Each vector component will be rounded, preserving one decimal place.
RoundTwoDecimals: Each vector component will be rounded, preserving two decimal places.

VelocityQuantization Level: (default ERotatorQuantization::RoundWholeNumber)

1
2
3
4
5
6
7
enum ERotatorQuantization
{
ByteComponents,
ShortComponents,
}
ByteComponents: The rotator will be compressed to 8 bits per component.
ShortComponents: The rotator will be compressed to 16 bits per component.

RotationQuantization Level (default ByteComponents)

Allow tuning the compression level for replicated rotation. You should only need to change this from the default if you see visual artifacts.

1
2
3
4
5
6
7
enum ERotatorQuantization
{
ByteComponents,
ShortComponents,
}
ByteComponents: The rotator will be compressed to 8 bits per component.
ShortComponents: The rotator will be compressed to 16 bits per component.

Disabling Auto-Connect in UE Editor for Multiplayer Testing


Issues Using UE for Online Networking

UE provides AController::IsLocalControlled to determine whether the current controller is controlling a local entity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bool AController::IsLocalController() const
{
const ENetMode NetMode = GetNetMode();

if (NetMode == NM_Standalone)
{
// Not networked.
return true;
}

if (NetMode == NM_Client && Role == ROLE_AutonomousProxy)
{
// Networked client in control.
return true;
}

if (GetRemoteRole() != ROLE_AutonomousProxy && Role == ROLE_Authority)
{
// Local authority in control.
return true;
}

return false;
}

Note: Be very cautious not to use the AutoPossessPlayer option in Pawn. This will cause the player’s Controller joining in to automatically act as Player0, leading to bizarre bugs.

Also available are UKismetSystemLibrary::IsServer / UKismetSystemLibrary::IsDedicatedServer, which both get the current World‘s GetNetMode for judgment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bool UKismetSystemLibrary::IsServer(UObject* WorldContextObject)
{
UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull);
return World ? (World->GetNetMode() != NM_Client) : false;
}

bool UKismetSystemLibrary::IsDedicatedServer(UObject* WorldContextObject)
{
UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull);
if (World)
{
return (World->GetNetMode() == NM_DedicatedServer);
}
return IsRunningDedicatedServer();
}

bool UKismetSystemLibrary::IsStandalone(UObject* WorldContextObject)
{
UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull);
return World ? (World->GetNetMode() == NM_Standalone) : false;
}

The members of enum ENetMode and their meanings are as follows:

Name Description
NM_Standalone Standalone: a game without networking, with one or more local players.
NM_DedicatedServer Dedicated server: server with no local players.
NM_ListenServer Listen server: a server that also has a local player hosting the game, available to other players on the network.
NM_Client Network client: client connected to a remote server.
NM_MAX

Using UE OnlineSubsystem for Networking

UE’s networking can be implemented through CreateSession / FindSession / JoinSession, but requires some simple configuration first.
Edit Config\DefaultEngine.ini and add the following content:

1
2
3
4
+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="/Script/OnlineSubsystemSteam.SteamNetDriver",DriverClassNameFallback="/Script/OnlineSubsystemUtils.IpNetDriver")

[OnlineSubsystem]
DefaultPlatformService=Null

Note that unlike when interfacing with Steam, DefaultPlatformService should be set to Null; otherwise, you cannot join a Session.

UE Networking Client Not Navigating

In Project Settings - NavigationSystem, set Allow Client Side Navigation to true:

UE’s Session Not Discoverable in LAN

When creating a session with UE’s CreateSession and trying to use FindSession on another machine in the LAN, the session list cannot be found.
The cause of this problem is having VMs or VirtualBox installed on the computer; disabling the VMware Network Adapter or VirtualBox Network Adapter will resolve the issue.
The specific location in the Control Panel is Control Panel/ Network and Internet/ Network and Sharing Center/ Change Adapter Settings:

Other Script

UE Strange Compilation and Linking Errors

After modifying the code, it’s best to delete the following files:

1
2
3
4
5
Binaries
Debug
Intermediate
Saved (note that packaging cache can be left uncleaned)
*.VC.db

The bat script is as follows:

1
2
3
4
5
del *.VC.db
rd /s /q Binaries
rd /s /q Debug
rd /s /q Intermediate
rd /s /q Saved

Also search for Binaries and Intermediate in all plugin directories:

1
2
$ find . -name "Binaries"|xargs rm -rf
$ find . -name "Intermediate"|xargs rm -rf

Profiling Commands

Show FPS

1
stat FPS

Show Statistics Units (total time per frame, logic thread/render thread and GPU time consumption):

1
stat Unit

Show Various Parameter Values in Render Thread

1
Stat SceneRendering

Show Render Thread Command Consumption

1
stat RenderThreadCommands

Show Parameters on Game Thread (AI/Physics/Animation/Blueprint/Memory Allocation, etc.)

1
stat game

Show Time and Efficiency Data Required for Culling

1
Stat InitViews

Show Lighting and Shading Render Time

1
Stat LightRendering

Capture Current Frame GPU Information

1
ProfileGPU

This will popup the GPU Data Visualizer displaying the captured frame data.

Capture Profiling Information (CPU/GPU)

1
2
stat StartFile
stat StopFile

This will generate *.ue4stats files in the project’s Saved/Profiling/UnrealStats folder, which can be opened using UnrealFrontend.

UE Network Analysis

1
2
3
4
5
6
# Start and stop recording
netprofile
# If not recorded yet, begin recording
netprofile enable
# If currently recording, stop recording
netprofile disable

Captured data is saved under Saved/Profiling with the extension *.nprof.
Documentation for UE’s Network Profiler tool can be found at Network Profiler.

Links:

The article is finished. If you have any questions, please comment and communicate.

Scan the QR code on WeChat and follow me.

Title:UE和VR开发技术笔记
Author:LIPENGZHA
Publish Date:2018/06/06 08:16
Update Date:2019/01/17 14:00
World Count:27k Words
Link:https://en.imzlp.com/posts/3380/
License: CC BY-NC-SA 4.0
Reprinting of the full article is prohibited.
Your donation will encourage me to keep creating!