Unreal Engine


GIST NOTES

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


C#检测命令行参数

在build.cs以及target.cs中可以获取命令行参数,对编译选项进行控制:

1
2
3
4
5
string[] CmdArgs = Environment.GetCommandLineArgs();
foreach (string CmdLineArg in CmdArgs)
{
Console.WriteLine("CmdLineArg: " + CmdLineArg);
}

编译时输出:

1
2
3
4
5
6
7
8
9
0>E:\UnrealEngine\Launcher\UE_4.26\Engine\Build\BatchFiles\Build.bat GWorldEditor Win64 Development -Project="E:\UnrealProjects\GWorld\GWorld.uproject" -WaitMutex -FromMsBuild -test
0>CmdLineArg: ..\..\Engine\Binaries\DotNET\UnrealBuildTool.exe
0>CmdLineArg: GWorldEditor
0>CmdLineArg: Win64
0>CmdLineArg: Development
0>CmdLineArg: -Project=E:\UnrealProjects\GWorld\GWorld.uproject
0>CmdLineArg: -WaitMutex
0>CmdLineArg: -FromMsBuild
0>CmdLineArg: -test

基于传递的参数可以动态地修改编译行为,比如添加宏、依赖的模块等。

IniPlatformName

因为如果只有打包的资源平台不同,App读取的ini存在共用,如下面这些,其实都对应基础的ini Platform:

  • WindowsNoEditor -> Windows
  • WindowsClient -> Windows
  • WindowsServer -> Windows
  • Android_ASTC -> Android
  • Android_2TC2 -> Android

可以通过以下方式转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FString Conv2IniPlatform(const FString& Platform)
{
FString Result;
ITargetPlatformManagerModule& TPM = GetTargetPlatformManagerRef();
const TArray<ITargetPlatform*>& TargetPlatforms = TPM.GetTargetPlatforms();
TArray<ITargetPlatform*> CookPlatforms;
for (ITargetPlatform *TargetPlatform : TargetPlatforms)
{
if (TargetPlatform->PlatformName().Equals(Platform))
{
Result = TargetPlatform->IniPlatformName();
}
}
return Result;
}

IOS内存阈值

距离FootPrint有100M的范围是比较保险的。

Device: (crash amount/total amount/percentage of total)

  • iPad1: 127MB/256MB/49%
  • iPad2: 275MB/512MB/53%
  • iPad3: 645MB/1024MB/62%
  • iPad4: 585MB/1024MB/57% (iOS 8.1)
  • iPad Mini 1st Generation: 297MB/512MB/58%
  • iPad Mini retina: 696MB/1024MB/68% (iOS 7.1)
  • iPad Air: 697MB/1024MB/68%
  • iPad Air 2: 1383MB/2048MB/68% (iOS 10.2.1)
  • iPad Pro 9.7”: 1395MB/1971MB/71% (iOS 10.0.2 (14A456))
  • iPad Pro 10.5”: 3057/4000/76% (iOS 11 beta4)
  • iPad Pro 12.9” (2015): 3058/3999/76% (iOS 11.2.1)
  • iPad Pro 12.9” (2017): 3057/3974/77% (iOS 11 beta4)
  • iPad Pro 11.0” (2018): 2858/3769/76% (iOS 12.1)
  • iPad Pro 12.9” (2018, 1TB): 4598/5650/81% (iOS 12.1)
  • iPad 10.2: 1844/2998/62% (iOS 13.2.3)
  • iPod touch 4th gen: 130MB/256MB/51% (iOS 6.1.1)
  • iPod touch 5th gen: 286MB/512MB/56% (iOS 7.0)
  • iPhone4: 325MB/512MB/63%
  • iPhone4s: 286MB/512MB/56%
  • iPhone5: 645MB/1024MB/62%
  • iPhone5s: 646MB/1024MB/63%
  • iPhone6: 645MB/1024MB/62% (iOS 8.x)
  • iPhone6+: 645MB/1024MB/62% (iOS 8.x)
  • iPhone6s: 1396MB/2048MB/68% (iOS 9.2)
  • iPhone6s+: 1392MB/2048MB/68% (iOS 10.2.1)
  • iPhoneSE: 1395MB/2048MB/69% (iOS 9.3)
  • iPhone7: 1395/2048MB/68% (iOS 10.2)
  • iPhone7+: 2040MB/3072MB/66% (iOS 10.2.1)
  • iPhone8: 1364/1990MB/70% (iOS 12.1)
  • iPhone X: 1392/2785/50% (iOS 11.2.1)
  • iPhone XS: 2040/3754/54% (iOS 12.1)
  • iPhone XS Max: 2039/3735/55% (iOS 12.1)
  • iPhone XR: 1792/2813/63% (iOS 12.1)
  • iPhone 11: 2068/3844/54% (iOS 13.1.3)
  • iPhone 11 Pro Max: 2067/3740/55% (iOS 13.2.3)

ensure

在shipping时不会Break。

Runtime\Core\Public\Misc\AssertionMacros.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
/**
* ensure() can be used to test for *non-fatal* errors at runtime
*
* Rather than crashing, an error report (with a full call stack) will be logged and submitted to the crash server.
* This is useful when you want runtime code verification but you're handling the error case anyway.
*
* Note: ensure() can be nested within conditionals!
*
* Example:
*
* if (ensure(InObject != nullptr))
* {
* InObject->Modify();
* }
*
* This code is safe to execute as the pointer dereference is wrapped in a non-nullptr conditional block, but
* you still want to find out if this ever happens so you can avoid side effects. Using ensure() here will
* force a crash report to be generated without crashing the application (and potentially causing editor
* users to lose unsaved work.)
*
* ensure() resolves to just evaluate the expression when DO_CHECK is 0 (typically shipping or test builds).
*
* By default a given call site will only print the callstack and submit the 'crash report' the first time an
* ensure is hit in a session; ensureAlways can be used instead if you want to handle every failure
*/

#if DO_CHECK && !USING_CODE_ANALYSIS // The Visual Studio 2013 analyzer doesn't understand these complex conditionals

#define UE_ENSURE_IMPL(Capture, Always, InExpression, ...) \
(LIKELY(!!(InExpression)) || (DispatchCheckVerify<bool>([Capture] () FORCENOINLINE UE_DEBUG_SECTION \
{ \
static bool bExecuted = false; \
if ((!bExecuted || Always) && FPlatformMisc::IsEnsureAllowed()) \
{ \
bExecuted = true; \
FDebug::OptionallyLogFormattedEnsureMessageReturningFalse(true, #InExpression, __FILE__, __LINE__, ##__VA_ARGS__); \
if (!FPlatformMisc::IsDebuggerPresent()) \
{ \
FPlatformMisc::PromptForRemoteDebugging(true); \
return false; \
} \
return true; \
} \
return false; \
}) && ([] () { PLATFORM_BREAK(); } (), false)))

#define ensure( InExpression ) UE_ENSURE_IMPL( , false, InExpression, TEXT(""))
#define ensureMsgf( InExpression, InFormat, ... ) UE_ENSURE_IMPL(&, false, InExpression, InFormat, ##__VA_ARGS__)
#define ensureAlways( InExpression ) UE_ENSURE_IMPL( , true, InExpression, TEXT(""))
#define ensureAlwaysMsgf( InExpression, InFormat, ... ) UE_ENSURE_IMPL(&, true, InExpression, InFormat, ##__VA_ARGS__)

#else // DO_CHECK

#define ensure( InExpression ) (!!(InExpression))
#define ensureMsgf( InExpression, InFormat, ... ) (!!(InExpression))
#define ensureAlways( InExpression ) (!!(InExpression))
#define ensureAlwaysMsgf( InExpression, InFormat, ... ) (!!(InExpression))

#endif // DO_CHECK

堡垒之夜为多平台的内容优化

自动化测试框架

《堡垒之夜》跨平台开发迭代经验分享

Cooker优化

  1. Shared DDC 必须要使用,对于常规的资源,DDC是一种非常有效的加速手段,例如纹理压缩需要在多个平台分别处理,而且压缩算法非常稳定,通过使用DDC,仅需要处理一次即可。
  2. Shaders与Texture 不同, 经常会发生变化,编译 Shader 可以使用 XGE (IncrediBuild)加速。(注:Fastbuild 好像也可以并行编译Shader,网上有相关的教程)

当DDC已经完全使用以后,Cook就成为了一个完全主线程绑定的任务了,它需要反复加载、保存资源,无法利用多线程进行并行化。所以建议最好使用核数较少,但是单核主频较高的主机进行Cook处理。

  1. 在 DefaultEngine.ini 设置 MaxMemoryAllowance 增大 Cook 使用内存数量,默认是16G。(注:Cook 是 IO密集,所以增加内存使用量可以减少因为内存不足导致的 Purge 次数)
  2. 使用 FCookModificationDelegate 来控制需要 Cook 的 Asset 数量。

Device Profile

在线迭代

RootComponent警告

1
LogActor: Warning: Bullet /Game/Map.Map:PersistentLevel.Bullet_0 has natively added scene component(s), but none of them were set as the actor's RootComponent - picking one arbitrarily

这是因为直接在无参构造函数里写了赋值:

1
2
3
4
5
ABullet::ABullet()
{
PrimaryActorTick.bCanEverTick = true;
RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("RootComponent"));
}

加上SetRootComponent或者在FObjectInitializer构造函数里写(建议)即可:

1
2
USceneComponent* NewRootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("RootComponent"));
SetRootComponent(NewRootComponent);

就像:

Runtime/Engine/Private/Camera/CameraActor.cpp
1
2
3
4
5
6
7
8
9
ACameraActor::ACameraActor(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
SceneComponent = CreateDefaultSubobject<USceneComponent>(TEXT("SceneComponent"));

// Make the scene component the root component
RootComponent = SceneComponent;
// ...
}

ConfigLayer

引擎Config的文件路径规则:

Runtime\Core\Private\Misc\ConfigCacheIni.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
/**
* Structure to define all the layers of the config system. Layers can be expanded by expansion files (NoRedist, etc), or by ini platform parents
* (coming soon from another branch)
*/
struct FConfigLayer
{
// Used by the editor to display in the ini-editor
const TCHAR* EditorName;
// Path to the ini file (with variables)
const TCHAR* Path;
// Path to the platform extension version
const TCHAR* PlatformExtensionPath;
// Special flag
EConfigLayerFlags Flag;

} GConfigLayers[] =
{
/**************************************************
**** CRITICAL NOTES
**** If you change this array, you need to also change EnumerateConfigFileLocations() in ConfigHierarchy.cs!!!
**** And maybe UObject::GetDefaultConfigFilename(), UObject::GetGlobalUserConfigFilename()
**************************************************/

// Engine/Base.ini
{ TEXT("AbsoluteBase"), TEXT("{ENGINE}Base.ini"), TEXT(""), EConfigLayerFlags::Required },

// Engine/Base*.ini
{ TEXT("Base"), TEXT("{ENGINE}{ED}{EF}Base{TYPE}.ini") },
// Engine/Platform/BasePlatform*.ini
{ TEXT("BasePlatform"), TEXT("{ENGINE}{ED}{PLATFORM}/{EF}Base{PLATFORM}{TYPE}.ini"), TEXT("{EXTENGINE}/{ED}{EF}Base{PLATFORM}{TYPE}.ini"), },
// Project/Default*.ini
{ TEXT("ProjectDefault"), TEXT("{PROJECT}{ED}{EF}Default{TYPE}.ini"), TEXT(""), EConfigLayerFlags::AllowCommandLineOverride | EConfigLayerFlags::GenerateCacheKey },
// Engine/Platform/Platform*.ini
{ TEXT("EnginePlatform"), TEXT("{ENGINE}{ED}{PLATFORM}/{EF}{PLATFORM}{TYPE}.ini"), TEXT("{EXTENGINE}/{ED}{EF}{PLATFORM}{TYPE}.ini") },
// Project/Platform/Platform*.ini
{ TEXT("ProjectPlatform"), TEXT("{PROJECT}{ED}{PLATFORM}/{EF}{PLATFORM}{TYPE}.ini"), TEXT("{EXTPROJECT}/{ED}{EF}{PLATFORM}{TYPE}.ini") },

// UserSettings/.../User*.ini
{ TEXT("UserSettingsDir"), TEXT("{USERSETTINGS}Unreal Engine/Engine/Config/User{TYPE}.ini") },
// UserDir/.../User*.ini
{ TEXT("UserDir"), TEXT("{USER}Unreal Engine/Engine/Config/User{TYPE}.ini") },
// Project/User*.ini
{ TEXT("GameDirUser"), TEXT("{PROJECT}User{TYPE}.ini"), TEXT(""), EConfigLayerFlags::GenerateCacheKey },
};

