UE and VR Development Technical Notes

UE和VR开发技术笔记

Some technical notes written in the daily cursive about UE4 and VR development, as well as some related materials. Previously scattered in imzlp.com/notes, they have been organized today, and future notes will be placed in this article.

Engine Documents

UE4 Online Documents

UE GamePlay Framework Tutorial

VR Devices

Height Issue When Adapting Different VR Devices in UE

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

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

VR Headset Initialization When Starting the Game in UE

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

Reset VR Headset Rotation and Position

As the default orientation of the VR headset along the X-axis is related to the room’s setup, when we initially deviate from the orientation during positioning, entering the game will lead to a dislocation from what we set for the player to see, necessitating correction.
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 of 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 documentation page adapted for Oculus Rift in Unreal is: Developing for Oculus Rift.

The official key operation introduction provided by Oculus:

Additionally, key mapping for Oculus Rift in UE:

HTC Vive Key Mapping

In comparison to the above Oculus Rift, here is a key mapping for HTC Vive:

HTC VIVE Device Setup



SteamVR 2.0 Base Stations

HTC Vive Pro supports SteamVR 2.0 base stations, with a tracking range of 150° and 7 meters, and supports base station daisy chaining (up to 16) to achieve large space tracking solutions.

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

Using Proxifier to Route Oculus through SS Proxy

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

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

After a successful connection, you can add a proxy rule. The processes related to 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 in the above content in the Application text box, then select the local proxy port for SS routing.

For other programs (such as foreign-made games, Steam, etc.), if you need to force them through the SS proxy, the same method applies.

VR Blurriness Issue

Because the default Screen Percentage value in UE projects is very low, it can appear very blurry. However, increasing it directly can result in a severe frame rate drop.
Typically, setting it to under 200 provides good image quality. Below are the recommended values from UE (ideal values). The art team then needs to optimize the scene based on this to ensure a frame rate close to full.

For versions prior to 4.19 (excluding 4.19), the recommended ideal value for VR HMD (r.ScreenPercentage) is:

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

In versions after 4.19, UE introduced vr.PixelDensity; with r.ScreenPercentage 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 display quality by adjusting the standardized value of vr.PixelDensity.
For a detailed introduction, refer to the link: Significant Changes Coming To VR Resolution Settings in 4.19

Solution for HTC VIVE Tracker Fault (03)

If the tracker panel flashes red and SteamVR shows Tracker Fault (03), please try to manually flash the tracker (base station) firmware. The steps are as follows:
The firmware path is located under the Steam installation path: Steam\steamapps\common\SteamVR\tools\lighthouse\firmware\lighthouse_tx\archive\htc_2.0, locate 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. With the tracker (base station) powered off, connect it to the computer using a micro-B USB cable.
  2. Press and hold the mode button on the back of the tracker (base station) and insert the power cable.
  3. Once the computer confirms it as a USB mass storage device, you can release the mode button.
  4. The connected tracker (base station) storage device will be 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 base station’s storage.
  6. After copying, unplug the power cable.
  7. Wait a few seconds, and then plug the power back in. Do not press the mode button during this process. After a few seconds, the tracker (base station) should blink green or red quickly. A green light indicates successful recovery.
  8. If it flashes red, it indicates it cannot fix automatically, please send it for repairs.
  9. Unplug the power again.
  10. Repeat steps 1 to 7, but in step 5, change the copied file to “lighthouse_tx_htc_2_0-244-2016-03-12.bin”.
  11. Once complete, the tracker (base station) should return to normal; set its channel to “A” and track it individually (with the other base station powered off) to confirm functionality. Once confirmed to be working properly, turn on the other base station.

If there is a flashing green light and it cannot be used normally, repeat steps 1 to 7, but in the fifth step, clear all files in “CRP_DISABLED” and only copy “lighthouse_tx_htc_2_0-244-2016-03-12.bin”.

  • If manually flashing the tracker (base station) firmware and calibration still doesn’t work, please take a photo to ensure whether the two laser dots are functioning normally (an iPhone/iPad cannot capture laser dots). If either of these two laser dots does not show, it indicates laser damage, please send it for customer service inspection.


Reference link: Tracker Fault (03)

Engine Analysis

Starting UE Game

The game’s initialization starts from UGameInstance::StartGameInstance.
Called through FEngineLoop::Init(), it polymorphically calls GEngine->Start(), which then invokes UGameEngine::Start()’s GameInstance->StartGameInstance();:

Actor Initialization in UE

