Unreal Engine


GIST NOTES

注意:使用GIST管理的笔记,在国内网络可能会无法显示。


Standalone模式的ES3预览

在启动时加入参数-FeatureLevelES31,在启动时就会由PCD3D_SM5变为PCD3D_ES31了。

修改引擎的编译器参数

如果想要在编译引擎时自定义加一些编译器参数可以修改Programs\UnrealBuildTool\Platform下各个平台的*Toolchain.cs中的:

1
2
3
4
5
6
string GetCompileArguments_Global(CppCompileEnvironment CompileEnvironment)
{
// ++[RSTUDIO][lipengzha] support xcode12
Result += " -Wno-range-loop-analysis";
// --[RSTUDIO]
}

这是全局配置,如果想要给某个Target添加编译器参数可以在target.cs里通过AdditionalCompilerArguments设置:

1
AdditionalCompilerArguments = "-Wno-range-loop-analysis";

独立半透明

有些半透明效果不希望受到景深的影响,可以开启独立半透明。

1
2
[/Script/Engine.RendererSettings]
r.SeparateTranslucency=False

但是项目中遇到在IOS上开启时有些特效层级会显示在人物和场景前面。

Slate图片

在Slate中可以使用FSlateIcon同通过指定名字(ContentBrowser.AssetActions)来指定使用UE中的图片资源,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Asset Actions sub-menu
Section.AddSubMenu(
"CookActionsSubMenu",
LOCTEXT("CookActionsSubMenuLabel", "Cook Actions"),
LOCTEXT("CookActionsSubMenuToolTip", "Cook actions"),
FNewToolMenuDelegate::CreateRaw(this, &FHotPatcherEditorModule::MakeCookActionsSubMenu),
FUIAction(
FExecuteAction()
),
EUserInterfaceActionType::Button,
false,
FSlateIcon(FEditorStyle::GetStyleSetName(), "ContentBrowser.AssetActions")
);

那么名字(ContentBrowser.AssetActions)与真实的图片是如何对应起来的呢?

Editor\EditorStyle\Private\SlateEditorStyle.cpp文件中定义了这些名字与图片的映射:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Set( "ContentBrowser.AssetActions", new IMAGE_BRUSH( "Icons/icon_tab_Tools_16x", Icon16x16 ) );
Set( "ContentBrowser.AssetActions.Edit", new IMAGE_BRUSH( "Icons/Edit/icon_Edit_16x", Icon16x16 ) );
Set( "ContentBrowser.AssetActions.Delete", new IMAGE_BRUSH( "Icons/icon_delete_16px", Icon16x16, FLinearColor( 0.4f, 0.5f, 0.7f, 1.0f ) ) );
//Set( "ContentBrowser.AssetActions.Delete", new IMAGE_BRUSH( "Icons/Edit/icon_Edit_Delete_16x", Icon16x16) );
Set( "ContentBrowser.AssetActions.Rename", new IMAGE_BRUSH( "Icons/Icon_Asset_Rename_16x", Icon16x16) );
Set( "ContentBrowser.AssetActions.Duplicate", new IMAGE_BRUSH( "Icons/Edit/icon_Edit_Duplicate_16x", Icon16x16) );
Set( "ContentBrowser.AssetActions.OpenSourceLocation", new IMAGE_BRUSH( "Icons/icon_Asset_Open_Source_Location_16x", Icon16x16) );
Set( "ContentBrowser.AssetActions.OpenInExternalEditor", new IMAGE_BRUSH( "Icons/icon_Asset_Open_In_External_Editor_16x", Icon16x16) );
Set( "ContentBrowser.AssetActions.ReimportAsset", new IMAGE_BRUSH( "Icons/icon_TextureEd_Reimport_40x", Icon16x16 ) );
Set( "ContentBrowser.AssetActions.GoToCodeForAsset", new IMAGE_BRUSH( "GameProjectDialog/feature_code_32x", Icon16x16 ) );
Set( "ContentBrowser.AssetActions.FindAssetInWorld", new IMAGE_BRUSH( "/Icons/icon_Genericfinder_16x", Icon16x16 ) );
Set( "ContentBrowser.AssetActions.CreateThumbnail", new IMAGE_BRUSH( "Icons/icon_Asset_Create_Thumbnail_16x", Icon16x16) );
Set( "ContentBrowser.AssetActions.DeleteThumbnail", new IMAGE_BRUSH( "Icons/icon_Asset_Delete_Thumbnail_16x", Icon16x16) );
Set( "ContentBrowser.AssetActions.GenericFind", new IMAGE_BRUSH( "Icons/icon_Genericfinder_16x", Icon16x16) );

这样引擎就能通过一个名字找到对应的图片了,这些图片位于Engine\Content\Editor\Slate目录下。

创建项目设置中的选项

在Editor的模块中添加Settings的模块依赖,在模块启动时加入以下代码:

1
2
3
4
5
6
7
if (ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings"))
{
SettingsModule->RegisterSettings("Project", "Plugins", "Hot Patcher",
LOCTEXT("HotPatcherSettingsName", "Hot Patcher"),
LOCTEXT("HotPatcherSettingsDescroption", "Configure the HotPatcher plugin"),
GetMutableDefault<UHotPatcherSettings>());
}

UPL读取ini

如有以下ini配置:

1
2
3
[/Script/AndroidRuntimeSettings.AndroidRuntimeSettings]
+PackageForOculusMobile=Quest
bSupportQuestHandsTracking=True

可以使用以下方式在UPL中使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<init>
<setBoolFromPropertyContains result="bPackageForOculusQuest" ini="Engine" section="/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" property="PackageForOculusMobile" contains="Quest"/>
<setBoolFromProperty result="bSupportsHandsTracking" ini="Engine" section="/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" property="bSupportQuestHandsTracking" default="true"/>
</init>
<!-- optional updates applied to AndroidManifest.xml -->
<androidManifestUpdates>
<if condition="bPackageForOculusQuest">
<true>
<if condition="bSupportsHandsTracking">
<true>
<!-- Oculus Hands Support -->
<log text="Oculus Quest Hands Tracking Permissions Added!"/>
<addPermission android:name="oculus.permission.handtracking"/>
<addPermission android:name="oculus.permission.HAND_TRACKING"/>
<addFeature android:name="oculus.software.handtracking" android:required="false"/>
</true>
</if>
</true>
</if>
</androidManifestUpdates>

访问系统环境变量

获取环境变量:

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
#include <stdlib.h>
#include <string>
namespace
{
std::string StdString(FString UEString)
{
return std::string(TCHAR_TO_UTF8(*UEString));
}

FString FStringFromStd(std::string StdString)
{
return FString(UTF8_TO_TCHAR(StdString.c_str()));
}
}

FString GetEnvironmentVariable(const FString& Name)
{
FString Value = TEXT("");

#if PLATFORM_WINDOWS
char* Buffer;
size_t Size;
if (_dupenv_s(&Buffer, &Size, StdString(Name).c_str()) == 0 && Buffer != nullptr)
{
Value = Buffer;
free(Buffer);
}
#else
char* ValueChars = getenv(StdString(Name).c_str());

if (ValueChars != NULL)
{
Value = ValueChars;
}
#endif

return Value;
}

设置环境变量:

1
2
3
4
5
6
7
8
9
int32 SetEnvironmentVariable(const FString& Name, const FString& Value)
{
FString Combined = Name + TEXT("=") + Value;
#if PLATFORM_WINDOWS
return _putenv(StdString(Combined).c_str());
#else
return putenv(StdString(Combined).c_str());
#endif
}

Xcode stdc++错误

在Xcode10中移除了libstdc++的支持,如果在代码中使用:

1
2
3
PublicAdditionalLibraries.AddRange(new string[] {
"stdc++.6.0.9",
});

会有以下错误:

1
ld: library not found for -lstdc++.6.0.9

息屏控制

UE中封装了通用的接口来控制,可以使用以下函数:

1
UKismetSystemLibrary::ControlScreensaver(false);

它会调用到对应平台的F*ApplicationMisc中的同名函数:

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
// for Android
bool FAndroidApplicationMisc::ControlScreensaver(EScreenSaverAction Action)
{
#if USE_ANDROID_JNI
extern void AndroidThunkCpp_KeepScreenOn(bool Enable);
switch (Action)
{
case EScreenSaverAction::Disable:
// Prevent display sleep.
AndroidThunkCpp_KeepScreenOn(true);
break;

case EScreenSaverAction::Enable:
// Stop preventing display sleep now that we are done.
AndroidThunkCpp_KeepScreenOn(false);
break;
}
return true;
#else
return false;
#endif
}

// for IOS
bool FIOSPlatformApplicationMisc::ControlScreensaver(EScreenSaverAction Action)
{
IOSAppDelegate* AppDelegate = [IOSAppDelegate GetDelegate];
[AppDelegate EnableIdleTimer : (Action == FGenericPlatformApplicationMisc::Enable)];
return true;
}

Android

在UE的GameActivity.java.template文件中有以下函数:

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
public void AndroidThunkJava_KeepScreenOn(boolean Enable)
{
bKeepScreenOn = Enable;
if (Enable)
{
_activity.runOnUiThread(new Runnable()
{
@Override
public void run()
{
Log.debug("==============> [JAVA] AndroidThunkJava_KeepScreenOn(true) - Disabled screen saver");
_activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
});
}
else
{
_activity.runOnUiThread(new Runnable()
{
@Override
public void run()
{
Log.debug("==============> [JAVA] AndroidThunkJava_KeepScreenOn(false) - Enabled screen saver");
_activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
});
}
}

在GameActivity的OnResume函数中会调用,是否开启的值为bKeepScreenOn

可以通过UPL、或者运行时的JNI调用来控制Android是否息屏。

1
2
3
4
5
<gameActivityOnCreateFinalAdditions>
<insert>
AndroidThunkJava_KeepScreenOn(true);
</insert>
</gameActivityOnCreateFinalAdditions>

IOS

在引擎中有由下代码控制:

Runtime/ApplicationCore/Private/IOS/IOSAppDelegate.cpp
1
2
3
4
5
6
7
8
9
10
-(void)InitIdleTimerSettings
{
float TimerDuration = 0.0F;
GConfig->GetFloat(TEXT("/Script/IOSRuntimeSettings.IOSRuntimeSettings"), TEXT("IdleTimerEnablePeriod"), TimerDuration, GEngineIni);
IdleTimerEnablePeriod = TimerDuration;
self.IdleTimerEnableTimer = nil;
bool bEnableTimer = YES;
GConfig->GetBool(TEXT("/Script/IOSRuntimeSettings.IOSRuntimeSettings"), TEXT("bEnableIdleTimer"), bEnableTimer, GEngineIni);
[self EnableIdleTimer : bEnableTimer];
}

可以从GEngineIni中读取两个配置:

1
2
3
[/Script/IOSRuntimeSettings.IOSRuntimeSettings]
IdleTimerEnablePeriod=0.0
bEnableIdleTimer=true

bEnableIdleTimer=false,在游戏运行时就不会息屏。

运行时设置需要调用OC的代码:

Runtime\ApplicationCore\Private\IOS\IOSAppDelegate.cpp
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
-(void)EnableIdleTimer:(bool)bEnabled
{
dispatch_async(dispatch_get_main_queue(),^
{
if (bEnabled)
{
// Nothing needs to be done, if the enable timer is already running.
if (self.IdleTimerEnableTimer == nil)
{
self.IdleTimerEnableTimer = [NSTimer scheduledTimerWithTimeInterval:IdleTimerEnablePeriod target:self selector:@selector(DeferredEnableIdleTimer) userInfo:nil repeats:NO];
}
}
else
{
// Ensure pending attempts to enable the idle timer are cancelled.
if (self.IdleTimerEnableTimer != nil)
{
[self.IdleTimerEnableTimer invalidate];
self.IdleTimerEnableTimer = nil;
}

[UIApplication sharedApplication].idleTimerDisabled = NO;
[UIApplication sharedApplication].idleTimerDisabled = YES;
}
});
}

调用EnableIdleTimer传入false会关闭游戏运行时息屏。

Unreal Swarm

在UE编辑器中构建光照时,会拉起一个SwarmAgent进程在后台执行光照构建任务。
UE提供了多台机器联机构建光照的支持,通过配置SwarmCoordinatorSwarmAgent作为C/S的方式来下发构建任务,实现联机烘培的效果。官方文档:Unreal Swarm

要求:

  1. 在局域网内需要一台设备当作调度器(Coordinator),该机器上需要具有完整的引擎,并且该机器需要开放TCP的8008/8009端口的in/out,不然无法连接。
  2. 其余的机器上不要求安装引擎,但是需要运行SwarmAgent。

SwarmAgent可以从引擎的Engine\Binaries\DotNET目录下提取以下文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
C:\build_agent\workspace\SwarmAgent>tree /a /f
C:.
AgentInterface.dll
SwarmAgent.DeveloperOptions.xml
SwarmAgent.exe
SwarmAgent.exe.config
SwarmAgent.Options.xml
SwarmCommonUtils.dll
SwarmCoordinator.exe
SwarmCoordinator.exe.config
SwarmCoordinatorInterface.dll
SwarmInterface.dll
UnrealControls.dll

其中SwarmAgent.Options.xmlSwarmAgent.DeveloperOptions.xml是配置文件。

需要关注的参数:

  • CacheFolder:注意该目录需要有读写权限,因为SwarmAgent可以脱离引擎单独运行,默认的引擎目录不适合,建议设置为C:\tmp\SwarmCache
  • AllowedRemoteAgentNames:允许远程Agent分配任务到本地的名字,如果不做分组的情况,可以用通配符*允许所有的Agent
  • CoordinatorRemotingHost:局域网中运行SwarmCoordinator机器的IP地址(注意该机器需要开放TCP 8008/8009端口)
  • AvoidLocalExecution:只允许从本地分发任务,但本地不执行任务。
  • EnableStandaloneMode:SwarmAgent的独立模式,不允许传入任务,也不向其他机器分发任务。
  • ShowDeveloperMenu:显示DeveloperSettings菜单。

在DevelopSettings中可以控制超时时间、核心数等。
当配置完之后把SwarmAgent运行在其余设备上,在Coordinator的机器上就可以看到以下连接:

展示了连接的机器数,工作状态(如果SwarmCoordinator进程关闭,重启后会自动连接Agent)。
在任意启动了Agent的机器上可以开启UE启动光照构建任务(注意一定要先单独启动被配置完成的SwarmAgent,如果使用引擎自动拉起SwarmAgent则会使用一份额外的配置文件),就能在本地SwarmAgent进程中看到以下任务分配:

在SwarmCoordinator中可以看到任务分配情况:

注意:不一定所有的任务都会被分配至其他的Agent执行,取决于任务数量、Agent的负载情况等。

当Agent处于Busy状态时,Coordinator不会给其分配任务:


其他资料:

AsyncTask

有时候会开其他的线程执行任务,但是在任务中有些逻辑必须要在GameThread执行,这就需要用AsyncTask这种方式:

Runtime\Core\Private\Async\Async.cpp
1
2
3
4
5
#include "Async/Async.h"
void AsyncTask(ENamedThreads::Type Thread, TUniqueFunction<void()> Function)
{
TGraphTask<FAsyncGraphTask>::CreateTask().ConstructAndDispatchWhenReady(Thread, MoveTemp(Function));
}

通过AsyncTask函数可以指定在某个线程执行任务:

1
2
3
4
AsyncTask(ENamedThreads::GameThread, []()
{
// do something in GameThread
});

ENamedThread的枚举定义:

Runtime\Core\Public\Async\TaskGraphInterfaces.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
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
namespace ENamedThreads
{
enum Type : int32
{
UnusedAnchor = -1,
/** The always-present, named threads are listed next **/
#if STATS
StatsThread,
#endif
RHIThread,
AudioThread,
GameThread,
// The render thread is sometimes the game thread and is sometimes the actual rendering thread
ActualRenderingThread = GameThread + 1,
// CAUTION ThreadedRenderingThread must be the last named thread, insert new named threads before it

/** not actually a thread index. Means "Unknown Thread" or "Any Unnamed Thread" **/
AnyThread = 0xff,

/** High bits are used for a queue index and priority**/

MainQueue = 0x000,
LocalQueue = 0x100,

NumQueues = 2,
ThreadIndexMask = 0xff,
QueueIndexMask = 0x100,
QueueIndexShift = 8,

/** High bits are used for a queue index task priority and thread priority**/

NormalTaskPriority = 0x000,
HighTaskPriority = 0x200,

NumTaskPriorities = 2,
TaskPriorityMask = 0x200,
TaskPriorityShift = 9,

NormalThreadPriority = 0x000,
HighThreadPriority = 0x400,
BackgroundThreadPriority = 0x800,

NumThreadPriorities = 3,
ThreadPriorityMask = 0xC00,
ThreadPriorityShift = 10,

/** Combinations **/
#if STATS
StatsThread_Local = StatsThread | LocalQueue,
#endif
GameThread_Local = GameThread | LocalQueue,
ActualRenderingThread_Local = ActualRenderingThread | LocalQueue,

AnyHiPriThreadNormalTask = AnyThread | HighThreadPriority | NormalTaskPriority,
AnyHiPriThreadHiPriTask = AnyThread | HighThreadPriority | HighTaskPriority,

AnyNormalThreadNormalTask = AnyThread | NormalThreadPriority | NormalTaskPriority,
AnyNormalThreadHiPriTask = AnyThread | NormalThreadPriority | HighTaskPriority,

AnyBackgroundThreadNormalTask = AnyThread | BackgroundThreadPriority | NormalTaskPriority,
AnyBackgroundHiPriTask = AnyThread | BackgroundThreadPriority | HighTaskPriority,
};
// ...
}

MSVC的警告/错误编号

MSVC成员初始化顺序不一致警告

在编译MAC/IOS时开启了Wreorder,在以下情况中会产生错误:

1
2
3
4
5
6
struct A
{
A(int a) : y(a), x(y) {}
int x;
int y;
};

但是MSVC默认没有这个检测,会导致在MSVC编译的过,在Mac上编译不过的情况。

从VS2017开始,提供了一个编译器参数/w15038,可以用在Win上开启相似的警告。

在UE中使用AdditionalCompilerArguments添加即可(非Editor的TargetRules)。

1
StarterContent425.h(9): [C5038] data member 'A::y' will be initialized after data member 'A::x'

按照名字规则加载模块

如Shader编译、Texture的压缩等功能,支持很多的类型,UE里也分别实现了很多个模块,每个模块支持某种规则,这也是UE功能组织的一种方法,类似的方法UE中还有Modular Feature:ModularFeature:为UE4集成ZSTD压缩算法

Source/Developer/TargetPlatform/Private/TargetPlatformManagerModule.cpp
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
virtual const TArray<const IShaderFormat*>& GetShaderFormats() override
{
static bool bInitialized = false;
static TArray<const IShaderFormat*> Results;

if (!bInitialized || bForceCacheUpdate)
{
bInitialized = true;
Results.Empty(Results.Num());

TArray<FName> Modules;

FModuleManager::Get().FindModules(SHADERFORMAT_MODULE_WILDCARD, Modules);

if (!Modules.Num())
{
UE_LOG(LogTargetPlatformManager, Error, TEXT("No target shader formats found!"));
}

for (int32 Index = 0; Index < Modules.Num(); Index++)
{
IShaderFormatModule* Module = FModuleManager::LoadModulePtr<IShaderFormatModule>(Modules[Index]);
if (Module)
{
IShaderFormat* Format = Module->GetShaderFormat();
if (Format != nullptr)
{
Results.Add(Format);
}
}
}
}
return Results;
}

需要注意的是FMouduleManager::Get().FindModules这个,第一个参数支持规则:

1
2
#define SHADERFORMAT_MODULE_WILDCARD TEXT("*ShaderFormat*")
FModuleManager::Get().FindModules(SHADERFORMAT_MODULE_WILDCARD, Modules);

这样就会把所有匹配这个名字规则的Module得到,然后再按照统一的接口来维护

性能优化:设备分级

为高性能和较低性能的设备进行差异化的配置策略。

打印调用栈

使用FDebug中的函数:

1
FDebug::DumpStackTraceToLog();

会有以下输出:

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
LogStats: FPlatformStackWalk::StackWalkAndDump -  0.012 s
LogOutputDevice: Error: begin: stack for UAT
LogOutputDevice: Error: === FDebug::DumpStackTrace(): ===
LogOutputDevice: Error:
LogOutputDevice: Error: [Callstack] 0x00007ffce24a94f6 UE4Editor-DerivedDataCache.dll!FDerivedDataCache::GetSynchronous() [C:\build_agent\workspace\FGameEngine\Engine\Engine\Source\Developer\DerivedDataCache\Private\DerivedDataCache.cpp:328]
LogOutputDevice: Error: [Callstack] 0x00007ffcdec9218b UE4Editor-NavigationSystem.dll!UNavCollision::GetCookedData() [C:\build_agent\workspace\FGameEngine\Engine\Engine\Source\Runtime\NavigationSystem\Private\NavCollision.cpp:553]
LogOutputDevice: Error: [Callstack] 0x00007ffcdecc5662 UE4Editor-NavigationSystem.dll!UNavCollision::Setup() [C:\build_agent\workspace\FGameEngine\Engine\Engine\Source\Runtime\NavigationSystem\Private\NavCollision.cpp:219]
LogOutputDevice: Error: [Callstack] 0x00007ffce9e21b56 UE4Editor-Engine.dll!UStaticMesh::PostLoad() [C:\build_agent\workspace\FGameEngine\Engine\Engine\Source\Runtime\Engine\Private\StaticMesh.cpp:5100]
LogOutputDevice: Error: [Callstack] 0x00007ffcef299ae2 UE4Editor-CoreUObject.dll!UObject::ConditionalPostLoad() [C:\build_agent\workspace\FGameEngine\Engine\Engine\Source\Runtime\CoreUObject\Private\UObject\Obj.cpp:1067]
LogOutputDevice: Error: [Callstack] 0x00007ffce8de7432 UE4Editor-Engine.dll!UBodySetup::PostLoad() [C:\build_agent\workspace\FGameEngine\Engine\Engine\Source\Runtime\Engine\Private\PhysicsEngine\BodySetup.cpp:1079]
LogOutputDevice: Error: [Callstack] 0x00007ffcef299ae2 UE4Editor-CoreUObject.dll!UObject::ConditionalPostLoad() [C:\build_agent\workspace\FGameEngine\Engine\Engine\Source\Runtime\CoreUObject\Private\UObject\Obj.cpp:1067]
LogOutputDevice: Error: [Callstack] 0x00007ffcef36147c UE4Editor-CoreUObject.dll!EndLoad() [C:\build_agent\workspace\FGameEngine\Engine\Engine\Source\Runtime\CoreUObject\Private\UObject\UObjectGlobals.cpp:1592]
LogOutputDevice: Error: [Callstack] 0x00007ffcef34b8b7 UE4Editor-CoreUObject.dll!<lambda_84bbae4d81099727504625d58dce15aa>::operator()() [C:\build_agent\workspace\FGameEngine\Engine\Engine\Source\Runtime\CoreUObject\Private\UObject\UObjectGlobals.cpp:1231]
LogOutputDevice: Error: [Callstack] 0x00007ffcef370b4b UE4Editor-CoreUObject.dll!LoadPackageInternal() [C:\build_agent\workspace\FGameEngine\Engine\Engine\Source\Runtime\CoreUObject\Private\UObject\UObjectGlobals.cpp:1332]
LogOutputDevice: Error: [Callstack] 0x00007ffcef36fad0 UE4Editor-CoreUObject.dll!LoadPackage() [C:\build_agent\workspace\FGameEngine\Engine\Engine\Source\Runtime\CoreUObject\Private\UObject\UObjectGlobals.cpp:1427]
LogOutputDevice: Error: [Callstack] 0x00007ffcef384aff UE4Editor-CoreUObject.dll!ResolveName() [C:\build_agent\workspace\FGameEngine\Engine\Engine\Source\Runtime\CoreUObject\Private\UObject\UObjectGlobals.cpp:767]
LogOutputDevice: Error: [Callstack] 0x00007ffcef3975d5 UE4Editor-CoreUObject.dll!StaticLoadObjectInternal() [C:\build_agent\workspace\FGameEngine\Engine\Engine\Source\Runtime\CoreUObject\Private\UObject\UObjectGlobals.cpp:829]
LogOutputDevice: Error: [Callstack] 0x00007ffcef396ca3 UE4Editor-CoreUObject.dll!StaticLoadObject() [C:\build_agent\workspace\FGameEngine\Engine\Engine\Source\Runtime\CoreUObject\Private\UObject\UObjectGlobals.cpp:904]
LogOutputDevice: Error: [Callstack] 0x00007ffd02b83b11 UE4Editor-CinematicCamera.dll!ConstructorHelpersInternal::FindOrLoadObject<UStaticMesh>() [C:\build_agent\workspace\FGameEngine\Engine\Engine\Source\Runtime\CoreUObject\Public\UObject\ConstructorHelpers.h:36]
LogOutputDevice: Error: [Callstack] 0x00007ffd02b861ac UE4Editor-CinematicCamera.dll!ACameraRig_Crane::ACameraRig_Crane() [C:\build_agent\workspace\FGameEngine\Engine\Engine\Source\Runtime\CinematicCamera\Private\CameraRig_Crane.cpp:45]
LogOutputDevice: Error: [Callstack] 0x00007ffcef0f6cc3 UE4Editor-CoreUObject.dll!UClass::CreateDefaultObject() [C:\build_agent\workspace\FGameEngine\Engine\Engine\Source\Runtime\CoreUObject\Private\UObject\Class.cpp:3672]
LogOutputDevice: Error: [Callstack] 0x00007ffcef39baf1 UE4Editor-CoreUObject.dll!UObjectLoadAllCompiledInDefaultProperties() [C:\build_agent\workspace\FGameEngine\Engine\Engine\Source\Runtime\CoreUObject\Private\UObject\UObjectBase.cpp:861]
LogOutputDevice: Error: [Callstack] 0x00007ffcef37920f UE4Editor-CoreUObject.dll!ProcessNewlyLoadedUObjects() [C:\build_agent\workspace\FGameEngine\Engine\Engine\Source\Runtime\CoreUObject\Private\UObject\UObjectBase.cpp:950]
LogOutputDevice: Error: [Callstack] 0x00007ff6fc231ce1 UE4Editor-Cmd.exe!FEngineLoop::PreInitPostStartupScreen() [C:\build_agent\workspace\FGameEngine\Engine\Engine\Source\Runtime\Launch\Private\LaunchEngineLoop.cpp:3039]
LogOutputDevice: Error: [Callstack] 0x00007ff6fc22b7ad UE4Editor-Cmd.exe!GuardedMain() [C:\build_agent\workspace\FGameEngine\Engine\Engine\Source\Runtime\Launch\Private\Launch.cpp:127]
LogOutputDevice: Error: [Callstack] 0x00007ff6fc22bb0a UE4Editor-Cmd.exe!GuardedMainWrapper() [C:\build_agent\workspace\FGameEngine\Engine\Engine\Source\Runtime\Launch\Private\Windows\LaunchWindows.cpp:137]
LogOutputDevice: Error: [Callstack] 0x00007ff6fc23e2dd UE4Editor-Cmd.exe!WinMain() [C:\build_agent\workspace\FGameEngine\Engine\Engine\Source\Runtime\Launch\Private\Windows\LaunchWindows.cpp:268]
LogOutputDevice: Error: [Callstack] 0x00007ff6fc2403be UE4Editor-Cmd.exe!__scrt_common_main_seh() [d:\A01\_work\6\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288]
LogOutputDevice: Error: [Callstack] 0x00007ffd5c217c24 KERNEL32.DLL!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ffd5ce4d4d1 ntdll.dll!UnknownFunction []
LogOutputDevice: Error:
LogOutputDevice: Error: end: stack for UAT

Windows Metal Shader Compiler for IOS

在4.26及更高的引擎版本中,支持在Windows上直接安装Metal Sahder Compiler来支持在Windows上编译Metal的Shader,只需要在Apple开发者网站上安装Metal Developer Tools for Windows工具安装即可。OneDrive分流:Metal_Developer_Tools1.2Windows.exe

4.26引擎执行Cook时的Log,可以看到创建了Metallib:

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
Running: C:\Program Files\Epic Games\UE_4.26\Engine\Binaries\Win64\UE4Editor-Cmd.exe "C:\Users\lipengzha\Documents\Unreal Projects\StarterContent426\StarterContent426.uproject" -run=Cook  -TargetPlatform=IOS -fileopenlog -ddc=InstalledDerivedDataBackendGraph -unversioned -abslog="C:\Program Files\Epic Games\UE_4.26\Engine\Programs
\AutomationTool\Saved\Cook-2021.04.08-10.06.40.txt" -stdout -CrashForUAT -unattended -NoLogTimes -UTF8Output
LogInit: Display: Running engine for game: StarterContent426
LogHAL: Display: Platform has ~ 32 GB [34123063296 / 34359738368 / 32], which maps to Largest [LargestMinGB=32, LargerMinGB=12, DefaultMinGB=8, SmallerMinGB=6, SmallestMinGB=0)
LogTargetPlatformManager: Display: Loaded TargetPlatform 'AllDesktop'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'Android'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'Android_ASTC'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'Android_DXT'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'Android_ETC2'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'AndroidClient'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'Android_ASTCClient'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'Android_DXTClient'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'Android_ETC2Client'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'Android_Multi'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'Android_MultiClient'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'IOSClient'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'IOS'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'Linux'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'LinuxNoEditor'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'LinuxClient'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'LinuxServer'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'LinuxAArch64NoEditor'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'LinuxAArch64Client'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'LinuxAArch64Server'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'Lumin'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'LuminClient'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'MacNoEditor'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'Mac'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'MacClient'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'MacServer'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'TVOSClient'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'TVOS'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'WindowsNoEditor'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'Windows'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'WindowsClient'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'WindowsServer'
LogTargetPlatformManager: Display: Building Assets For IOS
LogAudioDebug: Display: Lib vorbis DLL was dynamically loaded.
LogShaderCompilers: Display: Using Local Shader Compiler.
LogDerivedDataCache: Display: Max Cache Size: 512 MB
LogDerivedDataCache: Display: Loaded Boot cache: C:/Users/lipengzha/AppData/Local/UnrealEngine/4.26/DerivedDataCache/Boot.ddc
LogDerivedDataCache: Display: Pak cache opened for reading ../../../Engine/DerivedDataCache/Compressed.ddp.
LogDerivedDataCache: Display: Performance to C:/Users/lipengzha/AppData/Local/UnrealEngine/Common/DerivedDataCache: Latency=0.06ms. RandomReadSpeed=291.68MBs, RandomWriteSpeed=137.99MBs. Assigned SpeedClass 'Local'
LogMaterial: Display: Missing cached shader map for material Widget3DPassThrough, compiling.
LogMaterial: Display: Missing cached shader map for material Widget3DPassThrough, compiling.
LogMaterial: Display: Missing cached shader map for material Widget3DPassThrough, compiling.
LogMaterial: Display: Missing cached shader map for material Widget3DPassThrough, compiling.
LogMaterial: Display: Missing cached shader map for material Widget3DPassThrough, compiling.
LogMaterial: Display: Missing cached shader map for material Widget3DPassThrough, compiling.
LogMaterial: Display: Missing cached shader map for material DefaultSpriteMaterial, compiling.
LogAudioCaptureCore: Display: No Audio Capture implementations found. Audio input will be silent.
LogAudioCaptureCore: Display: No Audio Capture implementations found. Audio input will be silent.
LogCook: Display: CookSettings for Memory: MemoryMaxUsedVirtual 0MiB, MemoryMaxUsedPhysical 16384MiB, MemoryMinFreeVirtual 0MiB, MemoryMinFreePhysical 1024MiB
LogCook: Display: Mobile HDR setting 1
LogCook: Display: Creating asset registry
LogCook: Display: Discovering localized assets for cultures: en
LogCook: Display: Unable to read previous cook inisettings for platform IOS invalidating cook
LogCook: Display: Clearing all cooked content for platform IOS
LogCook: Display: Sandbox cleanup took 0.025 seconds for platforms IOS
LogMetalShaderCompiler: Display: Creating Native Library C:/Users/lipengzha/Documents/Unreal Projects/StarterContent426/Saved/Cooked/IOS/StarterContent426/Content/Global_SF_METAL.0.metallib
LogMetalShaderCompiler: Display: Archiving 685 shaders for shader platform: SF_METAL
LogZipArchiveWriter: Display: Closing zip file with 0 entries.
LogMetalShaderCompiler: Display: Post-processing archive for shader platform: SF_METAL
LogCook: Display: Cooked packages 0 Packages Remain 314 Total 314

打包出来的Shadercode不是ushaderbytecode文件,而是和Mac上打包一致的metallib

Loaded a text shader (will be slower to load)

当使用远程构建的方式打包了IOS包,在运行时会有以下log:

1
LogMetal: Display: Loaded a text shader (will be slower to load)

该日志在以下代码中输出:

Source\Runtime\Apple\MetalRHI\Private\MetalShaders.cpp
1
2
3
4
5
6
/** Initialization constructor. */
template<typename BaseResourceType, int32 ShaderType>
void TMetalBaseShader<BaseResourceType, ShaderType>::Init(TArrayView<const uint8> InShaderCode, FMetalCodeHeader& Header, mtlpp::Library InLibrary)
{
// ...
}

这是因为加载的Shader需要实时编译,会比较慢,可以在项目设置中为IOS开启remote shader compile,在UE4.26之后,也可以通过本地安装metal的工具链在本地编译metal的shader。

rust in unreal

uhtmanifest

在以下路径里有个xxxx.uhtmanifest文件:

1
2
Intermediate/Build/PLATFORM/TARGETNAME/CONFIGURATION/*.uhtmanifest
Intermediate/Build/Win64/UE4Game/Development/UE4Game.uhtmanifest

里面记录了编译的Module和类型等信息:

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
{
"IsGameTarget":true,
"RootLocalPath":"C:\\build_agent\\workspace\\FGameEngine\\Engine",
"TargetName":"UE4Game",
"ExternalDependenciesFile":"C:\\build_agent\\workspace\\FGameEngine\\Engine\\Engine\\Intermediate\\Build\\Win64\\UE4Game\\Development\\UE4Game.deps",
"Modules":[
{
"Name":"CoreUObject",
"ModuleType":"EngineRuntime",
"BaseDirectory":"C:\\build_agent\\workspace\\FGameEngine\\Engine\\Engine\\Source\\Runtime\\CoreUObject",
"IncludeBase":"C:\\build_agent\\workspace\\FGameEngine\\Engine\\Engine\\Source\\Runtime",
"OutputDirectory":"C:\\build_agent\\workspace\\FGameEngine\\Engine\\Engine\\Intermediate\\Build\\Win64\\UE4\\Inc\\CoreUObject",
"ClassesHeaders":[

],
"PublicHeaders":[
"C:\\build_agent\\workspace\\FGameEngine\\Engine\\Engine\\Source\\Runtime\\CoreUObject\\Public\\UObject\\CoreNetTypes.h",
"C:\\build_agent\\workspace\\FGameEngine\\Engine\\Engine\\Source\\Runtime\\CoreUObject\\Public\\UObject\\CoreOnline.h",
"C:\\build_agent\\workspace\\FGameEngine\\Engine\\Engine\\Source\\Runtime\\CoreUObject\\Public\\UObject\\NoExportTypes.h"
],
"PrivateHeaders":[

],
"GeneratedCPPFilenameBase":"C:\\build_agent\\workspace\\FGameEngine\\Engine\\Engine\\Intermediate\\Build\\Win64\\UE4\\Inc\\CoreUObject\\CoreUObject.gen",
"SaveExportedHeaders":true,
"UHTGeneratedCodeVersion":"None"
}
]
}

附录一份引擎中编译UE4Game的uhtmanifest:UE4Game.uhtmanifest,可以对引擎中的Module进行分析,如果有些模块不需要,可以进行裁剪。

震动反馈

Incredibuild对FASTBuild的影响

在安装Visual Studio时勾选了安装Incredibuild,如果系统中同时安装了FASTBuild等工具(如NEXTBuild),在使用BuildGraph执行编译时会优先使用Incredibuild,起不到加速的效果。
解决办法就是在Visual Studio Installer中卸载掉Incredibuild。

交叉编译工具链

在编译Linux用的DS时需要用到交叉编译工具链:

安装之后把安装目录添加至LINUX_MULTIARCH_ROOT环境变量,可以通过以下命令检测是否安装成功:

1
%LINUX_MULTIARCH_ROOT%x86_64-unknown-linux-gnu\bin\clang++ -v 

安装工具链之后重新生成工程,可以通过RunUAT来进行交叉编译DS:

1
2
3
4
5
6
7
8
# REM 【ProjectName】, 【ProjectNameServer】需要修改为项目对应的名称,【ArchiveDirectory】修改为输出目录:
Engine\Build\BatchFiles\RunUAT.bat BuildCookRun -nocompileeditor -nop4
-project=【ProjectName】.uproject
-cook -stage -archive -archivedirectory=【ArchiveDirectory】
-package -ue4exe="Engine\Binaries\Win64\UE4Editor-Cmd.exe" -ddc=DerivedDataBackendGraph -pak -prereqs
-nodebuginfo -server -noclient -targetplatform=Linux -serverplatform=Linux -build -skipbuildclient
-target=【ProjectNameServer】
-clientconfig=Development -serverconfig=Development -utf8output -compile

IOS签名错误排查

No certificate for team xxxx matching

如果有以下错误提示:

1
Code Signing Error: No certificate for team '9TV4ZYSS4J' matching 'iPhone Developer: Created via API (JDPXHYVWYZ)' found: Select a different signing certificate for CODE_SIGN_IDENTITY, a team that matches your selected certificate, or switch to autom atic provisioning.

解决办法:

  1. 在Mac上的~/Library/MobileDevice/Provisioning\ Profiles清理掉多余的mobileprovision文件。
  2. 在Mac钥匙串中清理掉过期的开发者证书
  3. 重新导入mobileprovision与证书

注意:导入的mobileprovision的文件命名要与在BaseEngine.ini中指定的MobileProvision相同。

errSecInternalComponent错误

是因为通过ssh去调用/usr/bin/codesign访问钥匙串没有权限,可以使用以下命令在ssh中执行解锁:

1
security unlock-keychain -p password login.keychain

在UE远程构建时,可以先执行这条命令在当前的ssh环境下解锁keychain,使后面的签名可以正常执行。
修改UE中的Engine\Build\BatchFiles\Mac\Build.sh文件,在调用UBT编译之前,写入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/sh

cd "`dirname "$0"`/../../../.."

# Setup Mono
source Engine/Build/BatchFiles/Mac/SetupMono.sh Engine/Build/BatchFiles/Mac

if [ "$4" == "-buildscw" ] || [ "$5" == "-buildscw" ]; then
echo Building ShaderCompileWorker...
mono Engine/Binaries/DotNET/UnrealBuildTool.exe ShaderCompileWorker Mac Development
fi
echo unlock mac keychain...
security unlock-keychain -p password login.keychain
echo Running command : Engine/Binaries/DotNET/UnrealBuildTool.exe "$@"
mono Engine/Binaries/DotNET/UnrealBuildTool.exe "$@"

ExitCode=$?
if [ $ExitCode -eq 254 ] || [ $ExitCode -eq 255 ] || [ $ExitCode -eq 2 ]; then
exit 0
else
exit $ExitCode
fi

因为编译时会把Build.sh通过RSync传递到Mac上,所以可以看到以下log:

1
2
3
4
5
6
7
8
9
10
11
[Remote] Executing build
Running bundled mono, version: Mono JIT compiler version 5.16.0.220 (2018-06/bb3ae37d71a Fri Nov 16 17:12:11 EST 2018)
unlock mac keychain...
Running command : Engine/Binaries/DotNET/UnrealBuildTool.exe UnrealHeaderTool Mac Development -SkipRulesCompile -XmlConfigCache=/Users/buildmachine/UE4/Builds/lipengzha-PC2/C/BuildAgent/workspace/FGameEngine/Engine/Engine/Intermediate/Build/XmlConfigCache.bin -precompile -allmodules -Log=/Users/buildmachine/UE4/Builds/lipengzha-PC2/C/BuildAgent/workspace/FGameEngine/Engine/Engine/Programs/AutomationTool/Saved/Logs/UBT-UnrealHeaderTool-Mac-Development_Remote.txt -Manifest=/Users/buildmachine/UE4/Builds/lipengzha-PC2/C/BuildAgent/workspace/FGameEngine/Engine/Engine/Intermediate/Remote/UnrealHeaderTool/Mac/Development/Manifest.xml
Target is up to date
Deploying UnrealHeaderTool Mac Development...
Deploying now!
Total execution time: 1.01 seconds
[Remote] Downloading C:\BuildAgent\workspace\FGameEngine\Engine\Engine\Intermediate\Remote\UnrealHeaderTool\Mac\Development\Manifest.xml
[Remote] Downloading build products
receiving file list ... done

这样每次编译都会解锁keychain,从而避免ssh连接时没有访问codesign导致的签名错误。

注意:也需要排查BaseEngine.ini中SigningCertificate的值是否被指定。

Invalid trust settings.

如果Log中出现以下错误:

1
2
Code Signing Error: Invalid trust settings. Restore system default trust settings for certificate "iPhone Developer: Created via API (JDPXHYVWYZ)" in order to sign code with it.
Code Signing Error: Code signing is required for product type 'Application' in SDK 'iOS 13.6'

这是因为在Mac上的钥匙串中对证书的设置被修改为了始终信任,修改回使用系统默认即可。

IOS MobileProvision路径

位于以下路径,可以清理掉过期或者多余的mobileprovision:

1
~/Library/MobileDevice/Provisioning\ Profiles

漏掉PRAGMA_ENABLE_OPTIMIZATION导致的编译错误

有时候希望关闭代码优化,在UE中可以使用封装的宏:

1
2
3
PRAGMA_DISABLE_OPTIMIZATION
// ...
PRAGMA_ENABLE_OPTIMIZATION

但是如果漏掉了PRAGMA_ENABLE_OPTIMIZATION,有时会产生以下编译错误(尤其是包含了Slate等模块的代码):

1
2
3
2>D:\Client\Plugins\UMGExtention\Intermediate\Build\Win64\UE4Editor\Development\UMGExtention\Module.UMGExtention.cpp(16): Error C4426 : optimization flags changed after including header, may be due to #pragma optimize()
2>D:\Client\Plugins\UMGExtention\Intermediate\Build\Win64\UE4Editor\Development\UMGExtention\Module.UMGExtention.cpp(24): Error C4426 : optimization flags changed after including header, may be due to #pragma optimize()
2>EXEC: Error : 2 (0x02) Target: 'D:\Client\Plugins\UMGExtention\Intermediate\Build\Win64\UE4Editor\Development\UMGExtention\Module.UMGExtention.cpp.obj'

读取文件的一部分

可以使用IFileHandle来实现,它有seek函数可以随意偏移。

Runtime\Core\Public\GenericPlatform\GenericPlatformFile.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
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
/** 
* File handle interface.
**/
class CORE_API IFileHandle
{
public:
/** Destructor, also the only way to close the file handle **/
virtual ~IFileHandle()
{
}

/** Return the current write or read position. **/
virtual int64 Tell() = 0;
/**
* Change the current write or read position.
* @param NewPosition new write or read position
* @return true if the operation completed successfully.
**/
virtual bool Seek(int64 NewPosition) = 0;

/**
* Change the current write or read position, relative to the end of the file.
* @param NewPositionRelativeToEnd new write or read position, relative to the end of the file should be <=0!
* @return true if the operation completed successfully.
**/
virtual bool SeekFromEnd(int64 NewPositionRelativeToEnd = 0) = 0;

/**
* Read bytes from the file.
* @param Destination Buffer to holds the results, should be at least BytesToRead in size.
* @param BytesToRead Number of bytes to read into the destination.
* @return true if the operation completed successfully.
**/
virtual bool Read(uint8* Destination, int64 BytesToRead) = 0;

/**
* Write bytes to the file.
* @param Source Buffer to write, should be at least BytesToWrite in size.
* @param BytesToWrite Number of bytes to write.
* @return true if the operation completed successfully.
**/
virtual bool Write(const uint8* Source, int64 BytesToWrite) = 0;

/**
* Flushes file handle to disk.
* @param bFullFlush true to flush everything about the file (including its meta-data) with a strong guarantee that it will be on disk by the time this function returns,
* or false to let the operating/file system have more leeway about when the data actually gets written to disk
* @return true if operation completed successfully.
**/
virtual bool Flush(const bool bFullFlush = false) = 0;

/**
* Truncate the file to the given size (in bytes).
* @param NewSize Truncated file size (in bytes).
* @return true if the operation completed successfully.
**/
virtual bool Truncate(int64 NewSize) = 0;

public:
/////////// Utility Functions. These have a default implementation that uses the pure virtual operations.

/** Return the total size of the file **/
virtual int64 Size();
};

创建它的方法首先需要拿到PlatformFile:

1
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();

然后通过它的OpenRead/OpenWrite方法来创建一个IFileHandle:

Runtime\Core\Public\GenericPlatform\GenericPlatformFile.h
1
2
3
4
5
6
7
8
9
10
11
/** Attempt to open a file for reading.
*
* @param Filename file to be opened
* @param bAllowWrite (applies to certain platforms only) whether this file is allowed to be written to by other processes. This flag is needed to open files that are currently being written to as well.
*
* @return If successful will return a non-nullptr pointer. Close the file by delete'ing the handle.
*/
virtual IFileHandle* OpenRead(const TCHAR* Filename, bool bAllowWrite = false) = 0;

/** Attempt to open a file for writing. If successful will return a non-nullptr pointer. Close the file by delete'ing the handle. **/
virtual IFileHandle* OpenWrite(const TCHAR* Filename, bool bAppend = false, bool bAllowRead = false) = 0;

PlatformFile具有跨平台实现,在不同的平台拿到的类型是不一样的,如FIOSPlatformFile/FAndroidPlatformFile等等,具有相同接口的实现。

UE从Pak中加载文件就是通过这样的方式来实现的。

PakBlackList

可以在{PROJECT_DIR}/Build/{PLATFORM}下创建PakBlackList-{CONFIGURATION}.txt文件来限制打包项目时生成的PakList*.txt中的内容:

1
2
3
4
{PROJECTDIR}/Build/Android/PakBlacklist-Shipping.txt
{PROJECTDIR}/Build/Android/PakBlacklist-Debug.txt
{PROJECTDIR}/Build/Android/PakBlacklist-Development.txt
{PROJECTDIR}/Build/Android/PakBlacklist-Test.txt

等文件,按照PakBlackList-{CONFIGURATION}.txt的规则命名,打不同Configuration的包就会去读取对应的文件。

文件中需要填写MountPoint的路径:

1
../../../Client/BlackDir

当打包时生成Paklist时会判断是否匹配这里的规则(StartWith),从而控制是否包含。

相关的代码在Programs/AutomationTool/Script/CopyBuildToStagingDirectory.Automation.cs

Programs/AutomationTool/Script/CopyBuildToStagingDirectory.Automation.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
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
/// <summary>
/// Creates a pak response file using stage context
/// </summary>
/// <param name="SC"></param>
/// <returns></returns>
private static Dictionary<string, string> CreatePakResponseFileFromStagingManifest(DeploymentContext SC, Dictionary<StagedFileReference, FileReference> FilesToStage)
{
// look for optional packaging blacklist if only one config active
List<string> Blacklist = null;
if (SC.StageTargetConfigurations.Count == 1)
{
FileReference PakBlacklistFilename = FileReference.Combine(SC.ProjectRoot, "Build", SC.PlatformDir, string.Format("PakBlacklist-{0}.txt", SC.StageTargetConfigurations[0].ToString()));
if (FileReference.Exists(PakBlacklistFilename))
{
LogInformation("Applying PAK blacklist file {0}. This is deprecated in favor of DefaultPakFileRules.ini", PakBlacklistFilename);
string[] BlacklistContents = FileReference.ReadAllLines(PakBlacklistFilename);
foreach (string Candidate in BlacklistContents)
{
if (Candidate.Trim().Length > 0)
{
if (Blacklist == null)
{
Blacklist = new List<string>();
}
Blacklist.Add(Candidate);
}
}
}
}

var UnrealPakResponseFile = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
foreach (KeyValuePair<StagedFileReference, FileReference> Pair in FilesToStage)
{
FileReference Src = Pair.Value;
string Dest = Pair.Key.Name;

Dest = CombinePaths(PathSeparator.Slash, SC.PakFileInternalRoot, Dest);

if (Blacklist != null)
{
bool bExcludeFile = false;
foreach (string ExcludePath in Blacklist)
{
if (Dest.StartsWith(ExcludePath))
{
bExcludeFile = true;
break;
}
}

if (bExcludeFile) {
LogInformation("Excluding {0}", Src);
continue;
}
}

// Filter I/O store container files
if (Src.HasExtension(".ucas") || Src.HasExtension(".utoc"))
{
LogInformation("Excluding {0}", Src);
continue;
}

// Do a filtered copy of all ini files to allow stripping of values that we don't want to distribute
if (Src.HasExtension(".ini"))
{
string SubFolder = Pair.Key.Name.Replace('/', Path.DirectorySeparatorChar);
FileReference NewIniFilename = FileReference.Combine(SC.ProjectRoot, "Saved", "Temp", SC.PlatformDir, SubFolder);
InternalUtils.SafeCreateDirectory(NewIniFilename.Directory.FullName, true);
InternalUtils.SafeCopyFile(Src.FullName, NewIniFilename.FullName, IniKeyBlacklist
: SC.IniKeyBlacklist, IniSectionBlacklist
: SC.IniSectionBlacklist);
Src = NewIniFilename;
}

// there can be files that only differ in case only, we don't support that in paks as paks are case-insensitive
if (UnrealPakResponseFile.ContainsKey(Src.FullName))
{
if (UnrealPakResponseFile[Src.FullName] != Dest)
{
throw new AutomationException("Staging manifest already contains {0} (or a file that differs in case only)", Src);
}
LogWarning("Tried to add duplicate file to stage " + Src + " ignoring second attempt pls fix");
continue;
}

UnrealPakResponseFile.Add(Src.FullName, Dest);
}

return UnrealPakResponseFile;
}

[/Script/BuildSettings.BuildSettings]

可以在Engine.ini中控制以下参数,选择性的编译某些功能,这些参数在TargetRules中定义:Programs/UnrealBuildTool/Configuration/TargetRules.cs

它们都是TargetRules的成员,可以直接在Target.cs中设置值,部分参数也可以在ini中配置,可配置的ini如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[/Script/BuildSettings.BuildSettings]
;Whether to include PhysX APEX support.
bCompileAPEX=true
;Whether to include ICU unicode/i18n support in Core.
bCompileICU=true
;Whether to compile CEF3 support.
bCompileCEF3=true
;Whether we should compile SQLite using the custom "Unreal" platform (true), or using the native platform (false).
bCompileCustomSQLitePlatform=true
;Whether to utilize cache freed OS allocs with MallocBinned
bUseCacheFreedOSAllocs=true
;Whether to compile Recast navmesh generation.
bCompileRecast=true
;Whether to compile SpeedTree support.
bOverrideCompileSpeedTree=true
;Whether to include plugin support.
bCompileWithPluginSupport=false
;Whether to include PerfCounters support.
bWithPerfCountersOverride=true
;True if we need FreeType support.
bCompileFreeType=true
;True if we want to favor optimizing size over speed.
bCompileForSize=true

选择性地去关闭这些可以减少so的大小5-10M。

Unreal Insights的初始化

开启Unreal Insights的采集可以使用命令行参数:-trace=counters,cpu,frame,bookmark,gpu,在引擎启动时会解析要采集的模块:

Runtime\Launch\Private\LaunchEngineLoop.cpp
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
int32 FEngineLoop::PreInitPreStartupScreen(const TCHAR* CmdLine)
{
FDelayedAutoRegisterHelper::RunAndClearDelayedAutoRegisterDelegates(EDelayedRegisterRunPhase::StartOfEnginePreInit);

#if UE_TRACE_ENABLED
{
Trace::Initialize();

FString EnabledChannels;
FParse::Value(CmdLine, TEXT("-trace="), EnabledChannels, false);
UE::String::ParseTokens(EnabledChannels, TEXT(","), [](FStringView Token) {
TCHAR ChannelName[64];
const size_t ChannelNameSize = Token.CopyString(ChannelName, 64);
ChannelName[ChannelNameSize] = '\0';
Trace::ToggleChannel(ChannelName, true);
});

TRACE_REGISTER_GAME_THREAD(FPlatformTLS::GetCurrentThreadId());
TRACE_CPUPROFILER_INIT(CmdLine);
TRACE_PLATFORMFILE_INIT(CmdLine);
TRACE_COUNTERS_INIT(CmdLine);
}
#endif

SCOPED_BOOT_TIMING("FEngineLoop::PreInitPreStartupScreen");

// ...
}

UPackage::Save

Package的存储过程是放到一个单独的线程去执行的:

Runtime\CoreUObject\Private\UObject\SavePackage.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static void AsyncWriteFileWithSplitExports(TAsyncWorkSequence<FMD5>& AsyncWriteAndHashSequence, FLargeMemoryPtr Data, const int64 DataSize, const int64 HeaderSize, const TCHAR* Filename, EAsyncWriteOptions Options)
{
OutstandingAsyncWrites.Increment();
FString OutputFilename(Filename);
AsyncWriteAndHashSequence.AddWork([Data = MoveTemp(Data), DataSize, HeaderSize, OutputFilename = MoveTemp(OutputFilename), Options](FMD5& State) mutable
{
if (EnumHasAnyFlags(Options, EAsyncWriteOptions::ComputeHash))
{
State.Update(Data.Get(), DataSize);
}

if (EnumHasAnyFlags(Options, EAsyncWriteOptions::WriteFileToDisk))
{
// Write .uasset file
WriteToFile(OutputFilename, Data.Get(), HeaderSize);

// Write .uexp file
const FString FilenameExports = FPaths::ChangeExtension(OutputFilename, TEXT(".uexp"));
WriteToFile(FilenameExports, Data.Get() + HeaderSize, DataSize - HeaderSize);
}

OutstandingAsyncWrites.Decrement();
});
}

调用栈为:

在通过UPackage::Save来执行Cook资源的存储并立即打包的操作可能会导致在该Task线程中还没有存储完毕,其他线程就已经开始打包了,导致打包包含文件失败。

UE内置的Release Patch

UE的Patch有一些缺点:

  • 无法精确地控制Patch包含的内容且不方便拆分。
  • 无法进行迭代Patch,不能直观预览资源信息。
  • 无法方便地管理工程和Patch版本。
  • 开发阶段无法方便地进行测试补丁打包。

具体操作是在Project Launcher中进行Release打包:

Cook时的命令:

1
UE4Editor-Cmd.exe "D:\ThirdPerson425\ThirdPerson425.uproject" -run=Cook  -TargetPlatform=WindowsNoEditor -fileopenlog -unversioned -createreleaseversion=1.0.0.0 -compressed -abslog="C:\Program Files\Epic Games\UE_4.25\Engine\Programs\AutomationTool\Saved\Cook-2021.03.19-19.00.30.txt" -stdout -CrashForUAT -unattended -NoLogTimes  -UTF8Output

当打包成功后会在项目的根目录中创建出一个Releases目录,存储以下文件:

1
2
3
4
5
6
7
8
9
D:\ThirdPerson425\Releases>tree /a /f
D:.
\---1.0.0.0
\---WindowsNoEditor
| AssetRegistry.bin
| ThirdPerson425-WindowsNoEditor.pak
|
\---Metadata
DevelopmentAssetRegistry.bin

可以看到它的实现实际上是备份了当前打包版本的AssetRegistry文件,但经过分析发现,它并不会用来和后续的版本做比对,使用的是另一种方式。

在基于某个Release进行Patch时会自动把AssetRegistry.bin和ushaderbytecode包含到pak中(并且shaderbytecode并没有进行patch):

当基于某个基础版本进行Patch的时候,在Project Launher的设置如下:

执行流程中首先需要对项目进行Cook,但是Cook时不需要指定任何版本信息:

1
2
3
4
5
6
7
8
9
10
11
12
UE4Editor-Cmd.exe
"D:\Project\ThirdPerson425.uproject"
-run=Cook
-TargetPlatform=WindowsNoEditor
-fileopenlog
-unversioned
-abslog="C:\Program Files\Epic Games\UE_4.25\Engine\Programs\AutomationTool\Saved\Cook-2021.03.22-15.29.04.txt"
-stdout
-CrashForUAT
-unattended
-NoLogTimes
-UTF8Output

当Cook完毕,执行Pak打包的时候,需要传入版本信息:

1
2
3
4
5
6
7
8
9
10
11
12
UnrealPak.exe
"D:\Projects\ThirdPerson425.uproject"
"D:\Projects\Package\1.0.0.0_patch1\WindowsNoEditor\ThirdPerson425\Content\Paks\ThirdPerson425-WindowsNoEditor_0_P.pak"
-create="C:\Users\lipengzha\AppData\Roaming\Unreal Engine\AutomationTool\Logs\C+Program+Files+Epic+Games+UE_4.25\PakList_ThirdPerson425-WindowsNoEditor_0_P.txt"
-cryptokeys="D:\Projects\Saved\Cooked\WindowsNoEditor\ThirdPerson425\Metadata\Crypto.json"
-order="D:\Projects\Build\WindowsNoEditor\FileOpenOrder\CookerOpenOrder.log"
-generatepatch="D:\Projects\Releases\1.0.0.0\WindowsNoEditor\ThirdPerson425-WindowsNoEditor*.pak"
-tempfiles="C:\Program Files\Epic Games\UE_4.25\TempFilesThirdPerson425-WindowsNoEditor_0_P"
-patchpaddingalign=2048
-platform=Windows
-multiprocess
-abslog="C:\Users\lipengzha\AppData\Roaming\Unreal Engine\AutomationTool\Logs\C+Program+Files+Epic+Games+UE_4.25\UnrealPak-ThirdPerson425-WindowsNoEditor_0_P-2021.03.22-15.29.17.txt"

注意:

  1. -create=传递进去的Response的txt中是包含整个项目的资源列表的,并非是差异的文件列表。(-create=也可以传递pak文件,用来生成差异)
  2. 需要通过-generatepatch=传递基础包中的pak文件
  3. 读取基础版本包pak中所有文件,计算出每个文件的hash值
  4. 与Response中的资源进行比对,得到新加或者与基础包pak中Hash文件不同的文件列表
  5. 把差异部分打包至新的pak中。

执行时会加载基础包中pak的文件,计算pak中资源的hash值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
LogPakFile: Display: Parsing crypto keys from a crypto key cache file
LogPakFile: Display: Loading response file C:\Users\lipengzha\AppData\Roaming\Unreal Engine\AutomationTool\Logs\C+Program+Files+Epic+Games+UE_4.25\PakList_ThirdPerson425-WindowsNoEditor_0_P.txt
LogPakFile: Display: Added 1559 entries to add to pak file.
LogPakFile: Display: Loading pak order file C:\Users\lipengzha\Documents\Unreal Projects\ThirdPerson425\Build\WindowsNoEditor\FileOpenOrder\CookerOpenOrder.log...
LogPakFile: Display: Finished loading pak order file C:\Users\lipengzha\Documents\Unreal Projects\ThirdPerson425\Build\WindowsNoEditor\FileOpenOrder\CookerOpenOrder.log.
LogPakFile: Display: Generating patch from C:\Users\lipengzha\Documents\Unreal Projects\ThirdPerson425\Releases\1.0.0.0\WindowsNoEditor\ThirdPerson425-WindowsNoEditor*.pak.
LogPakFile: Display: Generated hash for "Engine/Content/Animation/DefaultAnimBoneCompressionSettings.uasset"
LogPakFile: Display: Generated hash for "Engine/Content/Animation/DefaultAnimCurveCompressionSettings.uasset"
LogPakFile: Display: Generated hash for "Engine/Content/Animation/DefaultAnimBoneCompressionSettings.uexp"
LogPakFile: Display: Generated hash for "Engine/Content/Animation/DefaultAnimCurveCompressionSettings.uexp"
LogPakFile: Display: Generated hash for "Engine/GlobalShaderCache-PCD3D_SM5.bin"
LogPakFile: Display: Generated hash for "Engine/Content/Functions/Engine_MaterialFunctions01/Opacity/CameraDepthFade.uasset"
LogPakFile: Display: Generated hash for "Engine/Content/Functions/Engine_MaterialFunctions01/Opacity/CameraDepthFade.uexp"
LogPakFile: Display: Generated hash for "Engine/Content/EngineMaterials/T_Default_Material_Grid_M.uasset"
LogPakFile: Display: Generated hash for "Engine/Content/EngineMaterials/T_Default_Material_Grid_N.uasset"
LogPakFile: Display: Generated hash for "Engine/Content/EngineMaterials/WorldGridMaterial.uasset"
LogPakFile: Display: Generated hash for "Engine/Content/EngineMaterials/DefaultMaterial.uasset"

生成hash的函数在GenerateHashesFromPak中,位于PakFileUtilities/Private/PakFileUtilities.cpp中。
其作用是:读取基础包中的文件,计算hash值,用作与新Cook之后的文件进行比对。

核心的代码在以下部分:

PakFileUtilities/Private/PakFileUtilities.cpp
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
bool ExecuteUnrealPak(const TCHAR* CmdLine)
{
// ...

// List of all items to add to pak file
TArray<FPakInputPair> Entries;
FPakCommandLineParameters CmdLineParameters;
ProcessCommandLine(CmdLine, NonOptionArguments, Entries, CmdLineParameters);

// ...

if(NonOptionArguments.Num() > 0)
{
CheckAndReallocThreadPool();

// since this is for creation, we pass true to make it not look in LaunchDir
FString PakFilename = GetPakPath(*NonOptionArguments[0], true);

// List of all items to add to pak file
TArray<FPakInputPair> Entries;
FPakCommandLineParameters CmdLineParameters;
ProcessCommandLine(CmdLine, NonOptionArguments, Entries, CmdLineParameters);

FPakOrderMap OrderMap;
FString ResponseFile;
if (FParse::Value(CmdLine, TEXT("-order="), ResponseFile) && !OrderMap.ProcessOrderFile(*ResponseFile))
{
return false;
}

FString SecondaryResponseFile;
if (FParse::Value(CmdLine, TEXT("-secondaryOrder="), SecondaryResponseFile) && !OrderMap.ProcessOrderFile(*SecondaryResponseFile, true))
{
return false;
}

int32 LowestSourcePakVersion = 0;
TMap<FString, FFileInfo> SourceFileHashes;

if ( CmdLineParameters.GeneratePatch )
{
FString OutputPath;
if (!FParse::Value(CmdLine, TEXT("TempFiles="), OutputPath))
{
OutputPath = FPaths::GetPath(PakFilename) / FString(TEXT("TempFiles"));
}

IFileManager::Get().DeleteDirectory(*OutputPath);

// Check command line for the "patchcryptokeys" param, which will tell us where to look for the encryption keys that
// we need to access the patch reference data
FString PatchReferenceCryptoKeysFilename;
FKeyChain PatchKeyChain;

if (FParse::Value(FCommandLine::Get(), TEXT("PatchCryptoKeys="), PatchReferenceCryptoKeysFilename))
{
LoadKeyChainFromFile(PatchReferenceCryptoKeysFilename, PatchKeyChain);
ApplyEncryptionKeys(PatchKeyChain);
}

UE_LOG(LogPakFile, Display, TEXT("Generating patch from %s."), *CmdLineParameters.SourcePatchPakFilename, true );

if (!GenerateHashesFromPak(*CmdLineParameters.SourcePatchPakFilename, *PakFilename, SourceFileHashes, true, PatchKeyChain, /*Out*/LowestSourcePakVersion))
{
if (ExtractFilesFromPak(*CmdLineParameters.SourcePatchPakFilename, SourceFileHashes, *OutputPath, true, PatchKeyChain, nullptr) == false)
{
UE_LOG(LogPakFile, Warning, TEXT("Unable to extract files from source pak file for patch"));
}
else
{
CmdLineParameters.SourcePatchDiffDirectory = OutputPath;
}
}

ApplyEncryptionKeys(KeyChain);
}


// Start collecting files
TArray<FPakInputPair> FilesToAdd;
CollectFilesToAdd(FilesToAdd, Entries, OrderMap, CmdLineParameters);

if ( CmdLineParameters.GeneratePatch )
{
// We need to get a list of files that were in the previous patch('s) Pak, but NOT in FilesToAdd
TArray<FPakInputPair> DeleteRecords = GetNewDeleteRecords(FilesToAdd, SourceFileHashes);

//if the patch is built using old source pak files, we need to handle the special case where a file has been moved between chunks but no delete record was created (this would cause a rogue delete record to be created in the latest pak), and also a case where the file was moved between chunks and back again without being changed (this would cause the file to not be included in this chunk because the file would be considered unchanged)
if (LowestSourcePakVersion < FPakInfo::PakFile_Version_DeleteRecords)
{
int32 CurrentPatchChunkIndex = GetPakChunkIndexFromFilename(PakFilename);

UE_LOG(LogPakFile, Display, TEXT("Some patch source paks were generated with an earlier version of UnrealPak that didn't support delete records. checking for historic assets that have moved between chunks to avoid creating invalid delete records"));
FString SourcePakFolder = FPaths::GetPath(CmdLineParameters.SourcePatchPakFilename);

//remove invalid items from DeleteRecords and set 'bForceInclude' on some SourceFileHashes
ProcessLegacyFileMoves(DeleteRecords, SourceFileHashes, SourcePakFolder, FilesToAdd, CurrentPatchChunkIndex);
}
FilesToAdd.Append(DeleteRecords);

// if we are generating a patch here we remove files which are already shipped...
RemoveIdenticalFiles(FilesToAdd, CmdLineParameters.SourcePatchDiffDirectory, SourceFileHashes, CmdLineParameters.SeekOptParams, CmdLineParameters.ChangedFilesOutputFilename);
}


bool bResult = CreatePakFile(*PakFilename, FilesToAdd, CmdLineParameters, KeyChain);

if (CmdLineParameters.GeneratePatch)
{
FString OutputPath = FPaths::GetPath(PakFilename) / FString(TEXT("TempFiles"));
// delete the temporary directory
IFileManager::Get().DeleteDirectory(*OutputPath, false, true);
}

GetDerivedDataCacheRef().WaitForQuiescence(true);

return bResult;
}

}

通过分析UE的Patch机制可以知道,它的版本比对比较粗暴,是直接得到Pak中文件的二进制信息计算Hash值与当前工程中Cook之后的HASH值来进行比对的,本质上就是基于二进制的比对,这就需要管理好DDC等COOK之后生成的文件,我觉得这样是不合理的,所以HotPatcher是基于原始资源的GUID的比对。

配置ConsoleVariable默认值

有时候需要在打包时设定某些控制台变量的默认值,可以通过以下方式来设置:

DefaultEngine.ini
1
2
[ConsoleVariables]
pakcache.Enable=0

引擎中的相关代码为:

Engine\Source\Runtime\Core\Private\Misc\ConfigCacheIni.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void FConfigCacheIni::LoadConsoleVariablesFromINI()
{
FString ConsoleVariablesPath = FPaths::EngineDir() + TEXT("Config/ConsoleVariables.ini");

#if !DISABLE_CHEAT_CVARS
// First we read from "../../../Engine/Config/ConsoleVariables.ini" [Startup] section if it exists
// This is the only ini file where we allow cheat commands (this is why it's not there for UE_BUILD_SHIPPING || UE_BUILD_TEST)
ApplyCVarSettingsFromIni(TEXT("Startup"), *ConsoleVariablesPath, ECVF_SetByConsoleVariablesIni, true);
#endif // !DISABLE_CHEAT_CVARS

// We also apply from Engine.ini [ConsoleVariables] section
ApplyCVarSettingsFromIni(TEXT("ConsoleVariables"), *GEngineIni, ECVF_SetBySystemSettingsIni);

IConsoleManager::Get().CallAllConsoleVariableSinks();
}

默认会读取Engine/Config/ConsoleVariables.ini[Startup]GEngineIniEngine/Config/BaseEngine.ini/{PROJECT}/Config/DefaultEngine.ini/{PROJECT}/Config/{PLATFORM}/{PLATFORM}Engine.ini)中的[ConsoleVariables]中的配置。

UPROPERTY的ConsoleVariable

成员属性可以绑定到某个控制台变量:

RendererSettings.h
1
2
3
4
5
6
7
8
/**
"Skin cache allows a compute shader to skin once each vertex, save those results into a new buffer and reuse those calculations when later running the depth, base and velocity passes. This also allows opting into the 'recompute tangents' for skinned mesh instance feature. Disabling will reduce the number of shader permutations required per material. Changing this setting requires restarting the editor."
*/
UPROPERTY(config, EditAnywhere, Category = Optimizations, meta = (
ConsoleVariable = "r.SkinCache.CompileShaders", DisplayName = "Support Compute Skin Cache",
ToolTip = "Cannot be disabled while Ray Tracing is enabled as it is then required.",
ConfigRestartRequired = true))
uint32 bSupportSkinCacheShaders : 1;

定义在其他文件中的FAutoConsoleVariableRef:

GPUSkinCache.cpp
1
2
3
4
5
6
7
8
9
10
11
static int32 GEnableGPUSkinCacheShaders = 0;

static FAutoConsoleVariableRef CVarEnableGPUSkinCacheShaders(
TEXT("r.SkinCache.CompileShaders"),
GEnableGPUSkinCacheShaders,
TEXT("Whether or not to compile the GPU compute skinning cache shaders.\n")
TEXT("This will compile the shaders for skinning on a compute job and not skin on the vertex shader.\n")
TEXT("GPUSkinVertexFactory.usf needs to be touched to cause a recompile if this changes.\n")
TEXT("0 is off(default), 1 is on"),
ECVF_RenderThreadSafe | ECVF_ReadOnly
);

代码编译时执行脚本

在插件的uplugin文件中可以写入PreBuildSteps/PostBuildSteps以下两个元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"PostBuildSteps":
{
"Win64":
[
"\"$(PluginDir)\\Source\\AkAudio\\WwisePostBuildSteps.bat\" \"$(EngineDir)\\Binaries\\Win64\\UE4Editor-cmd.exe\" \"$(ProjectFile)\" $(TargetType) -run=AkPluginActivator -platform=$(TargetPlatform) -configuration=Profile -targetconfig=$(TargetConfiguration)"
],
"PS4":
[
"\"$(PluginDir)\\Source\\AkAudio\\WwisePostBuildSteps.bat\" \"$(EngineDir)\\Binaries\\Win64\\UE4Editor-cmd.exe\" \"$(ProjectFile)\" -run=AkPluginActivator -platform=$(TargetPlatform) -configuration=Profile -targetconfig=$(TargetConfiguration)"
]
}
}

可以在构建时指定自动执行一个脚本,用来处理一些特殊的功能。

ProjectDescriptor的声明:Source\Programs\UnrealBuildTool\System\ProjectDescriptor.cs

在UBT的Source\Programs\UnrealBuildTool\Configuration\UEBuildTarget.cs中被解析:

Source\Programs\UnrealBuildTool\Configuration\UEBuildTarget.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
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
/// <summary>
/// Creates scripts for executing the pre-build scripts
/// </summary>
public FileReference[] CreatePreBuildScripts() {
// Find all the pre-build steps
List < Tuple < string[],
UEBuildPlugin >> PreBuildCommandBatches = new List < Tuple < string[],
UEBuildPlugin >> ();
if (ProjectDescriptor != null && ProjectDescriptor.PreBuildSteps != null) {
AddCustomBuildSteps(ProjectDescriptor.PreBuildSteps, null, PreBuildCommandBatches);
}
if (Rules.PreBuildSteps.Count > 0) {
PreBuildCommandBatches.Add(new Tuple < string[], UEBuildPlugin > (Rules.PreBuildSteps.ToArray(), null));
}
foreach(UEBuildPlugin BuildPlugin in BuildPlugins.Where(x =>x.Descriptor.PreBuildSteps != null)) {
AddCustomBuildSteps(BuildPlugin.Descriptor.PreBuildSteps, BuildPlugin, PreBuildCommandBatches);
}
return WriteCustomBuildStepScripts(BuildHostPlatform.Current.Platform, ProjectIntermediateDirectory, "PreBuild", PreBuildCommandBatches);
}

/// <summary>
/// Creates scripts for executing post-build steps
/// </summary>
/// <returns>Array of post-build scripts</returns>
private FileReference[] CreatePostBuildScripts() {
// Find all the post-build steps
List < Tuple < string[],
UEBuildPlugin >> PostBuildCommandBatches = new List < Tuple < string[],
UEBuildPlugin >> ();
if (!Rules.bDisableLinking) {
if (ProjectDescriptor != null && ProjectDescriptor.PostBuildSteps != null) {
AddCustomBuildSteps(ProjectDescriptor.PostBuildSteps, null, PostBuildCommandBatches);
}
if (Rules.PostBuildSteps.Count > 0) {
PostBuildCommandBatches.Add(new Tuple < string[], UEBuildPlugin > (Rules.PostBuildSteps.ToArray(), null));
}
foreach(UEBuildPlugin BuildPlugin in BuildPlugins.Where(x =>x.Descriptor.PostBuildSteps != null)) {
AddCustomBuildSteps(BuildPlugin.Descriptor.PostBuildSteps, BuildPlugin, PostBuildCommandBatches);
}
}
return WriteCustomBuildStepScripts(BuildHostPlatform.Current.Platform, ProjectIntermediateDirectory, "PostBuild", PostBuildCommandBatches);
}

/// <summary>
/// Adds custom build steps from the given JSON object to the list of command batches
/// </summary>
/// <param name="BuildSteps">The custom build steps</param>
/// <param name="Plugin">The plugin to associate with these commands</param>
/// <param name="CommandBatches">List to receive the command batches</param>
private void AddCustomBuildSteps(CustomBuildSteps BuildSteps, UEBuildPlugin Plugin, List < Tuple < string[], UEBuildPlugin >> CommandBatches) {
string[] Commands;
if (BuildSteps.TryGetCommands(BuildHostPlatform.Current.Platform, out Commands)) {
CommandBatches.Add(Tuple.Create(Commands, Plugin));
}
}

/// <summary>
/// Write scripts containing the custom build steps for the given host platform
/// </summary>
/// <param name="HostPlatform">The current host platform</param>
/// <param name="Directory">The output directory for the scripts</param>
/// <param name="FilePrefix">Bare prefix for all the created script files</param>
/// <param name="CommandBatches">List of custom build steps, and their matching PluginInfo (if appropriate)</param>
/// <returns>List of created script files</returns>
private FileReference[] WriteCustomBuildStepScripts(UnrealTargetPlatform HostPlatform, DirectoryReference Directory, string FilePrefix, List < Tuple < string[], UEBuildPlugin >> CommandBatches) {
List < FileReference > ScriptFiles = new List < FileReference > ();
foreach(Tuple < string[], UEBuildPlugin > CommandBatch in CommandBatches) {
// Find all the standard variables
Dictionary < string,
string > Variables = GetTargetVariables(CommandBatch.Item2);

// Get the output path to the script
string ScriptExtension = (HostPlatform == UnrealTargetPlatform.Win64) ? ".bat": ".sh";
FileReference ScriptFile = FileReference.Combine(Directory, String.Format("{0}-{1}{2}", FilePrefix, ScriptFiles.Count + 1, ScriptExtension));

// Write it to disk
List < string > Contents = new List < string > ();
if (HostPlatform == UnrealTargetPlatform.Win64) {
Contents.Insert(0, "@echo off");
}
foreach(string Command in CommandBatch.Item1) {
Contents.Add(Utils.ExpandVariables(Command, Variables));
}
if (!DirectoryReference.Exists(ScriptFile.Directory)) {
DirectoryReference.CreateDirectory(ScriptFile.Directory);
}
File.WriteAllLines(ScriptFile.FullName, Contents);

// Add the output file to the list of generated scripts
ScriptFiles.Add(ScriptFile);
}
return ScriptFiles.ToArray();
}

在编译代码之后会执行指定的脚本,会有以下Log:

1
2>8> Exec: PostBuild-1.bat.ran

注意:测试中发现如果插件的代码没有变动,它不会执行。

CopyDirectory

可以使用UE中的IPlatformFile::CopyDirectoryTree,但是注意调用之前要保证两个目标路径都存在:

1
2
3
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
PlatformFile.CreateDirectoryTree(*OutMetadir);
PlatformFile.CopyDirectoryTree(*OutMetadir,*MetadataDir,true);

UE4快捷键

Windows.h造成的名字冲突

有时候需要包含一些平台相关的代码,如windows.h里面包含了很多头文件,其中一些定义了很多宏,如:

fileapi.h
1
2
3
4
5
#ifdef UNICODE
#define DeleteFile DeleteFileW
#else
#define DeleteFile DeleteFileA
#endif // !UNICODE

如果我们同时在代码中包含了windows.h和使用了IPlatformFile的接口来调用它的DeleteFile函数,因为DeleteFile被定义成了宏,所以在预处理阶段就会被替换,导致编译时访问IPlatformFileDeleteFileW成员,但实际上它是不存在的,就产生了以下编译错误:

1
error C2039: 'DeleteFileW': is not a member of 'IPlatformFile'

解决办法就是不直接包含windows.h而是使用以下封装:

1
2
3
#include "Windows/AllowWindowsPlatformTypes.h"
#include "windows.h"
#include "Windows/HideWindowsPlatformTypes.h"

这样可以避免污染UE的符号名字。

UE的文档中也有介绍:第三方库#故障排除

Core Redirects

有时候Class/Enums/functions/packages/property/struct被改了名字,导致所有引用到它们的资源都要手动修改一遍,非常麻烦。

为了解决这个问题,UE提供了重定向功能,可以在不变动大量资源的情况下进行重定向到新的资源。

打包时Shader的编译

UE打包项目时并不是所有的shader都会被编译到ushaderbytecode中的,只有打包进去的才会编译进去。
进行了一个测试:

  1. /Game添加到了Directories to Never Cook
  2. 打包工程到Android

最终编译出来的ushaderbytecode的大小为:

在UE打包时会拉起Cook进行执行资源的Cook和Shader的编译,应该是在当前的Cook进程中编译的Shader最终会写入到ushaderbytecode文件中,没有被执行Cook的资源不会被编译Shader,从而实现了不会把没有用到的Shader打包的行为。

不过以上只是猜测,有时间具体分析下Cook和Shader编译相关的代码。

DDC共享

共享DDC的作用是:当同一网络内共享DDC的人只要有一个人编译了DDC,其他人就无需重新编译,节省Shader的编译时间,提高效率。

所以,一般情况下只需要在局域网内部署一个高IO吞吐的机器,每个人都把该机器的共享目录挂载到本地,就可以实现DDC的共享,因为DDC的目录是相对于网络路径的,所以每个人的修改都会影响到其他人,从而实现一个编译多人共享的效果。

UE的文档里介绍了三种设置的方法:

  1. 修改DefaultEngine.ini添加DerivedDataBackendGraph项;
  2. 系统中添加UE-SharedDataCachePath环境变量;
  3. 在UE的Editor Preferences-Global-Shared Derived Data Cache设置共享的DDC目录;

UE推荐的是使用第一种方法,但是具体实践和文档中介绍的略有不同。

对于源码版引擎,使用DDC文档中介绍的第一种方法,在项目的DefaultEngine.ini中添加以下项:

DefaultEngine.ini
1
2
[DerivedDataBackendGraph]
Shared=(Type=FileSystem, ReadOnly=false, Clean=false, Flush=false, DeleteUnused=true, UnusedFileAge=10, FoldersToClean=10, MaxFileChecksPerSec=1, ConsiderSlowAt=70, PromptIfMissing=false, Path=\\YourDDCServer\DDC, EnvPathOverride=UE-SharedDataCachePath, EditorOverrideSetting=SharedDerivedDataCache)

但是对于安装版引擎(Installed),要把DerivedDataBackendGraph改成InstalledDerivedDataBackendGraph

DefaultEngine.ini
1
2
[InstalledDerivedDataBackendGraph]
Shared=(Type=FileSystem, ReadOnly=false, Clean=false, Flush=false, DeleteUnused=true, UnusedFileAge=10, FoldersToClean=10, MaxFileChecksPerSec=1, ConsiderSlowAt=70, PromptIfMissing=false, Path=\\YourDDCServer\FMGame\DDC, EnvPathOverride=UE-SharedDataCachePath, EditorOverrideSetting=SharedDerivedDataCache)

这是因为引擎在Developer/DerivedDataCache/Private/DerivedDataBackends.cpp中对安装版和源码版做了区分:

1
2
3
4
5
6
7
8
9
10
11
FDerivedDataBackendGraph()
// ...
{
// ...
if( !RootCache )
{
// Use default graph
GraphName = FApp::IsEngineInstalled() ? TEXT("InstalledDerivedDataBackendGraph") : TEXT("DerivedDataBackendGraph");
// ...
}
};

在设置完之后就可以通过Commandlet来执行DDC的生成了:

1
Engine\Binaries\Win64\UE4Editor.exe Client\Client.uproject -run=DerivedDataCache -fill

该Commandlet定义在Editor/UnrealEd/Classes/Commandlets/DerivedDataCacheCommandlet.h

如果想要在配置文件添加之后关闭掉DDC,可以在Editor启动时添加参数-ddc=noshared

.target文件

UE编译项目(Game/Program等)的时候会生成该项目的target文件,记录了该项目的文件依赖,以UHT为例

UnrealHeaderTool.target
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
{
"TargetName": "UnrealHeaderTool",
"Platform": "Win64",
"Configuration": "Development",
"TargetType": "Program",
"Architecture": "",
"Launch": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool.exe",
"Version":
{
"MajorVersion": 4,
"MinorVersion": 25,
"PatchVersion": 1,
"Changelist": 0,
"CompatibleChangelist": 13144385,
"IsLicenseeVersion": 0,
"IsPromotedBuild": 0,
"BranchName": "++UE4+Release-4.25",
"BuildId": "b5ff8f70-3501-456c-bde4-438215a9b5c5",
"BuildVersion": ""
},
"BuildProducts": [
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool.exe",
"Type": "Executable"
},
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool.pdb",
"Type": "SymbolFile"
},
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool-BuildSettings.dll",
"Type": "DynamicLibrary"
},
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool-BuildSettings.pdb",
"Type": "SymbolFile"
},
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool-TraceLog.dll",
"Type": "DynamicLibrary"
},
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool-TraceLog.pdb",
"Type": "SymbolFile"
},
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool-Core.dll",
"Type": "DynamicLibrary"
},
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool-Core.pdb",
"Type": "SymbolFile"
},
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool-Json.dll",
"Type": "DynamicLibrary"
},
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool-Json.pdb",
"Type": "SymbolFile"
},
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool-Projects.dll",
"Type": "DynamicLibrary"
},
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool-Projects.pdb",
"Type": "SymbolFile"
},
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool-CoreUObject.dll",
"Type": "DynamicLibrary"
},
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool-CoreUObject.pdb",
"Type": "SymbolFile"
},
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool.version",
"Type": "RequiredResource"
},
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool.modules",
"Type": "RequiredResource"
}
],
"RuntimeDependencies": [
{
"Path": "$(EngineDir)/Binaries/ThirdParty/DbgHelp/dbghelp.dll",
"Type": "NonUFS"
}
],
"AdditionalProperties": [
{
"Name": "SDK",
"Value": "Not Applicable"
}
]
}

基于这个文件列表,可以实现自动提取程序依赖的功能,在想要提取UE的Program类型的程序时很有用。

IOS Crash分析文档

材质Sampler超限制Crash

打包Android运行时发现一个Crash问题:

1
2
3
4
[2021.02.26-06.24.25:291][593]LogRHI: Error: Failed to link program. Current total programs: 112 program binary bytes: 1786425
log:
Error: Sampler Sampler location or component exceeds max allowed.
Error: Linking failed.

该错误执行在Runtime/OpenGLDrv/Private/OpenGLShaders.cpp中。

原因是某些材质的Sampler超了限制,导致错误。

排查方法为:打开材质,查看在平台中的Sampler的数量:

DoesPackageExists分析

1
2
3
4
5
6
7
8
9
10
11
12
13
class COREUOBJECT_API FPackageName
{
public:
/**
* Checks if the package exists on disk.
*
* @param LongPackageName Package name.
* @param OutFilename Package filename on disk.
* @param InAllowTextFormats Detect text format packages as well as binary (priority to text)
* @return true if the specified package name points to an existing package, false otherwise.
**/
static bool DoesPackageExist(const FString& LongPackageName, const FGuid* Guid = NULL, FString* OutFilename = NULL, bool InAllowTextFormats = true);
};

该函数用来检测Package是否在磁盘上存在

注意:在磁盘上存在在UE里有两个情况:

  1. Editor下存在uasset文件
  2. 打包模式下uasset是否在Mounted的Pak中存在

事实上,UE也是这么做检测的:

  1. 首先把要检测的LongPackageName根据规则转换为文件路径
  2. 通过FileManager来检测文件路径是否存在

在Editor和打包模式下,FileManager通过GetLowLevel()拿到IPlatformFile,之后再进行FileExist的检测,UE针对各个平台封装了IPlatformFile,而且也具有PakPlatformFile的实现,可以实现从Pak中读取文件与在普通文件系统中访问一样的接口。


UE的跨平台写法,声明在通用接口里,定义在各个平台的单独文件中:

在引擎启动时会把这些IPlatformFile的对象创建:

Runtime/Launch/Private/LaunchEngineLoop.cpp
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
153
/**
* Look for any file overrides on the command line (i.e. network connection file handler)
*/
bool LaunchCheckForFileOverride(const TCHAR* CmdLine, bool& OutFileOverrideFound)
{
OutFileOverrideFound = false;

// Get the physical platform file.
IPlatformFile* CurrentPlatformFile = &FPlatformFileManager::Get().GetPlatformFile();

// Try to create pak file wrapper
{
IPlatformFile* PlatformFile = ConditionallyCreateFileWrapper(TEXT("PakFile"), CurrentPlatformFile, CmdLine);
if (PlatformFile)
{
CurrentPlatformFile = PlatformFile;
FPlatformFileManager::Get().SetPlatformFile(*CurrentPlatformFile);
}
PlatformFile = ConditionallyCreateFileWrapper(TEXT("CachedReadFile"), CurrentPlatformFile, CmdLine);
if (PlatformFile)
{
CurrentPlatformFile = PlatformFile;
FPlatformFileManager::Get().SetPlatformFile(*CurrentPlatformFile);
}
}

// Try to create sandbox wrapper
{
IPlatformFile* PlatformFile = ConditionallyCreateFileWrapper(TEXT("SandboxFile"), CurrentPlatformFile, CmdLine);
if (PlatformFile)
{
CurrentPlatformFile = PlatformFile;
FPlatformFileManager::Get().SetPlatformFile(*CurrentPlatformFile);
}
}

#if !UE_BUILD_SHIPPING // UFS clients are not available in shipping builds.
// Streaming network wrapper (it has a priority over normal network wrapper)
bool bNetworkFailedToInitialize = false;
do
{
bool bShouldUseStreamingFile = false;
IPlatformFile* NetworkPlatformFile = ConditionallyCreateFileWrapper(TEXT("StreamingFile"), CurrentPlatformFile, CmdLine, &bNetworkFailedToInitialize, &bShouldUseStreamingFile);
if (NetworkPlatformFile)
{
CurrentPlatformFile = NetworkPlatformFile;
FPlatformFileManager::Get().SetPlatformFile(*CurrentPlatformFile);
}

bool bShouldUseCookedIterativeFile = false;
if ( !bShouldUseStreamingFile && !NetworkPlatformFile )
{
NetworkPlatformFile = ConditionallyCreateFileWrapper(TEXT("CookedIterativeFile"), CurrentPlatformFile, CmdLine, &bNetworkFailedToInitialize, &bShouldUseCookedIterativeFile);
if (NetworkPlatformFile)
{
CurrentPlatformFile = NetworkPlatformFile;
FPlatformFileManager::Get().SetPlatformFile(*CurrentPlatformFile);
}
}

// if streaming network platform file was tried this loop don't try this one
// Network file wrapper (only create if the streaming wrapper hasn't been created)
if ( !bShouldUseStreamingFile && !bShouldUseCookedIterativeFile && !NetworkPlatformFile)
{
NetworkPlatformFile = ConditionallyCreateFileWrapper(TEXT("NetworkFile"), CurrentPlatformFile, CmdLine, &bNetworkFailedToInitialize);
if (NetworkPlatformFile)
{
CurrentPlatformFile = NetworkPlatformFile;
FPlatformFileManager::Get().SetPlatformFile(*CurrentPlatformFile);
}
}

if (bNetworkFailedToInitialize)
{
FString HostIpString;
FParse::Value(CmdLine, TEXT("-FileHostIP="), HostIpString);
#if PLATFORM_REQUIRES_FILESERVER
FPlatformMisc::LowLevelOutputDebugStringf(TEXT("Failed to connect to file server at %s. RETRYING in 5s.\n"), *HostIpString);
FPlatformProcess::Sleep(5.0f);
uint32 Result = 2;
#else //PLATFORM_REQUIRES_FILESERVER
// note that this can't be localized because it happens before we connect to a filserver - localizing would cause ICU to try to load.... from over the file server connection!
FString Error = FString::Printf(TEXT("Failed to connect to any of the following file servers:\n\n %s\n\nWould you like to try again? No will fallback to local disk files, Cancel will quit."), *HostIpString.Replace( TEXT("+"), TEXT("\n ")));
uint32 Result = FMessageDialog::Open( EAppMsgType::YesNoCancel, FText::FromString( Error ) );
#endif //PLATFORM_REQUIRES_FILESERVER

if (Result == EAppReturnType::No)
{
break;
}
else if (Result == EAppReturnType::Cancel)
{
// Cancel - return a failure, and quit
return false;
}
}
}
while (bNetworkFailedToInitialize);
#endif

#if !UE_BUILD_SHIPPING
// Try to create file profiling wrapper
{
IPlatformFile* PlatformFile = ConditionallyCreateFileWrapper(TEXT("ProfileFile"), CurrentPlatformFile, CmdLine);
if (PlatformFile)
{
CurrentPlatformFile = PlatformFile;
FPlatformFileManager::Get().SetPlatformFile(*CurrentPlatformFile);
}
}
{
IPlatformFile* PlatformFile = ConditionallyCreateFileWrapper(TEXT("SimpleProfileFile"), CurrentPlatformFile, CmdLine);
if (PlatformFile)
{
CurrentPlatformFile = PlatformFile;
FPlatformFileManager::Get().SetPlatformFile(*CurrentPlatformFile);
}
}
// Try and create file timings stats wrapper
{
IPlatformFile* PlatformFile = ConditionallyCreateFileWrapper(TEXT("FileReadStats"), CurrentPlatformFile, CmdLine);
if (PlatformFile)
{
CurrentPlatformFile = PlatformFile;
FPlatformFileManager::Get().SetPlatformFile(*CurrentPlatformFile);
}
}
// Try and create file open log wrapper (lists the order files are first opened)
{
IPlatformFile* PlatformFile = ConditionallyCreateFileWrapper(TEXT("FileOpenLog"), CurrentPlatformFile, CmdLine);
if (PlatformFile)
{
CurrentPlatformFile = PlatformFile;
FPlatformFileManager::Get().SetPlatformFile(*CurrentPlatformFile);
}
}
#endif //#if !UE_BUILD_SHIPPING

// Wrap the above in a file logging singleton if requested
{
IPlatformFile* PlatformFile = ConditionallyCreateFileWrapper(TEXT("LogFile"), CurrentPlatformFile, CmdLine);
if (PlatformFile)
{
CurrentPlatformFile = PlatformFile;
FPlatformFileManager::Get().SetPlatformFile(*CurrentPlatformFile);
}
}

// If our platform file is different than it was when we started, then an override was used
OutFileOverrideFound = (CurrentPlatformFile != &FPlatformFileManager::Get().GetPlatformFile());

return true;
}

LoadObject加载磁盘文件栈

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
FFileManagerGeneric::CreateFileReaderInternal(const wchar_t *,unsigned int,unsigned int) FileManagerGeneric.cpp:47
FLinkerLoad::CreateLoader(TFunction<void __cdecl(void)> &&) LinkerLoad.cpp:1037
FLinkerLoad::Tick(float,bool,bool,TMap<TTuple<FName,FPackageIndex>,FPackageIndex,FDefaultSetAllocator,TDefaultMapHashableKeyFuncs<TTuple<FName,FPackageIndex>,FPackageIndex,0> > *) LinkerLoad.cpp:696
FLinkerLoad::CreateLinker(FUObjectSerializeContext *,UPackage *,const wchar_t *,unsigned int,FArchive *) LinkerLoad.cpp:459
GetPackageLinker(UPackage *,const wchar_t *,unsigned int,UPackageMap *,FGuid *,FArchive *,FUObjectSerializeContext **) Linker.cpp:745
LoadPackageInternal(UPackage *,const wchar_t *,unsigned int,FLinkerLoad *,FArchive *,FUObjectSerializeContext *) UObjectGlobals.cpp:1208
LoadPackage(UPackage *,const wchar_t *,unsigned int,FArchive *,FUObjectSerializeContext *) UObjectGlobals.cpp:1427
ResolveName(UObject *&,FString &,bool,bool,unsigned int,FUObjectSerializeContext *) UObjectGlobals.cpp:767
StaticLoadObjectInternal(UClass *,UObject *,const wchar_t *,const wchar_t *,unsigned int,UPackageMap *,bool,FUObjectSerializeContext *) UObjectGlobals.cpp:828
StaticLoadObject(UClass *,UObject *,const wchar_t *,const wchar_t *,unsigned int,UPackageMap *,bool,FUObjectSerializeContext *) UObjectGlobals.cpp:904
FSoftObjectPath::TryLoad(FUObjectSerializeContext *) SoftObjectPath.cpp:431
FSoftObjectPtr::LoadSynchronous() SoftObjectPtr.h:54
UPaperSprite::GetSourceTexture() PaperSprite.cpp:1474
UPaperSprite::PostLoad() PaperSprite.cpp:1842
UObject::ConditionalPostLoad() Obj.cpp:1067
FAsyncPackage::PostLoadObjects() AsyncLoading.cpp:6356
FAsyncPackage::TickAsyncPackage(bool,bool,float &,FFlushTree *) AsyncLoading.cpp:5533
FAsyncLoadingThread::ProcessAsyncLoading(int &,bool,bool,float,FFlushTree *) AsyncLoading.cpp:4178
FAsyncLoadingThread::TickAsyncThread(bool,bool,float,bool &,FFlushTree *) AsyncLoading.cpp:4819
FAsyncLoadingThread::TickAsyncLoading(bool,bool,float,FFlushTree *) AsyncLoading.cpp:4519
FAsyncLoadingThread::ProcessLoading(bool,bool,float) AsyncLoading.cpp:7057
StaticTick(float,bool,float) UObjectGlobals.cpp:464
UEditorEngine::Tick(float,bool) EditorEngine.cpp:1355
UUnrealEdEngine::Tick(float,bool) UnrealEdEngine.cpp:411
FEngineLoop::Tick() LaunchEngineLoop.cpp:4844
GuardedMain(const wchar_t *) Launch.cpp:171
WinMain(HINSTANCE__ *,HINSTANCE__ *,char *,int) LaunchWindows.cpp:257
__scrt_common_main_seh() 0x00007ff6ab4e140a
BaseThreadInitThunk 0x00007ffabeff7c24
RtlUserThreadStart 0x00007ffabfcad4d1

rebuild metadata

可以通过执行cook的Commandlet来重新生成AssetRegistry以及ushaderbytecode:

1
UE4Editor-cmd.exe PROJECT_NAME.uproject -run=cook -targetplatform=WindowsNoEditor -Iterate -UnVersioned -Compressed

执行完毕之后Saved/Cooked下的AssetRegistry.bin/Metadate目录/Content/ShaderArchive-*.ushaderbytecode以及Ending/GlobalShaderCache*.bin等文件都是生成之后最新的了,可以在之后通过HotPatcher来打包他们了。

IPA包最大为4GB

自动化Editor的Crash上报

因为UE的Crash是拉起一个CrashReportClient程序来执行的,所以如果我们需要在程序出现Crash时上报log信息,可以在CrashReportClientMainWindows中做这部分逻辑。

监听资源创建

在Editor中创建资源,并没有直接保存到磁盘上,所以要监听OnInMemoryAssetCreated

1
2
3
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(
TEXT("AssetRegistry"));
AssetRegistryModule.Get().OnInMemoryAssetCreated().AddRaw(this, &FTestEditorModule::OnInMemoryAssetCreated);

回调过来的就是一个UObject*,实际上是一个UBlueprint*

1
2
3
4
5
6
7
8
void FTestEditorModule::OnInMemoryAssetCreated(UObject* Object)
{
if (nullptr == Object) return;

UBlueprint* Blueprint = Cast<UBlueprint>(Object);
if (nullptr == Blueprint) return;
// ...
}

可以实现监听uasset创建事件,对该uasset执行一些操作(如默认添加接口等)。

拿到UBlueprint后就可以通过FBlueprintEditorUtils等辅助类来实现对蓝图资源的操作了。

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
bool FTestEditor::AddInterface(UBlueprint* Blueprint)
{
if (nullptr == Blueprint) return false;

UClass* Class = Blueprint ? *Blueprint->GeneratedClass : Blueprint ? Blueprint->GetClass() : NULL;
if (nullptr == Class) return false;

if (!Class->IsChildOf<UUserWidget>()) return false;

static UClass* InterfaceClass = UUnLuaInterface::StaticClass();

UFunction* Func = FBlueprintEditorUtils::GetInterfaceFunction(Blueprint, FName("GetModuleName"));
if (nullptr == Func)
{
FBlueprintEditorUtils::ImplementNewInterface(Blueprint, InterfaceClass->GetFName());
}

Func = FBlueprintEditorUtils::GetInterfaceFunction(Blueprint, FName("GetModuleName"));
if (nullptr == Func) return false;

auto ImplementedInterfaces = Blueprint->ImplementedInterfaces;
if (ImplementedInterfaces.Num() <= 0) return false;

auto InterfacesDesc = ImplementedInterfaces[0];

auto Graphs = InterfacesDesc.Graphs;
if (Graphs.Num() <= 0) return false;

auto Graph = Graphs[0]; //UEdGraph
if (nullptr == Graph) return false;

auto Nodes = Graph->Nodes;
if (Nodes.Num() <= 0) return false;

auto Node = Nodes[1]; //UEdGraphNode
if (nullptr == Node) return false;

auto Pins = Node->Pins;
if (Pins.Num() <= 0) return false;

auto Pin = Pins[1]; //UEdGraphPin
if (nullptr == Pin) return false;

FString moduleName;
UnLuaExtensionUtils::GetLuaModuleName(Class->GetName(), Class->GetPathName(), moduleName);

Pin->DefaultValue = moduleName;

return true;
}

Build lighting from commandlet

在命令行构建光照,可以使用ResavePackages这个commandlet:

1
UE4Editor-cmd.exe "E:\UE4Project.uproject" -run=resavepackages -buildlighting -quality=Preview -allowcommandletrendering -map=MapName

AutomationTool的ErrorCode

打包时的错误,可以通过这些错误码的描述来排查原因,该enum定义在Programs/AutomationTool/AutomationUtils/AutomationException.cs

Programs/AutomationTool/AutomationUtils/AutomationException.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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
namespace AutomationTool
{
// NOTE: this needs to be kept in sync with EditorAnalytics.h and iPhonePackager.cs
public enum ExitCode
{
Error_UATNotFound = -1,
Success = 0,
Error_Unknown = 1,
Error_Arguments = 2,
Error_UnknownCommand = 3,
Error_SDKNotFound = 10,
Error_ProvisionNotFound = 11,
Error_CertificateNotFound = 12,
Error_ProvisionAndCertificateNotFound = 13,
Error_InfoPListNotFound = 14,
Error_KeyNotFoundInPList = 15,
Error_ProvisionExpired = 16,
Error_CertificateExpired = 17,
Error_CertificateProvisionMismatch = 18,
Error_CodeUnsupported = 19,
Error_PluginsUnsupported = 20,
Error_UnknownCookFailure = 25,
Error_UnknownDeployFailure = 26,
Error_UnknownBuildFailure = 27,
Error_UnknownPackageFailure = 28,
Error_UnknownLaunchFailure = 29,
Error_StageMissingFile = 30,
Error_FailedToCreateIPA = 31,
Error_FailedToCodeSign = 32,
Error_DeviceBackupFailed = 33,
Error_AppUninstallFailed = 34,
Error_AppInstallFailed = 35,
Error_AppNotFound = 36,
Error_StubNotSignedCorrectly = 37,
Error_IPAMissingInfoPList = 38,
Error_DeleteFile = 39,
Error_DeleteDirectory = 40,
Error_CreateDirectory = 41,
Error_CopyFile = 42,
Error_OnlyOneObbFileSupported = 50,
Error_FailureGettingPackageInfo = 51,
Error_OnlyOneTargetConfigurationSupported = 52,
Error_ObbNotFound = 53,
Error_AndroidBuildToolsPathNotFound = 54,
Error_NoApkSuitableForArchitecture = 55,
Error_FilesInstallFailed = 56,
Error_RemoteCertificatesNotFound = 57,
Error_LauncherFailed = 100,
Error_UATLaunchFailure = 101,
Error_FailedToDeleteStagingDirectory = 102,
Error_MissingExecutable = 103,
Error_DeviceNotSetupForDevelopment = 150,
Error_DeviceOSNewerThanSDK = 151,
Error_TestFailure = 152,
Error_SymbolizedSONotFound = 153,
Error_LicenseNotAccepted = 154,
Error_AndroidOBBError = 155,
};
// ...
}

使用AssetRegistry检测资源是否存在

Editor/UnrealEd/Private/FileHelpers.cpp中提供了一个实现,优先通过AssetRegistry来查找,查找不到则退回到从磁盘查找:

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
namespace FileHelperPackageUtil
{
/**
* DoesPackageExist helper that rely on the AssetRegistry to validate if a package exists instead of hitting the FS
* Fallback to the FS if the asset registry initial scan isn't done or we aren't in Editor
*/
bool DoesPackageExist(UPackage* Package, FString* OutFilename = nullptr)
{
// Test using asset registry to figure out existence
IAssetRegistry& AssetRegistry = FAssetRegistryModule::GetRegistry();
if (!AssetRegistry.IsLoadingAssets() || !GIsEditor)
{
TArray<FAssetData> Data;
FAssetRegistryModule::GetRegistry().GetAssetsByPackageName(Package->GetFName(), Data, true);

if (Data.Num() > 0 && OutFilename)
{
*OutFilename = FPackageName::LongPackageNameToFilename(Package->GetName(), Package->ContainsMap() ? FPackageName::GetMapPackageExtension() : FPackageName::GetAssetPackageExtension());
}

return Data.Num() > 0;
}
return FPackageName::DoesPackageExist(Package->GetName(), nullptr, OutFilename);
}
}

分析打包的资源

工程中有很多的资源其实并没有打到包中去,当需要分析包体中资源大小时,可以通过Asset Audit工具来实现,通过Window-Developer Tools-AssetAudit打开。

可以看到资源路径、大小、位于哪些Chunk中等一系列的信息,便于排查资源大小和Chunk中的资源冗余。

首先,需要说明Editor里的资源大小和最终打到包内的大小是不一样的,在右上角会列出已经打包的平台(Saved/Cooked)下的平台。

Asset Audit需要读取DevelopmentAssetRegistry.bin文件来得到某个平台的资源信息的,它在以下路径中:

1
Client\Saved\Cooked\WindowsNoEditor\FGame\Metadata\DevelopmentAssetRegistry.bin

这个文件记录着某个平台执行完Cook之后资源的大小信息,注意Cook之后的资源如Texture2D等设置的压缩均以执行,但是打包成pak也会执行压缩,这里列出来的大小是没有经过打包pak压缩的Cook资源之后的原始大小。

可以在打包时自动提取Cooked目录下的Metadata目录,在AssetAudit窗口的右上角选择Custom,选择DevelopmentAssetRegistry.bin文件即可。

UE4中ES3.1的75根骨骼限制

之前提到过在ES2.0上使用单个材质蒙皮的骨骼不能超过75根,在ES3之后就没有这个限制了,但是UE里目前还有这个限制。

Warning: SkeletalMesh SK_m0146b0003, is not supported for current feature level (ES3_1) and will not be rendered. NumBones 78 (supported 75), NumBoneInfluences: 4

Runtime/RHI/Public/RHIDefinitions.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
inline int32 GetFeatureLevelMaxNumberOfBones(const FStaticFeatureLevel FeatureLevel)
{
switch (FeatureLevel)
{
case ERHIFeatureLevel::ES3_1:
return 75;
case ERHIFeatureLevel::SM5:
return 65536; // supports uint16
default:
checkf(0, TEXT("Unknown FeatureLevel %d"), (int32)FeatureLevel);
}

return 0;
}

看了下相关的代码,UE在下面这次提交中修改了ES3.1原本256到75,commit里提到的是修复了Mobile Preview的Crash:

Limit ES3.1 to 75 bones like ES2. All ES3.1 feature level platforms use UB for Bones. Project has to set Compat.MAX_GPUSKIN_BONES=75 to support SkelMeshes with more than 75 bones for ES3.1 and ES2.

该代码在4.21 Preview 4中提交:Unreal Engine 4.21 Preview

在answers上也有一些相关的问题:

upluginmanifest

项目打包时会根据项目启用的插件生成一个PROJECT_NAME.upluginmanifest文件,其中记录了每个启用的插件的uplugin的路径和内容信息,该文件也会打包到pak中。

Mount Point为:../../../PROJECT_NAME/Plugins/PROJECT_NAME.upluginmanifest

在Editor下运行时不会读取这个文件,通过扫描引擎和项目以及Mods目录下的Plugin目录来查找插件的,相关的逻辑在Runtime/Projects/Private/PluginManager.cppReadAllPlugins函数中。

1
2
3
4
5
6
#if !WITH_EDITOR
if (Project != nullptr)
{
FindPluginManifestsInDirectory(*FPaths::ProjectPluginsDir(), ManifestFileNames);
}
#endif // !WITH_EDITOR

在非Editor下通过加载upluginmanifest文件来确定当前工程中有哪些插件的(upluginmanifest文件可以有多个,只要放在../../../PROJECT_NAME/Plugins目录下即可),如果一个插件在基础包中不存在,但是热更时新建了一个Content Only插件打包资源,需要把该插件添加至upluginmanifest中并且也需要把该插件的uplugin打包至pak中。

添加非Content路径的Non-Asset目录到基础包

Project Settings-Packaging-Additional Non-Asset Directories to Package可以添加相对路Content下的目录,但是不能够直接选Content之外的目录。

但是,其实这里是可以填相对路径的,如添加[PROJECT_DIR]/Source/Script目录:

1
2
[/Script/UnrealEd.ProjectPackagingSettings]
+DirectoriesToAlwaysStageAsUFS=(Path="../Source/Script")

在打包时能够正确地处理这个相对路径的,Mount Point也正常:

1
"D:\UnrealProjects\Client\Source\Script\UnLua.lua" "../../../FGame/Source/Script/UnLua.lua"

使用这种相对路径可以实现把位于项目Content之外的Non-Asset目录添加到基础包中。

实现分析:
[/Script/UnrealEd.ProjectPackagingSettings]DirectoriesToAlwaysStageAsUFS值是在Programs/AutomationTool/Scripts/CopyBuildToStagingDirectory.Automation.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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
private static void StageAdditionalDirectoriesFromConfig(DeploymentContext SC, DirectoryReference ProjectContentRoot, StagedDirectoryReference StageContentRoot, ConfigHierarchy PlatformGameConfig, bool bUFS, string ConfigKeyName)
{
List<string> ExtraDirs;
if (PlatformGameConfig.GetArray("/Script/UnrealEd.ProjectPackagingSettings", ConfigKeyName, out ExtraDirs))
{
// Each string has the format '(Path="TheDirToStage")'
foreach (var PathStr in ExtraDirs)
{
string RelativePath = null;
var PathParts = PathStr.Split('"');
if (PathParts.Length == 3)
{
RelativePath = PathParts[1];
}
else if (PathParts.Length == 1)
{
RelativePath = PathParts[0];
}
if (RelativePath != null)
{
DirectoryReference InputDir = DirectoryReference.Combine(ProjectContentRoot, RelativePath);
StagedDirectoryReference OutputDir = StagedDirectoryReference.Combine(StageContentRoot, RelativePath);
if (bUFS)
{
List<FileReference> Files = SC.FindFilesToStage(InputDir, StageFilesSearch.AllDirectories);
Files.RemoveAll(x => x.HasExtension(".uasset") || x.HasExtension(".umap") || (SC.DedicatedServer && x.HasExtension(".mp4")));
SC.StageFiles(StagedFileType.UFS, InputDir, Files, OutputDir);
}
else
{
SC.StageFiles(StagedFileType.NonUFS, InputDir, StageFilesSearch.AllDirectories, OutputDir);
}
}
}
}
}

public static void CreateStagingManifest(ProjectParams Params, DeploymentContext SC)
{
// ...
// Stage any additional UFS and NonUFS paths specified in the project ini files; these dirs are relative to the game content directory
if (PlatformGameConfig != null)
{
StageAdditionalDirectoriesFromConfig(SC, ProjectContentRoot, StageContentRoot, PlatformGameConfig, true, "DirectoriesToAlwaysStageAsUFS");
// NonUFS files are never in pak files and should always be remapped
StageAdditionalDirectoriesFromConfig(SC, ProjectContentRoot, StageContentRoot, PlatformGameConfig, false, "DirectoriesToAlwaysStageAsNonUFS");

if (SC.DedicatedServer)
{
StageAdditionalDirectoriesFromConfig(SC, ProjectContentRoot, StageContentRoot, PlatformGameConfig, true, "DirectoriesToAlwaysStageAsUFSServer");
// NonUFS files are never in pak files and should always be remapped
StageAdditionalDirectoriesFromConfig(SC, ProjectContentRoot, StageContentRoot, PlatformGameConfig, false, "DirectoriesToAlwaysStageAsNonUFSServer");
}
}
// ...
}

Directory.Combine里正确地处理了我们所指定的相对于Content的../Source/Script路径。

FDataTime::UtcNow

注意FDataTime::UtcNow在同一帧的不同时机获取的到是不一样的,因为它底层调用的是GetSystemTime(Windows)。

uexp和ubulk的作用

Rather than one large asset, these allow us to write an asset’s bulk data (.ubulk) and exports (uexp) out into separate files. This system improves perf in certain circumstances where file read contiguity is lost due to the large size of assets. This feature avoids this by enabling the reader to skip over an asset’s bulk data when seeking to the next file in a series without having to actually have serialized and seeked past that data (since it’s in separate file).

为了优化性能把资源的信息和数据进行拆分,在进行资源信息的索引时不用访问真正的数据,提高了查找性能和内存消耗。

资源调试命令

可以在Console中使用以下命令:

1
obj list

会列出当前的资源加载信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
                          Class    Count      NumKB      MaxKB   ResExcKB  ResExcDedSysKB  ResExcShrSysKB  ResExcDedVidKB  ResExcShrVidKB     ResExcUnkKB
Class 4331 10240.85 12969.37 0.00 0.00 0.00 0.00 0.00 0.00
FontFace 9 9498.02 9498.02 9495.54 9495.54 0.00 0.00 0.00 0.00
MetaData 732 8441.93 8441.93 0.00 0.00 0.00 0.00 0.00 0.00
ScriptStruct 2165 3923.20 5156.56 0.00 0.00 0.00 0.00 0.00 0.00
SkeletalMesh 1 3988.86 3989.04 1974.06 30.55 0.00 0.00 0.00 1943.52
Function 7928 2012.63 2457.73 0.00 0.00 0.00 0.00 0.00 0.00
Package 732 1316.65 1453.87 0.00 0.00 0.00 0.00 0.00 0.00
Enum 1275 312.38 760.76 0.00 0.00 0.00 0.00 0.00 0.00
DeviceProfile 85 409.36 661.37 0.00 0.00 0.00 0.00 0.00 0.00
Material 103 418.41 467.83 4722.42 4722.42 0.00 0.00 0.00 0.00
ToolMenu 36 193.83 466.16 0.00 0.00 0.00 0.00 0.00 0.00
DelegateFunction 652 165.51 185.93 0.00 0.00 0.00 0.00 0.00 0.00
Texture2D 146 111.05 111.05 56192.00 0.00 0.00 56192.00 0.00 0.00
MaterialExpressionMultiply 194 66.84 98.67 0.00 0.00 0.00 0.00 0.00 0.00
StaticMesh 29 77.02 98.41 2055.31 23.56 0.00 0.00 0.00 2031.75
MaterialExpressionTextureSample 61 44.87 79.66 0.00 0.00 0.00 0.00 0.00 0.00
MaterialExpressionCustom 68 59.53 70.68 0.00 0.00 0.00 0.00 0.00 0.00
MaterialInstanceDynamic 32 53.74 65.76 16.45 16.45 0.00 0.00 0.00 0.00
GameNetworkMgr 1 64.43 64.43 0.00 0.00 0.00 0.00 0.00 0.00
BodySetup 41 42.91 62.30 1158.44 1158.44 0.00 0.00 0.00 0.00
MaterialExpressionConstant 153 36.16 61.26 0.00 0.00 0.00 0.00 0.00 0.00

还有下列相关的命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Mem FromReport
obj list -alphasort
rhi.DumpMemory
LogOutStatLevels
ListSpawnedActors
DumpParticleMem
ConfigMem
r.DumpRenderTargetPoolMemory
ListTextures -alphasort
ListSounds -alphasort
ListParticleSystems -alphasort
obj list class=SoundWave -alphasort
obj list class=SkeletalMesh -alphasort
obj list class=StaticMesh -alphasort
obj list class=Level -alphasort

也可以使用memreport来进行详细的分析:

1
memreport -full

会在Saved/Profiling/MemReports下创建.memreport文件。

IOS基础包拆分

在前面提到了UE为Android提供了打包到obb中的文件过滤规则:

1
2
3
# Config/DefaultEngine.ini
[/Script/AndroidRuntimeSettings.AndroidRuntimeSettings]
+ObbFilters=-pakchunk1-*

但是UE并没有为IOS提供相应的操作,默认情况下会把IOS的所有的pak文件都打包至IPA中。

为了统一Android和IOS的基础包规则,我自己实现了IOS上类似Android那种指定过滤规则的功能,做个简单的介绍。

我使用的是Mac远程打包,流程是在Mac上编译代码生成IPA,拉回Win,在Win上进行Cook,生成Pak文件,最后把原始IPA解包,再添加Pak等文件组合成最终IPA。

我的需求是,自定义指定过滤规则,可以把某些文件忽略,不打包到IPA中。那么这一步的操作其实就位于把IPA解包再打包的流程里,经过翻阅UE的代码,发现这个操作是通过iPhonePackager这个独立程序来实现的,那么就需要对这个程序的代码进行改造了。

经过调试分析,发现真正实现重新打包IPA的操作是在以下函数中执行的:

Programs/IOS/iPhonePackager/CookTime.cs
1
2
3
4
/** 
* Using the stub IPA previously compiled on the Mac, create a new IPA with assets
*/
static public void RepackageIPAFromStub();

该函数位于iPhonePackager-CookTime类中。

1
2
3
4
5
6
7
8
9
10
11
12
static public void RepackageIPAFromStub()
{
// ...
string SourceDir = Path.GetFullPath(ZipSourceDir);
string[] PayloadFiles = Directory.GetFiles(SourceDir, "*.*", Config.bIterate ? SearchOption.TopDirectoryOnly : SearchOption.AllDirectories);
foreach (string Filename in PayloadFiles)
{
// read file to memory,add to ZipFileSystem
// generate stub and ipa
}
//...
}

需要做的操作就是介入这个过程,把PayloadFiles中的文件列表通过我们自定义的规则来执行过滤。

从流程上分为以下几个步骤:

  1. 从项目中读取Filter的配置
  2. 创建出真正的过滤器
  3. RepackageIPAFromStub遍历文件的流程里使用过滤器进行检测是否需要被打入ipa

只需要几十行代码就可以实现,首先需要添加一个IniReader的类:

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
using Tools.DotNETCommon;
using System.Runtime.InteropServices;
using Ini;

namespace Ini
{
public class IniReader
{
private string path;

[DllImport("kernel32")]
private static extern int GetPrivateProfileString(string section, string key, string def,
StringBuilder retVal, int size, string filePath);

public IniReader(string INIPath)
{
path = INIPath;
}

public string ReadValue(string Section, string Key)
{
StringBuilder ReaderBuffer = new StringBuilder(255);
int ret = GetPrivateProfileString(Section, Key, "", ReaderBuffer, 255, this.path);
return ReaderBuffer.ToString();
}
}
}

然后在RepackageIPAFromStub函数中创建过滤器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
FileFilter IpaPakFileFilter = new FileFilter(FileFilterType.Include);
{
string ProjectDir = Directory.GetParent(Path.GetFullPath(Config.ProjectFile)).FullName;
// Program.Log("ProjectDir path {0}", ProjectDir);
string EngineIni = Path.Combine(ProjectDir,"Config","DefaultEngine.ini");
// Program.Log("EngineIni path {0}", EngineIni);
IniReader EngineIniReader = new IniReader(EngineIni);
// string RawPakFilterRules = EngineIniReader.ReadValue("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "IPAFilters");
Program.Log("RawPakFilterRules {0}", RawPakFilterRules);
string[] PakRules = RawPakFilterRules.Split(',');
// foreach(string Rule in PakRules) {Program.Log("PakRules {0}", Rule);}

List<string> PakFilters = new List<string>(PakRules);
if (PakFilters != null)
{
IpaPakFileFilter.AddRules(PakFilters);
}
}

这里从项目的Config/DefaultEngine.ini的[/Script/IOSRuntimeSettings.IOSRuntimeSettings]项读取IPAFilters的值,规则与Android相同,但是要把规则都写在一行,多个规则以逗号分隔。

1
2
[/Script/IOSRuntimeSettings.IOSRuntimeSettings]
IPAFilters=-*.pak,pakchunk0-*

最终,还需要在RepackageIPAFromStub遍历Payload文件的循环中进行检测是否匹配我们指定的过滤规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static public void RepackageIPAFromStub()
{
// ...
string SourceDir = Path.GetFullPath(ZipSourceDir);
string[] PayloadFiles = Directory.GetFiles(SourceDir, "*.*", Config.bIterate ? SearchOption.TopDirectoryOnly : SearchOption.AllDirectories);
foreach (string Filename in PayloadFiles)
{
if (!IpaPakFileFilter.Matches(Filename))
{
Program.Log("IpaPakFileFilter not match file {0}", Filename);
continue;
}
// Program.Log("IpaPakFileFilter match file {0}", Filename);
}
//...
}

这样再执行打包IOS,就会按照指定的过滤规则来添加文件了,实现了与Android上一致的行为。

打包过程中的Log如下(上文代码已注释):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Saving IPA ...
ProjectDir path C:\BuildAgent\workspace\PackageWindows\Client
EngineIni path C:\BuildAgent\workspace\PackageWindows\Client\Config\DefaultEngine.ini
RawPakFilterRules -*.pak,pakchunk0-*
PakRules -*.pak
PakRules pakchunk0-*
IpaPakFileFilter match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\Assets.car
IpaPakFileFilter match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\Info.plist
IpaPakFileFilter match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\LaunchScreenIOS.webp
IpaPakFileFilter match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\Manifest_DebugFiles_IOS.txt
IpaPakFileFilter match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\Manifest_NonUFSFiles_IOS.txt
IpaPakFileFilter match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\mute.caf
IpaPakFileFilter match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\ue4commandline.txt
IpaPakFileFilter match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\cookeddata\fgame\content\movies\logo.mp4
IpaPakFileFilter match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\cookeddata\fgame\content\movies\sparkmore.mp4
IpaPakFileFilter match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\cookeddata\fgame\content\paks\pakchunk0-ios.pak
IpaPakFileFilter not match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\cookeddata\fgame\content\paks\pakchunk1-ios.pak
IpaPakFileFilter not match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\cookeddata\fgame\content\paks\pakchunk2-ios.pak
IpaPakFileFilter match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\Engine\Content\SlateDebug\Fonts\LastResort.ttf
IpaPakFileFilter match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\GCloudVoice.bundle\files\config.json
IpaPakFileFilter match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\GCloudVoice.bundle\files\libwxvoiceembed.bin
IpaPakFileFilter match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\GCloudVoice.bundle\files\mute_detection.aiff
IpaPakFileFilter match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\GRobotResource.bundle\config.json
...

可以看到,过滤规则已经生效了。

Android基础包拆分

在打包时,不想要把所有的资源都打包到apk中,所以可以在打包时进行拆分,只把必要资源打包到apk中,首先需要把基础包中的资源进行pak拆分,可以把通过Project Settings-Asset Manager中进行设置或者通过创建PrimaryAssetLable资源进行标记。

目标是:

  1. 把基础包的打包资源拆分到多个Pak中
  2. 只把必要的pak文件打包到apk里
  3. 其余的pak在运行时进行下载

第一步都可以通过项目设置进行控制,第二部的条件就是要实现一个过滤规则,不过UE已经提供了这个机制,可以指定过滤掉哪些文件,只需要添加配置即可。

1
2
3
4
5
# Config/DefaultEngine.ini
[/Script/AndroidRuntimeSettings.AndroidRuntimeSettings]
+ObbFilters=-pakchunk1-*
+ObbFilters=-pakchunk2-*
+ObbFilters=-pakchunk3-*

ObbFilters的规则以-开头就是排除规则,会把基础包中的chunk1-3的pak给过滤掉,可以用于后续的下载流程。

也可以指定ExcluteInclude规则组合来用:

1
2
+ObbFilters=-*.pak
+ObbFilters=pakchunk0-*

第一步忽略掉所有的pak文件,然后把pakchunk0-*.pak显式添加至obb中。

UMG OnTouchStarted不触发

注意不要使用UButton来作为触发控件来接收OnTouchStarted的事件,使用Board或者Image都可以,但是UButton不行,估计是UButton拦截了事件。

监听资源操作事件

可以通过IAssetRegistry获取到下列事件的delegate并监听:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
DECLARE_DERIVED_EVENT( UAssetRegistryImpl, IAssetRegistry::FAssetAddedEvent, FAssetAddedEvent);
virtual FAssetAddedEvent& OnAssetAdded() override { return AssetAddedEvent; }

DECLARE_DERIVED_EVENT( UAssetRegistryImpl, IAssetRegistry::FAssetRemovedEvent, FAssetRemovedEvent);
virtual FAssetRemovedEvent& OnAssetRemoved() override { return AssetRemovedEvent; }

DECLARE_DERIVED_EVENT( UAssetRegistryImpl, IAssetRegistry::FAssetRenamedEvent, FAssetRenamedEvent);
virtual FAssetRenamedEvent& OnAssetRenamed() override { return AssetRenamedEvent; }

DECLARE_DERIVED_EVENT( UAssetRegistryImpl, IAssetRegistry::FAssetUpdatedEvent, FAssetUpdatedEvent );
virtual FAssetUpdatedEvent& OnAssetUpdated() override { return AssetUpdatedEvent; }

DECLARE_DERIVED_EVENT( UAssetRegistryImpl, IAssetRegistry::FInMemoryAssetCreatedEvent, FInMemoryAssetCreatedEvent );
virtual FInMemoryAssetCreatedEvent& OnInMemoryAssetCreated() override { return InMemoryAssetCreatedEvent; }

DECLARE_DERIVED_EVENT( UAssetRegistryImpl, IAssetRegistry::FInMemoryAssetDeletedEvent, FInMemoryAssetDeletedEvent );
virtual FInMemoryAssetDeletedEvent& OnInMemoryAssetDeleted() override { return InMemoryAssetDeletedEvent; }

DECLARE_DERIVED_EVENT( UAssetRegistryImpl, IAssetRegistry::FFilesLoadedEvent, FFilesLoadedEvent );
virtual FFilesLoadedEvent& OnFilesLoaded() override { return FileLoadedEvent; }

DECLARE_DERIVED_EVENT( UAssetRegistryImpl, IAssetRegistry::FFileLoadProgressUpdatedEvent, FFileLoadProgressUpdatedEvent );
virtual FFileLoadProgressUpdatedEvent& OnFileLoadProgressUpdated() override { return FileLoadProgressUpdatedEvent; }

Android not found uproject

UE中有一个BUG,在4.25.1引擎版本中可以复现,步骤如下:

  1. 安装apk,第一次启动游戏
  2. 打开UE的沙盒数据目录UE4Game/PROJECTNAME,在这个目录下创建Content/Paks目录
  3. 重新启动游戏

Log中也有Project file not found: ../../../FGame/FGame.uproject提示。

在Android上自动挂载的Pak文件可以放到Saved/Paks下,有时间具体分析一下这个问题。

提取chunk的paklist文件

在开启Generate Chunks之后,如果项目中有添加PrimaryAssetLable资源,会生成对应的Chunk文件。
生成的paklist文件所在目录为:

1
2
3
4
5
6
# 源码版
Engine\Programs\AutomationTool\Saved\Logs
# 安装版
C:\Users\lipengzha\AppData\Roaming\Unreal Engine\AutomationTool\Logs\C+Program+Files+Epic+Games+UE_4.26
# 安装版 BuildCookRun
C:\Users\lipengzha\AppData\Roaming\Unreal Engine\AutomationTool\Logs\C+Program+Files+Epic+Games+UE_4.26\BuildCookRun

paklist相关的文件列表如下:

1
2
3
4
5
6
PakList_pakchunk0-WindowsNoEditor.txt
PakList_pakchunk1-WindowsNoEditor.txt
PakList_pakchunk2-WindowsNoEditor.txt
PrePak_WindowsNoEditor_NonUFSFiles.txt
PrePak_WindowsNoEditor_NonUFSFilesDebug.txt
PrePak_WindowsNoEditor_UFSFiles.txt

需要重点关注的是PakList_pakchunk*.txtPrePak*_UFSFiles.txt这几个文件,NonUFSFiles.txt中的文件不会被打包到Pak中。
PakList_pakchunk*.txt中是每一个chunk中包含的文件,并且是以绝对路径 Mount路径的方式来组织的,PrePak*_UFSFiles.txt是当前打包的版本中所有包含的文件,但其中的路径不是绝对路径 Mount路径

在项目设置中添加的NoUFS文件夹都会默认打包到chunk0的pak中。

在Windows上可以使用以下命令来自动拷贝:

1
echo f|xcopy /y/i/s/e "%AppData%\Unreal Engine\AutomationTool\Logs\E+UnrealEngine+Launcher+UE_4.25\PakList_*.txt" "E:\ClientVersion\0.0.1.0"

IOS远程构建最大文件不能超过2G

Win上远程构建出IOS包的流程是代码和bundle都上传到Mac上编译,生成不包含资源的IPA,拉回本地执行资源Cook生成Pak后,把代码的IPA和资源的Pak合并成真正的IPA文件。
但是这样有个问题,UE里的实现是把所有要合并的文件读到内存中再合并打包的:

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
static public void RepackageIPAFromStub()
{
// ...
// Add all of the payload files, replacing existing files in the stub IPA if necessary (should only occur for icons)
{
string SourceDir = Path.GetFullPath(ZipSourceDir);
string[] PayloadFiles = Directory.GetFiles(SourceDir, "*.*", Config.bIterate ? SearchOption.TopDirectoryOnly : SearchOption.AllDirectories);

foreach (string Filename in PayloadFiles)
{
// Get the relative path to the file (this implementation only works because we know the files are all
// deeper than the base dir, since they were generated from a search)
string AbsoluteFilename = Path.GetFullPath(Filename);
string RelativeFilename = AbsoluteFilename.Substring(SourceDir.Length + 1).Replace('\\', '/');

string ZipAbsolutePath = String.Format("Payload/{0}{1}.app/{2}",
Config.GetTargetName(),
Program.Architecture,
RelativeFilename);

byte[] FileContents = File.ReadAllBytes(AbsoluteFilename);
if (FileContents.Length == 0)
{
// Zero-length files added by Ionic cause installation/upgrade to fail on device with error 0xE8000050
// We store a single byte in the files as a workaround for now
FileContents = new byte[1];
FileContents[0] = 0;
}

FileSystem.WriteAllBytes(RelativeFilename, FileContents);

if ((FileContents.Length >= 1024 * 1024) || (Config.bVerbose))
{
FilesBeingModifiedToPrintOut.Add(ZipAbsolutePath);
}
}
}
// ...
}

可以看到是把Payload的每个文件读到byte[]里的,这就有了一个限制,在C#中,数组的长度最大是int32.MaxValue,意味着byte[]不能存储超过2G的文件,不然就会触发异常。
查询了MSDN的文档,发现设置gcAllowVeryLargeObjects也不会改变单个维度的数组的大小。

所以这个问题只能从其他方面入手了,让UE进行资源打包的时候把Pak的文件拆分,让每个文件都小于2GB即可,可以通过UE里的Chunk机制进行拆分。

DS产生corefile

在Shipping的时候DS Crash可以通过启动参数-core来指定可以生成core文件。

指定SkeletalMesh的LOD级别

可以直接对设置ForcedLodModel的值(LOD0需要设置1,实际的LOD级别就是N-1,值为0则是自动):

也可以对USkinnedMeshComponent实例调用SetForcedLOD函数:

Runtime/Engine/Classes/Components/SkinnedMeshComponent.h
1
2
3
4
// Get ForcedLodModel of the mesh component. Note that the actual forced LOD level is the return value minus one and zero means no forced LOD 
int32 USkinnedMeshComponent::GetForcedLOD() const
// Set new ForcedLODModel that forces to set the incoming LOD. Range from [1, Max Number of LOD]. This will affect in the next tick update.
void USkinnedMeshComponent::SetForcedLOD(int32 InNewForcedLOD)

可以用在背包中显示3D模型的场景,避免使用比较低的LOD级别。

HotPatcher的自动化导出Release脚本

组合命令使用HotPatcher的Commandlet,实现Release信息的自动化导出:

HotRelease.py
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
import os 
import sys
import argparse
parser = argparse.ArgumentParser(description="used for build engine or project")
hot_release = parser.add_argument_group('HotRelease')
# project
hot_release.add_argument('--enginebin',help='UE4Editor-cmd.exe binary path')
hot_release.add_argument('--projectdir',help='project root directory')
hot_release.add_argument('--projectname',help='project name,match projectname.uproject')
hot_release.add_argument('--versiondir',help='client version file dir')
hot_release.add_argument('--versionid',help='current release version id')
hot_release.add_argument('--outdir',help='export release result save dir')
platform_list=[
"WindowsNoEditor",
"Android_ASTC",
"IOS",
]
def getPlatformPakListName(PlatformName,ProjectName):
return "PakList_%s-%s.txt" % (ProjectName,PlatformName)
def getProjectFullPath(ProjectDir,ProjectName):
return "%s\\%s.uproject" % (ProjectDir,ProjectName)
def get_platform_paklist(clientversion_path,versionid,project_name):
result_dict = {}
if os.path.exists(clientversion_path):
version_paklist_path = os.path.normpath(os.path.abspath(os.path.join(clientversion_path, versionid)))
print("versionid: %s" % versionid)
for platform in platform_list:
platform_paklist_path = os.path.join(version_paklist_path, getPlatformPakListName(platform,project_name))
if os.path.exists(platform_paklist_path):
result_dict[platform] = platform_paklist_path
print("platform:%s paklist:%s" % (platform,result_dict[platform]))
return result_dict
def ExportRelease(versionid,engine_bin_path,project_dir,project_name,platform_paklist_dict,savepath):
AddPlatformPakListCmd = "-AddPlatformPakList="
for key,value in platform_paklist_dict.items():
AddPlatformPakListCmd = "%s%s+%s," % (AddPlatformPakListCmd,key,value)
print(AddPlatformPakListCmd)
commands_tuple = [
engine_bin_path,
getProjectFullPath(project_dir,project_name),
"-run=HotRelease",
"-versionid=%s" % (versionid),
"-byPakList=true",
AddPlatformPakListCmd,
"-savepath.path=%s" % (savepath),
# "-wait"
]
final_cmd = ""
for param in commands_tuple:
final_cmd = "%s %s" % (final_cmd,param)
print(final_cmd)
os.system(final_cmd)
def GetArgByName(ParserArgs,ArgName):
ArgsPairs = ParserArgs.__dict__
for key,value in ArgsPairs.items():
if key == ArgName:
return value
def printSelectorHelp():
print("Args is invalid!")
def main():
ParserArgs = parser.parse_args()
engine_bin_path = GetArgByName(ParserArgs,"enginebin")
project_dir = GetArgByName(ParserArgs,"projectdir")
project_name = GetArgByName(ParserArgs,"projectname")
version_id = GetArgByName(ParserArgs,"versionid")
clientversion_path = GetArgByName(ParserArgs,"versiondir")
outdir = GetArgByName(ParserArgs,"outdir")
if engine_bin_path and project_dir and project_name and version_id and clientversion_path and outdir:
ExportRelease(
version_id,
engine_bin_path,
project_dir,
project_name,
get_platform_paklist(clientversion_path,version_id,project_name),
outdir
)
else:
printSelectorHelp()
if __name__ == "__main__":
main()

Texture的压缩

之前的笔记中,提到过可以在Project Settings-Cooker-Texture-ASTC Compression vs Size可以设置默认的资源质量和大小的级别:

1
2
3
4
5
0=12x12 
1=10x10
2=8x8
3=6x6
4=4x4

在Texture的资源编辑中也可以针对某个Texture单独设置:

Lowest->Hightest对应着0-4的值,使用Default则使用项目设置中的配置。

并且,设置Compression Settings的类型也会对资源压缩的类型有差别,Default则是项目设置中的参数,如果设置成NormalMap的类型会是ASTC_4x4的。

listtextures

控制台命令listtextures可以列出已被加载过的Texture的信息。

Unreal Insights实时分析Android

上一节提到了使用SessionFrontEnd实时分析Android的方法,在实际的测试当中发现不太稳定,会造成游戏的Crash,UE在新的引擎版本中也提供了新的性能分析工具Unreal Insights,可以更方便和直观地进行Profile。
文档:

同样也需要端口映射,需要把PC的1980端口映射到设备上:

1
adb reverse tcp:1980 tcp:1980 

然后需要给Android设备添加启动命令:

1
../../../FGame/FGame.uproject -Messaging -SessionOwner="lipengzha" -SessionName="Launch On Android Device" -iterative -tracehost=127.0.0.1 -Trace=CPU 

在PC上开启Unreal Insights,在手机上启动游戏,即可实时捕获:

Unreal Insights也可以实时捕获PIE的数据,需要在Editor启动时添加-trace参数:

1
UE4Editor.exe PROJECT_NAME.uproject -trace=counters,cpu,frame,bookmark,gpu

在启动游戏后在Unreal Insights里通过New Connection监听127.0.0.1即可。

SessionFrontEnd实时Profile Android

有几个条件:

  1. 需要USB连接PC和手机
  2. 需要安装adb

首先需要映射端口,因为SessionFrontEnd是通过监听端口的方式来与游戏内通信的,手机和PC并不在同一个网段,所以需要以adb的形式把PC的监听端口转发给手机的端口。

SessionFrontEnd的监听端口可以通过对UE4Editor.exe的端口分析获取:

1
2
3
4
5
6
7
8
9
C:\Users\lipengzha>netstat -ano | findstr "231096"  
TCP 0.0.0.0:1985 0.0.0.0:0 LISTENING 231096
TCP 0.0.0.0:3961 0.0.0.0:0 LISTENING 231096
TCP 0.0.0.0:3963 0.0.0.0:0 LISTENING 231096
TCP 127.0.0.1:4014 127.0.0.1:12639 ESTABLISHED 231096
TCP 127.0.0.1:4199 127.0.0.1:12639 ESTABLISHED 231096
UDP 0.0.0.0:6666 *:* 231096
UDP 0.0.0.0:24024 *:* 231096
UDP 0.0.0.0:58101 *:* 231096

需要把PC的1985端口映射到Android的1985端口,这样手机上APP启动时,连接0.0.0.01985端口就可以连接到PC上的端口。

通过adb命令来执行:

1
adb reverse tcp:1985 tcp:1985 

然后需要给手机上App指定启动参数:

1
../../../FGame/FGame.uproject -Messaging -SessionOwner="lipengzha" -SessionName="Launch On Android Device"  

把这些文本保存为UE4Commandline.txt文件,放到项目的数据目录下即可,具体路径为:

1
/sdcard/UE4Game/PROJECT_NAME/ 

之后直接启动App,在PC上的SessionFrontEnd中就可以看到设备的数据了。

C++中获取引擎的版本信息

可以通过FEngineVersion来获取:

Runtime/Core/Public/Misc/EngineVersion.h
1
2
/** Gets the current engine version */  
static const FEngineVersion& Current();

判断对象是否有效的方式与区别

在UE中的UObject对象实例传递和存储都是以UObject*的指针来存储的,因为指针只是一块内存的地址,而这块内存是否是有效的对象是不清楚的。所以需要有不同检测方式来检测,一般情况下有以下几种状态:

  1. 指针为NULL/nullptr
  2. 指针非NULL,对象被GC标记为PaddingKill
  3. 一块无效的内存地址

如果指针地址是一个无效的内存地址,那么不能通过它来调用任何获取/修改到任何数据成员的函数的。如果对无效的内存地址调用IsPaddingKill的,会Crash,所以要从更底层的角度来检测。

这三种状态可以通过以下几种检测方式来判断:

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
// 检测指针是否为Null,对象是否被注册(UClass是否存在)  
/**
* Checks to see if the object appears to be valid
* @return true if this appears to be a valid object
*/
bool IsValidLowLevel() const;
// 从内存布局上检测对象是否有效,并会检测对象的CDO/UClass是否存在
// 可以用于加载资源的检测
/**
* Faster version of IsValidLowLevel.
* Checks to see if the object appears to be valid by checking pointers and their alignment.
* Name and InternalIndex checks are less accurate than IsValidLowLevel.
* @param bRecursive true if the Class pointer should be checked with IsValidLowLevelFast
* @return true if this appears to be a valid object
*/
bool IsValidLowLevelFast(bool bRecursive = true) const;
// 检测指针是否为null,以及是否被GC清理,如果指针是无效的内存地址会Crash
/**
* Test validity of object
*
* @param Test The object to test
* @return Return true if the object is usable: non-null and not pending kill
*/
FORCEINLINE bool IsValid(const UObject *Test)
{
return Test && !Test->IsPendingKill();
}

M1 Mac的UE4.26兼容性报告

UMG CanvasPanel合批

Project Settings-Engine-Slate Settings中开启Explicit Canvas Child ZOrder可以开启Canvas的合并。

蓝图节点右上角标识的含义

OptimizeCode代码优化

注意:如果关闭代码优化导致构造对象Crash(或者new走不到对象的构造函数),需要检查项目和插件的代码中是否有重名的类,会导致一些奇怪的问题。
在build.cs中可以通过OptimizeCode来控制是否执行代码优化:

1
OptimizeCode = CodeOptimization.InShippingBuildsOnly; 

有以下几个可选值:

1
2
3
4
5
6
7
8
public enum CodeOptimization  
{
Never,
InNonDebugBuilds,
InShippingBuildsOnly,
Always,
Default,
}

当开启优化时,对翻译单元的编译指令参数:

1
2
3
4
/Ox 
/Ot
/GF
/Zo

并且会使用优化版本的Engine/SharedPCH.Engine.h

当关闭优化时的编译指令为:

1
/Od 

会使用非优化版本的Engine/SharedPCH.Engine.NonOptimized.h.

ASTC Compression Quality By Size

在项目的DefaultEngine.ini中的[/Script/UnrealEd.CookerSettings]中通过DefaultASTCQualityBySize设置(0-4):

1
2
3
[/Script/UnrealEd.CookerSettings] 
DefaultASTCQualityBySize=2
DefaultASTCQualityBySpeed=3

0-4分别对应以下压缩级别:

1
2
3
4
5
0=12x12 
1=10x10
2=8x8
3=6x6
4=4x4

引擎修改语言的配置存储

在UE引擎中的的编辑器偏好设置中修改区域和语言,会被存储在以下文件中:

1
C:\Users\lipengzha\AppData\Local\UnrealEngine\4.25\Saved\Config\Windows\EditorSettings.ini  

其值如下:

1
2
3
4
[Internationalization]  
Language=zh-Hans
Culture=
Locale=zh-Hans

如果是英文的,则是en.

中文赋值给FString

首先,把文件编码修改为UTF8,然后使用以下方式:

1
FString str = UTF8_TO_TCHAR("中文");  

注意对UTF8编码的中文不要使用TEXT,因为文件编码UTF8已经是把中文字符串编码成了UTF8的方式,所以可以直接使用""来包裹中文字符,如果此时使用TEXT作为宽字符存储UTF8的编码,在UTF8_TO_TCHAR中会出现错误。

IOS相对到绝对路径转换

在接入的一些库中,需要传递文件的绝对路径,可以通过下面的方式进行转换:

1
IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*InRelatePath);; 

它是定义在IFileManager接口中的一个虚函数,应该在各个平台的PlatformFile中均有自己的实现,但是在Android中依然是相对路径的,不知道UE是不是忘了实现了。

IOS:Runtime/Core/Public/IOS/IOSPlatformFile.h

Android上相对路径转换成绝对路径的方式在之前的笔记中有写。

Android相对路径转绝对路径

有些需求需要把FPaths::ProjectDir()等路径转换为移动设备上的绝对路径,可以参考Core/Private/Android/AndroidPlatformFile.cpp#L126中的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Constructs the base path for any files which are not in OBB/pak data
const FString &GetFileBasePath()
{
static FString BasePath = GFilePathBase + FString(FILEBASE_DIRECTORY) + FApp::GetProjectName() + FString("/");
return BasePath;
}


FString AndroidRelativeToAbsolutePath(bool bUseInternalBasePath, FString RelPath)
{
if (RelPath.StartsWith(TEXT("../"), ESearchCase::CaseSensitive))
{

do {
RelPath.RightChopInline(3, false);
} while (RelPath.StartsWith(TEXT("../"), ESearchCase::CaseSensitive));

return (bUseInternalBasePath ? GInternalFilePath : GetFileBasePath()) / RelPath;
}
return RelPath;
}

强引用UClass

如果在UPROPERTY中通过TSubclassOf引用了一个UClass,会导致该UClass的BP的CDO无法被释放:

1
2
URPOPERTY()
TSubclassOf<class UClassName> StrongClassRef;

可以使用软引用的方式来解决:

1
2
URPOPERTY()
TSoftClassPtr<class UClassName> StrongClassRef;

TSoftClassPtr is a templatized wrapper around FSoftObjectPtr that works like a TSubclassOf, it can be used in UProperties for blueprint subclasses

UE4编译代码的真正命令参数

在之前的文章Build flow of the Unreal Engine4 project中有提到,UE编译模块的时候会执行到ExecuteActions中。
那么UE真正编译每个翻译单元的编译器参数是什么呢?
在UBT调用ExecuteAction之前,会完成所有编译参数的拼接和预处理。

1
static void ExecuteAction(ManagedProcessGroup ProcessGroup, BuildAction Action, List CompletedActions, AutoResetEvent CompletedEvent)

在Win上是通过调用cl-filter.exe来执行的,而如何把代码和编译参数喂给编译器呢?
UE是通过生成了一个.cpp.obj.response来记录当前编译单元的信息的,包含编译器参数和包含目录/输出等等。

该文件在生成位置在模块的Intermediate目录下:

1
Intermediate\Build\Win64\UE4Editor\Development\HotPatcherRuntime\FlibPakReader.cpp.obj.response

文件内容太长,可以下载该文件查看:FlibPakReader.cpp.obj.response

可以使用手动调用cl.exe的方式来执行测试:

1
2
cl.exe @CPP_OBJ_RESPONSE_PATH //showIncludes
// "C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\VC\Tools\MSVC\14.27.29110\bin\HostX64\x64\cl.exe" @"C:\BuildAgent\workspace\PackageWindows\Client\Plugins\UnLua\Intermediate\Build\Win64\UE4\Development\LuaProtobuf\Module.LuaProtobuf.cpp.obj.response" /showIncludes

build.cs添加宏定义的值

在UE中使用build.cs添加宏定义:

1
2
PrivateDefinitions.Add("TEST_MACRO_HAS_VALUE=1");
PublicDefinitions.Add("TEST_MACRO_NOT_VALUE");

可以指定值,也可以不指定宏的值,但是UE生成时会给没有值的宏为1

1
#define TEST_MACRO_NOT_VALUE 1

可以在UE生成的Deginitions.MODULENAME.h中查看,位于以下位置:

1
Intermediate\Build\PLATFIRM_NAME\UE4\Development\MODULE_NAME\Definitions.MODULE_NAME.h

编译引擎的命令

与BuildGraph的方式不同,直接在VS中点UE4编译所使用的命令行:

1
Engine/Build/BatchFiles/Build.bat -Target="UE4Editor Win64 Development" -Target="ShaderCompileWorker Win64 Development -Quiet" -WaitMutex -FromMsBuild

编辑器检测Actor移动

可以通过重写Editor中的PostEditMove函数:

1
virtual void PostEditMove(bool bFinished) override;

Actor Tick in Editor

在Actor的构造函数中开启Tick:

1
PrimaryActorTick.bCanEverTick = true;

但是这个只能设置Runtime的Actor的Tick,当想要让Editor下的Actor也能执行Tick,则需要重写Actor的ShouldTickIfViewportsOnly函数:

1
2
3
4
5
6
7
8
// .h
virtual bool ShouldTickIfViewportsOnly()const override;

// .cpp
bool ARecastDetourTestingActor::ShouldTickIfViewportsOnly() const
{
return true;
}

AActor的类中,默认返回false.

UE4 ERROR: Missing object file

在编译时遇到以下错误:

1
2
3
ERROR: Missing object file C:\BuildAgent\workspace\PackageWindows\InstalledEngine\Engine\Engine\Plugins\Runtime\Database\SQLiteCore\Intermediate\Build\Android\UE4\Developmen
t\SQLiteCore\codec.ca7.o listed in C:\BuildAgent\workspace\PackageWindows\InstalledEngine\Engine\Engine\Plugins\Runtime\Database\SQLiteCore\Intermediate\Build\Android\UE4\Develo
pment\SQLiteCore\SQLiteCore.precompiled

在引擎中添加了代码,并且编译了Android的的平台支持(通过Make Installed Win64),但是在编译IOS时出现这样的报错。
经过排查发现,这个错误时找不到codec.ca7.o文件导致的:

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
// 
public override List<FileItem> Compile(ReadOnlyTargetRules Target, UEToolChain ToolChain, CppCompileEnvironment BinaryCompileEnvironment, FileReference SingleFileToCompile, ISourceFileWorkingSet WorkingSet, IActionGraphBuilder Graph)
{
//UEBuildPlatform BuildPlatform = UEBuildPlatform.GetBuildPlatform(BinaryCompileEnvironment.Platform);

List<FileItem> LinkInputFiles = base.Compile(Target, ToolChain, BinaryCompileEnvironment, SingleFileToCompile, WorkingSet, Graph);

CppCompileEnvironment ModuleCompileEnvironment = CreateModuleCompileEnvironment(Target, BinaryCompileEnvironment);

// If the module is precompiled, read the object files from the manifest
if(Rules.bUsePrecompiled && Target.LinkType == TargetLinkType.Monolithic)
{
if(!FileReference.Exists(PrecompiledManifestLocation))
{
throw new BuildException("Missing precompiled manifest for '{0}'. This module was most likely not flagged for being included in a precompiled build - set 'PrecompileForTargets = PrecompileTargetsType.Any;' in {0}.build.cs to override.", Name);
}

PrecompiledManifest Manifest = PrecompiledManifest.Read(PrecompiledManifestLocation);
foreach(FileReference OutputFile in Manifest.OutputFiles)
{
FileItem ObjectFile = FileItem.GetItemByFileReference(OutputFile);
if(!ObjectFile.Exists)
{
throw new BuildException("Missing object file {0} listed in {1}", OutputFile, PrecompiledManifestLocation);
}
LinkInputFiles.Add(ObjectFile);
}
return LinkInputFiles;
}
// ...
}

去插件的Intermediate中查看了一下,确实没有这个文件,估计是拷贝造成的问题。

分隔String为数组

可以使用FString的ParserIntoArray函数:

1
2
TArray<FString> BreakedPoints;
UFlibAppHelper::GetSourceVersion().ParseIntoArray(BreakedPoints,TEXT("."));

UE4.25中ShaderPatch问题

在4.25引擎版本中调用FShaderCodeLibrary::CreatePatchLibrary来创建ShaderCode Patch会触发check抛异常:

这是因为FEditorShaderCodeArchive的构造函数中调用了ShaderHashTable的Initialize,并给了默认值0x1000

1
2
3
4
5
6
7
8
9
10
FEditorShaderCodeArchive(FName InFormat)
: FormatName(InFormat)
, Format(nullptr)
{
Format = GetTargetPlatformManagerRef().FindShaderFormat(InFormat);
check(Format);

SerializedShaders.ShaderHashTable.Initialize(0x10000);
SerializedShaders.ShaderMapHashTable.Initialize(0x10000);
}

导致在后续的流程中(FSerializedShaderArchive::Serialize)调用Initialize的时候check失败了(因为HaseSize已经有值了,并不是0,对其再调用Initialize就触发了check):

查了下FEditorShaderCodeArchive构造函数中调用Initialize的代码是在4.25之后的引擎版本才有的,所以影响到的之后4.25+的版本。
代码对比:

解决方案:把FSerializedShaderArchive::SerializeShaderMapHashTableInitializeShaderHashTableInitialize在Editor下注释掉,因为FEditorShaderCodeArchive的代码只在Editor下有效,并且是只在生成ShaderPatch时有用。

这就造成了以下几个问题:

  1. FEditorShaderCodeArchive的构造只有Eidotor并且ShaderPatch是才有用,也就意味着这里写的ShaderMapHashTableInitializeShaderHashTableInitialize只有在创建ShaderPatch时才会执行
  2. 在打基础包时执行Cook会编译shader,但是不会执行FEditorShaderCodeArchive的构造,ShaderMapHashTableInitializeShaderHashTableInitialize也就不会执行,就需要在使用的地方来调用它们的初始化

这也是UE中没有管理好这两个状态的地方:在FEditorShaderCodeArchiveFSerializedShaderArchive::Serialize中都做了Initialize的操作,在打基础包时造成了ShaderMapHashTableShaderHashTableInitialize已经被FEditorShaderCodeArchive初始化的情况下又被FSerializedShaderArchive::Serialize执行了一遍,导致Crash,但是我们又不能粗暴地把任何一处的初始化操作去掉,只能通过检测ShaderMapHashTableShaderHashTableInitialize是否已经被执行,来选择性的跳过。

阅读代码可以知道ShaderMapHashTableShaderHashTableInitialize只应该执行一次,并且初始化之后HashSize和IndexSize应该具有非0值:

Runtime/Core/Public/Containers/HashTable.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
FORCEINLINE void FHashTable::Initialize(uint32 InHashSize, uint32 InIndexSize)
{
check(HashSize == 0u);
check(IndexSize == 0u);

HashSize = InHashSize;
IndexSize = InIndexSize;

check(HashSize <= 0x10000);
check(FMath::IsPowerOfTwo(HashSize));

if (IndexSize)
{
HashMask = (uint16)(HashSize - 1);

Hash = new uint32[HashSize];
NextIndex = new uint32[IndexSize];

FMemory::Memset(Hash, 0xff, HashSize * 4);
}
}

Initialize时会检测当前的HashSizeIndexSize是否为0,并在之后进行赋值。所以,我们只要获取FHashTableHashSizeIndexSize检测它们是否为0即可判断当前的HashTable对象是否已经被Initialize过,但是,UE里的FHashTable里这两个成员都是protected的,只能修改引擎来实现了:

添加获取FHashTableHashSizeIndexSize属性的成员函数:

1
2
3
4
5
6
7
8
class FHashTable
{
public:
// ...
FORCEINLINE uint32 GetHashSize()const{return HashSize;};
FORCEINLINE uint32 GetIndexSize()const{return IndexSize;};
// ...
};

然后在FSerializedShaderArchive::Serialize进行检测,如果已被初始化则跳过Initialize逻辑:

Runtime/RenderCore/Private/ShaderCodeArchive.cpp
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
void FSerializedShaderArchive::Serialize(FArchive& Ar)
{
Ar << ShaderMapHashes;
Ar << ShaderHashes;
Ar << ShaderMapEntries;
Ar << ShaderEntries;
Ar << PreloadEntries;
Ar << ShaderIndices;

check(ShaderHashes.Num() == ShaderEntries.Num());
check(ShaderMapHashes.Num() == ShaderMapEntries.Num());

if (Ar.IsLoading())
{
// ++[SHADER_PATCH][lipengzha]
auto ShaderHashInitialized = [](const FHashTable& HashTable)->bool
{
return HashTable.GetHashSize() || HashTable.GetIndexSize();
};
// ++[SHADER_PATCH][lipengzha]
{
const uint32 HashSize = FMath::Min<uint32>(0x10000, 1u << FMath::CeilLogTwo(ShaderMapHashes.Num()));

// ++[SHADER_PATCH][lipengzha]
if(!ShaderHashInitialized(ShaderMapHashTable))
{
ShaderMapHashTable.Initialize(HashSize, ShaderMapHashes.Num());
}
// ++[SHADER_PATCH][lipengzha]

for (int32 Index = 0; Index < ShaderMapHashes.Num(); ++Index)
{
const uint32 Key = GetTypeHash(ShaderMapHashes[Index]);
ShaderMapHashTable.Add(Key, Index);
}
}
{
const uint32 HashSize = FMath::Min<uint32>(0x10000, 1u << FMath::CeilLogTwo(ShaderHashes.Num()));

// ++[SHADER_PATCH][lipengzha]
if(!ShaderHashInitialized(ShaderHashTable))
{
ShaderHashTable.Initialize(HashSize, ShaderHashes.Num());
}
// ++[SHADER_PATCH][lipengzha]

for (int32 Index = 0; Index < ShaderHashes.Num(); ++Index)
{
const uint32 Key = GetTypeHash(ShaderHashes[Index]);
ShaderHashTable.Add(Key, Index);
}
}
}
}

这样可以统一ShaderPatch和Runtime的HashTable的Initialize流程。

而且,需要注意的是:生成出来的ShaderPatch的ushaderbytecode文件是与基础包内的文件名一致的,所以不能使用引擎启动时的默认挂载(会导致基础包内的ushaderbytecode文件无法被加载,从而crash)。

应该在挂载之后自己处理ShaderPatch的ushaderbytecode文件的加载,使用以下函数加载:

1
2
3
4
bool UFlibPatchParserHelper::LoadShaderbytecode(const FString& LibraryName, const FString& LibraryDir)
{
return FShaderCodeLibrary::OpenLibrary(LibraryName, LibraryDir);
}

注意:ShaderPatch的更新不直接支持Patch的迭代,如:1.0 Metadata + 1.1的ShaderPatch,并不能生成1.2的ShaderPatch,必须要基于1.1的完整Metadata才可以,即每次Patch必须要基于上一次完整的Metadate数据(Project和Global的ushaderbytecode文件),在工程管理上每次打包都需要把完整的Metadata收集起来。

Win和Mac出IOS包的区别

UE支持以远程构建的方式来出IOS包,Mac上只编译代码,Cook和编译shader等操作在Win上执行,生成的ios包使用ushaderbytecode,而在Mac上打包IOS则会使用Matellib。

Metallib是IOS上的原生shader格式,远程构建出包的ushaderbytecode是Text Shader,在运行时加载实时编译,效率上是有比较大差距的。
在UE4.26提供了在Win上编译Metal原生Shader的方法,在4.25及之前的引擎只能使用Enable Shader Compile了。

UE4新地图的Package问题

发现UE中的一个问题:

  1. 创建一个新的地图
  2. 获取这个新地图的UPackage,得到的是Engine/Maps/Templates/Template_Default

但是在第二次启动的时候就正常了,怀疑是新建资源的时候没有更新AssetRegistry,有时间具体分析一下原因。

C++加载BP的Enum

在蓝图中新建的枚举资源,在C++中访问:

1
2
3
4
5
6
7
8
9
10
FString UFlibAppHelper::GetEnumNameByValue(TSoftObjectPtr<UUserDefinedEnum> EnumPath, int32 value)
{
FString result;
UUserDefinedEnum* Enumer = LoadObject<UUserDefinedEnum>(nullptr, *EnumPath.ToString());
if (Enumer)
{
result = Enumer->GetDisplayNameTextByValue(value).ToString();
}
return result;
}

效果:

DS设置超时时间

修改DefaultEngine.ini:

1
2
[/Script/OnlineSubsystemUtils.IpNetDriver]
ConnectionTimeout=10.0

DDC的资料

路径过长导致远程构建失败

当引擎的路径过长时,远程构建会出现以下错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  ********** BUILD COMMAND STARTED **********
Running: C:\BuildAgent\workspace\PackageWindows\InstalledEngine\Engine\Engine\Binaries\DotNET\UnrealBuildTool.exe FGame IOS Development -Project=C:\BuildAgent\workspace\PackageW
indows\Client\FGame.uproject C:\BuildAgent\workspace\PackageWindows\Client\FGame.uproject -NoUBTMakefiles -remoteini="C:\BuildAgent\workspace\PackageWindows\Client" -skipdeploy
-Manifest=C:\BuildAgent\workspace\PackageWindows\Client\Intermediate\Build\Manifest.xml -NoHotReload -log="C:\Users\lipengzha\AppData\Roaming\Unreal Engine\AutomationTool\Logs\C+B
uildAgent+workspace+PackageWindows+InstalledEngine+Engine\BuildCookRun\UBT-FGame-IOS-Development.txt"
WARNING: Running from a path with a long directory name ("C:\BuildAgent\workspace\PackageWindows\InstalledEngine\Engine" = 61 characters). Root paths shorter than 50 character
s are recommended to avoid exceeding maximum path lengths on Windows.
[Remote] Using remote server 'xx.xx.xx.xx' on port 2222 (user 'buildmachine')
[Remote] Using private key at C:\BuildAgent\workspace\PackageWindows\Client\Build\NotForLicensees\SSHKeys\xx.xx.xx.xx\buildmachine\RemoteToolChainPrivate.key
ERROR: Unable to determine home directory for remote user. SSH output:
Host key verification failed.
Took 0.8051806s to run UnrealBuildTool.exe, ExitCode=6
UnrealBuildTool failed. See log for more details. (C:\Users\lipengzha\AppData\Roaming\Unreal Engine\AutomationTool\Logs\C+BuildAgent+workspace+PackageWindows+InstalledEngine+Eng
ine\BuildCookRun\UBT-FGame-IOS-Development.txt)
AutomationTool exiting with ExitCode=6 (6)
Took 1.6488741s to run AutomationTool.exe, ExitCode=6
AutomationTool exiting with ExitCode=1 (Error_Unknown)
BUILD FAILED

这里有一个Running from a path with a long directory name的警告,然后跟着一个SSH Key的验证错误。
造成这个错误的原因就是因为警告中的内容,而非SSHKey的问题,因为Win默认的路径长度不能长于260,所以当引擎根目录位于较深的目录中时,可能会导致引擎的路径超过限制,导致后续的失败。

解决方案自然就是两个办法:

  1. 减少引擎的路径深度
  2. 修改系统的最长路径支持

Win10现在支持了长路径支持,开启即可:Win10 开启长路径支持

Mount Point的作用

在Mount Pak的时候,有一个参数可以指定MountPoint:

1
2
3
4
5
6
7
/**
* Mounts a pak file at the specified path.
*
* @param InPakFilename Pak filename.
* @param InPath Path to mount the pak at.
*/
bool Mount(const TCHAR* InPakFilename, uint32 PakOrder, const TCHAR* InPath = NULL, bool bLoadIndex = true);

那么它是干什么的呢?
首先从Mount函数开始:

1
2
3
4
if (InPath != NULL)
{
Pak->SetMountPoint(InPath);
}

如果在调用Mount时传递了InPath,则通过加载Pak的FPakFile实例调用SetMountPoint,把InPath设置给它。
其实在FPakFile中,MountPath是有默认值的(从Pak文件中读取),在FPakFile的构造函数中调用了Initialize(Reader, bLoadIndex);,Initialize中又调用了LoadIndex,在LoadIndex中从Pak中读取Pak的Mount Point的逻辑:

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
// Runtime/PakFile/Private/IPlatformFilePak.cpp
void FPakFile::LoadIndex(FArchive* Reader)
{
if (CachedTotalSize < (Info.IndexOffset + Info.IndexSize))
{
UE_LOG(LogPakFile, Fatal, TEXT("Corrupted index offset in pak file."));
}
else
{
if (Info.Version >= FPakInfo::PakFile_Version_FrozenIndex && Info.bIndexIsFrozen)
{
SCOPED_BOOT_TIMING("PakFile_LoadFrozen");

// read frozen data
Reader->Seek(Info.IndexOffset);
int32 FrozenSize = Info.IndexSize;

// read in the index, etc data in one lump
void* DataMemory = FMemory::Malloc(FrozenSize);
Reader->Serialize(DataMemory, FrozenSize);
Data = TUniquePtr<FPakFileData>((FPakFileData*)DataMemory);

// cache the number of entries
NumEntries = Data->Files.Num();
// @todo loadtime: it is nice to serialize the mountpoint right into the Data so that IndexSize is right here
// but it takes this to copy it out, because it's too painful for the string manipulation when dealing with
// MemoryImageString everywhere MountPoint is used
MountPoint = Data->MountPoint;
}
// ...
}
// ...
}

简单的可以理解为:如果Mount时不传递Mount Point就会从Pak文件中读取,如果有传入就设置为传入的值(Pak文件中的MountPoint是Pak中所有文件的公共路径)。

那么,给Pak设置MountPoint的作用是什么呢?
真实目的是,检测要加载的文件是否存在于当前Pak中!因为Pak的Mount Point的默认含义是当前Pak中所有文件的公共路径,所以只需要检测要读取的文件是否以这个路径开头,就可以首先排除掉基础路径不对的文件(基础路径都不对,意味着这个文件在Pak中也不存在)。

具体逻辑可以看这个函数的实现:

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
// Runtime/PakFile/Public/IPlatformFilePak.h
/**
* Finds a file in the specified pak files.
*
* @param Paks Pak files to find the file in.
* @param Filename File to find in pak files.
* @param OutPakFile Optional pointer to a pak file where the filename was found.
* @return Pointer to pak entry if the file was found, NULL otherwise.
*/
static bool FindFileInPakFiles(TArray<FPakListEntry>& Paks,const TCHAR* Filename,FPakFile** OutPakFile,FPakEntry* OutEntry = nullptr)
{
FString StandardFilename(Filename);
FPaths::MakeStandardFilename(StandardFilename);

int32 DeletedReadOrder = -1;

for (int32 PakIndex = 0; PakIndex < Paks.Num(); PakIndex++)
{
int32 PakReadOrder = Paks[PakIndex].ReadOrder;
if (DeletedReadOrder != -1 && DeletedReadOrder > PakReadOrder)
{
//found a delete record in a higher priority patch level, but now we're at a lower priority set - don't search further back or we'll find the original, old file.
UE_LOG( LogPakFile, Verbose, TEXT("Delete Record: Accepted a delete record for %s"), Filename );
return false;
}

FPakFile::EFindResult FindResult = Paks[PakIndex].PakFile->Find(*StandardFilename, OutEntry);
if (FindResult == FPakFile::EFindResult::Found )
{
if (OutPakFile != NULL)
{
*OutPakFile = Paks[PakIndex].PakFile;
}
UE_CLOG( DeletedReadOrder != -1, LogPakFile, Verbose, TEXT("Delete Record: Ignored delete record for %s - found it in %s instead (asset was moved between chunks)"), Filename, *Paks[PakIndex].PakFile->GetFilename() );
return true;
}
else if (FindResult == FPakFile::EFindResult::FoundDeleted )
{
DeletedReadOrder = PakReadOrder;
UE_LOG( LogPakFile, Verbose, TEXT("Delete Record: Found a delete record for %s in %s"), Filename, *Paks[PakIndex].PakFile->GetFilename() );
}
}

UE_CLOG( DeletedReadOrder != -1, LogPakFile, Warning, TEXT("Delete Record: No lower priority pak files looking for %s. (maybe not downloaded?)"), Filename );
return false;
}

当我们从Pak中读取文件时,通过对游戏中所有Mount的Pak调用Find函数,而FPakFile::Find的函数就实现了上述我说的逻辑:

1
2
3
4
5
6
7
8
9
10
11
// Runtime/PakFile/Private/IPlatformFilePak.cpp
FPakFile::EFindResult FPakFile::Find(const FString& Filename, FPakEntry* OutEntry) const
{
QUICK_SCOPE_CYCLE_COUNTER(PakFileFind);
if (Filename.StartsWith(MountPoint))
{
FString Path(FPaths::GetPath(Filename));
// ...
}
// ...
}

所以,MountPoint的作用就是在从Pak中查找文件时,首先判断文件的路径是否与Pak中所有文件的基础路径相匹配(StartWith),如果不存在也就不会进入后续的流程了。

引擎的Splash过程

通过在这里断点可以比较直观地分析,引擎初始化到什么阶段做了什么事情。

UE4.25.1的Assembly路径错误

在UE4.25.1中,引擎生成的Engine/Intermediate/Build/BuildRules/UE4Rules.dll等文件具有路径错误。

具体的原因是在UnrealBuildTool/System/RulesCompiler.csCreateEngineOrEnterpriseRulesAssembly函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// UE 4.25.1
private static RulesAssembly CreateEngineOrEnterpriseRulesAssembly(RulesScope Scope, List<DirectoryReference> RootDirectories, string AssemblyPrefix, IReadOnlyList<PluginInfo> Plugins, bool bReadOnly, bool bSkipCompile, RulesAssembly Parent)
{
// ...
// Get the path to store any generated assemblies
DirectoryReference AssemblyDir = RootDirectories[0];
if (UnrealBuildTool.IsFileInstalled(FileReference.Combine(AssemblyDir, AssemblyPrefix)))
{
DirectoryReference UserDir = Utils.GetUserSettingDirectory();
if (UserDir != null)
{
ReadOnlyBuildVersion Version = ReadOnlyBuildVersion.Current;
AssemblyDir = DirectoryReference.Combine(UserDir, "UnrealEngine", String.Format("{0}.{1}", Version.MajorVersion, Version.MinorVersion));
}
}

// Create the assembly
FileReference EngineAssemblyFileName = FileReference.Combine(AssemblyDir, "Intermediate", "Build", "BuildRules", AssemblyPrefix + "Rules" + FrameworkAssemblyExtension);
RulesAssembly EngineAssembly = new RulesAssembly(Scope, RootDirectories, Plugins, ModuleFileToContext, new List<FileReference>(), EngineAssemblyFileName, bContainsEngineModules: true, DefaultBuildSettings: BuildSettingsVersion.Latest, bReadOnly: bReadOnly, bSkipCompile: bSkipCompile, Parent: Parent);
//...
}

上面的代码中AssemblyDir是引擎目录,AssemblyPrefixUE4,拼接起来能够通过UnrealBuildTool.IsFileInstalled的检测。
但是,在if的代码块中,获取了用户目录,在IOS中就是:

1
2
3
4
# win
C:\Users\lipengzha\AppData\Local\UnrealEngine\4.25
# Mac
/Users/buildmachine/Library/Application Support/Epic

拼接起来就是上面这两个USER_DIRUnrealEngine/4.25/,在下面读取Assembly的流程中就会使用这个路径。

在使用Win的时候,其实没有问题,因为就算把UE4Rules.dll写入到用户目录下,在Win上同样是可以访问到的。但是,在使用Win远程构建IOS的时候就会出现问题。
在远程构建时,会使用Rsync把引擎的文件同步到Mac上再执行编译,其中就包括Engine/Intermediate/Build/BuildRuls/下的所有文件,因为4.25.1中的代码会把Build/BuildRuls/UE4Rules.dll等生成到Win的用户目录下,所以远程构建,RSync就不能正确地把BuildRuls下的文件上传到Mac上,故而引起打包错误:

1
2
ERROR: Precompiled rules assembly '/Users/buildmachine/Library/Application Support/Epic/UnrealEngine/4.25/Intermediate/Build/BuildRules/UE4Rules.dl
l' does not exist.

可以看到,在Mac上也是从Mac的用户目录查找的,因为压根Mac上就没有这俩文件,所以就会产生这个错误。
解决这个问题的办法,就是修改UnrealBuildTool/System/RulesCompiler.csCreateEngineOrEnterpriseRulesAssembly函数,把BuildRules相关的文件写入到Engine/Intermediate/Build/BuildRules中,在UE4.25.2中已经修复了这个错误。
4.25.2 Hotfix released中列出了Fixed! UE-94140 Fix assembly location for remote toolchain,其实就是直接修改了CreateEngineOrEnterpriseRulesAssembly函数:

1
2
3
4
5
6
7
8
9
// UE 4.25.2
private static RulesAssembly CreateEngineOrEnterpriseRulesAssembly(RulesScope Scope, List<DirectoryReference> RootDirectories, string AssemblyPrefix, IReadOnlyList<PluginInfo> Plugins, bool bReadOnly, bool bSkipCompile, RulesAssembly Parent)
{
// ...
// Create the assembly
FileReference EngineAssemblyFileName = FileReference.Combine(RootDirectories[0], "Intermediate", "Build", "BuildRules", AssemblyPrefix + "Rules" + FrameworkAssemblyExtension);
RulesAssembly EngineAssembly = new RulesAssembly(Scope, RootDirectories[0], Plugins, ModuleFileToContext, new List<FileReference>(), EngineAssemblyFileName, bContainsEngineModules: true, DefaultBuildSettings: BuildSettingsVersion.Latest, bReadOnly: bReadOnly, bSkipCompile: bSkipCompile, Parent: Parent);
// ...
}

可以直接在github中查看:UnrealBuildTool/System/RulesCompiler.cs#L442

跨Level选择Actor

在场景中,美术和游戏逻辑是区分开的,所以有时候需要程序关卡去操控美术关卡的对象,但是UE的不同关卡其实是不同的资源,属于不同的Pacakge,是不能直接跨关卡来选择对象实例的。
选择时会有以下错误:

1
LogProperty: Warning: Illegal TEXT reference to a private object in external package (StaticMeshActor /Game/Test/Map/Level_Sub2.Level_Sub2:PersistentLevel.Cube_2) from referencer (BP_AActor_C /Game/Test/Map/Level_Sub1.Level_Sub1:PersistentLevel.BP_AActor_2).  Import failed...

这是因为在PropertyBaseObject.cppFObjectPropertyBase::ImportText_Internal中对Object属性是否可以跨关卡做了检测:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
UObject* FObjectPropertyBase::FindImportedObject( const FProperty* Property, UObject* OwnerObject, UClass* ObjectClass, UClass* RequiredMetaClass, const TCHAR* Text, uint32 PortFlags/*=0*/, FUObjectSerializeContext* InSerializeContext /*= nullptr*/, bool bAllowAnyPackage /*= true*/)
{
// ...
// if we found an object, and we have a parent, make sure we are in the same package if the found object is private, unless it's a cross level property
if (Result && !Result->HasAnyFlags(RF_Public) && OwnerObject && Result->GetOutermost() != OwnerObject->GetOutermost())
{
const FObjectPropertyBase* ObjectProperty = CastField<const FObjectPropertyBase>(Property);
if ( !ObjectProperty || !ObjectProperty->AllowCrossLevel())
{
UE_LOG(LogProperty, Warning, TEXT("Illegal TEXT reference to a private object in external package (%s) from referencer (%s). Import failed..."), *Result->GetFullName(), *OwnerObject->GetFullName());
Result = nullptr;
}
}
// ...
}

其中AllowCrossLevel有两个继承类有覆写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Runtime/CoreUObject/Private/UObject/PropertyBaseObject.cpp
bool FObjectPropertyBase::AllowCrossLevel() const
{
return false;
}
// Runtime/CoreUObject/Private/UObject/PropertyLazyObjectPtr.cpp
bool FLazyObjectProperty::AllowCrossLevel() const
{
return true;
}
// Runtime/CoreUObject/Private/UObject/PropertySoftObjectPtr.cpp
bool FSoftObjectProperty::AllowCrossLevel() const
{
return true;
}

所以,不能够直接通过创建FObjectPropertyBase这种硬引用方式的属性从SubLevel1选择SubLevel2中的Actor。
那么如何解决这么问题呢?,上面已经列出了两个可以跨平台选择的属性,分别是FLazyObjectPropertyFSoftObjectProperty,那么以FSoftObjectProperty为例,可以通过TSoftObjectPtr来实现:

1
TSoftObjectPtr<AActor> Actor;

TSoftObjectPtr获取到的其实是SubLevel2中的资源的路径:

1
/Game/Test/Map/Level_Sub2.Level_Sub2:PersistentLevel.Cube_2

在运行时访问需要使用以下操作来获取:

上面蓝图中节点Load Asset BlockingUKismetSystemLibrary中的函数:

1
2
3
4
5
// Runtime/Engine/Private/KismetSystemLibrary.cpp
UObject* UKismetSystemLibrary::LoadAsset_Blocking(TSoftObjectPtr<UObject> Asset)
{
return Asset.LoadSynchronous();
}

看来UE加载资源时,并没有区分真正的物理资源和场景中的实例,统一使用资源的路径来加载,这一点做的非常爽,可以把另一个关卡中的Actor当作资源来读取,并且获取的还就是运行时的那个实例,非常Nice。

添加外部库的注意事项

在添加外部的代码库时,需要关注以下几个问题:

  1. 纯代码的库,要测试是否具有平台相关的写法,需要同时支持Win/Android/IOS/Mac四个平台
  2. 对于Android的so要同时支持arm64/armv7,打包时so文件的拷贝需要使用UPL执行
  3. ios的.a要同时具有bitcode和非bitcode版本,不然在shipping时如果开启了bitcode,链接不支持bitcode的库会有链接错误的问题。

检测当前构建是否支持bitcode的流程:

1
2
3
4
5
6
7
8
if (Target.IOSPlatform.bShipForBitcode)
{
// add support bitcode lib
}

{
// add not-support bitcode lib
}

Target.cs中可以直接通过IOSPlatform获取当前构建是否支持bitcode,在其他的Module中,可以通过target.cs中的Target.IOSPlatform获取。

Outer

获取ContentBrowser中选择的资源

1
2
3
4
5
6
7
8
#include "IContentBrowserSingleton.h"
TArray<FAssetData> FHotPatcherEditorModule::GetSelectedAssetsInBrowserContent()
{
FContentBrowserModule& ContentBrowserModule = FModuleManager::Get().LoadModuleChecked<FContentBrowserModule>(TEXT("ContentBrowser"));
TArray<FAssetData> AssetsData;
ContentBrowserModule.Get().GetSelectedAssets(AssetsData);
return AssetsData;
}

命令行太长无法适应调试记录

1
无法执行“C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\VC\Tools\MSVC\14.16.27023\bin\HostX64\x64\c1xx.dll”: 命令行太长,无法适应调试记录

在报错的模块的build.cs中添加下列属性即可:

1
bLegacyPublicIncludePaths = false;

添加资源右键菜单按钮

在ContentBrowser选择资源时的右键菜单按钮。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void FHotPatcherEditorModule::AddAssetContentMenu()
{
if (!UToolMenus::IsToolMenuUIEnabled())
{
return;
}

FToolMenuOwnerScoped MenuOwner("CookUtilities");
UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("ContentBrowser.AssetContextMenu");
FToolMenuSection& Section = Menu->AddSection("AssetContextCookUtilities", LOCTEXT("CookUtilitiesMenuHeading", "CookUtilities"));;

Section.AddDynamicEntry("SoundWaveAsset", FNewToolMenuSectionDelegate::CreateLambda([this](FToolMenuSection& InSection)
{
const TAttribute<FText> Label = LOCTEXT("CookUtilities_CookAsset", "Cook Assets");
const TAttribute<FText> ToolTip = LOCTEXT("CookUtilities_CookAssetsTooltip", "Cook Assets");
const FSlateIcon Icon = FSlateIcon(FEditorStyle::GetStyleSetName(), "ClassIcon.SoundSimple");
const FToolMenuExecuteAction UIAction = FToolMenuExecuteAction::CreateRaw(this,&FHotPatcherEditorModule::OnCookAssets);

InSection.AddMenuEntry("CookUtilities_CookAssets", Label, ToolTip, Icon, UIAction);
}));
}

可以在绑定的函数中对资源进行操作。

GenerateProjectFiles指定VS版本

GenerateProjectFiles.bat最终也是调用到UnrealBuildTool.exe,可以通过-2015/-2017来指定VS2015和VS2017引擎版本。

Module的WhitelistPlatforms

有一些Module用到了平台相关的内容,在另一个平台会编译不过,所以需要在uplugin中给Module添加模块白名单,只有在其中的平台上才会进行编译。

1
2
3
4
5
6
7
8
9
10
11
"Modules": [
{
"Name": "OculusHMD",
"Type": "Runtime",
"LoadingPhase": "PostConfigInit",
"WhitelistPlatforms": [
"Win64",
"Win32",
"Android"
]
},

也可以设置平台的黑名单,使用BlacklistPlatforms

IDetailCustomization

使用IDetailCustomization的方式可以给UE的属性面板添加特殊的东西,比如按钮。

以给F的结构的Detail添加按钮的方法如下:

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
// ReleaseSettingsDetails.h
#pragma once
#include "IDetailCustomization.h"

class FReleaseSettingsDetails : public IDetailCustomization
{
public:
/** Makes a new instance of this detail layout class for a specific detail view requesting it */
static TSharedRef<IDetailCustomization> MakeInstance();

/** IDetailCustomization interface */
virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;
};
````
.cpp:

```cpp
// ReleaseSettingsDetails.cpp
#include "CreatePatch/ReleaseSettingsDetails.h"
#include "CreatePatch/FExportReleaseSettings.h"

// engine header
#include "DetailLayoutBuilder.h"
#include "DetailCategoryBuilder.h"
#include "DetailWidgetRow.h"
#include "Widgets/Input/SButton.h"

#define LOCTEXT_NAMESPACE "ReleaseSettingsDetails"

TSharedRef<IDetailCustomization> FReleaseSettingsDetails::MakeInstance()
{
return MakeShareable(new FReleaseSettingsDetails());
}

void FReleaseSettingsDetails::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
{
TArray< TSharedPtr<FStructOnScope> > StructBeingCustomized;
DetailBuilder.GetStructsBeingCustomized(StructBeingCustomized);
check(StructBeingCustomized.Num() == 1);

FExportReleaseSettings* ReleaseSettingsIns = (FExportReleaseSettings*)StructBeingCustomized[0].Get()->GetStructMemory();

IDetailCategoryBuilder& VersionCategory = DetailBuilder.EditCategory("Version",FText::GetEmpty(),ECategoryPriority::Default);
VersionCategory.SetShowAdvanced(true);

VersionCategory.AddCustomRow(LOCTEXT("ImportPakLists", "Import Pak Lists"),true)
.ValueContent()
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.Padding(0)
.AutoWidth()
[
SNew(SButton)
.Text(LOCTEXT("Import", "Import"))
.ToolTipText(LOCTEXT("ImportPakLists_Tooltip", "Import Pak Lists"))
.IsEnabled_Lambda([this,ReleaseSettingsIns]()->bool
{
return ReleaseSettingsIns->IsByPakList();
})
.OnClicked_Lambda([this, ReleaseSettingsIns]()
{
if (ReleaseSettingsIns)
{
ReleaseSettingsIns->ImportPakLists();
}
return(FReply::Handled());
})
]
+ SHorizontalBox::Slot()
.Padding(5,0,0,0)
.AutoWidth()
[
SNew(SButton)
.Text(LOCTEXT("Clear", "Clear"))
.ToolTipText(LOCTEXT("ClearPakLists_Tooltip", "Clear Pak Lists"))
.IsEnabled_Lambda([this,ReleaseSettingsIns]()->bool
{
return ReleaseSettingsIns->IsByPakList();
})
.OnClicked_Lambda([this, ReleaseSettingsIns]()
{
if (ReleaseSettingsIns)
{
ReleaseSettingsIns->ClearImportedPakList();
}
return(FReply::Handled());
})
]
];
}
#undef LOCTEXT_NAMESPACE

这里我们只是定义了一个IDetailCustomization的类,其中的CustomizeDetails是对FExportReleaseSettings添加的细节。

该类在创建DetailView时使用:

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
void SHotPatcherExportRelease::CreateExportFilterListView()
{
// Create a property view
FPropertyEditorModule& EditModule = FModuleManager::Get().GetModuleChecked<FPropertyEditorModule>("PropertyEditor");

FDetailsViewArgs DetailsViewArgs;
{
DetailsViewArgs.bAllowSearch = true;
DetailsViewArgs.bHideSelectionTip = true;
DetailsViewArgs.bLockable = false;
DetailsViewArgs.bSearchInitialKeyFocus = true;
DetailsViewArgs.bUpdatesFromSelection = false;
DetailsViewArgs.NotifyHook = nullptr;
DetailsViewArgs.bShowOptions = true;
DetailsViewArgs.bShowModifiedPropertiesOption = false;
DetailsViewArgs.bShowScrollBar = false;
DetailsViewArgs.bShowOptions = true;
DetailsViewArgs.bUpdatesFromSelection= true;
}

FStructureDetailsViewArgs StructureViewArgs;
{
StructureViewArgs.bShowObjects = true;
StructureViewArgs.bShowAssets = true;
StructureViewArgs.bShowClasses = true;
StructureViewArgs.bShowInterfaces = true;
}

SettingsView = EditModule.CreateStructureDetailView(DetailsViewArgs, StructureViewArgs, nullptr);
FStructOnScope* Struct = new FStructOnScope(FExportReleaseSettings::StaticStruct(), (uint8*)ExportReleaseSettings.Get());
SettingsView->GetOnFinishedChangingPropertiesDelegate().AddRaw(ExportReleaseSettings.Get(),&FExportReleaseSettings::OnFinishedChangingProperties);
SettingsView->GetDetailsView()->RegisterInstancedCustomPropertyLayout(FExportReleaseSettings::StaticStruct(),FOnGetDetailCustomizationInstance::CreateStatic(&FReleaseSettingsDetails::MakeInstance));
SettingsView->SetStructureData(MakeShareable(Struct));
}

使用RegisterInstancedCustomPropertyLayout把所写的FReleaseSettingsDetails实例注册到DetailView中。注意调用时机要在SetStructureData之前。
然后就可以看到添加的两个按钮了:

提取PakList文件

打包时生成的PakList*.txt文件存放位置为:

1
Engine\Programs\AutomationTool\Saved\Logs

PakList的命名规则为PakList_PROJECTNAME_PLATFORM.txt,如:

1
2
3
PakList_Blank425-WindowsNoEditor.txt
PakList_Blank425-Android_ASTC.txt
PakList_blank425-ios.txt

但是UE在打包下一次时会把上一次生成的PakList*.txt文件给清理掉。所以如果要提取某个平台的PakList需要在打包完当前平台之后立即提取,不然打包下个平台就把之前的删掉了。

绑定DetailsView的属性变化事件

在编辑器中创建DetailsView时,如果使用继承自UObject的对象,可以重载PostEditChangeProperty来实现属性变化的监听,但是如果使用F的结构,则不能直接在类的函数中监听,需要通过绑定IStructureDetailsView的属性变动代理:

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
TSharedPtr<IStructureDetailsView> SettingsView;

// Create a property view
FPropertyEditorModule& EditModule = FModuleManager::Get().GetModuleChecked<FPropertyEditorModule>("PropertyEditor");

FDetailsViewArgs DetailsViewArgs;
{
DetailsViewArgs.bAllowSearch = true;
DetailsViewArgs.bHideSelectionTip = true;
DetailsViewArgs.bLockable = false;
DetailsViewArgs.bSearchInitialKeyFocus = true;
DetailsViewArgs.bUpdatesFromSelection = false;
DetailsViewArgs.NotifyHook = nullptr;
DetailsViewArgs.bShowOptions = true;
DetailsViewArgs.bShowModifiedPropertiesOption = false;
DetailsViewArgs.bShowScrollBar = false;
DetailsViewArgs.bShowOptions = true;
DetailsViewArgs.bUpdatesFromSelection= true;
}

FStructureDetailsViewArgs StructureViewArgs;
{
StructureViewArgs.bShowObjects = true;
StructureViewArgs.bShowAssets = true;
StructureViewArgs.bShowClasses = true;
StructureViewArgs.bShowInterfaces = true;
}

SettingsView = EditModule.CreateStructureDetailView(DetailsViewArgs, StructureViewArgs, nullptr);
FStructOnScope* Struct = new FStructOnScope(FExportReleaseSettings::StaticStruct(), (uint8*)ExportReleaseSettings.Get());
SettingsView->GetOnFinishedChangingPropertiesDelegate().AddRaw(ExportReleaseSettings.Get(),&FExportReleaseSettings::OnFinishedChangingProperties);
SettingsView->SetStructureData(MakeShareable(Struct));

关键是这行代码:

1
SettingsView->GetOnFinishedChangingPropertiesDelegate().AddRaw(ExportReleaseSettings.Get(),&FExportReleaseSettings::OnFinishedChangingProperties);

把当前DetailsView的属性变动事件绑定到OnFinishedChangingProperties上。

从指定Pak加载文件

具体可以看FPakPlatformFile::OpenRead的代码:

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
IFileHandle* FPakPlatformFile::OpenRead(const TCHAR* Filename, bool bAllowWrite)
{
IFileHandle* Result = NULL;
FPakFile* PakFile = NULL;
FPakEntry FileEntry;
if (FindFileInPakFiles(Filename, &PakFile, &FileEntry))
{
#if PAK_TRACKER
TrackPak(Filename, &FileEntry);
#endif

Result = CreatePakFileHandle(Filename, PakFile, &FileEntry);

if (Result)
{
FCoreDelegates::OnFileOpenedForReadFromPakFile.Broadcast(*PakFile->GetFilename(), Filename);
}
}
else
{
if (IsNonPakFilenameAllowed(Filename))
{
// Default to wrapped file
Result = LowerLevel->OpenRead(Filename, bAllowWrite);
}
}
return Result;
}

先通过文件名拿到传入文件在所有Mounted的pak中最大Order的Pak文件,然后使用CreatePakFileHandle从Pak文件中读取文件。
FPakFile描述的是Pak文件,FPakEntry描述的是Pak中文件的信息,比如大小、偏移等。通过FPakFileFPakEntry可以从指定的Pak中读取指定的文件。

WITH_EDITOR包裹反射属性的问题

有时只想要一些属性在编辑器下存在,打包时不需要,按照常规的思路,需要对这些属性使用WITH_EDITOR包裹:

1
2
3
4
#if WITH_EDITOR
UPROPERTY()
int32 ival;
#endif

这个代码在Editor的Configuration下没有问题,但是一旦编译非Editor就会产生如下错误:

1
ERROR: Build/Win64/FGame/Inc/FGame/NetActor.gen.cpp(97): error C2039: 'ival': is not a member of 'ANetActor'

那么,既然我们明明已经用WITH_EDITOR包裹了ival的属性,为什么在编译非Editor的时候UHT还会为这个属性生成反射代码呢?
这个问题涉及到了以下几个概念:

  1. gen.cpp中是UHT为反射标记的类和属性生成的反射信息
  2. UHT的生成流程在调用编译器之前

UE构建系统的流程我之前做过分析:Build flow of the Unreal Engine4 project

因为C++的宏是在调用编译器后预处理阶段做的事情,在执行UHT时,压根不会检测宏条件,所以上面的代码,UHT依然会为ival生成反射信息到gen.cpp中,而UHT执行完毕之后进入编译阶段WITH_EDITOR会参与预处理,ival因此在类定义中不存在,但是UHT已经为它生成了反射代码,会通过获取成员函数指针的方式访问到它,进而产生了上述的编译错误。

所以这是UE反射代码生成先于预处理造成的问题,在写代码时是比较反直觉的。但是这个问题也并非不能解决,UE提供了WITH_EDITORONLY_DATA宏来专门处理这个问题,一个宏解决不了,就引入一个新的。

但是为什么WITH_EDITOR不可以,而WITH_EDITORONLY_DATA就可以呢?因为UHT在生成反射代码时为WITH_EDITORONLY_DATA做了特殊检测:

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
void FNativeClassHeaderGenerator::ExportProperties(FOutputDevice& Out, UStruct* Struct, int32 TextIndent)
{
FProperty* Previous = NULL;
FProperty* PreviousNonEditorOnly = NULL;
FProperty* LastInSuper = NULL;
UStruct* InheritanceSuper = Struct->GetInheritanceSuper();

// Find last property in the lowest base class that has any properties
UStruct* CurrentSuper = InheritanceSuper;
while (LastInSuper == NULL && CurrentSuper)
{
for( TFieldIterator<FProperty> It(CurrentSuper,EFieldIteratorFlags::ExcludeSuper); It; ++It )
{
FProperty* Current = *It;

// Disregard properties with 0 size like functions.
if( It.GetStruct() == CurrentSuper && Current->ElementSize )
{
LastInSuper = Current;
}
}
// go up a layer in the hierarchy
CurrentSuper = CurrentSuper->GetSuperStruct();
}

FMacroBlockEmitter WithEditorOnlyData(Out, TEXT("WITH_EDITORONLY_DATA"));

// Iterate over all properties in this struct.
for( TFieldIterator<FProperty> It(Struct, EFieldIteratorFlags::ExcludeSuper); It; ++It )
{
FProperty* Current = *It;

// Disregard properties with 0 size like functions.
if (It.GetStruct() == Struct)
{
WithEditorOnlyData(Current->IsEditorOnlyProperty());

// Export property specifiers
// Indent code and export CPP text.
{
FUHTStringBuilder JustPropertyDecl;

const FString* Dim = GArrayDimensions.Find(Current);
Current->ExportCppDeclaration( JustPropertyDecl, EExportedDeclaration::Member, Dim ? **Dim : NULL);
ApplyAlternatePropertyExportText(*It, JustPropertyDecl, EExportingState::TypeEraseDelegates);

// Finish up line.
Out.Logf(TEXT("%s%s;\r\n"), FCString::Tab(TextIndent + 1), *JustPropertyDecl);
}

LastInSuper = NULL;
Previous = Current;
if (!Current->IsEditorOnlyProperty())
{
PreviousNonEditorOnly = Current;
}
}
}
}

看下FMacroBlockEmitter的定义:

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
struct FMacroBlockEmitter
{
explicit FMacroBlockEmitter(FOutputDevice& InOutput, const TCHAR* InMacro)
: Output(InOutput)
, bEmittedIf(false)
, Macro(InMacro)
{
}

~FMacroBlockEmitter()
{
if (bEmittedIf)
{
Output.Logf(TEXT("#endif // %s\r\n"), Macro);
}
}

void operator()(bool bInBlock)
{
if (!bEmittedIf && bInBlock)
{
Output.Logf(TEXT("#if %s\r\n"), Macro);
bEmittedIf = true;
}
else if (bEmittedIf && !bInBlock)
{
Output.Logf(TEXT("#endif // %s\r\n"), Macro);
bEmittedIf = false;
}
}

FMacroBlockEmitter(const FMacroBlockEmitter&) = delete;
FMacroBlockEmitter& operator=(const FMacroBlockEmitter&) = delete;

private:
FOutputDevice& Output;
bool bEmittedIf;
const TCHAR* Macro;
};

当生成代码时会为使用WITH_EDITORONLY_DATA包裹的属性在gen.cpp中添加WITH_EDITORONLY_DATA宏(有点套娃的感觉),使gen.cpp在非EDITOR下编译时也不会把这部分反射代码参与真正的编译,从而解决了上面的问题。

Cook资源

可以看CookOnTheFlyServer.cpp中的代码:

Editor/UnrealEd/Private/CookOnTheFlyServer.cpp
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
uint32 UCookOnTheFlyServer::FullLoadAndSave(uint32& CookedPackageCount)
{
// ...
if (bCookPackage)
{
FString PlatFilename = Filename.Replace(TEXT("[Platform]"), *Target->PlatformName());

UE_CLOG(GCookProgressDisplay & (int32)ECookProgressDisplayMode::PackageNames, LogCook, Display, TEXT("Cooking %s -> %s"), *Package->GetName(), *PlatFilename);

bool bSwap = (!Target->IsLittleEndian()) ^ (!PLATFORM_LITTLE_ENDIAN);
if (!Target->HasEditorOnlyData())
{
Package->SetPackageFlags(PKG_FilterEditorOnly);
}
else
{
Package->ClearPackageFlags(PKG_FilterEditorOnly);
}

GIsCookerLoadingPackage = true;
FSavePackageResultStruct SaveResult = GEditor->Save(Package, World, FlagsToCook, *PlatFilename, GError, NULL, bSwap, false, SaveFlags, Target, FDateTime::MinValue(), false);
GIsCookerLoadingPackage = false;

if (SaveResult == ESavePackageResult::Success && UAssetManager::IsValid())
{
if (!UAssetManager::Get().VerifyCanCookPackage(Package->GetFName()))
{
SaveResult = ESavePackageResult::Error;
}
}

const bool bSucceededSavePackage = (SaveResult == ESavePackageResult::Success || SaveResult == ESavePackageResult::GenerateStub || SaveResult == ESavePackageResult::ReplaceCompletely);
if (bSucceededSavePackage)
{
FAssetRegistryGenerator* Generator = PlatformManager->GetPlatformData(Target)->RegistryGenerator.Get();
UpdateAssetRegistryPackageData(Generator, Package->GetFName(), SaveResult);

FPlatformAtomics::InterlockedIncrement(&ParallelSavedPackages);
}

if (SaveResult != ESavePackageResult::ReferencedOnlyByEditorOnlyData)
{
SavePackageSuccessPerPlatform[PlatformIndex] = true;
}
else
{
SavePackageSuccessPerPlatform[PlatformIndex] = false;
}
}

// ...
}

Compile对Instanced的替换调用栈

在这个函数中会收集到当前资源被修改后,依赖它的资源,存储在Dependencies数组中。

得到Dependencies之后,会在FBlueprintCompileReinstancer::UpdateBytecodeReferences中使用:

对资源点击Compile的调用栈

会执行到FKismetEditorUtilities::CompileBlueprint

Editor/UnrealEd/Private/Kismet2/Kismet2.cpp
1
2
3
4
5
6
void FKismetEditorUtilities::CompileBlueprint(UBlueprint* BlueprintObj, EBlueprintCompileOptions CompileFlags, FCompilerResultsLog* pResults)
{
DECLARE_SCOPE_HIERARCHICAL_COUNTER_FUNC()

FBlueprintCompilationManager::CompileSynchronously(FBPCompileRequest(BlueprintObj, CompileFlags, pResults));
}

UMG的子控件引用热更问题

UMG的UserWidget如果UI_A添加了另一个UserWidget UI_B,它们并不是先创建了UI_A,再去创建加载UI_B的资源并创建,UMG的子控件是以Instanced的方式创建的,相当于UI_A中存储的只是当时UI_B的一份示例,并不涉及资源的直接引用。这样会导致在热更时,如果我们只修改了UI_B,此时并没有造成UI_A资源变动,Cook和打包时如果只把UI_B打包,其实对UI_A是没有效果的,并不会有相应的变动。
这是UE Asset和Instanced没有区分的问题,资源并没有修改,但是实际上却对它造成了变化。解决的办法只能在修改了子控件后把使用Instanced方式引用的父控件一起Cook打包,才能有正确的效果。

注意环形引用导致的宏未定义错误

在UE中发现编译报错宏的未定义错误,但是发现头文件已经被包含了,照常是不会出现问题的,如果出现这个问题,检查下代码中是否有头文件的环形引用,解决之后即可。
猜测的原因是环形引用导致预处理爆栈没有包含到真正的宏定义头文件,从而产生了编译错误。

监听Slate的输入事件

UE提供了注册Listener的方法,通过FSlateApplication::Get()进行注册:

1
2
InputProcessor = MakeShared<FTranslationPickerInputProcessor>(this);
FSlateApplication::Get().RegisterInputPreProcessor(InputProcessor, 0);

RegisterInputPreProcessor接收的第一个参数是TSharedPtr<class IInputProcessor>,它是一个接口类型,第二个参数是插入Listener的位置,默认是插入到尾部。

IInputProcessor提供的接口:

Runtime/Slate/Public/Framework/Application/IInputProcessor.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
36
37
38
/**
* Interface for a Slate Input Handler
*/
class SLATE_API IInputProcessor
{
public:
IInputProcessor(){};
virtual ~IInputProcessor(){}

virtual void Tick(const float DeltaTime, FSlateApplication& SlateApp, TSharedRef<ICursor> Cursor) = 0;

/** Key down input */
virtual bool HandleKeyDownEvent(FSlateApplication& SlateApp, const FKeyEvent& InKeyEvent) { return false; }

/** Key up input */
virtual bool HandleKeyUpEvent(FSlateApplication& SlateApp, const FKeyEvent& InKeyEvent) { return false; }

/** Analog axis input */
virtual bool HandleAnalogInputEvent(FSlateApplication& SlateApp, const FAnalogInputEvent& InAnalogInputEvent) { return false; }

/** Mouse movement input */
virtual bool HandleMouseMoveEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) { return false; }

/** Mouse button press */
virtual bool HandleMouseButtonDownEvent( FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) { return false; }

/** Mouse button release */
virtual bool HandleMouseButtonUpEvent( FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) { return false; }

/** Mouse button double clicked. */
virtual bool HandleMouseButtonDoubleClickEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) { return false; }

/** Mouse wheel input */
virtual bool HandleMouseWheelOrGestureEvent(FSlateApplication& SlateApp, const FPointerEvent& InWheelEvent, const FPointerEvent* InGestureEvent) { return false; }

/** Called when a motion-driven device has new input */
virtual bool HandleMotionDetectedEvent(FSlateApplication& SlateApp, const FMotionEvent& MotionEvent) { return false; };
};

我们可以通过继承它来实现自己的监听需求。

修改LaunchScreen视频比例

当使用Project Settings-Project-Movies中来为游戏启动时播放视频时,默认情况下是锁定视频的长宽比的,在全面屏流行的现在,长宽比为2.x的比比皆是,锁定视频比例会导致两侧有黑边,所以希望视频能够拉伸来适应屏幕的大小。

需要修改引擎的代码:

Runtime/MoviePlayer/Private/DefaultGameMoviePlayer.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
FVector2D FDefaultGameMoviePlayer::GetMovieSize() const
{
const FVector2D ScreenSize = MainWindow.Pin()->GetClientSizeInScreen();
// if (MovieStreamingIsPrepared() && ActiveMovieStreamer.IsValid())
// {
// const float MovieAspectRatio = ActiveMovieStreamer->GetAspectRatio();
// const float ScreenAspectRatio = ScreenSize.X / ScreenSize.Y;
// if (MovieAspectRatio < ScreenAspectRatio)
// {
// return FVector2D(ScreenSize.Y * MovieAspectRatio, ScreenSize.Y);
// }
// else
// {
// return FVector2D(ScreenSize.X, ScreenSize.X / MovieAspectRatio);
// }
// }

// No movie, so simply return the size of the window
return ScreenSize;
}

FDefaultGameMoviePlayer::GetMovieSize()修改为上面的代码,其实就是把从视频获取长宽的代码去掉,强制使用窗口的大小。

Rider传递命令行参数

在VS中有UnrealVS可以方便地给工程传递命令行参数,但是Rider中要复杂一点。
要选择Run-Edit Configurations,在弹出窗口的左侧选择要修改的工程,右侧的Program Arguments则是传递给程序的参数:

为了方便编辑,可以把Edit Configurations添加到Toolbar中,在Toolbar上点击右键,点击Customize Menus and Toolbars,在弹出的Menus and Toolbars窗口中,把Edit Configurations添加至Toolbar Run Actions中即可。

IOS自动化导入Certificate和Provision

传统的打包ios时,需要手动在Project Settings-Platforms-IOS中选择打包要使用的CertificateProvision,当需要切换打包Configuration的时候就需要打开编辑器重新选择一遍(因为Development和Shipping用到的证书不同),很麻烦,我是一个非常讨厌做重复操作的人,所以研究了一下解决了这个问题。
也是得益于UE本身提供了代码中导入CertificateProvision的方式(之前我还写了自动化把证书导入到系统中,其实不用了)。
在前面的笔记中提到过UE的Target也提供了平台相关的Target对象:平台相关Target,要实现本文提到的需求就要通过控制IOSPlatform来实现。

IOSPlatform提供了以下几个属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// UnrealBuildTool/Platform/UEBuildIOS.cs

/// <summary>
/// Manual override for the provision to use. Should be a full path.
/// </summary>
[CommandLine("-ImportProvision=")]
public string ImportProvision = null;

/// <summary>
/// Imports the given certificate (inc private key) into a temporary keychain before signing.
/// </summary>
[CommandLine("-ImportCertificate=")]
public string ImportCertificate = null;

/// <summary>
/// Password for the imported certificate
/// </summary>
[CommandLine("-ImportCertificatePassword=")]
public string ImportCertificatePassword = null;

通过操作它们的值来实现自动化导入证书和Provision,我写了一段使用代码:

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
public class FGameTarget : TargetRules
{
public FGameTarget( TargetInfo Target) : base(Target)
{
Type = TargetType.Game;
DefaultBuildSettings = BuildSettingsVersion.V1;
ExtraModuleNames.AddRange( new string[] { "FGame" } );

// for package dSYM
bDisableDebugInfo = true;
if(Target.Platform == UnrealTargetPlatform.IOS)
{
DirectoryReference ProjectDir = ProjectFile.Directory;
IOSPlatform.bGeneratedSYM = true;
string PackageConfiguration = "";

// import cer/provision
switch (Target.Configuration)
{
case UnrealTargetConfiguration.Debug:
case UnrealTargetConfiguration.Development:
case UnrealTargetConfiguration.Test:
{
PackageConfiguration = "Development";
IOSPlatform.bForDistribution = false;
break;
};
case UnrealTargetConfiguration.Shipping:
{
PackageConfiguration = "Distibution";
IOSPlatform.bForDistribution = true;
break;
};
}

string cerPath = Path.Combine(ProjectDir.FullName, "Source/ThirdParty/iOS/",PackageConfiguration,"XXXXXX_IOS.p12");
string proversionPath = Path.Combine(ProjectDir.FullName, "Source/ThirdParty/iOS/",PackageConfiguration,"com.tencent.xxxx.xx_SignProvision.mobileprovision");
string cerPassword = "password";

Console.WriteLine("Import Certificate:"+cerPath);
Console.WriteLine("Import Provision:"+proversionPath);

if (File.Exists(cerPath) && File.Exists(proversionPath))
{
Console.WriteLine("Import Certificate & Provision set to IOSPlatform");
IOSPlatform.ImportCertificate = cerPath;
IOSPlatform.ImportProvision = proversionPath;
IOSPlatform.ImportCertificatePassword = cerPassword;
}
}
}
}

在打包IOS时就会自动使用所指定的证书了,并且会在Shipping时自动化启用Distribution,这样就可以避免要事先把证书和provision导入到系统中。

Mac打包iOS的codesign错误

在Mac上直接打包iOS时遇到以下错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2020-09-27 19:23:48:778 :         /usr/bin/codesign --force --sign 2C74981D1576F95021XXXXXXXXXXA7ECBD8A81A0 --entitlements /Users/buildmachine/Documents/BuildWorkspace/workspace/PackageClient/Client/Intermediate/ProjectFilesIOS/build/FGame.build/Development-iphoneos/FGame.build/FGame.app.xcent --timestamp=none /Users/buildmachine/Documents/BuildWorkspace/workspace/PackageClient/Client/Binaries/IOS/Payload/FGame.app
2020-09-27 19:23:59:896 : /Users/buildmachine/Documents/BuildWorkspace/workspace/PackageClient/Client/Binaries/IOS/Payload/FGame.app: errSecInternalComponent
2020-09-27 19:23:59:896 : Command /usr/bin/codesign failed with exit code 1
2020-09-27 19:23:59:896 :
2020-09-27 19:23:59:899 : ** BUILD FAILED **
2020-09-27 19:23:59:899 :
2020-09-27 19:23:59:899 : The following build commands failed:
2020-09-27 19:23:59:899 : CodeSign /Users/buildmachine/Documents/BuildWorkspace/workspace/PackageClient/Client/Binaries/IOS/Payload/FGame.app
2020-09-27 19:23:59:900 : (1 failure)
2020-09-27 19:23:59:915 : Took 15.181822s to run env, ExitCode=65
2020-09-27 19:23:59:920 : ERROR: CodeSign Failed
2020-09-27 19:23:59:920 : (see /Users/buildmachine/Library/Logs/Unreal Engine/LocalBuildLogs/BuildCookRun/Log.txt for full exception trace)
2020-09-27 19:23:59:922 : AutomationTool exiting with ExitCode=32 (Error_FailedToCodeSign)
2020-09-27 19:23:59:962 : Took 568.832115s to run mono, ExitCode=32
2020-09-27 19:23:59:974 : AutomationTool exiting with ExitCode=1 (Error_Unknown)
2020-09-27 19:24:00:010 : RunUAT ERROR: AutomationTool was unable to run successfully.

可以看到是执行codesign的时候遇到了错误导致打包失败的。

这是因为打包时会访问钥匙串,需要输入密码授权,如果弹窗之后没有授权就会导致codesign执行失败。Stack overflow上有相同的问题:Xcode Command /usr/bin/codesign failed with exit code 1 : errSecInternalComponent

解决方案有三种:

  1. 在打包时的弹窗中输入密码解锁钥匙串
  2. 在打包之前的解锁钥匙串
  3. 在弹窗中输入密码后选择始终允许codesign访问钥匙串

解锁钥匙串使用以下终端命令:

1
$ security unlock-keychain login.keychain

VirtualCamera

可以使用UE4+支持ARKit的设备来实现通过获取iOS设备的设备位置信息来控制游戏中的相机,从而实现类似虚拟制片的相机追踪效果。官方文档:VirtualCameraPlugin

首先,支持ARKit的设备在Apple的网站上有列出:Augmented Reality - Apple,以下设备支持:

ARKit 3.0 is only supported on devices with iOS 13, A12/A12X Bionic chips (or later), the Apple Neural Engine (ANE), and a TrueDepth Camera, such as the iPhone XS, iPhone XS Max, iPhone XR, and the 11-inch and 12.9-inch 2018 iPad Pros.

刚好我的iPad Air3在支持之列。而且目前预览版的UE4.26,支持了ARKit3.5

需要在UE项目中启用三个插件:

操作方法为:打开地图,编辑地图所使用的GameMode为VirtualCameraGameMode,在iPad上打开Unreal Remote 2输入PC的IP地址,连接成功后在UE编辑器内Play,游戏画面就会传递到iPad,iPad的设备位置和旋转就会回传到UE里控制编辑器内的相机。

这是UE默认提供的方案,但是我后面想要iPad可以与Oculus Quest结合起来,其实问题的关键点在于需要获取到ARKit的设备数据,然后通过网络与Oculus Quest通信。
目前的思路是:

  1. 使用UE访问ARKit的设备位置数据在局域网内同步
  2. 获取Oculus的设备位置数据
  3. 想办法统一坐标系

两边都拿到基于地面高度为基准的高度信息是没问题的,但是如何把ARKit设备的XY和Oculus的结合结合起来是个要思考的问题。

远程构建在4.26的问题

之前的不少笔记中都写到了使用远程构建的方式出iOS的ipa(详见UE4开发笔记:Mac/iOS篇#配置远程构建),但是在4.26发现了一个问题,会导致代码的编译和Cook的不一致。
再来复习一遍远程构建的流程:

  1. 把本机的引擎和工程代码上传至Mac
  2. 在Mac上执行编译
  3. 编译完毕之后在Mac上生成ipa包(但不包含Cook资源)
  4. 把生成的ipa包拉回本地,解包,Cook美术资源,再合并为ipa

这其中有个关键的点是:代码的编译和Cook是分别在Mac和Win上执行的,这意味着执行这两个操作的引擎分别是Mac版引擎和Win版引擎。

这个问题就在于,目前4.26的一些代码中加入了PLATFORM_IOS || PLATFORM_MAC的宏判断,如果完整的打包过程都是在Mac上执行的,就不会出现问题,因为代码编译和Cook都是调用Mac版引擎的,但是在远程构建时就会出现问题了。会导致在Mac上编译工程代码时PLATFORM_IOS || PLATFORM_MAC这个检查会通过,而在Win上Cook去编译Shader时,因为使用的是Win版引擎,会导致这个宏检查是false,就会导致Cook和代码之间的版本差异,会有Crash。
具体错误如下:

1
2
3
4
5
Sep 19 16:48:01 lipengzha-iPhone FGame[1895] <Notice>: [UE4] [2020.09.19-08.48.01:821][  0]LogPakFile: New pak file ../../../FGame/Content/Paks/fgame-ios.pak added to pak precacher.
Sep 19 16:48:01 lipengzha-iPhone FGame[1895] <Notice>: [UE4] Assertion failed: Shader->Bindings.StructureLayoutHash == ParameterStructMetadata->GetLayoutHash() [File:/Users/buildmachine/UE4/Builds/lipengzha-PC1/C/BuildAgent/workspace/BuildEngine/Engine/Engine/Source/Runtime/Engine/Private/ShaderCompiler/ShaderCompiler.cpp] [Line: 4308]
Seams shader FPixelProjectedReflectionMobile_ReflectionPassPS's parameter structure has changed without recompilation of the shader
Sep 19 16:48:01 lipengzha-iPhone FGame[1895] <Notice>: [UE4] [2020.09.19-08.48.01:834][ 0]Assertion failed: Shader->Bindings.StructureLayoutHash == ParameterStructMetadata->GetLayoutHash() [File:/Users/buildmachine/UE4/Builds/lipengzha-PC1/C/BuildAgent/workspace/BuildEngine/Engine/Engine/Source/Runtime/Engine/Private/ShaderCompiler/ShaderCompiler.cpp] [Line: 4308]
Seams shader FPixelProjectedReflectionMobile_ReflectionPassPS's parameter structure has changed without recompilation of the shader

导致这个问题的代码:Renderer/Private/PostProcess/PostProcessPixelProjectedReflectionMobile.h#L15,这个宏在Win和Mac两个引擎是不同的值。
不过目前4.26还没有出正式版本,观望正式版会不会修正。

Log

在UE中,经常需要在C++代码中打印日志,UE也提供了方法来创建出可以通过UE_LOG打印日志的宏。
首先,先来看一下UE_LOG是什么,它是个宏,使用的方法:

1
UE_LOG(LogTemp,Log,TEXT(""));

它被定义在Core/Public/Logging/LogMacros.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
/** 
* A macro that outputs a formatted message to log if a given logging category is active at a given verbosity level
* @param CategoryName name of the logging category
* @param Verbosity, verbosity level to test against
* @param Format, format text
***/
#define UE_LOG(CategoryName, Verbosity, Format, ...) \
{ \
static_assert(TIsArrayOrRefOfType<decltype(Format), TCHAR>::Value, "Formatting string must be a TCHAR array."); \
static_assert((ELogVerbosity::Verbosity & ELogVerbosity::VerbosityMask) < ELogVerbosity::NumVerbosity && ELogVerbosity::Verbosity > 0, "Verbosity must be constant and in range."); \
CA_CONSTANT_IF((ELogVerbosity::Verbosity & ELogVerbosity::VerbosityMask) <= ELogVerbosity::COMPILED_IN_MINIMUM_VERBOSITY && (ELogVerbosity::Warning & ELogVerbosity::VerbosityMask) <= FLogCategory##CategoryName::CompileTimeVerbosity) \
{ \
UE_LOG_EXPAND_IS_FATAL(Verbosity, PREPROCESSOR_NOTHING, if (!CategoryName.IsSuppressed(ELogVerbosity::Verbosity))) \
{ \
auto UE_LOG_noinline_lambda = [](const auto& LCategoryName, const auto& LFormat, const auto&... UE_LOG_Args) FORCENOINLINE \
{ \
TRACE_LOG_MESSAGE(LCategoryName, Verbosity, LFormat, UE_LOG_Args...) \
UE_LOG_EXPAND_IS_FATAL(Verbosity, \
{ \
FMsg::Logf_Internal(UE_LOG_SOURCE_FILE(__FILE__), __LINE__, LCategoryName.GetCategoryName(), ELogVerbosity::Verbosity, LFormat, UE_LOG_Args...); \
_DebugBreakAndPromptForRemote(); \
FDebug::ProcessFatalError(); \
}, \
{ \
FMsg::Logf_Internal(nullptr, 0, LCategoryName.GetCategoryName(), ELogVerbosity::Verbosity, LFormat, UE_LOG_Args...); \
} \
) \
}; \
UE_LOG_noinline_lambda(CategoryName, Format, ##__VA_ARGS__); \
UE_LOG_EXPAND_IS_FATAL(Verbosity, CA_ASSUME(false);, PREPROCESSOR_NOTHING) \
} \
} \
}

可以看到,使用UE_LOG时传入的第一个参数,是一个对象,后面的参数则是一个枚举值以及输出的Formater,以及更多的参数(用于匹配Formater中的占位符)。

UE提供了几种方法来创建Log的Category:

1
2
3
DECLARE_LOG_CATEGORY_EXTERN(LogCategoryName,All,All);
DECLARE_LOG_CATEGORY_CLASS(LogCategoryName2,All,All);
DECLARE_LOG_CATEGORY_EXTERN_HELPER(LogCategoryName3,All,All);

挨个来看一下它们的定义,其实他们都是定义了一个类,并需要创建出一个对象,可以用来传递给UE_LOG的第一个参数,而后两个参数则都是ELogVerbosity的枚举值,用于给当前的LogCategory指定运行时和编译时的日志等级。
看一下这个枚举的定义:

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
/** 
* Enum that defines the verbosity levels of the logging system.
* Also defines some non-verbosity levels that are hacks that allow
* breaking on a given log line or setting the color.
**/
namespace ELogVerbosity
{
enum Type : uint8
{
/** Not used */
NoLogging = 0,

/** Always prints a fatal error to console (and log file) and crashes (even if logging is disabled) */
Fatal,

/**
* Prints an error to console (and log file).
* Commandlets and the editor collect and report errors. Error messages result in commandlet failure.
*/
Error,

/**
* Prints a warning to console (and log file).
* Commandlets and the editor collect and report warnings. Warnings can be treated as an error.
*/
Warning,

/** Prints a message to console (and log file) */
Display,

/** Prints a message to a log file (does not print to console) */
Log,

/**
* Prints a verbose message to a log file (if Verbose logging is enabled for the given category,
* usually used for detailed logging)
*/
Verbose,

/**
* Prints a verbose message to a log file (if VeryVerbose logging is enabled,
* usually used for detailed logging that would otherwise spam output)
*/
VeryVerbose,

// Log masks and special Enum values

All = VeryVerbose,
NumVerbosity,
VerbosityMask = 0xf,
SetColor = 0x40, // not actually a verbosity, used to set the color of an output device
BreakOnLog = 0x80
};
}

可以根据自己的需求来指定不同的日志等级。

DECLARE_LOG_CATEGORY_EXTERN

1
2
3
4
5
6
7
8
9
10
11
/** 
* A macro to declare a logging category as a C++ "extern", usually declared in the header and paired with DEFINE_LOG_CATEGORY in the source. Accessible by all files that include the header.
* @param CategoryName, category to declare
* @param DefaultVerbosity, default run time verbosity
* @param CompileTimeVerbosity, maximum verbosity to compile into the code
**/
#define DECLARE_LOG_CATEGORY_EXTERN(CategoryName, DefaultVerbosity, CompileTimeVerbosity) \
extern struct FLogCategory##CategoryName : public FLogCategory<ELogVerbosity::DefaultVerbosity, ELogVerbosity::CompileTimeVerbosity> \
{ \
FORCEINLINE FLogCategory##CategoryName() : FLogCategory(TEXT(#CategoryName)) {} \
} CategoryName;

如果有以下声明:

1
DECLARE_LOG_CATEGORY_EXTERN(LogCategoryName,All,All);

宏展开之后就为:

1
2
3
4
extern struct FLogCategoryLogCategoryName : public FLogCategory<ELogVerbosity::All, ELogVerbosity::All>
{
FORCEINLINE FLogCategoryLogCategoryName() : FLogCategory(TEXT("LogCategoryName")) {}
} LogCategoryName;

其实就是继承自FLogCategory的一个类定义,并且声明了一个LogCategoryName的对象。
注意,这里只是声明,还需要定义,不然在编译时会有未定义错误,所以就需要在cpp里写代码进行定义,UE也提供了一个宏:

1
DEFINE_LOG_CATEGORY(LogCategoryName);

它的定义就很简单了,只是定义一个对象而已:

1
2
3
4
5
/** 
* A macro to define a logging category, usually paired with DECLARE_LOG_CATEGORY_EXTERN from the header.
* @param CategoryName, category to define
**/
#define DEFINE_LOG_CATEGORY(CategoryName) FLogCategory##CategoryName CategoryName;

对象经过定义之后就可以在UE_LOG使用了。
这种分离声明和定义的方式可以用在暴露给外部使用的情况,别的文件或者模块只需要包含具有声明的头文件即可。

DECLARE_LOG_CATEGORY_CLASS

DECLARE_LOG_CATEGORY_CLASS宏的实现就比DECLARE_LOG_CATEGORY_EXTERN多做了一些操作,它定义了一个结构并创建出一个static对象,不需要自己再使用DEFINE_LOG_CATEGRORY进行定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/** 
* A macro to define a logging category as a C++ "static". This should ONLY be declared in a source file. Only accessible in that single file.
* @param CategoryName, category to declare
* @param DefaultVerbosity, default run time verbosity
* @param CompileTimeVerbosity, maximum verbosity to compile into the code
**/
#define DEFINE_LOG_CATEGORY_STATIC(CategoryName, DefaultVerbosity, CompileTimeVerbosity) \
static struct FLogCategory##CategoryName : public FLogCategory<ELogVerbosity::DefaultVerbosity, ELogVerbosity::CompileTimeVerbosity> \
{ \
FORCEINLINE FLogCategory##CategoryName() : FLogCategory(TEXT(#CategoryName)) {} \
} CategoryName;

/**
* A macro to declare a logging category as a C++ "class static"
* @param CategoryName, category to declare
* @param DefaultVerbosity, default run time verbosity
* @param CompileTimeVerbosity, maximum verbosity to compile into the code
**/
#define DECLARE_LOG_CATEGORY_CLASS(CategoryName, DefaultVerbosity, CompileTimeVerbosity) \
DEFINE_LOG_CATEGORY_STATIC(CategoryName, DefaultVerbosity, CompileTimeVerbosity)

它的声明会展开为:

1
2
3
4
static struct FLogCategoryLogCategoryName : public FLogCategory<ELogVerbosity::All, ELogVerbosity::All>
{
FORCEINLINE FLogCategoryLogCategoryName() : FLogCategory(TEXT("LogCategoryName")) {}
} LogCategoryName;

可以看到,和DECLARE_LOG_CATEGORY_EXTERN的区别在于:

  1. 去掉了extern修饰符
  2. 增加了static修饰符,定义对象

使用这个宏的用途一般直接写在.cpp文件中,只供当前的翻译单元使用。

DECLARE_LOG_CATEGORY_EXTERN_HELPER

DECLARE_LOG_CATEGORY_EXTERN_HELPER这个宏只是DECLARE_LOG_CATEGORY_EXTERN的封装,并没有自己做什么特别的事情,和DECLARE_LOG_CATEGORY_EXTERN的用法完全一致。

1
2
3
// Platform specific logs, set here to make it easier to use them from anywhere
// need another layer of macro to help using a define in a define
#define DECLARE_LOG_CATEGORY_EXTERN_HELPER(A,B,C) DECLARE_LOG_CATEGORY_EXTERN(A,B,C)

后记

DECLARE_LOG_CATEGORY_EXTERN也可以通过XXXX_API的方式修饰并导出符号,使其可以在外部模块中使用。

平台相关Target

在项目的Target.cs中定义着项目的TargetRules,但是也不是所有的平台都可以通用全部的参数,每个平台都有自己特定的属性,所以UE的TargetRules定义中中还包含各个平台的Target:

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
/// <summary>
/// Android-specific target settings.
/// </summary>
[ConfigSubObject]
public AndroidTargetRules AndroidPlatform = new AndroidTargetRules();

/// <summary>
/// IOS-specific target settings.
/// </summary>
[ConfigSubObject]
public IOSTargetRules IOSPlatform = new IOSTargetRules();

/// <summary>
/// Lumin-specific target settings.
/// </summary>
[ConfigSubObject]
public LuminTargetRules LuminPlatform = new LuminTargetRules();

/// <summary>
/// Linux-specific target settings.
/// </summary>
[ConfigSubObject]
public LinuxTargetRules LinuxPlatform = new LinuxTargetRules();

/// <summary>
/// Mac-specific target settings.
/// </summary>
[ConfigSubObject]
public MacTargetRules MacPlatform = new MacTargetRules();

/// <summary>
/// PS4-specific target settings.
/// </summary>
[ConfigSubObject]
public PS4TargetRules PS4Platform = new PS4TargetRules();

/// <summary>
/// Switch-specific target settings.
/// </summary>
[ConfigSubObject]
public SwitchTargetRules SwitchPlatform = new SwitchTargetRules();

/// <summary>
/// Windows-specific target settings.
/// </summary>
[ConfigSubObject]
public WindowsTargetRules WindowsPlatform; // Requires 'this' parameter; initialized in constructor

/// <summary>
/// Xbox One-specific target settings.
/// </summary>
[ConfigSubObject]
public XboxOneTargetRules XboxOnePlatform = new XboxOneTargetRules();

/// <summary>
/// HoloLens-specific target settings.
/// </summary>
[ConfigSubObject]
public HoloLensTargetRules HoloLensPlatform;

当需要对某个平台进行特殊控制时,可以在TargetRules中访问特定平台的对象。

如在IOS平台生成dSYM:

1
2
3
4
if(Target.Platform == UnrealTargetPlatform.IOS)
{
IOSPlatform.bGeneratedSYM = true;
}

TargetRule获取项目路径

使用ProjectFile

1
Console.WriteLine("ProjectDir:" + ProjectFile.Directory);

UObject获取资源路径

可以使用FStringAssetReference

1
2
FStringAssetReference ObjectPath(Object);
FString AssetPackagePath = ObjectPath.ToString();

获取的路径格式为:

1
/Game/StarterContent/Materials/M_Basic_Floor.M_Basic_Floor

如果想要获得类似编辑器Copy Reference的信息,则可以使用FAssetData

1
2
FAssetData AssetData(Object);
FString ReferenceInfo = AssetData.GetExportTextName();

获取的格式为:

1
Material'/Game/StarterContent/Materials/M_Basic_Floor.M_Basic_Floor'

也能够根据UObject获取到它的资源类型。

如果用FAssetData获取C++类会得到下面的这样的信息(在运行时动态创建的对象):

1
Actor'/Game/StarterContent/Maps/UEDPIE_0_Minimal_Default.Minimal_Default:PersistentLevel.Actor_0'

打包iOS导出dSYM

像Bugly之类的crash上报平台都需要上传符号表才能看到具体的堆栈信息,而iOS上的符号和调试信息都是在dSYM文件中的。
UE提供了dSYM的生成选项,在Project Settings-Platforms-iOS-Build

  • Generate dSYM file for code debugging and profiling:只开启这个会在Binaries/IOS下生成PROJECT_NAME.dSYM文件
  • Generate dSYM bundle for third party crash tools:依赖上面的选项,如果开启会在Binaries/IOS下生成PROJECT_NAME.dSYM.zip,并且不会再生成PROJECT_NAME.dSYM文件。

但是,在使用源码版打包iOS项目的时候生成的dSYM特别大,超过2G,而同样的工程用Luncher引擎打包就只有100+M,而且bugly之类的上传还有大小限制。

经过对比之后发现,大小的差距主要是在_DWARF_debug_*这些上(左侧为Launcher版,右侧为DebugGame源码版):

本来以为是源码版会把所有参与编译的代码都导出到dSYM文件中,但是经过翻阅引擎代码发现,其实TargetRules中有控制调试信息的选项,就是来控制产生这些_DWARF_debug_*的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// TargetRules.cs

/// <summary>
/// Whether to globally disable debug info generation; see DebugInfoHeuristics.cs for per-config and per-platform options.
/// </summary>
[CommandLine("-NoDebugInfo")]
[XmlConfigFile(Category = "BuildConfiguration")]
public bool bDisableDebugInfo = false;

/// <summary>
/// Whether to disable debug info generation for generated files. This improves link times for modules that have a lot of generated glue code.
/// </summary>
[XmlConfigFile(Category = "BuildConfiguration")]
public bool bDisableDebugInfoForGeneratedCode = false;

/// <summary>
/// Whether to disable debug info on PC in development builds (for faster developer iteration, as link times are extremely fast with debug info disabled).
/// </summary>
[XmlConfigFile(Category = "BuildConfiguration")]
public bool bOmitPCDebugInfoInDevelopment = false;

在项目的Target.cs中控制这些变量即可,如bDisableDebugInfo=true,在源码版引擎中也不会生很大的_DWARF_debug文件了(左侧为Lunch版引擎,右侧为DebugGame源码版控制bDisableDebugInfo=true):

而且,还发现UE在打包IOS平台的时候处理有问题,本来以为打Shipping生成的dSYM会没有调试信息了,但是测试发现还是非常大。

经过分析后发现,在UE的构建系统中是通过两个值来控制是否创建调试信息的:

1
2
3
4
5
6
// UEBuildPlatforms.cs

// Create debug info based on the heuristics specified by the user.
GlobalCompileEnvironment.bCreateDebugInfo =
!Target.bDisableDebugInfo && ShouldCreateDebugInfo(Target);
GlobalLinkEnvironment.bCreateDebugInfo = GlobalCompileEnvironment.bCreateDebugInfo;

可以看到是通过Target.bDisableDebugInfoShouldCreateDebugInfo(Target)两个值来控制的,而ShouldCreateDebugInfo函数则是每个平台的有自己的重写实现。

如在Windows上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// UnrealBuildTool/Platform/Windows/UEBuildWindows.cs

/// <summary>
/// Whether this platform should create debug information or not
/// </summary>
/// <param name="Target">The target being built</param>
/// <returns>bool true if debug info should be generated, false if not</returns>
public override bool ShouldCreateDebugInfo(ReadOnlyTargetRules Target)
{
switch (Target.Configuration)
{
case UnrealTargetConfiguration.Development:
case UnrealTargetConfiguration.Shipping:
case UnrealTargetConfiguration.Test:
return !Target.bOmitPCDebugInfoInDevelopment;
case UnrealTargetConfiguration.DebugGame:
case UnrealTargetConfiguration.Debug:
default:
return true;
};
}

但是,在IOS和Mac上完全没有判断!

1
2
3
4
5
6
// UnrealBuildTool/Platform/IOS/UEBuildIOS.cs

public override bool ShouldCreateDebugInfo(ReadOnlyTargetRules Target)
{
return true;
}

这导致只能自己用bDisableDebugInfo=true来控制,太坑爹了。翻了代码才发现bOmitPCDebugInfoInDevelopment这个选项只在PC上有效:BuildConfigProperties.INT.udn#L85

坑点

根据上面的介绍,可以通过控制bDisableDebugInfo=true来生成体积较小的dSYM,但这样有牵扯出来一个坑的问题:使用远程构建时dSYM无法通过UE的构建系统传回本地。
查了下代码,没有什么比较方便的办法来控制从远程拷贝的文件,目前我使用pscp从远程拉取dSYM文件回本地。

编译引擎支持Android和iOS

把源码版引擎导出为安装版引擎时可以使用BuildGraph(Win+Android+iOS):

1
Engine/Build/BatchFiles/RunUAT.bat BuildGraph -Script=InstalledEngineBuild.xml -Target="Make Installed Build Win64" -set:WithDDC=false -set:WithWin32=false -std:HostPlatformEditorOnly=true -std:HostPlatformOnly=true -set:WithAndroid=true -set:WithIOS=true -set:WithLumin=false -set:WithLuminMac=false -set:WithTVOS=false -set:WithLinux=false -set:WithLinuxAArch64=false -set:WithHoloLens=false -set:EmbedSrcSrvInfo=false -set:WithFullDebugInfo=false -set:GameConfigurations=Development -set:VS2019=true -set:InstalledDir=D:\EngineBin -compile

但是编译iOS需要一台Mac,类似项目的远程构建,引擎构建iOS支持时也需要。
在之前的笔记中写到,项目的远程构建iOS时可以在DefaultEngine.ini中加上远程机器的地址和SSHKey:

1
2
3
4
[/Script/IOSRuntimeSettings.IOSRuntimeSettings]
RemoteServerName=xxx.xx.xx.xxx
RSyncUsername=machinename
SSHPrivateKeyOverridePath=D:\XXXX\RemoteToolChainPrivate.key

在构建引擎时需要把它们写到Engine\Config\BaseEngine.ini中,然后再使用上面的命令构建即可。

UPARAM

可以在UFUNCTION的函数中给指定的参数来指定它的UPARAM,可以用来控制函数的参数属性。之前是用来指定Ref(UPARAM(Ref)),其实也可以使用UPARAM(meta=())来指定meta属性。

如在蓝图实现使用枚举bitmask:

1
2
3
4
5
6
7
8
UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category="Audiokinetic|Actor", meta=(AdvancedDisplay="2", AutoCreateRefTerm = "PostEventCallback,ExternalSources"))
static int32 PostEvent( class UAkAudioEvent* AkEvent,
class AActor* Actor,
UPARAM(meta = (Bitmask, BitmaskEnum = EAkCallbackType)) int32 CallbackMask,
const FOnAkPostEventCallback& PostEventCallback,
const TArray<FAkExternalSourceInfo>& ExternalSources,
bool bStopWhenAttachedToDestroyed = false,
FString EventName = FString(""));

指定返回值的名字什么的都不在话下:

1
static UPARAM(DisplayName = "Bundle") FOSCBundle& AddMessageToBundle(const FOSCMessage& Message, UPARAM(ref) FOSCBundle& Bundle);

可以在UPARAM看到引擎代码中的各种用法。

无边框模式

可以开启Project Settings-Description-Settings-Use Boardless Window:

这个变量在UGameEngine::CreateGameWindow中用到,用来控制引擎启动创建窗口时,窗口的Style.

在不重新打包的情况下可以将下面配置写到Saved/Config/WindowsNoEditor/Game.ini中:

1
2
[/Script/EngineSettings.GeneralProjectSettings]
bUseBorderlessWindow=true

重启游戏就是无边框模式了。

命令行导入iOS证书和Provision

在部署构建机的时候,需要给每台构建机都导入iOS的provisionp12证书,如果每个都要打开一遍编辑器,纯粹是重复劳动,翻了下代码,UE编辑器中导入证书是通过调用IPhonePackager.exe来实现的,而且是以命令行的方式执行,这样就好办了,按照它的参数自己实现脚本即可。
具体看引擎中的相关代码:IOSTargetSettingsCustomization.cpp#L1312

Certificate

导入证书的命令为:

1
Engine\Binaries\DotNET\IOS\IPhonePackager.exe Install Engine -project "D:\Client\FGame.uproject" -certificate "D:\IOS_DEVELOPMENT.p12" -bundlename "com.tencent.xxxx.xx"

证书是导入到系统中的,可以在certmgr.msc中查看导入的证书:

注意:因为iPhonePackager.exe在导入证书时会让弹框输入证书的密码:

这导致不能用在自动化流程里,但是我怎么可能会老老实实每台电脑都输一次密码呢,看了一下iPhonePackager的代码,找到了弹窗让输入密码的地方Programs/IOS/iPhonePackager/ToolsHub.cs#L263,我给它加了个从命令行参数读取密码的选项,如果有该参数就不会弹框:

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
// Load the certificate
string CertificatePassword = "";
string[] arguments = Environment.GetCommandLineArgs();

for (int index = 0;index<arguments.Length;++index)
{
if (arguments[index] == "-cerpassword" && index != (arguments.Length-1))
{
CertificatePassword = arguments[index + 1];
Console.WriteLine("Usage -cerpasseord argument");
}
}

X509Certificate2 Cert = null;
try
{
Cert = new X509Certificate2(CertificateFilename, CertificatePassword, X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet);
}
catch (System.Security.Cryptography.CryptographicException ex)
{
// Try once with a password
if (CertificatePassword.Length > 0 || PasswordDialog.RequestPassword(out CertificatePassword))
{
Cert = new X509Certificate2(CertificateFilename, CertificatePassword, X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet);
}
else
{
// User cancelled dialog, rethrow
throw ex;
}
}

把上面的代码替换掉Programs/IOS/iPhonePackager/ToolsHub.cs#L263这里的部分,重新编译iPhonePackager即可。

Provision

导入Provision的命令为:

1
Engine\Binaries\DotNET\IOS\IPhonePackager.exe Install Engine -project "D:\Client\FGame.uproject" -provision "D:\com.tencent.xxxx.xx_Development_SignProvision.mobileprovision" -bundlename "com.tencent.xxxx.xx"

Provision文件导入后会放在用户目录下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/// <summary>
/// The shared provision library directory (on PC)
/// </summary>
public static string ProvisionDirectory
{
get {
if (Environment.OSVersion.Platform == PlatformID.MacOSX || Environment.OSVersion.Platform == PlatformID.Unix) {
return Environment.GetEnvironmentVariable("HOME") + "/Library/MobileDevice/Provisioning Profiles/";
} else {
return Environment.GetFolderPath (Environment.SpecialFolder.LocalApplicationData) + "/Apple Computer/MobileDevice/Provisioning Profiles/";
}
}
}

Win上就为:C:\Users\USER_NAME\AppData\Local\Apple Computer\MobileDevice\Provisioning Profiles.

C#获取命令行参数

需要using system;.

1
2
3
4
5
6
string[] arguments = Environment.GetCommandLineArgs();

for (int index = 0;index<arguments.Length;++index)
{

}

打包时给不同的平台添加资源

有时我们需要在打包时给不同的平台添加不同目录下的外部资源(如WWise),UE提供了Project Settings-Packaging-Additional Non-Asset Directories to Package,找了一下,并没有发现单独给某个平台指定添加路径的地方。
但是Additional Non-Asset Directories to Package本身是支持给不同平台添加不同目录的,只要在Additional Non-Asset Directories to Package添加的目录下根据不同的平台创建不同的目录。
如:

1
2
3
4
5
6
7
D:\EmptyProject\Content\WwiseAudio>tree /a
+---Android
| \---English(US)
+---iOS
| \---English(US)
\---Windows
\---English(US)

在项目的Content\WwiseAudio下,有Android/iOS/Windows等目录,在Additional Non-Asset Directories to Package中添加的是WwiseAudio目录,打包时只会把对应平台的目录给打包进去,但是没有看到UE的文档里哪有写。

监听资源保存的事件

1
2
3
4
5
6
7
8
void PackageSaved(const FString& PacStr,UObject* PackageSaved)
{
UE_LOG(LogTemp,Log,TEXT("Package %s Saved."),*PacStr);
}
void FEmptyProjectModule::StartupModule()
{
UPackage::PackageSavedEvent.AddStatic(&PackageSaved);
}

UStruct的json序列化

因为UStruct在UE内是具有反射的,所以不用自己去解析编码就可以实现序列化和反序列化,UE中提供了一个辅助模块:JsonUtilities,里面具有FJsonObjectConverter类,定义了一系列的操作。
我简单封装了一下,对Ustrut的序列化和反序列化:

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
template<typename TStructType>
static bool TSerializeStructAsJsonObject(const TStructType& InStruct,TSharedPtr<FJsonObject>& OutJsonObject)
{
if(!OutJsonObject.IsValid())
{
OutJsonObject = MakeShareable(new FJsonObject);
}
bool bStatus = FJsonObjectConverter::UStructToJsonObject(TStructType::StaticStruct(),&InStruct,OutJsonObject.ToSharedRef(),0,0);
return bStatus;
}

template<typename TStructType>
static bool TDeserializeJsonObjectAsStruct(const TSharedPtr<FJsonObject>& OutJsonObject,TStructType& InStruct)
{
bool bStatus = false;
if(OutJsonObject.IsValid())
{
bStatus = FJsonObjectConverter::JsonObjectToUStruct(OutJsonObject.ToSharedRef(),TStructType::StaticStruct(),&InStruct,0,0);
}
return bStatus;
}

template<typename TStructType>
static bool TSerializeStructAsJsonString(const TStructType& InStruct,FString& OutJsonString)
{
bool bRunStatus = false;

{
TSharedPtr<FJsonObject> JsonObject;
if (TSerializeStructAsJsonObject<TStructType>(InStruct,JsonObject) && JsonObject.IsValid())
{
auto JsonWriter = TJsonWriterFactory<>::Create(&OutJsonString);
FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter);
bRunStatus = true;
}
}
return bRunStatus;
}

template<typename TStructType>
static bool TDeserializeJsonStringAsStruct(const FString& InJsonString,TStructType& OutStruct)
{
bool bRunStatus = false;
TSharedRef<TJsonReader<TCHAR>> JsonReader = TJsonReaderFactory<TCHAR>::Create(InJsonString);
TSharedPtr<FJsonObject> DeserializeJsonObject;
if (FJsonSerializer::Deserialize(JsonReader, DeserializeJsonObject))
{
bRunStatus = TDeserializeJsonObjectAsStruct<TStructType>(DeserializeJsonObject,OutStruct);
}
return bRunStatus;
}

Redirector

Redirector是标记被移动资源的引用关系的,在移动具有引用的资源时会产生。
/Game/GameMap引用到了一个UI资源/Game/UMG_Main,当移动/Game/UMG_Main/Game/TEST/UMG_Main时,会在/Game/UMG_Main的磁盘路径下创建出一个Redirector,用与告诉引用到该UI的资源,它的真实路径已经发生变化了,所以叫Redirector。
在项目中需要尽量避免Redirector的存在。

检查引擎是否运行在Commandlet

UE中有检测的方法,定义在CoreGlobals.hIsRunningCommandlet

1
2
3
4
5
6
7
8
9
10
11
/**
* Check to see if this executable is running a commandlet (custom command-line processing code in an editor-like environment)
*/
FORCEINLINE bool IsRunningCommandlet()
{
#if WITH_ENGINE
return PRIVATE_GIsRunningCommandlet;
#else
return false;
#endif
}

FSoftObjectPath限定类型

默认FSoftObjectPath可以指定任何继承自UObject的资源类型,但有时候只想要指定某些类型,UE提供了限定类型的功能,要使用UPROPERTY:

1
2
UPROPERTY(config, EditAnywhere, Category=DefaultMaps, meta=(AllowedClasses="World"))
FSoftObjectPath EditorStartupMap;

meta中使用AllowedClasses即可,AllowedClassess的值是实际类型去掉前缀U,如USoundClass要使用SoundClass

AllowedClasses可以指定多个,使用逗号分隔,使当前FSoftObjectPath可以指定多个限定类型的资源。而且还可以使用ExactClass来控制是否严格限定类型(是否允许继承层次中类型中的资源,如UDataTableUCompositeDataTable),如果不指定,默认是严格限定类型的,如果ExactClass=true则可以使用继承层次中类型的资源。

编辑器viewport显示文字

UKismetSystemLibrary::PrintString是运行时可以输出到viewport,在编辑器下无效果,想要实现编辑器下的文本输出可以通过FCanvasDrawItem来实现:

1
2
3
FCanvasTextItem TextItem(FVector2D(100, 200), LOCTEXT("OutOfTextureMemory", "RAN OUT OF TEXTURE MEMORY, EXPECT CORRUPTION AND GPU HANGS!"), GEngine->GetMediumFont(), FLinearColor::Red);
TextItem.EnableShadow(FLinearColor::Black);
Canvas->DrawItem(TextItem);

stat fps等,都是通过FCanvas来实现的,如DrawMapWarnings:

以及RenderStatFPS:

FCanvas可以通过FViewport::GetDebugCanvas()来获得。

开启多核心编译

UBT会从下面三个文件中读取配置:

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

我们只需要修改其中的任意一个就可以。

默认具有以下内容:

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

更多的BuildConfigutation的参数可以看引擎文档Configuring Unreal Build System。在文档中ProcessorCountMultiplier元素的数字就是允许使用的处理器数,但是,UE的文档和实际的代码有出入,按照上面的文档,设置ProcessorCountMultiplier是在BuildConfigutation下的,但是这么写会与错误:

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

错误信息:

1
BuildConfiguration.xml(4): [] 元素 命名空间“https://www.unrealengine.com/BuildConfiguration”中的“BuildConfiguration”。 的子元素 命名空间“https://www.unrealengine.com/BuildConfiguration”中的“ProcessorCountMultiplier”。 无效。应为可能元素的列表: 命名空间“https://www.unrealengine.com/BuildConfiguration”中的“bPGOProfile, bAllowHybridExecutor, DMUCSDistProp, bAllowXGE, bGeneratedSYMFile, bAllowSNDBS, bIgnoreOutdatedImportLibraries, bUseFastSemanticsRenderContexts, bDisableDebugInfo, bParseTimingInfoForTracing, CppStandard, bPrintDebugInfo, bUseSharedPCHs, bDisableDebugInfoForGeneratedCode, bForcePrecompiledHeaderForGameModules, bUseShippingPhysXLibraries, bUseAdaptiveUnityBuild, bXGENoWatchdogThread, MinGameModuleSourceFilesForUnityBuild, bAdaptiveUnityDisablesOptimizations, bCheckLicenseViolations, bAllowParallelExecutor, bOmitFramePointers, bUseCheckedPhysXLibraries, bSupportEditAndContinue, bAllowASLRInShipping, bStripSymbols, bAllowDistcc, bVerboseDistccOutput, bUseInlining, bAllowDistccLocalFallback, bAdaptiveUnityEnablesEditAndContinue, bEnableMemorySanitizer, bCheckSystemHeadersForModification, bUseFastPDBLinking, bAdaptiveUnityDisablesProjectPCHForProjectPrivate, BaseLogFileName, bUsePerFileIntellisense, bCreateMapFile, bUsePCHFiles, DMUCSCoordinat...。

查阅代码之后发现,这些配置选项要去看BuildConfigProperties.INT.udn里的参数,ProcessorCountMultiplier 是在ParallelExecutor下的,所以改成以下配置:

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

目前我使用的完整配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="utf-8" ?>
<Configuration xmlns="https://www.unrealengine.com/BuildConfiguration">
<BuildConfiguration>
<ProcessorCountMultiplier>7</ProcessorCountMultiplier>
<MaxParallelActions>7</MaxParallelActions>
<bAllowParallelExecutor>true</bAllowParallelExecutor>
</BuildConfiguration>
<SNDBS>
<ProcessorCountMultiplier>4</ProcessorCountMultiplier>
<MaxProcessorCount>4</MaxProcessorCount>
</SNDBS>
<ParallelExecutor>
<ProcessorCountMultiplier>7</ProcessorCountMultiplier>
<MaxProcessorCount>7</MaxProcessorCount>
<bStopCompilationAfterErrors>true</bStopCompilationAfterErrors>
</ParallelExecutor>
</Configuration>

相关连接:

URL Encode/Decode

UE提供了相关的API:

1
2
static FString FGenericPlatformHttp::UrlDecode(const FString & EncodedString)
static FString FGenericPlatformHttp::UrlEncode(const FString & UnencodedString)

执行结果:

查看APK的签名信息

可以使用keytool.exe(在JDK的bin中),使用以下参数:

1
keytool.exe" -list -printcert -jarfile FGame-armv7.apk

会打印出apk的签名色所有者和发布者,以及证书的有效时间、证书指纹等等。

ASTC设置压缩率

Project Settings-Engine-Cooker中有选项:

打包Windows以窗口模式启动

在项目的Config下新建DefaultGameUserSettings.ini文件,填入以下内容:

1
2
3
4
5
6
7
8
9
10
11
[/Script/Engine.GameUserSettings]
bUseVSync=False
ResolutionSizeX=1920
ResolutionSizeY=1080
LastUserConfirmedResolutionSizeX=1920
LastUserConfirmedResolutionSizeY=1080
WindowPosX=-1
WindowPosY=-1
FullscreenMode=2
LastConfirmedFullscreenMode=2
Version=5

打包之后就会以窗口模式启动了,分辨率可以自己修改。

指定地图Cook及打包

DefaultEditor.ini中添加以下项:

1
2
3
4
5
6
7
[AllMaps]
+Map=/Game/Assets/Scene/Map/Fb/v3/8r/xzzn/Fb_ThePoolOfTribute
+Map=/Game/Assets/Scene/Map/LightSpeed/LightSpeed

[AlwaysCookMaps]
+Map=/Game/Assets/Scene/Map/Fb/v3/8r/xzzn/Fb_ThePoolOfTribute
+Map=/Game/Assets/Scene/Map/LightSpeed/LightSpeed

C++访问Collision Chanel

ECollisionChanel是定义在EngineTypes.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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
UENUM(BlueprintType)
enum ECollisionChannel
{

ECC_WorldStatic UMETA(DisplayName="WorldStatic"),
ECC_WorldDynamic UMETA(DisplayName="WorldDynamic"),
ECC_Pawn UMETA(DisplayName="Pawn"),
ECC_Visibility UMETA(DisplayName="Visibility" , TraceQuery="1"),
ECC_Camera UMETA(DisplayName="Camera" , TraceQuery="1"),
ECC_PhysicsBody UMETA(DisplayName="PhysicsBody"),
ECC_Vehicle UMETA(DisplayName="Vehicle"),
ECC_Destructible UMETA(DisplayName="Destructible"),

/** Reserved for gizmo collision */
ECC_EngineTraceChannel1 UMETA(Hidden),

ECC_EngineTraceChannel2 UMETA(Hidden),
ECC_EngineTraceChannel3 UMETA(Hidden),
ECC_EngineTraceChannel4 UMETA(Hidden),
ECC_EngineTraceChannel5 UMETA(Hidden),
ECC_EngineTraceChannel6 UMETA(Hidden),

ECC_GameTraceChannel1 UMETA(Hidden),
ECC_GameTraceChannel2 UMETA(Hidden),
ECC_GameTraceChannel3 UMETA(Hidden),
ECC_GameTraceChannel4 UMETA(Hidden),
ECC_GameTraceChannel5 UMETA(Hidden),
ECC_GameTraceChannel6 UMETA(Hidden),
ECC_GameTraceChannel7 UMETA(Hidden),
ECC_GameTraceChannel8 UMETA(Hidden),
ECC_GameTraceChannel9 UMETA(Hidden),
ECC_GameTraceChannel10 UMETA(Hidden),
ECC_GameTraceChannel11 UMETA(Hidden),
ECC_GameTraceChannel12 UMETA(Hidden),
ECC_GameTraceChannel13 UMETA(Hidden),
ECC_GameTraceChannel14 UMETA(Hidden),
ECC_GameTraceChannel15 UMETA(Hidden),
ECC_GameTraceChannel16 UMETA(Hidden),
ECC_GameTraceChannel17 UMETA(Hidden),
ECC_GameTraceChannel18 UMETA(Hidden),

/** Add new serializeable channels above here (i.e. entries that exist in FCollisionResponseContainer) */
/** Add only nonserialized/transient flags below */

// NOTE!!!! THESE ARE BEING DEPRECATED BUT STILL THERE FOR BLUEPRINT. PLEASE DO NOT USE THEM IN CODE

ECC_OverlapAll_Deprecated UMETA(Hidden),
ECC_MAX,
};

但是我们在项目设置中添加是可以取任意的名字的,而且创建的Chanel的数量有上限(18个),这是因为ECollisionChanel是预先定义了18个供游戏创建的枚举值,假如我们创建了一个名字为AAA的Chanel,会在当前项目的Config/DefaultEngine.ini[/Script/Engine.CollisionProfile]中创建以下项:

1
+DefaultChannelResponses=(Channel=ECC_GameTraceChannel1,DefaultResponse=ECR_Overlap,bTraceType=False,bStaticObject=False,Name="AAA")

在这里跟枚举值做了绑定,在C++代码中进行设置的时候就需要指定ECC_GameTraceChannel1的枚举值。

Todo:可以写一个方便从名字获取ECollisionChanel枚举值的库。

Actor的延迟Spawn

1
2
3
4
5
6
7
8
FTransform SpawnTransform(Rotation, Origin);
auto MyDeferredActor = Cast<ADeferredActor>(UGameplayStatics::BeginDeferredActorSpawnFromClass(this, DeferredActorClass, SpawnTransform));
if (MyDeferredActor != nullptr)
{
MyDeferredActor->Init(ShootDir);

UGameplayStatics::FinishSpawningActor(MyDeferredActor, SpawnTransform);
}

编译引擎的WindowsDebugTools错误

编译引擎出现以下错误:

1
2
3
4
ERROR: Unable to find installation of PDBCOPY.EXE, which is required to strip symbols. This tool is included as part of the 'Windows Debugging Tools' component of the Windows 10 SDK (https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk).
while executing task <Strip Platform="Win64" BaseDir="C:\BuildAgent\workspace\FGameEngine\FEngine" Files="#UE4Editor Win64 Unstripped" OutputDir="C:\BuildAgent\workspace\FGameEngine\FEngine\Engine\Saved" Tag="#UE4Editor Win64 Stripped" />
at Engine\Build\InstalledEngineBuild.xml(183)
(see C:\BuildAgent\workspace\FGameEngine\FEngine\Engine\Programs\AutomationTool\Saved\Logs\Log.txt for full exception trace)

这是因为没有安装Windows Debugging Tools,在这里下载Win10SDK安装程序,在其中选择安装Windows Debug Tools安装即可。

BuildGraph构建引擎

可以使用下列命令:
构建引擎的工具集:

1
Engine\Build\BatchFiles\RunUAT.bat BuildGraph -Script=Engine\Build\InstalledEngineBuild.xml -Target="Build Tools Win64" -set:HostPlatformOnly=true -set:WithWin64=true -set:WithWin32=false -set:WithDDC=false -clean

构建引擎:

1
2
Engine/Build/BatchFiles/RunUAT.bat BuildGraph -Script=Engine\Build\InstalledEngineBuild.xml -target=\"Make Installed Build Win64\" -set:WithDDC=false -set:WithWin64=true -set:WithWin32=false -set:WithAndroid=false -set:WithIOS=false -set:WithTVOS=false -set:WithLinux=false -set:WithLinuxAArch64=false -set:WithLumin=false -set:WithHoloLens=false

Game module could not be loaded

在引入其他模块的代码时,有时候会具有下面这样的情况:

Log中的提示:

1
2
3
4
5
[2020.07.18-01.58.50:729][  0]LogWindows: Failed to load 'E:/UnrealProjects/Examples/HotPatcherExample/Binaries/Win64/UE4Editor-HotPatcherExample.dll' (GetLastError=1114)
[2020.07.18-01.58.50:729][ 0]LogModuleManager: Warning: ModuleManager: Unable to load module 'E:/UnrealProjects/Examples/HotPatcherExample/Binaries/Win64/UE4Editor-HotPatcherExample.dll' because the file couldn't be loaded by the OS.
[2020.07.18-02.01.38:988][ 0]LogWindowsTextInputMethodSystem: Display: IME system now activated using TSF (微软拼音).
[2020.07.18-02.01.38:991][ 0]Message dialog closed, result: Ok, title: Message, text: The game module 'HotPatcherExample' could not be loaded. There may be an operating system error or the module may not be properly set up.
[2020.07.18-02.01.38:991][ 0]LogCore: Engine exit requested (reason: EngineExit() was called)

这个问题应该是项目中(或者插件中)加载DLL失败的问题,一般情况下是DLL文件存在问题或者DLL中逻辑的错误造成。

经过排查后发现我触发这个问题的方式为在使用UnLua注册lua_Reg函数时漏掉了置空最后一个元素:

1
2
3
4
5
6
7
8
9
10
static const luaL_Reg AMyActorLib[]=
{
{"ReceiveBytes",ReceiveBytes},
// {nullptr,nullptr}
};

BEGIN_EXPORT_REFLECTED_CLASS(AMyActor)
ADD_LIB(AMyActorLib)
END_EXPORT_CLASS(AMyActor)
IMPLEMENT_EXPORTED_CLASS(AMyActor)

因为AMyActorLib这个数组最后一个元素没有置空,在UnLuaEx.inl中的AddLib函数中:

1
2
3
4
5
6
7
8
9
10
11
12
template <bool bIsReflected>
void TExportedClassBase<bIsReflected>::AddLib(const luaL_Reg *InLib)
{
if (InLib)
{
while (InLib->name && InLib->func)
{
GlueFunctions.Add(new FGlueFunction(ANSI_TO_TCHAR(InLib->name), InLib->func));
++InLib;
}
}
}

如果lua_Reg数组的最后一个元素不为空就会导致这里出发UB行为(死循环),导致当前模块加载失败,也就触发了前面模块加载失败的问题。

蓝图编辑器中节点属性的修改

在编辑器模式下修改节点的值:

执行的函数是UEdGraphSchema_K2::TrySetDefaultValue(Editor/BlueprintGraph/Private/EdGraphSchema_K2.cpp),是通过Schema来调用的。简单来说就是通过当前的节点,去修改节点上的Pin的值,不管原始类型是什么,FString/int/float还是枚举,都可以通过这个方法设置。

UMG资料文档

文章:

Enum反射

UHT为Enum生成的代码

在UE中,当我们声明一个枚举类型时可以像UClass一样地形式为其添加UENUM标记,指导UHT为其生成反射代码:

1
2
3
4
5
6
7
UENUM(BlueprintType)
enum class ETypeName :uint8
{
None,
Int,
Float
};

经过UHT之后就变成了:

1
2
3
4
5
6
7
8
// generated.h
#define FOREACH_ENUM_ETYPENAME(op) \
op(ETypeName::None) \
op(ETypeName::Int) \
op(ETypeName::Float)

enum class ETypeName : uint8;
template<> TOPDOWNEXAMPLE_API UEnum* StaticEnum<ETypeName>();
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
// .gen.cpp
// End Cross Module References
static UEnum* ETypeName_StaticEnum()
{
static UEnum* Singleton = nullptr;
if (!Singleton)
{
Singleton = GetStaticEnum(Z_Construct_UEnum_TopdownExample_ETypeName, Z_Construct_UPackage__Script_TopdownExample(), TEXT("ETypeName"));
}
return Singleton;
}
template<> TOPDOWNEXAMPLE_API UEnum* StaticEnum<ETypeName>()
{
return ETypeName_StaticEnum();
}
static FCompiledInDeferEnum Z_CompiledInDeferEnum_UEnum_ETypeName(ETypeName_StaticEnum, TEXT("/Script/TopdownExample"), TEXT("ETypeName"), false, nullptr, nullptr);
uint32 Get_Z_Construct_UEnum_TopdownExample_ETypeName_Hash() { return 2221805252U; }
UEnum* Z_Construct_UEnum_TopdownExample_ETypeName()
{
#if WITH_HOT_RELOAD
UPackage* Outer = Z_Construct_UPackage__Script_TopdownExample();
static UEnum* ReturnEnum = FindExistingEnumIfHotReloadOrDynamic(Outer, TEXT("ETypeName"), 0, Get_Z_Construct_UEnum_TopdownExample_ETypeName_Hash(), false);
#else
static UEnum* ReturnEnum = nullptr;
#endif // WITH_HOT_RELOAD
if (!ReturnEnum)
{
static const UE4CodeGen_Private::FEnumeratorParam Enumerators[] = {
{ "ETypeName::None", (int64)ETypeName::None },
{ "ETypeName::Int", (int64)ETypeName::Int },
{ "ETypeName::Float", (int64)ETypeName::Float },
};
#if WITH_METADATA
const UE4CodeGen_Private::FMetaDataPairParam Enum_MetaDataParams[] = {
{ "BlueprintType", "true" },
{ "ModuleRelativePath", "MyK2Node.h" },
};
#endif
static const UE4CodeGen_Private::FEnumParams EnumParams = {
(UObject*(*)())Z_Construct_UPackage__Script_TopdownExample,
nullptr,
"ETypeName",
"ETypeName",
Enumerators,
ARRAY_COUNT(Enumerators),
RF_Public|RF_Transient|RF_MarkAsNative,
UE4CodeGen_Private::EDynamicType::NotDynamic,
(uint8)UEnum::ECppForm::EnumClass,
METADATA_PARAMS(Enum_MetaDataParams, ARRAY_COUNT(Enum_MetaDataParams))
};
UE4CodeGen_Private::ConstructUEnum(ReturnEnum, EnumParams);
}
return ReturnEnum;
}

UEnum的构造思路和UClass差不多,通过UHT生成Enum的反射代码,记录枚举类型的名字、枚举值的名字、元数据等等,通过UE4CodeGen_Private::ConstructUEnum把这些反射数据构造出UEnum。

同样也是通过延迟注册的方式把UEnum构造出来。

运行时访问UEnum

如果要获取一个UENMU的UEnum*可以通过StaticEnum<ETypeName>()或者通过UEnum* const MethodEnum = FindObjectChecked<UEnum>(ANY_PACKAGE, TEXT("ETypeName"), true);来拿。

在UE4.22+的版本中可以使用下列方法:

1
const UEnum* TypeEnum = StaticEnum<EnumType>();

在4.21及之前的版本就要麻烦一点:

1
const UEnum* TypeEnum = FindObject<UEnum>(ANY_PACKAGE, TEXT("EnumType"), true);

注意上面的TEXT("EnumType")其中要填想要获取的枚举类型名字。

根据枚举名字获取枚举值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// get enum value by name
{
FString EnumTypeName = TEXT("ETargetPlatform");
FString EnumName = FString::Printf(TEXT("%s::%s"),*EnumTypeName,TEXT("Int"))

UEnum* ETargetPlatformEnum = FindObject<UEnum>(ANY_PACKAGE, *EnumTypeName, true);

int32 EnumIndex = ETargetPlatformEnum->GetIndexByName(FName(*EnumName));
if (EnumIndex != INDEX_NONE)
{
UE_LOG(LogTemp, Log, TEXT("FOUND ENUM INDEX SUCCESS"));
int32 EnumValue = ETargetPlatformEnum->GetValueByIndex(EnumIndex);
ETargetPlatform CurrentEnum = (ETargetPlatform)EnumValue;
}
}

如果也想再封装一层模板类,让枚举名字也可以自动获取,则需要用得到C++的RTTI特性:

1
2
3
4
5
6
7
8
9
10
template<typename T>
static std::string GetCPPTypeName()
{
std::string result;
std::string type_name = typeid(T).name();

std::for_each(type_name.begin(),type_name.end(),[&result](const char& character){if(!std::isdigit(character)) result.push_back(character);});

return result;
}

枚举值与字符串的互相转换

有些需要序列化枚举值的需要,虽然我们可以通过FindObject<UEnum>传入枚举名字拿到UEnum*,再通过GetNameByValue拿到名字,但是这样需要针对每个枚举都要单独写,我写了模板函数来做这个事情:

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
// 同时支持4.21- 和4.21+版本引擎
#include <typeinfo>
#include <cctype>
#include <algorithm>

template<typename T>
static std::string GetCPPTypeName()
{
std::string result;
std::string type_name = typeid(T).name();

std::for_each(type_name.begin(),type_name.end(),[&result](const char& character){if(!std::isdigit(character)) result.push_back(character);});

return result;
}

template<typename ENUM_TYPE>
static FString GetEnumNameByValue(ENUM_TYPE InEnumValue, bool bFullName = false)
{
FString result;
{
FString TypeName;
FString ValueName;

#if ENGINE_MINOR_VERSION > 21
UEnum* FoundEnum = StaticEnum<ENUM_TYPE>();
#else
FString EnumTypeName = ANSI_TO_TCHAR(GetCPPTypeName<ENUM_TYPE>().c_str());
UEnum* FoundEnum = FindObject<UEnum>(ANY_PACKAGE, *EnumTypeName, true);
#endif
if (FoundEnum)
{
result = FoundEnum->GetNameByValue((int64)InEnumValue).ToString();
result.Split(TEXT("::"), &TypeName, &ValueName, ESearchCase::CaseSensitive, ESearchDir::FromEnd);
if (!bFullName)
{
result = ValueName;
}
}
}
return result;
}

以及从字符串获取枚举值:

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
template<typename ENUM_TYPE>
static bool GetEnumValueByName(const FString& InEnumValueName, ENUM_TYPE& OutEnumValue)
{
bool bStatus = false;

#if ENGINE_MINOR_VERSION >22
UEnum* FoundEnum = StaticEnum<ENUM_TYPE>();
FString EnumTypeName = FoundEnum->CppType;
#else
FString EnumTypeName = *GetCPPTypeName<ENUM_TYPE>();
UEnum* FoundEnum = FindObject<UEnum>(ANY_PACKAGE, *EnumTypeName, true);
#endif

if (FoundEnum)
{
FString EnumValueFullName = EnumTypeName + TEXT("::") + InEnumValueName;
int32 EnumIndex = FoundEnum->GetIndexByName(FName(*EnumValueFullName));
if (EnumIndex != INDEX_NONE)
{
int32 EnumValue = FoundEnum->GetValueByIndex(EnumIndex);
ENUM_TYPE ResultEnumValue = (ENUM_TYPE)EnumValue;
OutEnumValue = ResultEnumValue;
bStatus = false;
}
}
return bStatus;
}

Struct反射

前面讲到了Class/function/property的反射,UE还支持结构体的反射,其实从C++的标准语义来说并没有区分“结构”和“类”,关键字structclass的区别只在于默认的访问权限。

在UE里面,支持反射的结构提只能使用struct,并且不能包含任何UFUNCTION的函数,命名必须以F开头。

1
2
3
4
5
6
7
8
9
10
USTRUCT(BlueprintType)
struct FTestStruct
{
GENERATED_USTRUCT_BODY()

UPROPERTY(EditAnywhere)
int32 ival;
UPROPERTY(EditAnywhere)
UTexture2D* Texture;
};

UE的标记语法和Class的类似,不过UHT为Struct生成的代码要简单许多,因为没有UFUNCTION也没有继承UObject。

GENERATED_USTRUCT_BODY这个标记UHT会展开生成一个真正的C++宏(在genreated.h中):

1
2
3
4
5
6
// generated.h
#define HotPatcherExample_Source_HotPatcherExample_TestStruct_h_11_GENERATED_BODY \
friend struct Z_Construct_UScriptStruct_FTestStruct_Statics; \
HOTPATCHEREXAMPLE_API static class UScriptStruct* StaticStruct();

template<> HOTPATCHEREXAMPLE_API UScriptStruct* StaticStruct<struct FTestStruct>();

把UHT生成的用于记录当前struct反射数据的结构Z_Construct_UScriptStruct_FTestStruct_Statis声明为当前struct类的友元,可以让它访问到自己的私有成员。而且还声明了当前Struct的static成员函数StaticStruct和全局模板函数StaticStruct<FTestStruct>

对Struct生成的反射数;

据的结构与class的类似,只有Property的反射信息。

gen.cpp中有以下内容:

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
class UScriptStruct* FTestStruct::StaticStruct()
{
static class UScriptStruct* Singleton = NULL;
if (!Singleton)
{
extern HOTPATCHEREXAMPLE_API uint32 Get_Z_Construct_UScriptStruct_FTestStruct_Hash();
Singleton = GetStaticStruct(Z_Construct_UScriptStruct_FTestStruct, Z_Construct_UPackage__Script_HotPatcherExample(), TEXT("TestStruct"), sizeof(FTestStruct), Get_Z_Construct_UScriptStruct_FTestStruct_Hash());
}
return Singleton;
}
template<> HOTPATCHEREXAMPLE_API UScriptStruct* StaticStruct<FTestStruct>()
{
return FTestStruct::StaticStruct();
}
static FCompiledInDeferStruct Z_CompiledInDeferStruct_UScriptStruct_FTestStruct(FTestStruct::StaticStruct, TEXT("/Script/HotPatcherExample"), TEXT("TestStruct"), false, nullptr, nullptr);
static struct FScriptStruct_HotPatcherExample_StaticRegisterNativesFTestStruct
{
FScriptStruct_HotPatcherExample_StaticRegisterNativesFTestStruct()
{
UScriptStruct::DeferCppStructOps(FName(TEXT("TestStruct")),new UScriptStruct::TCppStructOps<FTestStruct>);
}
} ScriptStruct_HotPatcherExample_StaticRegisterNativesFTestStruct;

UScriptStruct* Z_Construct_UScriptStruct_FTestStruct()
{
#if WITH_HOT_RELOAD
extern uint32 Get_Z_Construct_UScriptStruct_FTestStruct_Hash();
UPackage* Outer = Z_Construct_UPackage__Script_HotPatcherExample();
static UScriptStruct* ReturnStruct = FindExistingStructIfHotReloadOrDynamic(Outer, TEXT("TestStruct"), sizeof(FTestStruct), Get_Z_Construct_UScriptStruct_FTestStruct_Hash(), false);
#else
static UScriptStruct* ReturnStruct = nullptr;
#endif
if (!ReturnStruct)
{
UE4CodeGen_Private::ConstructUScriptStruct(ReturnStruct, Z_Construct_UScriptStruct_FTestStruct_Statics::ReturnStructParams);
}
return ReturnStruct;
}
uint32 Get_Z_Construct_UScriptStruct_FTestStruct_Hash() { return 4266809061U; }

上面的代码包含了从UHT生成的反射信息中构造出UStructSctruct以及延迟注册的方法,和Class的方式类似。

还包含生成的结构Z_Construct_UScriptStruct_STRUCTNAME_Statics如下:

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
struct Z_Construct_UScriptStruct_FTestStruct_Statics
{
#if WITH_METADATA
static const UE4CodeGen_Private::FMetaDataPairParam Struct_MetaDataParams[];
#endif
static void* NewStructOps();
#if WITH_METADATA
static const UE4CodeGen_Private::FMetaDataPairParam NewProp_Texture_MetaData[];
#endif
static const UE4CodeGen_Private::FObjectPropertyParams NewProp_Texture;
#if WITH_METADATA
static const UE4CodeGen_Private::FMetaDataPairParam NewProp_ival_MetaData[];
#endif
static const UE4CodeGen_Private::FIntPropertyParams NewProp_ival;
static const UE4CodeGen_Private::FPropertyParamsBase* const PropPointers[];
static const UE4CodeGen_Private::FStructParams ReturnStructParams;
};

#if WITH_METADATA
const UE4CodeGen_Private::FMetaDataPairParam Z_Construct_UScriptStruct_FTestStruct_Statics::Struct_MetaDataParams[] = {
{ "BlueprintType", "true" },
{ "ModuleRelativePath", "TestStruct.h" },
};
#endif
void* Z_Construct_UScriptStruct_FTestStruct_Statics::NewStructOps()
{
return (UScriptStruct::ICppStructOps*)new UScriptStruct::TCppStructOps<FTestStruct>();
}
#if WITH_METADATA
const UE4CodeGen_Private::FMetaDataPairParam Z_Construct_UScriptStruct_FTestStruct_Statics::NewProp_Texture_MetaData[] = {
{ "Category", "TestStruct" },
{ "ModuleRelativePath", "TestStruct.h" },
};
#endif

const UE4CodeGen_Private::FObjectPropertyParams Z_Construct_UScriptStruct_FTestStruct_Statics::NewProp_Texture = {
"Texture",
nullptr,
(EPropertyFlags)0x0010000000000001,
UE4CodeGen_Private::EPropertyGenFlags::Object,
RF_Public|RF_Transient|RF_MarkAsNative,
1,
STRUCT_OFFSET(FTestStruct, Texture),
Z_Construct_UClass_UTexture2D_NoRegister,
METADATA_PARAMS(Z_Construct_UScriptStruct_FTestStruct_Statics::NewProp_Texture_MetaData,
UE_ARRAY_COUNT(Z_Construct_UScriptStruct_FTestStruct_Statics::NewProp_Texture_MetaData))
};
#if WITH_METADATA
const UE4CodeGen_Private::FMetaDataPairParam Z_Construct_UScriptStruct_FTestStruct_Statics::NewProp_ival_MetaData[] = {
{ "Category", "TestStruct" },
{ "ModuleRelativePath", "TestStruct.h" },
};
#endif
const UE4CodeGen_Private::FIntPropertyParams Z_Construct_UScriptStruct_FTestStruct_Statics::NewProp_ival = {
"ival",
nullptr,
(EPropertyFlags)0x0010000000000001,
UE4CodeGen_Private::EPropertyGenFlags::Int,
RF_Public|RF_Transient|RF_MarkAsNative,
1,
STRUCT_OFFSET(FTestStruct, ival),
METADATA_PARAMS(Z_Construct_UScriptStruct_FTestStruct_Statics::NewProp_ival_MetaData,
UE_ARRAY_COUNT(Z_Construct_UScriptStruct_FTestStruct_Statics::NewProp_ival_MetaData))
};

const UE4CodeGen_Private::FPropertyParamsBase* const Z_Construct_UScriptStruct_FTestStruct_Statics::PropPointers[] = {
(const UE4CodeGen_Private::FPropertyParamsBase*)&Z_Construct_UScriptStruct_FTestStruct_Statics::NewProp_Texture,
(const UE4CodeGen_Private::FPropertyParamsBase*)&Z_Construct_UScriptStruct_FTestStruct_Statics::NewProp_ival,
};
const UE4CodeGen_Private::FStructParams Z_Construct_UScriptStruct_FTestStruct_Statics::ReturnStructParams = {
(UObject* (*)())Z_Construct_UPackage__Script_HotPatcherExample,
nullptr,
&NewStructOps,
"TestStruct",
sizeof(FTestStruct),
alignof(FTestStruct),
Z_Construct_UScriptStruct_FTestStruct_Statics::PropPointers,
UE_ARRAY_COUNT(Z_Construct_UScriptStruct_FTestStruct_Statics::PropPointers),
RF_Public|RF_Transient|RF_MarkAsNative,
EStructFlags(0x00000001),
METADATA_PARAMS(Z_Construct_UScriptStruct_FTestStruct_Statics::Struct_MetaDataParams, UE_ARRAY_COUNT(Z_Construct_UScriptStruct_FTestStruct_Statics::Struct_MetaDataParams))
};

可以看到Struct的每个属性也是通过FPropertyParamsBase来存储的,与Class一致。

区别在于,Struct使用UE4CodeGen_Private::FStructParams来存储当前结构的反射信息,其声明如下(CoreUObject/Public/UObject/UObjectGlobals.h):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// CoreUObject/Public/UObject/UObjectGlobals.h
struct FStructParams
{
UObject* (*OuterFunc)();
UScriptStruct* (*SuperFunc)();
void* (*StructOpsFunc)(); // really returns UScriptStruct::ICppStructOps*
const char* NameUTF8;
SIZE_T SizeOf;
SIZE_T AlignOf;
const FPropertyParamsBase* const* PropertyArray;
int32 NumProperties;
EObjectFlags ObjectFlags;
uint32 StructFlags; // EStructFlags
#if WITH_METADATA
const FMetaDataPairParam* MetaDataArray;
int32 NumMetaData;
#endif
};

这个结构中比较特殊的一点是StructOpsFunc是一个函数指针,用来管理C++结构的构造和析构,使用的也是placement-new的方式,TCppStructOps<>模板定义在CoreUObject/Public/UObject/Class.h

类反射

UHT为类产生的反射信息

当在UE中新建一个类并继承自UObject时,可以在类声明的上一行添加UCLASS标记,当执行编译的时候UBT会调用UHT来根据标记来生成C++代码(不过非UCLASS的类也可以用宏来生成反射信息)。

UHT为类生成的代码为:

  1. 为所有的UFUNCTION的函数创建FName,命名规则为NAME_CLASSNAME_FUNCTIONNAME,如NAME_AMyActor_TestFunc
  2. BlueprintNativeEvent和BlueprintImplementEvent创建同名函数实现,并通过ProcessEvent转发调用
  3. 为所有加了UFUNCTION的函数生成Thunk函数,为当前类的static函数,原型为static void execFUNCNAME( UObject* Context, FFrame& Stack, RESULT_DECL)
  4. 创建当前类的StaticRegisterNatives*函数,并把上一步提到的exec这样的thunk函数通过Name-execFunc指针的形式通过FNativeFunctionRegistrar::RegisterFunctions注册到UClass;
  5. 创建出Z_Construct_UClass_CLASSNAME_NoRegister函数,返回值是CLASSNAME::StaticClass()
  6. 创建出Z_Construct_UClass_CLASSNAME_Statics类(GENERATED_BODY等宏会把该类添加为我们创建类的友元,使其可以访问私有成员,用于获取成员指针)

Z_Construct_UClass_CLASSNAME_Statics类的结构为:

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
// AMyActor.h
UCLASS(BlueprintType)
class XXXX_API AMyActor:public AActor
{
GENERATED_BODY()
// ...

UPROPERTY()
int32 ival;
UFUNCTION()
int32 GetIval();
UFUNCTION()
void TESTFUNC();
};

// generated code in AMyActor.gen.cpp
struct Z_Construct_UClass_AMyActor_Statics
{
static UObject* (*const DependentSingletons[])();
static const FClassFunctionLinkInfo FuncInfo[];

#if WITH_METADATA
static const UE4CodeGen_Private::FMetaDataPairParam NewProp_ival_MetaData[];
#endif
static const UE4CodeGen_Private::FIntPropertyParams NewProp_ival;
static const UE4CodeGen_Private::FPropertyParamsBase* const PropPointers[];
static const UE4CodeGen_Private::FImplementedInterfaceParams InterfaceParams[];
static const FCppClassTypeInfoStatic StaticCppClassTypeInfo;
static const UE4CodeGen_Private::FClassParams ClassParams;
};
UObject* (*const Z_Construct_UClass_AMyActor_Statics::DependentSingletons[])() = {
(UObject* (*)())Z_Construct_UClass_AActor,
(UObject* (*)())Z_Construct_UPackage__Script_MicroEnd_423,
};
const FClassFunctionLinkInfo Z_Construct_UClass_AMyActor_Statics::FuncInfo[] = {
{ &Z_Construct_UFunction_AMyActor_GetIval, "GetIval" }, // 3480851337
{ &Z_Construct_UFunction_AMyActor_TESTFUNC, "TESTFUNC" }, // 2984899165
};

该类中的成员为:

  • static UObject* (*const DependentSingletons[])();记录当前类基类的Z_Construct_UClass_BASECLASSNAME函数指针,用它可以构造出基类的UClass,还记录了当前类属于哪个Package的函数指针Z_Construct_UPackage__Script_MODULENAME

Z_Construct_UPackage__Script_MODULENAME函数是定义在MODULE_NAME.init.gen.cpp里。

  • static const UE4CodeGen_Private::FMetaDataPairParam Class_MetaDataParams[];用于记录UCLASS的元数据,如BlueprintType标记
  • 反射属性的F*PropertyParams以及其Metadata,均为static成员
  • static const UE4CodeGen_Private::FPropertyParamsBase* const PropPointers[];,数组,用于存储当前类所有的反射属性的信息(是个指针数组,用于存储5.3中的static成员的地址)
  • static const UE4CodeGen_Private::FImplementedInterfaceParams InterfaceParams[];,数组,用于存储当前类所有的接口信息
1
2
3
const UE4CodeGen_Private::FImplementedInterfaceParams Z_Construct_UClass_AMyActor_Statics::InterfaceParams[] = {
{ Z_Construct_UClass_UUnLuaInterface_NoRegister, (int32)VTABLE_OFFSET(AMyActor, IUnLuaInterface), false },
};
  • static const FCppClassTypeInfoStatic StaticCppClassTypeInfo;用于类型萃取,记录当前类是否是抽象类。
1
2
3
const FCppClassTypeInfoStatic Z_Construct_UClass_AMyActor_Statics::StaticCppClassTypeInfo = {
TCppClassTypeTraits<AMyActor>::IsAbstract,
};
  • static const UE4CodeGen_Private::FClassParams ClassParams;构造出UClass需要的所有反射数据,统一记录上面所有生成的反射信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const UE4CodeGen_Private::FClassParams Z_Construct_UClass_AMyActor_Statics::ClassParams = {
&AMyActor::StaticClass,
nullptr,
&StaticCppClassTypeInfo,
DependentSingletons,
FuncInfo,
Z_Construct_UClass_AMyActor_Statics::PropPointers,
InterfaceParams,
ARRAY_COUNT(DependentSingletons),
ARRAY_COUNT(FuncInfo),
ARRAY_COUNT(Z_Construct_UClass_AMyActor_Statics::PropPointers),
ARRAY_COUNT(InterfaceParams),
0x009000A0u,
METADATA_PARAMS(Z_Construct_UClass_AMyActor_Statics::Class_MetaDataParams, ARRAY_COUNT(Z_Construct_UClass_AMyActor_Statics::Class_MetaDataParams))
};
  1. 全局函数Z_Construct_UClass_AMyActor通过ClassParams构造出真正的UClass对象。
  2. 使用IMPLEMENT_CLASS注册当前类到GetDeferredClassRegistration(),如果在WITH_HOT_RELOAD为true的情况下也会注册到GetDeferRegisterClassMap()中。
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
// Register a class at startup time.
#define IMPLEMENT_CLASS(TClass, TClassCrc) \
static TClassCompiledInDefer<TClass> AutoInitialize##TClass(TEXT(#TClass), sizeof(TClass), TClassCrc); \
UClass* TClass::GetPrivateStaticClass() \
{ \
static UClass* PrivateStaticClass = NULL; \
if (!PrivateStaticClass) \
{ \
/* this could be handled with templates, but we want it external to avoid code bloat */ \
GetPrivateStaticClassBody( \
StaticPackage(), \
(TCHAR*)TEXT(#TClass) + 1 + ((StaticClassFlags & CLASS_Deprecated) ? 11 : 0), \
PrivateStaticClass, \
StaticRegisterNatives##TClass, \
sizeof(TClass), \
alignof(TClass), \
(EClassFlags)TClass::StaticClassFlags, \
TClass::StaticClassCastFlags(), \
TClass::StaticConfigName(), \
(UClass::ClassConstructorType)InternalConstructor<TClass>, \
(UClass::ClassVTableHelperCtorCallerType)InternalVTableHelperCtorCaller<TClass>, \
&TClass::AddReferencedObjects, \
&TClass::Super::StaticClass, \
&TClass::WithinClass::StaticClass \
); \
} \
return PrivateStaticClass; \
}

AMyActorIMPLEMENT_CLASS(AMyActor,3240835608)经过预处理之后为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static TClassCompiledInDefer<AMyActor> AutoInitializeAMyActor(TEXT("AMyActor"), sizeof(AMyActor), 3240835608);
UClass * AMyActor::GetPrivateStaticClass() {
static UClass * PrivateStaticClass = NULL;
if (!PrivateStaticClass)
{
GetPrivateStaticClassBody(
StaticPackage(),
(TCHAR*)TEXT("AMyActor") + 1 + ((StaticClassFlags & CLASS_Deprecated) ? 11 : 0),
PrivateStaticClass,
StaticRegisterNativesAMyActor,
sizeof(AMyActor),
alignof(AMyActor),
(EClassFlags)AMyActor::StaticClassFlags,
AMyActor::StaticClassCastFlags(),
AMyActor::StaticConfigName(),
(UClass::ClassConstructorType)InternalConstructor<AMyActor>,
(UClass::ClassVTableHelperCtorCallerType) InternalVTableHelperCtorCaller<AMyActor>,
&AMyActor::AddReferencedObjects,
&AMyActor::Super::StaticClass,
&AMyActor::WithinClass::StaticClass
);
}
return PrivateStaticClass;
};

其中TClassCompiledInDefer<TClass>这个模板类的构造函数中通过调用UClassCompiledInDefer将当前反射类的注册到GetDeferredClassRegistration(),它得到的是一个类型为FFieldCompiledInInfo*的数组,用于记录引擎中所有反射类的信息,用于在CoreUObjectModule启动时将UHT生成的这些反射信息在ProcessNewlyLoadedUObjects函数中通过UClassRegisterAllCompiledInClasses将所有反射类的UClass构造出来。

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
/** Register all loaded classes */
void UClassRegisterAllCompiledInClasses()
{
#if WITH_HOT_RELOAD
TArray<UClass*> AddedClasses;
#endif
SCOPED_BOOT_TIMING("UClassRegisterAllCompiledInClasses");

TArray<FFieldCompiledInInfo*>& DeferredClassRegistration = GetDeferredClassRegistration();
for (const FFieldCompiledInInfo* Class : DeferredClassRegistration)
{
UClass* RegisteredClass = Class->Register();
#if WITH_HOT_RELOAD
if (GIsHotReload && Class->OldClass == nullptr)
{
AddedClasses.Add(RegisteredClass);
}
#endif
}
DeferredClassRegistration.Empty();

#if WITH_HOT_RELOAD
if (AddedClasses.Num() > 0)
{
FCoreUObjectDelegates::RegisterHotReloadAddedClassesDelegate.Broadcast(AddedClasses);
}
#endif
}

TClassCompiledInDefer<AMyActor>Register函数就是调用AMyActor::StaticClass的,然后StaticClass中调用GetPrivateStaticClass,其中有一个static对象,就是当前类的UClass,所以它只会构造依次,使用UXXXX::StaticClass都是直接获得。

注意:UClass的构造是跟着模块的启动创建的,所以之后当引擎启动到一个模块的时候它的UClass才被创建出来)。

非UCLASS的反射

有些继承自UObject的类是没有加UCLASS标记的,所以也不会包含gen.cppgenerated.h文件,但是UE也提供了非UCLASS的反射方法,类似于UTextureBuffer这个类。

在类内时添加DECLARE_CASTED_CLASS_INTRINSIC_WITH_API宏用于手动添加,实现类似UHT生成GENERATED_BODY宏的操作:

1
2
3
4
5
6
class UTextBuffer
: public UObject
, public FOutputDevice
{
DECLARE_CASTED_CLASS_INTRINSIC_WITH_API(UTextBuffer, UObject, 0, TEXT("/Script/CoreUObject"), CASTCLASS_None, COREUOBJECT_API)
}

DECLARE_CASTED_CLASS_INTRINSIC_WITH_API可以处理类似generated.h的行为,但是gen.cpp里创建出static TClassCompiledInDefer<CLASS_NAME>的代码还没有,UE提供了另一个宏:

1
IMPLEMENT_CORE_INTRINSIC_CLASS(UTextBuffer, UObject, { });

虽然和gen.cpp里通过UHT生成的代码不同,但是统一使用TClassCompiledInDefer<TClass>FCompiledInDefer来注册到引擎中。
这样就实现了可以不使用UCLASS标记也可以为继承自UObject的类生成反射信息。

UClass的构造思路

前面讲了这么多都是在分析UE创建UClass的代码,我想从UE的实现思路上分析一下设计过程。

  1. 首先UHT通过分析代码创建出gen.cpp和generated.h中间记录着当前类的反射信息、类本身的反射信息、类中函数的反射信息、类数据成员的反射信息。
  2. 当前类的反射信息(类、成员函数、数据成员)等被统一存储在一个名为Z_Construct_UClass_CLASSNAME_Statics的结构中;
  3. 该结构通过IMPLEMENT_CLASS生成的代码将当前类添加到GetDeferredClassRegistration()中。因为全局作用域static对象的构造时机是先于主函数的第一条语句的,所以当进入引擎逻辑的时候,引擎内置的模块中类的TClassCompiledInDefer<>都已经被创建完毕,在编辑器模式下, 因为不同的模块都是编译为DLL的,所以在加载模块的时候它们的static对象才会被创建。
1
2
// gen.cpp
static TClassCompiledInDefer<AMyActor> AutoInitializeAMyActor(TEXT("AMyActor"), sizeof(AMyActor), 3240835608);
  1. 与上一步同样的手法,把类生成的反射信息通过FCompiledInDefer收集到GetDeferredCompiledInRegistration()
1
static FCompiledInDefer Z_CompiledInDefer_UClass_AMyActor(Z_Construct_UClass_AMyActor, &AMyActor::StaticClass, TEXT("/Script/MicroEnd_423"), TEXT("AMyActor"), false, nullptr, nullptr, nullptr);

引擎如何使用生成的反射信息

在UHT生成反射的代码之后,引擎会根据这些代码生成UClass、UStruct、UEnum、UFunction和UProperty等。

它们都是在ProcessNewlyLoadedUObjects中被执行的,注意该函数会进来很多次,当每一个模块被加载的时候都会走一遍,因为在Obj.cppInitUObject函数中,把函数ProcessNewlyLoadedUObjects添加到了FModuleManager::Get().OnProcessLoadedObjectsCallback()中:

1
2
3
#if !USE_PER_MODULE_UOBJECT_BOOTSTRAP // otherwise this is already done
FModuleManager::Get().OnProcessLoadedObjectsCallback().AddStatic(ProcessNewlyLoadedUObjects);
#endif

之所以要这么做,是因为UE的Module中都会有很多的反射类,但引擎一启动并不是所有的类在同一时刻都被加载了,因为模块有不同的加载时机,所以引擎中对于UClass的构造也不是一个一次性过程。

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
// CoreUObject/Private/UObject/UObjectBase.cpp
void ProcessNewlyLoadedUObjects()
{
LLM_SCOPE(ELLMTag::UObject);
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("ProcessNewlyLoadedUObjects"), STAT_ProcessNewlyLoadedUObjects, STATGROUP_ObjectVerbose);

UClassRegisterAllCompiledInClasses();

const TArray<UClass* (*)()>& DeferredCompiledInRegistration = GetDeferredCompiledInRegistration();
const TArray<FPendingStructRegistrant>& DeferredCompiledInStructRegistration = GetDeferredCompiledInStructRegistration();
const TArray<FPendingEnumRegistrant>& DeferredCompiledInEnumRegistration = GetDeferredCompiledInEnumRegistration();

bool bNewUObjects = false;
while( GFirstPendingRegistrant || DeferredCompiledInRegistration.Num() || DeferredCompiledInStructRegistration.Num() || DeferredCompiledInEnumRegistration.Num() )
{
bNewUObjects = true;
UObjectProcessRegistrants();
UObjectLoadAllCompiledInStructs();
UObjectLoadAllCompiledInDefaultProperties();
}
#if WITH_HOT_RELOAD
UClassReplaceHotReloadClasses();
#endif

if (bNewUObjects && !GIsInitialLoad)
{
UClass::AssembleReferenceTokenStreams();
}
}

UClass构造的调用栈:

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
// CoreUObject/Private/UObject/UObjectBase.cpp
/** Register all loaded classes */
void UClassRegisterAllCompiledInClasses()
{
#if WITH_HOT_RELOAD
TArray<UClass*> AddedClasses;
#endif

TArray<FFieldCompiledInInfo*>& DeferredClassRegistration = GetDeferredClassRegistration();
for (const FFieldCompiledInInfo* Class : DeferredClassRegistration)
{
UClass* RegisteredClass = Class->Register();
#if WITH_HOT_RELOAD
if (GIsHotReload && Class->OldClass == nullptr)
{
AddedClasses.Add(RegisteredClass);
}
#endif
}
DeferredClassRegistration.Empty();

#if WITH_HOT_RELOAD
if (AddedClasses.Num() > 0)
{
FCoreUObjectDelegates::RegisterHotReloadAddedClassesDelegate.Broadcast(AddedClasses);
}
#endif
}

可以看到,在UClassRegisterAllCompiledInClasses只是去调用了每个反射类的StaticClass函数(Class->Register()内部是对类型的StaticClass的转发调用),在开启WITH_HOT_RELOAD的情况下也会把新的UClass给代理调用传递出去,然后把当前的数组置空。

之所以要置空,就是因为前面说的,UE的UClass构造是一个模块一个模块来执行的,当一个模块执行完毕之后就把当前模块注册到GetDeferredClassRegistration()里的元素置空,等着下个模块启动的时候(加载DLL时它们的static成员会构造然后注册到里面),再执行LoadModuleWithFailureReason就是又一遍循环。

在模块启动的时候会执行LoadModuleWithFailureReason里面调用了这个Delegate,所以每一个模块启动的时候都会执行ProcessNewlyLoadedUObjects,把自己当前模块中的UClass/UStruct/UEnum都构造出来。

函数反射

UHT生成的反射信息

在UE的代码中加了UFUNCTION()修饰后UHT就会为该函数生成反射代码。

每一个支持反射的函数UHT都会给它生成一个类和一个函数:
如在AMyActor这个类下有一个ReflexFunc的函数:

1
2
3
4
5
UFUNCTION()
bool ReflexFunc(int32 InIval, UObject* InObj)
{
return false;
}

UHT会生成这样命名规则的一个类和函数:

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
// UHT为ReflexFunc生成的反射代码
struct Z_Construct_UFunction_AMyActor_ReflexFunc_Statics
{
struct MyActor_eventReflexFunc_Parms
{
int32 InIval;
UObject* InObj;
bool ReturnValue;
};
static void NewProp_ReturnValue_SetBit(void* Obj);
static const UE4CodeGen_Private::FBoolPropertyParams NewProp_ReturnValue;
static const UE4CodeGen_Private::FObjectPropertyParams NewProp_InObj;
static const UE4CodeGen_Private::FIntPropertyParams NewProp_InIval;
static const UE4CodeGen_Private::FPropertyParamsBase* const PropPointers[];
#if WITH_METADATA
static const UE4CodeGen_Private::FMetaDataPairParam Function_MetaDataParams[];
#endif
static const UE4CodeGen_Private::FFunctionParams FuncParams;
};
void Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::NewProp_ReturnValue_SetBit(void* Obj)
{
((MyActor_eventReflexFunc_Parms*)Obj)->ReturnValue = 1;
}
const UE4CodeGen_Private::FBoolPropertyParams Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::NewProp_ReturnValue = {
"ReturnValue",
nullptr,
(EPropertyFlags)0x0010000000000580,
UE4CodeGen_Private::EPropertyGenFlags::Bool | UE4CodeGen_Private::EPropertyGenFlags::NativeBool,
RF_Public|RF_Transient|RF_MarkAsNative,
1,
sizeof(bool),
sizeof(MyActor_eventReflexFunc_Parms),
&Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::NewProp_ReturnValue_SetBit,
METADATA_PARAMS(nullptr,
0)
};
const UE4CodeGen_Private::FObjectPropertyParams Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::NewProp_InObj = {
"InObj",
nullptr,
(EPropertyFlags)0x0010000000000080,
UE4CodeGen_Private::EPropertyGenFlags::Object,
RF_Public|RF_Transient|RF_MarkAsNative,
1,
STRUCT_OFFSET(MyActor_eventReflexFunc_Parms,
InObj),
Z_Construct_UClass_UObject_NoRegister,
METADATA_PARAMS(nullptr,
0)
};
const UE4CodeGen_Private::FIntPropertyParams Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::NewProp_InIval = {
"InIval",
nullptr,
(EPropertyFlags)0x0010000000000080,
UE4CodeGen_Private::EPropertyGenFlags::Int,
RF_Public|RF_Transient|RF_MarkAsNative,
1,
STRUCT_OFFSET(MyActor_eventReflexFunc_Parms,
InIval),
METADATA_PARAMS(nullptr,
0)
};
const UE4CodeGen_Private::FPropertyParamsBase* const Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::PropPointers[] = {
(const UE4CodeGen_Private::FPropertyParamsBase*)&Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::NewProp_ReturnValue,
(const UE4CodeGen_Private::FPropertyParamsBase*)&Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::NewProp_InObj,
(const UE4CodeGen_Private::FPropertyParamsBase*)&Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::NewProp_InIval,
};
#if WITH_METADATA
const UE4CodeGen_Private::FMetaDataPairParam Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::Function_MetaDataParams[] = {
{ "ModuleRelativePath",
"MyActor.h" },
};
#endif
const UE4CodeGen_Private::FFunctionParams Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::FuncParams = { (UObject*(*)())Z_Construct_UClass_AMyActor,
nullptr,
"ReflexFunc",
nullptr,
nullptr,
sizeof(MyActor_eventReflexFunc_Parms),
Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::PropPointers,
UE_ARRAY_COUNT(Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::PropPointers),
RF_Public|RF_Transient|RF_MarkAsNative,
(EFunctionFlags)0x00020401,
0,
0,
METADATA_PARAMS(Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::Function_MetaDataParams,
UE_ARRAY_COUNT(Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::Function_MetaDataParams))
};

// 通过UHT生成的反射信息来构造出UFunction
UFunction* Z_Construct_UFunction_AMyActor_ReflexFunc()
{
static UFunction* ReturnFunction = nullptr;
if (!ReturnFunction)
{
UE4CodeGen_Private::ConstructUFunction(ReturnFunction, Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::FuncParams);
}
return ReturnFunction;
}

定义的Z_Construct_UFunction_AMyActor_ReflexFunc_Statics类中包含了以下信息:

  1. 存储函数的参数、返回值的结构体(POD),注意该结构的声明顺序是按照函数参数的顺序+最后一个成员是函数返回值的方式排列。
  2. 函数参数、返回值的F*PropertyParams,用来给函数的每个参数以及返回值生成反射信息,用于构造出UProperty,static成员;
  3. 成员static const UE4CodeGen_Private::FPropertyParamsBase* const PropPointers[];,数组,用于记录该函数的参数和返回值的类型为FPropertyParamsBase的static数据成员的地址。
  4. 成员static const UE4CodeGen_Private::FMetaDataPairParam Function_MetaDataParams[];,用于记录函数的元数据。如所属文件、Category、注释等等。
  5. 成员static const UE4CodeGen_Private::FFunctionParams FuncParams;用于记录当前函数的名字、Flag、参数的F*PropertyParams、参数数量,参数的结构大小等等,用于通过它来创建出UFunction*
  6. 至于生成的SetBitd的函数的作用,在上面属性反射的部分已经讲到了。

UE4CodeGen_Private::FFunctionParams结构声明为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Source/Runtime/CoreUObject/Public/UObject/UObjectGlobals.h
struct FFunctionParams
{
UObject* (*OuterFunc)();
UFunction* (*SuperFunc)();
const char* NameUTF8;
const char* OwningClassName;
const char* DelegateName;
SIZE_T StructureSize;
const FPropertyParamsBase* const* PropertyArray;
int32 NumProperties;
EObjectFlags ObjectFlags;
EFunctionFlags FunctionFlags;
uint16 RPCId;
uint16 RPCResponseId;
#if WITH_METADATA
const FMetaDataPairParam* MetaDataArray;
int32 NumMetaData;
#endif
};

UHT生成的Z_Construct_UFunction_AMyActor_ReflexFunc函数做了以下事情:

1
2
3
4
5
6
7
8
9
UFunction* Z_Construct_UFunction_AMyActor_ReflexFunc()
{
static UFunction* ReturnFunction = nullptr;
if (!ReturnFunction)
{
UE4CodeGen_Private::ConstructUFunction(ReturnFunction, Z_Construct_UFunction_AMyActor_Add_Statics::FuncParams);
}
return ReturnFunction;
}

根据定义的Z_Construct_UFunction_AMyActor_ReflexFunc_Statics结构中的FuncParams成员来创建出真正的UFunction对象。

最后,Z_Construct_UFunction_AMyActor_ReflexFunc这个函数会被注册到当前类反射数据的FuncInfo中。

Thunk函数

UE会为标记为UFUNCTION的Native函数生成对应的Thunk函数(BlueprintImplementableEvent的函数不会)。如下列函数:

1
2
3
4
5
UFUNCTION()
bool ReflexFunc(int32 InIval, UObject* InObj)
{
return false;
}

生成的Thunk函数形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// generated.h
DECLARE_FUNCTION(execReflexFunc);

// gen.cpp
DEFINE_FUNCTION(AMyActor::execReflexFunc)
{
P_GET_PROPERTY(FIntProperty,Z_Param_InIval);
P_GET_OBJECT(UObject,Z_Param_InObj);
P_FINISH;
P_NATIVE_BEGIN;
*(bool*)Z_Param__Result=P_THIS->ReflexFunc(Z_Param_InIval,Z_Param_InObj);
P_NATIVE_END;
}

DECLARE_FUNCTION/DEFINE_FUNCTION这两个宏是定义在CoreUObject/Public/UObject/ObjectMacros.h中的:

1
2
3
4
5
// This macro is used to declare a thunk function in autogenerated boilerplate code
#define DECLARE_FUNCTION(func) static void func( UObject* Context, FFrame& Stack, RESULT_DECL )

// This macro is used to define a thunk function in autogenerated boilerplate code
#define DEFINE_FUNCTION(func) void func( UObject* Context, FFrame& Stack, RESULT_DECL )

展开这两个宏:

1
2
3
4
5
6
7
8
9
10
11
12
// generated.h
static void AMyActor::execReflexFunc( UObject* Context, FFrame& Stack, RESULT_DECL );
// gen.cpp
void AMyActor::execReflexFunc( UObject* Context, FFrame& Stack, RESULT_DECL )
{
P_GET_PROPERTY(FIntProperty,Z_Param_InIval);
P_GET_OBJECT(UObject,Z_Param_InObj);
P_FINISH;
P_NATIVE_BEGIN;
*(bool*)Z_Param__Result=P_THIS->ReflexFunc(Z_Param_InIval,Z_Param_InObj);
P_NATIVE_END;
}

可以看到,UHT为每个反射函数生成的都是一个参数一致的static成员函数,接收通用的参数,就可以用来处理所有的函数调用。

Thunk函数中用到的这些宏:

RESULT_DECL(CoreUObject/Public/UObject/Script.h):

1
2
3
4
5
//
// Blueprint VM intrinsic return value declaration.
//
#define RESULT_PARAM Z_Param__Result
#define RESULT_DECL void*const RESULT_PARAM

其他的形如P_GET_PROPERTY之类的宏,都是定义在CoreUObject/Public/UObject/ScriptMacros.h文件中的,作用就是从栈上操作参数(因为Thunk函数是通用的参数,所以要从通用的参数中获取到每个函数具体的参数,UE提供这些宏来做这些事情)。

这些Thunk函数通过UHT生成的StaticRegisterNatives*函数注册到UClass中(在GetPrivateStaticClass把该函数指针传递了进去):

1
2
3
4
5
6
7
8
9
void AMyActor::StaticRegisterNativesAMyActor()
{
UClass* Class = AMyActor::StaticClass();
static const FNameNativePtrPair Funcs[] = {
{ "BPNativeEvent", &AMyActor::execBPNativeEvent },
{ "ReflexFunc", &AMyActor::execReflexFunc },
};
FNativeFunctionRegistrar::RegisterFunctions(Class, Funcs, UE_ARRAY_COUNT(Funcs));
}

UE不会为BlueprintImplementableEvent生成Thunk函数,但是会为它生成函数的反射信息,所以也可以通过反射的信息来调用BlueprintImplementableEvent的函数。

因为`BlueprintImplementatableEventd的函数是C++提供原型不提供实现,让蓝图来进行覆写的,所以它不使用Thunk的形式调用(应该执行字节码的方式,这个暂时还没看到,有时间再来分析)。

Custom Thunk

前面讲到当给函数加了UFUNCTION标记时UHT会给我们生成对应的Thunk函数,但是有些情况下需要我们自己来写Thunk的函数,如UKismetArrayLibrary中的对Array进行操作的函数,或者UDataTableFunctionLibrary中的GetDataTableRowFromName函数。

UE提供了让我们自己实现Thunk函数的方法,在UFUNCTION中添加CustomThunk标记:

1
2
UFUNCTION(CustomThunk)
bool ReflexFunc(int32 InIval, UObject* InObj)

这样UHT就不会为这个函数生成出它的Thunk函数,这种情况下就需要自己提供了。自己写的方式和UHT生成的代码一样,可以使用DECLARE_FUNCTION或者DEFINE_FUNCTION(手动写按照Thunk函数的签名规则也是没问题的)

1
2
3
4
DECLARE_FUNCTION(execReflexFunc)
{
// do something...
}

运行时访问反射函数

1
2
3
4
5
6
7
8
9
10
11
12
for (TFieldIterator<UFunction> It(InActor->GetClass()); It; ++It)
{
UFunction* FuncProperty = *It;
if (FuncProperty->GetName() == TEXT("GetIval"))
{
struct CallParam
{
int32 ival;
}CallParamIns;
InActor->ProcessEvent(FuncProperty, &CallParamIns);
}
}

通过ProcessEvent来调用,第二个参数传递进去参数和返回值的结构。

每个被反射的函数UHT都会给它生成一个参数的结构体,其排列的顺序为:函数参数依次排列,最后一个成员为返回值。如:

1
2
UFUNCTION()
int32 Add(int32 R, int32 L);

UHT为其生成的参数结构为:

1
2
3
4
5
6
struct MyActor_eventAdd_Parms
{
int32 R;
int32 L;
int32 ReturnValue;
};

在通过UFunction*来调用函数时,需要把这个布局的结构传递作为传递给函数的参数以及接收返回值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for (TFieldIterator<UFunction> It(InActor->GetClass()); It; ++It)
{
UFunction* Property = *It;
if (Property->GetName() == TEXT("Add"))
{
struct AddFuncCallParam
{
int32 R;
int32 L;
int32 RetValue;
}CallParamIns;
CallParamIns.R = 123;
CallParamIns.L = 456;
InActor->ProcessEvent(Property, &CallParamIns);
UE_LOG(LogTemp, Log, TEXT("UFunction:%s value:%d"), *Property->GetName(), CallParamIns.RetValue);
}
}

可以通过UFunction拿到当前函数的参数的结构的大小UFunction::ParmsSize,在运行时动态访问的话可以通过这个结构大小分配出一块内存,然后用UProperty对这块内存进行访问,因为通过UProperty访问成员其实本质上也是通过该成员在类内的偏移来做的(对数据成员获取成员指针得到的是一个相对于对象基址的偏移值)。

坑点

注意:通过UE的UFunction调用并不能正确地处理引用类型,如:

1
2
UFUNCTION()
int32& Add(int32 R, int32& L);

这个函数生成的反射代码和非引用的一摸一样(对L参数生成的UProperty的Flag会多一个CPF_OutParm,返回值的UProperty还具有CPF_ReturnParm)。
这会造成通过UFunction*调用传递的参数和想要获取的返回值都只是一份拷贝(因为本来调用时的参数传递到ProcessEvent之前都会被赋值到UHT创建出来的参数结构),不能再后续的流程中对得到的结果进行赋值。

而且,通过遍历UFunction得到的参数和返回值UProperty,其中的Offset值是相对于UHT生成的参数结构。

在获取蓝图或者C++中具有多个返回值UFunction的时候,因为UE的具有多个返回值的机制是通过传递进来引用实现的,所以不能够只是通过检测UProperty是否具有CPF_ReturnValue来检测,因为包含该flag的UProperty只有一个,还需要检测CPF_OutParam来判断是都是通过引用方式传递的“返回值”。

属性反射

当在UCLASS类中给一个属性添加UPROPERTY标记时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
UCLASS()
class MICROEND_423_API AMyActor : public AActor
{
GENERATED_BODY()
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;

public:
// Called every frame
virtual void Tick(float DeltaTime) override;

UPROPERTY(EditAnywhere)
int32 ival;
};

会生成反射代码,生成反射代码的代码是在UHT中的Programs/UnrealHeaderTool/Private/CodeGenerator.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#if WITH_METADATA
const UE4CodeGen_Private::FMetaDataPairParam Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData[] = {
{ "Category", "MyActor" },
{ "ModuleRelativePath", "Public/MyActor.h" },
};
#endif
const UE4CodeGen_Private::FIntPropertyParams Z_Construct_UClass_AMyActor_Statics::NewProp_ival = {
"ival",
nullptr,
(EPropertyFlags) 0x0010000000000001,
UE4CodeGen_Private::EPropertyGenFlags::Int,
RF_Public | RF_Transient | RF_MarkAsNative,
1,
STRUCT_OFFSET(AMyActor, ival),
METADATA_PARAMS(Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData, ARRAY_COUNT(Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData))
};

首先const UE4CodeGen_Private::FIntPropertyParams Z_Construct_UClass_AMyActor_Statics::NewProp_ival是创建出一个结构,用于记录当前属性的信息,比如变量名、相对于对象起始地址的偏移。

注意UE4CodeGen_Private::FIntPropertyParams这样的类型都是定义在UObject/UObjectGlobals.h中的,对于基础类型的属性(如int8/int32/float/array/map)等使用的都是FGenericPropertyParams

F*PropertyParams

UObject/UObjectGlobals.h文件中,引擎里定义了很多的F*PropertyParams,但是他们的数据结构基本相同(但是他们并没有继承关系),都是POD的类型,每个数据依次排列,而且不同类型的Params尽量都保持了一致的顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// UObject/UObjectGlobals.h
struct FGenericPropertyParams // : FPropertyParamsBaseWithOffset
{
const char* NameUTF8;
const char* RepNotifyFuncUTF8;
EPropertyFlags PropertyFlags;
EPropertyGenFlags Flags;
EObjectFlags ObjectFlags;
int32 ArrayDim;
int32 Offset;
#if WITH_METADATA
const FMetaDataPairParam* Z_Construct_UClass_;
int32 NumMetaData;
#endif
};

一个一个来分析它的参数。

NameUTF8

NameUTF8是属性的UTF8的名字,在运行时可以通过UPropertyGetNameCPP来获取。

RepNotifyFuncUTF8

RepNotifyFuncUTF8是在当前属性绑定的修改之后的函数名字。

如果属性是这样声明:

1
2
3
4
UPROPERTY(EditAnywhere,ReplicatedUsing=OnRep_Replicatedival)
int32 ival;
UFUNCTION()
virtual void OnRep_Replicatedival() {}

这样生成的反射代码就为:

1
2
3
4
5
6
7
8
9
10
const UE4CodeGen_Private::FIntPropertyParams Z_Construct_UClass_AMyActor_Statics::NewProp_ival = { 
"ival",
"OnRep_Replicatedival",
(EPropertyFlags)0x0010000100000021,
UE4CodeGen_Private::EPropertyGenFlags::Int,
RF_Public|RF_Transient|RF_MarkAsNative,
1,
STRUCT_OFFSET(AMyActor, ival),
METADATA_PARAMS(Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData,ARRAY_COUNT(Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData))
};

因为OnRep_Replicatedival也是UFUNCTION的函数,所以可以通过反射来访问。

PropertyFlags

PropertyFlags是一个类型为EPropertyFlags的枚举,枚举值按照位排列,根据在UPROPERTY中的标记来按照位或来记录当前属性包含的标记信息。
UnrealHeaderTool/Private/CodeGenerator.cpp中生成代码时会根据当前属性的flag和CPF_ComputedFlags做位运算:

1
2
3
4
5
6
7
8
9
10
/*
// All the properties that should never be loaded or saved
#define CPF_ComputedFlags (CPF_IsPlainOldData | CPF_NoDestructor | CPF_ZeroConstructor | CPF_HasGetValueTypeHash)

0x0000000040000000 CPF_IsPlainOldData
0x0000001000000000 CPF_NoDestructor
0x0000000000000200 CPF_ZeroConstructor
0x0008000000000000 CPF_HasGetValueTypeHash
*/
EPropertyFlags PropFlags = Prop->PropertyFlags & ~CPF_ComputedFlags;

在运行时可以通过UPropertyHasAnyPropertyFlags函数来检测是否具有特定的flag。

一般情况下,可以通过UProperty来获取到该参数的标记属性,比如当通过UFunction获取函数的参数时,可以区分哪个UProperty是输入参数、哪个是返回值。

Flags

第四个参数Flags是类型为UE4CodeGen_Private::EPropertyGenFlags是一个枚举,定义在UObject/UObjectGlobals.h中,标记了当前Property的类型。

ObjectFlags

ObjectFlags是EObjectFlags类型的枚举,其枚举值也是按照位来划分的。定义在CoreUObject/Public/UObject/ObjectMacros.h

UHT生成这部分代码在Programs/UnrealHeaderTool/Private/CodeGenerator.cpp中:

1
const TCHAR*   FPropertyObjectFlags = FClass::IsOwnedByDynamicType(Prop) ? TEXT("RF_Public|RF_Transient") : TEXT("RF_Public|RF_Transient|RF_MarkAsNative");

通过FClass::IsOwnedByDynamicType函数来检测是否为PROPERTY添加RF_MarkAsNative的flag。

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
// Programs/UnrealHeaderTool/Public/ParserClass.h

/** Helper function that checks if the field is a dynamic type (can be constructed post-startup) */
template <typename T>
static bool IsDynamic(const T* Field)
{
return Field->HasMetaData(NAME_ReplaceConverted);
}

// Programs/UnrealHeaderTool/Private/ParserClass.cpp
bool FClass::IsOwnedByDynamicType(const UField* Field)
{
for (const UField* OuterField = Cast<const UField>(Field->GetOuter()); OuterField; OuterField = Cast<const UField>(OuterField->GetOuter()))
{
if (IsDynamic(OuterField))
{
return true;
}
}
return false;
}

bool FClass::IsOwnedByDynamicType(const FField* Field)
{
for (FFieldVariant Owner = Field->GetOwnerVariant(); Owner.IsValid(); Owner = Owner.GetOwnerVariant())
{
if (Owner.IsUObject())
{
return IsOwnedByDynamicType(Cast<const UField>(Owner.ToUObject()));
}
else if (IsDynamic(Owner.ToField()))
{
return true;
}
}
return false;
}

通过调用UFieldHasMetaData检测是否具有TEXT("ReplaceConverted")的元数据(该元数据就是后面要讲到的Metadata)。

ArrayDim

ArrayDim用于记录当前属性的元素数量,当只是声明一个单个对象时,如:

1
2
3
4
5
6
7
8
9
10
11
12
UPROPERTY()
float fval;
UPROPERTY(EditAnywhere)
FString StrVal = TEXT("123456");
UPROPERTY(EditAnywhere)
TSubclassOf<UObject> ClassVal;
UPROPERTY(EditAnywhere)
UTexture2D* Texture2D;
UPROPERTY()
FResultDyDlg ResultDlg;
UPROPERTY()
UMySceneComponent* SceneComp;

这些属性所有的ArrayDim均为1.

但是当使用C++原生数组时:

1
2
UPROPERTY()
int32 iArray[12];

它的ArrayDim就为:

1
CPP_ARRAY_DIM(iArray, AMyActor)

CPP_ARRAY_DIM这个宏定义在CoreUObject/Public/UObject/UnrealType.h

1
2
3
/** helper to calculate an array's dimensions **/
#define CPP_ARRAY_DIM(ArrayName, ClassName) \
(sizeof(((ClassName*)0)->ArrayName) / sizeof(((ClassName*)0)->ArrayName[0]))

就是用来计算数组内的元素数量的,普通的非数组属性其值为1,可以当作是元素数量为1的数组。

Offset

STRUCT_OFFSET宏的作用为得到数据成员相对于类起始地址的偏移,通过获取数据成员指针得到,类型为size_t

然后当前类中的反射属性都会被添加到PropPointers中,也是UHT生成的代码:

1
2
3
const UE4CodeGen_Private::FPropertyParamsBase* const Z_Construct_UClass_AMyActor_Statics::PropPointers[] = {
(const UE4CodeGen_Private::FPropertyParamsBase*)&Z_Construct_UClass_AMyActor_Statics::NewProp_ival,
};

在运行时可以通过UProperty得到指定的对象值:

1
2
3
4
5
6
7
8
9
for (TFieldIterator<UProperty> It(InActor->GetClass()); It; ++It)
{
UProperty* Property = *It;
if (Property->GetNameCPP() == FString("ival"))
{
int32* i32 = Property->ContainerPtrToValuePtr<int32>(InActor);
UE_LOG(LogTemp, Log, TEXT("Property:%s value:%d"), *Property->GetNameCPP(),i32);
}
}

其中UProperty中的ContainerPtrToValuePtr系列函数都会转发到ContainerVoidPtrToValuePtrInternal

1
2
3
4
5
6
7
8
9
10
11
FORCEINLINE void* ContainerVoidPtrToValuePtrInternal(void* ContainerPtr, int32 ArrayIndex) const
{
check(ArrayIndex < ArrayDim);
check(ContainerPtr);
if (0)
{
// in the future, these checks will be tested if the property is NOT relative to a UClass
check(!Cast<UClass>(GetOuter())); // Check we are _not_ calling this on a direct child property of a UClass, you should pass in a UObject* in that case
}
return (uint8*)ContainerPtr + Offset_Internal + ElementSize * ArrayIndex;
}

其实就是拿到UObject的指针,然后偏移到指定位置(第二个参数用在对象是数组的情况,用来访问指定下标的元素,默认情况下访问下标为0)。

Z_Construct_UClass_和NumMetaData

它们的类型分别为FMetaDataPairParamint32,用来记录当前反射属性的元数据:比如属性的Category、注释、所属的文件、ToolTip信息等等,比如在C++函数上添加的注释能够在编辑器蓝图中看到注释的信息,都是靠解析这些元数据来实现的。

1
2
3
4
5
6
7
8
// CoreUObject/Public/UObject/UObjectGlobals.h
#if WITH_METADATA
struct FMetaDataPairParam
{
const char* NameUTF8;
const char* ValueUTF8;
};
#endif

这两个参数通过METADATA_PARAMS包裹,用于处理WITH_MATEDATA的不同情况:

1
2
3
4
5
6
// METADATA_PARAMS(x, y) expands to x, y, if WITH_METADATA is set, otherwise expands to nothing
#if WITH_METADATA
#define METADATA_PARAMS(x, y) x, y,
#else
#define METADATA_PARAMS(x, y)
#endif

把UHT生成的代码宏展开为:

1
2
3
4
5
6
7
8
#if WITH_METADATA
const UE4CodeGen_Private::FMetaDataPairParam Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData[] = {
{ "Category", "MyActor" },
{ "ModuleRelativePath", "Public/MyActor.h" }
};
#endif

METADATA_PARAMS(Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData, ARRAY_COUNT(Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData))

就是把UE4CodeGen_Private::FMetaDataPairParam这个类型的数组和数组元素个数传递给F*PropertyParams,实现在WITH_METADATA的情况下处理是否具有Metadata的情况。

运行时的Property

引擎中通过UE4CodeGen_Private::ConstructFProperty来创建出真正Runtime使用的UProperty(4.25之后是FProperty),定义在UObject/UObjectGlobals.cpp

FBoolPropertyParams特例

当一个反射的数据是bool类型时,引擎产生的反射信息中有一个比较有意思的特例,FBoolPropertyParams,可以看一下它的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct FBoolPropertyParams // : FPropertyParamsBase
{
const char* NameUTF8;
const char* RepNotifyFuncUTF8;
EPropertyFlags PropertyFlags;
EPropertyGenFlags Flags;
EObjectFlags ObjectFlags;
int32 ArrayDim;
uint32 ElementSize;
SIZE_T SizeOfOuter;
void (*SetBitFunc)(void* Obj);
#if WITH_METADATA
const FMetaDataPairParam* MetaDataArray;
int32 NumMetaData;
#endif
};

它具有一个SetBitFunc的函数指针。看一下生成的反射代码:

1
2
3
4
5
6
7
8
9
10
// source code 
UPROPERTY()
bool bEnabled;

// gen.cpp
void Z_Construct_UClass_AMyActor_Statics::NewProp_bEnabled_SetBit(void* Obj)
{
((AMyActor*)Obj)->bEnabled = 1;
}
const UE4CodeGen_Private::FBoolPropertyParams Z_Construct_UClass_AMyActor_Statics::NewProp_bEnabled = { "bEnabled", nullptr, (EPropertyFlags)0x0010000000000000, UE4CodeGen_Private::EPropertyGenFlags::Bool | UE4CodeGen_Private::EPropertyGenFlags::NativeBool, RF_Public|RF_Transient|RF_MarkAsNative, 1, sizeof(bool), sizeof(AMyActor), &Z_Construct_UClass_AMyActor_Statics::NewProp_bEnabled_SetBit, METADATA_PARAMS(Z_Construct_UClass_AMyActor_Statics::NewProp_bEnabled_MetaData, ARRAY_COUNT(Z_Construct_UClass_AMyActor_Statics::NewProp_bEnabled_MetaData)) };

注意:其中的关键是,UHT给该属性生成了一个SetBit的函数,why?其他类型的属性都可以通过STRUCT_OFFSET来获取该成员的类内偏移,为什么bool就不行了呢?

这是因为C++有位域(bit-field)这个概念,一个bool可能只占1bit,而不是1byte,但这不是真正的原因。

真正的原因是,C++标准规定了不能对位域进行取地址操作!前面已经提到了STRUCT_OFFSET实际上是获取到数据成员的指针,得到的是类内偏移,但是因为C++的不能对位域取地址的规定,STRUCT_OFFSET无法用在位域的成员上的。

[IOS/IEC 14882:2014 §9.6]The address-of operator & shall not be applied to a bit-field, so there are no pointers to bit-fields.

那么这又是因为什么呢?因为系统编址的最小单位是字节而不是位,所以没办法取到1字节零几位的地址。也就决定了不能对位域的数据成员取地址。

UE内其实大量用到了bool使用位域的方式来声明(如果不使用位域,bool类型的空间浪费率达到87.5% :)),所以UE就生成了一个函数来为以位域方式声明的成员设置值。

但是!UE不支持直接对加了UPROPERTY的bool使用位域:

1
2
UPROPERTY()
bool bEnabled:1;

编译时会有下列错误:

LogCompile: Error: bool bitfields are not supported.

要写成下列方式:

1
2
UPROPERTY()
uint8 bEnabled:1;

使用这种方式和使用bool bEnabled;方式生成的反射代码一模一样,所以,UE之所以会生成一个函数来设置bool的值,是因为既要支持原生bool,也要支持位域。

通过UProperty获取值

如果我知道某个类的对象内有一个属性名字,那么怎么能够得到它的值呢?这个可以基于UE的属性反射来实现:

首先通过TFieldIterator可以遍历该对象的UProperty:

1
2
3
4
for (TFieldIterator<UProperty> It(InActor->GetClass()); It; ++It)
{
UProperty* Property = *It;
}

然后可以根据得到的Property来判读名字:

1
if (Property->GetNameCPP() == FString("ival"))

检测是指定名字的Property后可以通过UProperty上的ContainerPtrToValuePtr函数来获取对象内该属性的指针:

1
int32* i32 = Property->ContainerPtrToValuePtr<int32>(InActor)

前面讲到过,UPropery里存储Offdet值就是当前属性相对于对象起始地址的偏移。而ContainerPtrToValuePtr函数所做的就是得到当前对象偏移Offset的地址然后做了类型转换。

Property的Flag

通过上面的分析,可以看到UPROPERTY添加的标记,如EditAnywhere等,会给指定的Property生成FLAG存储在F*PropertyParams结构的第三个参数中,是位描述的。

可选值为EPropertyFlags枚举值:

Runtime/CoreUObject/Public/UObject/ObjectMacros.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
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
/**
* Flags associated with each property in a class, overriding the
* property's default behavior.
* @warning When adding one here, please update ParsePropertyFlags()
*/
enum EPropertyFlags : uint64
{
CPF_None = 0,

CPF_Edit = 0x0000000000000001, ///< Property is user-settable in the editor.
CPF_ConstParm = 0x0000000000000002, ///< This is a constant function parameter
CPF_BlueprintVisible = 0x0000000000000004, ///< This property can be read by blueprint code
CPF_ExportObject = 0x0000000000000008, ///< Object can be exported with actor.
CPF_BlueprintReadOnly = 0x0000000000000010, ///< This property cannot be modified by blueprint code
CPF_Net = 0x0000000000000020, ///< Property is relevant to network replication.
CPF_EditFixedSize = 0x0000000000000040, ///< Indicates that elements of an array can be modified, but its size cannot be changed.
CPF_Parm = 0x0000000000000080, ///< Function/When call parameter.
CPF_OutParm = 0x0000000000000100, ///< Value is copied out after function call.
CPF_ZeroConstructor = 0x0000000000000200, ///< memset is fine for construction
CPF_ReturnParm = 0x0000000000000400, ///< Return value.
CPF_DisableEditOnTemplate = 0x0000000000000800, ///< Disable editing of this property on an archetype/sub-blueprint
//CPF_ = 0x0000000000001000, ///<
CPF_Transient = 0x0000000000002000, ///< Property is transient: shouldn't be saved or loaded, except for Blueprint CDOs.
CPF_Config = 0x0000000000004000, ///< Property should be loaded/saved as permanent profile.
//CPF_ = 0x0000000000008000, ///<
CPF_DisableEditOnInstance = 0x0000000000010000, ///< Disable editing on an instance of this class
CPF_EditConst = 0x0000000000020000, ///< Property is uneditable in the editor.
CPF_GlobalConfig = 0x0000000000040000, ///< Load config from base class, not subclass.
CPF_InstancedReference = 0x0000000000080000, ///< Property is a component references.
//CPF_ = 0x0000000000100000, ///<
CPF_DuplicateTransient = 0x0000000000200000, ///< Property should always be reset to the default value during any type of duplication (copy/paste, binary duplication, etc.)
CPF_SubobjectReference = 0x0000000000400000, ///< Property contains subobject references (TSubobjectPtr)
//CPF_ = 0x0000000000800000, ///<
CPF_SaveGame = 0x0000000001000000, ///< Property should be serialized for save games, this is only checked for game-specific archives with ArIsSaveGame
CPF_NoClear = 0x0000000002000000, ///< Hide clear (and browse) button.
//CPF_ = 0x0000000004000000, ///<
CPF_ReferenceParm = 0x0000000008000000, ///< Value is passed by reference; CPF_OutParam and CPF_Param should also be set.
CPF_BlueprintAssignable = 0x0000000010000000, ///< MC Delegates only. Property should be exposed for assigning in blueprint code
CPF_Deprecated = 0x0000000020000000, ///< Property is deprecated. Read it from an archive, but don't save it.
CPF_IsPlainOldData = 0x0000000040000000, ///< If this is set, then the property can be memcopied instead of CopyCompleteValue / CopySingleValue
CPF_RepSkip = 0x0000000080000000, ///< Not replicated. For non replicated properties in replicated structs
CPF_RepNotify = 0x0000000100000000, ///< Notify actors when a property is replicated
CPF_Interp = 0x0000000200000000, ///< interpolatable property for use with matinee
CPF_NonTransactional = 0x0000000400000000, ///< Property isn't transacted
CPF_EditorOnly = 0x0000000800000000, ///< Property should only be loaded in the editor
CPF_NoDestructor = 0x0000001000000000, ///< No destructor
//CPF_ = 0x0000002000000000, ///<
CPF_AutoWeak = 0x0000004000000000, ///< Only used for weak pointers, means the export type is autoweak
CPF_ContainsInstancedReference = 0x0000008000000000, ///< Property contains component references.
CPF_AssetRegistrySearchable = 0x0000010000000000, ///< asset instances will add properties with this flag to the asset registry automatically
CPF_SimpleDisplay = 0x0000020000000000, ///< The property is visible by default in the editor details view
CPF_AdvancedDisplay = 0x0000040000000000, ///< The property is advanced and not visible by default in the editor details view
CPF_Protected = 0x0000080000000000, ///< property is protected from the perspective of script
CPF_BlueprintCallable = 0x0000100000000000, ///< MC Delegates only. Property should be exposed for calling in blueprint code
CPF_BlueprintAuthorityOnly = 0x0000200000000000, ///< MC Delegates only. This delegate accepts (only in blueprint) only events with BlueprintAuthorityOnly.
CPF_TextExportTransient = 0x0000400000000000, ///< Property shouldn't be exported to text format (e.g. copy/paste)
CPF_NonPIEDuplicateTransient = 0x0000800000000000, ///< Property should only be copied in PIE
CPF_ExposeOnSpawn = 0x0001000000000000, ///< Property is exposed on spawn
CPF_PersistentInstance = 0x0002000000000000, ///< A object referenced by the property is duplicated like a component. (Each actor should have an own instance.)
CPF_UObjectWrapper = 0x0004000000000000, ///< Property was parsed as a wrapper class like TSubclassOf<T>, FScriptInterface etc., rather than a USomething*
CPF_HasGetValueTypeHash = 0x0008000000000000, ///< This property can generate a meaningful hash value.
CPF_NativeAccessSpecifierPublic = 0x0010000000000000, ///< Public native access specifier
CPF_NativeAccessSpecifierProtected = 0x0020000000000000, ///< Protected native access specifier
CPF_NativeAccessSpecifierPrivate = 0x0040000000000000, ///< Private native access specifier
CPF_SkipSerialization = 0x0080000000000000, ///< Property shouldn't be serialized, can still be exported to text
};

StaticClass和GetClass的区别

StaticClass是继承自UObject类的static函数,GetClassUObjectBase的成员函数。

UObjectBaseGetClass获取到的UClass就是在NewObject时传递进来的UClass.(代码在UObject\UObjectGlobal.cpp中)

用途不一样,StaticClass是在获取具体类型的UClass,而GetClass是获取到当前对象的真实UClass。

UObject的FObjectInitializer构造函数的调用

注意:只有GENERATER_UCLASS_BODY才可以实现FObjectInitializer的构造函数。

在继承自UObject的类中,都可以自己写一个接收const FObjectInitializer&参数的构造函数,在创建对象时会被调用:

1
UMyObject::UMyObject(const FObjectInitializer& Initializer){}

在类中的GENERATED_UCLASS_BODY中默认声明这样一个构造函数。并且,在UHT生成的代码中通过宏还定义了一个函数:

1
DEFINE_DEFAULT_OBJECT_INITIALIZER_CONSTRUCTOR_CALL(UMyObject) 

这个宏经过展开之后就是这样的:

1
static void __DefaultConstructor(const FObjectInitializer& X) { new((EInternal*)X.GetObj())UMyObject(X); }

为当前的对象类型UMyObject定义了一个__DefaultConstructor函数,作用是在当前FObjectInitializer传入的参数的Object上调用FObjectInitializer的构造函数。

注意:如果是GENERATED_BODY则不会声明这个构造函数,使用的就是另一个宏:

1
DEFINE_DEFAULT_CONSTRUCTOR_CALL(UMyObject)

展开之后是这样的:

1
static void __DefaultConstructor(const FObjectInitializer& X) { new((EInternal*)X.GetObj())UMyObject; }

是通过GANERATED_BODYGENERATED_UCLASS_BODY来定义了两种__DefaultConstructor的实现,一种是调用FObjectInitializer的构造函数,另一种是调用类的默认构造函数。

所以,默认情况下FObjectInitializer的构造函数和UObject的默认构造函数在调用时只会走一个。

那么它是如何被调用到的呢?

NewObject中调用的StaticConstructObject_Internal中有以下代码:

1
2
3
4
5
6
7
8
9
bool bRecycledSubobject = false;	
Result = StaticAllocateObject(InClass, InOuter, InName, InFlags, InternalSetFlags, bCanRecycleSubobjects, &bRecycledSubobject);
check(Result != NULL);
// Don't call the constructor on recycled subobjects, they haven't been destroyed.
if (!bRecycledSubobject)
{
STAT(FScopeCycleCounterUObject ConstructorScope(InClass, GET_STATID(STAT_ConstructObject)));
(*InClass->ClassConstructor)( FObjectInitializer(Result, InTemplate, bCopyTransientsFromClassDefaults, true, InInstanceGraph) );
}

通过传入进来的UClass对象获取其中的ClassConstructor函数指针,并构造出一个FObjectInitializer作为参数传递。

在之前的笔记(StaticClass是如何定义的)中,写到了,通过GetPrivateStaticClass获取到当前UObject类的UClass实例会实例化出一个当前类的InternalConstructor函数(注意这个模板参数T是UObject的类):

1
2
3
4
5
6
7
8
// class.h

// Helper template to call the default constructor for a class
template<class T>
void InternalConstructor( const FObjectInitializer& X )
{
T::__DefaultConstructor(X);
}

就将其转发到了上面讲到的__DefaultConstructor函数上,然后它里面又转发到了所传入FObjectInitializer对象上的const FObjectInitializer&构造函数上(或者默认构造函数上)。

创建对象并调用const FObjectInitializer&构造函数的调用流程为:

  1. 通过调用StaticAllocateObject根据传入的UClass创建出对象
  2. 通过传入的UClass对象内的ClassConstructor函数指针调用所创建UObject类的InternalConstructor函数
  3. UMyObject::InternalConstructor会转发到UMyObject::__DefaultConstructor
  4. UMyObject::__DefaultConstructor会从接收到的FObjectInitializer对象上获取到通过StaticAllocateObject创建的对象,然后通过placement-new的方式,在这块内存上调用UMyObject类的const FObjectInitializer&构造函数。

通过以上的流程就实现了NewObject时会自动调用到它的FObjectInitializer构造函数。

注意:在CDO的构造上有点区别,CDO的构造是通过UClass::GetDefaultObject中实现上述流程的。

StaticClass是如何定义的

在UE中可以对UObject的类执行UXXX::StaticClass方法来获取到它的UClass对象。

但是它是如何定义的?首先要看我们声明对象的MyActor.generated.h中(以AMyActor类为例):

1
template<> MICROEND_423_API UClass* StaticClass<class AMyActor>();

为AMyActor类特化了StaticClass的版本。再去MyActor.gen.cpp中找以下它的实现:

1
2
3
4
5
IMPLEMENT_CLASS(AMyActor, 31943282);
template<> MICROEND_423_API UClass* StaticClass<AMyActor>()
{
return AMyActor::StaticClass();
}

从这里看也就只是转发调用而已,但是关键点隐藏在其他地方。
首先AMyActor::StaticClass类的定义是在AMyActor.generated.hDECLARE_CLASS宏中(该宏定义在ObjectMacros.h#L1524),返回的是GetPrivateStaticClass的调用。

GetPrivateStaticClass则在AMyAcotr.gen.cpp中的IMPLEMENT_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
// Runtime/CoreUObject/Public/UObject/ObjectMacros.h
// Register a class at startup time.
#define IMPLEMENT_CLASS(TClass, TClassCrc) \
static TClassCompiledInDefer<TClass> AutoInitialize##TClass(TEXT(#TClass), sizeof(TClass), TClassCrc); \
UClass* TClass::GetPrivateStaticClass() \
{ \
static UClass* PrivateStaticClass = NULL; \
if (!PrivateStaticClass) \
{ \
/* this could be handled with templates, but we want it external to avoid code bloat */ \
GetPrivateStaticClassBody( \
StaticPackage(), \
(TCHAR*)TEXT(#TClass) + 1 + ((StaticClassFlags & CLASS_Deprecated) ? 11 : 0), \
PrivateStaticClass, \
StaticRegisterNatives##TClass, \
sizeof(TClass), \
alignof(TClass), \
(EClassFlags)TClass::StaticClassFlags, \
TClass::StaticClassCastFlags(), \
TClass::StaticConfigName(), \
(UClass::ClassConstructorType)InternalConstructor<TClass>, \
(UClass::ClassVTableHelperCtorCallerType)InternalVTableHelperCtorCaller<TClass>, \
&TClass::AddReferencedObjects, \
&TClass::Super::StaticClass, \
&TClass::WithinClass::StaticClass \
); \
} \
return PrivateStaticClass; \
}

可以看到GetPrivateStaticClass其实就是通过这些元数据构造出UClass的。

如下面的代码:

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
IMPLEMENT_CLASS(AMyActor, 3240835608);

// 宏展开之后
static TClassCompiledInDefer < AMyActor > AutoInitializeAMyActor(TEXT("AMyActor"), sizeof(AMyActor), 3240835608);
UClass * AMyActor::GetPrivateStaticClass() {
static UClass * PrivateStaticClass = NULL;
if (!PrivateStaticClass) {
GetPrivateStaticClassBody(
StaticPackage(),
(TCHAR*)TEXT("AMyActor") + 1 + ((StaticClassFlags & CLASS_Deprecated) ? 11 : 0),
PrivateStaticClass,
StaticRegisterNativesAMyActor,
sizeof(AMyActor),
alignof(AMyActor),
(EClassFlags) AMyActor::StaticClassFlags,
AMyActor::StaticClassCastFlags(),
AMyActor::StaticConfigName(),
(UClass::ClassConstructorType) InternalConstructor<AMyActor>,
(UClass::ClassVTableHelperCtorCallerType) InternalVTableHelperCtorCaller<AMyActor>,
&AMyActor::AddReferencedObjects,
&AMyActor::Super::StaticClass,
&AMyActor::WithinClass::StaticClass
);
}
return PrivateStaticClass;
};

UClass中的函数指针

上面代码中比较关键的点为:

1
2
(UClass::ClassConstructorType)InternalConstructor<TClass>, \
(UClass::ClassVTableHelperCtorCallerType)InternalVTableHelperCtorCaller<TClass>, \

这两行是模板实例化出了两个函数并转换成函数指针传递给GetPrivateStaticClassBody

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// class.h

// Helper template to call the default constructor for a class
template<class T>
void InternalConstructor( const FObjectInitializer& X )
{
T::__DefaultConstructor(X);
}

// Helper template to call the vtable ctor caller for a class
template<class T>
UObject* InternalVTableHelperCtorCaller(FVTableHelper& Helper)
{
return T::__VTableCtorCaller(Helper);
}

就是对__DefaultConstructor这样函数的的转发调用。

UClass::ClassConstructorTypeUClass::ClassVTableHelperCtorCallerType这两个typedef为:

1
2
typedef void		(*ClassConstructorType)				(const FObjectInitializer&);
typedef UObject* (*ClassVTableHelperCtorCallerType) (FVTableHelper& Helper);

GetPrivateStaticClassBody

其中的GetPrivateStaticClassBody函数是定义在Runtime\CoreUObject\Private\UObject\Class.cpp中的。

原型为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void GetPrivateStaticClassBody(
const TCHAR* PackageName,
const TCHAR* Name,
UClass*& ReturnClass,
void(*RegisterNativeFunc)(),
uint32 InSize,
uint32 InAlignment,
EClassFlags InClassFlags,
EClassCastFlags InClassCastFlags,
const TCHAR* InConfigName,
UClass::ClassConstructorType InClassConstructor,
UClass::ClassVTableHelperCtorCallerType InClassVTableHelperCtorCaller,
UClass::ClassAddReferencedObjectsType InClassAddReferencedObjects,
UClass::StaticClassFunctionType InSuperClassFn,
UClass::StaticClassFunctionType InWithinClassFn,
bool bIsDynamic /*= false*/
);

其中第四个参数是传入注册Native函数的函数指针,该函数在MyActor.gen.cpp中生成,也可以通过在UFUNCTION中添加CustomThunk函数来自己实现,UnLua的覆写C++函数就是基于替换thunk函数做的。

GetPrivateStaticClassBody中通过(UClass*)GUObjectAllocator.AllocateUObject来分配出UClass的内存,因为所有的UClass结构都一致。

1
2
3
4
5
6
7
8
9
10
// MyActor.gen.cpp
void AMyActor::StaticRegisterNativesAMyActor()
{
UClass* Class = AMyActor::StaticClass();
static const FNameNativePtrPair Funcs[] = {
{ "ReceiveBytes", &AMyActor::execReceiveBytes },
{ "TESTFUNC", &AMyActor::execTESTFUNC },
};
FNativeFunctionRegistrar::RegisterFunctions(Class, Funcs, ARRAY_COUNT(Funcs));
}

其实就是把Native的函数通过AddNativeFunction添加到UClass中:

1
2
3
4
5
6
7
8
// Runtime\CoreUObject\Private\UObject\Class.cpp 
void FNativeFunctionRegistrar::RegisterFunctions(class UClass* Class, const FNameNativePtrPair* InArray, int32 NumFunctions)
{
for (; NumFunctions; ++InArray, --NumFunctions)
{
Class->AddNativeFunction(UTF8_TO_TCHAR(InArray->NameUTF8), InArray->Pointer);
}
}

获取UObject的资源路径

可以通过FSoftObjectPath传入UObject来获得:

1
2
3
4
5
FString GetObjectResource(UObject* Obj)
{
FSoftObjectPath SoftRef(Obj);
return SoftRef.ToString();
}

注意:直接ToString获取到的路径是PackagePath的,形如/Game/XXXX.XXXX这种形式,可以通过GetLongPackageName得到去掉.XXXX的字符串。

BP to CPP

Project Settings-Packaging-Blueprint下添加想要转换的蓝图资源:

设置之后执行打包就会在项目的下列路径中产生对应的.h/.cpp以及生成generated.h/gen.cpp

1
Intermediate\Plugins\NativizedAssets\Windows\Game\Intermediate\Build\Win64\UE4\Inc\NativizedAssets

监听窗口关闭

可以通过监听FSlateFontServices里的OnSlateWindowDestroyed:

1
2
3
4
5
6
7
/**
* Called on the game thread right before the slate window handle is destroyed.
* This gives users a chance to release any viewport specific resources they may have active when the window is destroyed
* @param Pointer to the API specific backbuffer type
*/
DECLARE_MULTICAST_DELEGATE_OneParam(FOnSlateWindowDestroyed, void*);
FOnSlateWindowDestroyed& OnSlateWindowDestroyed() { return OnSlateWindowDestroyedDelegate; }

监听方法:

1
FSlateApplication::Get().GetRenderer()->OnSlateWindowDestroyed().AddRaw(this, &FSceneViewport::OnWindowBackBufferResourceDestroyed);

UnLua的EXPORT_PRIMITIVE_TYPE

UnLua里使用EXPORT_PRIMITIVE_TYPE宏来导出内置类型:

1
EXPORT_PRIMITIVE_TYPE(uint64, TPrimitiveTypeWrapper<uint64>, uint64)

宏展开之后为:

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
template < > struct UnLua::TType < TPrimitiveTypeWrapper < uint64 > , false > {
static
const char * GetName() {
return "uint64";
}
};
struct FExporteduint64Helper {
typedef TPrimitiveTypeWrapper < uint64 > ClassType;
static FExporteduint64Helper StaticInstance;
UnLua::TExportedClass < false, TPrimitiveTypeWrapper < uint64 > , uint64 > * ExportedClass;
~FExporteduint64Helper() {
delete ExportedClass;
}
FExporteduint64Helper(): ExportedClass(nullptr) {
UnLua::TExportedClass < false, TPrimitiveTypeWrapper < uint64 > , uint64 > * Class = (UnLua::TExportedClass < false, TPrimitiveTypeWrapper < uint64 > , uint64 > * ) UnLua::FindExportedClass("uint64");
if (!Class) {
ExportedClass = new UnLua::TExportedClass < false, TPrimitiveTypeWrapper < uint64 > , uint64 > ("uint64", nullptr);
UnLua::ExportClass((UnLua::IExportedClass * ) ExportedClass);
Class = ExportedClass;
}
Class - > AddProperty("Value", & ClassType::Value);
}
};
FExporteduint64Helper FExporteduint64Helper::StaticInstance;
static struct FTypeInterfaceuint64 {
FTypeInterfaceuint64() {
UnLua::AddTypeInterface("uint64", UnLua::GetTypeInterface < uint64 > ());
}
}TypeInterfaceuint64;

4.25 MountPak没有材质

在项目打包时在Project Settgins-Packaging中开启了Share Material shader code时,后续的热更pak打包,如果没有同步把ushaderbytecode打包进去并自己加载,会产生下列材质丢失的问题:

经过调试后发现,是因为4.25在mount pak之后不会加载新的Pak中的shaderbytecode,找到了问题,解决办法就手到擒来了,找到引擎中加载shaderbytecode的代码自己调用一遍即可。

在项目打包时默认会生成两个shaderbytecode文件:

1
2
ShaderArchive-Global-PCD3D_SM5.ushaderbytecode
ShaderArchive-HotPatcherExample-PCD3D_SM5.ushaderbytecode

并且它们存在于pak中Mount point的路径均为:

1
../../../PROJECT_NAME/Content/

而且根据shaderbytecode文件路径的组成规则:

1
2
3
4
5
6
7
8
static FString ShaderExtension = TEXT(".ushaderbytecode");
static FString StableExtension = TEXT(".scl.csv");
static FString PipelineExtension = TEXT(".ushaderpipelines");

static FString GetCodeArchiveFilename(const FString& BaseDir, const FString& LibraryName, FName Platform)
{
return BaseDir / FString::Printf(TEXT("ShaderArchive-%s-"), *LibraryName) + Platform.ToString() + ShaderExtension;
}

所以,只需要传递基础路径和LibraryName即可(OpenLibrary中通过调用GetCodeArchiveFilename来获取要加载的文件)。

即要重新加载global和项目的shaderbytecode,在mount成功之后执行下面两行代码即可:

1
2
FShaderCodeLibrary::OpenLibrary("Global", FPaths::ProjectContentDir());
FShaderCodeLibrary::OpenLibrary(FApp::GetProjectName(), FPaths::ProjectContentDir());

通过笔记最前面的一段话,可以总结出两个解决方案:

  1. 开启了Share Material shader code的情况下,需要把shaderbytecode打包,并自己在Mount时加载;
  2. 打Pak时Cook资源不要开启Share Material shader code,这样会把资源的shader都打包在资源内部,从而避免需要单独加载shader的问题;

Android上Arrow组件的crash

在游戏中把一个Actor上的Arrow组件设置为visible,打包Android上运行会Crash:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
06-17 15:23:51.976 26991 27112 F libc    : Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 in tid 27112 (RenderThread 2), pid 26991 (MainThread-UE4)
06-17 15:23:52.350 27129 27129 F DEBUG : #00 pc 062fa090 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (FArrowSceneProxy::GetDynamicMeshElements(TArray<FSceneView const*, FDefaultAllocator> const&, FSceneViewFamily const&, unsigned int, FMeshElementCollector&) const+824)
06-17 15:23:52.350 27129 27129 F DEBUG : #01 pc 058010f4 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (_ZN14FSceneRenderer25GatherDynamicMeshElementsER6TArrayI9FViewInfo17FDefaultAllocatorEPK6FSceneRK16FSceneViewFamilyR25FGlobalDynamicIndexBufferR26FGlobalDynamicVertexBufferR24FGlobalDynamicReadBufferRKS0_Ih18TMemStackAllocatorILj0EEESL_SL_R21FMeshElementCollector+2456)
06-17 15:23:52.350 27129 27129 F DEBUG : #02 pc 0580f8a8 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (_ZN14FSceneRenderer21ComputeViewVisibilityER24FRHICommandListImmediateN22FExclusiveDepthStencil4TypeER6TArrayI13FViewCommands16TInlineAllocatorILj4E17FDefaultAllocatorEER25FGlobalDynamicIndexBufferR26FGlobalDynamicVertexBufferR24FGlobalDynamicReadBuffer+39716)
06-17 15:23:52.350 27129 27129 F DEBUG : #03 pc 054ffb30 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (FMobileSceneRenderer::InitViews(FRHICommandListImmediate&)+1076)
06-17 15:23:52.350 27129 27129 F DEBUG : #04 pc 05500a98 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (FMobileSceneRenderer::Render(FRHICommandListImmediate&)+1228)
06-17 15:23:52.350 27129 27129 F DEBUG : #05 pc 057fa970 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (_ZZN15FRendererModule24BeginRenderingViewFamilyEP7FCanvasP16FSceneViewFamilyENK4$_85clER24FRHICommandListImmediate+2316)
06-17 15:23:52.350 27129 27129 F DEBUG : #06 pc 057fc944 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (_ZN10TGraphTaskI31TEnqueueUniqueRenderCommandTypeIZN15FRendererModule24BeginRenderingViewFamilyEP7FCanvasP16FSceneViewFamilyE21FDrawSceneCommandNameZNS1_24BeginRenderingViewFamilyES3_S5_E4$_85EE11ExecuteTaskER6TArrayIP14FBaseGraphTask17FDefaultAllocatorEN13ENamedThreads4TypeE+712)
06-17 15:23:52.350 27129 27129 F DEBUG : #07 pc 040bfa04 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (FNamedTaskThread::ProcessTasksNamedThread(int, bool)+2876)
06-17 15:23:52.350 27129 27129 F DEBUG : #08 pc 040be518 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (FNamedTaskThread::ProcessTasksUntilQuit(int)+108)
06-17 15:23:52.350 27129 27129 F DEBUG : #09 pc 0518eaec /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (RenderingThreadMain(FEvent*)+436)
06-17 15:23:52.350 27129 27129 F DEBUG : #10 pc 051d93d8 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (FRenderingThread::Run()+20)
06-17 15:23:52.350 27129 27129 F DEBUG : #11 pc 04142c8c /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (FRunnableThreadPThread::Run()+164)
06-17 15:23:52.350 27129 27129 F DEBUG : #12 pc 040b9ef0 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (FRunnableThreadPThread::_ThreadProc(void*)+80)
06-17 15:23:52.430 26991 27061 D UE4 : Used memory: 361838
06-17 15:27:43.270 13042 13231 I MtpDatabase: Mediaprovider didn't delete /storage/emulated/0/UE4Game/GWorld/GWorld/Saved/Paks/1.45_Android_ETC2_001_P.pak
06-17 15:27:45.781 13042 13231 D MtpServer: path: /storage/emulated/0/UE4Game/GWorld/GWorld/Saved/Paks/1.45_Android_ETC2_001_P.pak parent: 68 storageID: 00010001

有时间在来具体分析。

target/build.cs输出Log

可以使用C#里的以下代码:

1
2
3
using System;

System.Console.WriteLine("12346");

TargetRules的BuildSettingsVersion

PS:UE4.24之后才有。

可以在Target.cs中指定:

1
DefaultBuildSettings = BuildSettingsVersion.V2;

BuildSettingsVersion可以指定构建时使用的默认设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/// <summary>
/// Determines which version of the engine to take default build settings from. This allows for backwards compatibility as new options are enabled by default.
/// </summary>
public enum BuildSettingsVersion
{
/// <summary>
/// Legacy default build settings for 4.23 and earlier.
/// </summary>
V1,

/// <summary>
/// New defaults for 4.24: ModuleRules.PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs, ModuleRules.bLegacyPublicIncludePaths = false.
/// </summary>
V2,

// *** When adding new entries here, be sure to update GameProjectUtils::GetDefaultBuildSettingsVersion() to ensure that new projects are created correctly. ***

/// <summary>
/// Always use the defaults for the current engine version. Note that this may cause compatibility issues when upgrading.
/// </summary>
Latest
}

UE4.23及之前的引擎版本是V1,用来控制当项目升级引擎版本时使用之前引擎的构建设置,用于解决项目升级之后会有大量错误的问题。

注意:因为4.25之后的是V2,默认bLegacyPublicIncludePaths=false,这个会导致如果模块中相对于Public的代码路径,如Public/Core/CoreCode.h,如果没有添加Core目录到PublicIncludePaths中,在工程的其他地方不指定相对路径,直接用CoreCode.h,在V1的版本里是可以编译过的,但是在V2中就会有编译错误。

从TargetRules获取引擎版本

TargetRules中具有**Version (ReadOnlyBuildVersion)**成员,它是BuildVersion的类型,定义在Programs\UnrealBuildTool\System\BuildVersion.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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
[Serializable]
public class BuildVersion
{
/// <summary>
/// The major engine version (4 for UE4)
/// </summary>
public int MajorVersion;

/// <summary>
/// The minor engine version
/// </summary>
public int MinorVersion;

/// <summary>
/// The hotfix/patch version
/// </summary>
public int PatchVersion;

/// <summary>
/// The changelist that the engine is being built from
/// </summary>
public int Changelist;

/// <summary>
/// The changelist that the engine maintains compatibility with
/// </summary>
public int CompatibleChangelist;

/// <summary>
/// Whether the changelist numbers are a licensee changelist
/// </summary>
public bool IsLicenseeVersion;

/// <summary>
/// Whether the current build is a promoted build, that is, built strictly from a clean sync of the given changelist
/// </summary>
public bool IsPromotedBuild;

/// <summary>
/// Name of the current branch, with '/' characters escaped as '+'
/// </summary>
public string BranchName;

/// <summary>
/// The current build id. This will be generated automatically whenever engine binaries change if not set in the default Engine/Build/Build.version.
/// </summary>
public string BuildId;

/// <summary>
/// The build version string
/// </summary>
public string BuildVersionString;
// ...
}

可以在Target.cs或者Build.cs里通过Target.Version来访问引擎版本,可以根据不同的引擎版本来使用不同的库。

从TargetRules获取Configuration

ReadOnlyTargetRules接收到的Target,可以从其中获取Configuration成员,用于检测打包的BuildConfiguration

1
2
3
4
if (Target.Configuration == UnrealTargetConfiguration.Shipping)
{
// ...
}

枚举值为Development/Debug/DebugGame/Shipping/Test等。

IOS CrashLog分析

GC

UE4使用标记-清扫式的GC方式,它是一种经典的垃圾回收方式。一次垃圾回收分为两个阶段。第一阶段从一个根集合出发,遍历所有可达对象,遍历完成后就能标记出可达对象和不可达对象了,这个阶段会在一帧内完成。第二阶段会渐进式的清理这些不可达对象,因为不可达的对象将永远不能被访问到,所以可以分帧清理它们,避免一下子清理很多UObject,比如map卸载时,发生明显的卡顿。

UObject之间的引用关系需要用强指针引用加UPROPERTY标记完成。

UPROPERTY标记通过UHT之后会生成UProperty对象,UProperty对象可以控制对属性的访问。也通过UProperty对象保存引用关系。

如果想要给没有添加UPROPERTY标记的对象添加引用可以通过重写UObject的虚函数AddReferencedObjects,比如AActor中的OwnedComponents

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Actor.h
TSet<UActorComponent*> OwnedComponents;

// Actor.cpp
void AActor::AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector)
{
AActor* This = CastChecked<AActor>(InThis);
Collector.AddReferencedObjects(This->OwnedComponents);
#if WITH_EDITOR
if (This->CurrentTransactionAnnotation.IsValid())
{
This->CurrentTransactionAnnotation->AddReferencedObjects(Collector);
}
#endif
Super::AddReferencedObjects(InThis, Collector);
}

SDetailsView监听属性变化

可以通过监听基类SDetailsViewBase中的OnFinishedChangingPropertiesDelegate代理来实现。

接收参数是FPropertyChangedEvent

1
DECLARE_MULTICAST_DELEGATE_OneParam(FOnFinishedChangingProperties, const FPropertyChangedEvent&);

UObject serializer的调用栈

有时间再来分析具体内容。

FName/FString/FText的区别

FName

FName的字符串一般用在为资源命名或者访问资源时(比如命名的骨骼)需要使用。
它使用一个轻型系统使用字符串,特定字符串会被重复使用,在数据表中也就只存储一次。

  1. 只在内存中存储一次
  2. 不区分大小写
  3. 不能被修改
  4. 查找和访问速度比较快
  5. 内部有一个HASH值
    FName在Edior中占12个字节,打包8字节,FName的XXXX_12这样的字符串会被分成string part和number part,估计是为了想不为每个拼接的结果都在NamePool中创建一份吧。

FString

FSting比较类似于标准库的std::string,区分大小写,可修改,每份实例都是单独的内存。

FText

FText是用于本地化的类,所有需要展示的文本都需要使用FText,它提供了以下功能:

  1. 创建本地化文本
  2. 格式化文本
  3. 从数字生成文本
  4. 从日期或时间生成文本
  5. 转换文本(大小写转换等)

文档

Plugin添加其他Plugin的模块

如果插件A要引用插件B中的模块,那么就需要在插件A的uplugin文件中添加对插件B的依赖:

1
2
3
4
5
6
"Plugins": [
{
"Name": "B",
"Enabled": true
}
]

然后就可以在插件A中添加插件B中的模块了。

Build Configurations

Build Status Describle
Debug 引擎和游戏符号都以debug方式编译,需要源码版引擎
DebugGame 优化引擎代码,只可以调试Game符号
Development 默认的配置,在DebugGame的模式上进行优化,只可以调试游戏符号
Shipping 不包含调试符号、Console、stats、profiling工具,用于发行版本
Test 与Shipping相同,但是要会包含Console、stats、profiling等工具

如果使用从EpicLauncher安装的引擎,打包Debug时会提示:

1
Targets cannot be built in the Debug configuration with this engine distribution.

这个报错是在UBT中产生的,具体代码在UnrealBuildTool\Configuration\UEBuildTarget.cs

GlobalShaderCache的加载

Runtime/Engine/Private/ShaderCompiler/ShaderCompiler.cpp中有获取GlobalShaderCache*.bin的方法:

1
2
3
4
static FString GetGlobalShaderCacheFilename(EShaderPlatform Platform)
{
return FString(TEXT("Engine")) / TEXT("GlobalShaderCache-") + LegacyShaderPlatformToShaderFormat(Platform).ToString() + TEXT(".bin");
}

在同文件中定义的CompileGlobalShaderMap函数中被读取。
调用栈:

完整流程有时间再来分析。

ushaderbytecode的加载

Runtime/RenderCore/Private/ShaderCodeLibrary.cpp文件中,可以获取到shaderbytecode相关的文件:

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
static uint32 GShaderCodeArchiveVersion = 2;
static uint32 GShaderPipelineArchiveVersion = 1;

static FString ShaderExtension = TEXT(".ushaderbytecode");
static FString StableExtension = TEXT(".scl.csv");
static FString PipelineExtension = TEXT(".ushaderpipelines");

static FString GetCodeArchiveFilename(const FString& BaseDir, const FString& LibraryName, FName Platform)
{
return BaseDir / FString::Printf(TEXT("ShaderArchive-%s-"), *LibraryName) + Platform.ToString() + ShaderExtension;
}

static FString GetStableInfoArchiveFilename(const FString& BaseDir, const FString& LibraryName, FName Platform)
{
return BaseDir / FString::Printf(TEXT("ShaderStableInfo-%s-"), *LibraryName) + Platform.ToString() + StableExtension;
}

static FString GetPipelinesArchiveFilename(const FString& BaseDir, const FString& LibraryName, FName Platform)
{
return BaseDir / FString::Printf(TEXT("ShaderArchive-%s-"), *LibraryName) + Platform.ToString() + PipelineExtension;
}

static FString GetShaderCodeFilename(const FString& BaseDir, const FString& LibraryName, FName Platform)
{
return BaseDir / FString::Printf(TEXT("ShaderCode-%s-"), *LibraryName) + Platform.ToString() + ShaderExtension;
}

static FString GetShaderDebugFolder(const FString& BaseDir, const FString& LibraryName, FName Platform)
{
return BaseDir / FString::Printf(TEXT("ShaderDebug-%s-"), *LibraryName) + Platform.ToString();
}

然后在同文件的FShaderLibraryInstance::Create来加载。

完整流程有时间再来分析。

默认打包到pak里的资源

UE在打包的时候会把工程下的Content中的资源进行依赖分析然后打包,但是经过对比之后发现,引擎中还会添加额外的没有引用到的资源,经过分析代码发现引擎的ini中有指定:

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
;Engine\Config\BaseEngine.ini
[Engine.StartupPackages]
bSerializeStartupPackagesFromMemory=true
bFullyCompressStartupPackages=false
+Package=/Engine/EngineMaterials/BlinkingCaret
+Package=/Engine/EngineMaterials/DefaultBokeh
+Package=/Engine/EngineMaterials/DefaultBloomKernel
+Package=/Engine/EngineMaterials/DefaultDeferredDecalMaterial
;+Package=/Engine/EngineMaterials/DefaultPostProcessMaterial
+Package=/Engine/EngineMaterials/DefaultDiffuse
+Package=/Engine/EngineMaterials/DefaultLightFunctionMaterial
+Package=/Engine/EngineMaterials/WorldGridMaterial
+Package=/Engine/EngineMaterials/DefaultMaterial
+Package=/Engine/EngineMaterials/DefaultNormal
+Package=/Engine/EngineMaterials/DefaultPhysicalMaterial
+Package=/Engine/EngineMaterials/DefaultVirtualTextureMaterial
+Package=/Engine/EngineMaterials/DefaultWhiteGrid
+Package=/Engine/EngineMaterials/EditorBrushMaterial
+Package=/Engine/EngineMaterials/EmissiveMeshMaterial
+Package=/Engine/EngineMaterials/Good64x64TilingNoiseHighFreq
+Package=/Engine/EngineMaterials/Grid
+Package=/Engine/EngineMaterials/Grid_N
+Package=/Engine/EngineMaterials/LandscapeHolePhysicalMaterial
+Package=/Engine/EngineMaterials/MiniFont
+Package=/Engine/EngineMaterials/PaperDiffuse
+Package=/Engine/EngineMaterials/PaperNormal
+Package=/Engine/EngineMaterials/PhysMat_Rubber
+Package=/Engine/EngineMaterials/PreintegratedSkinBRDF
+Package=/Engine/EngineMaterials/RemoveSurfaceMaterial
+Package=/Engine/EngineMaterials/WeightMapPlaceholderTexture

; Console platforms will remove EngineDebugMaterials from their StartupPackages
+Package=/Engine/EngineDebugMaterials/BoneWeightMaterial
+Package=/Engine/EngineDebugMaterials/DebugMeshMaterial
+Package=/Engine/EngineDebugMaterials/GeomMaterial
+Package=/Engine/EngineDebugMaterials/HeatmapGradient
+Package=/Engine/EngineDebugMaterials/LevelColorationLitMaterial
+Package=/Engine/EngineDebugMaterials/LevelColorationUnlitMaterial
+Package=/Engine/EngineDebugMaterials/MAT_LevelColorationLitLightmapUV
+Package=/Engine/EngineDebugMaterials/ShadedLevelColorationLitMaterial
+Package=/Engine/EngineDebugMaterials/ShadedLevelColorationUnlitMateri
+Package=/Engine/EngineDebugMaterials/TangentColorMap
+Package=/Engine/EngineDebugMaterials/VertexColorMaterial
+Package=/Engine/EngineDebugMaterials/VertexColorViewMode_AlphaAsColor
+Package=/Engine/EngineDebugMaterials/VertexColorViewMode_BlueOnly
+Package=/Engine/EngineDebugMaterials/VertexColorViewMode_ColorOnly
+Package=/Engine/EngineDebugMaterials/VertexColorViewMode_GreenOnly
+Package=/Engine/EngineDebugMaterials/VertexColorViewMode_RedOnly
+Package=/Engine/EngineDebugMaterials/WireframeMaterial

+Package=/Engine/EngineSounds/WhiteNoise

+Package=/Engine/EngineFonts/SmallFont
+Package=/Engine/EngineFonts/TinyFont
+Package=/Engine/EngineFonts/Roboto
+Package=/Engine/EngineFonts/RobotoTiny

; only needed for TextRender feature (3d Text in world)
+Package=/Engine/EngineMaterials/DefaultTextMaterialTranslucent
+Package=/Engine/EngineFonts/RobotoDistanceField

就算是工程中没有任何资源,也会默认把这些资源给打包进来。

创建Commandlet

在一个Editor的Module下创建下列文件和代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// .h
#pragma once

#include "Commandlets/Commandlet.h"
#include "HotPatcherPatcherCommandlet.generated.h"


UCLASS()
class UHotPatcherPatcherCommandlet :public UCommandlet
{
GENERATED_BODY()

public:

virtual int32 Main(const FString& Params)override;
};
// .cpp
#include "HotPatcherCookerCommandlet.h"

int32 UHotPatcherCookerCommandlet::Main(const FString& Params)
{
UE_LOG(LogTemp, Log, TEXT("UHotPatcherCookerCommandlet::Main"));
return 0;
}

然后在启动的时候就可以使用下列参数来运行Commandlet,并且可以给它传递参数:

1
UE4Editor.exe PROJECT_NAME.uproject -run=HotPatcherCooker  -aaa="D:\\AAA.json" -test1

UnLua如何实现函数覆写

概括来说:UnLua绑定了UE创建对象的事件,当创建CDO时会调用到UnLua的NotifyUObjectCreated,在其中拿到了该对象的UClass,对该对象的UClass中的UFUNCTION通过SetNativeFunc修改为CallLua函数,这样就实现了覆写UFUNCTION。

下面来具体分析一下实现。UnLua实现覆写完整的调用栈:

替换Thunk函数

在UnLua的FLuaContext的initialize函数中,将GLuaCxt注册到了GUObjectArray中:

1
2
3
4
5
6
// LuaContext.cpp
if (!bAddUObjectNotify)
{
GUObjectArray.AddUObjectCreateListener(GLuaCxt); // add listener for creating UObject
GUObjectArray.AddUObjectDeleteListener(GLuaCxt); // add listener for deleting UObject
}

FLuaContext继承自FUObjectArray::FUObjectCreateListenerFUObjectArray::FUObjectDeleteListener,所以当UE的对象系统创建对象的时候会把调用到FLuaContext的NotifyUObjectCreatedNotifyUObjectDeleted

当创建一个UObject的时候会在FObjectArrayAllocateUObjectIndex中对多有注册过的CreateListener调用NotifyUObjectDeleted函数。

而UnLua实现覆写UFUNCTION的逻辑就是写在NotifyUObjectCreated中的TryBindLua调用中,栈如下:

一个一个来说他们的作用:

FLuaContext::TryBindUnlua

1
2
// Try to bind Lua module for a UObject
bool FLuaContext::TryToBindLua(UObjectBaseUtility *Object);

主要作用是:如果创建的对象继承了UUnLuaInterface,具有GetModuleName函数,则通过传进来的UObject获取到它的UCclass,然后再通过UClass得到GetModuleName函数的UFunction,并通过CDO对象调用该UFunction,得到该CLass绑定的Lua模块名。

若没有静态绑定,则检查是否具有动态绑定。

UUnLuaManager::Bind

该函数定义在UnLua/pRIVATE/UnLuaManager.cpp文件中。

TryBindUnlua中得到了当前创建对象的UClass和绑定的模块名,传递到了Bind函数中,它主要做了几件事情:

  1. 注册Class到lua
  2. require对应的lua模块
  3. 调用UnLuaManager::BindInternal函数
  4. 为当前对象创建一个lua端对象并push上一个Initialize函数并调用

BindInternal

其中的关键函数为UnLuaManager::BindInternal

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
/**
* Bind a Lua module for a UObject
*/
bool UUnLuaManager::BindInternal(UObjectBaseUtility *Object, UClass *Class, const FString &InModuleName, bool bNewCreated)
{
if (!Object || !Class)
{
return false;
}

lua_State *L = *GLuaCxt;
TStringConversion<TStringConvert<TCHAR, ANSICHAR>> ModuleName(*InModuleName);

if (!bNewCreated)
{
if (!BindSurvivalObject(L, Object, Class, ModuleName.Get())) // try to bind Lua module for survival UObject again...
{
return false;
}

FString *ModuleNamePtr = ModuleNames.Find(Class);
if (ModuleNamePtr)
{
return true;
}
}

ModuleNames.Add(Class, InModuleName);
Classes.Add(InModuleName, Class);

#if UE_BUILD_DEBUG
TSet<FName> *LuaFunctionsPtr = ModuleFunctions.Find(InModuleName);
check(!LuaFunctionsPtr);
TMap<FName, UFunction*> *UEFunctionsPtr = OverridableFunctions.Find(Class);
check(!UEFunctionsPtr);
#endif

TSet<FName> &LuaFunctions = ModuleFunctions.Add(InModuleName);
GetFunctionList(L, ModuleName.Get(), LuaFunctions); // get all functions defined in the Lua module
TMap<FName, UFunction*> &UEFunctions = OverridableFunctions.Add(Class);
GetOverridableFunctions(Class, UEFunctions); // get all overridable UFunctions

OverrideFunctions(LuaFunctions, UEFunctions, Class, bNewCreated); // try to override UFunctions

return ConditionalUpdateClass(Class, LuaFunctions, UEFunctions);
}

这个函数接受到的参数是创建出来的UObject,以及它的UClass,还有对应的Lua的模块名。

  1. 把对象的UClass与Lua的模块名对应添加到ModuleNamesClasses
  2. 从Lua端通过L获取所指定模块名中的所有函数
  3. 从UClass获取所有的BlueprintEvent、RepNotifyFunc函数
  4. 对两边获取的结果调用UUnLuaManager::OverrideFunctions执行替换

UUnLuaManager::OverrideFunctions

对从Lua端获取的函数使用名字在当前类的UFunction中查找,依次对其调用UUnLuaManager::OverrideFunction.

UUnLuaManager::OverrideFunction

  1. 判断传入的UFunction是不是属于传入的Outer UClasss
  2. 判断是否允许调用被覆写的函数
  3. 调用AddFunction函数

UUnLuaManager::AddFunction

  1. 如果函数为FUNC_Native则将FLuaInvoker::execCallLua和所覆写的函数名通过AddNativeFunction添加至UClass
  2. UFunction内的函数指针替换为(FNativeFuncPtr)&FLuaInvoker::execCallLua
  3. 如果开启了允许调用被覆写的函数,则把替换NativeFunc之前的UFunction对象存到GReflectionRegistry

Call lua

首先,需要说的一点是,当使用UEC++写的带有UFUNCTION并具有BlueprintNativeEvent或者BlueprintImplementableEvent标记的函数,UHT会给生成对应名字的函数:

1
2
3
4
5
6
UFUNCTION(BlueprintNativeEvent,BlueprintCallable)
bool TESTFUNC();
bool TESTFUNC_Implementation();

UFUNCTION(BlueprintImplementableEvent, meta = (DisplayName = "BeginPlay"))
bool TESTImplEvent(AActor* InActor,int32 InIval);

UHT生成的函数和传递的数据结构:

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
// generated.h
#define MicroEnd_423_Source_MicroEnd_423_Public_MyActor_h_13_EVENT_PARMS \
struct MyActor_eventReceiveBytes_Parms \
{ \
TArray<uint8> InData; \
}; \
struct MyActor_eventTESTFUNC_Parms \
{ \
bool ReturnValue; \
\
/** Constructor, initializes return property only **/ \
MyActor_eventTESTFUNC_Parms() \
: ReturnValue(false) \
{ \
} \
}; \
struct MyActor_eventTESTImplEvent_Parms \
{ \
AActor* InActor; \
int32 InIval; \
bool ReturnValue; \
\
/** Constructor, initializes return property only **/ \
MyActor_eventTESTImplEvent_Parms() \
: ReturnValue(false) \
{ \
} \
};

// gen.cpp
static FName NAME_AMyActor_TESTFUNC = FName(TEXT("TESTFUNC"));
bool AMyActor::TESTFUNC()
{
MyActor_eventTESTFUNC_Parms Parms;
ProcessEvent(FindFunctionChecked(NAME_AMyActor_TESTFUNC),&Parms);
return !!Parms.ReturnValue;
}
static FName NAME_AMyActor_TESTImplEvent = FName(TEXT("TESTImplEvent"));
bool AMyActor::TESTImplEvent(AActor* InActor, int32 InIval)
{
MyActor_eventTESTImplEvent_Parms Parms;
Parms.InActor=InActor;
Parms.InIval=InIval;
ProcessEvent(FindFunctionChecked(NAME_AMyActor_TESTImplEvent),&Parms);
return !!Parms.ReturnValue;
}

可以看到,UHT帮我们定义了同名函数,并将其转发给ProcessEvent

注意:这里通过FindFunctionChecked方法是调用的UObject::FindFunctionChecked

1
2
3
4
5
6
7
8
9
10
11
12
13
14
UFunction* UObject::FindFunction( FName InName ) const
{
return GetClass()->FindFunctionByName(InName);
}

UFunction* UObject::FindFunctionChecked( FName InName ) const
{
UFunction* Result = FindFunction(InName);
if (Result == NULL)
{
UE_LOG(LogScriptCore, Fatal, TEXT("Failed to find function %s in %s"), *InName.ToString(), *GetFullName());
}
return Result;
}

可以看到,这里传递给ProcessEventUFunction*就是从当前对象的UClass中得到的。

经过前面分分析可以知道,UnLua实现的函数覆写,就是把UClass中的UFunction中的原生thunk函数指针替换为FLuaInvoker::execCallLua,而且当一个对象的BlueprintNativeEventBlueprintImplementableEvent函数被调用的时候会调用到ProcessEvent并传入对应的UFunction*,在ProcessEvent中又调Invork(调用其中的原生指针),也就是实现调用到了unlua中替换绑定的FLuaInvoker::execCallLua,在这个函数中再转发给调用lua端的函数,从而实现了覆写函数的目的。

从TargetRules获取引擎版本

之前写到过在C++代码里,UE提供了几个宏可以获取引擎版本(UE版本号的宏定义),那么怎么在build.cs里检测引擎版本?

在UE4.19版本之前从UBT获取引擎版本比较麻烦:

1
2
3
4
5
6
7
BuildVersion Version;
if (BuildVersion.TryRead(BuildVersion.GetDefaultFileName(), out Version))
{
System.Console.WriteLine(Version.MajorVersion);
System.Console.WriteLine(Version.MinorVersion);
System.Console.WriteLine(Version.PatchVersion);
}

在UE4.19及以后的引擎版本,可以通过ReadOnlyTargetRules.Version来获得,它是ReadOnlyBuildVersion类型,包裹了一个BuildVersion类:

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
// UnrealBuildTools/System/BuildVersion.cs
namespace UnrealBuildTool
{
/// <summary>
/// Holds information about the current engine version
/// </summary>
[Serializable]
public class BuildVersion
{
/// <summary>
/// The major engine version (4 for UE4)
/// </summary>
public int MajorVersion;

/// <summary>
/// The minor engine version
/// </summary>
public int MinorVersion;

/// <summary>
/// The hotfix/patch version
/// </summary>
public int PatchVersion;

/// <summary>
/// The changelist that the engine is being built from
/// </summary>
public int Changelist;

/// <summary>
/// The changelist that the engine maintains compatibility with
/// </summary>
public int CompatibleChangelist;

/// <summary>
/// Whether the changelist numbers are a licensee changelist
/// </summary>
public bool IsLicenseeVersion;

/// <summary>
/// Whether the current build is a promoted build, that is, built strictly from a clean sync of the given changelist
/// </summary>
public bool IsPromotedBuild;

/// <summary>
/// Name of the current branch, with '/' characters escaped as '+'
/// </summary>
public string BranchName;

/// <summary>
/// The current build id. This will be generated automatically whenever engine binaries change if not set in the default Engine/Build/Build.version.
/// </summary>
public string BuildId;

/// <summary>
/// The build version string
/// </summary>
public string BuildVersionString;

// ...
}
}

其中的MajorVersion/MinorVersion/PatchVersion分别对应X.XX.X。

FPaths中Dir函数的对应路径

FPaths提供了很多EngineDir等之类的函数,我在unlua里导出了这些符号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
print(fmt("EngineDir: {}",UE4.FPaths.EngineDir()))
print(fmt("EngineUserDir: {}",UE4.FPaths.EngineUserDir()))
print(fmt("EngineContentDir: {}",UE4.FPaths.EngineContentDir()))
print(fmt("EngineConfigDir: {}",UE4.FPaths.EngineConfigDir()))
print(fmt("EngineSavedDir: {}",UE4.FPaths.EngineSavedDir()))
print(fmt("EnginePluginsDir: {}",UE4.FPaths.EnginePluginsDir()))
print(fmt("RootDir: {}",UE4.FPaths.RootDir()))
print(fmt("ProjectDir: {}",UE4.FPaths.ProjectDir()))
print(fmt("ProjectUserDir: {}",UE4.FPaths.ProjectUserDir()))
print(fmt("ProjectContentDir: {}",UE4.FPaths.ProjectContentDir()))
print(fmt("ProjectConfigDir: {}",UE4.FPaths.ProjectConfigDir()))
print(fmt("ProjectSavedDir: {}",UE4.FPaths.ProjectSavedDir()))
print(fmt("ProjectIntermediateDir: {}",UE4.FPaths.ProjectIntermediateDir()))
print(fmt("ProjectPluginsDir: {}",UE4.FPaths.ProjectPluginsDir()))
print(fmt("ProjectLogDir: {}",UE4.FPaths.ProjectLogDir()))

他们对应的具体路径为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
EngineDir: ../../../Engine/
EngineUserDir: : /Users/imzlp/AppData/Local/UnrealEngine/4.22/
EngineContentDir: ../../../Engine/Content/
EngineConfigDir: ../../../Engine/Config/
EngineSavedDir: : /Users/imzlp/AppData/Local/UnrealEngine/4.22/Saved/
EnginePluginsDir: ../../../Engine/Plugins/
RootDir: : /Program Files/Epic Games/UE_4.22/
ProjectDir: ../../../../../../Users/imzlp/Documents/UnrealProjectSSD/GWorldClient/
ProjectUserDir: ../../../../../../Users/imzlp/Documents/UnrealProjectSSD/GWorldClient/
ProjectContentDir: ../../../../../../Users/imzlp/Documents/UnrealProjectSSD/GWorldClient/Content/
ProjectConfigDir: ../../../../../../Users/imzlp/Documents/UnrealProjectSSD/GWorldClient/Config/
ProjectSavedDir: ../../../../../../Users/imzlp/Documents/UnrealProjectSSD/GWorldClient/Saved/
ProjectIntermediateDir: ../../../../../../Users/imzlp/Documents/UnrealProjectSSD/GWorldClient/Intermediate/
ProjectPluginsDir: ../../../../../../Users/imzlp/Documents/UnrealProjectSSD/GWorldClient/Plugins/
ProjectLogDir: ../../../../../../Users/imzlp/Documents/UnrealProjectSSD/GWorldClient/Saved/Logs/

这些相对路径都是相对于引擎的exe的路径的:

通过Commandline替换加载的ini

如项目下的DefaultEngine.ini/DefaultGame.ini等。
去掉Defaultini后缀之后是它们的baseName,可以通过下列命令行来替换:

1
2
3
4
# engine
-EngineINI=REPLACE_INI_FILE_PAT.ini
# game
-GameINI=REPLACE_INI_FILE_PAT.ini

具体实现是在FConfigCacheIni::GetDestIniFilename中做的:

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
// Core/Private/Misc/ConfigCacheIni.cpp
FString FConfigCacheIni::GetDestIniFilename(const TCHAR* BaseIniName, const TCHAR* PlatformName, const TCHAR* GeneratedConfigDir)
{
// figure out what to look for on the commandline for an override
FString CommandLineSwitch = FString::Printf(TEXT("%sINI="), BaseIniName);

// if it's not found on the commandline, then generate it
FString IniFilename;
if (FParse::Value(FCommandLine::Get(), *CommandLineSwitch, IniFilename) == false)
{
FString Name(PlatformName ? PlatformName : ANSI_TO_TCHAR(FPlatformProperties::PlatformName()));

// if the BaseIniName doens't contain the config dir, put it all together
if (FCString::Stristr(BaseIniName, GeneratedConfigDir) != nullptr)
{
IniFilename = BaseIniName;
}
else
{
IniFilename = FString::Printf(TEXT("%s%s/%s.ini"), GeneratedConfigDir, *Name, BaseIniName);
}
}

// standardize it!
FPaths::MakeStandardFilename(IniFilename);
return IniFilename;
}

获取当前平台信息

可以使用FPlatformProperties来获取当前程序的平台信息。
同样是使用UE的跨平台库写法,FGenericPlatformProperties是定义在Core/Public/GenericPlatform/GenericPlatformProperties.h中的。

例:可以使用FPlatformProperties::PlatformName()运行时来获取当前平台的名字。

FPlatformPropertiestypedef是定义在Core/Public/HAL/PlatformProperties.h中。

COMPILED_PLATFORM_HEADER

在UE4.22之前,UE的跨平台库的实现方式都是创建一个泛型平台类:

1
2
3
4
struct FGenericPlatformUtils
{
static void GenericMethod(){}
};

然后每个平台实现:

1
2
3
4
5
6
7
8
// Windows/WindowsPlatformUtils.h
struct FWindowsPlatformUtils:public FGenericPlatformUtils
{
static void GenericMethod(){
//doSomething...
}
};
typedef FWindowsPlatformUtils FPlatformUtils;

在UE4.22之前,需要使用下面这种方法:

1
2
3
4
5
6
7
8
9
10
// PlatformUtils.h
#if PLATFORM_ANDROID
#include "Android/AndroidPlatformUtils.h"
#elif PLATFORM_IOS
#include "IOS/IOSPlatformUtils.h"
#elif PLATFORM_WINDOWS
#include "Windows/WindowsPlatformUtils.h"
#elif PLATFORM_MAC
#include "Mac/MacPlatformUtils.h"
#endif

需要手动判断每个平台再进行包含,也是比较麻烦的,在4.23之后,UE引入了一个宏:COMPILED_PLATFORM_HEADER,可以把上面的包含简化为下面的代码:

1
#include COMPILED_PLATFORM_HEADER(PlatformUtils.h)

它是定义在Runtime/Core/Public/HAL/PreprocessorHelpers.h下的宏:

1
2
3
4
5
6
7
#if PLATFORM_IS_EXTENSION
// Creates a string that can be used to include a header in the platform extension form "PlatformHeader.h", not like below form
#define COMPILED_PLATFORM_HEADER(Suffix) PREPROCESSOR_TO_STRING(PREPROCESSOR_JOIN(PLATFORM_HEADER_NAME, Suffix))
#else
// Creates a string that can be used to include a header in the form "Platform/PlatformHeader.h", like "Windows/WindowsPlatformFile.h"
#define COMPILED_PLATFORM_HEADER(Suffix) PREPROCESSOR_TO_STRING(PREPROCESSOR_JOIN(PLATFORM_HEADER_NAME/PLATFORM_HEADER_NAME, Suffix))
#endif

注释已经比较说明作用了。而且它还有兄弟宏:

1
2
3
4
5
6
7
#if PLATFORM_IS_EXTENSION
// Creates a string that can be used to include a header with the platform in its name, like "Pre/Fix/PlatformNameSuffix.h"
#define COMPILED_PLATFORM_HEADER_WITH_PREFIX(Prefix, Suffix) PREPROCESSOR_TO_STRING(Prefix/PREPROCESSOR_JOIN(PLATFORM_HEADER_NAME, Suffix))
#else
// Creates a string that can be used to include a header with the platform in its name, like "Pre/Fix/PlatformName/PlatformNameSuffix.h"
#define COMPILED_PLATFORM_HEADER_WITH_PREFIX(Prefix, Suffix) PREPROCESSOR_TO_STRING(Prefix/PLATFORM_HEADER_NAME/PREPROCESSOR_JOIN(PLATFORM_HEADER_NAME, Suffix))
#endif

命名有规律是多么重要的一件事…

遍历UCLASS或USTRUCT的反射成员

可以通过TFieldIterator来遍历:

1
2
3
4
for (TFieldIterator<UProperty> PropertyIt(ProxyClass); PropertyIt; ++PropertyIt)
{
// ...
}

注意:4.25之后没有UProperty,变成了FProperty.

Lua:Metatable

控制打包时ini的拷贝

DeploymentContext.cs中的DeploymentContext函数中,有以下两行代码:

1
2
3
// Read the list of files which are whitelisted to be staged
ReadConfigFileList(GameConfig, "Staging", "WhitelistConfigFiles", WhitelistConfigFiles);
ReadConfigFileList(GameConfig, "Staging", "BlacklistConfigFiles", BlacklistConfigFiles);

这两个数组会在CopyBuildToStageingDirectory.Automation.cs中使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/// <summary>
/// Determines if an individual config file should be staged
/// </summary>
/// <param name="SC">The staging context</param>
/// <param name="ConfigDir">Directory containing the config files</param>
/// <param name="ConfigFile">The config file to check</param>
/// <returns>True if the file should be staged, false otherwise</returns>
static Nullable<bool> ShouldStageConfigFile(DeploymentContext SC, DirectoryReference ConfigDir, FileReference ConfigFile)
{
StagedFileReference StagedConfigFile = SC.GetStagedFileLocation(ConfigFile);
if (SC.WhitelistConfigFiles.Contains(StagedConfigFile))
{
return true;
}
if (SC.BlacklistConfigFiles.Contains(StagedConfigFile))
{
return false;
}
// ...
}

用途就是指定哪些config会添加到包体中。
用法如下(写到DefaultGame.ini中):

1
2
[Staging]
+BlacklistConfigFiles=GWorldClient/Config/DefaultGameExtensionSettings.ini

CharCast的坑

CharCast是定义在StringConv.h的模板函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Casts one fixed-width char type into another.
*
* @param Ch The character to convert.
* @return The converted character.
*/
template <typename To, typename From>
FORCEINLINE To CharCast(From Ch)
{
To Result;
FPlatformString::Convert(&Result, 1, &Ch, 1, (To)UNICODE_BOGUS_CHAR_CODEPOINT);
return Result;
}

就是对FPlatformString::Convert的转发调用。

PS:UNICODE_BOGUS_CHAR_CODEPOINT 宏定义为'?'

FPlatformString::Convert有两个版本:

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
/**
* Converts the [Src, Src+SrcSize) string range from SourceChar to DestChar and writes it to the [Dest, Dest+DestSize) range.
* The Src range should contain a null terminator if a null terminator is required in the output.
* If the Dest range is not big enough to hold the converted output, NULL is returned. In this case, nothing should be assumed about the contents of Dest.
*
* @param Dest The start of the destination buffer.
* @param DestSize The size of the destination buffer.
* @param Src The start of the string to convert.
* @param SrcSize The number of Src elements to convert.
* @param BogusChar The char to use when the conversion process encounters a character it cannot convert.
* @return A pointer to one past the last-written element.
*/
template <typename SourceEncoding, typename DestEncoding>
static FORCEINLINE typename TEnableIf<
// This overload should be called when SourceEncoding and DestEncoding are 'compatible', i.e. they're the same type or equivalent (e.g. like UCS2CHAR and WIDECHAR are on Windows).
TAreEncodingsCompatible<SourceEncoding, DestEncoding>::Value,
DestEncoding*
>::Type Convert(DestEncoding* Dest, int32 DestSize, const SourceEncoding* Src, int32 SrcSize, DestEncoding BogusChar = (DestEncoding)'?')
{
if (DestSize < SrcSize)
return nullptr;

return (DestEncoding*)Memcpy(Dest, Src, SrcSize * sizeof(SourceEncoding)) + SrcSize;
}


template <typename SourceEncoding, typename DestEncoding>
static typename TEnableIf<
// This overload should be called when the types are not compatible but the source is fixed-width, e.g. ANSICHAR->WIDECHAR.
!TAreEncodingsCompatible<SourceEncoding, DestEncoding>::Value && TIsFixedWidthEncoding<SourceEncoding>::Value,
DestEncoding*
>::Type Convert(DestEncoding* Dest, int32 DestSize, const SourceEncoding* Src, int32 SrcSize, DestEncoding BogusChar = (DestEncoding)'?')
{
const int32 Size = DestSize <= SrcSize ? DestSize : SrcSize;
bool bInvalidChars = false;
for (int I = 0; I < Size; ++I)
{
SourceEncoding SrcCh = Src[I];
Dest[I] = (DestEncoding)SrcCh;
bInvalidChars |= !CanConvertChar<DestEncoding>(SrcCh);
}

if (bInvalidChars)
{
for (int I = 0; I < Size; ++I)
{
if (!CanConvertChar<DestEncoding>(Src[I]))
{
Dest[I] = BogusChar;
}
}

LogBogusChars<DestEncoding>(Src, Size);
}

return DestSize < SrcSize ? nullptr : Dest + Size;
}

其中关键的是第二个实现, 通过判断CanConvertChar来检测是否能够转换字符,如果不能转换就把转换结果设置为BogusChar,默认也就是?,这也是把不同编码的数据转换为FString有些会显示一堆?的原因。

1
2
3
4
5
6
7
8
9
10
11
/**
* Tests whether a particular character can be converted to the destination encoding.
*
* @param Ch The character to test.
* @return True if Ch can be encoded as a DestEncoding.
*/
template <typename DestEncoding, typename SourceEncoding>
static bool CanConvertChar(SourceEncoding Ch)
{
return IsValidChar(Ch) && (SourceEncoding)(DestEncoding)Ch == Ch && IsValidChar((DestEncoding)Ch);
}

所以:类似LoadFileToString去读文件如果编码不支持,那么读出来的数据和原始文件里是不一样的。

bUsesSlate

在UE的TargetRules中有一项属性bUsesSlate,可以用来控制是否启用Slate,UE文档里的描述如下:

Whether the project uses visual Slate UI (as opposed to the low level windowing/messaging, which is always available).

但是我想知道是否启用对于项目打出的包有什么区别。经过测试发现,以移动端为例,bUsesSlate的值并不会影响libUE4.so的大小。

有影响的地方只在于打包时的pak大小,这一点可以从两次分别打包的PakList*.txt中得知,经过对比发现若bUsesSlate=false,则在打包时不会把Engine\Content\Slate下的图片资源打包。我把两个版本的PakList*.txt都放在这里,有兴趣的可以看都是有哪些资源没有被打包。

下面这幅图是两个分别开启bUsesSlate的图(左侧false右侧true),可以看到只有main.obb.webp的大小不一样。

可以看到默认情况下main.obb.webp减小了大概6-7M,APK的大小也减小的差不多。

Unreal Plugin Language

在UE中为移动端添加第三方模块或者修改配置文件时经常会用到AdditionalPropertiesForReceipt,里面创建ReceiptProperty传入的xml文件就是UE的Unreal Plugin Language脚本。

ReceiptProperty的平台名称在IOS和Android上是固定的,分别是IOSPluginAndroidPlugin,不可以指定其他的名字(详见代码UEDeployIOS.cs#L1153UEDeployAndroid.cs#L4303)。

1
AdditionalPropertiesForReceipt.Add(new ReceiptProperty("AndroidPlugin", Path.Combine(ThirdPartyPath, "Android/PlatformUtils_UPL_Android.xml")));

打包时Paklist文件的生成

UE打出Pak时,需要一个txt的参数传入,里面记录着要打到pak里的文件信息,直接使用UE的打包改文件会存储在:

1
C:\Users\imzlp\AppData\Roaming\Unreal Engine\AutomationTool\Logs\D+UnrealEngine+Epic+UE_4.23\PakList_microend_423-ios.txt

类似的路径下。

这个文件生成的地方为:

1
D:\UnrealEngine\Epic\UE_4.24\Engine\Source\Programs\AutomationTool\BuildGraph\Tasks\PakFileTask.cs

在它的Execute函数里,有通过外部传入的PakFileTaskParameters的参数来把文件写入。

ShaderStableInfo*.scl.csv

在Cook的时候会在Cooked/PLATFORM/PROJECT_NAME/Metadata/PipelineCaches下生成类似下面这样的文件:

1
2
3
4
ShaderStableInfo-Global-PCD3D_SM4.scl.csv
ShaderStableInfo-Global-PCD3D_SM5.scl.csv
ShaderStableInfo-GWorld-PCD3D_SM4.scl.csv
ShaderStableInfo-GWorld-PCD3D_SM5.scl.csv

里面记录了FStableShaderKeyAndValue结构的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// RenderCore/Public/ShaderCodeLibrary.h
struct RENDERCORE_API FStableShaderKeyAndValue
{
FCompactFullName ClassNameAndObjectPath;
FName ShaderType;
FName ShaderClass;
FName MaterialDomain;
FName FeatureLevel;
FName QualityLevel;
FName TargetFrequency;
FName TargetPlatform;
FName VFType;
FName PermutationId;
FSHAHash PipelineHash;

uint32 KeyHash;
FSHAHash OutputHash;

FStableShaderKeyAndValue()
: KeyHash(0)
{
}
}

作用有时间再来分析。

PE的DLL为什么需要导入库?

在ELF中,共享库所有的全局函数和变量在默认情况下都可以被其他模块使用,也就是说ELF默认导出所有的全局符号。但是在DLL中不同,PE环境下需要显式地告诉编译器我们需要导出的符号,否则编译器就默认所有符号都不导出。

在MSVC中可以使用__declspec(dllexport)以及__declspec(dllimport)来分别表示导出本DLL的符号以及从别的DLL中导入符号。除了上面两个属性关键字还可以定义def文件来声明导入导出符号,def文件时连接器的链接脚本文件,可以当作链接器的输入文件,用于控制链接过程。

在我之前的一篇文章(动态链接库的使用:加载和链接)中写到过DLL导入库的创建和使用,但是为什么DLL需要导入库而so不需要呢?前面已经回答,因为ELF是默认全导出的,PE是默认不导出的,但是我想知道原因是什么。

其实在有了上面的两个属性关键字之后不使用导入库也可以实现符号的导入和导出。

  1. 当某个PE文件被加载时。Windows加载器的其中一个任务就是把所有需要导入的函数地址确定并且将导入表中的元素调整到正确的地址,以实现动态链接的过程,导入表中有IAT,其中的每个元素对应一个被导入的符号。
  2. 编译器无法知道一个符号是从外部导入的还是本模块中定义的,所以编译器是直接产生调用指令
1
CALL XXXXXXXXX
  1. __declspec出现之前,微软提供的方法就是使用导入库,在这种情况下,对于导入函数的调用并不区分是导入函数还是导出函数,它统一地产生直接调用的指令,但是链接器在链接时会将导入函数的目标地址导向一小段桩代码(stub),由这个桩代码再将控制权交给IAT中的真正目标。
  2. 所以导入库的作用就是将编译器产生的调用命令转发到导入表的IAT中目标地址。

UCLASS的config

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Implements the settings for the Paper2D plugin.
*/
UCLASS(config=Engine, defaultconfig)
class PAPER2D_API UPaperRuntimeSettings : public UObject
{
GENERATED_UCLASS_BODY()

// Enables experimental *incomplete and unsupported* texture atlas groups that sprites can be assigned to
UPROPERTY(EditAnywhere, config, Category=Experimental)
bool bEnableSpriteAtlasGroups;

// Enables experimental *incomplete and unsupported* 2D terrain spline editing. Note: You need to restart the editor when enabling this setting for the change to fully take effect.
UPROPERTY(EditAnywhere, config, Category=Experimental, meta=(ConfigRestartRequired=true))
bool bEnableTerrainSplineEditing;

// Enables automatic resizing of various sprite data that is authored in texture space if the source texture gets resized (sockets, the pivot, render and collision geometry, etc...)
UPROPERTY(EditAnywhere, config, Category=Settings)
bool bResizeSpriteDataToMatchTextures;
};

这个类是个config的类,可以从ini中读取配置,关键的地方就是UCLASS(Config=)的东西,一般情况下是Engine/Game/Editor,它们的ini文件都是Default*.ini,如上面这个类,如果想要自己在ini中来指定它们这些参数的值,则需要写到项目的Config/DefaultEngine.ini中:

1
2
[/Script/Paper2D.PaperRuntimeSettings]
bEnableSpriteAtlasGroups = true;

其中ini的Section为该配置类的PackagePath

操作剪贴板Clipboard

有些需求是要能够访问到用户的粘贴板,来进行复制、和粘贴的功能。

在UE中访问粘贴板的方法如下:

1
2
3
4
5
6
FString PasteString;
// 从剪贴板读取内容
FPlatformApplicationMisc::ClipboardPaste(PasteString);

// 把123456放入剪贴板
FPlatformApplicationMisc::ClipboardCopy(TEXT("123465"));

注意:FPlatformApplicationMisc是定义在ApplicationCore下的,使用时要包含该模块。

在场景中Copy/Paste的实现

在UE的场景编辑器中对一个选中的Actor进行Ctrl+C时把拷贝的内容粘贴到一个文本编辑器里可以看到类似以下的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Begin Map
Begin Level
Begin Actor Class=/Script/Engine.Pawn Name=Pawn_1 Archetype=/Script/Engine.Pawn'/Script/Engine.Default__Pawn'
Begin Object Class=/Script/Engine.SceneComponent Name="DefaultSceneRoot"
End Object
Begin Object Name="DefaultSceneRoot"
RelativeLocation=(X=600.000000,Y=280.000000,Z=150.000000)
bVisualizeComponent=True
CreationMethod=Instance
End Object
RootComponent=SceneComponent'"DefaultSceneRoot"'
ActorLabel="Pawn"
InstanceComponents(0)=SceneComponent'"DefaultSceneRoot"'
End Actor
End Level
Begin Surface
End Surface
End Map

它记录了当前拷贝的Actor的类,位置、以及与默认对象(CDO)不一致的属性。
拷贝上面的文本,在UE的场景编辑器里粘贴,会在场景里创建出来一个一摸一样的对象。

Copy

在场景编辑器中执行Ctrl+C会把文本拷贝到粘贴板的实现为UEditorEngine::CopySelectedActorsToClipboard函数,其定义在EditorServer.cpp中:

1
2
3
4
5
6
7
8
9
10
/**
* Copies selected actors to the clipboard. Supports copying actors from multiple levels.
* NOTE: Doesn't support copying prefab instance actors!
*
* @param InWorld World to get the selected actors from
* @param bShouldCut If true, deletes the selected actors after copying them to the clipboard
* @param bIsMove If true, this cut is part of a move and the actors wi