Mac生成的xocde工程

在UE项目的Intermediate/ProjectFilesIOS目录下,会创建一个项目同名的xcodeproj并且具有UE4.xcodeproj文件。

Unreal Insight默认端口

在Unreal Insight启动时默认监听本地的1980端口:

Source/Developer/TraceInsights/Private/Insights/TraceInsightsModule.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void FTraceInsightsModule::CreateDefaultStore()
{
const FString StoreDir = FPaths::ProjectSavedDir() / TEXT("TraceSessions");

FInsightsManager::Get()->SetStoreDir(StoreDir);

// Create the Store Service.
Trace::FStoreService::FDesc StoreServiceDesc;
StoreServiceDesc.StoreDir = *StoreDir;
StoreServiceDesc.RecorderPort = 1980;
StoreServiceDesc.ThreadCount = 2;
StoreService = TUniquePtr<Trace::FStoreService>(Trace::FStoreService::Create(StoreServiceDesc));

if (StoreService.IsValid())
{
ConnectToStore(TEXT("127.0.0.1"), StoreService->GetPort());
}
}

Unreal Insight的启动:Programs/UnrealInsights/Private/UserInterfaceCommand.cpp#L211

可以传递给Unreal Insight的参数:

  • -OpenTraceId=
  • -Store=-Store=127.0.0.1:1980
  • -StoreHost=
  • -StorePort=
  • -ExecOnAnalysisCompleteCmd=
  • -AutoQuit
  • -InsightsTest
  • -DebugTools

当地图相当大,在生成导航时会有以下提示:

1
LogNavigation: Error: Navmesh bounds are too large! Limiting requested tiles count (5472000) to: (1048576) for RecastNavMesh /Game/Level/Map.Map:PersistentLevel.RecastNavMesh-Default

这是因为地图太大,超出了Tile的数量限制。
引擎中默认Tile的最大数量为1<<20也就是1048576,这个值可以修改配置,但不能大于1<<30(因为它用int32存储)。

Source/Runtime/NavigationSystem/Public/NavMesh/RecastNavMesh.h
1
2
3
4
5
6
7
8
9
10
11
12
UCLASS(config=Engine, defaultconfig, hidecategories=(Input,Rendering,Tags,"Utilities|Transformation",Actor,Layers,Replication), notplaceable)
class NAVIGATIONSYSTEM_API ARecastNavMesh : public ANavigationData
{
// ...
/** Absolute hard limit to number of navmesh tiles. Be very, very careful while modifying it while
* having big maps with navmesh. A single, empty tile takes 176 bytes and empty tiles are
* allocated up front (subject to change, but that's where it's at now)
* @note TileNumberHardLimit is always rounded up to the closest power of 2 */
UPROPERTY(EditAnywhere, Category = Generation, config, meta = (ClampMin = "1", UIMin = "1"), AdvancedDisplay)
int32 TileNumberHardLimit;
// ...
};

以及使用:

Runtime/NavigationSystem/Private/NavMesh/RecastNavMesh.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
FRecastNavMeshGenerationProperties::FRecastNavMeshGenerationProperties()
{
TilePoolSize = 1024;
TileSizeUU = 988.f;
CellSize = 19;
CellHeight = 10;
AgentRadius = 34.f;
AgentHeight = 144.f;
AgentMaxSlope = 44.f;
AgentMaxStepHeight = 35.f;
MinRegionArea = 0.f;
MergeRegionSize = 400.f;
MaxSimplificationError = 1.3f; // from RecastDemo
TileNumberHardLimit = 1 << 20;
RegionPartitioning = ERecastPartitioning::Watershed;
LayerPartitioning = ERecastPartitioning::Watershed;
RegionChunkSplits = 2;
LayerChunkSplits = 2;
bSortNavigationAreasByCost = false;
bPerformVoxelFiltering = true;
bMarkLowHeightAreas = false;
bUseExtraTopCellWhenMarkingAreas = true;
bFilterLowSpanSequences = false;
bFilterLowSpanFromTileCache = false;
bFixedTilePoolSize = false;
}