The PreInitializeComponents/InitializeComponents/PostInitializeComponents and DispatchBeginPlay of AActor 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 Macros Defined

Some log definitions in UE source code:
These logs are 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);

In CoreGlobals.h, the following logs are defined:

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 that use this
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogTemp, Log, All);

Some log definitions recently used are pasted here:

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

Method to Detect if Engine is Installed Version

The engine version installed via EpicGameLauncher cannot create a Target Program project using UnrealBuildTool.exe -ProjectFiles "ProgramProjectName", and it 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 UBT’s related code is in ProjectFileGenreator.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
// ProjectFileGenreator.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;
}
}

From this code, it can be seen that if the -game parameter is passed to UBT and the engine is an installed version (EpicGameLauncher), it will check if the project parameter is provided; if not, it will throw an exception.
The normal UBT call command is:

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

This ordinary Game parameter will not throw an exception in UBT. However, calling UBT with the command to generate Program will throw an exception:

1
UnrelBuildTool.exe -ProjectFiles ProgramName

According to the above ProjectFileGenreator.cs detection code, what we need to do is change the result returned by UnrealBuildTool.IsEngineInstalled() to false.
Continuing with the code, UnrealBuildTool.IsEngineInstalled() retrieves a bool 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;
}

A search for references reveals that the bIsEngineInstalled variable 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, we can see that UBT has three methods to determine whether the engine version is installed:

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

(So there are actually three methods but effectively two; even if -notinstallengine is specified, it still needs to check for the existence of the Engine\Build\InstalledBuild.txt file.)
The detection order is as follows:

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

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

  1. Pass the -notinstalledengine parameter to UBT and delete the InstalledBuild.txt file from the Engine\Build directory.
  2. Call UBT without passing -installled and -installedengine parameters, and delete the InstalledBuild.txt file from the Engine\Build directory.

Note: There are also the following two checks in UBT:

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 obtain all engine versions installed from the launcher, and those registered by the user using UnrealVersionSelector in the current system.

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

UnrealVersionSelector Registered Engine Path

When registering an engine using UnrealVersionSelector, it will write the registered engine path into the registry:

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

The registry entry’s 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
// DesktopPlatfromWindows.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 an API in IDesktopPlatfrom for obtaining the list of engines registered by users through the UnrealVersionSelector.
However, it writes the function to obtain per-user installations engines from the registry in EnumerateEngineInstallations:

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 Version Engine Registry Path

It was mentioned above that if the engine is a non-source version, the registered paths will be written into the registry when using UnrealVersionSelector:

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

The versions installed from EpicGameLauncher will be written to another registry path:

1
HKEY_LOCAL_MACHINE\SOFTWARE\EpicGames\Unreal Engine

Its value (exported from the registry) is:

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 IDesktopPlatfrom interface):

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

UnrealVersionSelector Registry File Association

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 add launch methods for your defined files, you can use this method.

UE Registered Compiled Engine Installation Path

UE provides the UnrealVersionSelector tool to select engine versions/generate sln, etc., which can also be used to register local engines (non-Launcher installed, such as self-compiled source engines) into the right-click Select Unreal Engine Version... list in the project file.
A simple analysis of the UnrealVersionSelector code shows its registration process 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 of writing to the registry is in the FDesktopPlatformWindows::RegisterEngineInstallation function.
The operation is to write the registered engine directory into a string item in HKEY_CURRENT_USER\Software\Epic Games\Unreal Engine\Builds.
When right-clicking Switch Unreal Engine Version..., it will modify the .uproject file’s EngineAssociation value:

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
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) // Popup select engine list box; current selection is default
SetEngineIdentifierForProject_func=>operation: FDesktopPlatformModule::Get()->SetEngineIdentifierForProject(ProjectFileName, Identifier) // Set the selected engine and write to 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 execution of writing the project file operation 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);
}

So if you choose a project that uses a compiled engine, you can open the project’s .uproject file with a text editor and see that the value of the EngineAssociation item is the version value of the engine from the registry.

Note: The command line arguments supported by UnrealVersionSelector and their purposes 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, you need to check the path of the 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 the location of UnrealVersionSelector.exe (if you haven’t installed the source version, it will default to the Binaries path of EpicGamesLauncher), for example:

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

Then go to 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

UE Macro Library Issues

When using macro libraries in UE blueprints, simply saving changes to the macros inside the macro library isn’t sufficient; you must compile the blueprint using the macro library to see changes take effect. Essentially, the compilation inside the blueprint is what directly expands the macro. Otherwise, it will still be the earlier unmodified version, which is very frustrating. This issue exists in UE4.18.3; other versions haven’t been tested.