FRecastNavMeshGenerationProperties::FRecastNavMeshGenerationProperties(const ARecastNavMesh& RecastNavMesh)
{
TilePoolSize = RecastNavMesh.TilePoolSize;
TileSizeUU = RecastNavMesh.TileSizeUU;
CellSize = RecastNavMesh.CellSize;
CellHeight = RecastNavMesh.CellHeight;
AgentRadius = RecastNavMesh.AgentRadius;
AgentHeight = RecastNavMesh.AgentHeight;
AgentMaxSlope = RecastNavMesh.AgentMaxSlope;
AgentMaxStepHeight = RecastNavMesh.AgentMaxStepHeight;
MinRegionArea = RecastNavMesh.MinRegionArea;
MergeRegionSize = RecastNavMesh.MergeRegionSize;
MaxSimplificationError = RecastNavMesh.MaxSimplificationError;
TileNumberHardLimit = RecastNavMesh.TileNumberHardLimit;
RegionPartitioning = RecastNavMesh.RegionPartitioning;
LayerPartitioning = RecastNavMesh.LayerPartitioning;
RegionChunkSplits = RecastNavMesh.RegionChunkSplits;
LayerChunkSplits = RecastNavMesh.LayerChunkSplits;
bSortNavigationAreasByCost = RecastNavMesh.bSortNavigationAreasByCost;
bPerformVoxelFiltering = RecastNavMesh.bPerformVoxelFiltering;
bMarkLowHeightAreas = RecastNavMesh.bMarkLowHeightAreas;
bUseExtraTopCellWhenMarkingAreas = RecastNavMesh.bUseExtraTopCellWhenMarkingAreas;
bFilterLowSpanSequences = RecastNavMesh.bFilterLowSpanSequences;
bFilterLowSpanFromTileCache = RecastNavMesh.bFilterLowSpanFromTileCache;
bFixedTilePoolSize = RecastNavMesh.bFixedTilePoolSize;
}
ARecastNavMesh::ARecastNavMesh(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
, bDrawFilledPolys(true)
, bDrawNavMeshEdges(true)
, bDrawNavLinks(true)
, bDrawOctreeDetails(true)
, bDrawMarkedForbiddenPolys(false)
, bDistinctlyDrawTilesBeingBuilt(true)
, DrawOffset(10.f)
, TilePoolSize(1024)
, MaxSimplificationError(1.3f) // from RecastDemo
, DefaultMaxSearchNodes(RECAST_MAX_SEARCH_NODES)
, DefaultMaxHierarchicalSearchNodes(RECAST_MAX_SEARCH_NODES)
, bPerformVoxelFiltering(true)
, bMarkLowHeightAreas(false)
, bUseExtraTopCellWhenMarkingAreas(true)
, bFilterLowSpanSequences(false)
, bFilterLowSpanFromTileCache(false)
, bStoreEmptyTileLayers(false)
, bUseVirtualFilters(true)
, bAllowNavLinkAsPathEnd(false)
, TileSetUpdateInterval(1.0f)
, NavMeshVersion(NAVMESHVER_LATEST)
, RecastNavMeshImpl(NULL)
{
HeuristicScale = 0.999f;
RegionPartitioning = ERecastPartitioning::Watershed;
LayerPartitioning = ERecastPartitioning::Watershed;
RegionChunkSplits = 2;
LayerChunkSplits = 2;
MaxSimultaneousTileGenerationJobsCount = 1024;
bDoFullyAsyncNavDataGathering = false;
TileNumberHardLimit = 1 << 20;

#if RECAST_ASYNC_REBUILDING
BatchQueryCounter = 0;
#endif // RECAST_ASYNC_REBUILDING


if (HasAnyFlags(RF_ClassDefaultObject) == false)
{
INC_DWORD_STAT_BY( STAT_NavigationMemory, sizeof(*this) );

FindPathImplementation = FindPath;
FindHierarchicalPathImplementation = FindPath;

TestPathImplementation = TestPath;
TestHierarchicalPathImplementation = TestHierarchicalPath;

RaycastImplementation = NavMeshRaycast;

RecastNavMeshImpl = new FPImplRecastNavMesh(this);

// add predefined areas up front
SupportedAreas.Add(FSupportedAreaData(UNavArea_Null::StaticClass(), RECAST_NULL_AREA));
SupportedAreas.Add(FSupportedAreaData(UNavArea_LowHeight::StaticClass(), RECAST_LOW_AREA));
SupportedAreas.Add(FSupportedAreaData(UNavArea_Default::StaticClass(), RECAST_DEFAULT_AREA));
}
}

TileNumberHardLimit的值可以通过配置文件进行设置:

DefaultEngine.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
[/Script/NavigationSystem.RecastNavMesh]
bDrawPolyEdges=False
bDistinctlyDrawTilesBeingBuilt=True
DrawOffset=10.000000
bFixedTilePoolSize=False
TilePoolSize=1024
TileSizeUU=1000.000000
CellSize=19.000000
CellHeight=10.000000
AgentRadius=34.000000
AgentHeight=144.000000
AgentMaxHeight=160.000000
AgentMaxSlope=44.000000
AgentMaxStepHeight=35.000000
MinRegionArea=0.000000
MergeRegionSize=400.000000
MaxSimplificationError=1.300000
MaxSimultaneousTileGenerationJobsCount=1024
TileNumberHardLimit=1048576
DefaultDrawDistance=5000.000000
DefaultMaxSearchNodes=2048.000000
DefaultMaxHierarchicalSearchNodes=2048.000000
RegionPartitioning=Watershed
LayerPartitioning=Watershed
RegionChunkSplits=2
LayerChunkSplits=2
bSortNavigationAreasByCost=False
bPerformVoxelFiltering=True
bMarkLowHeightAreas=False
bDoFullyAsyncNavDataGathering=False
bUseBetterOffsetsFromCorners=True
bUseVirtualFilters=True
bUseVoxelCache=False
TileSetUpdateInterval=1.000000
HeuristicScale=0.999000
VerticalDeviationFromGroundCompensation=0.000000
bForceRebuildOnLoad=True

加密PakIndex造成的无法挂载shaderbytecode

在UE5中Project Settings-Crypto中开启EncryptPakIndex,发现在自动挂载中无法加载最新的Shaderbytecode,造成材质丢失(不确定UE4中是否存在)。

运行时给Enum添加值

通过UEnum的SetEnums可以实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
UEnum* TargetPlatform = UFlibPatchParserHelper::GetUEnum<ETargetPlatform>();
uint64 MaxEnumValue = TargetPlatform->GetMaxEnumValue();
FString EnumName = TargetPlatform->GetName();
TArray<TPair<FName, int64>> EnumNames;

for (ETargetPlatform Platform:TEnumRange<ETargetPlatform>())
{
EnumNames.Emplace(TargetPlatform->GetNameByValue((int64)Platform),(int64)Platform);
}
for(const auto& AppendEnumItem:AppendPlatformEnums)
{
++MaxEnumValue;
EnumNames.Emplace(
FName(*FString::Printf(TEXT("%s::%s"),*EnumName,*AppendEnumItem)),
MaxEnumValue
);
}
#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION > 25
TargetPlatform->SetEnums(EnumNames,UEnum::ECppForm::EnumClass,EEnumFlags::None,true);
#else
TargetPlatform->SetEnums(EnumNames,UEnum::ECppForm::EnumClass,true);
#endif

遍历Enum的所有值

需要用ENUM_RANGE_BY_COUNT标记,然后可以用TEnumRange遍历。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
UENUM()
enum class ETargetPlatform : uint8
{
None,
AllPlatforms,
Count UMETA(Hidden)
};
ENUM_RANGE_BY_COUNT(ETargetPlatform, ETargetPlatform::Count);

// for each
for (ETargetPlatform Platform:TEnumRange<ETargetPlatform>())
{
//...
}

IoStore的生成分析

UE4.25添加了一个新的打包选项IO Store(在UE5中默认开启):

Youtube上Epic JP有一个介绍视频:【UE4.25 新機能】ロードの高速化機能「IOStore」について

在UE5中默认是开启的,打包时除了pak之外多了两种类型的文件:ucasutoc,在运行时Mount pak时也会mount它们,看了下代码在UE4.25/26就中存在,有时间详细地分析下它们的作用和加载流程。

它们也在CopyBuildToStagingDirectory.Automation.cs文件中的CreatePaks函数中和Pak一同创建:

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
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
private static bool ShouldCreateIoStoreContainerFiles(ProjectParams Params, Platform StageTargetPlatform)
{
if (Params.CookOnTheFly)
{
return false;
}

if (Params.SkipIoStore)
{
return false;
}

if (Params.IoStore)
{
return true;
}

if (Params.Stage && !Params.SkipStage)
{
ConfigHierarchy PlatformGameConfig = ConfigCache.ReadHierarchy(ConfigHierarchyType.Game, DirectoryReference.FromFile(Params.RawProjectPath), StageTargetPlatform.IniPlatformType);
bool bUseIoStore = false;
PlatformGameConfig.GetBool("/Script/UnrealEd.ProjectPackagingSettings", "bUseIoStore", out bUseIoStore);
return bUseIoStore;
}

return false;
}

private static void CreatePaks(ProjectParams Params, DeploymentContext SC, List<CreatePakParams> PakParamsList, EncryptionAndSigning.CryptoSettings CryptoSettings, FileReference CryptoKeysCacheFilename)
{
// ...
if (ShouldCreateIoStoreContainerFiles(Params, SC.StageTargetPlatform))
{
bool bAllowBulkDataInIoStore = true;
if(!PlatformEngineConfig.GetBool("Core.System", "AllowBulkDataInIoStore", out bAllowBulkDataInIoStore))
{
bAllowBulkDataInIoStore = true; // Default is to allow it in the IoStore
}

UnrealPakResponseFile = new Dictionary<string, string>();
Dictionary<string, string> IoStoreResponseFile = new Dictionary<string, string>();
foreach (var Entry in PakParams.UnrealPakResponseFile)
{
// Temporary solution to filter non cooked packages from I/O store container file(s)
if (SC.OnlyAllowPackagesFromStdCookPathInIoStore && !Entry.Key.ToLower().Contains("\\saved\\cooked\\"))
{
UnrealPakResponseFile.Add(Entry.Key, Entry.Value);
continue;
}

if (Path.GetExtension(Entry.Key).Contains(".uasset") ||
Path.GetExtension(Entry.Key).Contains(".umap"))
{
IoStoreResponseFile.Add(Entry.Key, Entry.Value);
}
else if(Path.GetExtension(Entry.Key).Contains(".ubulk") ||
Path.GetExtension(Entry.Key).Contains(".uptnl"))
{
if(bAllowBulkDataInIoStore)
{
IoStoreResponseFile.Add(Entry.Key, Entry.Value);
}
else
{
UnrealPakResponseFile.Add(Entry.Key, Entry.Value);
}
}
else if (!Path.GetExtension(Entry.Key).Contains(".uexp"))
{
UnrealPakResponseFile.Add(Entry.Key, Entry.Value);
}

}

string ContainerPatchSourcePath = null;
if (Params.HasBasedOnReleaseVersion)
{
string ContainerWildcard = PakParams.PakName + "-" + SC.FinalCookPlatform + "*.utoc";
ContainerPatchSourcePath = CombinePaths(Params.GetBasedOnReleaseVersionPath(SC, Params.Client), ContainerWildcard);
}
bool bGenerateDiffPatch = bShouldGeneratePatch && !ShouldSkipGeneratingPatch(PlatformGameConfig, PakParams.PakName);
bool bCompressContainers = PakParams.bCompressed || Params.AdditionalIoStoreOptions.Contains("-compressed");

IoStoreCommands.Add(GetIoStoreCommandArguments(
IoStoreResponseFile,
PakParams.PakName,
OutputLocation,
bCompressContainers,
CryptoSettings,
PakParams.EncryptionKeyGuid,
ContainerPatchSourcePath,
bGenerateDiffPatch,
Params.HasDLCName));
}
// ...
}

与生成Pak类似,也会生成一个PakListIoStore_*.txt的文件,格式与Paklist*.txt相同。

1
2
3
4
5
6
7
"C:\Users\lipengzha\Documents\Unreal Projects\ThirdPerson_UE5\Saved\Cooked\Windows\ThirdPerson_UE5\Content\BP_GF_Actor.uasset" "../../../ThirdPerson_UE5/Content/BP_GF_Actor.uasset"
"C:\Users\lipengzha\Documents\Unreal Projects\ThirdPerson_UE5\Saved\Cooked\Windows\Engine\Content\Animation\DefaultAnimBoneCompressionSettings.uasset" "../../../Engine/Content/Animation/DefaultAnimBoneCompressionSettings.uasset"
"C:\Users\lipengzha\Documents\Unreal Projects\ThirdPerson_UE5\Saved\Cooked\Windows\Engine\Content\Animation\DefaultAnimCurveCompressionSettings.uasset" "../../../Engine/Content/Animation/DefaultAnimCurveCompressionSettings.uasset"
"C:\Users\lipengzha\Documents\Unreal Projects\ThirdPerson_UE5\Saved\Cooked\Windows\Engine\Content\BasicShapes\Cone.uasset" "../../../Engine/Content/BasicShapes/Cone.uasset"
"C:\Users\lipengzha\Documents\Unreal Projects\ThirdPerson_UE5\Saved\Cooked\Windows\Engine\Content\BasicShapes\Cone.ubulk" "../../../Engine/Content/BasicShapes/Cone.ubulk"
"C:\Users\lipengzha\Documents\Unreal Projects\ThirdPerson_UE5\Saved\Cooked\Windows\Engine\Content\BasicShapes\Cube.uasset" "../../../Engine/Content/BasicShapes/Cube.uasset"
"C:\Users\lipengzha\Documents\Unreal Projects\ThirdPerson_UE5\Saved\Cooked\Windows\Engine\Content\BasicShapes\Cube.ubulk" "../../../Engine/Content/BasicShapes/Cube.ubulk"

但里面只包含.uasset/.umap的资源,其余的文件存储在pak中,相当于把原本的pak文件拆分成utoc和pak两个文件,加速IO。

UE提供了一个Commandlet,方便调用:

Editor/UnrealEd/Private/Commandlets/IoStoreCommandlet.cpp
1
2
3
4
int32 UIoStoreCommandlet::Main(const FString& Params)
{
return CreateIoStoreContainerFiles(*Params);
}

核心实现是在IoStoreUtilities.cpp中定义的CreateIoStoreContainerFiles函数。

IoStoreCommandlet的调用方式:

1
2
3
4
5
6
7
8
9
10
"C:\Program Files\Epic Games\UE_5.0ea\Engine\Binaries\Win64\UnrealEditor-Cmd.exe"
"C:\Users\lipengzha\Documents\Unreal Projects\ThirdPerson_UE5\ThirdPerson_UE5.uproject"
-run=IoStore
-CreateGlobalContainer="C:\Users\lipengzha\Documents\Unreal Projects\ThirdPerson_UE5\Saved\StagedBuilds\Windows\ThirdPerson_UE5\Content\Paks\global.utoc"
-CookedDirectory="C:\Users\lipengzha\Documents\Unreal Projects\ThirdPerson_UE5\Saved\Cooked\Windows"
-Commands="C:\Users\lipengzha\AppData\Roaming\Unreal Engine\AutomationTool\Logs\C+Program+Files+Epic+Games+UE_5.0ea\IoStoreCommands.txt"
-CookerOrder="C:\Users\lipengzha\Documents\Unreal Projects\ThirdPerson_UE5\Build\Windows\FileOpenOrder\CookerOpenOrder.log"
-patchpaddingalign=2048
-cryptokeys="C:\Users\lipengzha\Documents\Unreal Projects\ThirdPerson_UE5\Saved\Cooked\Windows\ThirdPerson_UE5\Metadata\Crypto.json"
-TargetPlatform=Windows

最小执行命令:

1
2
3
4
5
6
7
8
Engine\Binaries\Win64\UE4Editor-Cmd.exe
E:\UnrealProjects\StarterContent\StarterContent.uproject
-run=IoStore
-CreateGlobalContainer=E:\UnrealProjects\StarterContent\Saved\StagedBuilds\WindowsNoEditor\StarterContent\Content\Paks\global.utoc
-CookedDirectory=E:\UnrealProjects\StarterContent\Saved\Cooked\WindowsNoEditor
-Commands="C:\Users\visionsmile\AppData\Roaming\Unreal Engine\AutomationTool\Logs\E+UnrealEngine+Launcher+UE_4.26\IoStoreCommands.txt"
-CookerOrder="C:\Users\lipengzha\Documents\Unreal Projects\ThirdPerson_UE5\Build\Windows\FileOpenOrder\CookerOpenOrder.log"
-TargetPlatform=WindowsNoEditor

IoStoreCommands.txt的内容为:

1
-Output="D:\ThirdPerson_UE5\Saved\StagedBuilds\Windows\ThirdPerson_UE5\Content\Paks\ThirdPerson_UE5-Windows.utoc" -ContainerName=ThirdPerson_UE5 -ResponseFile="D:\PakListIoStore_ThirdPerson_UE5.txt"

创建IO Store的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
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
LogIoStore: Display: ==================== IoStore Utils ====================
LogIoStore: Display: Parsing crypto keys from a crypto key cache file 'C:\Users\lipengzha\Documents\Unreal Projects\ThirdPerson_UE5\Saved\Cooked\Windows\ThirdPerson_UE5\Metadata\Crypto.json'
LogIoStore: Display: Container signing - DISABLED
LogIoStore: Display: Directory index - ENABLED
LogIoStore: Display: Using memory mapping alignment '16384'
LogIoStore: Display: Using compression block size '65536'
LogIoStore: Display: Using compression block alignment '2048'
LogIoStore: Display: Using max partition size '0'
LogIoStore: Display: Using command list file: 'C:\Users\lipengzha\AppData\Roaming\Unreal Engine\AutomationTool\Logs\C+Program+Files+Epic+Games+UE_5.0ea\IoStoreCommands.txt'
LogIoStore: Display: Using target platform 'Windows'
LogIoStore: Display: Searching for cooked assets in folder 'C:\Users\lipengzha\Documents\Unreal Projects\ThirdPerson_UE5\Saved\Cooked\Windows'
LogIoStore: Display: Found '1342' files
LogIoStore: Display: Loaded Bulk Data manifest 'C:\Users\lipengzha\Documents\Unreal Projects\ThirdPerson_UE5\Saved\Cooked\Windows/ThirdPerson_UE5/Metadata/BulkDataInfo.ubulkmanifest'
LogIoStore: Display: Creating container targets...
LogIoStore: Display: Parsing packages...
LogIoStore: Display: Reading package assets...
LogIoStore: Display: Parsing package assets...
LogIoStore: Display: Parsing 0/556: 'C:/Users/lipengzha/Documents/Unreal Projects/ThirdPerson_UE5/Saved/Cooked/Windows/ThirdPerson_UE5/Content/ThirdPersonCPP/Chunks/BP_LightInstance.uasset'
LogIoStore: Display: Creating global script objects...
LogIoStore: Display: Creating global imports and exports...
LogIoStore: Display: Converting export map import indices...
LogIoStore: Display: Conforming localized packages...
LogIoStore: Display: Adding localized import packages...
LogIoStore: Display: Conforming localized imports...
LogIoStore: Display: Adding preload dependencies...
LogIoStore: Display: Building bundles...
LogIoStore: Display: Finalizing name maps...
LogIoStore: Display: Finalizing package headers...
LogIoStore: Display: Creating disk layout...
LogIoStore: Display: Ordered 0/556 packages using game open order
LogIoStore: Display: Ordered 551/556 packages using cooker open order
LogIoStore: Display: Ordered 5 packages using fallback bundle order
LogIoStore: Display: Finalizing initial load...
LogIoStore: Display: Serializing global meta data
LogIoStore: Display: Saving global name map to container file
LogIoStore: Display: Serializing container(s)...
LogIoStore: Display: Hashed, Compressed, Serialized: 790, 790, 790 / 790
LogIoStore: Display: Calculating stats...
LogIoStore: Display: --------------------------------------------------- IoDispatcher --------------------------------------------------------
LogIoStore: Display:
LogIoStore: Display: Container Flags TOC Size (KB) TOC Entries Size (MB) Compressed (MB)
LogIoStore: Display: -------------------------------------------------------------------------------------------------------------------------
LogIoStore: Display: global -/-/-/- 0.52 3 1.10 -
LogIoStore: Display: ThirdPerson_UE5-Windows -/-/-/I 189.89 787 571.22 -
LogIoStore: Display: TOTAL 190.41 790 572.32 -
LogIoStore: Display:
LogIoStore: Display: ** Flags: (C)ompressed / (E)ncrypted / (S)igned) / (I)ndexed) **
LogIoStore: Display:
LogIoStore: Display: Compression block padding: 0.55 MB
LogIoStore: Display:
LogIoStore: Display: -------------------------------------------- Container Directory Index --------------------------------------------------
LogIoStore: Display: Container Size (KB)
LogIoStore: Display: global 0.00
LogIoStore: Display: ThirdPerson_UE5-Windows 33.53
LogIoStore: Display:
LogIoStore: Display: ---------------------------------------------- Container Patch Report ---------------------------------------------------
LogIoStore: Display: Container Total (count) Modified (count) Added (count) Modified (MB) Added (MB)
LogIoStore: Display: global 3 0 0 0.00 0.00
LogIoStore: Display: ThirdPerson_UE5-Windows 787 0 787 0.00 570.67
LogIoStore: Display:
LogIoStore: Display:
LogIoStore: Display: --------------------------------------------------- PackageStore (KB) ---------------------------------------------------
LogIoStore: Display:
LogIoStore: Display: Container Store Size Packages Localized
LogIoStore: Display: -------------------------------------------------------------------------------------------------------------------------
LogIoStore: Display: ThirdPerson_UE5 22 556 0
LogIoStore: Display: TOTAL 22 556 0
LogIoStore: Display:
LogIoStore: Display:
LogIoStore: Display: --------------------------------------------------- PackageHeader (KB) --------------------------------------------------
LogIoStore: Display:
LogIoStore: Display: Container Header Summary Graph ImportMap ExportMap NameMap
LogIoStore: Display: -------------------------------------------------------------------------------------------------------------------------
LogIoStore: Display: ThirdPerson_UE5 911 35 15 35 165 662
LogIoStore: Display: TOTAL 911 35 15 35 165 662
LogIoStore: Display:
LogIoStore: Display:
LogIoStore: Display: Input: 132.91 MB UExp
LogIoStore: Display: Input: 1.12 MB UAsset
LogIoStore: Display: Input: 0.10 MB FPackageFileSummary
LogIoStore: Display: Input: 556 Packages
LogIoStore: Display: Input: 641 Imported package entries
LogIoStore: Display: Input: 327 Packages without imports
LogIoStore: Display: Input: 26088 Name map entries
LogIoStore: Display: Input: 10631 PreloadDependencies entries
LogIoStore: Display: Input: 4480 ImportMap entries
LogIoStore: Display: Input: 2342 ExportMap entries
LogIoStore: Display: Input: 712 Public exports
LogIoStore: Display:
LogIoStore: Display: Output: 558 Export bundles
LogIoStore: Display: Output: 4684 Export bundle entries
LogIoStore: Display: Output: 641 Export bundle arcs
LogIoStore: Display: Output: 18041 Public runtime script objects
LogIoStore: Display: Output: 1.10 MB InitialLoadData
LogInit: Display:
LogInit: Display: Warning/Error Summary (Unique only)
LogInit: Display: -----------------------------------

引擎中mount ucas/utoc的Initialize代码在:IPlatformFilePak.cpp#L7136

FIoDispatcher也在IPlatformFilePak.cppInitialize中初始化:IPlatformFilePak.cpp#L6862

运行时Log:

1
2
3
4
5
6
7
8
9
10
11
12
13
LogIoDispatcher: Display: Reading toc: ../../../ThirdPerson_UE5/Content/Paks/global.utoc
LogIoDispatcher: Display: Mounting container '../../../ThirdPerson_UE5/Content/Paks/global' in location slot 0
LogPakFile: Display: Initialized I/O dispatcher
LogPakFile: Display: Found Pak file ../../../ThirdPerson_UE5/Content/Paks/ThirdPerson_UE5-Windows.pak attempting to mount.
LogPakFile: Display: Mounting pak file ../../../ThirdPerson_UE5/Content/Paks/ThirdPerson_UE5-Windows.pak.
LogPakFile: PakFile PrimaryIndexSize=14754
LogPakFile: PakFile PathHashIndexSize=37787
LogPakFile: PakFile FullDirectoryIndexSize=36832
LogIoDispatcher: Display: Reading toc: ../../../ThirdPerson_UE5/Content/Paks/ThirdPerson_UE5-Windows.utoc
LogIoDispatcher: Display: Mounting container '../../../ThirdPerson_UE5/Content/Paks/ThirdPerson_UE5-Windows' in location slot 0
LogPakFile: Display: Mounted IoStore container "../../../ThirdPerson_UE5/Content/Paks/ThirdPerson_UE5-Windows"
LogShaderLibrary: Display: ShaderCodeLibraryPakFileMountedCallback: PakFile '../../../ThirdPerson_UE5/Content/Paks/ThirdPerson_UE5-Windows.pak' (chunk index -1, root '../../../') mounted
LogShaderLibrary: Display: ShaderCodeLibraryPakFileMountedCallback: pending pak file info (ChunkID:-1 Root:../../../ File:../../../ThirdPerson_UE5/Content/Paks/ThirdPerson_UE5-Windows.pak)

注意:UE5默认开启了Async Loading Thread Enabled,在加载时可能会遇到Shader失败的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[2021.06.04-07.56.38:156][144]LogNet: Browse: /Game/StarterContent/Maps/StarterMap
[2021.06.04-07.56.38:156][144]LogLoad: LoadMap: /Game/StarterContent/Maps/StarterMap
[2021.06.04-07.56.38:156][144]LogWorld: BeginTearingDown for /Game/NewMap
[2021.06.04-07.56.38:157][144]LogWorld: UWorld::CleanupWorld for NewMap, bSessionEnded=true, bCleanupResources=true
[2021.06.04-07.56.38:157][144]LogSlate: InvalidateAllWidgets triggered. All widgets were invalidated
[2021.06.04-07.56.38:187][144]LogStreaming: Display: 0.035 ms (0.013+0.022) ms for processing 33/149 objects in NotifyUnreachableObjects( Queued=0, Async=0). Removed 12/12 (103->91 tracked) packages and 21/21 (139->118 tracked) public exports.
[2021.06.04-07.56.38:188][144]LogAudio: Display: Audio Device unregistered from world 'None'.
[2021.06.04-07.56.38:189][144]LogUObjectHash: Compacting FUObjectHashTables data took 0.54ms
[2021.06.04-07.56.38:192][144]LogStreaming: Display: FlushAsyncLoading: 1 QueuedPackages, 0 AsyncPackages
[2021.06.04-07.56.38:206][144]LogStreaming: Warning: ImportPackages: SkipPackage: None (0xC0782D3BC7B113E7) - Skipping non mounted imported package with id '0xA6A13DDB269EBB86'
[2021.06.04-07.56.38:206][144]LogStreaming: Warning: ImportPackages: SkipPackage: None (0x18C447EA162E9CF0) - Skipping non mounted imported package with id '0x592F17301901E4D4'
[2021.06.04-07.56.38:206][144]LogStreaming: Warning: ImportPackages: SkipPackage: None (0xAF2D3B82DFED12AB) - Skipping non mounted imported package with id '0x90DE907FA36D8947'
[2021.06.04-07.56.38:206][144]LogStreaming: Warning: ImportPackages: SkipPackage: None (0x89DE226562FF0557) - Skipping non mounted imported package with id '0x90DE907FA36D8947'
[2021.06.04-07.56.38:206][144]LogStreaming: Warning: ImportPackages: SkipPackage: None (0xFA20E9343783C7DC) - Skipping non mounted imported package with id '0x90DE907FA36D8947'
// .....
[2021.06.04-07.56.38:207][144]LogStreaming: Warning: ImportPackages: SkipPackage: None (0xB64F7A6CD99A047) - Skipping non mounted imported package with id '0x90DE907FA36D8947'
[2021.06.04-07.56.38:207][144]LogStreaming: Warning: ImportPackages: SkipPackage: None (0xFEFAEB73A2846157) - Skipping non mounted imported package with id '0x90DE907FA36D8947'
[2021.06.04-07.56.38:209][144]LogStreaming: Warning: ImportPackages: SkipPackage: None (0x997474132A59335D) - Skipping non mounted imported package with id '0x90DE907FA36D8947'
[2021.06.04-07.56.38:209][144]LogStreaming: Warning: LoadPackage: SkipPackage: /Game/StarterContent/Audio/Starter_Wind05 (0x5E446250F73B1FA8) - The package to load does not exist on disk or in the loader
[2021.06.04-07.56.38:209][144]LogStreaming: Warning: LoadPackage: SkipPackage: /Game/StarterContent/Audio/Starter_Birds01 (0x724FF2357C734AD2) - The package to load does not exist on disk or in the loader
[2021.06.04-07.56.38:209][144]LogStreaming: Warning: LoadPackage: SkipPackage: /Game/StarterContent/Audio/Starter_Wind06 (0x982833CC14F6C1E7) - The package to load does not exist on disk or in the loader
[2021.06.04-07.56.38:209][144]LogStreaming: Warning: LoadPackage: SkipPackage: /Game/StarterContent/Audio/Collapse01 (0x938C37337F3982BF) - The package to load does not exist on disk or in the loader
[2021.06.04-07.56.38:210][144]LogStreaming: Warning: LoadPackage: SkipPackage: /Game/StarterContent/Audio/Collapse02 (0x3820625B5DBE09A3) - The package to load does not exist on disk or in the loader
[2021.06.04-07.56.38:213][144]LogStreaming: Warning: LoadPackage: SkipPackage: /Game/StarterContent/Audio/Starter_Music01 (0x1D0AA270D31EFC87) - The package to load does not exist on disk or in the loader

Project Settings-Engine-Streaming中关闭Async Loading Thread Enabled即可成功加载:

4.25.2- Mac打包的metal-ar错误