UE Saved Directory When Packing in Shipping Mode

The UE project includes the Saved directory under the project directory or target directory packed for Development or DebugGame, which contains Autosaves/Backup/Config/Logs/Crashs/SaveGames, etc.
However, with Shipping packaging, the Saved directory will not be located in the target directory but instead in C:/User/%username%/AppData/Local/ProjectName, with ProjectName replaced by your project name.

For more about UE packaging, you can check the UE documentation:

CharacterMovement Enabling RVO Obstacle Avoidance

CharacterMovementComponent has an automatic avoidance function; you just need to enable UseRVOAvoidance.
However, remember to disable the RVO obstacles after the character dies; otherwise, it will lead to issues where monsters avoid obstacles on open ground.
You can use SetAvoidanceEnable to disable it.

VR Bow Model Standards

The bow needed for VR should be a SkeleMesh, with the root bone point located at the gripping position (preferably at the center of the bow), and for computation convenience, the model direction should follow the requirement below:

This is to facilitate the calculation of the rotation when the VR player grabs the bow:

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

All bows should follow the same model rotation standard, which is easy to implement visually, but adjusting them differently programmatically is cumbersome.

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

Creating a satisfying bow experience is quite complicated, and the key lies in detail optimization.

Implementation of Outfit Change in UE

In the UE USkinnedMeshComponent, there is a function like this:

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);
};

It essentially means that the current SkinnedMeshComponent will move according to the pose of NewMasterBoneComponent.
We can use a set of models using the same skeleton to implement the outfit change functionality, such as head, torso, legs, feet, and hands, by having a character possess five separate USkinnedMeshComponent model components to represent different parts.
The character can have an animated USkinnedMeshComponent as the body, and then set the MasterPose of the five upper USkinnedMeshComponent parts to it.
At this point, all parts will animate according to the body’s animation; achieving outfit changes can simply be done by modifying the models of the respective parts.

Compiling Blueprint to C++ Code in UE

UE supports compiling blueprints to C++ code. You need to select Exclusive (only selected blueprint assets will generate C++) or Inclusive (generates C++ for all blueprint assets) in Project Setting - Packaging - Blueprints.
After selection, pack the project. Once packed successfully, you can find the .cpp and .h files starting with the blueprint name in the public/private directories under the Intermediate\Plugins\NativizedAssets\Windows\Game\Source\NativizedAssets path.

UE Blueprint Function Library Cannot Be Called in UObject

When creating a Blueprint Function Library in the editor and creating a function, that function cannot be called in the blueprint’s Object, it will prompt:

1
Function Requires A World Context.

Writing a test blueprint function library generates C++ code:

The generated C++ code is as follows (removing redundant parts; full code can be found 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;
}

Simply copying the above code to a C++ blueprint function library can be callable in UObject… this is perplexing.

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. In C++, it’s similar but slightly more complex. Let’s look at a code 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 method is used, and in Blueprints, you can also use a node with the same name 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 using GetGameMode:

Using C++, the 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 retain specified Actors during level switch, use the GameMode’s GetSeamlessTravelActorList.

Converting 3D Positions in Game Scenes to Screen Positions

Setting the Mouse Cursor’s Relative Position in Viewport

First, install the LowEntry-ExtendedStandardLibrary plugin, which provides the ULowEntryExtendedStandardLibrary::SetMousePosition function to set the mouse cursor’s relative position in the viewport. The viewport size can be obtained using UWidgetLayoutLibrary::GetViewportSize.

Mouse Focus Not in Window After Starting Game in UE

You can use SetInputModeGameAndUI to set the mouse focus within the window, with InMourseLockMode set to LockAlways. By using this operation when starting the game, the mouse focus will be within the game window, thus avoiding the need to click the game window before clicking UI elements.

Determining if a Particle Effect is Looping

If an UParticleSystem‘s Emitter has its Required Emitter Loops set to 0, it is a looping effect:

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

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

As long as there is one looping Emitter in the effect, the effect is considered looping. However, this method is not exposed to Blueprints; you can write a function library to wrap it for Blueprints.

Cross-Module Class Symbol Export Considerations

When inheriting/accessing classes within the UE engine, always check if the class has export symbols. Classes without export symbols cannot be linked to definitions. For example, the FParticleSpriteEmitterInstance class under the UE Engine module does not have export symbols.

Creating Asynchronous Nodes in Blueprints