有执行metal-ar的报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
UATHelper: Packaging (iOS):   LogShaders: Error: Archiving failed: metal-ar failed with code 2: Couldn't posix_spawn: error 7
UATHelper: Packaging (iOS): LogShaders: Error: Archiving failed: metal-ar failed with code 2: Couldn't posix_spawn: error 7
UATHelper: Packaging (iOS): LogShaders: Error: Archiving failed: metal-ar failed with code 2: Couldn't posix_spawn: error 7
UATHelper: Packaging (iOS): LogShaders: Error: Archiving failed: no valid input for metallib.
UATHelper: Packaging (iOS): LogShaders: Error: Archiving failed: no valid input for metallib.
UATHelper: Packaging (iOS): LogShaders: Error: Archiving failed: no valid input for metallib.
UATHelper: Packaging (iOS): LogZipArchiveWriter: Display: Closing zip file with 25491 entries.
UATHelper: Packaging (iOS): CookResults: Error: Package Native Shader Library failed for IOS.
UATHelper: Packaging (iOS): LogCook: Display: Saved scl.csv /Users/buildmachine/Documents/BuildWorkspace/workspace/PackageClient/Client/Saved/Cooked/IOS/FGame/Metadata/PipelineCaches/ShaderStableInfo-Global-SF_METAL.scl.csv for platform IOS
UATHelper: Packaging (iOS): LogCook: Display: Saved scl.csv /Users/buildmachine/Documents/BuildWorkspace/workspace/PackageClient/Client/Saved/Cooked/IOS/FGame/Metadata/PipelineCaches/ShaderStableInfo-FGame-SF_METAL.scl.csv for platform IOS
PackagingResults: Error: Archiving failed: metal-ar failed with code 2: Couldn't posix_spawn: error 7
PackagingResults: Error: Archiving failed: metal-ar failed with code 2: Couldn't posix_spawn: error 7
PackagingResults: Error: Archiving failed: metal-ar failed with code 2: Couldn't posix_spawn: error 7
PackagingResults: Error: Archiving failed: no valid input for metallib.
PackagingResults: Error: Archiving failed: no valid input for metallib.
PackagingResults: Error: Archiving failed: no valid input for metallib.
PackagingResults: Error: Package Native Shader Library failed for IOS.

引擎中调用metal-ar的代码如下:

感觉错误的问题像是传递给metal-ar的参数太长了导致的,把metal-ar参数打出来发现确实非常长:

解决办法,把在Mac上GetMaxArgLength的返回值改成一个较小的值或者从环境变量中获取即可:

Developer\Apple\MetalShaderFormat\Private\MetalShaderCompiler.cpp
1
2
3
4
5
6
7
8
9
10
11
static uint32 GetMaxArgLength()
{
#if PLATFORM_MAC && !UNIXLIKE_TO_MAC_REMOTE_BUILDING
// 引擎中原始代码为返回ARG_MAX
// return ARG_MAX;
return 4096;
#else
// Ask the remote machine via "getconf ARG_MAX"
return 1024;
#endif
}

在官方版本的4.25.2中已经修复:Use runtime evaluation of ARG_MAX instead of the compile-time constant (which in Xcode 12 is now 1m)

Developer\Apple\MetalShaderFormat\Private\MetalShaderCompiler.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
static uint32 GetMaxArgLength()
{
#if PLATFORM_MAC && !UNIXLIKE_TO_MAC_REMOTE_BUILDING
static uint32 MaxLength = 0;
if (!MaxLength)
{
// It's dangerous to use "ARG_MAX" directly because it's a compile time constant and may not be compatible with the running OS.
// It's safer to get the number from "getconf ARG_MAX" and only use the constant as the fallback
FString StdOut, StdError;
if (ExecRemoteProcess(TEXT("/usr/bin/getconf"), TEXT("ARG_MAX"), nullptr, &StdOut, &StdError))
{
MaxLength = FCString::Atoi(*StdOut);
check(MaxLength > 0);
UE_LOG(LogMetalShaderCompiler, Display, TEXT("Set MaxArgLength to %d via getconf"), MaxLength);
}
else
{
MaxLength = FMath::Min(ARG_MAX, 256 * 1024);
UE_LOG(LogMetalShaderCompiler, Warning, TEXT("Failed to determine MaxArgLength via getconf: %s\nSet it to %d which is the lesser of MAX_ARG and the value from the 10.15 SDK"), *StdError, MaxLength);
}
}
return MaxLength;
#else
// Ask the remote machine via "getconf ARG_MAX"
return 1024;
#endif
}

关闭UnityBuild

默认情况下,UE默认开启了bUseUnityBuild,会把多个cpp合并成一个翻译单元进行编译,加快项目的编译速度,编译时有*_1_of_8.cpp.obj等log。所以如果项目的头文件包含不规范,编译时头文件检测可能会出现问题:修改了A文件导致B文件出现错误。

所以,想在检测出中这种错误,可以关闭UnityBuild,使每个cpp都作为单独的翻译单元编译,有两种方法:

  1. 对整个工程(包含插件)关闭bUseUnityBuild,在项目的Target.cs中添加
target.cs
1
2
bForceUnityBuild = false;
bUseUnityBuild = false;

不修改代码,也可以在BuildCongiguration.xml中配置关闭:

1
2
3
4
5
6
7
8
9
10
11
12
13
/// <summary>
/// Whether to unify C++ code into larger files for faster compilation.
/// </summary>
[CommandLine("-DisableUnity", Value = "false")]
[XmlConfigFile(Category = "BuildConfiguration")]
public bool bUseUnityBuild = true;

/// <summary>
/// Whether to force C++ source files to be combined into larger files for faster compilation.
/// </summary>
[CommandLine("-ForceUnity")]
[XmlConfigFile(Category = "BuildConfiguration")]
public bool bForceUnityBuild = false;
  1. 对单个模块关闭UnityBuild,在Build.cs中关闭
1
bUseUnity = false;

可以在Target.cs中指定关闭UnityBuild的模块,也可以配置在BuildConfiguration.xml中:

1
2
3
4
5
/// <summary>
/// List of modules to disable unity builds for
/// </summary>
[XmlConfigFile(Category = "ModuleConfiguration", Name = "DisableUnityBuild")]
public string[] DisableUnityBuildForModules = null;

可以在Intermediate\Build\Win64\UE4Editor\Development\Client等路径下看到生成的大量.response文件。

TextureStreaming

Runtime\Engine\Private\Streaming\TextureStreamingHelpers.cpp
1
2
3
4
5
6
7
8
9
#if PLATFORM_SUPPORTS_TEXTURE_STREAMING
TAutoConsoleVariable<int32> CVarSetTextureStreaming(
TEXT("r.TextureStreaming"),
1,
TEXT("Allows to define if texture streaming is enabled, can be changed at run time.\n")
TEXT("0: off\n")
TEXT("1: on (default)"),
ECVF_Default | ECVF_RenderThreadSafe);
#endif

引擎中的默认配置:

Engine/Config/BaseScalability.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
[TextureQuality@0]
; Must be used with r.streaming.usepertexturebias set to 1. Otherwise, all textures will have a constant 16 mip bias
r.Streaming.MipBias=16
r.Streaming.AmortizeCPUToGPUCopy=1
r.Streaming.MaxNumTexturesToStreamPerFrame=1
r.Streaming.Boost=0.3
r.MaxAnisotropy=0
r.VT.MaxAnisotropy=4
r.Streaming.LimitPoolSizeToVRAM=1
r.Streaming.PoolSize=400
r.Streaming.MaxEffectiveScreenSize=0

[TextureQuality@1]
r.Streaming.MipBias=1
r.Streaming.AmortizeCPUToGPUCopy=0
r.Streaming.MaxNumTexturesToStreamPerFrame=0
r.Streaming.Boost=1
r.MaxAnisotropy=2
r.VT.MaxAnisotropy=4
r.Streaming.LimitPoolSizeToVRAM=1
r.Streaming.PoolSize=600
r.Streaming.MaxEffectiveScreenSize=0

[TextureQuality@2]
r.Streaming.MipBias=0
r.Streaming.AmortizeCPUToGPUCopy=0
r.Streaming.MaxNumTexturesToStreamPerFrame=0
r.Streaming.Boost=1
r.MaxAnisotropy=4
r.VT.MaxAnisotropy=8
r.Streaming.LimitPoolSizeToVRAM=1
r.Streaming.PoolSize=800
r.Streaming.MaxEffectiveScreenSize=0

[TextureQuality@3]
r.Streaming.MipBias=0
r.Streaming.AmortizeCPUToGPUCopy=0
r.Streaming.MaxNumTexturesToStreamPerFrame=0
r.Streaming.Boost=1
r.MaxAnisotropy=8
r.VT.MaxAnisotropy=8
r.Streaming.LimitPoolSizeToVRAM=0
r.Streaming.PoolSize=1000
r.Streaming.MaxEffectiveScreenSize=0

[TextureQuality@Cine]
r.Streaming.MipBias=0
r.Streaming.AmortizeCPUToGPUCopy=0
r.Streaming.MaxNumTexturesToStreamPerFrame=0
r.Streaming.Boost=1
r.MaxAnisotropy=8
r.VT.MaxAnisotropy=8
r.Streaming.LimitPoolSizeToVRAM=0
r.Streaming.PoolSize=3000
r.Streaming.MaxEffectiveScreenSize=0

PackagePath获取资源磁盘路径

/Game/Texture/TextureBG获取uasset在本地磁盘的路径。

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
FString LongPackageName = TEXT("/Game/Texture/TextureBG")
TArray<FAssetData> AssetsData;
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
AssetRegistryModule.Get().GetAssetsByPackageName(*LongPackageName, AssetsData, true);
UPackage* Package = AssetsData[index].GetPackage();
FString AssetDiskPath;
const FString* PackageExtension = Package->ContainsMap() ? &FPackageName::GetMapPackageExtension() : &FPackageName::GetAssetPackageExtension();
FPackageName::TryConvertLongPackageNameToFilename(AssetsData[0].PackageName.ToString(), AssetDiskPath, *PackageExtension);
````

## 关闭PCH
在TargetRules.cs中有`bUsePCHFiles`和`bUseSharedPCHs`两个选项,可以控制项目是否开启PCH:

```cpp UnrealBuildTool\Configuration\TargetRules.cs
/// <summary>
/// Whether PCH files should be used.
/// </summary>
[CommandLine("-NoPCH", Value = "false")]
[XmlConfigFile(Category = "BuildConfiguration")]
public bool bUsePCHFiles = true;

/// <summary>
/// Enables "Shared PCHs", a feature which significantly speeds up compile times by attempting to
/// share certain PCH files between modules that UBT detects is including those PCH's header files.
/// </summary>
[CommandLine("-NoSharedPCH", Value = "false")]
[XmlConfigFile(Category = "BuildConfiguration")]
public bool bUseSharedPCHs = true;

但是为了不改动代码,也可以通过BuildConfiguration.xml中的设置:

1
2
C:\Users\lipengzha\AppData\Roaming\Unreal Engine\UnrealBuildTool\BuildConfiguration.xml
F:\EngineSource\UE_4.26\Engine\Saved\UnrealBuildTool\BuildConfiguration.xml

这两个路径下的均可,在Configuration下添加以下配置:

1
2
<bUsePCHFiles>true</bUsePCHFiles>
<bUseSharedPCHs>false</bUseSharedPCHs>

源码版与安装版IOS数据路径差异

使用安装版引擎打包IOS时,开启FileSharing能够在通过工具访问程序的Document目录,里面类似Android的UE4Game目录结构。
但是当使用源码版引擎打包时,开启FileSharing的包,UE的数据目录并不在Document目录下,因为项目配置没有任何更改,怀疑是引擎中有些地方对源码版或安装版做了检测。
搜索代码发现,引擎中通过FILESHARING_ENABLED宏进行检测,当开启时,数据目录位于Library目录下,没有开启时则位于Document目录下。

Core/Private/IOS/IOSPlatformMisc.cpp
1
2
3
4
5
6
7
8
9
10
void FIOSPlatformMisc::PlatformInit()
{
// ...
#if FILESHARING_ENABLED
FString DownloadPath = FString([NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0]) + TEXT("/");
#else
FString DownloadPath = FString([NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]) + TEXT("/");
#endif
// ...
}

这个宏由UBT生成,通过读取DefaultEngine.ini[/Script/IOSRuntimeSettings.IOSRuntimeSettings]下的bSupportsITunesFileSharing来确定宏的值。

Source/Programs/UnrealBuildTool/Platform/IOS/UEBuildIOS.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/// <summary>
/// Stores project-specific IOS settings. Instances of this object are cached by IOSPlatform.
/// </summary>
public class IOSProjectSettings
{
// ...
/// <summary>
/// true if iTunes file sharing support is enabled
/// </summary>
[ConfigFile(ConfigHierarchyType.Engine, "/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bSupportsITunesFileSharing")]
public readonly bool bFileSharingEnabled = false;
// ...
}

添加宏的代码如下:

Source/Programs/UnrealBuildTool/Platform/IOS/UEBuildIOS.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/// <summary>
/// Setup the target environment for building
/// </summary>
/// <param name="Target">Settings for the target being compiled</param>
/// <param name="CompileEnvironment">The compile environment for this target</param>
/// <param name="LinkEnvironment">The link environment for this target</param>
public override void SetUpEnvironment(ReadOnlyTargetRules Target, CppCompileEnvironment CompileEnvironment, LinkEnvironment LinkEnvironment)
{
// ...
if (ProjectSettings.bFileSharingEnabled)
{
CompileEnvironment.Definitions.Add("FILESHARING_ENABLED=1");
}
else
{
CompileEnvironment.Definitions.Add("FILESHARING_ENABLED=0");
}
// ...
}

原因分析:

  1. 因为编译安装版引擎时,没有项目,引擎中BaseEngine.inibSupportsITunesFileSharing=False,所以通过BuildGraph编译安装版引擎时,bFileSharingEnabled默认值是false,通过安装版引擎打包项目引擎也不会编译了,所以bFileSharingEnabled的值一直是false,使用的是NSDocumentDirectory目录
  2. 相同的道理,使用源码版引擎打包项目时,因为需要通过打包项目才编译引擎,如果项目中指定了bSupportsITunesFileSharing=True,则编译引擎时bFileSharingEnabled就为true,导致FILESHARING_ENABLED=1,则使用了NSLibraryDirectory目录。

需要注意的是:bSupportsITunesFileSharing不仅仅只是控制数据存储路径,还控制打包时plist的生成:

Programs/UnrealBuildTool/Platform/IOS/UEDeployIOS.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static bool GenerateIOSPList(FileReference ProjectFile, UnrealTargetConfiguration Config, string ProjectDirectory, bool bIsUE4Game, string GameName, bool bIsClient, string ProjectName, string InEngineDir, string AppDirectory, VersionNumber SdkVersion, UnrealPluginLanguage UPL, string BundleID, bool bBuildAsFramework, out bool bSupportsPortrait, out bool bSupportsLandscape, out bool bSkipIcons)
{
// ITunes file sharing
bool bSupportsITunesFileSharing = false;
Ini.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bSupportsITunesFileSharing", out bSupportsITunesFileSharing);
bool bSupportsFilesApp = false;
Ini.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bSupportsFilesApp", out bSupportsFilesApp);

// ...

Text.AppendLine("\t<key>UIFileSharingEnabled</key>");
Text.AppendLine(string.Format("\t<{0}/>", bSupportsITunesFileSharing ? "true" : "false"));
if (bSupportsFilesApp)
{
Text.AppendLine("\t<key>LSSupportsOpeningDocumentsInPlace</key>");
Text.AppendLine("\t<true/>");
}
// ...
}

所以,为了同时支持数据存储在Document下和开启plist中的UIFileSharingEnabled,对于FileSharing功能的支持需要进行以下操作:

  1. DefaultEngine.ini中的bSupportsITunesFileSharing=False
  2. 修改引擎中生成FILESHARING_ENABLED宏的代码,保持为false即可

Viveport选中Actor按F调用函数

在编辑器中选中一个Actor,按F键,当前的编辑器视口会移动到Actor,调用的是这些函数:

Editor\UnrealEd\Classes\Editor\EditorEngine.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Moves all viewport cameras to the target actor.
* @param Actor Target actor.
* @param bActiveViewportOnly If true, move/reorient only the active viewport.
*/
void MoveViewportCamerasToActor(AActor& Actor, bool bActiveViewportOnly);

/**
* Moves all viewport cameras to focus on the provided array of actors.
* @param Actors Target actors.

* @param bActiveViewportOnly If true, move/reorient only the active viewport.
*/
void MoveViewportCamerasToActor(const TArray<AActor*> &Actors, bool bActiveViewportOnly);

/**
* Moves all viewport cameras to focus on the provided array of actors.
* @param Actors Target actors.
* @param Components Target components (used of actors array is empty)
* @param bActiveViewportOnly If true, move/reorient only the active viewport.
*/
void MoveViewportCamerasToActor(const TArray<AActor*> &Actors, const TArray<UPrimitiveComponent*>& Components, bool bActiveViewportOnly);

Searchable Names

在UE的资源依赖关系中,除了Hard/Soft之外,还有一种Searchable Name,排查之后发现GamePlayTag标记的是SearchableName的类型:


因为在C++中加载资源是直接指定路径的,所以在想有没有一种办法,通过C++加载,也可以分析出代码中加载资源的依赖关系,作为一种思路,先记录一下。

基础包过滤config中的ini文件

打包时会有以下Log:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
UATHelper: Packaging (Windows (64-bit)): Creating Staging Manifest...
UATHelper: Packaging (Windows (64-bit)): Excluding config file C:\build_agent\workspace\FGameEngine\Engine\Engine\Config\BaseEditor.ini
UATHelper: Packaging (Windows (64-bit)): Excluding config file C:\build_agent\workspace\FGameEngine\Engine\Engine\Config\BaseEditorKeyBindings.ini
UATHelper: Packaging (Windows (64-bit)): Excluding config file C:\build_agent\workspace\FGameEngine\Engine\Engine\Config\BaseEditorPerProjectUserSettings.ini
UATHelper: Packaging (Windows (64-bit)): Excluding config file C:\build_agent\workspace\FGameEngine\Engine\Engine\Config\BaseEditorSettings.ini
UATHelper: Packaging (Windows (64-bit)): Excluding config file C:\build_agent\workspace\FGameEngine\Engine\Engine\Config\BaseLightmass.ini
UATHelper: Packaging (Windows (64-bit)): Excluding config file C:\build_agent\workspace\FGameEngine\Engine\Engine\Config\BasePakFileRules.ini
UATHelper: Packaging (Windows (64-bit)): Excluding config file C:\build_agent\workspace\FGameEngine\Engine\Engine\Config\Localization\Category.ini
UATHelper: Packaging (Windows (64-bit)): Excluding config file C:\build_agent\workspace\FGameEngine\Engine\Engine\Config\Localization\Editor.ini
UATHelper: Packaging (Windows (64-bit)): Excluding config file C:\build_agent\workspace\FGameEngine\Engine\Engine\Config\Localization\EditorTutorials.ini
UATHelper: Packaging (Windows (64-bit)): Excluding config file C:\build_agent\workspace\FGameEngine\Engine\Engine\Config\Localization\Engine.ini
UATHelper: Packaging (Windows (64-bit)): Excluding config file C:\build_agent\workspace\FGameEngine\Engine\Engine\Config\Localization\Keywords.ini
UATHelper: Packaging (Windows (64-bit)): Excluding config file C:\build_agent\workspace\FGameEngine\Engine\Engine\Config\Localization\PortableObjectExport.ini
UATHelper: Packaging (Windows (64-bit)): Excluding config file C:\build_agent\workspace\FGameEngine\Engine\Engine\Config\Localization\PortableObjectImport.ini
UATHelper: Packaging (Windows (64-bit)): Excluding config file C:\build_agent\workspace\FGameEngine\Engine\Engine\Config\Localization\PropertyNames.ini
UATHelper: Packaging (Windows (64-bit)): Excluding config file C:\build_agent\workspace\FGameEngine\Engine\Engine\Config\Localization\RepairData.ini
UATHelper: Packaging (Windows (64-bit)): Excluding config file C:\build_agent\workspace\FGameEngine\Engine\Engine\Config\Localization\ToolTips.ini
UATHelper: Packaging (Windows (64-bit)): Excluding config file C:\build_agent\workspace\FGameEngine\Engine\Engine\Config\Localization\WordCount.ini
UATHelper: Packaging (Windows (64-bit)): WARNING: The config file 'FGame/Config/DefaultAssetTags.ini' will be staged, but is not whitelisted or blacklisted. Add +WhitelistConfigFiles=FGame/Config/DefaultAssetTags.ini or +BlacklistConfigFiles=FGame/Config/DefaultAssetTags.ini to the [Staging] section of DefaultGame.ini
UATHelper: Packaging (Windows (64-bit)): Excluding config file C:\Users\lipengzha\Documents\UnrealProjects\Client\Config\DefaultEditor.ini
UATHelper: Packaging (Windows (64-bit)): Cleaning Stage Directory: C:\Users\lipengzha\Documents\UnrealProjects\Client\Saved\StagedBuilds\WindowsNoEditor

根据上面的提示,可以在DefaultGame.ini中通过设置[Staging]中的值来控制白名单与黑名单:

1
2
3
[Staging]
+WhitelistConfigFiles=FGame/Config/DefaultAssetTags.ini
+BlacklistConfigFiles=FGame/Config/DefaultAssetTags.ini

引擎中打包时有一个默认的规则,代码如下:

Source\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
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
/// <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, string PlatformExtensionName) {
StagedFileReference StagedConfigFile = SC.GetStagedFileLocation(ConfigFile);
if (SC.WhitelistConfigFiles.Contains(StagedConfigFile)) {
return true;
}
if (SC.BlacklistConfigFiles.Contains(StagedConfigFile)) {
return false;
}

string NormalizedPath = ConfigFile.MakeRelativeTo(ConfigDir).ToLowerInvariant().Replace('\\', '/');

int DirectoryIdx = NormalizedPath.IndexOf('/');
if (DirectoryIdx == -1) {
const string BasePrefix = "base";
if (NormalizedPath.StartsWith(BasePrefix)) {
string ShortName = NormalizedPath.Substring(BasePrefix.Length);
if (PlatformExtensionName != null) {
if (!ShortName.StartsWith(PlatformExtensionName, StringComparison.InvariantCultureIgnoreCase)) {
// Ignore config files in the platform directory that don't start with the platform name.
return false;
}

ShortName = ShortName.Substring(PlatformExtensionName.Length);
}

return ShouldStageConfigSuffix(SC, ConfigFile, ShortName);
}

const string DefaultPrefix = "default";
if (NormalizedPath.StartsWith(DefaultPrefix)) {
string ShortName = NormalizedPath.Substring(DefaultPrefix.Length);
if (PlatformExtensionName != null) {
if (!ShortName.StartsWith(PlatformExtensionName, StringComparison.InvariantCultureIgnoreCase)) {
// Ignore config files in the platform directory that don't start with the platform name.
return false;
}

ShortName = ShortName.Substring(PlatformExtensionName.Length);
}

return ShouldStageConfigSuffix(SC, ConfigFile, ShortName);
}

const string DedicatedServerPrefix = "dedicatedserver";
if (NormalizedPath.StartsWith(DedicatedServerPrefix)) {
return SC.DedicatedServer ? ShouldStageConfigSuffix(SC, ConfigFile, NormalizedPath.Substring(DedicatedServerPrefix.Length)) : false;
}

if (NormalizedPath == "consolevariables.ini") {
return SC.StageTargetConfigurations.Any(x = >x != UnrealTargetConfiguration.Test && x != UnrealTargetConfiguration.Shipping);
}

if (NormalizedPath == "locgatherconfig.ini") {
return false;
}

if (NormalizedPath == "designertoolsconfig.ini") {
return false;
}

if (PlatformExtensionName != null) {
if (NormalizedPath.StartsWith(PlatformExtensionName, StringComparison.InvariantCultureIgnoreCase)) {
string ShortName = NormalizedPath.Substring(PlatformExtensionName.Length);
return ShouldStageConfigSuffix(SC, ConfigFile, ShortName);
}

if (NormalizedPath == "datadrivenplatforminfo.ini") {
return true;
}
}
}
else {
if (NormalizedPath.StartsWith("layouts/")) {
return true;
}

if (NormalizedPath.StartsWith("localization/")) {
return false;
}

string PlatformPrefix = String.Format("{0}/{0}", NormalizedPath.Substring(0, DirectoryIdx));
if (NormalizedPath.StartsWith(PlatformPrefix)) {
return ShouldStageConfigSuffix(SC, ConfigFile, NormalizedPath.Substring(PlatformPrefix.Length));
}

string PlatformBasePrefix = String.Format("{0}/base{0}", NormalizedPath.Substring(0, DirectoryIdx));
if (NormalizedPath.StartsWith(PlatformBasePrefix)) {
return ShouldStageConfigSuffix(SC, ConfigFile, NormalizedPath.Substring(PlatformBasePrefix.Length));
}

if (NormalizedPath.EndsWith("/datadrivenplatforminfo.ini")) {
return true;
}

}
return null;
}

/// <summary>
/// Determines if the given config file suffix ("engine", "game", etc...) should be staged for the given context.
/// </summary>
/// <param name="SC">The staging context</param>
/// <param name="ConfigFile">Full path to the config file</param>
/// <param name="InvariantSuffix">Suffix for the config file, as a lowercase invariant string</param>
/// <returns>True if the suffix should be staged, false if not, null if unknown</returns>
static Nullable < bool > ShouldStageConfigSuffix(DeploymentContext SC, FileReference ConfigFile, string InvariantSuffix) {
switch (InvariantSuffix) {
case ".ini":
case "compat.ini":
case "deviceprofiles.ini":
case "engine.ini":
case "enginechunkoverrides.ini":
case "game.ini":
case "gameplaytags.ini":
case "gameusersettings.ini":
case "hardware.ini":
case "input.ini":
case "scalability.ini":
case "runtimeoptions.ini":
case "installbundle.ini":
return true;
case "crypto.ini":
case "editor.ini":
case "editorgameagnostic.ini":
case "editorkeybindings.ini":
case "editorlayout.ini":
case "editorperprojectusersettings.ini":
case "editorsettings.ini":
case "editorusersettings.ini":
case "lightmass.ini":
case "pakfilerules.ini":
return false;
default:
return null;
}
}

t.MaxFPS启动时无效

之前在游戏启动时设置锁帧:

DefaultEngine.ini
1
2
[ConsoleVariables]
t.MaxFPS=30

但是发现在目前的引擎版本无效了,从Log里看出来确实从配置中读取了这个值,但是游戏中帧率无变化,所以猜测是哪里又把帧率给改了。

调试后发现,引擎启动时从GameUserSetting读取并执行了SetMaxFPS:

所以,正确的办法是设置DefaultGameUserSettings.ini里的FrameRateLimit值:

DefaultGameUserSettings.ini
1
2
[/Script/Engine.GameUserSettings]
FrameRateLimit=30

如果写在DefaultGameUserSetting.ini中,无论是编辑器或者任何打包的平台,默认都是锁帧,如果想要针对某个平台锁帧,可以将其写到特定平台的*GameUserSettings.ini中:

1
2
3
Config/Android/AndroidGameUserSettings.ini
Config/IOS/IOSGameUserSettings.ini
Config/Windows/WindowsGameUserSettings.ini

UE执行py的Commandlet

把Py脚本放入Content/Python下,使用Commandlet的形式执行以下命令:

1
UE4Editor-cmd.exe PROJECT.uproject -run=pythonscript -script=NavMeshExporter.py

一个脚本例子:

1
2
3
4
5
import unreal
import sys

if __name__ == '__main__':
do_something()

设置ConsoleVariables值的几种方式

  1. 修改引擎Config/ConsoleVariabls.ini
  2. 修改引擎(BaseEngine.ini)/项目(DefaultEngine.ini)中的[SystemSettings]
  3. 运行时console输入
  4. 编辑Device Profiles

运行时通过FConsoleManager获取/修改:

1
2
IConsoleVariable* CVar = IConsoleManager::Get().FindConsoleVariable(TEXT("r.Shadow.MinResolution"));
CVar->Set(16, SetBy);

Set有几个重载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/** Set the internal value from the specified bool. */
void Set(bool InValue, EConsoleVariableFlags SetBy = ECVF_SetByCode)
{
// NOTE: Bool needs to use 1 and 0 here rather than true/false, as this may be a int32 or something
// and eventually this code calls, TTypeFromString<T>::FromString which won't handle the true/false,
// but 1 and 0 will work for whatever.
// inefficient but no common code path
Set(InValue ? TEXT("1") : TEXT("0"), SetBy);
}
/** Set the internal value from the specified int. */
void Set(int32 InValue, EConsoleVariableFlags SetBy = ECVF_SetByCode)
{
// inefficient but no common code path
Set(*FString::Printf(TEXT("%d"), InValue), SetBy);
}
/** Set the internal value from the specified float. */
void Set(float InValue, EConsoleVariableFlags SetBy = ECVF_SetByCode)
{
// inefficient but no common code path
Set(*FString::Printf(TEXT("%g"), InValue), SetBy);
}

第二个参数为EConsoleVariableFlags

HAL/IConsoleManager.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
enum EConsoleVariableFlags
{
ECVF_FlagMask = 0x0000ffff,
ECVF_Default = 0x0,
ECVF_Cheat = 0x1,
ECVF_ReadOnly = 0x4,
ECVF_Unregistered = 0x8,
ECVF_CreatedFromIni = 0x10,
ECVF_RenderThreadSafe = 0x20,
ECVF_Scalability = 0x40,
ECVF_ScalabilityGroup = 0x80,
ECVF_SetFlagMask = 0x00ff0000,
ECVF_Set_NoSinkCall_Unsafe = 0x00010000,
ECVF_SetByMask = 0xff000000,
ECVF_SetByConstructor = 0x00000000,
ECVF_SetByScalability = 0x01000000,
ECVF_SetByGameSetting = 0x02000000,
ECVF_SetByProjectSetting = 0x03000000,
ECVF_SetBySystemSettingsIni = 0x04000000,
ECVF_SetByDeviceProfile = 0x05000000,
ECVF_SetByConsoleVariablesIni = 0x06000000,
ECVF_SetByCommandline = 0x07000000,
ECVF_SetByCode = 0x08000000,
ECVF_SetByConsole = 0x09000000,
}

debug shader compile

1
2
3
4
5
6
7
8
9
10
[Startup]
; Uncomment to get detailed logs on shader compiles and the opportunity to retry on errors
r.ShaderDevelopmentMode=1
; Uncomment to dump shaders in the Saved folder (1 dump all, 2 dump on compilation failure only, 3 dump on compilation failure or warnings)
; Warning: leaving this on for a while will fill your hard drive with many small files and folders
r.DumpShaderDebugInfo=1
; When this is enabled, dumped shader paths will get collapsed (in the cases where paths are longer than the OS's max)
r.DumpShaderDebugShortNames=1
; When this is enabled, when dumping shaders an additional file to use with ShaderCompilerWorker -direct mode will be generated
r.DumpShaderDebugWorkerCommandLine=1

移动平台纹理压缩格式

Android有ETC1/ETC2/ASTC。
IOS有ASTC/PVRTC,ASTC从A8开始支持。

控制打包的log level

在非shipping模式下可以通过-LogCmds指定:

1
-LogCmds="global Verbose, LogPython Verbose, LogAnimMontage off, LogDeepDriveAgent VeryVerbose"

也可以在DefaultEngine.ini中进行控制:

DefaultEngine.ini
1
2
3
4
[Core.Log]
global=[default verbosity for things not listed later]
[cat]=[level]
foo=verbose break

配置参数的使用:

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
------- Log conventions
[cat] = a category for the command to operate on, or 'global' for all categories.
[level] = verbosity level, one of: none, error, warning, display, log, verbose, all, default
At boot time, compiled in default is overridden by ini files setting, which is overridden by command line
------- Log console command usage
Log list - list all log categories
Log list [string] - list all log categories containing a substring
Log reset - reset all log categories to their boot-time default
Log [cat] - toggle the display of the category [cat]
Log [cat] off - disable display of the category [cat]
Log [cat] on - resume display of the category [cat]
Log [cat] only - enables [cat] and disables all other categories"));
Log [cat] [level] - set the verbosity level of the category [cat]
Log [cat] break - toggle the debug break on display of the category [cat]
------- Log command line
-LogCmds=\"[arguments],[arguments]...\" - applies a list of console commands at boot time
-LogCmds=\"foo verbose, bar off\" - turns on the foo category and turns off the bar category
------- Environment variables
Any command line option can be set via the environment variable UE-CmdLineArgs
set UE-CmdLineArgs=\"-LogCmds=foo verbose breakon, bar off\"
------- Config file
[Core.Log]
global=[default verbosity for things not listed later]
[cat]=[level]
foo=verbose break

以下为参考配置,使用LogCategory=LogLevel这种方式进行设置,在游戏运行时会自动读取:

1
2
3
4
5
6
7
8
9
[Core.Log]
LogInit=warning
LogTaskGraph=warning
LogDevObjectVersion=warning
LogMemory=warning
LogTextLocalizationManager=warning
LogObj=warning
LogExit=warning
LogPlatformFile=warning

使用[Core.Log]-LogCmds在非shipping模式下在相同的执行流程了,在Shipping打包时-LogCmds的支持就被剔除了。

具体代码在FLogSuppressionImplementation::ProcessConfigAndCommandLine函数里:Logging/LogSuppressionInterface.cpp#L479

修改PCH Buffer size

默认是/Zm1000,就是750M,如果编译时产生页面文件太小的错误:

1
2
3
4
c1xx: error C3859: Failed to create virtual memory for PCH
c1xx: note: the system returned code 1455: 页面文件太小,无法完成操作。
c1xx: note: please visit https://aka.ms/pch-help for more details
c1xx: fatal error C1076: compiler limit: internal heap limit reached

可以在项目的target.cs里添加以下代码:

1
2
3
4
5
if(Target.Platform == UnrealTargetPlatform.Win64)
{
bOverrideBuildEnvironment = true;
AdditionalCompilerArguments = "/Zm2000";
}

PSO Caching配置

Runtime\RenderCore\Private\ShaderPipelineCache.cpp中,定义了一批与PSO相关的ConsoleVariable对象,可以使用以下配置或者Console指定来使用:

DefaultEngine.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
[ConsoleVariables]
;Sets the startup mode for the PSO cache, determining what the cache does after initialisation:
;0: Precompilation is paused and nothing will compile until a call to ResumeBatching().
;1: Precompilation is enabled in the 'Fast' mode.
;2: Precompilation is enabled in the 'Background' mode.
;Default is 1.
r.ShaderPipelineCache.StartupMode=1

;Set the number of PipelineStateObjects to compile in a single batch operation when compiling takes priority. Defaults to a maximum of 50 per frame, due to async. file IO it is less in practice.
r.ShaderPipelineCache.BackgroundBatchSize=1

;Set the number of PipelineStateObjects to compile in a single batch operation when pre-optimizing the cache. Defaults to a maximum of 50 per frame, due to async. file IO it is less in practice.
r.ShaderPipelineCache.PrecompileBatchSize=50

;The target time (in ms) to spend precompiling each frame when in the background or 0.0 to disable. When precompiling is faster the batch size will grow and when slower will shrink to attempt to occupy the full amount. Defaults to 0.0 (off).
r.ShaderPipelineCache.BackgroundBatchTime=0.0

;The target time (in ms) to spend precompiling each frame when compiling takes priority or 0.0 to disable. When precompiling is faster the batch size will grow and when slower will shrink to attempt to occupy the full amount. Defaults to 16.0 (max. ms per-frame of precompilation).
r.ShaderPipelineCache.BatchTime=16.0

;The target time (in ms) to spend precompiling each frame when cpre-optimizing or 0.0 to disable. When precompiling is faster the batch size will grow and when slower will shrink to attempt to occupy the full amount. Defaults to 10.0 (off).
r.ShaderPipelineCache.PrecompileBatchTime=0.0

;Set the number of PipelineStateObjects to log before automatically saving. 0 will disable automatic saving. Shipping defaults to 0, otherwise default is 100.
r.ShaderPipelineCache.SaveAfterPSOsLogged=100

;Set the time where any logged PSO's will be saved if the number is < r.ShaderPipelineCache.SaveAfterPSOsLogged. Disabled when r.ShaderPipelineCache.SaveAfterPSOsLogged is 0

;Set the time where any logged PSO's will be saved if the number is < r.ShaderPipelineCache.SaveAfterPSOsLogged. Disabled when r.ShaderPipelineCache.SaveAfterPSOsLogged is 0
r.ShaderPipelineCache.AutoSaveTime=30

;Mask used to precompile the cache. Defaults to all PSOs (-1)
r.ShaderPipelineCache.PreCompileMask=-1

;Set the time where any logged PSO's will be saved when -logpso is on the command line.
r.ShaderPipelineCache.AutoSaveTimeBoundPSO=10

;If > 0 then a log of all bound PSOs for this run of the program will be saved to a writable user cache file. Defaults to 0 but is forced on with -logpso.
r.ShaderPipelineCache.SaveBoundPSOLog=0

;Set non zero to use GameFileMask during PSO precompile - recording should always save out the usage masks to make that data availble when needed.
r.ShaderPipelineCache.GameFileMaskEnabled=0

;Set non zero to PreOptimize PSOs - this allows some PSOs to be compiled in the foreground before going in to game
r.ShaderPipelineCache.PreOptimizeEnabled=0

;The minimum bind count to allow a PSO to be precompiled. Changes to this value will not affect PSOs that have already been removed from consideration.
r.ShaderPipelineCache.MinBindCount=0

;The maximum time to allow a PSO to be precompiled. if greather than 0, the amount of wall time we will allow pre-compile of PSOs and then switch to background processing.
r.ShaderPipelineCache.MaxPrecompileTime=0.0

DefaultGameUserSettings.ini:

1
2
3
4
5
6
[ShaderPipelineCache.CacheFile]
;default is 0
;Default = 0, // Whatever order they are already in.
;FirstToLatestUsed = 1, // Start with the PSOs with the lowest first-frame used and work toward those with the highest.
;MostToLeastUsed = 2 // Start with the most often used PSOs working toward the least.
SortOrder=1

MacSDK下载

清理Mac DDC

删除以下三个目录:

1
2
3
rm -rf Engine/DerivedDataCache
rm -rf /Users/buildmachine/Library/Application\ Support/Epic/UnrealEngine
rm -rf Client/DerivedDataCache

修改平台使用的RHI

前面的笔记中提到了再Win上可以通过-FeatureLevelES31来指定Standalone模式使用ES3.1的RHI,但是打包时不能简单地这么指定,因为打包之后shaderbytecode都编译完了,SM5和ES3.1不通用,会提示Shader加载错误。

可以通过以下方式设置:

DefaultEngine.ini
1
2
3
4
5
6
[/Script/WindowsTargetPlatform.WindowsTargetSettings]
Compiler=Default
-TargetedRHIs=PCD3D_SM5
+TargetedRHIs=PCD3D_ES31
+TargetedRHIs=PCD3D_SM5
DefaultGraphicsRHI=DefaultGraphicsRHI_Default

该配置在以下代码中使用,代码篇幅太长,可从github上查看:Runtime/RHI/Private/Windows/WindowsDynamicRHI.cpp#L49

Runtime/RHI/Private/Windows/WindowsDynamicRHI.cpp
1
static IDynamicRHIModule* LoadDynamicRHIModule(ERHIFeatureLevel::Type& DesiredFeatureLevel, const TCHAR*& LoadedRHIModuleName);

修改了之后重新打包,再通过-FeatureLevelES31指定即可启动ES3.1,不加默认则是SM5

拆开pak可以看到,同时支持了SM5ES31的Windows包内的ShaderCacheushaderbytecode都包含了两份:

1
2
3
4
5
6
"D:\ES31ForWin\Saved\Cooked\WindowsNoEditor\Engine\GlobalShaderCache-PCD3D_ES31.bin" "../../../Engine/GlobalShaderCache-PCD3D_ES31.bin"
"D:\ES31ForWin\Saved\Cooked\WindowsNoEditor\Engine\GlobalShaderCache-PCD3D_SM5.bin" "../../../Engine/GlobalShaderCache-PCD3D_SM5.bin"
"D:\ES31ForWin\Saved\Cooked\WindowsNoEditor\ES31ForWin\Content\ShaderArchive-Global-PCD3D_ES31.ushaderbytecode" "../../../ES31ForWin/Content/ShaderArchive-Global-PCD3D_ES31.ushaderbytecode"
"D:\ES31ForWin\Saved\Cooked\WindowsNoEditor\ES31ForWin\Content\ShaderArchive-Global-PCD3D_SM5.ushaderbytecode" "../../../ES31ForWin/Content/ShaderArchive-Global-PCD3D_SM5.ushaderbytecode"
"D:\ES31ForWin\Saved\Cooked\WindowsNoEditor\ES31ForWin\Content\ShaderArchive-ES31ForWin-PCD3D_ES31.ushaderbytecode" "../../../ES31ForWin/Content/ShaderArchive-ES31ForWin-PCD3D_ES31.ushaderbytecode"
"D:\ES31ForWin\Saved\Cooked\WindowsNoEditor\ES31ForWin\Content\ShaderArchive-ES31ForWin-PCD3D_SM5.ushaderbytecode" "../../../ES31ForWin/Content/ShaderArchive-ES31ForWin-PCD3D_SM5.ushaderbytecode"

相关资料:

修改DDC的路径

源码版与安装版引擎的路径有区别:

BaseEngine.ini
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[DerivedDataBackendGraph_Fill_Seattle]
MinimumDaysToKeepFile=7
Root=(Type=KeyLength, Length=120, Inner=AsyncPut)
AsyncPut=(Type=AsyncPut, Inner=Hierarchy)
Hierarchy=(Type=Hierarchical, Inner=Boot, Inner=Pak, Inner=EnginePak, Inner=Local, Inner=Seattle)
Boot=(Type=Boot, Filename="%GAMEDIR%DerivedDataCache/Boot.ddc", MaxCacheSize=512)
Local=(Type=FileSystem, ReadOnly=false, Clean=false, Flush=false, PurgeTransient=true, DeleteUnused=true, UnusedFileAge=34, FoldersToClean=-1, Path=%ENGINEDIR%DerivedDataCache)
Seattle=(Type=FileSystem, ReadOnly=false, Clean=false, Flush=false, DeleteUnused=true, UnusedFileAge=23, FoldersToClean=10, MaxFileChecksPerSec=1, Path=?EpicSeaDDC, EnvPathOverride=UE-SharedDataCachePath_Seattle)
Pak=(Type=ReadPak, Filename="%GAMEDIR%DerivedDataCache/DDC.ddp")
EnginePak=(Type=ReadPak, Filename=%ENGINEDIR%DerivedDataCache/DDC.ddp)

[InstalledDerivedDataBackendGraph]
MinimumDaysToKeepFile=7
Root=(Type=KeyLength, Length=120, Inner=AsyncPut)
AsyncPut=(Type=AsyncPut, Inner=Hierarchy)
Hierarchy=(Type=Hierarchical, Inner=Boot, Inner=Pak, Inner=CompressedPak, Inner=EnginePak, Inner=EnterprisePak, Inner=Local, Inner=Shared)
Boot=(Type=Boot, Filename="%ENGINEUSERDIR%DerivedDataCache/Boot.ddc", MaxCacheSize=512)
Local=(Type=FileSystem, ReadOnly=false, Clean=false, Flush=false, PurgeTransient=true, DeleteUnused=true, UnusedFileAge=34, FoldersToClean=-1, Path="%ENGINEVERSIONAGNOSTICUSERDIR%DerivedDataCache", EditorOverrideSetting=LocalDerivedDataCache)
Shared=(Type=FileSystem, ReadOnly=false, Clean=false, Flush=false, DeleteUnused=true, UnusedFileAge=10, FoldersToClean=10, MaxFileChecksPerSec=1, Path=?EpicDDC, EnvPathOverride=UE-SharedDataCachePath, EditorOverrideSetting=SharedDerivedDataCache)
Pak=(Type=ReadPak, Filename="%GAMEDIR%DerivedDataCache/DDC.ddp")
CompressedPak=(Type=ReadPak, Filename="%GAMEDIR%DerivedDataCache/Compressed.ddp", Compressed=true)
EnginePak=(Type=ReadPak, Filename=../../../Engine/DerivedDataCache/Compressed.ddp, Compressed=true)
EnterprisePak=(Type=ReadPak, Filename=../../../Enterprise/DerivedDataCache/Compressed.ddp, Compressed=true)

源码版的引擎存在于Engine/DerivedDataCache与项目的DerivedDataCache目录下。
安装版引擎的则位于以下几个路径中:

  • %ENGINEUSERDIR%DerivedDataCache/Boot.ddc
  • %ENGINEVERSIONAGNOSTICUSERDIR%DerivedDataCache
  • %GAMEDIR%DerivedDataCache/DDC.ddp
  • ../../../Engine/DerivedDataCache/Compressed.ddp
  • ../../../Enterprise/DerivedDataCache/Compressed.ddp

这几个路径规则的真实路径:

Runtime/Core/Private/Misc/ConfigCacheIni.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
static const FConfigExpansion* MatchExpansions(const TCHAR* PotentialVariable)
{
// Allocate replacement value strings once
static const FConfigExpansion Expansions[] =
{
FConfigExpansion(TEXT("%GAME%"), FString(FApp::GetProjectName())),
FConfigExpansion(TEXT("%GAMEDIR%"), FPaths::ProjectDir()),
FConfigExpansion(TEXT("%ENGINEDIR%"), FPaths::EngineDir()),
FConfigExpansion(TEXT("%ENGINEUSERDIR%"), FPaths::EngineUserDir()),
FConfigExpansion(TEXT("%ENGINEVERSIONAGNOSTICUSERDIR%"), FPaths::EngineVersionAgnosticUserDir()),
FConfigExpansion(TEXT("%APPSETTINGSDIR%"), GetApplicationSettingsDirNormalized()),
};
// ...
}

可以在BaseEngine.ini中修改DDC的路径占位符,替换为上面的可选路径。

注意:EngineUserDirEngineVersionAgnosticUserDir在安装版与源码版上也有区分:

Runtime/Core/Private/Misc/Paths.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
FString FPaths::EngineUserDir()
{
if (ShouldSaveToUserDir() || FApp::IsEngineInstalled())
{
return FPaths::Combine(FPlatformProcess::UserSettingsDir(), *FApp::GetEpicProductIdentifier(), *FEngineVersion::Current().ToString(EVersionComponent::Minor)) + TEXT("/");
}
else
{
return FPaths::EngineDir();
}
}

FString FPaths::EngineVersionAgnosticUserDir()
{
if (ShouldSaveToUserDir() || FApp::IsEngineInstalled())
{
return FPaths::Combine(FPlatformProcess::UserSettingsDir(), *FApp::GetEpicProductIdentifier(), TEXT("Common")) + TEXT("/");
}
else
{
return FPaths::EngineDir();
}
}

默认情况下,如果要完全清理系统中的DDC缓存,需要清理以下路径:

1
2
3
Engine\Engine\DerivedDataCache\*
ProjectName\DerivedDataCache\*
C:\Users\username\AppData\Local\UnrealEngine\*

可以使用以下cmd命令:

1
2
3
echo y|del Engine\DerivedDataCache
echo y|del ProjectName\DerivedDataCache
echo y|del C:\Users\username\AppData\Local\UnrealEngine

类似问题:

App版本号

IOS

在IOS包的plist中可以通过控制以下两个值:

1
2
3
4
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>0.1</string>

一般使用CFBundleVersion来作为游戏版本号,在UE中可以通过UPL来控制plist。

Android

Android通过控制以下值:

1
2
3
[/Script/AndroidRuntimeSettings.AndroidRuntimeSettings]
VersionDisplayName=1.0.0
StoreVersion=1

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
2
bOverrideBuildEnvironment = true;
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"))
{