In a project, there might be needs for asynchronous operations, such as downloading files, where a download command waits for completion before callback, while minimizing excessive event bindings and unbindings. In Blueprint nodes, some asynchronous operations like PlayMontage/DownloadImage have multiple output nodes:

Can we write our asynchronous 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 create a similar node:

  1. The key is to inherit UBlueprintAsyncActionBase, which is necessary.
  2. Write a static function that returns a pointer to the class and mark it with UFUNCTION BlueprintInternalUseOnly="true".
  3. Declare and define several dispatcher members, which will be the asynchronous nodes—these are the exec nodes on the right side of Blueprint nodes.

Let’s first 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 it with the DownloadImage node above, we see:
0. First, a dynamic multicast delegate FDownloadImageDelegate is declared, requiring one parameter.

  1. The UAsyncTaskDownloadImage class declares two event dispatchers, OnSuccess and OnFail, which correspond to the exec nodes and parameter Texture in the Blueprint node.
  2. The static function DownloadImage receives a FString parameter and returns a UAsyncTaskDownloadImage*, making the dispatcher execution nodes visible in the Blueprint.

That is, the dynamically declared member and its parameters will appear on the right side of the Blueprint node.

I implemented an asynchronous operation behavior, and here’s how it looks in Blueprint:

The behavior involves first creating an AsyncActionObject to later trigger asynchronous operations, and then executing CreateAsyncTask, which binds events to the previously created AsyncActionObject.
You can then call operations like OnActionStart on that ActionObj to invoke the relevant nodes on the right of CreateAsyncTask.
Note: My implementation restricts that one object corresponds to one Task; if the current AsyncActionObject is already bound to another task, task creation will fail.
Here’s 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
56
// AsyncTask.h
// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved.
// 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);
};

Now for 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
69
// AsyncTask.cpp
// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved.
// 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
66
67
68
// AsyncActionObject.h
// Fill out your copyright notice in the Description page of Project Settings.
// 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
152
// AsyncActionObject.cpp
// Fill out your copyright notice in the Description page of Project Settings.
// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved.
// Fill out your copyright notice in the Description page of Project Settings.

#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 can be used to listen for an animation being terminated or completed in a behavior tree, then execute other behaviors, thus solving coupling between different modules.

UE Warning/Error for Including Windows Header Files

If you use windows.h in UE, you may encounter error 4668 in VS, which was previously a warning in earlier UE versions but became an error in UE4.18. UE itself encapsulates the header files of the Windows API, and you can include the relevant:

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

Alternatively, you can use:

1
#pragma warning(disable:4668)

to also resolve the issue.

Additionally, if you use Windows API in your code, like windows.h, you may encounter similar errors like:

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

The solution to this problem is to avoid including these Windows API header files in UE header files; move them to the corresponding .cpp files instead.

Getting the Relative Ratio of the Cursor in the Window in UE

First, obtain the current window size using UGameViewportClient to get the SWindow, and then you can use SWindow::GetSizeInScreen to obtain the size:

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;
}

Then to get the cursor’s position within the current window, you can use APlayerController::GetMousePosition (it is also obtained 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 will give you the relative position of the mouse in the current window.

Gamma Value of SceneCapture2D

When capturing images using USceneCapture2D to RenderTarget and displaying it on the Spectator Screen, the colors might appear incorrect:

The solution is to:

  1. Set the SceneCapture2D’s CaptureSouce to Final Color (LDR) in RGB.
  2. Set the RenderTarget resource’s gamma value to 2.2 to make it look close to the original image.

A Cool Trick with Event Parameters in Blueprints

In some plugins, similar operations can be seen:

In reality, the parameters of Unreal Blueprint event nodes are members of the class, allowing them to be accessed directly in other places.

Similar issues can be found with delay operations:

Collision Detection Issues with UProjectileMovementComponent in UE

When using the UProjectileMovementComponent for features like arrows, collisions may fail to trigger due to high speeds. This issue is recorded for future investigation.

Config and Editor

Modifying the Esc Key to Exit Editor Mode in UE

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

Displaying Plugin Content in Editor

Modifying Parallel Count During UE Compilation

On Windows, you can modify 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

On Mac and Linux, modify the following paths:

1
2
3
/Users/<USER>/.config//Unreal Engine/UnrealBuildTool/BuildConfiguration.xml
/Users/<USER>/Unreal Engine/UnrealBuildTool/BuildConfiguration.xml
<PROJECT_DIRECTORY>/Saved/UnrealBuildTool/BuildConfiguration.xml

The structure is as follows:

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 is the maximum number of processors for local execution.
For more configuration parameters, see: UBT configuration

Storing Data in OBB Files When Packaging for Android in UE

Lag Issues in Editor Mode When Focus is Not in Window in UE

When playing a game in the editor on Windows, if the focus is not in the editor/VR application, CPU utilization decreases, causing lag (this does not occur when packaged). This is because UE performs a focus-checking operation 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
40
41
// Runtime/ApplicationCore/Private/Windows/WindowsPlatformApplicationMisc.cpp
void FWindowsPlatformApplicationMisc::PumpMessages(bool bFromMainLoop)
{
if (!bFromMainLoop)
{
TGuardValue<bool> PumpMessageGuard( GPumpingMessagesOutsideOfMainLoop, true );
// Process pending windows messages, which is necessary to the rendering thread in some rare cases where D3D
// sends window messages (from IDXGISwapChain::Present) to the main thread owned viewport window.
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 commenting out the line FPlatformProcess::Sleep(0.005f); will resolve the issue.

Resolving Chinese Character Encoding Issues in UE Compiler Logs on Windows 10

Note: There may be encoding issues in certain software (for instance, GBK encoding might show garbled characters).

Package

ObservedKeyNames.Num()>0

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

UE Package Error:RenderDocPlugin

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


Solution: Disable the RenderDocPlugin before packaging.### UE4 Packaging 32bit Requires 16-byte Alignment Error
When selecting the 32bit platform to package in the engine, the following error occurs:

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

This is because FTransform is passed by value in the code:

1
AProps* SpawnProps(FTransform SpawnTransform);

Since 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).

Changing to pass by pointer or reference solves the issue (both passing by reference and pointer are sizeof(void*) in size):

1
AProps* SpawnProps(const FTransform& SpawnTransform);

Related issues:

UE Packaging Fails Due to File in Use

  • Engine version: 4.18.3
  • Packaging platform: Windows
  • Running mode: VR
  • Build Configuration: Shipping

Error message 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 error message during UE packaging indicates is being used by another process because the EpicGameLauncher is also running in the process; it can be closed before packaging.

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 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 the 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.

Disable Automatic Connection When Testing Multiplayer in UE Editor


Some Issues When Using UE Online

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 cautious not to use the AutoPossessPlayer option in Pawn. This will cause the joining player’s Controller to be automatically assigned as Player0, resulting in strange bugs.

There are also UKismetSystemLibrary::IsServer/UKismetSystemLibrary::IsDedicatedServer, which fetch 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;
}

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 who is hosting the game, available to other players on the network.
NM_Client Network client: client connected to a remote server.
NM_MAX

Using UE’s OnlineSubsystem for Networking

UE networking can be accomplished via CreateSession/FindSession/JoinSession, but some simple configurations need to be done 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 the difference in integration with Steam; you should set DefaultPlatformService to Null, otherwise you won’t be able to join the session.

UE Networking Client Cannot Pathfind

You need to set Allow Client Side Navigation to true in Project Settings - NavigationSystem:

UE Sessions Not Found in LAN

After creating a session using CreateSession, if you cannot find the session list using FindSession on another machine in LAN, the cause is that the computer has VM or VirtualBox installed. 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

Unexplained Compilation and Linking Errors in UE

It’s best to delete the following files after modifying the code:

1
2
3
4
5
Binaries
Debug
Intermediate
Saved (note that packaging cache can be left)
*.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 all plugin directories for Binaries and Intermediate:

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

Profiling Commands

Display FPS

1
stat FPS

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

1
stat Unit

Display Various Parameter Values in the Render Thread

1
Stat SceneRendering

Display Consumption of Render Thread Commands

1
stat RenderThreadCommands

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

1
stat game

Display Time and Efficiency Data Required for Culling

1
Stat InitViews

Display the Time Required for Lighting and Shading Rendering

1
Stat LightRendering

Capture GPU Information for the Current Frame

1
ProfileGPU

This will pop up the GPU Data Visualizer showing the data for the captured frame.

Capture Profiling Information (CPU/GPU)

1
2
stat StartFile
stat StopFile

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

UE Network Analysis

1
2
3
4
5
6
# Start and stop recording
netprofile
# If not already recording, start recording
netprofile enable
# If currently recording, stop recording
netprofile disable

Captured data will be saved in Saved/Profiling with the extension *.nprof.
Documentation for the UE 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 and VR Development Technical Notes
Author:LIPENGZHA
Publish Date:2018/06/06 08:16
Update Date:2019/01/17 14:00
Word 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!