Unreal Engine


GIST NOTES

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


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

ShareMaterialShaderCode

在打包时可以在Project Settings-Packaging中设置Share Material Shader CodeShadred Material Native Libraries来减少包体的大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/** 
* By default shader code gets saved inline inside material assets,
* enabling this option will store only shader code once as individual files
* This will reduce overall package size but might increase loading time
*/
UPROPERTY(config, EditAnywhere, Category=Packaging)
bool bShareMaterialShaderCode;

/**
* By default shader shader code gets saved into individual platform agnostic files,
* enabling this option will use the platform-specific library format if and only if one is available
* This will reduce overall package size but might increase loading time
*/
UPROPERTY(config, EditAnywhere, Category=Packaging, meta = (EditCondition = "bShareMaterialShaderCode", ConfigRestartRequired = true))
bool bSharedMaterialNativeLibraries;

开启了之后打出的包中会生成下列文件:

1
2
ShaderArchive-Blank425-PCD3D_SM5.ushaderbytecode
ShaderCode-Global-PCD3D_SM5.ushaderbytecode

但是,如果开启之后如果后续的Cook资源Shader发生了变动,而基础包内还是旧的ShaderBytecode信息,会导致材质丢失。

有两个办法:

  1. 后续的打包时可以把Shaderbytecode文件打包在pak中,挂载时加载;
  2. Cook热更资源时把Shaderbytecode打包在资源内;

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

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

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;

UE4:添加资源右键菜单按钮

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

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

UE4:GenerateProjectFiles指定VS版本

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

UE4: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

UE4: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之前。
然后就可以看到添加的两个按钮了:

UE4:提取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需要在打包完当前平台之后立即提取,不然打包下个平台就把之前的删掉了。

UE4:绑定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上。

UE4:从指定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中读取指定的文件。

UE4: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下编译时也不会把这部分反射代码参与真正的编译,从而解决了上面的问题。

UE4:Android获取已安装App的Apk路径

有个需求,需要在运行时获取到,App的Apk路径,查了一下UE里没有现成的接口,只能用JNI调用从Java那边想办法了。
通过在Android Developer上查找,发现ApplicationInfo中具有sourceDir属性,记录着APK的路径。
而可以通过PackageManager调用getApplicationInfo可以获取指定包名的ApplicationInfo。

那么就好说了,UPL里java代码走起:

1
2
3
4
5
6
7
8
9
10
11
12
public String AndroidThunkJava_GetInstalledApkPath()
{
Context context = getApplicationContext();
PackageManager packageManager = context.getPackageManager();
ApplicationInfo appInfo;
try{
appInfo = packageManager.getApplicationInfo(context.getPackageName(),PackageManager.GET_META_DATA);
return appInfo.sourceDir;
}catch (PackageManager.NameNotFoundException e){
return "invalid";
}
}

然后在UE里使用JNI调用:

1
2
3
4
5
if (JNIEnv* Env = FAndroidApplication::GetJavaEnv())
{
jmethodID GetInstalledPakPathMethodID = FJavaWrapper::FindMethod(Env, FJavaWrapper::GameActivityClassID, "AndroidThunkJava_GetInstalledApkPath", "()Ljava/lang/String;", false);
FString ResultApkPath = FJavaHelperEx::FStringFromLocalRef(Env, (jstring)FJavaWrapper::CallObjectMethod(Env, FJavaWrapper::GameActivityThis,GetInstalledPakPathMethodID));
}

其中FJavaHelperEx::FStringFromLocalRef是我封装的从jstring到FString的转换函数:

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
namespace FJavaHelperEx
{
FString FStringFromParam(JNIEnv* Env, jstring JavaString)
{
if (!Env || !JavaString || Env->IsSameObject(JavaString, NULL))
{
return {};
}

const auto chars = Env->GetStringUTFChars(JavaString, 0);
FString ReturnString(UTF8_TO_TCHAR(chars));
Env->ReleaseStringUTFChars(JavaString, chars);
return ReturnString;
}

FString FStringFromLocalRef(JNIEnv* Env, jstring JavaString)
{
FString ReturnString = FStringFromParam(Env, JavaString);

if (Env && JavaString)
{
Env->DeleteLocalRef(JavaString);
}

return ReturnString;

}
}

获取的结果:

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

// ...
}

UE4:Compile对Instanced的替换调用栈

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

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

UE4:对资源点击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));
}

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

UE4:监听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; };
};

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

UE4:修改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()修改为上面的代码,其实就是把从视频获取长宽的代码去掉,强制使用窗口的大小。

UE4: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中即可。

UE4:IOS自动化导入Certificate和Provision

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

UE4: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

UE4: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的结合结合起来是个要思考的问题。

UE4:升级至AndroidX资料

看操作方式也是使用UPL来介入打包过程,先记录下。

UE4:远程构建在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还没有出正式版本,观望正式版会不会修正。

UE4: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的方式修饰并导出符号,使其可以在外部模块中使用。

UE4:JNI调用接收ActivityResult

有时需要通过startActivityForResult来创建Intent来执行一些操作,如打开摄像头、打开相册选择图片等。

但是Android做这些操作的时候不是阻塞在当前的函数中的,所以不能直接在调用的函数里接收这些数据。而通过startActivityForResult执行的Action的结果都会调用到Activity的onActivityResult中。

1
2
3
// GameActivity.java
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data){}

UE在UPL中提供了往OnActivityResult追加Java代码的用法:

1
2
<!-- optional additions to GameActivity onActivityResult in GameActivity.java -->
<gameActivityOnActivityResultAdditions> </gameActivityOnActivityResultAdditions>

使用这种方式添加的Java代码会追加到OnActivityResult函数的末尾,但是这种方式有一个问题,那就是执行了自己追加到OnActivityResult的代码之后,还要处理接收到的结果,并且传递到UE端来,有点麻烦。

经过翻阅代码,发现UE提供了Java端的OnActivityResult的多播代理事件,这样就可以直接在UE里用C++来监听OnActivityResult的事件,自己做处理。

1
2
3
4
5
6
// Launch/Puclic/Android/AndroidJNI.h
DECLARE_MULTICAST_DELEGATE_SixParams(FOnActivityResult, JNIEnv *, jobject, jobject, jint, jint, jobject);

// 该代理是定义在`FJavaWrapper`里的
// Delegate that can be registered to that is called when an activity is finished
static FOnActivityResult OnActivityResultDelegate;

在UE侧就可以通过绑定这个多播代理来监听Java端的OnActivityResult调用,可以在其中做分别的处理。

它由AndroidJNI.cpp中的Java_com_epicgames_ue4_GameActivity_nativeOnActivityResult函数从Java那边调用过来,调用机制在上个笔记中有记录。

UE4:Java调C++

有些需求和实现需要从Java调到C++这边,可以通过下面这种方式:
首先,在GameActivity中新建一个native的java函数声明(不需要定义):

1
public native void nativeOnActivityResult(GameActivity activity, int requestCode, int resultCode, Intent data);

然后在C++端按照下面的规则定义一个C++函数:

1
2
3
4
JNI_METHOD void Java_com_epicgames_ue4_GameActivity_nativeOnActivityResult(JNIEnv* jenv, jobject thiz, jobject activity, jint requestCode, jint resultCode, jobject data)
{
FJavaWrapper::OnActivityResultDelegate.Broadcast(jenv, thiz, activity, requestCode, resultCode, data);
}

可以看到函数名字的规则为:

  1. 函数前需要加JNI_METHOD修饰,它是一个宏__attribute__ ((visibility ("default"))) extern "C"
  2. 函数名需要以Java_开头,并且后面跟上com_epicgames_ue4_GameActivity_,标识是定义在GameActivity中的
  3. 然后再跟上java中的函数名

接受参数的规则:

  1. 第一个参数是Java的Env
  2. 第二个是java里的this
  3. 后面的参数以此是从java里传递参数

UE4:AndroidP的全面屏适配

在UE4打包的时候,会给项目生成GameActivity.java文件,里面的OnCreate具有适配全面屏的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void onCreate(Bundle savedInstanceState)
{
// ...
if (UseDisplayCutout)
{
// will not be true if not Android Pie or later
WindowManager.LayoutParams params = getWindow().getAttributes();
params.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
getWindow().setAttributes(params);
}
// ...
}

Android P和之后的系统不支持,所以就要自己写Jni调用来强制让P和之后系统版本支持。
在UE4.23+以后的引擎版本,支持了通过UPL往OnCreate函数添加代码,就可以直接把代码插入到GameActivity.java了:

1
2
3
4
5
6
7
8
9
10
<gameActivityOnCreateFinalAdditions>
<insert>
// P版本允许使用刘海
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
WindowManager.LayoutParams lp = this.getWindow().getAttributes();
lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
this.getWindow().setAttributes(lp);
}
</insert>
</gameActivityOnCreateFinalAdditions>

在UE4.23之前不可以给OnCreate添加代码,只能自己写个JNI调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void AndroidThunkJava_SetFullScreenDisplayForP()
{
final GameActivity Activity = this;
runOnUiThread(new Runnable()
{
private GameActivity InActivity = Activity;
public void run()
{
WindowManager windowManager = InActivity.getWindowManager();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
{

WindowManager.LayoutParams lp = InActivity.getWindow().getAttributes();
lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
InActivity.getWindow().setAttributes(lp);
Log.debug( "call AndroidThunkJava_SetFullScreenDisplayForP");
}
}
});
}

将其在UPL中通过<gameActivityClassAdditions>添加到GameActivity.java中,在游戏启动时通过JNI调用即可。

注:

layoutInDisplayCutoutMode的可选项为:

外部资料

UE4:远程编译Shader的Key查找bug

Project Settings-Platforms-IOS中开启Enable Remote Shader Compile后,如果填入的构建机地址具有指定端口,在查找SSHkey时会有问题:

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
// Developer/Apple/MetalShaderFormat/Private/MetalShaderCompiler.cpp
bool IsRemoteBuildingConfigured(const FShaderCompilerEnvironment* InEnvironment)
{
// ...
GRemoteBuildServerSSHKey = "";
if (InEnvironment != nullptr && InEnvironment->RemoteServerData.Contains(TEXT("SSHPrivateKeyOverridePath")))
{
GRemoteBuildServerSSHKey = InEnvironment->RemoteServerData[TEXT("SSHPrivateKeyOverridePath")];
}
if (GRemoteBuildServerSSHKey.Len() == 0)
{
GConfig->GetString(TEXT("/Script/IOSRuntimeSettings.IOSRuntimeSettings"), TEXT("SSHPrivateKeyOverridePath"), GRemoteBuildServerSSHKey, GEngineIni);

GConfig->GetString(TEXT("/Script/IOSRuntimeSettings.IOSRuntimeSettings"), TEXT("SSHPrivateKeyOverridePath"), GRemoteBuildServerSSHKey, GEngineIni);
if (GRemoteBuildServerSSHKey.Len() == 0)
{
if (!FParse::Value(FCommandLine::Get(), TEXT("serverkey"), GRemoteBuildServerSSHKey) && GRemoteBuildServerSSHKey.Len() == 0)
{
if (GRemoteBuildServerSSHKey.Len() == 0)
{
// RemoteToolChain.cs in UBT looks in a few more places but the code in FIOSTargetSettingsCustomization::OnGenerateSSHKey() only puts the key in this location so just going with that to keep things simple
FString Path = FPlatformMisc::GetEnvironmentVariable(TEXT("APPDATA"));
GRemoteBuildServerSSHKey = FString::Printf(TEXT("%s\\Unreal Engine\\UnrealBuildTool\\SSHKeys\\%s\\%s\\RemoteToolChainPrivate.key"), *Path, *GRemoteBuildServerHost, *GRemoteBuildServerUser);
}
}
}
}
// ...
}

可以看到这里查找的Key路径时直接通过GRemoteBuildServerHost拼接的,但是如果在配置中指定了端口,那么GRemoteBuildServerHost的值为这种格式xxx.xx.xx.xx:1234,但是Win上目录名不能带:,就会导致Key查找失败。

还有在同文件的ExecRemoteProcess函数中,没有针对具有指定端口的情况做处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bool ExecRemoteProcess(const TCHAR* Command, const TCHAR* Params, int32* OutReturnCode, FString* OutStdOut, FString* OutStdErr)
{
#if PLATFORM_MAC && !UNIXLIKE_TO_MAC_REMOTE_BUILDING
return FPlatformProcess::ExecProcess(Command, Params, OutReturnCode, OutStdOut, OutStdErr);
#else
if (GRemoteBuildServerHost.IsEmpty())
{
return false;
}
FString CmdLine = FString(TEXT("-i \"")) + GRemoteBuildServerSSHKey + TEXT("\" \"") + GRemoteBuildServerUser + '@' + GRemoteBuildServerHost + TEXT("\" ") + Command + TEXT(" ") + (Params != nullptr ? Params : TEXT(""));
return ExecProcess(*GSSHPath, *CmdLine, OutReturnCode, OutStdOut, OutStdErr);

#endif
}

需要做一些处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bool ExecRemoteProcess(const TCHAR* Command, const TCHAR* Params, int32* OutReturnCode, FString* OutStdOut, FString* OutStdErr)
{
#if PLATFORM_MAC && !UNIXLIKE_TO_MAC_REMOTE_BUILDING
return FPlatformProcess::ExecProcess(Command, Params, OutReturnCode, OutStdOut, OutStdErr);
#else
if (GRemoteBuildServerHost.IsEmpty())
{
return false;
}

FString RemoteBuildServerIP = GRemoteBuildServerHost;
FString RemoteBuildServerPort = TEXT("22");

if(GRemoteBuildServerHost.Contains(TEXT(":")))
{
GRemoteBuildServerHost.Split(TEXT(":"),&RemoteBuildServerIP,&RemoteBuildServerPort);
}

FString CmdLine = FString(TEXT("-i \"")) + GRemoteBuildServerSSHKey + TEXT("\" \"") + GRemoteBuildServerUser + '@' + RemoteBuildServerIP + TEXT("\" ") TEXT("-p ") + RemoteBuildServerPort +TEXT(" ")+ Command + TEXT(" ") + (Params != nullptr ? Params : TEXT(""));
return ExecProcess(*GSSHPath, *CmdLine, OutReturnCode, OutStdOut, OutStdErr);

#endif
}

UE4:平台相关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;
}

UE4:TargetRule获取项目路径

使用ProjectFile

1
Console.WriteLine("ProjectDir:" + ProjectFile.Directory);

UE4: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'

UE4:打包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文件回本地。

UE4:编译引擎支持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中,然后再使用上面的命令构建即可。

UE4:Android设置宽高比

Project Settings-Platforms-Android-Maximum support aspect ratio的值,默认是2.1,但是在全面屏的情况下会有黑边。

它控制的值是AndroidManifest.xml中的值:

1
<meta-data android:name="android.max_aspect" android:value="2.1"/>

我目前设置的值是2.5.

注:Enable FullScreen Immersive on KitKat and above devices控制的是进入游戏时是否隐藏虚拟按键。

UE4: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看到引擎代码中的各种用法。

UE4:无边框模式

可以开启Project Settings-Description-Settings-Use Boardless Window:

这个变量在UGameEngine::CreateGameWindow中用到,用来控制引擎启动创建窗口时,窗口的Style.

在不重新打包的情况下可以将下面配置写到Saved/Config/WindowsNoEditor/Game.ini中:

1
2
[/Script/EngineSettings.GeneralProjectSettings]
bUseBorderlessWindow=true

重启游戏就是无边框模式了。

UE4:命令行导入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)
{

}

UE4:打包时给不同的平台添加资源

有时我们需要在打包时给不同的平台添加不同目录下的外部资源(如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的文档里哪有写。

UE4:监听资源保存的事件

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

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

UE4:Redirector

Redirector是标记被移动资源的引用关系的,在移动具有引用的资源时会产生。
/Game/GameMap引用到了一个UI资源/Game/UMG_Main,当移动/Game/UMG_Main/Game/TEST/UMG_Main时,会在/Game/UMG_Main的磁盘路径下创建出一个Redirector,用与告诉引用到该UI的资源,它的真实路径已经发生变化了,所以叫Redirector。
在项目中需要尽量避免Redirector的存在。

UE:检查引擎是否运行在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
}

UE4: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则可以使用继承层次中类型的资源。

UE4:编辑器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()来获得。

UE4:开启多核心编译

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>

相关连接:

UE4: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的签名色所有者和发布者,以及证书的有效时间、证书指纹等等。

UE4:ASTC设置压缩率

Project Settings-Engine-Cooker中有选项:

UE4:Android重启App

当游戏更新完毕之后有时候需要重启App才可以生效,在UE中可以使用UPL写入以下java代码:

1
2
3
4
5
6
7
8
9
10
public void AndroidThunkJava_AndroidAPI_RestartApplication( ) {
Context context = getApplicationContext();
PackageManager pm = context.getPackageManager();
Intent intent = pm.getLaunchIntentForPackage(context.getPackageName());
int delayTime = 500;
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
PendingIntent restartIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
alarmManager.set(AlarmManager.RTC, System.currentTimeMillis() + delayTime, restartIntent);
System.exit(0);
}

在需要重启的时候通过jni调用来触发:

1
2
3
4
5
6
7
8
9
void RestartApplication()
{
#if PLATFORM_ANDROID
if (JNIEnv* Env = FAndroidApplication::GetJavaEnv(true))
{
FJavaWrapper::CallVoidMethod(Env, FJavaWrapper::GameActivityThis, AndroidThunkJava_AndroidAPI_RestartApplication);
}
#endif
}

UE4:打包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

打包之后就会以窗口模式启动了,分辨率可以自己修改。

UE4:指定地图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

Windows通过bat存储环境变量

设置用户环境变量:

1
setx ENV_NAME env_value

设置系统环境变量:

1
setx ENV_NAME env_value /m

UE4: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枚举值的库。

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

UE4:编译引擎的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安装即可。

UE4: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

UE4: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行为(死循环),导致当前模块加载失败,也就触发了前面模块加载失败的问题。

UE4:蓝图编辑器中节点属性的修改

在编辑器模式下修改节点的值:

执行的函数是UEdGraphSchema_K2::TrySetDefaultValue(Editor/BlueprintGraph/Private/EdGraphSchema_K2.cpp),是通过Schema来调用的。简单来说就是通过当前的节点,去修改节点上的Pin的值,不管原始类型是什么,FString/int/float还是枚举,都可以通过这个方法设置。

UE4:UMG资料文档

文章:

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

UE4: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-newd的方式,TCppStructOps<>模板定义在CoreUObject/Public/UObject/Class.h

UE4:类反射

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都构造出来。

UE4:函数反射

UHT生成的反射信息

在UE的代码中加了UFUNCTION()修饰后UHT就会为该函数生成反射代码。

每一个支持反射的函数UHT都会给它生成一个类和一个函数:
如在AMyActor这个类下有一个ReflexFunc的函数:

1
2
3
4
5
UFUNCTION()
bool ReflexFunc(int32 InIval, UObject* InObj)
{
return false;
}

UHT会生成这样命名规则的一个类和函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
// UHT为ReflexFunc生成的反射代码
struct Z_Construct_UFunction_AMyActor_ReflexFunc_Statics
{
struct MyActor_eventReflexFunc_Parms
{
int32 InIval;
UObject* InObj;
bool ReturnValue;
};
static void NewProp_ReturnValue_SetBit(void* Obj);
static const UE4CodeGen_Private::FBoolPropertyParams NewProp_ReturnValue;
static const UE4CodeGen_Private::FObjectPropertyParams NewProp_InObj;
static const UE4CodeGen_Private::FIntPropertyParams NewProp_InIval;
static const UE4CodeGen_Private::FPropertyParamsBase* const PropPointers[];
#if WITH_METADATA
static const UE4CodeGen_Private::FMetaDataPairParam Function_MetaDataParams[];
#endif
static const UE4CodeGen_Private::FFunctionParams FuncParams;
};
void Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::NewProp_ReturnValue_SetBit(void* Obj)
{
((MyActor_eventReflexFunc_Parms*)Obj)->ReturnValue = 1;
}
const UE4CodeGen_Private::FBoolPropertyParams Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::NewProp_ReturnValue = {
"ReturnValue",
nullptr,
(EPropertyFlags)0x0010000000000580,
UE4CodeGen_Private::EPropertyGenFlags::Bool | UE4CodeGen_Private::EPropertyGenFlags::NativeBool,
RF_Public|RF_Transient|RF_MarkAsNative,
1,
sizeof(bool),
sizeof(MyActor_eventReflexFunc_Parms),
&Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::NewProp_ReturnValue_SetBit,
METADATA_PARAMS(nullptr,
0)
};
const UE4CodeGen_Private::FObjectPropertyParams Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::NewProp_InObj = {
"InObj",
nullptr,
(EPropertyFlags)0x0010000000000080,
UE4CodeGen_Private::EPropertyGenFlags::Object,
RF_Public|RF_Transient|RF_MarkAsNative,
1,
STRUCT_OFFSET(MyActor_eventReflexFunc_Parms,
InObj),
Z_Construct_UClass_UObject_NoRegister,
METADATA_PARAMS(nullptr,
0)
};
const UE4CodeGen_Private::FIntPropertyParams Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::NewProp_InIval = {
"InIval",
nullptr,
(EPropertyFlags)0x0010000000000080,
UE4CodeGen_Private::EPropertyGenFlags::Int,
RF_Public|RF_Transient|RF_MarkAsNative,
1,
STRUCT_OFFSET(MyActor_eventReflexFunc_Parms,
InIval),
METADATA_PARAMS(nullptr,
0)
};
const UE4CodeGen_Private::FPropertyParamsBase* const Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::PropPointers[] = {
(const UE4CodeGen_Private::FPropertyParamsBase*)&Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::NewProp_ReturnValue,
(const UE4CodeGen_Private::FPropertyParamsBase*)&Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::NewProp_InObj,
(const UE4CodeGen_Private::FPropertyParamsBase*)&Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::NewProp_InIval,
};
#if WITH_METADATA
const UE4CodeGen_Private::FMetaDataPairParam Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::Function_MetaDataParams[] = {
{ "ModuleRelativePath",
"MyActor.h" },
};
#endif
const UE4CodeGen_Private::FFunctionParams Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::FuncParams = { (UObject*(*)())Z_Construct_UClass_AMyActor,
nullptr,
"ReflexFunc",
nullptr,
nullptr,
sizeof(MyActor_eventReflexFunc_Parms),
Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::PropPointers,
UE_ARRAY_COUNT(Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::PropPointers),
RF_Public|RF_Transient|RF_MarkAsNative,
(EFunctionFlags)0x00020401,
0,
0,
METADATA_PARAMS(Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::Function_MetaDataParams,
UE_ARRAY_COUNT(Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::Function_MetaDataParams))
};

// 通过UHT生成的反射信息来构造出UFunction
UFunction* Z_Construct_UFunction_AMyActor_ReflexFunc()
{
static UFunction* ReturnFunction = nullptr;
if (!ReturnFunction)
{
UE4CodeGen_Private::ConstructUFunction(ReturnFunction, Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::FuncParams);
}
return ReturnFunction;
}

定义的Z_Construct_UFunction_AMyActor_ReflexFunc_Statics类中包含了以下信息:

  1. 存储函数的参数、返回值的结构体(POD),注意该结构的声明顺序是按照函数参数的顺序+最后一个成员是函数返回值的方式排列。
  2. 函数参数、返回值的F*PropertyParams,用来给函数的每个参数以及返回值生成反射信息,用于构造出UProperty,static成员;
  3. 成员static const UE4CodeGen_Private::FPropertyParamsBase* const PropPointers[];,数组,用于记录该函数的参数和返回值的类型为FPropertyParamsBase的static数据成员的地址。
  4. 成员static const UE4CodeGen_Private::FMetaDataPairParam Function_MetaDataParams[];,用于记录函数的元数据。如所属文件、Category、注释等等。
  5. 成员static const UE4CodeGen_Private::FFunctionParams FuncParams;用于记录当前函数的名字、Flag、参数的F*PropertyParams、参数数量,参数的结构大小等等,用于通过它来创建出UFunction*
  6. 至于生成的SetBitd的函数的作用,在上面属性反射的部分已经讲到了。

UE4CodeGen_Private::FFunctionParams结构声明为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Source/Runtime/CoreUObject/Public/UObject/UObjectGlobals.h
struct FFunctionParams
{
UObject* (*OuterFunc)();
UFunction* (*SuperFunc)();
const char* NameUTF8;
const char* OwningClassName;
const char* DelegateName;
SIZE_T StructureSize;
const FPropertyParamsBase* const* PropertyArray;
int32 NumProperties;
EObjectFlags ObjectFlags;
EFunctionFlags FunctionFlags;
uint16 RPCId;
uint16 RPCResponseId;
#if WITH_METADATA
const FMetaDataPairParam* MetaDataArray;
int32 NumMetaData;
#endif
};

UHT生成的Z_Construct_UFunction_AMyActor_ReflexFunc函数做了以下事情:

1
2
3
4
5
6
7
8
9
UFunction* Z_Construct_UFunction_AMyActor_ReflexFunc()
{
static UFunction* ReturnFunction = nullptr;
if (!ReturnFunction)
{
UE4CodeGen_Private::ConstructUFunction(ReturnFunction, Z_Construct_UFunction_AMyActor_Add_Statics::FuncParams);
}
return ReturnFunction;
}

根据定义的Z_Construct_UFunction_AMyActor_ReflexFunc_Statics结构中的FuncParams成员来创建出真正的UFunction对象。

最后,Z_Construct_UFunction_AMyActor_ReflexFunc这个函数会被注册到当前类反射数据的FuncInfo中。

Thunk函数

UE会为标记为UFUNCTION的Native函数生成对应的Thunk函数(BlueprintImplementableEvent的函数不会)。如下列函数:

1
2
3
4
5
UFUNCTION()
bool ReflexFunc(int32 InIval, UObject* InObj)
{
return false;
}

生成的Thunk函数形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// generated.h
DECLARE_FUNCTION(execReflexFunc);

// gen.cpp
DEFINE_FUNCTION(AMyActor::execReflexFunc)
{
P_GET_PROPERTY(FIntProperty,Z_Param_InIval);
P_GET_OBJECT(UObject,Z_Param_InObj);
P_FINISH;
P_NATIVE_BEGIN;
*(bool*)Z_Param__Result=P_THIS->ReflexFunc(Z_Param_InIval,Z_Param_InObj);
P_NATIVE_END;
}

DECLARE_FUNCTION/DEFINE_FUNCTION这两个宏是定义在CoreUObject/Public/UObject/ObjectMacros.h中的:

1
2
3
4
5
// This macro is used to declare a thunk function in autogenerated boilerplate code
#define DECLARE_FUNCTION(func) static void func( UObject* Context, FFrame& Stack, RESULT_DECL )

// This macro is used to define a thunk function in autogenerated boilerplate code
#define DEFINE_FUNCTION(func) void func( UObject* Context, FFrame& Stack, RESULT_DECL )

展开这两个宏:

1
2
3
4
5
6
7
8
9
10
11
12
// generated.h
static void AMyActor::execReflexFunc( UObject* Context, FFrame& Stack, RESULT_DECL );
// gen.cpp
void AMyActor::execReflexFunc( UObject* Context, FFrame& Stack, RESULT_DECL )
{
P_GET_PROPERTY(FIntProperty,Z_Param_InIval);
P_GET_OBJECT(UObject,Z_Param_InObj);
P_FINISH;
P_NATIVE_BEGIN;
*(bool*)Z_Param__Result=P_THIS->ReflexFunc(Z_Param_InIval,Z_Param_InObj);
P_NATIVE_END;
}

可以看到,UHT为每个反射函数生成的都是一个参数一致的static成员函数,接收通用的参数,就可以用来处理所有的函数调用。

Thunk函数中用到的这些宏:

RESULT_DECL(CoreUObject/Public/UObject/Script.h):

1
2
3
4
5
//
// Blueprint VM intrinsic return value declaration.
//
#define RESULT_PARAM Z_Param__Result
#define RESULT_DECL void*const RESULT_PARAM

其他的形如P_GET_PROPERTY之类的宏,都是定义在CoreUObject/Public/UObject/ScriptMacros.h文件中的,作用就是从栈上操作参数(因为Thunk函数是通用的参数,所以要从通用的参数中获取到每个函数具体的参数,UE提供这些宏来做这些事情)。

这些Thunk函数通过UHT生成的StaticRegisterNatives*函数注册到UClass中(在GetPrivateStaticClass把该函数指针传递了进去):

1
2
3
4
5
6
7
8
9
void AMyActor::StaticRegisterNativesAMyActor()
{
UClass* Class = AMyActor::StaticClass();
static const FNameNativePtrPair Funcs[] = {
{ "BPNativeEvent", &AMyActor::execBPNativeEvent },
{ "ReflexFunc", &AMyActor::execReflexFunc },
};
FNativeFunctionRegistrar::RegisterFunctions(Class, Funcs, UE_ARRAY_COUNT(Funcs));
}

UE不会为BlueprintImplementableEvent生成Thunk函数,但是会为它生成函数的反射信息,所以也可以通过反射的信息来调用BlueprintImplementableEvent的函数。

因为`BlueprintImplementatableEventd的函数是C++提供原型不提供实现,让蓝图来进行覆写的,所以它不使用Thunk的形式调用(应该执行字节码的方式,这个暂时还没看到,有时间再来分析)。

Custom Thunk

前面讲到当给函数加了UFUNCTION标记时UHT会给我们生成对应的Thunk函数,但是有些情况下需要我们自己来写Thunk的函数,如UKismetArrayLibrary中的对Array进行操作的函数,或者UDataTableFunctionLibrary中的GetDataTableRowFromName函数。

UE提供了让我们自己实现Thunk函数的方法,在UFUNCTION中添加CustomThunk标记:

1
2
UFUNCTION(CustomThunk)
bool ReflexFunc(int32 InIval, UObject* InObj)

这样UHT就不会为这个函数生成出它的Thunk函数,这种情况下就需要自己提供了。自己写的方式和UHT生成的代码一样,可以使用DECLARE_FUNCTION或者DEFINE_FUNCTION(手动写按照Thunk函数的签名规则也是没问题的)

1
2
3
4
DECLARE_FUNCTION(execReflexFunc)
{
// do something...
}

运行时访问反射函数

1
2
3
4
5
6
7
8
9
10
11
12
for (TFieldIterator<UFunction> It(InActor->GetClass()); It; ++It)
{
UFunction* FuncProperty = *It;
if (FuncProperty->GetName() == TEXT("GetIval"))
{
struct CallParam
{
int32 ival;
}CallParamIns;
InActor->ProcessEvent(FuncProperty, &CallParamIns);
}
}

通过ProcessEvent来调用,第二个参数传递进去参数和返回值的结构。

每个被反射的函数UHT都会给它生成一个参数的结构体,其排列的顺序为:函数参数依次排列,最后一个成员为返回值。如:

1
2
UFUNCTION()
int32 Add(int32 R, int32 L);

UHT为其生成的参数结构为:

1
2
3
4
5
6
struct MyActor_eventAdd_Parms
{
int32 R;
int32 L;
int32 ReturnValue;
};

在通过UFunction*来调用函数时,需要把这个布局的结构传递作为传递给函数的参数以及接收返回值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for (TFieldIterator<UFunction> It(InActor->GetClass()); It; ++It)
{
UFunction* Property = *It;
if (Property->GetName() == TEXT("Add"))
{
struct AddFuncCallParam
{
int32 R;
int32 L;
int32 RetValue;
}CallParamIns;
CallParamIns.R = 123;
CallParamIns.L = 456;
InActor->ProcessEvent(Property, &CallParamIns);
UE_LOG(LogTemp, Log, TEXT("UFunction:%s value:%d"), *Property->GetName(), CallParamIns.RetValue);
}
}

可以通过UFunction拿到当前函数的参数的结构的大小UFunction::ParmsSize,在运行时动态访问的话可以通过这个结构大小分配出一块内存,然后用UProperty对这块内存进行访问,因为通过UProperty访问成员其实本质上也是通过该成员在类内的偏移来做的(对数据成员获取成员指针得到的是一个相对于对象基址的偏移值)。

坑点

注意:通过UE的UFunction调用并不能正确地处理引用类型,如:

1
2
UFUNCTION()
int32& Add(int32 R, int32& L);

这个函数生成的反射代码和非引用的一摸一样(对L参数生成的UProperty的Flag会多一个CPF_OutParm,返回值的UProperty还具有CPF_ReturnParm)。
这会造成通过UFunction*调用传递的参数和想要获取的返回值都只是一份拷贝(因为本来调用时的参数传递到ProcessEvent之前都会被赋值到UHT创建出来的参数结构),不能再后续的流程中对得到的结果进行赋值。

而且,通过遍历UFunction得到的参数和返回值UProperty,其中的Offset值是相对于UHT生成的参数结构。

在获取蓝图或者C++中具有多个返回值UFunction的时候,因为UE的具有多个返回值的机制是通过传递进来引用实现的,所以不能够只是通过检测UProperty是否具有CPF_ReturnValue来检测,因为包含该flag的UProperty只有一个,还需要检测CPF_OutParam来判断是都是通过引用方式传递的“返回值”。

UE4:属性反射

当在UCLASS类中给一个属性添加UPROPERTY标记时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
UCLASS()
class MICROEND_423_API AMyActor : public AActor
{
GENERATED_BODY()
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;

public:
// Called every frame
virtual void Tick(float DeltaTime) override;

UPROPERTY(EditAnywhere)
int32 ival;
};

会生成反射代码,生成反射代码的代码是在UHT中的Programs/UnrealHeaderTool/Private/CodeGenerator.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#if WITH_METADATA
const UE4CodeGen_Private::FMetaDataPairParam Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData[] = {
{ "Category", "MyActor" },
{ "ModuleRelativePath", "Public/MyActor.h" },
};
#endif
const UE4CodeGen_Private::FIntPropertyParams Z_Construct_UClass_AMyActor_Statics::NewProp_ival = {
"ival",
nullptr,
(EPropertyFlags) 0x0010000000000001,
UE4CodeGen_Private::EPropertyGenFlags::Int,
RF_Public | RF_Transient | RF_MarkAsNative,
1,
STRUCT_OFFSET(AMyActor, ival),
METADATA_PARAMS(Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData, ARRAY_COUNT(Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData))
};

首先const UE4CodeGen_Private::FIntPropertyParams Z_Construct_UClass_AMyActor_Statics::NewProp_ival是创建出一个结构,用于记录当前属性的信息,比如变量名、相对于对象起始地址的偏移。

注意UE4CodeGen_Private::FIntPropertyParams这样的类型都是定义在UObject/UObjectGlobals.h中的,对于基础类型的属性(如int8/int32/float/array/map)等使用的都是FGenericPropertyParams

F*PropertyParams

UObject/UObjectGlobals.h文件中,引擎里定义了很多的F*PropertyParams,但是他们的数据结构基本相同(但是他们并没有继承关系),都是POD的类型,每个数据依次排列,而且不同类型的Params尽量都保持了一致的顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// UObject/UObjectGlobals.h
struct FGenericPropertyParams // : FPropertyParamsBaseWithOffset
{
const char* NameUTF8;
const char* RepNotifyFuncUTF8;
EPropertyFlags PropertyFlags;
EPropertyGenFlags Flags;
EObjectFlags ObjectFlags;
int32 ArrayDim;
int32 Offset;
#if WITH_METADATA
const FMetaDataPairParam* Z_Construct_UClass_;
int32 NumMetaData;
#endif
};

一个一个来分析它的参数。

NameUTF8

NameUTF8是属性的UTF8的名字,在运行时可以通过UPropertyGetNameCPP来获取。

RepNotifyFuncUTF8

RepNotifyFuncUTF8是在当前属性绑定的修改之后的函数名字。

如果属性是这样声明:

1
2
3
4
UPROPERTY(EditAnywhere,ReplicatedUsing=OnRep_Replicatedival)
int32 ival;
UFUNCTION()
virtual void OnRep_Replicatedival() {}

这样生成的反射代码就为:

1
2
3
4
5
6
7
8
9
10
const UE4CodeGen_Private::FIntPropertyParams Z_Construct_UClass_AMyActor_Statics::NewProp_ival = { 
"ival",
"OnRep_Replicatedival",
(EPropertyFlags)0x0010000100000021,
UE4CodeGen_Private::EPropertyGenFlags::Int,
RF_Public|RF_Transient|RF_MarkAsNative,
1,
STRUCT_OFFSET(AMyActor, ival),
METADATA_PARAMS(Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData,ARRAY_COUNT(Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData))
};

因为OnRep_Replicatedival也是UFUNCTION的函数,所以可以通过反射来访问。

PropertyFlags

PropertyFlags是一个类型为EPropertyFlags的枚举,枚举值按照位排列,根据在UPROPERTY中的标记来按照位或来记录当前属性包含的标记信息。
UnrealHeaderTool/Private/CodeGenerator.cpp中生成代码时会根据当前属性的flag和CPF_ComputedFlags做位运算:

1
2
3
4
5
6
7
8
9
10
/*
// All the properties that should never be loaded or saved
#define CPF_ComputedFlags (CPF_IsPlainOldData | CPF_NoDestructor | CPF_ZeroConstructor | CPF_HasGetValueTypeHash)

0x0000000040000000 CPF_IsPlainOldData
0x0000001000000000 CPF_NoDestructor
0x0000000000000200 CPF_ZeroConstructor
0x0008000000000000 CPF_HasGetValueTypeHash
*/
EPropertyFlags PropFlags = Prop->PropertyFlags & ~CPF_ComputedFlags;

在运行时可以通过UPropertyHasAnyPropertyFlags函数来检测是否具有特定的flag。

一般情况下,可以通过UProperty来获取到该参数的标记属性,比如当通过UFunction获取函数的参数时,可以区分哪个UProperty是输入参数、哪个是返回值。

Flags

第四个参数Flags是类型为UE4CodeGen_Private::EPropertyGenFlags是一个枚举,定义在UObject/UObjectGlobals.h中,标记了当前Property的类型。

ObjectFlags

ObjectFlags是EObjectFlags类型的枚举,其枚举值也是按照位来划分的。定义在CoreUObject/Public/UObject/ObjectMacros.h

UHT生成这部分代码在Programs/UnrealHeaderTool/Private/CodeGenerator.cpp中:

1
const TCHAR*   FPropertyObjectFlags = FClass::IsOwnedByDynamicType(Prop) ? TEXT("RF_Public|RF_Transient") : TEXT("RF_Public|RF_Transient|RF_MarkAsNative");

通过FClass::IsOwnedByDynamicType函数来检测是否为PROPERTY添加RF_MarkAsNative的flag。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Programs/UnrealHeaderTool/Public/ParserClass.h

/** Helper function that checks if the field is a dynamic type (can be constructed post-startup) */
template <typename T>
static bool IsDynamic(const T* Field)
{
return Field->HasMetaData(NAME_ReplaceConverted);
}

// Programs/UnrealHeaderTool/Private/ParserClass.cpp
bool FClass::IsOwnedByDynamicType(const UField* Field)
{
for (const UField* OuterField = Cast<const UField>(Field->GetOuter()); OuterField; OuterField = Cast<const UField>(OuterField->GetOuter()))
{
if (IsDynamic(OuterField))
{
return true;
}
}
return false;
}

bool FClass::IsOwnedByDynamicType(const FField* Field)
{
for (FFieldVariant Owner = Field->GetOwnerVariant(); Owner.IsValid(); Owner = Owner.GetOwnerVariant())
{
if (Owner.IsUObject())
{
return IsOwnedByDynamicType(Cast<const UField>(Owner.ToUObject()));
}
else if (IsDynamic(Owner.ToField()))
{
return true;
}
}
return false;
}

通过调用UFieldHasMetaData检测是否具有TEXT("ReplaceConverted")的元数据(该元数据就是后面要讲到的Metadata)。

ArrayDim

ArrayDim用于记录当前属性的元素数量,当只是声明一个单个对象时,如:

1
2
3
4
5
6
7
8
9
10
11
12
UPROPERTY()
float fval;
UPROPERTY(EditAnywhere)
FString StrVal = TEXT("123456");
UPROPERTY(EditAnywhere)
TSubclassOf<UObject> ClassVal;
UPROPERTY(EditAnywhere)
UTexture2D* Texture2D;
UPROPERTY()
FResultDyDlg ResultDlg;
UPROPERTY()
UMySceneComponent* SceneComp;

这些属性所有的ArrayDim均为1.

但是当使用C++原生数组时:

1
2
UPROPERTY()
int32 iArray[12];

它的ArrayDim就为:

1
CPP_ARRAY_DIM(iArray, AMyActor)

CPP_ARRAY_DIM这个宏定义在CoreUObject/Public/UObject/UnrealType.h

1
2
3
/** helper to calculate an array's dimensions **/
#define CPP_ARRAY_DIM(ArrayName, ClassName) \
(sizeof(((ClassName*)0)->ArrayName) / sizeof(((ClassName*)0)->ArrayName[0]))

就是用来计算数组内的元素数量的,普通的非数组属性其值为1,可以当作是元素数量为1的数组。

Offset

STRUCT_OFFSET宏的作用为得到数据成员相对于类起始地址的偏移,通过获取数据成员指针得到,类型为size_t

然后当前类中的反射属性都会被添加到PropPointers中,也是UHT生成的代码:

1
2
3
const UE4CodeGen_Private::FPropertyParamsBase* const Z_Construct_UClass_AMyActor_Statics::PropPointers[] = {
(const UE4CodeGen_Private::FPropertyParamsBase*)&Z_Construct_UClass_AMyActor_Statics::NewProp_ival,
};

在运行时可以通过UProperty得到指定的对象值:

1
2
3
4
5
6
7
8
9
for (TFieldIterator<UProperty> It(InActor->GetClass()); It; ++It)
{
UProperty* Property = *It;
if (Property->GetNameCPP() == FString("ival"))
{
int32* i32 = Property->ContainerPtrToValuePtr<int32>(InActor);
UE_LOG(LogTemp, Log, TEXT("Property:%s value:%d"), *Property->GetNameCPP(),i32);
}
}

其中UProperty中的ContainerPtrToValuePtr系列函数都会转发到ContainerVoidPtrToValuePtrInternal

1
2
3
4
5
6
7
8
9
10
11
FORCEINLINE void* ContainerVoidPtrToValuePtrInternal(void* ContainerPtr, int32 ArrayIndex) const
{
check(ArrayIndex < ArrayDim);
check(ContainerPtr);
if (0)
{
// in the future, these checks will be tested if the property is NOT relative to a UClass
check(!Cast<UClass>(GetOuter())); // Check we are _not_ calling this on a direct child property of a UClass, you should pass in a UObject* in that case
}
return (uint8*)ContainerPtr + Offset_Internal + ElementSize * ArrayIndex;
}

其实就是拿到UObject的指针,然后偏移到指定位置(第二个参数用在对象是数组的情况,用来访问指定下标的元素,默认情况下访问下标为0)。

Z_Construct_UClass_和NumMetaData

它们的类型分别为FMetaDataPairParamint32,用来记录当前反射属性的元数据:比如属性的Category、注释、所属的文件、ToolTip信息等等,比如在C++函数上添加的注释能够在编辑器蓝图中看到注释的信息,都是靠解析这些元数据来实现的。

1
2
3
4
5
6
7
8
// CoreUObject/Public/UObject/UObjectGlobals.h
#if WITH_METADATA
struct FMetaDataPairParam
{
const char* NameUTF8;
const char* ValueUTF8;
};
#endif

这两个参数通过METADATA_PARAMS包裹,用于处理WITH_MATEDATA的不同情况:

1
2
3
4
5
6
// METADATA_PARAMS(x, y) expands to x, y, if WITH_METADATA is set, otherwise expands to nothing
#if WITH_METADATA
#define METADATA_PARAMS(x, y) x, y,
#else
#define METADATA_PARAMS(x, y)
#endif

把UHT生成的代码宏展开为:

1
2
3
4
5
6
7
8
#if WITH_METADATA
const UE4CodeGen_Private::FMetaDataPairParam Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData[] = {
{ "Category", "MyActor" },
{ "ModuleRelativePath", "Public/MyActor.h" }
};
#endif

METADATA_PARAMS(Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData, ARRAY_COUNT(Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData))

就是把UE4CodeGen_Private::FMetaDataPairParam这个类型的数组和数组元素个数传递给F*PropertyParams,实现在WITH_METADATA的情况下处理是否具有Metadata的情况。

运行时的Property

引擎中通过UE4CodeGen_Private::ConstructFProperty来创建出真正Runtime使用的UProperty(4.25之后是FProperty),定义在UObject/UObjectGlobals.cpp

FBoolPropertyParams特例

当一个反射的数据是bool类型时,引擎产生的反射信息中有一个比较有意思的特例,FBoolPropertyParams,可以看一下它的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct FBoolPropertyParams // : FPropertyParamsBase
{
const char* NameUTF8;
const char* RepNotifyFuncUTF8;
EPropertyFlags PropertyFlags;
EPropertyGenFlags Flags;
EObjectFlags ObjectFlags;
int32 ArrayDim;
uint32 ElementSize;
SIZE_T SizeOfOuter;
void (*SetBitFunc)(void* Obj);
#if WITH_METADATA
const FMetaDataPairParam* MetaDataArray;
int32 NumMetaData;
#endif
};

它具有一个SetBitFunc的函数指针。看一下生成的反射代码:

1
2
3
4
5
6
7
8
9
10
// source code 
UPROPERTY()
bool bEnabled;

// gen.cpp
void Z_Construct_UClass_AMyActor_Statics::NewProp_bEnabled_SetBit(void* Obj)
{
((AMyActor*)Obj)->bEnabled = 1;
}
const UE4CodeGen_Private::FBoolPropertyParams Z_Construct_UClass_AMyActor_Statics::NewProp_bEnabled = { "bEnabled", nullptr, (EPropertyFlags)0x0010000000000000, UE4CodeGen_Private::EPropertyGenFlags::Bool | UE4CodeGen_Private::EPropertyGenFlags::NativeBool, RF_Public|RF_Transient|RF_MarkAsNative, 1, sizeof(bool), sizeof(AMyActor), &Z_Construct_UClass_AMyActor_Statics::NewProp_bEnabled_SetBit, METADATA_PARAMS(Z_Construct_UClass_AMyActor_Statics::NewProp_bEnabled_MetaData, ARRAY_COUNT(Z_Construct_UClass_AMyActor_Statics::NewProp_bEnabled_MetaData)) };

注意:其中的关键是,UHT给该属性生成了一个SetBit的函数,why?其他类型的属性都可以通过STRUCT_OFFSET来获取该成员的类内偏移,为什么bool就不行了呢?

这是因为C++有位域(bit-field)这个概念,一个bool可能只占1bit,而不是1byte,但这不是真正的原因。

真正的原因是,C++标准规定了不能对位域进行取地址操作!前面已经提到了STRUCT_OFFSET实际上是获取到数据成员的指针,得到的是类内偏移,但是因为C++的不能对位域取地址的规定,STRUCT_OFFSET无法用在位域的成员上的。

[IOS/IEC 14882:2014 §9.6]The address-of operator & shall not be applied to a bit-field, so there are no pointers to bit-fields.

那么这又是因为什么呢?因为系统编址的最小单位是字节而不是位,所以没办法取到1字节零几位的地址。也就决定了不能对位域的数据成员取地址。

UE内其实大量用到了bool使用位域的方式来声明(如果不使用位域,bool类型的空间浪费率达到87.5% :)),所以UE就生成了一个函数来为以位域方式声明的成员设置值。

但是!UE不支持直接对加了UPROPERTY的bool使用位域:

1
2
UPROPERTY()
bool bEnabled:1;

编译时会有下列错误:

LogCompile: Error: bool bitfields are not supported.

要写成下列方式:

1
2
UPROPERTY()
uint8 bEnabled:1;

使用这种方式和使用bool bEnabled;方式生成的反射代码一模一样,所以,UE之所以会生成一个函数来设置bool的值,是因为既要支持原生bool,也要支持位域。

通过UProperty获取值

如果我知道某个类的对象内有一个属性名字,那么怎么能够得到它的值呢?这个可以基于UE的属性反射来实现:

首先通过TFieldIterator可以遍历该对象的UProperty:

1
2
3
4
for (TFieldIterator<UProperty> It(InActor->GetClass()); It; ++It)
{
UProperty* Property = *It;
}

然后可以根据得到的Property来判读名字:

1
if (Property->GetNameCPP() == FString("ival"))

检测是指定名字的Property后可以通过UProperty上的ContainerPtrToValuePtr函数来获取对象内该属性的指针:

1
int32* i32 = Property->ContainerPtrToValuePtr<int32>(InActor)

前面讲到过,UPropery里存储Offdet值就是当前属性相对于对象起始地址的偏移。而ContainerPtrToValuePtr函数所做的就是得到当前对象偏移Offset的地址然后做了类型转换。

Property的Flag

通过上面的分析,可以看到UPROPERTY添加的标记,如EditAnywhere等,会给指定的Property生成FLAG存储在F*PropertyParams结构的第三个参数中,是位描述的。

可选值为EPropertyFlags枚举值:

Runtime/CoreUObject/Public/UObject/ObjectMacros.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/**
* Flags associated with each property in a class, overriding the
* property's default behavior.
* @warning When adding one here, please update ParsePropertyFlags()
*/
enum EPropertyFlags : uint64
{
CPF_None = 0,

CPF_Edit = 0x0000000000000001, ///< Property is user-settable in the editor.
CPF_ConstParm = 0x0000000000000002, ///< This is a constant function parameter
CPF_BlueprintVisible = 0x0000000000000004, ///< This property can be read by blueprint code
CPF_ExportObject = 0x0000000000000008, ///< Object can be exported with actor.
CPF_BlueprintReadOnly = 0x0000000000000010, ///< This property cannot be modified by blueprint code
CPF_Net = 0x0000000000000020, ///< Property is relevant to network replication.
CPF_EditFixedSize = 0x0000000000000040, ///< Indicates that elements of an array can be modified, but its size cannot be changed.
CPF_Parm = 0x0000000000000080, ///< Function/When call parameter.
CPF_OutParm = 0x0000000000000100, ///< Value is copied out after function call.
CPF_ZeroConstructor = 0x0000000000000200, ///< memset is fine for construction
CPF_ReturnParm = 0x0000000000000400, ///< Return value.
CPF_DisableEditOnTemplate = 0x0000000000000800, ///< Disable editing of this property on an archetype/sub-blueprint
//CPF_ = 0x0000000000001000, ///<
CPF_Transient = 0x0000000000002000, ///< Property is transient: shouldn't be saved or loaded, except for Blueprint CDOs.
CPF_Config = 0x0000000000004000, ///< Property should be loaded/saved as permanent profile.
//CPF_ = 0x0000000000008000, ///<
CPF_DisableEditOnInstance = 0x0000000000010000, ///< Disable editing on an instance of this class
CPF_EditConst = 0x0000000000020000, ///< Property is uneditable in the editor.
CPF_GlobalConfig = 0x0000000000040000, ///< Load config from base class, not subclass.
CPF_InstancedReference = 0x0000000000080000, ///< Property is a component references.
//CPF_ = 0x0000000000100000, ///<
CPF_DuplicateTransient = 0x0000000000200000, ///< Property should always be reset to the default value during any type of duplication (copy/paste, binary duplication, etc.)
CPF_SubobjectReference = 0x0000000000400000, ///< Property contains subobject references (TSubobjectPtr)
//CPF_ = 0x0000000000800000, ///<
CPF_SaveGame = 0x0000000001000000, ///< Property should be serialized for save games, this is only checked for game-specific archives with ArIsSaveGame
CPF_NoClear = 0x0000000002000000, ///< Hide clear (and browse) button.
//CPF_ = 0x0000000004000000, ///<
CPF_ReferenceParm = 0x0000000008000000, ///< Value is passed by reference; CPF_OutParam and CPF_Param should also be set.
CPF_BlueprintAssignable = 0x0000000010000000, ///< MC Delegates only. Property should be exposed for assigning in blueprint code
CPF_Deprecated = 0x0000000020000000, ///< Property is deprecated. Read it from an archive, but don't save it.
CPF_IsPlainOldData = 0x0000000040000000, ///< If this is set, then the property can be memcopied instead of CopyCompleteValue / CopySingleValue
CPF_RepSkip = 0x0000000080000000, ///< Not replicated. For non replicated properties in replicated structs
CPF_RepNotify = 0x0000000100000000, ///< Notify actors when a property is replicated
CPF_Interp = 0x0000000200000000, ///< interpolatable property for use with matinee
CPF_NonTransactional = 0x0000000400000000, ///< Property isn't transacted
CPF_EditorOnly = 0x0000000800000000, ///< Property should only be loaded in the editor
CPF_NoDestructor = 0x0000001000000000, ///< No destructor
//CPF_ = 0x0000002000000000, ///<
CPF_AutoWeak = 0x0000004000000000, ///< Only used for weak pointers, means the export type is autoweak
CPF_ContainsInstancedReference = 0x0000008000000000, ///< Property contains component references.
CPF_AssetRegistrySearchable = 0x0000010000000000, ///< asset instances will add properties with this flag to the asset registry automatically
CPF_SimpleDisplay = 0x0000020000000000, ///< The property is visible by default in the editor details view
CPF_AdvancedDisplay = 0x0000040000000000, ///< The property is advanced and not visible by default in the editor details view
CPF_Protected = 0x0000080000000000, ///< property is protected from the perspective of script
CPF_BlueprintCallable = 0x0000100000000000, ///< MC Delegates only. Property should be exposed for calling in blueprint code
CPF_BlueprintAuthorityOnly = 0x0000200000000000, ///< MC Delegates only. This delegate accepts (only in blueprint) only events with BlueprintAuthorityOnly.
CPF_TextExportTransient = 0x0000400000000000, ///< Property shouldn't be exported to text format (e.g. copy/paste)
CPF_NonPIEDuplicateTransient = 0x0000800000000000, ///< Property should only be copied in PIE
CPF_ExposeOnSpawn = 0x0001000000000000, ///< Property is exposed on spawn
CPF_PersistentInstance = 0x0002000000000000, ///< A object referenced by the property is duplicated like a component. (Each actor should have an own instance.)
CPF_UObjectWrapper = 0x0004000000000000, ///< Property was parsed as a wrapper class like TSubclassOf<T>, FScriptInterface etc., rather than a USomething*
CPF_HasGetValueTypeHash = 0x0008000000000000, ///< This property can generate a meaningful hash value.
CPF_NativeAccessSpecifierPublic = 0x0010000000000000, ///< Public native access specifier
CPF_NativeAccessSpecifierProtected = 0x0020000000000000, ///< Protected native access specifier
CPF_NativeAccessSpecifierPrivate = 0x0040000000000000, ///< Private native access specifier
CPF_SkipSerialization = 0x0080000000000000, ///< Property shouldn't be serialized, can still be exported to text
};

UE4:StaticClass和GetClass的区别

StaticClass是继承自UObject类的static函数,GetClassUObjectBase的成员函数。

UObjectBaseGetClass获取到的UClass就是在NewObject时传递进来的UClass.(代码在UObject\UObjectGlobal.cpp中)

用途不一样,StaticClass是在获取具体类型的UClass,而GetClass是获取到当前对象的真实UClass。

UE4:UObject的FObjectInitializer构造函数的调用

注意:只有GENERATER_UCLASS_BODY才可以实现FObjectInitializer的构造函数。

在继承自UObject的类中,都可以自己写一个接收const FObjectInitializer&参数的构造函数,在创建对象时会被调用:

1
UMyObject::UMyObject(const FObjectInitializer& Initializer){}

在类中的GENERATED_UCLASS_BODY中默认声明这样一个构造函数。并且,在UHT生成的代码中通过宏还定义了一个函数:

1
DEFINE_DEFAULT_OBJECT_INITIALIZER_CONSTRUCTOR_CALL(UMyObject) 

这个宏经过展开之后就是这样的:

1
static void __DefaultConstructor(const FObjectInitializer& X) { new((EInternal*)X.GetObj())UMyObject(X); }

为当前的对象类型UMyObject定义了一个__DefaultConstructor函数,作用是在当前FObjectInitializer传入的参数的Object上调用FObjectInitializer的构造函数。

注意:如果是GENERATED_BODY则不会声明这个构造函数,使用的就是另一个宏:

1
DEFINE_DEFAULT_CONSTRUCTOR_CALL(UMyObject)

展开之后是这样的:

1
static void __DefaultConstructor(const FObjectInitializer& X) { new((EInternal*)X.GetObj())UMyObject; }

是通过GANERATED_BODYGENERATED_UCLASS_BODY来定义了两种__DefaultConstructor的实现,一种是调用FObjectInitializer的构造函数,另一种是调用类的默认构造函数。

所以,默认情况下FObjectInitializer的构造函数和UObject的默认构造函数在调用时只会走一个。

那么它是如何被调用到的呢?

NewObject中调用的StaticConstructObject_Internal中有以下代码:

1
2
3
4
5
6
7
8
9
bool bRecycledSubobject = false;	
Result = StaticAllocateObject(InClass, InOuter, InName, InFlags, InternalSetFlags, bCanRecycleSubobjects, &bRecycledSubobject);
check(Result != NULL);
// Don't call the constructor on recycled subobjects, they haven't been destroyed.
if (!bRecycledSubobject)
{
STAT(FScopeCycleCounterUObject ConstructorScope(InClass, GET_STATID(STAT_ConstructObject)));
(*InClass->ClassConstructor)( FObjectInitializer(Result, InTemplate, bCopyTransientsFromClassDefaults, true, InInstanceGraph) );
}

通过传入进来的UClass对象获取其中的ClassConstructor函数指针,并构造出一个FObjectInitializer作为参数传递。

在之前的笔记(UE4:StaticClass是如何定义的)中,写到了,通过GetPrivateStaticClass获取到当前UObject类的UClass实例会实例化出一个当前类的InternalConstructor函数(注意这个模板参数T是UObject的类):

1
2
3
4
5
6
7
8
// class.h

// Helper template to call the default constructor for a class
template<class T>
void InternalConstructor( const FObjectInitializer& X )
{
T::__DefaultConstructor(X);
}

就将其转发到了上面讲到的__DefaultConstructor函数上,然后它里面又转发到了所传入FObjectInitializer对象上的const FObjectInitializer&构造函数上(或者默认构造函数上)。

创建对象并调用const FObjectInitializer&构造函数的调用流程为:

  1. 通过调用StaticAllocateObject根据传入的UClass创建出对象
  2. 通过传入的UClass对象内的ClassConstructor函数指针调用所创建UObject类的InternalConstructor函数
  3. UMyObject::InternalConstructor会转发到UMyObject::__DefaultConstructor
  4. UMyObject::__DefaultConstructor会从接收到的FObjectInitializer对象上获取到通过StaticAllocateObject创建的对象,然后通过placement-new的方式,在这块内存上调用UMyObject类的const FObjectInitializer&构造函数。

通过以上的流程就实现了NewObject时会自动调用到它的FObjectInitializer构造函数。

注意:在CDO的构造上有点区别,CDO的构造是通过UClass::GetDefaultObject中实现上述流程的。

UE4:StaticClass是如何定义的

在UE中可以对UObject的类执行UXXX::StaticClass方法来获取到它的UClass对象。

但是它是如何定义的?首先要看我们声明对象的MyActor.generated.h中(以AMyActor类为例):

1
template<> MICROEND_423_API UClass* StaticClass<class AMyActor>();

为AMyActor类特化了StaticClass的版本。再去MyActor.gen.cpp中找以下它的实现:

1
2
3
4
5
IMPLEMENT_CLASS(AMyActor, 31943282);
template<> MICROEND_423_API UClass* StaticClass<AMyActor>()
{
return AMyActor::StaticClass();
}

从这里看也就只是转发调用而已,但是关键点隐藏在其他地方。
首先AMyActor::StaticClass类的定义是在AMyActor.generated.hDECLARE_CLASS宏中(该宏定义在ObjectMacros.h#L1524),返回的是GetPrivateStaticClass的调用。

GetPrivateStaticClass则在AMyAcotr.gen.cpp中的IMPLEMENT_CLASS实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Runtime/CoreUObject/Public/UObject/ObjectMacros.h
// Register a class at startup time.
#define IMPLEMENT_CLASS(TClass, TClassCrc) \
static TClassCompiledInDefer<TClass> AutoInitialize##TClass(TEXT(#TClass), sizeof(TClass), TClassCrc); \
UClass* TClass::GetPrivateStaticClass() \
{ \
static UClass* PrivateStaticClass = NULL; \
if (!PrivateStaticClass) \
{ \
/* this could be handled with templates, but we want it external to avoid code bloat */ \
GetPrivateStaticClassBody( \
StaticPackage(), \
(TCHAR*)TEXT(#TClass) + 1 + ((StaticClassFlags & CLASS_Deprecated) ? 11 : 0), \
PrivateStaticClass, \
StaticRegisterNatives##TClass, \
sizeof(TClass), \
alignof(TClass), \
(EClassFlags)TClass::StaticClassFlags, \
TClass::StaticClassCastFlags(), \
TClass::StaticConfigName(), \
(UClass::ClassConstructorType)InternalConstructor<TClass>, \
(UClass::ClassVTableHelperCtorCallerType)InternalVTableHelperCtorCaller<TClass>, \
&TClass::AddReferencedObjects, \
&TClass::Super::StaticClass, \
&TClass::WithinClass::StaticClass \
); \
} \
return PrivateStaticClass; \
}

可以看到GetPrivateStaticClass其实就是通过这些元数据构造出UClass的。

如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
IMPLEMENT_CLASS(AMyActor, 3240835608);

// 宏展开之后
static TClassCompiledInDefer < AMyActor > AutoInitializeAMyActor(TEXT("AMyActor"), sizeof(AMyActor), 3240835608);
UClass * AMyActor::GetPrivateStaticClass() {
static UClass * PrivateStaticClass = NULL;
if (!PrivateStaticClass) {
GetPrivateStaticClassBody(
StaticPackage(),
(TCHAR*)TEXT("AMyActor") + 1 + ((StaticClassFlags & CLASS_Deprecated) ? 11 : 0),
PrivateStaticClass,
StaticRegisterNativesAMyActor,
sizeof(AMyActor),
alignof(AMyActor),
(EClassFlags) AMyActor::StaticClassFlags,
AMyActor::StaticClassCastFlags(),
AMyActor::StaticConfigName(),
(UClass::ClassConstructorType) InternalConstructor<AMyActor>,
(UClass::ClassVTableHelperCtorCallerType) InternalVTableHelperCtorCaller<AMyActor>,
&AMyActor::AddReferencedObjects,
&AMyActor::Super::StaticClass,
&AMyActor::WithinClass::StaticClass
);
}
return PrivateStaticClass;
};

UClass中的函数指针

上面代码中比较关键的点为:

1
2
(UClass::ClassConstructorType)InternalConstructor<TClass>, \
(UClass::ClassVTableHelperCtorCallerType)InternalVTableHelperCtorCaller<TClass>, \

这两行是模板实例化出了两个函数并转换成函数指针传递给GetPrivateStaticClassBody

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// class.h

// Helper template to call the default constructor for a class
template<class T>
void InternalConstructor( const FObjectInitializer& X )
{
T::__DefaultConstructor(X);
}

// Helper template to call the vtable ctor caller for a class
template<class T>
UObject* InternalVTableHelperCtorCaller(FVTableHelper& Helper)
{
return T::__VTableCtorCaller(Helper);
}

就是对__DefaultConstructor这样函数的的转发调用。

UClass::ClassConstructorTypeUClass::ClassVTableHelperCtorCallerType这两个typedef为:

1
2
typedef void		(*ClassConstructorType)				(const FObjectInitializer&);
typedef UObject* (*ClassVTableHelperCtorCallerType) (FVTableHelper& Helper);

GetPrivateStaticClassBody

其中的GetPrivateStaticClassBody函数是定义在Runtime\CoreUObject\Private\UObject\Class.cpp中的。

原型为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void GetPrivateStaticClassBody(
const TCHAR* PackageName,
const TCHAR* Name,
UClass*& ReturnClass,
void(*RegisterNativeFunc)(),
uint32 InSize,
uint32 InAlignment,
EClassFlags InClassFlags,
EClassCastFlags InClassCastFlags,
const TCHAR* InConfigName,
UClass::ClassConstructorType InClassConstructor,
UClass::ClassVTableHelperCtorCallerType InClassVTableHelperCtorCaller,
UClass::ClassAddReferencedObjectsType InClassAddReferencedObjects,
UClass::StaticClassFunctionType InSuperClassFn,
UClass::StaticClassFunctionType InWithinClassFn,
bool bIsDynamic /*= false*/
);

其中第四个参数是传入注册Native函数的函数指针,该函数在MyActor.gen.cpp中生成,也可以通过在UFUNCTION中添加CustomThunk函数来自己实现,UnLua的覆写C++函数就是基于替换thunk函数做的。

GetPrivateStaticClassBody中通过(UClass*)GUObjectAllocator.AllocateUObject来分配出UClass的内存,因为所有的UClass结构都一致。

1
2
3
4
5
6
7
8
9
10
// MyActor.gen.cpp
void AMyActor::StaticRegisterNativesAMyActor()
{
UClass* Class = AMyActor::StaticClass();
static const FNameNativePtrPair Funcs[] = {
{ "ReceiveBytes", &AMyActor::execReceiveBytes },
{ "TESTFUNC", &AMyActor::execTESTFUNC },
};
FNativeFunctionRegistrar::RegisterFunctions(Class, Funcs, ARRAY_COUNT(Funcs));
}

其实就是把Native的函数通过AddNativeFunction添加到UClass中:

1
2
3
4
5
6
7
8
// Runtime\CoreUObject\Private\UObject\Class.cpp 
void FNativeFunctionRegistrar::RegisterFunctions(class UClass* Class, const FNameNativePtrPair* InArray, int32 NumFunctions)
{
for (; NumFunctions; ++InArray, --NumFunctions)
{
Class->AddNativeFunction(UTF8_TO_TCHAR(InArray->NameUTF8), InArray->Pointer);
}
}

UE4:获取UObject的资源路径

可以通过FSoftObjectPath传入UObject来获得:

1
2
3
4
5
FString GetObjectResource(UObject* Obj)
{
FSoftObjectPath SoftRef(Obj);
return SoftRef.ToString();
}

注意:直接ToString获取到的路径是PackagePath的,形如/Game/XXXX.XXXX这种形式,可以通过GetLongPackageName得到去掉.XXXX的字符串。

UE4:BP to CPP

Project Settings-Packaging-Blueprint下添加想要转换的蓝图资源:

设置之后执行打包就会在项目的下列路径中产生对应的.h/.cpp以及生成generated.h/gen.cpp

1
Intermediate\Plugins\NativizedAssets\Windows\Game\Intermediate\Build\Win64\UE4\Inc\NativizedAssets

UE4:监听窗口关闭

可以通过监听FSlateFontServices里的OnSlateWindowDestroyed:

1
2
3
4
5
6
7
/**
* Called on the game thread right before the slate window handle is destroyed.
* This gives users a chance to release any viewport specific resources they may have active when the window is destroyed
* @param Pointer to the API specific backbuffer type
*/
DECLARE_MULTICAST_DELEGATE_OneParam(FOnSlateWindowDestroyed, void*);
FOnSlateWindowDestroyed& OnSlateWindowDestroyed() { return OnSlateWindowDestroyedDelegate; }

监听方法:

1
FSlateApplication::Get().GetRenderer()->OnSlateWindowDestroyed().AddRaw(this, &FSceneViewport::OnWindowBackBufferResourceDestroyed);

UE4:UnLua的EXPORT_PRIMITIVE_TYPE

UnLua里使用EXPORT_PRIMITIVE_TYPE宏来导出内置类型:

1
EXPORT_PRIMITIVE_TYPE(uint64, TPrimitiveTypeWrapper<uint64>, uint64)

宏展开之后为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
template < > struct UnLua::TType < TPrimitiveTypeWrapper < uint64 > , false > {
static
const char * GetName() {
return "uint64";
}
};
struct FExporteduint64Helper {
typedef TPrimitiveTypeWrapper < uint64 > ClassType;
static FExporteduint64Helper StaticInstance;
UnLua::TExportedClass < false, TPrimitiveTypeWrapper < uint64 > , uint64 > * ExportedClass;
~FExporteduint64Helper() {
delete ExportedClass;
}
FExporteduint64Helper(): ExportedClass(nullptr) {
UnLua::TExportedClass < false, TPrimitiveTypeWrapper < uint64 > , uint64 > * Class = (UnLua::TExportedClass < false, TPrimitiveTypeWrapper < uint64 > , uint64 > * ) UnLua::FindExportedClass("uint64");
if (!Class) {
ExportedClass = new UnLua::TExportedClass < false, TPrimitiveTypeWrapper < uint64 > , uint64 > ("uint64", nullptr);
UnLua::ExportClass((UnLua::IExportedClass * ) ExportedClass);
Class = ExportedClass;
}
Class - > AddProperty("Value", & ClassType::Value);
}
};
FExporteduint64Helper FExporteduint64Helper::StaticInstance;
static struct FTypeInterfaceuint64 {
FTypeInterfaceuint64() {
UnLua::AddTypeInterface("uint64", UnLua::GetTypeInterface < uint64 > ());
}
}TypeInterfaceuint64;

UE4:4.25 MountPak没有材质

在项目打包时在Project Settgins-Packaging中开启了Share Material shader code时,后续的热更pak打包,如果没有同步把ushaderbytecode打包进去并自己加载,会产生下列材质丢失的问题:

经过调试后发现,是因为4.25在mount pak之后不会加载新的Pak中的shaderbytecode,找到了问题,解决办法就手到擒来了,找到引擎中加载shaderbytecode的代码自己调用一遍即可。

在项目打包时默认会生成两个shaderbytecode文件:

1
2
ShaderArchive-Global-PCD3D_SM5.ushaderbytecode
ShaderArchive-HotPatcherExample-PCD3D_SM5.ushaderbytecode

并且它们存在于pak中Mount point的路径均为:

1
../../../PROJECT_NAME/Content/

而且根据shaderbytecode文件路径的组成规则:

1
2
3
4
5
6
7
8
static FString ShaderExtension = TEXT(".ushaderbytecode");
static FString StableExtension = TEXT(".scl.csv");
static FString PipelineExtension = TEXT(".ushaderpipelines");

static FString GetCodeArchiveFilename(const FString& BaseDir, const FString& LibraryName, FName Platform)
{
return BaseDir / FString::Printf(TEXT("ShaderArchive-%s-"), *LibraryName) + Platform.ToString() + ShaderExtension;
}

所以,只需要传递基础路径和LibraryName即可(OpenLibrary中通过调用GetCodeArchiveFilename来获取要加载的文件)。

即要重新加载global和项目的shaderbytecode,在mount成功之后执行下面两行代码即可:

1
2
FShaderCodeLibrary::OpenLibrary("Global", FPaths::ProjectContentDir());
FShaderCodeLibrary::OpenLibrary(FApp::GetProjectName(), FPaths::ProjectContentDir());

通过笔记最前面的一段话,可以总结出两个解决方案:

  1. 开启了Share Material shader code的情况下,需要把shaderbytecode打包,并自己在Mount时加载;
  2. 打Pak时Cook资源不要开启Share Material shader code,这样会把资源的shader都打包在资源内部,从而避免需要单独加载shader的问题;

UE4:Android上Arrow组件的crash

在游戏中把一个Actor上的Arrow组件设置为visible,打包Android上运行会Crash:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
06-17 15:23:51.976 26991 27112 F libc    : Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 in tid 27112 (RenderThread 2), pid 26991 (MainThread-UE4)
06-17 15:23:52.350 27129 27129 F DEBUG : #00 pc 062fa090 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (FArrowSceneProxy::GetDynamicMeshElements(TArray<FSceneView const*, FDefaultAllocator> const&, FSceneViewFamily const&, unsigned int, FMeshElementCollector&) const+824)
06-17 15:23:52.350 27129 27129 F DEBUG : #01 pc 058010f4 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (_ZN14FSceneRenderer25GatherDynamicMeshElementsER6TArrayI9FViewInfo17FDefaultAllocatorEPK6FSceneRK16FSceneViewFamilyR25FGlobalDynamicIndexBufferR26FGlobalDynamicVertexBufferR24FGlobalDynamicReadBufferRKS0_Ih18TMemStackAllocatorILj0EEESL_SL_R21FMeshElementCollector+2456)
06-17 15:23:52.350 27129 27129 F DEBUG : #02 pc 0580f8a8 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (_ZN14FSceneRenderer21ComputeViewVisibilityER24FRHICommandListImmediateN22FExclusiveDepthStencil4TypeER6TArrayI13FViewCommands16TInlineAllocatorILj4E17FDefaultAllocatorEER25FGlobalDynamicIndexBufferR26FGlobalDynamicVertexBufferR24FGlobalDynamicReadBuffer+39716)
06-17 15:23:52.350 27129 27129 F DEBUG : #03 pc 054ffb30 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (FMobileSceneRenderer::InitViews(FRHICommandListImmediate&)+1076)
06-17 15:23:52.350 27129 27129 F DEBUG : #04 pc 05500a98 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (FMobileSceneRenderer::Render(FRHICommandListImmediate&)+1228)
06-17 15:23:52.350 27129 27129 F DEBUG : #05 pc 057fa970 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (_ZZN15FRendererModule24BeginRenderingViewFamilyEP7FCanvasP16FSceneViewFamilyENK4$_85clER24FRHICommandListImmediate+2316)
06-17 15:23:52.350 27129 27129 F DEBUG : #06 pc 057fc944 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (_ZN10TGraphTaskI31TEnqueueUniqueRenderCommandTypeIZN15FRendererModule24BeginRenderingViewFamilyEP7FCanvasP16FSceneViewFamilyE21FDrawSceneCommandNameZNS1_24BeginRenderingViewFamilyES3_S5_E4$_85EE11ExecuteTaskER6TArrayIP14FBaseGraphTask17FDefaultAllocatorEN13ENamedThreads4TypeE+712)
06-17 15:23:52.350 27129 27129 F DEBUG : #07 pc 040bfa04 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (FNamedTaskThread::ProcessTasksNamedThread(int, bool)+2876)
06-17 15:23:52.350 27129 27129 F DEBUG : #08 pc 040be518 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (FNamedTaskThread::ProcessTasksUntilQuit(int)+108)
06-17 15:23:52.350 27129 27129 F DEBUG : #09 pc 0518eaec /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (RenderingThreadMain(FEvent*)+436)
06-17 15:23:52.350 27129 27129 F DEBUG : #10 pc 051d93d8 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (FRenderingThread::Run()+20)
06-17 15:23:52.350 27129 27129 F DEBUG : #11 pc 04142c8c /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (FRunnableThreadPThread::Run()+164)
06-17 15:23:52.350 27129 27129 F DEBUG : #12 pc 040b9ef0 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (FRunnableThreadPThread::_ThreadProc(void*)+80)
06-17 15:23:52.430 26991 27061 D UE4 : Used memory: 361838
06-17 15:27:43.270 13042 13231 I MtpDatabase: Mediaprovider didn't delete /storage/emulated/0/UE4Game/GWorld/GWorld/Saved/Paks/1.45_Android_ETC2_001_P.pak
06-17 15:27:45.781 13042 13231 D MtpServer: path: /storage/emulated/0/UE4Game/GWorld/GWorld/Saved/Paks/1.45_Android_ETC2_001_P.pak parent: 68 storageID: 00010001

有时间在来具体分析。

UE4:target/build.cs输出Log

可以使用C#里的以下代码:

1
2
3
using System;

System.Console.WriteLine("12346");

UE4:TargetRules的BuildSettingsVersion

PS:UE4.24之后才有。

可以在Target.cs中指定:

1
DefaultBuildSettings = BuildSettingsVersion.V2;

BuildSettingsVersion可以指定构建时使用的默认设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/// <summary>
/// Determines which version of the engine to take default build settings from. This allows for backwards compatibility as new options are enabled by default.
/// </summary>
public enum BuildSettingsVersion
{
/// <summary>
/// Legacy default build settings for 4.23 and earlier.
/// </summary>
V1,

/// <summary>
/// New defaults for 4.24: ModuleRules.PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs, ModuleRules.bLegacyPublicIncludePaths = false.
/// </summary>
V2,

// *** When adding new entries here, be sure to update GameProjectUtils::GetDefaultBuildSettingsVersion() to ensure that new projects are created correctly. ***

/// <summary>
/// Always use the defaults for the current engine version. Note that this may cause compatibility issues when upgrading.
/// </summary>
Latest
}

UE4.23及之前的引擎版本是V1,用来控制当项目升级引擎版本时使用之前引擎的构建设置,用于解决项目升级之后会有大量错误的问题。

注意:因为4.25之后的是V2,默认bLegacyPublicIncludePaths=false,这个会导致如果模块中相对于Public的代码路径,如Public/Core/CoreCode.h,如果没有添加Core目录到PublicIncludePaths中,在工程的其他地方不指定相对路径,直接用CoreCode.h,在V1的版本里是可以编译过的,但是在V2中就会有编译错误。

UE4:从TargetRules获取引擎版本

TargetRules中具有**Version (ReadOnlyBuildVersion)**成员,它是BuildVersion的类型,定义在Programs\UnrealBuildTool\System\BuildVersion.cs中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
[Serializable]
public class BuildVersion
{
/// <summary>
/// The major engine version (4 for UE4)
/// </summary>
public int MajorVersion;

/// <summary>
/// The minor engine version
/// </summary>
public int MinorVersion;

/// <summary>
/// The hotfix/patch version
/// </summary>
public int PatchVersion;

/// <summary>
/// The changelist that the engine is being built from
/// </summary>
public int Changelist;

/// <summary>
/// The changelist that the engine maintains compatibility with
/// </summary>
public int CompatibleChangelist;

/// <summary>
/// Whether the changelist numbers are a licensee changelist
/// </summary>
public bool IsLicenseeVersion;

/// <summary>
/// Whether the current build is a promoted build, that is, built strictly from a clean sync of the given changelist
/// </summary>
public bool IsPromotedBuild;

/// <summary>
/// Name of the current branch, with '/' characters escaped as '+'
/// </summary>
public string BranchName;

/// <summary>
/// The current build id. This will be generated automatically whenever engine binaries change if not set in the default Engine/Build/Build.version.
/// </summary>
public string BuildId;

/// <summary>
/// The build version string
/// </summary>
public string BuildVersionString;
// ...
}

可以在Target.cs或者Build.cs里通过Target.Version来访问引擎版本,可以根据不同的引擎版本来使用不同的库。

UE4:从TargetRules获取Configuration

ReadOnlyTargetRules接收到的Target,可以从其中获取Configuration成员,用于检测打包的BuildConfiguration

1
2
3
4
if (Target.Configuration == UnrealTargetConfiguration.Shipping)
{
// ...
}

枚举值为Development/Debug/DebugGame/Shipping/Test等。

IOS CrashLog分析

UE4:GC

UE4使用标记-清扫式的GC方式,它是一种经典的垃圾回收方式。一次垃圾回收分为两个阶段。第一阶段从一个根集合出发,遍历所有可达对象,遍历完成后就能标记出可达对象和不可达对象了,这个阶段会在一帧内完成。第二阶段会渐进式的清理这些不可达对象,因为不可达的对象将永远不能被访问到,所以可以分帧清理它们,避免一下子清理很多UObject,比如map卸载时,发生明显的卡顿。

UObject之间的引用关系需要用强指针引用加UPROPERTY标记完成。

UPROPERTY标记通过UHT之后会生成UProperty对象,UProperty对象可以控制对属性的访问。也通过UProperty对象保存引用关系。

如果想要给没有添加UPROPERTY标记的对象添加引用可以通过重写UObject的虚函数AddReferencedObjects,比如AActor中的OwnedComponents

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Actor.h
TSet<UActorComponent*> OwnedComponents;

// Actor.cpp
void AActor::AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector)
{
AActor* This = CastChecked<AActor>(InThis);
Collector.AddReferencedObjects(This->OwnedComponents);
#if WITH_EDITOR
if (This->CurrentTransactionAnnotation.IsValid())
{
This->CurrentTransactionAnnotation->AddReferencedObjects(Collector);
}
#endif
Super::AddReferencedObjects(InThis, Collector);
}

UE4:SDetailsView监听属性变化

可以通过监听基类SDetailsViewBase中的OnFinishedChangingPropertiesDelegate代理来实现。

接收参数是FPropertyChangedEvent

1
DECLARE_MULTICAST_DELEGATE_OneParam(FOnFinishedChangingProperties, const FPropertyChangedEvent&);

UE4:UObject serializer的调用栈

有时间再来分析具体内容。

UE4:FName/FString/FText的区别

FName

FName的字符串一般用在为资源命名或者访问资源时(比如命名的骨骼)需要使用。
它使用一个轻型系统使用字符串,特定字符串会被重复使用,在数据表中也就只存储一次。

  1. 只在内存中存储一次
  2. 不区分大小写
  3. 不能被修改
  4. 查找和访问速度比较快
  5. 内部有一个HASH值
    FName在Edior中占12个字节,打包8字节,FName的XXXX_12这样的字符串会被分成string part和number part,估计是为了想不为每个拼接的结果都在NamePool中创建一份吧。

FString

FSting比较类似于标准库的std::string,区分大小写,可修改,每份实例都是单独的内存。

FText

FText是用于本地化的类,所有需要展示的文本都需要使用FText,它提供了以下功能:

  1. 创建本地化文本
  2. 格式化文本
  3. 从数字生成文本
  4. 从日期或时间生成文本
  5. 转换文本(大小写转换等)

文档

UE4:Plugin添加其他Plugin的模块

如果插件A要引用插件B中的模块,那么就需要在插件A的uplugin文件中添加对插件B的依赖:

1
2
3
4
5
6
"Plugins": [
{
"Name": "B",
"Enabled": true
}
]

然后就可以在插件A中添加插件B中的模块了。

UE4:Build Configurations

Build Status Describle
Debug 引擎和游戏符号都以debug方式编译,需要源码版引擎
DebugGame 优化引擎代码,只可以调试Game符号
Development 默认的配置,在DebugGame的模式上进行优化,只可以调试游戏符号
Shipping 不包含调试符号、Console、stats、profiling工具,用于发行版本
Test 与Shipping相同,但是要会包含Console、stats、profiling等工具

如果使用从EpicLauncher安装的引擎,打包Debug时会提示:

1
Targets cannot be built in the Debug configuration with this engine distribution.

这个报错是在UBT中产生的,具体代码在UnrealBuildTool\Configuration\UEBuildTarget.cs

UE4:GlobalShaderCache的加载

Runtime/Engine/Private/ShaderCompiler/ShaderCompiler.cpp中有获取GlobalShaderCache*.bin的方法:

1
2
3
4
static FString GetGlobalShaderCacheFilename(EShaderPlatform Platform)
{
return FString(TEXT("Engine")) / TEXT("GlobalShaderCache-") + LegacyShaderPlatformToShaderFormat(Platform).ToString() + TEXT(".bin");
}

在同文件中定义的CompileGlobalShaderMap函数中被读取。
调用栈:

完整流程有时间再来分析。

UE4:ushaderbytecode的加载

Runtime/RenderCore/Private/ShaderCodeLibrary.cpp文件中,可以获取到shaderbytecode相关的文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
static uint32 GShaderCodeArchiveVersion = 2;
static uint32 GShaderPipelineArchiveVersion = 1;

static FString ShaderExtension = TEXT(".ushaderbytecode");
static FString StableExtension = TEXT(".scl.csv");
static FString PipelineExtension = TEXT(".ushaderpipelines");

static FString GetCodeArchiveFilename(const FString& BaseDir, const FString& LibraryName, FName Platform)
{
return BaseDir / FString::Printf(TEXT("ShaderArchive-%s-"), *LibraryName) + Platform.ToString() + ShaderExtension;
}

static FString GetStableInfoArchiveFilename(const FString& BaseDir, const FString& LibraryName, FName Platform)
{
return BaseDir / FString::Printf(TEXT("ShaderStableInfo-%s-"), *LibraryName) + Platform.ToString() + StableExtension;
}

static FString GetPipelinesArchiveFilename(const FString& BaseDir, const FString& LibraryName, FName Platform)
{
return BaseDir / FString::Printf(TEXT("ShaderArchive-%s-"), *LibraryName) + Platform.ToString() + PipelineExtension;
}

static FString GetShaderCodeFilename(const FString& BaseDir, const FString& LibraryName, FName Platform)
{
return BaseDir / FString::Printf(TEXT("ShaderCode-%s-"), *LibraryName) + Platform.ToString() + ShaderExtension;
}

static FString GetShaderDebugFolder(const FString& BaseDir, const FString& LibraryName, FName Platform)
{
return BaseDir / FString::Printf(TEXT("ShaderDebug-%s-"), *LibraryName) + Platform.ToString();
}

然后在同文件的FShaderLibraryInstance::Create来加载。

完整流程有时间再来分析。

UE4:默认打包到pak里的资源

UE在打包的时候会把工程下的Content中的资源进行依赖分析然后打包,但是经过对比之后发现,引擎中还会添加额外的没有引用到的资源,经过分析代码发现引擎的ini中有指定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
;Engine\Config\BaseEngine.ini
[Engine.StartupPackages]
bSerializeStartupPackagesFromMemory=true
bFullyCompressStartupPackages=false
+Package=/Engine/EngineMaterials/BlinkingCaret
+Package=/Engine/EngineMaterials/DefaultBokeh
+Package=/Engine/EngineMaterials/DefaultBloomKernel
+Package=/Engine/EngineMaterials/DefaultDeferredDecalMaterial
;+Package=/Engine/EngineMaterials/DefaultPostProcessMaterial
+Package=/Engine/EngineMaterials/DefaultDiffuse
+Package=/Engine/EngineMaterials/DefaultLightFunctionMaterial
+Package=/Engine/EngineMaterials/WorldGridMaterial
+Package=/Engine/EngineMaterials/DefaultMaterial
+Package=/Engine/EngineMaterials/DefaultNormal
+Package=/Engine/EngineMaterials/DefaultPhysicalMaterial
+Package=/Engine/EngineMaterials/DefaultVirtualTextureMaterial
+Package=/Engine/EngineMaterials/DefaultWhiteGrid
+Package=/Engine/EngineMaterials/EditorBrushMaterial
+Package=/Engine/EngineMaterials/EmissiveMeshMaterial
+Package=/Engine/EngineMaterials/Good64x64TilingNoiseHighFreq
+Package=/Engine/EngineMaterials/Grid
+Package=/Engine/EngineMaterials/Grid_N
+Package=/Engine/EngineMaterials/LandscapeHolePhysicalMaterial
+Package=/Engine/EngineMaterials/MiniFont
+Package=/Engine/EngineMaterials/PaperDiffuse
+Package=/Engine/EngineMaterials/PaperNormal
+Package=/Engine/EngineMaterials/PhysMat_Rubber
+Package=/Engine/EngineMaterials/PreintegratedSkinBRDF
+Package=/Engine/EngineMaterials/RemoveSurfaceMaterial
+Package=/Engine/EngineMaterials/WeightMapPlaceholderTexture

; Console platforms will remove EngineDebugMaterials from their StartupPackages
+Package=/Engine/EngineDebugMaterials/BoneWeightMaterial
+Package=/Engine/EngineDebugMaterials/DebugMeshMaterial
+Package=/Engine/EngineDebugMaterials/GeomMaterial
+Package=/Engine/EngineDebugMaterials/HeatmapGradient
+Package=/Engine/EngineDebugMaterials/LevelColorationLitMaterial
+Package=/Engine/EngineDebugMaterials/LevelColorationUnlitMaterial
+Package=/Engine/EngineDebugMaterials/MAT_LevelColorationLitLightmapUV
+Package=/Engine/EngineDebugMaterials/ShadedLevelColorationLitMaterial
+Package=/Engine/EngineDebugMaterials/ShadedLevelColorationUnlitMateri
+Package=/Engine/EngineDebugMaterials/TangentColorMap
+Package=/Engine/EngineDebugMaterials/VertexColorMaterial
+Package=/Engine/EngineDebugMaterials/VertexColorViewMode_AlphaAsColor
+Package=/Engine/EngineDebugMaterials/VertexColorViewMode_BlueOnly
+Package=/Engine/EngineDebugMaterials/VertexColorViewMode_ColorOnly
+Package=/Engine/EngineDebugMaterials/VertexColorViewMode_GreenOnly
+Package=/Engine/EngineDebugMaterials/VertexColorViewMode_RedOnly
+Package=/Engine/EngineDebugMaterials/WireframeMaterial

+Package=/Engine/EngineSounds/WhiteNoise

+Package=/Engine/EngineFonts/SmallFont
+Package=/Engine/EngineFonts/TinyFont
+Package=/Engine/EngineFonts/Roboto
+Package=/Engine/EngineFonts/RobotoTiny

; only needed for TextRender feature (3d Text in world)
+Package=/Engine/EngineMaterials/DefaultTextMaterialTranslucent
+Package=/Engine/EngineFonts/RobotoDistanceField

就算是工程中没有任何资源,也会默认把这些资源给打包进来。

UE4:创建Commandlet

在一个Editor的Module下创建下列文件和代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// .h
#pragma once

#include "Commandlets/Commandlet.h"
#include "HotPatcherPatcherCommandlet.generated.h"


UCLASS()
class UHotPatcherPatcherCommandlet :public UCommandlet
{
GENERATED_BODY()

public:

virtual int32 Main(const FString& Params)override;
};
// .cpp
#include "HotPatcherCookerCommandlet.h"

int32 UHotPatcherCookerCommandlet::Main(const FString& Params)
{
UE_LOG(LogTemp, Log, TEXT("UHotPatcherCookerCommandlet::Main"));
return 0;
}

然后在启动的时候就可以使用下列参数来运行Commandlet,并且可以给它传递参数:

1
UE4Editor.exe PROJECT_NAME.uproject -run=HotPatcherCooker  -aaa="D:\\AAA.json" -test1

UE4:UnLua如何实现函数覆写

概括来说:UnLua绑定了UE创建对象的事件,当创建CDO时会调用到UnLua的NotifyUObjectCreated,在其中拿到了该对象的UClass,对该对象的UClass中的UFUNCTION通过SetNativeFunc修改为CallLua函数,这样就实现了覆写UFUNCTION。

下面来具体分析一下实现。UnLua实现覆写完整的调用栈:

替换Thunk函数

在UnLua的FLuaContext的initialize函数中,将GLuaCxt注册到了GUObjectArray中:

1
2
3
4
5
6
// LuaContext.cpp
if (!bAddUObjectNotify)
{
GUObjectArray.AddUObjectCreateListener(GLuaCxt); // add listener for creating UObject
GUObjectArray.AddUObjectDeleteListener(GLuaCxt); // add listener for deleting UObject
}

FLuaContext继承自FUObjectArray::FUObjectCreateListenerFUObjectArray::FUObjectDeleteListener,所以当UE的对象系统创建对象的时候会把调用到FLuaContext的NotifyUObjectCreatedNotifyUObjectDeleted

当创建一个UObject的时候会在FObjectArrayAllocateUObjectIndex中对多有注册过的CreateListener调用NotifyUObjectDeleted函数。

而UnLua实现覆写UFUNCTION的逻辑就是写在NotifyUObjectCreated中的TryBindLua调用中,栈如下:

一个一个来说他们的作用:

FLuaContext::TryBindUnlua

1
2
// Try to bind Lua module for a UObject
bool FLuaContext::TryToBindLua(UObjectBaseUtility *Object);

主要作用是:如果创建的对象继承了UUnLuaInterface,具有GetModuleName函数,则通过传进来的UObject获取到它的UCclass,然后再通过UClass得到GetModuleName函数的UFunction,并通过CDO对象调用该UFunction,得到该CLass绑定的Lua模块名。

若没有静态绑定,则检查是否具有动态绑定。

UUnLuaManager::Bind

该函数定义在UnLua/pRIVATE/UnLuaManager.cpp文件中。

TryBindUnlua中得到了当前创建对象的UClass和绑定的模块名,传递到了Bind函数中,它主要做了几件事情:

  1. 注册Class到lua
  2. require对应的lua模块
  3. 调用UnLuaManager::BindInternal函数
  4. 为当前对象创建一个lua端对象并push上一个Initialize函数并调用

BindInternal

其中的关键函数为UnLuaManager::BindInternal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* Bind a Lua module for a UObject
*/
bool UUnLuaManager::BindInternal(UObjectBaseUtility *Object, UClass *Class, const FString &InModuleName, bool bNewCreated)
{
if (!Object || !Class)
{
return false;
}

lua_State *L = *GLuaCxt;
TStringConversion<TStringConvert<TCHAR, ANSICHAR>> ModuleName(*InModuleName);

if (!bNewCreated)
{
if (!BindSurvivalObject(L, Object, Class, ModuleName.Get())) // try to bind Lua module for survival UObject again...
{
return false;
}

FString *ModuleNamePtr = ModuleNames.Find(Class);
if (ModuleNamePtr)
{
return true;
}
}

ModuleNames.Add(Class, InModuleName);
Classes.Add(InModuleName, Class);

#if UE_BUILD_DEBUG
TSet<FName> *LuaFunctionsPtr = ModuleFunctions.Find(InModuleName);
check(!LuaFunctionsPtr);
TMap<FName, UFunction*> *UEFunctionsPtr = OverridableFunctions.Find(Class);
check(!UEFunctionsPtr);
#endif

TSet<FName> &LuaFunctions = ModuleFunctions.Add(InModuleName);
GetFunctionList(L, ModuleName.Get(), LuaFunctions); // get all functions defined in the Lua module
TMap<FName, UFunction*> &UEFunctions = OverridableFunctions.Add(Class);
GetOverridableFunctions(Class, UEFunctions); // get all overridable UFunctions

OverrideFunctions(LuaFunctions, UEFunctions, Class, bNewCreated); // try to override UFunctions

return ConditionalUpdateClass(Class, LuaFunctions, UEFunctions);
}

这个函数接受到的参数是创建出来的UObject,以及它的UClass,还有对应的Lua的模块名。

  1. 把对象的UClass与Lua的模块名对应添加到ModuleNamesClasses
  2. 从Lua端通过L获取所指定模块名中的所有函数
  3. 从UClass获取所有的BlueprintEvent、RepNotifyFunc函数
  4. 对两边获取的结果调用UUnLuaManager::OverrideFunctions执行替换

UUnLuaManager::OverrideFunctions

对从Lua端获取的函数使用名字在当前类的UFunction中查找,依次对其调用UUnLuaManager::OverrideFunction.

UUnLuaManager::OverrideFunction

  1. 判断传入的UFunction是不是属于传入的Outer UClasss
  2. 判断是否允许调用被覆写的函数
  3. 调用AddFunction函数

UUnLuaManager::AddFunction

  1. 如果函数为FUNC_Native则将FLuaInvoker::execCallLua和所覆写的函数名通过AddNativeFunction添加至UClass
  2. UFunction内的函数指针替换为(FNativeFuncPtr)&FLuaInvoker::execCallLua
  3. 如果开启了允许调用被覆写的函数,则把替换NativeFunc之前的UFunction对象存到GReflectionRegistry

Call lua

首先,需要说的一点是,当使用UEC++写的带有UFUNCTION并具有BlueprintNativeEvent或者BlueprintImplementableEvent标记的函数,UHT会给生成对应名字的函数:

1
2
3
4
5
6
UFUNCTION(BlueprintNativeEvent,BlueprintCallable)
bool TESTFUNC();
bool TESTFUNC_Implementation();

UFUNCTION(BlueprintImplementableEvent, meta = (DisplayName = "BeginPlay"))
bool TESTImplEvent(AActor* InActor,int32 InIval);

UHT生成的函数和传递的数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// generated.h
#define MicroEnd_423_Source_MicroEnd_423_Public_MyActor_h_13_EVENT_PARMS \
struct MyActor_eventReceiveBytes_Parms \
{ \
TArray<uint8> InData; \
}; \
struct MyActor_eventTESTFUNC_Parms \
{ \
bool ReturnValue; \
\
/** Constructor, initializes return property only **/ \
MyActor_eventTESTFUNC_Parms() \
: ReturnValue(false) \
{ \
} \
}; \
struct MyActor_eventTESTImplEvent_Parms \
{ \
AActor* InActor; \
int32 InIval; \
bool ReturnValue; \
\
/** Constructor, initializes return property only **/ \
MyActor_eventTESTImplEvent_Parms() \
: ReturnValue(false) \
{ \
} \
};

// gen.cpp
static FName NAME_AMyActor_TESTFUNC = FName(TEXT("TESTFUNC"));
bool AMyActor::TESTFUNC()
{
MyActor_eventTESTFUNC_Parms Parms;
ProcessEvent(FindFunctionChecked(NAME_AMyActor_TESTFUNC),&Parms);
return !!Parms.ReturnValue;
}
static FName NAME_AMyActor_TESTImplEvent = FName(TEXT("TESTImplEvent"));
bool AMyActor::TESTImplEvent(AActor* InActor, int32 InIval)
{
MyActor_eventTESTImplEvent_Parms Parms;
Parms.InActor=InActor;
Parms.InIval=InIval;
ProcessEvent(FindFunctionChecked(NAME_AMyActor_TESTImplEvent),&Parms);
return !!Parms.ReturnValue;
}

可以看到,UHT帮我们定义了同名函数,并将其转发给ProcessEvent

注意:这里通过FindFunctionChecked方法是调用的UObject::FindFunctionChecked

1
2
3
4
5
6
7
8
9
10
11
12
13
14
UFunction* UObject::FindFunction( FName InName ) const
{
return GetClass()->FindFunctionByName(InName);
}

UFunction* UObject::FindFunctionChecked( FName InName ) const
{
UFunction* Result = FindFunction(InName);
if (Result == NULL)
{
UE_LOG(LogScriptCore, Fatal, TEXT("Failed to find function %s in %s"), *InName.ToString(), *GetFullName());
}
return Result;
}

可以看到,这里传递给ProcessEventUFunction*就是从当前对象的UClass中得到的。

经过前面分分析可以知道,UnLua实现的函数覆写,就是把UClass中的UFunction中的原生thunk函数指针替换为FLuaInvoker::execCallLua,而且当一个对象的BlueprintNativeEventBlueprintImplementableEvent函数被调用的时候会调用到ProcessEvent并传入对应的UFunction*,在ProcessEvent中又调Invork(调用其中的原生指针),也就是实现调用到了unlua中替换绑定的FLuaInvoker::execCallLua,在这个函数中再转发给调用lua端的函数,从而实现了覆写函数的目的。

UE4:引擎对Android版本的支持

在之前的笔记里:Android SDK版本与Android的版本列出了Android系统版本和API Leve版本之间的对照表。

但是UE不同的引擎版本对Android的系统支持也是不一样的,在Project Setting-Android中的Minimum SDK Version中可以设置最小的SDK版本,也就是UE打包Android所支持的最低系统版本。

在UE4.25中,最低可以设置Level为19,即Android4.4,在4.25之前的引擎版本最低支持Level 9,也就是Android 2.3。

这部分的代码可以在Runtime/Android/AndroidRuntimeSettings/Classes/AndroidRuntimeSettings.h中查看,并对比不同引擎版本的区别。

UE4:引擎对AndroidNDK的要求

UE在打包Android的时候会要求系统中具有NDK环境,但是不同的引擎版本对NDK的版本要求也不一样。

当使用不支持的NDK版本时,打包会有如下错误:

1
2
UATHelper: Packaging (Android (ETC2)):   ERROR: Android toolchain NDK r14b not supported; please use NDK r21 to NDK r23 (NDK r21b recommended)
PackagingResults: Error: Android toolchain NDK r14b not supported; please use NDK r21 to NDK r23 (NDK r21b recommended)

提示当前系统中的NDK版本不支持,并会显示支持的版本。

UE打包时对NDK版本的检测是在UBT中执行的,具体文件为UnrealBuildTool/Platform/Android/AndroidToolChain.cs

其中定义了当前引擎版本支持的NDK的最低和最高版本:

1
2
3
4
// in ue 4.25
readonly int MinimumNDKToolchain = 210100;
readonly int MaximumNDKToolchain = 230100;
readonly int RecommendedNDKToolchain = 210200;

可以在Github上比较方便地查看不同引擎版本要求的NDK版本:UE_425_AndroidToolChain.cs

UE4:从TargetRules获取引擎版本

之前写到过在C++代码里,UE提供了几个宏可以获取引擎版本(UE版本号的宏定义),那么怎么在build.cs里检测引擎版本?

在UE4.19版本之前从UBT获取引擎版本比较麻烦:

1
2
3
4
5
6
7
BuildVersion Version;
if (BuildVersion.TryRead(BuildVersion.GetDefaultFileName(), out Version))
{
System.Console.WriteLine(Version.MajorVersion);
System.Console.WriteLine(Version.MinorVersion);
System.Console.WriteLine(Version.PatchVersion);
}

在UE4.19及以后的引擎版本,可以通过ReadOnlyTargetRules.Version来获得,它是ReadOnlyBuildVersion类型,包裹了一个BuildVersion类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// UnrealBuildTools/System/BuildVersion.cs
namespace UnrealBuildTool
{
/// <summary>
/// Holds information about the current engine version
/// </summary>
[Serializable]
public class BuildVersion
{
/// <summary>
/// The major engine version (4 for UE4)
/// </summary>
public int MajorVersion;

/// <summary>
/// The minor engine version
/// </summary>
public int MinorVersion;

/// <summary>
/// The hotfix/patch version
/// </summary>
public int PatchVersion;

/// <summary>
/// The changelist that the engine is being built from
/// </summary>
public int Changelist;

/// <summary>
/// The changelist that the engine maintains compatibility with
/// </summary>
public int CompatibleChangelist;

/// <summary>
/// Whether the changelist numbers are a licensee changelist
/// </summary>
public bool IsLicenseeVersion;

/// <summary>
/// Whether the current build is a promoted build, that is, built strictly from a clean sync of the given changelist
/// </summary>
public bool IsPromotedBuild;

/// <summary>
/// Name of the current branch, with '/' characters escaped as '+'
/// </summary>
public string BranchName;

/// <summary>
/// The current build id. This will be generated automatically whenever engine binaries change if not set in the default Engine/Build/Build.version.
/// </summary>
public string BuildId;

/// <summary>
/// The build version string
/// </summary>
public string BuildVersionString;

// ...
}
}

其中的MajorVersion/MinorVersion/PatchVersion分别对应X.XX.X。

UE4:FPaths中Dir函数的对应路径

FPaths提供了很多EngineDir等之类的函数,我在unlua里导出了这些符号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
print(fmt("EngineDir: {}",UE4.FPaths.EngineDir()))
print(fmt("EngineUserDir: {}",UE4.FPaths.EngineUserDir()))
print(fmt("EngineContentDir: {}",UE4.FPaths.EngineContentDir()))
print(fmt("EngineConfigDir: {}",UE4.FPaths.EngineConfigDir()))
print(fmt("EngineSavedDir: {}",UE4.FPaths.EngineSavedDir()))
print(fmt("EnginePluginsDir: {}",UE4.FPaths.EnginePluginsDir()))
print(fmt("RootDir: {}",UE4.FPaths.RootDir()))
print(fmt("ProjectDir: {}",UE4.FPaths.ProjectDir()))
print(fmt("ProjectUserDir: {}",UE4.FPaths.ProjectUserDir()))
print(fmt("ProjectContentDir: {}",UE4.FPaths.ProjectContentDir()))
print(fmt("ProjectConfigDir: {}",UE4.FPaths.ProjectConfigDir()))
print(fmt("ProjectSavedDir: {}",UE4.FPaths.ProjectSavedDir()))
print(fmt("ProjectIntermediateDir: {}",UE4.FPaths.ProjectIntermediateDir()))
print(fmt("ProjectPluginsDir: {}",UE4.FPaths.ProjectPluginsDir()))
print(fmt("ProjectLogDir: {}",UE4.FPaths.ProjectLogDir()))

他们对应的具体路径为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
EngineDir: ../../../Engine/
EngineUserDir: : /Users/imzlp/AppData/Local/UnrealEngine/4.22/
EngineContentDir: ../../../Engine/Content/
EngineConfigDir: ../../../Engine/Config/
EngineSavedDir: : /Users/imzlp/AppData/Local/UnrealEngine/4.22/Saved/
EnginePluginsDir: ../../../Engine/Plugins/
RootDir: : /Program Files/Epic Games/UE_4.22/
ProjectDir: ../../../../../../Users/imzlp/Documents/UnrealProjectSSD/GWorldClient/
ProjectUserDir: ../../../../../../Users/imzlp/Documents/UnrealProjectSSD/GWorldClient/
ProjectContentDir: ../../../../../../Users/imzlp/Documents/UnrealProjectSSD/GWorldClient/Content/
ProjectConfigDir: ../../../../../../Users/imzlp/Documents/UnrealProjectSSD/GWorldClient/Config/
ProjectSavedDir: ../../../../../../Users/imzlp/Documents/UnrealProjectSSD/GWorldClient/Saved/
ProjectIntermediateDir: ../../../../../../Users/imzlp/Documents/UnrealProjectSSD/GWorldClient/Intermediate/
ProjectPluginsDir: ../../../../../../Users/imzlp/Documents/UnrealProjectSSD/GWorldClient/Plugins/
ProjectLogDir: ../../../../../../Users/imzlp/Documents/UnrealProjectSSD/GWorldClient/Saved/Logs/

这些相对路径都是相对于引擎的exe的路径的:

UE4:通过Commandline替换加载的ini

如项目下的DefaultEngine.ini/DefaultGame.ini等。
去掉Defaultini后缀之后是它们的baseName,可以通过下列命令行来替换:

1
2
3
4
# engine
-EngineINI=REPLACE_INI_FILE_PAT.ini
# game
-GameINI=REPLACE_INI_FILE_PAT.ini

具体实现是在FConfigCacheIni::GetDestIniFilename中做的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Core/Private/Misc/ConfigCacheIni.cpp
FString FConfigCacheIni::GetDestIniFilename(const TCHAR* BaseIniName, const TCHAR* PlatformName, const TCHAR* GeneratedConfigDir)
{
// figure out what to look for on the commandline for an override
FString CommandLineSwitch = FString::Printf(TEXT("%sINI="), BaseIniName);

// if it's not found on the commandline, then generate it
FString IniFilename;
if (FParse::Value(FCommandLine::Get(), *CommandLineSwitch, IniFilename) == false)
{
FString Name(PlatformName ? PlatformName : ANSI_TO_TCHAR(FPlatformProperties::PlatformName()));

// if the BaseIniName doens't contain the config dir, put it all together
if (FCString::Stristr(BaseIniName, GeneratedConfigDir) != nullptr)
{
IniFilename = BaseIniName;
}
else
{
IniFilename = FString::Printf(TEXT("%s%s/%s.ini"), GeneratedConfigDir, *Name, BaseIniName);
}
}

// standardize it!
FPaths::MakeStandardFilename(IniFilename);
return IniFilename;
}

UE4:获取当前平台信息

可以使用FPlatformProperties来获取当前程序的平台信息。
同样是使用UE的跨平台库写法,FGenericPlatformProperties是定义在Core/Public/GenericPlatform/GenericPlatformProperties.h中的。

例:可以使用FPlatformProperties::PlatformName()运行时来获取当前平台的名字。

FPlatformPropertiestypedef是定义在Core/Public/HAL/PlatformProperties.h中。

UE4:COMPILED_PLATFORM_HEADER

在UE4.22之前,UE的跨平台库的实现方式都是创建一个泛型平台类:

1
2
3
4
struct FGenericPlatformUtils
{
static void GenericMethod(){}
};

然后每个平台实现:

1
2
3
4
5
6
7
8
// Windows/WindowsPlatformUtils.h
struct FWindowsPlatformUtils:public FGenericPlatformUtils
{
static void GenericMethod(){
//doSomething...
}
};
typedef FWindowsPlatformUtils FPlatformUtils;

在UE4.22之前,需要使用下面这种方法:

1
2
3
4
5
6
7
8
9
10
// PlatformUtils.h
#if PLATFORM_ANDROID
#include "Android/AndroidPlatformUtils.h"
#elif PLATFORM_IOS
#include "IOS/IOSPlatformUtils.h"
#elif PLATFORM_WINDOWS
#include "Windows/WindowsPlatformUtils.h"
#elif PLATFORM_MAC
#include "Mac/MacPlatformUtils.h"
#endif

需要手动判断每个平台再进行包含,也是比较麻烦的,在4.23之后,UE引入了一个宏:COMPILED_PLATFORM_HEADER,可以把上面的包含简化为下面的代码:

1
#include COMPILED_PLATFORM_HEADER(PlatformUtils.h)

它是定义在Runtime/Core/Public/HAL/PreprocessorHelpers.h下的宏:

1
2
3
4
5
6
7
#if PLATFORM_IS_EXTENSION
// Creates a string that can be used to include a header in the platform extension form "PlatformHeader.h", not like below form
#define COMPILED_PLATFORM_HEADER(Suffix) PREPROCESSOR_TO_STRING(PREPROCESSOR_JOIN(PLATFORM_HEADER_NAME, Suffix))
#else
// Creates a string that can be used to include a header in the form "Platform/PlatformHeader.h", like "Windows/WindowsPlatformFile.h"
#define COMPILED_PLATFORM_HEADER(Suffix) PREPROCESSOR_TO_STRING(PREPROCESSOR_JOIN(PLATFORM_HEADER_NAME/PLATFORM_HEADER_NAME, Suffix))
#endif

注释已经比较说明作用了。而且它还有兄弟宏:

1
2
3
4
5
6
7
#if PLATFORM_IS_EXTENSION
// Creates a string that can be used to include a header with the platform in its name, like "Pre/Fix/PlatformNameSuffix.h"
#define COMPILED_PLATFORM_HEADER_WITH_PREFIX(Prefix, Suffix) PREPROCESSOR_TO_STRING(Prefix/PREPROCESSOR_JOIN(PLATFORM_HEADER_NAME, Suffix))
#else
// Creates a string that can be used to include a header with the platform in its name, like "Pre/Fix/PlatformName/PlatformNameSuffix.h"
#define COMPILED_PLATFORM_HEADER_WITH_PREFIX(Prefix, Suffix) PREPROCESSOR_TO_STRING(Prefix/PLATFORM_HEADER_NAME/PREPROCESSOR_JOIN(PLATFORM_HEADER_NAME, Suffix))
#endif

命名有规律是多么重要的一件事…

UE4:遍历UCLASS或USTRUCT的反射成员

可以通过TFieldIterator来遍历:

1
2
3
4
for (TFieldIterator<UProperty> PropertyIt(ProxyClass); PropertyIt; ++PropertyIt)
{
// ...
}

注意:4.25之后没有UProperty,变成了FProperty.

Lua:Metatable

UE4:控制打包时ini的拷贝

DeploymentContext.cs中的DeploymentContext函数中,有以下两行代码:

1
2
3
// Read the list of files which are whitelisted to be staged
ReadConfigFileList(GameConfig, "Staging", "WhitelistConfigFiles", WhitelistConfigFiles);
ReadConfigFileList(GameConfig, "Staging", "BlacklistConfigFiles", BlacklistConfigFiles);

这两个数组会在CopyBuildToStageingDirectory.Automation.cs中使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/// <summary>
/// Determines if an individual config file should be staged
/// </summary>
/// <param name="SC">The staging context</param>
/// <param name="ConfigDir">Directory containing the config files</param>
/// <param name="ConfigFile">The config file to check</param>
/// <returns>True if the file should be staged, false otherwise</returns>
static Nullable<bool> ShouldStageConfigFile(DeploymentContext SC, DirectoryReference ConfigDir, FileReference ConfigFile)
{
StagedFileReference StagedConfigFile = SC.GetStagedFileLocation(ConfigFile);
if (SC.WhitelistConfigFiles.Contains(StagedConfigFile))
{
return true;
}
if (SC.BlacklistConfigFiles.Contains(StagedConfigFile))
{
return false;
}
// ...
}

用途就是指定哪些config会添加到包体中。
用法如下(写到DefaultGame.ini中):

1
2
[Staging]
+BlacklistConfigFiles=GWorldClient/Config/DefaultGameExtensionSettings.ini

UE4:CharCast的坑

CharCast是定义在StringConv.h的模板函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Casts one fixed-width char type into another.
*
* @param Ch The character to convert.
* @return The converted character.
*/
template <typename To, typename From>
FORCEINLINE To CharCast(From Ch)
{
To Result;
FPlatformString::Convert(&Result, 1, &Ch, 1, (To)UNICODE_BOGUS_CHAR_CODEPOINT);
return Result;
}

就是对FPlatformString::Convert的转发调用。

PS:UNICODE_BOGUS_CHAR_CODEPOINT 宏定义为'?'

FPlatformString::Convert有两个版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
* Converts the [Src, Src+SrcSize) string range from SourceChar to DestChar and writes it to the [Dest, Dest+DestSize) range.
* The Src range should contain a null terminator if a null terminator is required in the output.
* If the Dest range is not big enough to hold the converted output, NULL is returned. In this case, nothing should be assumed about the contents of Dest.
*
* @param Dest The start of the destination buffer.
* @param DestSize The size of the destination buffer.
* @param Src The start of the string to convert.
* @param SrcSize The number of Src elements to convert.
* @param BogusChar The char to use when the conversion process encounters a character it cannot convert.
* @return A pointer to one past the last-written element.
*/
template <typename SourceEncoding, typename DestEncoding>
static FORCEINLINE typename TEnableIf<
// This overload should be called when SourceEncoding and DestEncoding are 'compatible', i.e. they're the same type or equivalent (e.g. like UCS2CHAR and WIDECHAR are on Windows).
TAreEncodingsCompatible<SourceEncoding, DestEncoding>::Value,
DestEncoding*
>::Type Convert(DestEncoding* Dest, int32 DestSize, const SourceEncoding* Src, int32 SrcSize, DestEncoding BogusChar = (DestEncoding)'?')
{
if (DestSize < SrcSize)
return nullptr;

return (DestEncoding*)Memcpy(Dest, Src, SrcSize * sizeof(SourceEncoding)) + SrcSize;
}


template <typename SourceEncoding, typename DestEncoding>
static typename TEnableIf<
// This overload should be called when the types are not compatible but the source is fixed-width, e.g. ANSICHAR->WIDECHAR.
!TAreEncodingsCompatible<SourceEncoding, DestEncoding>::Value && TIsFixedWidthEncoding<SourceEncoding>::Value,
DestEncoding*
>::Type Convert(DestEncoding* Dest, int32 DestSize, const SourceEncoding* Src, int32 SrcSize, DestEncoding BogusChar = (DestEncoding)'?')
{
const int32 Size = DestSize <= SrcSize ? DestSize : SrcSize;
bool bInvalidChars = false;
for (int I = 0; I < Size; ++I)
{
SourceEncoding SrcCh = Src[I];
Dest[I] = (DestEncoding)SrcCh;
bInvalidChars |= !CanConvertChar<DestEncoding>(SrcCh);
}

if (bInvalidChars)
{
for (int I = 0; I < Size; ++I)
{
if (!CanConvertChar<DestEncoding>(Src[I]))
{
Dest[I] = BogusChar;
}
}

LogBogusChars<DestEncoding>(Src, Size);
}

return DestSize < SrcSize ? nullptr : Dest + Size;
}

其中关键的是第二个实现, 通过判断CanConvertChar来检测是否能够转换字符,如果不能转换就把转换结果设置为BogusChar,默认也就是?,这也是把不同编码的数据转换为FString有些会显示一堆?的原因。

1
2
3
4
5
6
7
8
9
10
11
/**
* Tests whether a particular character can be converted to the destination encoding.
*
* @param Ch The character to test.
* @return True if Ch can be encoded as a DestEncoding.
*/
template <typename DestEncoding, typename SourceEncoding>
static bool CanConvertChar(SourceEncoding Ch)
{
return IsValidChar(Ch) && (SourceEncoding)(DestEncoding)Ch == Ch && IsValidChar((DestEncoding)Ch);
}

所以:类似LoadFileToString去读文件如果编码不支持,那么读出来的数据和原始文件里是不一样的。

UE4:bUsesSlate

在UE的TargetRules中有一项属性bUsesSlate,可以用来控制是否启用Slate,UE文档里的描述如下:

Whether the project uses visual Slate UI (as opposed to the low level windowing/messaging, which is always available).

但是我想知道是否启用对于项目打出的包有什么区别。经过测试发现,以移动端为例,bUsesSlate的值并不会影响libUE4.so的大小。

有影响的地方只在于打包时的pak大小,这一点可以从两次分别打包的PakList*.txt中得知,经过对比发现若bUsesSlate=false,则在打包时不会把Engine\Content\Slate下的图片资源打包。我把两个版本的PakList*.txt都放在这里,有兴趣的可以看都是有哪些资源没有被打包。

下面这幅图是两个分别开启bUsesSlate的图(左侧false右侧true),可以看到只有main.obb.png的大小不一样。

可以看到默认情况下main.obb.png减小了大概6-7M,APK的大小也减小的差不多。

UE4:Unreal Plugin Language

在UE中为移动端添加第三方模块或者修改配置文件时经常会用到AdditionalPropertiesForReceipt,里面创建ReceiptProperty传入的xml文件就是UE的Unreal Plugin Language脚本。

ReceiptProperty的平台名称在IOS和Android上是固定的,分别是IOSPluginAndroidPlugin,不可以指定其他的名字(详见代码UEDeployIOS.cs#L1153UEDeployAndroid.cs#L4303)。

1
AdditionalPropertiesForReceipt.Add(new ReceiptProperty("AndroidPlugin", Path.Combine(ThirdPartyPath, "Android/PlatformUtils_UPL_Android.xml")));

UE4:打包时Paklist文件的生成

UE打出Pak时,需要一个txt的参数传入,里面记录着要打到pak里的文件信息,直接使用UE的打包改文件会存储在:

1
C:\Users\imzlp\AppData\Roaming\Unreal Engine\AutomationTool\Logs\D+UnrealEngine+Epic+UE_4.23\PakList_microend_423-ios.txt

类似的路径下。

这个文件生成的地方为:

1
D:\UnrealEngine\Epic\UE_4.24\Engine\Source\Programs\AutomationTool\BuildGraph\Tasks\PakFileTask.cs

在它的Execute函数里,有通过外部传入的PakFileTaskParameters的参数来把文件写入。

UE4:ShaderStableInfo*.scl.csv

在Cook的时候会在Cooked/PLATFORM/PROJECT_NAME/Metadata/PipelineCaches下生成类似下面这样的文件:

1
2
3
4
ShaderStableInfo-Global-PCD3D_SM4.scl.csv
ShaderStableInfo-Global-PCD3D_SM5.scl.csv
ShaderStableInfo-GWorld-PCD3D_SM4.scl.csv
ShaderStableInfo-GWorld-PCD3D_SM5.scl.csv

里面记录了FStableShaderKeyAndValue结构的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// RenderCore/Public/ShaderCodeLibrary.h
struct RENDERCORE_API FStableShaderKeyAndValue
{
FCompactFullName ClassNameAndObjectPath;
FName ShaderType;
FName ShaderClass;
FName MaterialDomain;
FName FeatureLevel;
FName QualityLevel;
FName TargetFrequency;
FName TargetPlatform;
FName VFType;
FName PermutationId;
FSHAHash PipelineHash;

uint32 KeyHash;
FSHAHash OutputHash;

FStableShaderKeyAndValue()
: KeyHash(0)
{
}
}

作用有时间再来分析。

PE的DLL为什么需要导入库?

在ELF中,共享库所有的全局函数和变量在默认情况下都可以被其他模块使用,也就是说ELF默认导出所有的全局符号。但是在DLL中不同,PE环境下需要显式地告诉编译器我们需要导出的符号,否则编译器就默认所有符号都不导出。

在MSVC中可以使用__declspec(dllexport)以及__declspec(dllimport)来分别表示导出本DLL的符号以及从别的DLL中导入符号。除了上面两个属性关键字还可以定义def文件来声明导入导出符号,def文件时连接器的链接脚本文件,可以当作链接器的输入文件,用于控制链接过程。

在我之前的一篇文章(动态链接库的使用:加载和链接)中写到过DLL导入库的创建和使用,但是为什么DLL需要导入库而so不需要呢?前面已经回答,因为ELF是默认全导出的,PE是默认不导出的,但是我想知道原因是什么。

其实在有了上面的两个属性关键字之后不使用导入库也可以实现符号的导入和导出。

  1. 当某个PE文件被加载时。Windows加载器的其中一个任务就是把所有需要导入的函数地址确定并且将导入表中的元素调整到正确的地址,以实现动态链接的过程,导入表中有IAT,其中的每个元素对应一个被导入的符号。
  2. 编译器无法知道一个符号是从外部导入的还是本模块中定义的,所以编译器是直接产生调用指令
1
CALL XXXXXXXXX
  1. __declspec出现之前,微软提供的方法就是使用导入库,在这种情况下,对于导入函数的调用并不区分是导入函数还是导出函数,它统一地产生直接调用的指令,但是链接器在链接时会将导入函数的目标地址导向一小段桩代码(stub),由这个桩代码再将控制权交给IAT中的真正目标。
  2. 所以导入库的作用就是将编译器产生的调用命令转发到导入表的IAT中目标地址。

UE4:UCLASS的config

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Implements the settings for the Paper2D plugin.
*/
UCLASS(config=Engine, defaultconfig)
class PAPER2D_API UPaperRuntimeSettings : public UObject
{
GENERATED_UCLASS_BODY()

// Enables experimental *incomplete and unsupported* texture atlas groups that sprites can be assigned to
UPROPERTY(EditAnywhere, config, Category=Experimental)
bool bEnableSpriteAtlasGroups;

// Enables experimental *incomplete and unsupported* 2D terrain spline editing. Note: You need to restart the editor when enabling this setting for the change to fully take effect.
UPROPERTY(EditAnywhere, config, Category=Experimental, meta=(ConfigRestartRequired=true))
bool bEnableTerrainSplineEditing;

// Enables automatic resizing of various sprite data that is authored in texture space if the source texture gets resized (sockets, the pivot, render and collision geometry, etc...)
UPROPERTY(EditAnywhere, config, Category=Settings)
bool bResizeSpriteDataToMatchTextures;
};

这个类是个config的类,可以从ini中读取配置,关键的地方就是UCLASS(Config=)的东西,一般情况下是Engine/Game/Editor,它们的ini文件都是Default*.ini,如上面这个类,如果想要自己在ini中来指定它们这些参数的值,则需要写到项目的Config/DefaultEngine.ini中:

1
2
[/Script/Paper2D.PaperRuntimeSettings]
bEnableSpriteAtlasGroups = true;

其中ini的Section为该配置类的PackagePath

UE4:操作剪贴板Clipboard

有些需求是要能够访问到用户的粘贴板,来进行复制、和粘贴的功能。

在UE中访问粘贴板的方法如下:

1
2
3
4
5
6
FString PasteString;
// 从剪贴板读取内容
FPlatformApplicationMisc::ClipboardPaste(PasteString);

// 把123456放入剪贴板
FPlatformApplicationMisc::ClipboardCopy(TEXT("123465"));

注意:FPlatformApplicationMisc是定义在ApplicationCore下的,使用时要包含该模块。

UE4:在场景中Copy/Paste的实现

在UE的场景编辑器中对一个选中的Actor进行Ctrl+C时把拷贝的内容粘贴到一个文本编辑器里可以看到类似以下的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Begin Map
Begin Level
Begin Actor Class=/Script/Engine.Pawn Name=Pawn_1 Archetype=/Script/Engine.Pawn'/Script/Engine.Default__Pawn'
Begin Object Class=/Script/Engine.SceneComponent Name="DefaultSceneRoot"
End Object
Begin Object Name="DefaultSceneRoot"
RelativeLocation=(X=600.000000,Y=280.000000,Z=150.000000)
bVisualizeComponent=True
CreationMethod=Instance
End Object
RootComponent=SceneComponent'"DefaultSceneRoot"'
ActorLabel="Pawn"
InstanceComponents(0)=SceneComponent'"DefaultSceneRoot"'
End Actor
End Level
Begin Surface
End Surface
End Map

它记录了当前拷贝的Actor的类,位置、以及与默认对象(CDO)不一致的属性。
拷贝上面的文本,在UE的场景编辑器里粘贴,会在场景里创建出来一个一摸一样的对象。

Copy

在场景编辑器中执行Ctrl+C会把文本拷贝到粘贴板的实现为UEditorEngine::CopySelectedActorsToClipboard函数,其定义在EditorServer.cpp中:

1
2
3
4
5
6
7
8
9
10
/**
* Copies selected actors to the clipboard. Supports copying actors from multiple levels.
* NOTE: Doesn't support copying prefab instance actors!
*
* @param InWorld World to get the selected actors from
* @param bShouldCut If true, deletes the selected actors after copying them to the clipboard
* @param bIsMove If true, this cut is part of a move and the actors will be immediately pasted
* @param bWarnAboutReferences Whether or not to show a modal warning about referenced actors that may no longer function after being moved
*/
void CopySelectedActorsToClipboard( UWorld* InWorld, const bool bShouldCut, const bool bIsMove = false, bool bWarnAboutReferences = true);

调用栈为:

之后又会调用UUnrealEngine::edactCopySelected函数(EditorActor.cpp),在edactCopySelected中通过构造出一个FExportObjectInnerContext的对象收集到所选择的对象:

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
/*-----------------------------------------------------------------------------
Actor adding/deleting functions.
-----------------------------------------------------------------------------*/

class FSelectedActorExportObjectInnerContext : public FExportObjectInnerContext
{
public:
FSelectedActorExportObjectInnerContext()
//call the empty version of the base class
: FExportObjectInnerContext(false)
{
// For each object . . .
for (UObject* InnerObj : TObjectRange<UObject>(RF_ClassDefaultObject, /** bIncludeDerivedClasses */ true, /** IternalExcludeFlags */ EInternalObjectFlags::PendingKill))
{
UObject* OuterObj = InnerObj->GetOuter();

//assume this is not part of a selected actor
bool bIsChildOfSelectedActor = false;

UObject* TestParent = OuterObj;
while (TestParent)
{
AActor* TestParentAsActor = Cast<AActor>(TestParent);
if (TestParentAsActor && TestParentAsActor->IsSelected())
{
bIsChildOfSelectedActor = true;
break;
}
TestParent = TestParent->GetOuter();
}

if (bIsChildOfSelectedActor)
{
InnerList* Inners = ObjectToInnerMap.Find(OuterObj);
if (Inners)
{
// Add object to existing inner list.
Inners->Add( InnerObj );
}
else
{
// Create a new inner list for the outer object.
InnerList& InnersForOuterObject = ObjectToInnerMap.Add(OuterObj, InnerList());
InnersForOuterObject.Add(InnerObj);
}
}
}
}
};

再通过UExporter::ExportToOutputDevice进行序列化操作,就得到了该对象序列化之后的字符串。

Paste

把文本拷贝之后在场景中粘贴会创建出Actor的核心实现为UEditorEngine::PasteSelectedActorFromClipboard函数,其定义在EditorServer.cpp中:

1
2
3
4
5
6
7
8
/**
* Pastes selected actors from the clipboard.
* NOTE: Doesn't support pasting prefab instance actors!
*
* @param InWorld World to get the selected actors from
* @param PasteTo Where to paste the content too
*/
void PasteSelectedActorsFromClipboard( UWorld* InWorld, const FText& TransDescription, const EPasteTo PasteTo );

调用栈为:

检测字符数组是UTF8还是GBK编码

基于上个笔记的需求,所以要能够区分一个字符数组是使用UTF8还是GBK编码的。

GBK

gbk 的第一字节是高位为 1 的,第 2 字节可能高位为 0 。这种情况一定是 gbk ,因为 UTF8 对 >127 的编码一定每个字节高位为 1 。

UTF8

UTF8 是兼容 ascii 的,所以 0~127 就和 ascii 完全一致了。

UTF8的中文文字一定编码成三个字节:

汉字以及汉字标点(包括日文汉字等),在 UTF8 中一定被编码成:1110**** 10****** 10******

如上个笔记中的字,其UTF8的编码为11101001 1011100 10100001,符合上面的规则。

其他

相关资料:

工具:

UE4:UTF8编码的字符数组转FString

从网络收过来的数据流是以字节形式接收的,但是对于不同使用UTF8或者GBK编码的字符来说,他们是由多个字节组成的,如这个汉字的UTF8编码为0xE9B8A1

1
2
char Chicken[] = { (char)0xE9,(char)0xB8,(char)0xA1,`\0`};
FString ChickenChnese(UTF8_TO_TCHAR(Name));

因为Chicken这个数据有四个字节,前三个字节是这个汉字的UTF8编码,最后一个字节是\0表示结束符。

之前想的是把这个数组再表示为UTF8的字符,但是这里混淆了一个概念:这个数组本身就是UTF8编码的信息了,所以应该是把它从UTF8转换为TCHAR表示的字符,要使用UE的UTF8_TO_CHAR

因为UTF8兼容ASCII编码,所以可以混用:

1
2
3
ANSICHAR TestArray[] = { 'a','b','c', (char)0xE9,(char)0xB8,(char)0xA1,'d','e','1','\0' };
// abc鸡de1
FString TestStr(UTF8_TO_TCHAR(TestArray));

UE4:编辑器SpawnActor

可以使用UEditorEngine中的SpawnActor函数。

1
2
// Editor/Private/EditorEngine.cpp
AActor* UEditorEngine::AddActor(ULevel* InLevel, UClass* Class, const FTransform& Transform, bool bSilent, EObjectFlags InObjectFlags)

使用这个方法Spawn出来的会自动选中。

虚拟制片的流程

最近想业余研究一下虚拟制片的工作流程,准备研究一下弄个个人版的方案玩玩,收集一些资料。

硬件要求:

  • Valve的定位基站(HTC Vive)
  • Vive Tracker一个
  • 相机+视频采集卡/网络摄像头
  • 绿幕

软件要求:

  • Unreal Engine
  • SteamVR
  • OBS

额外注意事项:

  • 拍摄时应该要和引擎内的帧率同步(高级点的相机
  • 拍摄时人物不应离绿幕太近会有反光的问题

UE4:EditCondition支持表达式

在UPROPERTY里可以对一个属性被设置的条件,比如某个bool开启时才允许编辑:

1
2
3
4
URPOPERTY()
bool EnableInput;
UPROPERTY(meta=(EditCondition="EnableInput"))
EInputMode InputMode;

也可以对其使用取反操作:

1
2
3
4
URPOPERTY()
bool EnableInput;
UPROPERTY(meta=(EditCondition="!EnableInput"))
EInputMode InputMode;

UE的文档介绍里说EditContion是支持表达式的:

The EditCondition meta tag is no longer limited to a single boolean property. It is now evaluated using a full-fledged expression parser, meaning you can include a full C++ expression.

UE4:获取资源依赖关系

最近有个需求要获取UE里资源的引用关系,类似UE的Reference Viewer的操作,既然知道了Reference Viewer中有想要的那么就去它的模块里面翻代码:

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
// Engine\Plugins\Editor\AssetManagerEditor\Source\AssetManagerEditor\Private\ReferenceViewer\EdGraph_ReferenceViewer.cpp
UEdGraphNode_Reference* UEdGraph_ReferenceViewer::RecursivelyConstructNodes(bool bReferencers, UEdGraphNode_Reference* RootNode, const TArray<FAssetIdentifier>& Identifiers, const FIntPoint& NodeLoc, const TMap<FAssetIdentifier, int32>& NodeSizes, const TMap<FName, FAssetData>& PackagesToAssetDataMap, const TSet<FName>& AllowedPackageNames, int32 CurrentDepth, TSet<FAssetIdentifier>& VisitedNames)
{
// ...
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
TArray<FAssetIdentifier> ReferenceNames;
TArray<FAssetIdentifier> HardReferenceNames;
if ( bReferencers )
{
for (const FAssetIdentifier& AssetId : Identifiers)
{
AssetRegistryModule.Get().GetReferencers(AssetId, HardReferenceNames, GetReferenceSearchFlags(true));
AssetRegistryModule.Get().GetReferencers(AssetId, ReferenceNames, GetReferenceSearchFlags(false));
}
}
else
{
for (const FAssetIdentifier& AssetId : Identifiers)
{
AssetRegistryModule.Get().GetDependencies(AssetId, HardReferenceNames, GetReferenceSearchFlags(true));
AssetRegistryModule.Get().GetDependencies(AssetId, ReferenceNames, GetReferenceSearchFlags(false));
}
}
// ...
}

通过FAssetRegistryModule模块去拿就可以了,FAssetIdentifier中只需要有PackageName即可,这个PackageName是LongPackageName,不是PackagePath

UE4:地图的存储和加载

存储栈:

加载栈:

UE4:DataTable

要创建一个可以用于创建DataTable的结构需要继承于FTableRowBase,如果要在编辑器中可编辑,该结构中的UPROPERTY不要包含Category

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataTable.h"
#include "EnemyProperty.generated.h"
USTRUCT(BlueprintType)
struct FRoleProperty : public FTableRowBase
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 AttackValue;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 Defense;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 HP;
};

UE4:FCommandLine过滤模式

FCommandLine是UE封装的启动参数的管理类,在Launch模块下的FEngineLoop::PreInit中被初始化(FCommandLine::Set)为程序启动的CmdLine
FCommandLine支持AppendParser这是比较常用的功能,但是今天要说的是另外一个:CommandLine的白名单和黑名单模式。
考虑这样的需求:在游戏开发阶段,有很多参数可以在启动时配置,方便测试,但是在发行时需要把启动时从命令行读取配置的功能给去掉,强制使用我们设置的默认参数。

OVERRIDE_COMMANDLINE_WHITELIST

怎么才是最简单的办法?其实这一点根本不需要自己去处理这部分的内容,因为FCommandLine支持白名单模式。
开启的方法为在target.cs中增加WANTS_COMMANDLINE_WHITELIST宏:

1
GlobalDefinitions.Add("WANTS_COMMANDLINE_WHITELIST=1");

如果只开启这个,则默认情况下不允许接收任何外部传入的参数,但具有默认的参数-fullscreen /windowed
当然,我们可以自己指定这个WHITLIST,那么就是在target.cs中使用OVERRIDE_COMMANDLINE_WHITELIST宏:

1
GlobalDefinitions.Add("OVERRIDE_COMMANDLINE_WHITELIST=\"-arg1 -arg2 -arg3 -arg4\"");

这样就只有我们指定的这些参数才可以在运行时接收,防止玩家恶意传递参数导致游戏出错。
这部分的代码是写在Misc/CommandLine.cpp中的。

Example:

1
2
3
// target.cs
GlobalDefinitions.Add("WANTS_COMMANDLINE_WHITELIST=1");
GlobalDefinitions.Add("OVERRIDE_COMMANDLINE_WHITELIST=\"-e -c\"");

这里只指定了-e/-c两个参数,如果程序在启动时被指定了其他的参数,如:

1
D:/UnrealEngine/EngineSource/UE_4.21_Source/Engine/Binaries/Win64/UE4Launcher.exe -e -c -d

FCommandLine::Get()只能得到-e-c-d和exe路径都被丢弃了,只能用在指定开关,不能用来传递值。

FILTER_COMMANDLINE_LOGGING

还可以在target.cs中指定FILTER_COMMANDLINE_LOGGING来控制对FCommandLine::LoggingCmdLine的过滤:

1
GlobalDefinitions.Add("FILTER_COMMANDLINE_LOGGING=\"-arg1 -arg2 -arg3 -arg4\"");

它会在程序从命令行中接收的参数中过滤所指定的参数,类似于参数的黑名单。

Example:

1
2
GlobalDefinitions.Add("WANTS_COMMANDLINE_WHITELIST=1");
GlobalDefinitions.Add("FILTER_COMMANDLINE_LOGGING=\"-e -c\"");

在运行时传入参数:

1
D:/UnrealEngine/EngineSource/UE_4.21_Source/Engine/Binaries/Win64/UE4Launcher.exe -e -c -d

得到的是:

1
-D:/UnrealEngine/EngineSource/UE_4.21_Source/Engine/Binaries/Win64/UE4Launcher.exe -d

-e-c被过滤掉了。

UE4:PIE的坑

UE在PIE中运行是和Standalone模式是不同的,在PIE运行时可以通过监听FEditorDelegates下的PreBeginPIE/BeginPIE以及PrePIEEnded/EndPIE等代理来检测PIE模式下的游戏运行和退出。
但是,PIE的PrePIEEndedEndPIE都是先于GameInstanceShutdown函数的,在一些情况下,如果PIEEnd中做了一些清理操作而在GameInstance的Shutdown函数中有使用的话会有问题。
解决办法为绑定FGameDelegatesEndPlayMapDelegate

1
FGameDelegates::Get().GetEndPlayMapDelegate().AddRaw(GLuaCxt, &FLuaContext::OnEndPlayMap);

UE4:UPARAM(ref)

UE中使用TArray<bool>&是作为返回值的,在蓝图中就是在节点右侧。

1
2
UFUNCTION(BlueprintCallable)
static void ModifySomeArray(TArray<bool> &BooleanArray);

如果想要传入已有的对象,则需要UPARAM(ref)

1
2
UFUNCTION(BlueprintCallable)
static void ModifySomeArray(UPARAM(ref) TArray<bool> &BooleanArray);

UE4:the file couldn’t be loaded by the OS.

启动引擎时如果具有类似的Crash信息:

1
ModuleManager: Unable to load module 'G:/UE_4.22/Engine/Binaries/Win64/UE4Editor-MeshUtilities.dll' because the file couldn't be loaded by the OS.

把引擎的DDC删掉之后重启引擎即可。DDC的目录:

1
2
C:\Program Files\Epic Games\UE_4.22\Engine\DerivedDataCache
C:\Users\imzlp\AppData\Local\UnrealEngine\4.22\DerivedDataCache

把上面两个目录都删掉。
如果启动项目时提示的是工程中的模块,则把工程和插件下的BinariesIntermediate都删了重新编译生成。

UE4:PURE_VIRTUAL macro

在UE的代码中看到一些虚函数使用UE的PURE_VIRTUAL宏来指定,类似下面这种代码:

1
virtual void GameInitialize() PURE_VIRTUAL(IINetGameInstance::GameInitialize,);

看起来有点奇怪,看一下它的代码:

1
2
3
4
5
#if CHECK_PUREVIRTUALS
#define PURE_VIRTUAL(func,extra) =0;
#else
#define PURE_VIRTUAL(func,extra) { LowLevelFatalError(TEXT("Pure virtual not implemented (%s)"), TEXT(#func)); extra }
#endif

相当于给了纯虚函数一个默认实现,在错误调用时能够看到信息。

使用tcping检测端口是否开放

可以使用tcping这个工具:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ tcping 127.0.0.1 10086
# 端口联通的情况
Probing 127.0.0.1:10086/tcp - Port is open - time=10.775ms
Probing 127.0.0.1:10086/tcp - Port is open - time=0.428ms
Probing 127.0.0.1:10086/tcp - Port is open - time=0.344ms
Probing 127.0.0.1:10086/tcp - Port is open - time=0.317ms

Ping statistics for 127.0.0.1:10086
4 probes sent.
4 successful, 0 failed. (0.00% fail)
Approximate trip times in milli-seconds:
Minimum = 0.317ms, Maximum = 10.775ms, Average = 2.966ms

# 未开放端口的情况
Probing 127.0.0.1:10086/tcp - No response - time=2002.595ms
Probing 127.0.0.1:10086/tcp - No response - time=2000.168ms
Probing 127.0.0.1:10086/tcp - No response - time=2000.202ms
Probing 127.0.0.1:10086/tcp - No response - time=2001.308ms

Ping statistics for 127.0.0.1:10086
4 probes sent.
0 successful, 4 failed. (100.00% fail)
Was unable to connect, cannot provide trip statistics

UE4:成员模板函数特化不要放在class scope

如下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Template
{
public:
template<typename T>
Template& operator>>(T& Value)
{
return *this;
}

template<>
Template& operator>><bool>(bool& Value)
{
// do something
return *this;
}
};

声明了模板函数operator>>,而且添加了一个bool的特化,这个代码在Windows下编译没有问题,但是在打包Android时会产生错误:

1
error: explicit specialization of 'operator>>' in class scope

说时显式特化写在了类作用域内,解决办法是把特化的版本放到类之外:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Template
{
public:
template<typename T>
Template& operator>>(T& Value)
{
return *this;
}
};
template<>
Template& Template::operator>><bool>(bool& Value)
{
// do something
return *this;
}

UE4:Runtime模块包含Editor模块的错误

如果打包时有下列错误:

1
2
UnrealBuildTool.Main: ERROR: Missing precompiled manifest for 'EditorWidgets'. This module was most likely not flagged for being included in a precompiled build - set 'PrecompileForTargets = PrecompileTargetsType.Any;' in EditorWidgets.build.cs to override.
UnrealBuildTool.Main: BuildException: Missing precompiled manifest for 'EditorWidgets'. This module was most likely not flagged for being included in a precompiled build - set 'PrecompileForTargets = PrecompileTargetsType.Any;' in EditorWidgets.build.cs to override.

我这里的错误提示是EditorWidgets是个预编译模块。
这个错误的原因是在Runtime的模块中添加了UnrealEd模块,因为它是属于Editor的,打包时不会把Editor的模块打包进来,所以就会有现在错误。

注意:一定不要在Runtime的模块中包含Editor或者Developer的模块,这个是Epic的EULA限制,如果需要用到Editor或者Developer的东西,则自己在插件或者工程下新建一个Editor或者Developer才行。

UE4:获取蓝图添加的所有接口

拿到UBlueprint之后可以通过获取ImplementedInterfaces来访问:

1
2
3
4
5
6
7
8
for (const auto& InterfaceItem : Blueprint->ImplementedInterfaces)
{
if (InterfaceItem.Interface.Get()->IsChildOf(UUnLuaInterface::StaticClass()))
{
bImplUnLuaInterface = true;
break;
}
}

UE4:UnrealFrontEnd DeviceLog

之前在PC上看移动端的Log的方式是是用Logcat,但是发现UE其实提供了工具,就是UnrealFrontLog中的DeviceLog

最下面的那一行也可以执行控制台命令,很方便。

PS:我测试了在PC上连接Android设备没有问题,但是连上IOS不显示,在Mac上连接IOS没有问题,不过不显示Log,但可以执行命令。

UE4:获取某个类的所有子类

查找所有继承自某个UClass的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include "UObject/UObjectIterator.h"
TArray<UClass*> UNetGameInstance::GetAllSubsystem()
{
TArray<UClass*> result;
for (TObjectIterator<UClass> It; It; ++It)
{
if (It->IsChildOf(UNetSubsystemBase::StaticClass()) && !It->HasAnyClassFlags(CLASS_Abstract))
{
result.Add(*It);
}
}
return result;
}

UE4:4.22.3打包IOS Crash的问题

在下列环境下:

  1. macOS Mojave 10.14.5
  2. XCode 11.2.1(11B500)
  3. UE4.22.3
  4. iPhone 7 IOS13.3.1

在这个环境下打包出来的IOS在运行时会Crash,但是换到4.23.1就没有这个问题。
Crash的Log:GWorld 2020-3-12 6-58-PM.crash

UE4:IOS贴图非2次幂的显示问题

默认情况下在iOS里使用非2次幂大小的贴图会有下列问题:

提示的是See Power of Two Settings in Texture Editor
这是因为使用到的贴图的大小不是2的次幂。

解决办法:
在编辑器中打开Texture,将Texture-Power Of Two Mode改成Pad to power of two,这样会填充贴图为2次幂大小。

如果不想要使用Power Of Two Mode也可以修改Compression-Compression SettingsUSerInterface2D,但是非2次幂的贴图大小会有性能问题(在iPhone6(A7处理器)中只能设置为填充才可以)。

UE4:最多75根骨骼的限制

因为移动平台缺少32位的索引支持,所以最多支持65k个顶点和75根骨骼。
但是可以通过拆分骨骼模型的材质来实现,每个材质支持75根,这是单次drawcall的限制,分成不同的批次就可以了。

PS:不能用uniform了,换其他方式,比如VTF,也可以实现超过75根骨骼。

UE4:使用HTTP请求下载文件的坑和技巧

使用HTTP可以请求下载文件,response的结果就是文件的内容。
在下载一个文件之前可以先使用HEAD请求来只获取头,可以从Content-Length头获取到文件的大小。

1
2
3
4
5
6
7
// head request
TSharedRef<IHttpRequest> HttpHeadRequest = FHttpModule::Get().CreateRequest();
HttpHeadRequest->OnHeaderReceived().BindUObject(this, &UDownloadProxy::OnRequestHeadHeaderReceived);
HttpHeadRequest->OnProcessRequestComplete().BindUObject(this, &UDownloadProxy::OnRequestHeadComplete);
HttpHeadRequest->SetURL(InternalDownloadFileInfo.URL);
HttpHeadRequest->SetVerb(TEXT("HEAD"));
HttpHeadRequest->ProcessRequest();

在UE中需要通过监听HTTP请求的OnHeaderReceived派发来获得想要的头数据:

1
2
3
4
5
6
7
void UDownloadProxy::OnRequestHeadHeaderReceived(FHttpRequestPtr RequestPtr, const FString& InHeaderName, const FString& InNewHeaderValue)
{
if (InHeaderName.Equals(TEXT("Content-Length")))
{
InternalDownloadFileInfo.Size = UKismetStringLibrary::Conv_StringToInt(InNewHeaderValue);
}
}

之后就可以用Get方法来请求文件了:

1
2
3
4
5
6
7
8
9
// get request
TSharedRef<IHttpRequest> HttpRequest = FHttpModule::Get().CreateRequest();
HttpRequest->OnRequestProgress().BindUObject(this, &UDownloadProxy::OnDownloadProcess, bIsSlice?EDownloadType::Slice:EDownloadType::Start);
HttpRequest->OnProcessRequestComplete().BindUObject(this, &UDownloadProxy::OnDownloadComplete);
HttpRequest->SetURL(InternalDownloadFileInfo.URL);
HttpRequest->SetVerb(TEXT("GET"));
RangeArgs = TEXT("bytes=0-")+FString::FromInt(FileTotalByte);
HttpRequest->SetHeader(TEXT("Range"), RangeArgs);
HttpRequest->ProcessRequest();

其中Range头的格式为:Byte=0-指请求整个文件的大小,Byte=0-99则是请求前100byte,注意请求的范围不要超过文件大小,不然会有400错误。
通过控制HTTP请求的Range头,我们可以指定下载文件的任意部分,可以实现暂停继续/分片下载。

在UE中使用HTTP请求一个大文件的时候,如果该请求没有结束就去拿response的结果一定要注意一个问题:那就是Response的Content数据Payload是一个TArray动态数组,当Content的内容不断地被写入,会导致容器的Reserve也就是内存重新分配,获取该数组的内存地址是非常危险的。

所以建议在HTTP请求时先对response的Content的Payload进行Reserve使其能够容纳足够数量的数据,缺点就是会一次性占用整个文件的内存。
解决内存占用的办法就是通过Http请求的Range来实现分片下载(也就是把一个大文件分成数个小块,一块一块地下载),从而降低内存占用,

当下载文件后,通常还有进行文件校验的操作,等文件下载完之后再执行校验(如MD5计算)时间会很长,所以要解决校验的时间问题,想过开一个线程去计算,但是开线程只解决了不阻塞主线程,不会加速MD5的计算过程,后来想到MD5是摘要计算,进而联想到可不可以边下边进行MD5计算,根据没有全新的轮子定理(我瞎掰的),我查到了OpenSSL中的MD5实现支持使用MD5_Update来增量计算,所以这个问题就迎刃而解了,具体看我前面的笔记MD5的分片校验

基于上面这些内容,可以实现一个简陋的下载器功能了,可作为游戏中的下载组件,虽然看似简单,但是设计一个合理的结构和没有bug的版本还是要花点功夫的。
我把上面介绍的内容写成了一个插件:

HTTP的分片请求在服务端的Log:

资料文档:

MD5的分片校验

有一个需求:对下载的文件执行MD5运算。但是当文件比较大的时候如果等文件下载完再执行校验耗时会很长。

所以我想有没有办法边下边进行MD5的计算,因为知道MD5是基于摘要的,所以觉得边下边校验的方法应该可行。我查了一下相关的实现,找到OpenSSL中有相关的操作:

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
// openssl/md5.h
#ifndef HEADER_MD5_H
#define HEADER_MD5_H

#include <openssl/e_os2.h>
#include <stddef.h>

#ifdef __cplusplus
extern "C" {
#endif

#ifdef OPENSSL_NO_MD5
#error MD5 is disabled.
#endif

/*
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
* ! MD5_LONG has to be at least 32 bits wide. If it's wider, then !
* ! MD5_LONG_LOG2 has to be defined along. !
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
*/

#if defined(__LP32__)
#define MD5_LONG unsigned long
#elif defined(OPENSSL_SYS_CRAY) || defined(__ILP64__)
#define MD5_LONG unsigned long
#define MD5_LONG_LOG2 3
/*
* _CRAY note. I could declare short, but I have no idea what impact
* does it have on performance on none-T3E machines. I could declare
* int, but at least on C90 sizeof(int) can be chosen at compile time.
* So I've chosen long...
* <[email protected]>
*/
#else
#define MD5_LONG unsigned int
#endif

#define MD5_CBLOCK 64
#define MD5_LBLOCK (MD5_CBLOCK/4)
#define MD5_DIGEST_LENGTH 16

typedef struct MD5state_st
{
MD5_LONG A,B,C,D;
MD5_LONG Nl,Nh;
MD5_LONG data[MD5_LBLOCK];
unsigned int num;
} MD5_CTX;

#ifdef OPENSSL_FIPS
int private_MD5_Init(MD5_CTX *c);
#endif
int MD5_Init(MD5_CTX *c);
int MD5_Update(MD5_CTX *c, const void *data, size_t len);
int MD5_Final(unsigned char *md, MD5_CTX *c);
unsigned char *MD5(const unsigned char *d, size_t n, unsigned char *md);
void MD5_Transform(MD5_CTX *c, const unsigned char *b);
#ifdef __cplusplus
}
#endif

#endif

其中提供的MD5计算可以分开操作的有三个函数:

1
2
3
int MD5_Init(MD5_CTX *c);
int MD5_Update(MD5_CTX *c, const void *data, size_t len);
int MD5_Final(unsigned char *md, MD5_CTX *c);

其中的MD5_Update就是我们需要的函数。

所以使用的伪代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
MD5_CTX Md5CTX;

void Request()
{
MD5_Init(&Md5CTX);
}
void TickRequestProgress(char* InData,uint32 InLength)
{
MD5_Update(&Md5CTX,InData,InLength);
}
void RequestCompleted()
{
unsigned char digest[16] = { 0 };
MD5_Final(digest, &Md5CTX);
char md5string[33];
for (int i = 0; i < 16; ++i)
std::sprintf(&md5string[i * 2], "%02x", (unsigned int)digest[i]);

// result
pritf("MD5:%s",md5string);
}

这样当文件下载完,MD5计算就完成了。

注:在UE4中(~4.24)提供的OpenSSL在Win下只支持到VS2015,可以自己把这个限制给去掉(VS2015的链接库在VS2017中使用也没有问题)。

UE4:PlatformMisc的跨平台实现

当我们使用FPaths::ProjectDir()的是否考虑过它是怎么实现在不同的平台上为不同的路径的呢?
首先看一下FPaths::ProjectDir()的代码:

1
2
3
4
5
6
// Runtime/Core/Private/Misc/Paths.cpp
FString FPaths::ProjectDir()
{
return FString(FPlatformMisc::ProjectDir());
}

可以看到它是FPlatformMisc的一层转发,继续深入代码去找FPlatformMisc::ProjectDir的实现,如果用VS的Go to definition可以看到一堆的文件里都有FPlatformMisc的定义:

FPlatformMisc只是一个类型定义(typedef),通过平台宏来判断,当编译为不同的平台是会把FPlatformMisc通过typedef为目标平台的类。
进行平台判断的代码在Runtime/Core/Public/HAL/PlatformMisc.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
// Runtime/Core/Public/HAL/PlatformMisc.h
// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.
#pragma once

#include "CoreTypes.h"
#include "GenericPlatform/GenericPlatformMisc.h"

#if PLATFORM_WINDOWS
#include "Windows/WindowsPlatformMisc.h"
#elif PLATFORM_PS4
#include "PS4/PS4Misc.h"
#elif PLATFORM_XBOXONE
#include "XboxOne/XboxOneMisc.h"
#elif PLATFORM_MAC
#include "Mac/MacPlatformMisc.h"
#elif PLATFORM_IOS
#include "IOS/IOSPlatformMisc.h"
#elif PLATFORM_LUMIN
#include "Lumin/LuminPlatformMisc.h"
#elif PLATFORM_ANDROID
#include "Android/AndroidMisc.h"
#elif PLATFORM_HTML5
#include "HTML5/HTML5PlatformMisc.h"
#elif PLATFORM_QUAIL
#include "Quail/QuailPlatformMisc.h"
#elif PLATFORM_LINUX
#include "Linux/LinuxPlatformMisc.h"
#elif PLATFORM_SWITCH
#include "Switch/SwitchPlatformMisc.h"
#endif

其中的每一个平台的*PlatformMisc.h文件中都有FPlatformMisc的类型定义(typedef),各个平台代码文件都是存放在Runtime/Core/Public下,每个平台有自己的目录。
而且,所有的*PlatformMisc类都继承自一个FGenericPlatformMisc的类,作为通用平台的接口,如WindowsPlatformMisc

1
2
3
4
5
/**
* Windows implementation of the misc OS functions
**/
struct CORE_API FWindowsPlatformMisc
: public FGenericPlatformMisc{/*.....*/}

FGenericPlatformMisc中声明了我们常用的ProjectDir函数,供各个平台来独立实现,这样在通过FPlatformMisc来调用的时候就是所编译的目标平台的实现,这是UE实现跨平台代码的思路。

UE4:为APK添加外部存储独写权限

Project Settings-Platform-Android-Advanced APK Packaging-Extra Permissions下添加:

1
2
android.permission.WRITE EXTERNAL STORAGE
android.permission.READ_EXTERNAL_STORAGE

UE4:Android写入文件

当调用FFileHelper::SaveArrayToFile时:

1
FFileHelper::SaveArrayToFile(TArrayView<const uint8>(data, delta), *path, &IFileManager::Get(), EFileWrite::FILEWRITE_Append));

在该函数内部会创建一个FArchive的对象来管理当前文件,其内部具有一个IFileHandle的对象Handle,在Android平台上是FFileHandleAndroid

FArchive中写入文件调用的是Serialize,它又会调用HandleWrite

1
2
3
4
bool FArchiveFileWriterGeneric::WriteLowLevel( const uint8* Src, int64 CountToWrite )
{
return Handle->Write( Src, CountToWrite );
}

Android的Write的实现为:

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
// Runtime/Core/Private/Android/AndroidFile.h
virtual bool Write(const uint8* Source, int64 BytesToWrite) override
{
CheckValid();
if (nullptr != File->Asset)
{
// Can't write to assets.
return false;
}

bool bSuccess = true;
while (BytesToWrite)
{
check(BytesToWrite >= 0);
int64 ThisSize = FMath::Min<int64>(READWRITE_SIZE, BytesToWrite);
check(Source);
if (__pwrite(File->Handle, Source, ThisSize, CurrentOffset) != ThisSize)
{
bSuccess = false;
break;
}
CurrentOffset += ThisSize;
Source += ThisSize;
BytesToWrite -= ThisSize;
}

// Update the cached file length
Length = FMath::Max(Length, CurrentOffset);

return bSuccess;
}

可以看到是每次1M往文件里存的。

Android写入文件错误码对照

根据 error code number 查找 error string.

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
// bionic_errdefs.h
#ifndef __BIONIC_ERRDEF
#error "__BIONIC_ERRDEF must be defined before including this file"
#endif
__BIONIC_ERRDEF( 0 , 0, "Success" )
__BIONIC_ERRDEF( EPERM , 1, "Operation not permitted" )
__BIONIC_ERRDEF( ENOENT , 2, "No such file or directory" )
__BIONIC_ERRDEF( ESRCH , 3, "No such process" )
__BIONIC_ERRDEF( EINTR , 4, "Interrupted system call" )
__BIONIC_ERRDEF( EIO , 5, "I/O error" )
__BIONIC_ERRDEF( ENXIO , 6, "No such device or address" )
__BIONIC_ERRDEF( E2BIG , 7, "Argument list too long" )
__BIONIC_ERRDEF( ENOEXEC , 8, "Exec format error" )
__BIONIC_ERRDEF( EBADF , 9, "Bad file descriptor" )
__BIONIC_ERRDEF( ECHILD , 10, "No child processes" )
__BIONIC_ERRDEF( EAGAIN , 11, "Try again" )
__BIONIC_ERRDEF( ENOMEM , 12, "Out of memory" )
__BIONIC_ERRDEF( EACCES , 13, "Permission denied" )
__BIONIC_ERRDEF( EFAULT , 14, "Bad address" )
__BIONIC_ERRDEF( ENOTBLK , 15, "Block device required" )
__BIONIC_ERRDEF( EBUSY , 16, "Device or resource busy" )
__BIONIC_ERRDEF( EEXIST , 17, "File exists" )
__BIONIC_ERRDEF( EXDEV , 18, "Cross-device link" )
__BIONIC_ERRDEF( ENODEV , 19, "No such device" )
__BIONIC_ERRDEF( ENOTDIR , 20, "Not a directory" )
__BIONIC_ERRDEF( EISDIR , 21, "Is a directory" )
__BIONIC_ERRDEF( EINVAL , 22, "Invalid argument" )
__BIONIC_ERRDEF( ENFILE , 23, "File table overflow" )
__BIONIC_ERRDEF( EMFILE , 24, "Too many open files" )
__BIONIC_ERRDEF( ENOTTY , 25, "Not a typewriter" )
__BIONIC_ERRDEF( ETXTBSY , 26, "Text file busy" )
__BIONIC_ERRDEF( EFBIG , 27, "File too large" )
__BIONIC_ERRDEF( ENOSPC , 28, "No space left on device" )
__BIONIC_ERRDEF( ESPIPE , 29, "Illegal seek" )
__BIONIC_ERRDEF( EROFS , 30, "Read-only file system" )
__BIONIC_ERRDEF( EMLINK , 31, "Too many links" )
__BIONIC_ERRDEF( EPIPE , 32, "Broken pipe" )
__BIONIC_ERRDEF( EDOM , 33, "Math argument out of domain of func" )
__BIONIC_ERRDEF( ERANGE , 34, "Math result not representable" )
__BIONIC_ERRDEF( EDEADLK , 35, "Resource deadlock would occur" )
__BIONIC_ERRDEF( ENAMETOOLONG , 36, "File name too long" )
__BIONIC_ERRDEF( ENOLCK , 37, "No record locks available" )
__BIONIC_ERRDEF( ENOSYS , 38, "Function not implemented" )
__BIONIC_ERRDEF( ENOTEMPTY , 39, "Directory not empty" )
__BIONIC_ERRDEF( ELOOP , 40, "Too many symbolic links encountered" )
__BIONIC_ERRDEF( ENOMSG , 42, "No message of desired type" )
__BIONIC_ERRDEF( EIDRM , 43, "Identifier removed" )
__BIONIC_ERRDEF( ECHRNG , 44, "Channel number out of range" )
__BIONIC_ERRDEF( EL2NSYNC , 45, "Level 2 not synchronized" )
__BIONIC_ERRDEF( EL3HLT , 46, "Level 3 halted" )
__BIONIC_ERRDEF( EL3RST , 47, "Level 3 reset" )
__BIONIC_ERRDEF( ELNRNG , 48, "Link number out of range" )
__BIONIC_ERRDEF( EUNATCH , 49, "Protocol driver not attached" )
__BIONIC_ERRDEF( ENOCSI , 50, "No CSI structure available" )
__BIONIC_ERRDEF( EL2HLT , 51, "Level 2 halted" )
__BIONIC_ERRDEF( EBADE , 52, "Invalid exchange" )
__BIONIC_ERRDEF( EBADR , 53, "Invalid request descriptor" )
__BIONIC_ERRDEF( EXFULL , 54, "Exchange full" )
__BIONIC_ERRDEF( ENOANO , 55, "No anode" )
__BIONIC_ERRDEF( EBADRQC , 56, "Invalid request code" )
__BIONIC_ERRDEF( EBADSLT , 57, "Invalid slot" )
__BIONIC_ERRDEF( EBFONT , 59, "Bad font file format" )
__BIONIC_ERRDEF( ENOSTR , 60, "Device not a stream" )
__BIONIC_ERRDEF( ENODATA , 61, "No data available" )
__BIONIC_ERRDEF( ETIME , 62, "Timer expired" )
__BIONIC_ERRDEF( ENOSR , 63, "Out of streams resources" )
__BIONIC_ERRDEF( ENONET , 64, "Machine is not on the network" )
__BIONIC_ERRDEF( ENOPKG , 65, "Package not installed" )
__BIONIC_ERRDEF( EREMOTE , 66, "Object is remote" )
__BIONIC_ERRDEF( ENOLINK , 67, "Link has been severed" )
__BIONIC_ERRDEF( EADV , 68, "Advertise error" )
__BIONIC_ERRDEF( ESRMNT , 69, "Srmount error" )
__BIONIC_ERRDEF( ECOMM , 70, "Communication error on send" )
__BIONIC_ERRDEF( EPROTO , 71, "Protocol error" )
__BIONIC_ERRDEF( EMULTIHOP , 72, "Multihop attempted" )
__BIONIC_ERRDEF( EDOTDOT , 73, "RFS specific error" )
__BIONIC_ERRDEF( EBADMSG , 74, "Not a data message" )
__BIONIC_ERRDEF( EOVERFLOW , 75, "Value too large for defined data type" )
__BIONIC_ERRDEF( ENOTUNIQ , 76, "Name not unique on network" )
__BIONIC_ERRDEF( EBADFD , 77, "File descriptor in bad state" )
__BIONIC_ERRDEF( EREMCHG , 78, "Remote address changed" )
__BIONIC_ERRDEF( ELIBACC , 79, "Can not access a needed shared library" )
__BIONIC_ERRDEF( ELIBBAD , 80, "Accessing a corrupted shared library" )
__BIONIC_ERRDEF( ELIBSCN , 81, ".lib section in a.out corrupted" )
__BIONIC_ERRDEF( ELIBMAX , 82, "Attempting to link in too many shared libraries" )
__BIONIC_ERRDEF( ELIBEXEC , 83, "Cannot exec a shared library directly" )
__BIONIC_ERRDEF( EILSEQ , 84, "Illegal byte sequence" )
__BIONIC_ERRDEF( ERESTART , 85, "Interrupted system call should be restarted" )
__BIONIC_ERRDEF( ESTRPIPE , 86, "Streams pipe error" )
__BIONIC_ERRDEF( EUSERS , 87, "Too many users" )
__BIONIC_ERRDEF( ENOTSOCK , 88, "Socket operation on non-socket" )
__BIONIC_ERRDEF( EDESTADDRREQ , 89, "Destination address required" )
__BIONIC_ERRDEF( EMSGSIZE , 90, "Message too long" )
__BIONIC_ERRDEF( EPROTOTYPE , 91, "Protocol wrong type for socket" )
__BIONIC_ERRDEF( ENOPROTOOPT , 92, "Protocol not available" )
__BIONIC_ERRDEF( EPROTONOSUPPORT, 93, "Protocol not supported" )
__BIONIC_ERRDEF( ESOCKTNOSUPPORT, 94, "Socket type not supported" )
__BIONIC_ERRDEF( EOPNOTSUPP , 95, "Operation not supported on transport endpoint" )
__BIONIC_ERRDEF( EPFNOSUPPORT , 96, "Protocol family not supported" )
__BIONIC_ERRDEF( EAFNOSUPPORT , 97, "Address family not supported by protocol" )
__BIONIC_ERRDEF( EADDRINUSE , 98, "Address already in use" )
__BIONIC_ERRDEF( EADDRNOTAVAIL , 99, "Cannot assign requested address" )
__BIONIC_ERRDEF( ENETDOWN , 100, "Network is down" )
__BIONIC_ERRDEF( ENETUNREACH , 101, "Network is unreachable" )
__BIONIC_ERRDEF( ENETRESET , 102, "Network dropped connection because of reset" )
__BIONIC_ERRDEF( ECONNABORTED , 103, "Software caused connection abort" )
__BIONIC_ERRDEF( ECONNRESET , 104, "Connection reset by peer" )
__BIONIC_ERRDEF( ENOBUFS , 105, "No buffer space available" )
__BIONIC_ERRDEF( EISCONN , 106, "Transport endpoint is already connected" )
__BIONIC_ERRDEF( ENOTCONN , 107, "Transport endpoint is not connected" )
__BIONIC_ERRDEF( ESHUTDOWN , 108, "Cannot send after transport endpoint shutdown" )
__BIONIC_ERRDEF( ETOOMANYREFS , 109, "Too many references: cannot splice" )
__BIONIC_ERRDEF( ETIMEDOUT , 110, "Connection timed out" )
__BIONIC_ERRDEF( ECONNREFUSED , 111, "Connection refused" )
__BIONIC_ERRDEF( EHOSTDOWN , 112, "Host is down" )
__BIONIC_ERRDEF( EHOSTUNREACH , 113, "No route to host" )
__BIONIC_ERRDEF( EALREADY , 114, "Operation already in progress" )
__BIONIC_ERRDEF( EINPROGRESS , 115, "Operation now in progress" )
__BIONIC_ERRDEF( ESTALE , 116, "Stale NFS file handle" )
__BIONIC_ERRDEF( EUCLEAN , 117, "Structure needs cleaning" )
__BIONIC_ERRDEF( ENOTNAM , 118, "Not a XENIX named type file" )
__BIONIC_ERRDEF( ENAVAIL , 119, "No XENIX semaphores available" )
__BIONIC_ERRDEF( EISNAM , 120, "Is a named type file" )
__BIONIC_ERRDEF( EREMOTEIO , 121, "Remote I/O error" )
__BIONIC_ERRDEF( EDQUOT , 122, "Quota exceeded" )
__BIONIC_ERRDEF( ENOMEDIUM , 123, "No medium found" )
__BIONIC_ERRDEF( EMEDIUMTYPE , 124, "Wrong medium type" )
__BIONIC_ERRDEF( ECANCELED , 125, "Operation Canceled" )
__BIONIC_ERRDEF( ENOKEY , 126, "Required key not available" )
__BIONIC_ERRDEF( EKEYEXPIRED , 127, "Key has expired" )
__BIONIC_ERRDEF( EKEYREVOKED , 128, "Key has been revoked" )
__BIONIC_ERRDEF( EKEYREJECTED , 129, "Key was rejected by service" )
__BIONIC_ERRDEF( EOWNERDEAD , 130, "Owner died" )
__BIONIC_ERRDEF( ENOTRECOVERABLE, 131, "State not recoverable" )

#undef __BIONIC_ERRDEF

Lua:从内存加载module

从内存执行:

1
2
3
4
5
6
int FLuaPanda::OpenLuaPanda(lua_State* L)
{
luaL_dostring(L, (const char*)LuaPanda_lua_data);
return 1;
}

添加到PRELOAD中:

1
2
3
4
luaL_getsubtable(L, LUA_REGISTRYINDEX, LUA_PRELOAD_TABLE);
lua_pushcfunction(L, &FLuaPanda::OpenLuaPanda);
lua_setfield(L, -2, "LuaPanda");
lua_pop(L, 1);

直接添加到LOADED中:

1
luaL_requiref(L, "LuaPanda", &FLuaPanda::OpenLuaPanda,1);

UE4:gituhb快捷代码搜索

可以把ue的官方仓库的搜索创建为Chrome的自定义搜索引擎:

1
https://github.com/EpicGames/UnrealEngine/search?q=%s&unscoped_q=%s

这样在chrome的地址栏就可以通过uesource来触发搜索。

UE4:控制写入文件FLAG

FFileHelper::SaveStringToFile的最后一个参数可以传入一个Flag,用来控制文件的写入规则:

1
2
3
4
5
6
7
8
static bool SaveStringToFile
(
const FString & String,
const TCHAR * Filename,
EEncodingOptions EncodingOptions,
IFileManager * FileManager,
uint32 WriteFlags
)

WriteFlags可用值为:

1
2
3
4
5
6
7
8
9
10
enum EFileWrite
{
FILEWRITE_None = 0x00,
FILEWRITE_NoFail = 0x01,
FILEWRITE_NoReplaceExisting = 0x02,
FILEWRITE_EvenIfReadOnly = 0x04,
FILEWRITE_Append = 0x08,
FILEWRITE_AllowRead = 0x10,
FILEWRITE_Silent = 0x20
};

它们被定义在Engine/Source/Runtime/Core/Public/HAL/FileManager.h,因为它们的值是支持位运算的,所以它们的使用方法为:

1
2
3
4
5
6
#include "FileHelper.h"
#include "Paths.h"

FString FilePath = FPaths::ConvertRelativePathToFull(FPaths::GameSavedDir()) + TEXT("/MessageLog.txt");
FString FileContent = TEXT("This is a line of text to put in the file.\n");
FFileHelper::SaveStringToFile(FileContent, *FilePath, FFileHelper::EEncodingOptions::AutoDetect, &IFileManager::Get(), EFileWrite::FILEWRITE_Append | EFileWrite::FILEWRITE_AllowRead | EFileWrite::FILEWRITE_EvenIfReadOnly);

UE4:Assertion failed: Class->Children == 0

这是UBT里产生的错误,原因是项目内有两个同名的类。

1
2
3
4
5
6
7
8
1>  Running UnrealHeaderTool "C:\Users\Administrator\Documents\Unreal Projects\GWorldSlg\GWorld.uproject" "C:\Users\Administrator\Documents\Unreal Projects\GWorldSlg\Intermediate\Build\Win64\GWorldEditor\Development\GWorldEditor.uhtmanifest" -LogCmds="loginit warning, logexit warning, logdatabase error" -Unattended -WarningsAsErrors -installed
1>C:/Users/Administrator/Documents/Unreal Projects/GWorldSlg/Source/GWorld/Public/Modules/CoreEntity/Instance/TouchControlledPawnTrace.h(20) : LogWindows: Error: === Critical error: ===
1>C:/Users/Administrator/Documents/Unreal Projects/GWorldSlg/Source/GWorld/Public/Modules/CoreEntity/Instance/TouchControlledPawnTrace.h(20) : LogWindows: Error:
1>C:/Users/Administrator/Documents/Unreal Projects/GWorldSlg/Source/GWorld/Public/Modules/CoreEntity/Instance/TouchControlledPawnTrace.h(20) : LogWindows: Error: Assertion failed: Class->Children == 0 [File:D:\Build\++UE4\Sync\Engine\Source\Programs\UnrealHeaderTool\Private\HeaderParser.cpp] [Line: 5758]
1>C:/Users/Administrator/Documents/Unreal Projects/GWorldSlg/Source/GWorld/Public/Modules/CoreEntity/Instance/TouchControlledPawnTrace.h(20) : LogWindows: Error:
1>C:/Users/Administrator/Documents/Unreal Projects/GWorldSlg/Source/GWorld/Public/Modules/CoreEntity/Instance/TouchControlledPawnTrace.h(20) : LogWindows: Error:
1>C:/Users/Administrator/Documents/Unreal Projects/GWorldSlg/Source/GWorld/Public/Modules/CoreEntity/Instance/TouchControlledPawnTrace.h(20) : LogWindows: Error:
1>C:/Users/Administrator/Documents/Unreal Projects/GWorldSlg/Source/GWorld/Public/Modules/CoreEntity/Instance/TouchControlledPawnTrace.h(20) : LogWindows: Error:

UE4:打包时添加外部文件

Project Settings-Project-Packaging-Addtional Non-Asset Directories to Package

注意:添加的目录必须要位于项目的Content下。

Mobile422/Content/Script/目录下的文件,在pak中的mount point为:

1
2
3
4
LogPakFile: Display: "Mobile422/Content/Script/.vscode/launch.json" offset: 80875520, size: 1173 bytes, sha1: 47DE6617E94EE86597148CCD53FC76E1E1A3EE22, compression: None.
LogPakFile: Display: "Mobile422/Content/Script/Cube_Blueprint_C.lua" offset: 80877568, size: 888 bytes, sha1: 00F529AE4206E38D322E2983478AFBC2999A036E, compression: None.
LogPakFile: Display: "Mobile422/Content/Script/UnLua.lua" offset: 80879616, size: 1977 bytes, sha1: 4015051A6663684CA3FBE1D60003CA62CD27A8AD, compression: None.
LogPakFile: Display: "Mobile422/Content/Script/UnLuaPerformanceTestProxy.lua" offset: 80881664, size: 6087 bytes, sha1: 6662D86BEF54610414C00E43CF2C0F514DDF7434, compression: None.

C++关键字绝对不要作为变量名

集成了一个库,其中有下列代码:

1
2
3
4
5
6
7
8
9
#define BLOCKSIZE 64
static inline void
xor_key(uint8_t key[BLOCKSIZE], uint32_t xor) {
int i;
for (i=0;i<BLOCKSIZE;i+=sizeof(uint32_t)) {
uint32_t * k = (uint32_t *)&key[i];
*k ^= xor;
}
}

编译时候发现有这样的报错:

1
2
3
4
5
6
7
8
9
10
11
inlineblock.cpp:9:42: error: blocks support disabled - compile with -fblocks or pick a deployment target that supports them
xor_key(uint8_t key[BLOCKSIZE], uint32_t xor) {
^
inlineblock.cpp:9:45: error: block pointer to non-function type is invalid
xor_key(uint8_t key[BLOCKSIZE], uint32_t xor) {
^
inlineblock.cpp:13:12: error: type name requires a specifier or qualifier
*k ^= xor;
^
inlineblock.cpp:13:12: error: expected expression
4 errors generated.

这个错误的原因就是函数参数xor^关键字

UE4:Android NDK

UE4支持r14b-r18b的Android NDK,但是我在UE4.22.3中设置r18b被引擎识别为r18c:

1
2
3
4
5
6
7
8
9
10
UATHelper: Packaging (Android (ETC2)):   Using 'git status' to determine working set for adaptive non-unity build (C:\Users\imzlp\Documents\Unreal Projects\GWorldClient).
UATHelper: Packaging (Android (ETC2)): ERROR: Android toolchain NDK r18c not supported; please use NDK r14b to NDK r18b (NDK r14b recommended)
PackagingResults: Error: Android toolchain NDK r18c not supported; please use NDK r14b to NDK r18b (NDK r14b recommended)
UATHelper: Packaging (Android (ETC2)): Took 7.4575476s to run UnrealBuildTool.exe, ExitCode=5
UATHelper: Packaging (Android (ETC2)): ERROR: UnrealBuildTool failed. See log for more details. (C:\Users\imzlp\AppData\Roaming\Unreal Engine\AutomationTool\Logs\C+Program+Files+Epic+Games+UE_4.22\UBT-GWorld-Android-Development.txt)
UATHelper: Packaging (Android (ETC2)): (see C:\Users\imzlp\AppData\Roaming\Unreal Engine\AutomationTool\Logs\C+Program+Files+Epic+Games+UE_4.22\Log.txt for full exception trace)
PackagingResults: Error: UnrealBuildTool failed. See log for more details. (C:\Users\imzlp\AppData\Roaming\Unreal Engine\AutomationTool\Logs\C+Program+Files+Epic+Games+UE_4.22\UBT-GWorld-Android-Development.txt)
UATHelper: Packaging (Android (ETC2)): AutomationTool exiting with ExitCode=5 (5)
UATHelper: Packaging (Android (ETC2)): BUILD FAILED
PackagingResults: Error: Unknown Error

之所以要换NDK的版本是因为不同的NDK版本所包含的编译器对C++11标准支持度不同。

NDK clang version
r14b clang 3.8.275480 (based on LLVM 3.8.275480)
r17c clang version 6.0.2
r18b clang version 7.0.2
r20b clang version 8.0.7

UE4:将BindAction暴露给蓝图

UInputComponent函数中的BindAction是个模板函数,可以在代码中使用,但是在蓝图中就很不方便了。

本来想着直接裹一个函数库的实现将BindAction暴露给蓝图,但是在BindAction需要传入的函数代理那里在蓝图里传递很不方便,看了一下BindAction的代码,写了下面这个函数,可以通过传递函数名来绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// .h
UCLASS()
class GWORLD_API UFlibInputEventHelper : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable,Category = "GWorld|FLib|InputHelper",meta=(AutoCreateRefTerm="InActionName,InFuncName"))
static void BindInputAction(UInputComponent* InInputComp, const FName& InActionName, EInputEvent InKeyEvent, UObject* InCallObject, const FName& InFuncName);
};

// .cpp
void UFlibInputEventHelper::BindInputAction(UInputComponent* InInputComp, const FName& InActionName, EInputEvent InKeyEvent, UObject* InCallObject, const FName& InFuncName)
{
FInputActionBinding AB(InActionName, InKeyEvent);
AB.ActionDelegate.BindDelegate(InCallObject, InFuncName);
InInputComp->AddActionBinding(MoveTemp(AB));
}

UE4:APK包中OBB文件

在选择把data文件打包APK之后,把打包出的APK解包,是可以看到obb文件的,在assets文件夹下有main.obb.webp,其就是Saved/StagedBuilds/目录下的PROJECT_NAME.obb文件,HASH值都是一样的。

其实OBB文件中存储的就是我们的pak文件,使用7z之类的压缩软件可以打开未加密的obb查看,可以看到它的目录结构就是PROJECT_NAME/Content/Paks/PROJECTNAME-Android_ETC2.pak这样的形式。

UE4:移动端的启动参数

UE4打出来的包可以使用XXXX.exe -Params等方式来传递给游戏参数,但是移动平台(IOS/Android)打包出来的怎么传递参数呢?

Android启动参数

看了一下引擎里的代码,在Launch模块下Launch\Private\Android\LaunchAndroid.cpp中有InitCommandLine函数:

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
// Launch\Private\Android\LaunchAndroid.cpp
static void InitCommandLine()
{
static const uint32 CMD_LINE_MAX = 16384u;

// initialize the command line to an empty string
FCommandLine::Set(TEXT(""));

AAssetManager* AssetMgr = AndroidThunkCpp_GetAssetManager();
AAsset* asset = AAssetManager_open(AssetMgr, TCHAR_TO_UTF8(TEXT("UE4CommandLine.txt")), AASSET_MODE_BUFFER);
if (nullptr != asset)
{
const void* FileContents = AAsset_getBuffer(asset);
int32 FileLength = AAsset_getLength(asset);

char CommandLine[CMD_LINE_MAX];
FileLength = (FileLength < CMD_LINE_MAX - 1) ? FileLength : CMD_LINE_MAX - 1;
memcpy(CommandLine, FileContents, FileLength);
CommandLine[FileLength] = '\0';

AAsset_close(asset);

// chop off trailing spaces
while (*CommandLine && isspace(CommandLine[strlen(CommandLine) - 1]))
{
CommandLine[strlen(CommandLine) - 1] = 0;
}

FCommandLine::Append(UTF8_TO_TCHAR(CommandLine));
FPlatformMisc::LowLevelOutputDebugStringf(TEXT("APK Commandline: %s"), FCommandLine::Get());
}

// read in the command line text file from the sdcard if it exists
FString CommandLineFilePath = GFilePathBase + FString("/UE4Game/") + (!FApp::IsProjectNameEmpty() ? FApp::GetProjectName() : FPlatformProcess::ExecutableName()) + FString("/UE4CommandLine.txt");
FILE* CommandLineFile = fopen(TCHAR_TO_UTF8(*CommandLineFilePath), "r");
if(CommandLineFile == NULL)
{
// if that failed, try the lowercase version
CommandLineFilePath = CommandLineFilePath.Replace(TEXT("UE4CommandLine.txt"), TEXT("ue4commandline.txt"));
CommandLineFile = fopen(TCHAR_TO_UTF8(*CommandLineFilePath), "r");
}

if(CommandLineFile)
{
char CommandLine[CMD_LINE_MAX];
fgets(CommandLine, ARRAY_COUNT(CommandLine) - 1, CommandLineFile);

fclose(CommandLineFile);

// chop off trailing spaces
while (*CommandLine && isspace(CommandLine[strlen(CommandLine) - 1]))
{
CommandLine[strlen(CommandLine) - 1] = 0;
}

// initialize the command line to an empty string
FCommandLine::Set(TEXT(""));

FCommandLine::Append(UTF8_TO_TCHAR(CommandLine));
FPlatformMisc::LowLevelOutputDebugStringf(TEXT("Override Commandline: %s"), FCommandLine::Get());
}

#if !UE_BUILD_SHIPPING
if (FString* ConfigRulesCmdLineAppend = FAndroidMisc::GetConfigRulesVariable(TEXT("cmdline")))
{
FCommandLine::Append(**ConfigRulesCmdLineAppend);
FPlatformMisc::LowLevelOutputDebugStringf(TEXT("ConfigRules appended: %s"), **ConfigRulesCmdLineAppend);
}
#endif
}

简单来说就是在UE4Game/ProjectName/ue4commandline.txt中把启动参数写到里面,引擎启动的时候会从这个文件去读,然后添加到FCommandLine中。

IOS启动参数

与Android的做法相同,IOS的参数传递是在main函数中调用FIOSCommandLineHelper::InitCommandArgs(FString());,不过路径和Android不一样:

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
static void InitCommandArgs(FString AdditionalCommandArgs)
{
// initialize the commandline
FCommandLine::Set(TEXT(""));

FString CommandLineFilePath = FString([[NSBundle mainBundle] bundlePath]) + TEXT("/ue4commandline.txt");

// read in the command line text file (coming from UnrealFrontend) if it exists
FILE* CommandLineFile = fopen(TCHAR_TO_UTF8(*CommandLineFilePath), "r");
if(CommandLineFile)
{
FPlatformMisc::LowLevelOutputDebugStringf(TEXT("Found ue4commandline.txt file") LINE_TERMINATOR);

char CommandLine[CMD_LINE_MAX] = {0};
char* DataExists = fgets(CommandLine, ARRAY_COUNT(CommandLine) - 1, CommandLineFile);
if (DataExists)
{
// chop off trailing spaces
while (*CommandLine && isspace(CommandLine[strlen(CommandLine) - 1]))
{
CommandLine[strlen(CommandLine) - 1] = 0;
}

FCommandLine::Append(UTF8_TO_TCHAR(CommandLine));
}
}
else
{
FPlatformMisc::LowLevelOutputDebugStringf(TEXT("No ue4commandline.txt [%s] found") LINE_TERMINATOR, *CommandLineFilePath);
}

if (!AdditionalCommandArgs.IsEmpty() && !FChar::IsWhitespace(AdditionalCommandArgs[0]))
{
FCommandLine::Append(TEXT(" "));
}
FCommandLine::Append(*AdditionalCommandArgs);

// now merge the GSavedCommandLine with the rest
FCommandLine::Append(*GSavedCommandLine);

FPlatformMisc::LowLevelOutputDebugStringf(TEXT("Combined iOS Commandline: %s") LINE_TERMINATOR, FCommandLine::Get());
}

关键就是[[NSBundle mainBundle] bundlePath]这一句,它获取的是App的包路径,所以把UE4CommandLine.txt放到包路径下就可以了。

UE编译环境的VS安装配置

保存为.vsconfig然后使用Visual Studio Installer导入配置安装即可:

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
{
"version": "1.0",
"components": [
"Microsoft.VisualStudio.Workload.NativeDesktop",
"Microsoft.VisualStudio.Workload.Python",
"Microsoft.VisualStudio.Workload.Node",
"Microsoft.VisualStudio.Workload.NativeGame",
"Microsoft.VisualStudio.Workload.NativeCrossPlat",
"microsoft.visualstudio.component.debugger.justintime",
"microsoft.net.component.4.6.2.sdk",
"microsoft.net.component.4.6.2.targetingpack",
"microsoft.net.component.4.7.sdk",
"microsoft.net.component.4.7.targetingpack",
"microsoft.net.component.4.7.1.sdk",
"microsoft.net.component.4.7.1.targetingpack",
"microsoft.net.component.4.7.2.sdk",
"microsoft.net.component.4.7.2.targetingpack",
"microsoft.visualstudio.component.vc.diagnostictools",
"microsoft.visualstudio.component.vc.cmake.project",
"microsoft.visualstudio.component.vc.atl",
"microsoft.visualstudio.component.vc.testadapterforboosttest",
"microsoft.visualstudio.component.vc.testadapterforgoogletest",
"microsoft.visualstudio.component.winxp",
"microsoft.visualstudio.component.vc.cli.support",
"microsoft.visualstudio.component.vc.modules.x86.x64",
"component.incredibuild",
"microsoft.component.netfx.core.runtime",
"microsoft.component.cookiecuttertools",
"microsoft.component.pythontools.web",
"microsoft.visualstudio.component.classdesigner",
"microsoft.net.component.3.5.developertools",
"component.unreal.android",
"component.linux.cmake",
"microsoft.component.helpviewer",
"microsoft.visualstudio.component.vc.clangc2",
"microsoft.visualstudio.component.vc.tools.14.14"
]
}

下载地址:vs_installer.vsconfig

UE4:扫描资源引用时需注意Redirector

当我们在UE的资源管理器中进行rename/move等操作时,会产生一个与更名之前名字一样的Redirector

在使用UAssetManager::GetAssets进行资源的扫描时也会查询到这些redirector,但是他们不是真正的资源,在处理时需要过滤掉它们。
FAssetData中有一个成员函数IsRedirector可以用来判断扫描到的FAssetData是不是重定向器。

但是良好的项目规范是每进行delete/rename/move之后都手动在编辑器中执行Fix up Redirector In Folder,就会清理掉这些Redirector了,保持项目的干净。

UE4:Cook执行代码

UE中执行Cook 的代码位于UnrealEd模块下,源码位于:

1
Editor/UnrealEd/Private/Commandlets/CookCommandlet.cpp

其中有int32 UCookCommandlet::Main(const FString& CmdLineParams)是起始逻辑。

UE4:No world was found for object

在写代码的时候在UObject里调用了一些需要传递WorldContentObject的函数,但是却把UObject的this传递了进去,因为这个东西不在场景中,无法通过它获取的World,所以会产生下列警告:

LogScript: Warning: Script Msg: No world was found for object (/Engine/Transient.SubsysTouchControllerTrace_1) passed in to UEngine::GetWorldFromContextObject().

解决办法就是传递一个能够获取到World的对象进去。

UE4:移动设备的渲染预览

ToolBar-Settings-PreviewRenderingLevel-Android ES3.1/Android ES2/IOS

Android SDK版本与Android的版本

可以在Google的开发者站点看到:Android SDK Platform
Build.VERSION_CODES的含义:Build.VERSION_CODES

AndroidVersion SDK Version Build.VERSION_CODES
Android 11 (API level 30) R
Android 10 (API level 29) Q
Android 9 (API level 28) P
Android 8.1 (API level27) O_MR1
Android 8.0 (API level 26) O
Android 7.1 (API level 25) N_MR1
Android 7.0 (API level 24) N
Android 6.0 (API level 23) M
Android 5.1 (API level 22) LOLLIPOP_MR1
Android 5.0 (API level 21) LOLLIPOP
Android 4.4W (API level 20) KITKAT_WATCH
Android 4.4 (API level 19) KITKAT
Android 4.3 (API level 18) JELLY_BEAN_MR2
Android 4.2 (API level 17) JELLY_BEAN_MR1
Android 4.1 (API level 16) JELLY_BEAN
Android 4.0.3 (API level15) ICE_CREAM_SANDWICH_MR1
Android 4.0 (API level 14) ICE_CREAM_SANDWICH
Android 3.2 (API level 13) HONEYCOMB_MR2
Android 3.1 (API level 12) HONEYCOMB_MR1
Android 3.0 (API level 11) HONEYCOMB
Android 2.3.3 (API level 10) GINGERBREAD_MR1
Android 2.3 (API level 9) GINGERBREAD

UE4对Android的最低支持是SDK9,也就是Android2.3。

UE4:Android项目设置

  • EnableGradleInsteadOfAnt:使用Gradle替代Ant用来编译和生成APK。
  • EnableFullScreenImmersiveOnKitKatAndAboveDevices:全屏模式下隐藏虚拟按键;
  • EnableImprovedVirtualKeyboard:启用虚拟键盘;

UE4:在构造函数中用SetupAttachmen替代AttachToComponent

在构造函数中使用AttachToComponent在打包时会有这样的错误:

Error: AttachToComponent when called from a constructor is only setting up attachment and will always be treated as KeepRelative. Consider calling SetupAttachment directly instead.

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
UATHelper: Packaging (Windows (64-bit)):   LogInit: Display: LogOutputDevice: Error: begin: stack for UAT
UATHelper: Packaging (Windows (64-bit)): LogInit: Display: LogOutputDevice: Error: === Handled ensure: ===
UATHelper: Packaging (Windows (64-bit)): LogInit: Display: LogOutputDevice: Error:
UATHelper: Packaging (Windows (64-bit)): LogInit: Display: LogOutputDevice: Error: Ensure condition failed: AttachmentRules.LocationRule == EAttachmentRule::KeepRelative && AttachmentRules.RotationRule == EAttachmentRule::KeepRelative && AttachmentRules.ScaleRule == EAttachmentRule::KeepRelative [File:D:\Build\++UE4\Sync\Engine\Source\Runtime\Engine\Privat
e\Components\SceneComponent.cpp] [Line: 1786]
UATHelper: Packaging (Windows (64-bit)): LogInit: Display: LogOutputDevice: Error: AttachToComponent when called from a constructor is only setting up attachment and will always be treated as KeepRelative. Consider calling SetupAttachment directly instead.
UATHelper: Packaging (Windows (64-bit)): LogInit: Display: LogOutputDevice: Error: Stack:
UATHelper: Packaging (Windows (64-bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ffe36fccd89 UE4Editor-Engine.dll!DispatchCheckVerify<bool,<lambda_2fa4c8014e6e2d59bee8e8ac7e5934f3> >() [d:\build\++ue4\sync\engine\source\runtime\core\public\misc\assertionmacros.h:161]
UATHelper: Packaging (Windows (64-bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ffe35e04fa1 UE4Editor-Engine.dll!USceneComponent::AttachToComponent() [d:\build\++ue4\sync\engine\source\runtime\engine\private\components\scenecomponent.cpp:1786]
UATHelper: Packaging (Windows (64-bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ffe215db1b3 UE4Editor-GWorld-0001.dll!ABasePlayerPawn::ABasePlayerPawn() [c:\users\imzlp\documents\unreal projects\gworldclient\source\gworld\private\modules\coreentity\instance\baseplayerpawn.cpp:21]
UATHelper: Packaging (Windows (64-bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ffe215fdf57 UE4Editor-GWorld-0001.dll!InternalConstructor<ABasePlayerPawn>() [c:\program files\epic games\ue_4.22\engine\source\runtime\coreuobject\public\uobject\class.h:2841]
UATHelper: Packaging (Windows (64-bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ffe382a610f UE4Editor-CoreUObject.dll!UClass::CreateDefaultObject() [d:\build\++ue4\sync\engine\source\runtime\coreuobject\private\uobject\class.cpp:3076]
UATHelper: Packaging (Windows (64-bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ffe384e1056 UE4Editor-CoreUObject.dll!UObjectLoadAllCompiledInDefaultProperties() [d:\build\++ue4\sync\engine\source\runtime\coreuobject\private\uobject\uobjectbase.cpp:793]
UATHelper: Packaging (Windows (64-bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ffe384cedef UE4Editor-CoreUObject.dll!ProcessNewlyLoadedUObjects() [d:\build\++ue4\sync\engine\source\runtime\coreuobject\private\uobject\uobjectbase.cpp:869]
UATHelper: Packaging (Windows (64-bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ffe382aa727 UE4Editor-CoreUObject.dll!TBaseStaticDelegateInstance<void __cdecl(void)>::ExecuteIfSafe() [d:\build\++ue4\sync\engine\source\runtime\core\public\delegates\delegateinstancesimpl.h:813]
UATHelper: Packaging (Windows (64-bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ffe38874a2b UE4Editor-Core.dll!TBaseMulticastDelegate<void>::Broadcast() [d:\build\++ue4\sync\engine\source\runtime\core\public\delegates\delegatesignatureimpl.inl:977]
UATHelper: Packaging (Windows (64-bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ffe38a45cb5 UE4Editor-Core.dll!FModuleManager::LoadModuleWithFailureReason() [d:\build\++ue4\sync\engine\source\runtime\core\private\modules\modulemanager.cpp:530]
UATHelper: Packaging (Windows (64-bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ffe5fb58d67 UE4Editor-Projects.dll!FModuleDescriptor::LoadModulesForPhase() [d:\build\++ue4\sync\engine\source\runtime\projects\private\moduledescriptor.cpp:596]
UATHelper: Packaging (Windows (64-bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ffe5fb58ff7 UE4Editor-Projects.dll!FProjectManager::LoadModulesForProject() [d:\build\++ue4\sync\engine\source\runtime\projects\private\projectmanager.cpp:63]
UATHelper: Packaging (Windows (64-bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ff65dabd260 UE4Editor-Cmd.exe!FEngineLoop::PreInit() [d:\build\++ue4\sync\engine\source\runtime\launch\private\launchengineloop.cpp:2425]
UATHelper: Packaging (Windows (64-bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ff65dab5377 UE4Editor-Cmd.exe!GuardedMain() [d:\build\++ue4\sync\engine\source\runtime\launch\private\launch.cpp:129]
UATHelper: Packaging (Windows (64-bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ff65dab55ca UE4Editor-Cmd.exe!GuardedMainWrapper() [d:\build\++ue4\sync\engine\source\runtime\launch\private\windows\launchwindows.cpp:145]
UATHelper: Packaging (Windows (64-bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ff65dac316c UE4Editor-Cmd.exe!WinMain() [d:\build\++ue4\sync\engine\source\runtime\launch\private\windows\launchwindows.cpp:275]
UATHelper: Packaging (Windows (64-bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ff65dac4cb6 UE4Editor-Cmd.exe!__scrt_common_main_seh() [d:\agent\_work\3\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288]
UATHelper: Packaging (Windows (64-bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ffe7b247974 KERNEL32.DLL!UnknownFunction []
UATHelper: Packaging (Windows (64-bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ffe7c62a271 ntdll.dll!UnknownFunction []
UATHelper: Packaging (Windows (64-bit)): LogInit: Display: LogOutputDevice: Error: end: stack for UAT

解决的办法就是提示中的那样,用SetupAttachment替换AttachToComponent

UE4:在其他的线程运行程序并获取程序输出

在写避编辑器功能的时候经常会启动外部的程序来执行任务,如果要求程序执行完成才走其他逻辑则会阻塞,这样的体验很不好,所以一般是开一个额外的线程来执行程序启动。废话不多说直接看代码:

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
#pragma  once
#include "FThreadUtils.hpp"
#include "CoreMinimal.h"
#include "GenericPlatform/GenericPlatformProcess.h"

DECLARE_MULTICAST_DELEGATE_OneParam(FOutputMsgDelegate, const FString&);
DECLARE_MULTICAST_DELEGATE(FProcStatusDelegate);


class FProcWorkerThread : public FThread
{
public:
explicit FProcWorkerThread(const TCHAR *InThreadName,const FString& InProgramPath,const FString& InParams)
: FThread(InThreadName, []() {}), mProgramPath(InProgramPath), mPragramParams(InParams)
{}

virtual uint32 Run()override
{
if (FPaths::FileExists(mProgramPath))
{
FPlatformProcess::CreatePipe(mReadPipe, mWritePipe);
// std::cout << TCHAR_TO_ANSI(*mProgramPath) << " " << TCHAR_TO_ANSI(*mPragramParams) << std::endl;

mProcessHandle = FPlatformProcess::CreateProc(*mProgramPath, *mPragramParams, false, true, true, &mProcessID, 0, NULL, mWritePipe,mReadPipe);
if (mProcessHandle.IsValid() && FPlatformProcess::IsApplicationRunning(mProcessID))
{
ProcBeginDelegate.Broadcast();
}

FString Line;
while (mProcessHandle.IsValid() && FPlatformProcess::IsApplicationRunning(mProcessID))
{
FPlatformProcess::Sleep(0.0f);

FString NewLine = FPlatformProcess::ReadPipe(mReadPipe);
if (NewLine.Len() > 0)
{
// process the string to break it up in to lines
Line += NewLine;
TArray<FString> StringArray;
int32 count = Line.ParseIntoArray(StringArray, TEXT("\n"), true);
if (count > 1)
{
for (int32 Index = 0; Index < count - 1; ++Index)
{
StringArray[Index].TrimEndInline();
ProcOutputMsgDelegate.Broadcast(StringArray[Index]);
}
Line = StringArray[count - 1];
if (NewLine.EndsWith(TEXT("\n")))
{
Line += TEXT("\n");
}
}
}
}

int32 ProcReturnCode;
if (FPlatformProcess::GetProcReturnCode(mProcessHandle,&ProcReturnCode))
{
if (ProcReturnCode == 0)
{
ProcSuccessedDelegate.Broadcast();
}
else
{
ProcFaildDelegate.Broadcast();
}
}

}
mThreadStatus = EThreadStatus::Completed;
return 0;
}
virtual void Exit()override
{
if (mProcessHandle.IsValid())
{

}
}
virtual void Cancel()override
{
if (GetThreadStatus() != EThreadStatus::Busy)
return;
mThreadStatus = EThreadStatus::Canceling;
if (mProcessHandle.IsValid() && FPlatformProcess::IsApplicationRunning(mProcessID))
{
FPlatformProcess::TerminateProc(mProcessHandle, true);
ProcFaildDelegate.Broadcast();
mProcessHandle.Reset();
mProcessID = 0;
}
mThreadStatus = EThreadStatus::Canceled;
CancelDelegate.Broadcast();
}

virtual uint32 GetProcesId()const { return mProcessID; }
virtual FProcHandle GetProcessHandle()const { return mProcessHandle; }

public:
FProcStatusDelegate ProcBeginDelegate;
FProcStatusDelegate ProcSuccessedDelegate;
FProcStatusDelegate ProcFaildDelegate;
FOutputMsgDelegate ProcOutputMsgDelegate;

private:
FRunnableThread* mThread;
FString mProgramPath;
FString mPragramParams;
void* mReadPipe;
void* mWritePipe;
uint32 mProcessID;
FProcHandle mProcessHandle;
};

可以通过监听ProcOutputMsgDelegate来接收程序的打印输出。

UE4:绑定FTicker

在非Actor的对象上如果想要使用Tick,可以使用下列代码:

1
FDelegateHandle TickHandle = FTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateRaw(this,&UGameExtensionSettings::Tick);

PS:FCoreDelegatesFCoreUObjectDelegates中有很多有用的代理。

UE4:地图加载的Delegate

FCoreUObjectDelegates中的这两个代理在地图加载时和地图加载完成时会调用,可以用来显示加载地图时的过场:

1
2
FCoreUObjectDelegates::PreLoadMap.AddUObject(this, &UMarinGameInstance::BeginLoadingScreen);
FCoreUObjectDelegates::PostLoadMapWithWorld.AddUObject(this, &UMarinGameInstance::EndL

UE4:StaticLoadClass

可以从通过一个字符串来加载UClass:

1
UClass* UserWidgetClass = StaticLoadClass(UUserWidget::StaticClass(), NULL, TEXT("WidgetBlueprintGeneratedClass'/Game/TEST/BP_UserWidget.BP_UserWidget_C'"));

同理也有StaticLoadObject

UE4:ConstructorHelpers::FClassFinder

只能在构造函数中调用,否则引擎会崩溃,在外部使用可以使用StaticLoadClass

CMD的sudo

在Windows上时长会遇到要使用管理员权限执行的命令,而Win的cmd又不如Linux那样可以直接sudo来获取管理员权限,还要手动敲cmd然后右键管理员权限运行 十分麻烦,下面这个脚本可以实现类似bash的sudo操作:

1
2
3
@echo off
powershell -Command "(($arg='/k cd /d '+$pwd+' && %*') -and (Start-Process cmd -Verb RunAs -ArgumentList $arg))| Out-Null"
@echo on

将其保存为sudo.bat然后放入C:\Windows下即可,随便打开一个cmd可以输入sudo来执行命令了。

UE4:在游戏项目中添加编辑器模块

首先创建一个空的C++项目,其目录结构为:

1
2
3
4
5
6
7
8
9
10
C:\GWorld\Source>>tree /a /f
| GWorld.Target.cs
| GWorldEditor.Target.cs
|
\---GWorld
GWorld.Build.cs
GWorld.cpp
GWorld.h
GWorldGameModeBase.cpp
GWorldGameModeBase.h
  1. 我们在Source目录下添加一个GWorldEditor的文件夹,在其中新建GWorldEditor.Build.cs/GWorldEditor.cpp/GWorldEditor.h三个文件,并仿照GWorld模块下的文件内容修改;

  2. GWorldEditor.Build.cs中添加GWorld的模块依赖;

  3. 修改GWorldEditor.Target.cs文件将ExtraModuleNames.AddRange( new string[] { "GWorld" } );其中的GWorld修改为GWorldEditor

  4. 修改项目的uproject文件,在Modules下添加GWorldEditor

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    {
    "FileVersion": 3,
    "EngineAssociation": "4.22",
    "Category": "",
    "Description": "",
    "Modules": [
    {
    "Name": "GWorld",
    "Type": "Runtime",
    "LoadingPhase": "Default",
    "AdditionalDependencies": [
    "Engine",
    "CoreUObject"
    ]
    },
    {
    "Name": "GWorldEditor",
    "Type": "Editor",
    "LoadingPhase": "PostEngineInit"
    }
    ]
    }

    之后重新生成VS的解决方案,编译即可。

可能遇到的错误

1
LogInit: Warning: Still incompatible or missing module: GWorld

如果遇到上面的错误是因为没在GWorldEditor.Build.cs中添加GWorld的模块依赖。

参考文章:Creating an Editor Module

UE4:does not match the necessary signature

在蓝图事件绑定C++的Dispatcher时会有这样的提示:

1
XXXXEvent Signature Error: The function/event `XXXXEvent` does not match the necessary signature - the delegate or function/event changed?

这是因为C++里的Dispather的传入参数不是const&的(上面图里是TArray<Type>),在代码里把Delegate的声明参数改为const TArray<Type>&然后重新编译即可。

UE4:数据成员初始化顺序必须要与声明顺序一致

如:

1
2
3
4
5
6
7
8
9
class A
{
// error in UE4,ill in c++
A():dval(0.0f),ival(0){}
public:
int ival;
double dval;
};

会有下列错误:

1
error C5038: data member 'UTcpNetPeer::RecvMessageDataRemaining' will be initialized after data member 'UTcpNetPeer::ConnectionRetryTimes'

UE4:Delegate

UE的Delegate分了几个不同的种类,普通的代理是模板类,Dynamic Delegate是通过宏展开生成类,Dynamic Mulitcast Delegate需要UHT参与生成代码,所以动态多播代理只能写到包含generated.h的文件中。

Delegate

  • DECLARE_DELEGATE:普通代理,可以被值传递,本质实现是TBaseDelegare<__VA_ARGS__>的对象,可以使用BindUObject等函数。

TBaseDelegate里定义了很多的辅助函数,如BindSP/BindRaw/BindStatic/CreateSP等。

Dynamic Delegate

  • DECLARE_DYNAMIC_DELEGATE:动态代理可以序列化,其函数可按命名查找,执行速度比常规代理慢。

动态代理本质上是继承自TBaseDynamicDelegate的类,TBaseDynamicDelegate又继承自TScriptDelegate,所以动态代理可以绑定UObject和绑定UFUNCTION。

其中BindUFunctionTScriptInterface的函数,而BindUbject是个宏,定义在Delegate.h中。

代码分析:

1
DECLARE_DYNAMIC_DELEGATE_OneParam(FOnTestDynamicDelegate, const FString&, InStr);

DECLARE_DYNAMIC_DELEGATE_OneParam的宏定义为:

1
#define DECLARE_DYNAMIC_DELEGATE_OneParam( DelegateName, Param1Type, Param1Name ) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_DELEGATE) FUNC_DECLARE_DYNAMIC_DELEGATE( FWeakObjectPtr, DelegateName, DelegateName##_DelegateWrapper, FUNC_CONCAT( Param1Type InParam1 ), FUNC_CONCAT( *this, InParam1 ), void, Param1Type )

BODY_MACRO_COMBINE宏其经过UHT之后生成的代码为:

1
2
3
4
5
6
7
8
9
10
11
#define GWorldClient_Plugins_HotPackage_HotPatcher_Source_HotPatcherEditor_Private_CreatePatch_ExportPatchSettings_h_22_DELEGATE \
struct _Script_HotPatcherEditor_eventOnTestDynamicDelegate_Parms \
{ \
FString InStr; \
}; \
static inline void FOnTestDynamicDelegate_DelegateWrapper(const FScriptDelegate& OnTestDynamicDelegate, const FString& InStr) \
{ \
_Script_HotPatcherEditor_eventOnTestDynamicDelegate_Parms Parms; \
Parms.InStr=InStr; \
OnTestDynamicDelegate.ProcessDelegate<UObject>(&Parms); \
}

进行展开之后,可以看到使用DECLARE_DYNAMIC_DELEGATE_OneParam声明的代理实际上是就被定义了一个static函数。

FUNC_DECLARE_DYNAMIC_DELEGATE宏是裹了一个TBaseDynamicDelegate

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
/** Declare user's dynamic delegate, with wrapper proxy method for executing the delegate */
#define FUNC_DECLARE_DYNAMIC_DELEGATE( TWeakPtr, DynamicDelegateName, ExecFunction, FuncParamList, FuncParamPassThru, ... ) \
class DynamicDelegateName : public TBaseDynamicDelegate<TWeakPtr, __VA_ARGS__> \
{ \
public: \
/** Default constructor */ \
DynamicDelegateName() \
{ \
} \
\
/** Construction from an FScriptDelegate must be explicit. This is really only used by UObject system internals. */ \
explicit DynamicDelegateName( const TScriptDelegate<>& InScriptDelegate ) \
: TBaseDynamicDelegate<TWeakPtr, __VA_ARGS__>( InScriptDelegate ) \
{ \
} \
\
/** Execute the delegate. If the function pointer is not valid, an error will occur. */ \
inline void Execute( FuncParamList ) const \
{ \
/* Verify that the user object is still valid. We only have a weak reference to it. */ \
checkSlow( IsBound() ); \
ExecFunction( FuncParamPassThru ); \
} \
/** Execute the delegate, but only if the function pointer is still valid */ \
inline bool ExecuteIfBound( FuncParamList ) const \
{ \
if( IsBound() ) \
{ \
ExecFunction( FuncParamPassThru ); \
return true; \
} \
return false; \
} \
};

所有的宏都被展开之后:

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
struct _Script_HotPatcherEditor_eventOnTestDynamicDelegate_Parms
{
FString InStr;
};
static inline void FOnTestDynamicDelegate_DelegateWrapper(const FScriptDelegate& OnTestDynamicDelegate, const FString& InStr)
{
_Script_HotPatcherEditor_eventOnTestDynamicDelegate_Parms Parms;
Parms.InStr=InStr;
OnTestDynamicDelegate.ProcessDelegate<UObject>(&Parms);
}


class FOnTestDynamicDelegate : public TBaseDynamicDelegate<FWeakObjectPtr, void, const FString&>
{
public:
FOnTestDynamicDelegate() { }
explicit FOnTestDynamicDelegate( const TScriptDelegate<>& InScriptDelegate ) : TBaseDynamicDelegate<FWeakObjectPtr, void, const FString&>( InScriptDelegate ) { }
inline void Execute( const FString& InStr ) const { checkSlow( IsBound() ); FOnTestDynamicDelegate_DelegateWrapper( *this, InStr ); }
inline bool ExecuteIfBound( const FString& InStr ) const
{
if( IsBound() )
{
FOnTestDynamicDelegate_DelegateWrapper( *this, InStr );
return true;
}
return false;
}
};

在使用的时候可以通过BindUFunction来绑定函数,通过Execute或者ExecuteIfBound来调用。

在当作回调函数传递的时候比较方便,因为它继承自FScriptDelegate,可以当作通用的方式传递。

Multicast Delegate

  • 多播代理:与普通代理的大部分功能相同,它们只拥有对象的弱引用,可以和结构体一起使用,可以复制。其本质是TMulticastDelegate<__VA_ARGS__>的对象。多播代理可以被加载/保存和远程触发,但多播代理不能使用返回值。

多播代理可以具有多个绑定,当Broadcast触发时,所有的绑定都会被调用。

多播代理可以使用Add/AddStatic/AddRaw/AddSP/AddUObject/Remove/RemoveAll等函数。

注意:RemoveAll会删除所有绑定时提供指针的代理,未绑定到对象的Raw代理不会被该函数删除。

Dynamic Multicast Delegate

  • DECLARE_DYNAMIC_MULITCAST_DELEGATE:动态多播代理。

动态多播代理必须要绑定到UFUNCTION的函数,使用宏AddDynamic或者AddUnique来添加绑定。

代码分析:

1
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnTestDynamicMultiDelegate, const FString&, InStr);

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam其宏定义为:

1
2
3
#define DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam( DelegateName, Param1Type, Param1Name )\
BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_DELEGATE)\
FUNC_DECLARE_DYNAMIC_MULTICAST_DELEGATE( FWeakObjectPtr, DelegateName, DelegateName##_DelegateWrapper, FUNC_CONCAT( Param1Type InParam1 ), FUNC_CONCAT( *this, InParam1 ), void, Param1Type )

其中BODY_MACRO_COMBINE经过UHT之后生成下列代码:

1
2
3
4
5
6
7
8
9
10
11
#define GWorldClient_Plugins_HotPackage_HotPatcher_Source_HotPatcherEditor_Private_CreatePatch_ExportPatchSettings_h_22_DELEGATE \
struct _Script_HotPatcherEditor_eventOnTestDynamicMultiDelegate_Parms \
{ \
FString InStr; \
}; \
static inline void FOnTestDynamicMultiDelegate_DelegateWrapper(const FMulticastScriptDelegate& OnTestDynamicMultiDelegate, const FString& InStr) \
{ \
_Script_HotPatcherEditor_eventOnTestDynamicMultiDelegate_Parms Parms; \
Parms.InStr=InStr; \
OnTestDynamicMultiDelegate.ProcessMulticastDelegate<UObject>(&Parms); \
}

FUNC_DECLARE_DYNAMIC_MULTICAST_DELEGATE则创建了一个继承自TBaseDynamicMulticastDelegate类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/** Declare user's dynamic multi-cast delegate, with wrapper proxy method for executing the delegate */
#define FUNC_DECLARE_DYNAMIC_MULTICAST_DELEGATE(TWeakPtr, DynamicMulticastDelegateName, ExecFunction, FuncParamList, FuncParamPassThru, ...) \
class DynamicMulticastDelegateName : public TBaseDynamicMulticastDelegate<TWeakPtr, __VA_ARGS__> \
{ \
public: \
/** Default constructor */ \
DynamicMulticastDelegateName() \
{ \
} \
\
/** Construction from an FMulticastScriptDelegate must be explicit. This is really only used by UObject system internals. */ \
explicit DynamicMulticastDelegateName( const TMulticastScriptDelegate<>& InMulticastScriptDelegate ) \
: TBaseDynamicMulticastDelegate<TWeakPtr, __VA_ARGS__>( InMulticastScriptDelegate ) \
{ \
} \
\
/** Broadcasts this delegate to all bound objects, except to those that may have expired */ \
void Broadcast( FuncParamList ) const \
{ \
ExecFunction( FuncParamPassThru ); \
} \
};

FUNC_CONCT宏只是简单的拼接:

1
2
/** Helper macro that enables passing comma-separated arguments as a single macro parameter */
#define FUNC_CONCAT( ... ) __VA_ARGS__

展开所有宏之后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct _Script_HotPatcherEditor_eventOnTestDynamicMultiDelegate_Parms
{
FString InStr;
};
static inline void FOnTestDynamicMultiDelegate_DelegateWrapper(const FMulticastScriptDelegate& OnTestDynamicMultiDelegate, const FString& InStr)
{
_Script_HotPatcherEditor_eventOnTestDynamicMultiDelegate_Parms Parms;
Parms.InStr=InStr;
OnTestDynamicMultiDelegate.ProcessMulticastDelegate<UObject>(&Parms);
}

class FOnTestDynamicMultiDelegate : public TBaseDynamicMulticastDelegate<FWeakObjectPtr, void, const FString&>
{
public:
FOnTestDynamicMultiDelegate() { }
explicit FOnTestDynamicMultiDelegate( const TMulticastScriptDelegate<>& InMulticastScriptDelegate ) : TBaseDynamicMulticastDelegate<FWeakObjectPtr, void, const FString&>( InMulticastScriptDelegate ) { }
void Broadcast( const FString& InStr ) const { FOnTestDynamicMultiDelegate_DelegateWrapper( *this, InStr ); }
};

注,由上面的代码可知,只有普通代理(DECLARE_DELEGATE)声明的代理才可以使用TBaseDelegateBindUObject等函数,动态代理和多播代理都不可以。

UE4: Pak所包含的文件

默认情况下(未设置忽略文件)UE4的Package时会把游戏的资源打包到一个Pak文件中,具体有以下内容:

以下描述中有几个关键字:PROJECT_NAME项目名,PLATFORN_NAME打包的平台名。

  • Package时不会检测资源是否有引用,工程内的所有资源都会被Cook然后打包到Pak里;
  • 引擎Slate的资源文件Engine\Content\Slate\,字体/图片等等
  • 引擎的Content\Internationalization下相关语言的文件
  • 引擎和启用插件目录下的Content\Localizationlocmeta/locres文件
  • 项目的uproject文件,挂载点为../../../PROJECT_NAME/PROJECT_NAME.uproject
  • 项目启用的所有插件的uplugin文件,挂载点为插件的相对与../../../Engine/或者../../../PROJECT_NAME/Plugins/的路径;
  • 项目目录下Intermediate\Staging\PROJECT_NAME.upluginmanifest文件,挂载点为../../../PROJECT_NAME/Plugins/PROJECT_NAME.upluginmanifest
  • 引擎的ini文件,在引擎的Engine/Config下除了Editor的ini和BaseLightmass.ini/BasePakFileRules.ini之外都包含;
  • 引擎下平台的ini,在Engine/Config/PLATFORM_NAME内的所有ini文件;
  • 项目启用的插件的ini,在插件的目录的config下;
  • Cook出来的AssetRegistry.bin
  • Cook出的PLATFORN_NAME\Engine\GlobalShaderCache*.bin
  • Cook出来的PLATFORM_NAME\PROJECT_NAME\Content\ShaderArchive-*.ushaderbytecode文件

UE4:Mount Pak时的一个问题

UE在Mount时调用的是FPakPlatformFile::Mount,对于要Mount的Pak创建了一个FPakFile的对象,该对象会存储到FPakPlatformFile::PakFiles中。

问题就出在这个PakFiles上,其声明为TArray<FPakListEntry>,而FPakListEntry这个struct是定义在类FPakPlatformFile中的类,而且还是个私有的类定义,在外部无法访问,虽然FPakPlatformFile中有GetMountedPaks这个函数,但是传入的参数外部无法定义(因为是私有的)。

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
// Runtime/PakFile/Public/IPlatformPak.h
class PAKFILE_API FPakPlatformFile : public IPlatformFile
{
struct FPakListEntry
{
FPakListEntry()
: ReadOrder(0)
, PakFile(nullptr)
{}

uint32 ReadOrder;
FPakFile* PakFile;

FORCEINLINE bool operator < (const FPakListEntry& RHS) const
{
return ReadOrder > RHS.ReadOrder;
}
};

// ...

/**
* Gets mounted pak files
*/
FORCEINLINE void GetMountedPaks(TArray<FPakListEntry>& Paks)
{
FScopeLock ScopedLock(&PakListCritical);
Paks.Append(PakFiles);
}

// ...
};

所以在不改动引擎源码的情况下无法直接得到已经Mount的Pak列表,有点坑。

绕一圈可以使用的方法为FPakPlatformFile::GetMountedPakFilenames用来获取当前已经Mount的Pak列表:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Get a list of all pak files which have been successfully mounted
*/
FORCEINLINE void GetMountedPakFilenames(TArray<FString>& PakFilenames)
{
FScopeLock ScopedLock(&PakListCritical);
PakFilenames.Empty(PakFiles.Num());
for (FPakListEntry& Entry : PakFiles)
{
PakFilenames.Add(Entry.PakFile->GetFilename());
}
}

然后再通过IPlatformFilePak::FindFileInPakFiles可以获取到已经Mount的某个Pak的FPakFile实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Finds a file in all available pak files.
*
* @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.
*/
bool FindFileInPakFiles(const TCHAR* Filename, FPakFile** OutPakFile = nullptr, FPakEntry* OutEntry = nullptr)
{
TArray<FPakListEntry> Paks;
GetMountedPaks(Paks);

return FindFileInPakFiles(Paks, Filename, OutPakFile, OutEntry);
}

可以通过传入Pak的路径信息来得到FPakFile。

UE4:FScopeSlowTask

执行一些任务的时候可以显示进度。

1
2
3
4
5
6
7
8
9
10
11
float AmountOfWorkProgress = 2.0f;
FScopeSlowTask SlowTask(AmountOfWorkProgress);
SlowTask.MakeDialog();

// something
// Update SlowTask Progress
{
FText Dialog = FText::Format(NSLOCTEXT("ExportPatch", "GeneratedPak", "Generating Pak list of {0} Platform."), FText::FromString(PlatformName));
SlowTask.EnterProgressFrame(1.0, Dialog);
}
// something

需要注意两点:

  1. EnterProgressFrame传入的参数每次为1.0f即可,里面是累增的。
  2. 不要在一个函数里创建多个FScopeSlowTaskDialog,会有窗口消不掉的问题(UE_4.22.3)。

UE4: MD5Hash

在UE中计算文件的MD5Hash值:

1
2
FMD5Hash FileHash = FMD5Hash::HashFile(*InFile);
FString HashValue = LexToString(FileHash);

UE4:运行时在Pak中访问非资源文件

可以把一些非UE资源文件(比如txt,视频)等文件打包到Pak中,在游戏运行中访问,可以使用我上面写的HotPatcher工具来打包,这里写一下在运行时访问的方法。

首先将文件打包到Pak:

这里我是将文件AAAAA.json打包到了Pak中,其挂载路径为../../../PROJECT_NAME/AAAAA.json.

在游戏中访问一定要使用挂载路径,可以使用FPaths::ProjectDir在打包后获取到的是相对路径,在PIE下是项目的相对路径。

使用方法:

其中的PeojectDirFPaths::ProjectDirLoadFileToString则是IFileManager::LoadFileToString的封装。

运行结果:

如果想要访问打包出的项目文件和挂载的Pak中的文件,可以使用下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void UPakVisitorHelper::VisitorProjectDir()
{
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();

FFillArrayDirectoryVisitor Visitor;
PlatformFile.IterateDirectory(*FPaths::ProjectDir(), Visitor);

UE_LOG(LogTemp,Log,TEXT("Found Files:"));
for (const auto& File : Visitor.Files)
{
UE_LOG(LogTemp, Log, TEXT("%s"), *File);
}
UE_LOG(LogTemp, Log, TEXT("Found Directorys:"));
for (const auto& Dir : Visitor.Directories)
{
UE_LOG(LogTemp, Log, TEXT("%s"), *Dir);
}
}

IPlatformFile::IterateDirectory这个函数有两个原型:

1
2
virtual bool IterateDirectory(const TCHAR * Directory, FDirectoryVisitor Visitor);
virtual bool IterateDirectory(const TCHAR * Directory,FDirectoryVisitorFunc Visitor);

可以传入一个继承自FDirectoryVisitor的对象,或者传入一个下列签名的函数对象:

1
typedef TFunctionRef < bool(const TCHAR *, bool)> FDirectoryVisitorFunc

支持传入一个Lambda也是可以的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
TArray<FString> Files;
TArray<FString> Dirs;

IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
auto FallArrayDirVisitor = [&Files,&Dirs](const TCHAR* InItem,bool bInDir)->bool
{
if (bInDir)
{
Dirs.AddUnique(InItem);
}
else
{
Files.AddUnique(InItem);
}
return true;
};
PlatformFile.IterateDirectory(*InRelativePath, FallArrayDirVisitor);

执行结果:

AssetRegistry.bin/*.uproject等文件都是在打包的时候打包进pak里的,AAAAA.json则是上面手动打到Patch的Pak里的。

UE4:枚举的反射信息

下列枚举类型:

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
// ETargetPlatform.h
#pragma once

#include "CoreMinimal.h"
#include "ETargetPlatform.generated.h"

UENUM(BlueprintType)
enum class ETargetPlatform : uint8
{
AllDesktop,
MacClient,
MacNoEditor,
MacServer,
Mac,
WindowsClient,
WindowsNoEditor,
WindowsServer,
Windows,
Android,
Android_ASTC,
Android_ATC,
Android_DXT,
Android_ETC1,
Android_ETC1a,
Android_ETC2,
Android_PVRTC,
AndroidClient,
Android_ASTCClient,
Android_ATCClient,
Android_DXTClient,
Android_ETC1Client,
Android_ETC1aClient,
Android_ETC2Client,
Android_PVRTCClient,
Android_Multi,
Android_MultiClient,
HTML5,
IOSClient,
IOS,
TVOSClient,
TVOS,
LinuxClient,
LinuxNoEditor,
LinuxServer,
Linux,
Lumin,
LuminClient
};

经过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
// ETargetPlatform.generated.h
#include "UObject/ObjectMacros.h"
#include "UObject/ScriptMacros.h"

PRAGMA_DISABLE_DEPRECATION_WARNINGS
#ifdef HOTPATCHERRUNTIME_ETargetPlatform_generated_h
#error "ETargetPlatform.generated.h already included, missing '#pragma once' in ETargetPlatform.h"
#endif
#define HOTPATCHERRUNTIME_ETargetPlatform_generated_h

#undef CURRENT_FILE_ID
#define CURRENT_FILE_ID HotThirdPerson_Plugins_ue4_HotPackage_HotPatcher_Source_HotPatcherRuntime_Public_ETargetPlatform_h


#define FOREACH_ENUM_ETARGETPLATFORM(op) \
op(ETargetPlatform::AllDesktop) \
op(ETargetPlatform::MacClient) \
op(ETargetPlatform::MacNoEditor) \
op(ETargetPlatform::MacServer) \
op(ETargetPlatform::Mac) \
op(ETargetPlatform::WindowsClient) \
op(ETargetPlatform::WindowsNoEditor) \
op(ETargetPlatform::WindowsServer) \
op(ETargetPlatform::Windows) \
op(ETargetPlatform::Android) \
op(ETargetPlatform::Android_ASTC) \
op(ETargetPlatform::Android_ATC) \
op(ETargetPlatform::Android_DXT) \
op(ETargetPlatform::Android_ETC1) \
op(ETargetPlatform::Android_ETC1a) \
op(ETargetPlatform::Android_ETC2) \
op(ETargetPlatform::Android_PVRTC) \
op(ETargetPlatform::AndroidClient) \
op(ETargetPlatform::Android_ASTCClient) \
op(ETargetPlatform::Android_ATCClient) \
op(ETargetPlatform::Android_DXTClient) \
op(ETargetPlatform::Android_ETC1Client) \
op(ETargetPlatform::Android_ETC1aClient) \
op(ETargetPlatform::Android_ETC2Client) \
op(ETargetPlatform::Android_PVRTCClient) \
op(ETargetPlatform::Android_Multi) \
op(ETargetPlatform::Android_MultiClient) \
op(ETargetPlatform::HTML5) \
op(ETargetPlatform::IOSClient) \
op(ETargetPlatform::IOS) \
op(ETargetPlatform::TVOSClient) \
op(ETargetPlatform::TVOS) \
op(ETargetPlatform::LinuxClient) \
op(ETargetPlatform::LinuxNoEditor) \
op(ETargetPlatform::LinuxServer) \
op(ETargetPlatform::Linux) \
op(ETargetPlatform::Lumin) \
op(ETargetPlatform::LuminClient)

enum class ETargetPlatform : uint8;
template<> HOTPATCHERRUNTIME_API UEnum* StaticEnum<ETargetPlatform>();

PRAGMA_ENABLE_DEPRECATION_WARNINGS

产生的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
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
#include "UObject/GeneratedCppIncludes.h"
#include "HotPatcherRuntime/Public/ETargetPlatform.h"
#ifdef _MSC_VER
#pragma warning (push)
#pragma warning (disable : 4883)
#endif
PRAGMA_DISABLE_DEPRECATION_WARNINGS
void EmptyLinkFunctionForGeneratedCodeETargetPlatform() {}
// Cross Module References
HOTPATCHERRUNTIME_API UEnum* Z_Construct_UEnum_HotPatcherRuntime_ETargetPlatform();
UPackage* Z_Construct_UPackage__Script_HotPatcherRuntime();
// End Cross Module References
static UEnum* ETargetPlatform_StaticEnum()
{
static UEnum* Singleton = nullptr;
if (!Singleton)
{
Singleton = GetStaticEnum(Z_Construct_UEnum_HotPatcherRuntime_ETargetPlatform, Z_Construct_UPackage__Script_HotPatcherRuntime(), TEXT("ETargetPlatform"));
}
return Singleton;
}
template<> HOTPATCHERRUNTIME_API UEnum* StaticEnum<ETargetPlatform>()
{
return ETargetPlatform_StaticEnum();
}
static FCompiledInDeferEnum Z_CompiledInDeferEnum_UEnum_ETargetPlatform(ETargetPlatform_StaticEnum, TEXT("/Script/HotPatcherRuntime"), TEXT("ETargetPlatform"), false, nullptr, nullptr);
uint32 Get_Z_Construct_UEnum_HotPatcherRuntime_ETargetPlatform_Hash() { return 2902485356U; }
UEnum* Z_Construct_UEnum_HotPatcherRuntime_ETargetPlatform()
{
#if WITH_HOT_RELOAD
UPackage* Outer = Z_Construct_UPackage__Script_HotPatcherRuntime();
static UEnum* ReturnEnum = FindExistingEnumIfHotReloadOrDynamic(Outer, TEXT("ETargetPlatform"), 0, Get_Z_Construct_UEnum_HotPatcherRuntime_ETargetPlatform_Hash(), false);
#else
static UEnum* ReturnEnum = nullptr;
#endif // WITH_HOT_RELOAD
if (!ReturnEnum)
{
static const UE4CodeGen_Private::FEnumeratorParam Enumerators[] = {
{ "ETargetPlatform::AllDesktop", (int64)ETargetPlatform::AllDesktop },
{ "ETargetPlatform::MacClient", (int64)ETargetPlatform::MacClient },
{ "ETargetPlatform::MacNoEditor", (int64)ETargetPlatform::MacNoEditor },
{ "ETargetPlatform::MacServer", (int64)ETargetPlatform::MacServer },
{ "ETargetPlatform::Mac", (int64)ETargetPlatform::Mac },
{ "ETargetPlatform::WindowsClient", (int64)ETargetPlatform::WindowsClient },
{ "ETargetPlatform::WindowsNoEditor", (int64)ETargetPlatform::WindowsNoEditor },
{ "ETargetPlatform::WindowsServer", (int64)ETargetPlatform::WindowsServer },
{ "ETargetPlatform::Windows", (int64)ETargetPlatform::Windows },
{ "ETargetPlatform::Android", (int64)ETargetPlatform::Android },
{ "ETargetPlatform::Android_ASTC", (int64)ETargetPlatform::Android_ASTC },
{ "ETargetPlatform::Android_ATC", (int64)ETargetPlatform::Android_ATC },
{ "ETargetPlatform::Android_DXT", (int64)ETargetPlatform::Android_DXT },
{ "ETargetPlatform::Android_ETC1", (int64)ETargetPlatform::Android_ETC1 },
{ "ETargetPlatform::Android_ETC1a", (int64)ETargetPlatform::Android_ETC1a },
{ "ETargetPlatform::Android_ETC2", (int64)ETargetPlatform::Android_ETC2 },
{ "ETargetPlatform::Android_PVRTC", (int64)ETargetPlatform::Android_PVRTC },
{ "ETargetPlatform::AndroidClient", (int64)ETargetPlatform::AndroidClient },
{ "ETargetPlatform::Android_ASTCClient", (int64)ETargetPlatform::Android_ASTCClient },
{ "ETargetPlatform::Android_ATCClient", (int64)ETargetPlatform::Android_ATCClient },
{ "ETargetPlatform::Android_DXTClient", (int64)ETargetPlatform::Android_DXTClient },
{ "ETargetPlatform::Android_ETC1Client", (int64)ETargetPlatform::Android_ETC1Client },
{ "ETargetPlatform::Android_ETC1aClient", (int64)ETargetPlatform::Android_ETC1aClient },
{ "ETargetPlatform::Android_ETC2Client", (int64)ETargetPlatform::Android_ETC2Client },
{ "ETargetPlatform::Android_PVRTCClient", (int64)ETargetPlatform::Android_PVRTCClient },
{ "ETargetPlatform::Android_Multi", (int64)ETargetPlatform::Android_Multi },
{ "ETargetPlatform::Android_MultiClient", (int64)ETargetPlatform::Android_MultiClient },
{ "ETargetPlatform::HTML5", (int64)ETargetPlatform::HTML5 },
{ "ETargetPlatform::IOSClient", (int64)ETargetPlatform::IOSClient },
{ "ETargetPlatform::IOS", (int64)ETargetPlatform::IOS },
{ "ETargetPlatform::TVOSClient", (int64)ETargetPlatform::TVOSClient },
{ "ETargetPlatform::TVOS", (int64)ETargetPlatform::TVOS },
{ "ETargetPlatform::LinuxClient", (int64)ETargetPlatform::LinuxClient },
{ "ETargetPlatform::LinuxNoEditor", (int64)ETargetPlatform::LinuxNoEditor },
{ "ETargetPlatform::LinuxServer", (int64)ETargetPlatform::LinuxServer },
{ "ETargetPlatform::Linux", (int64)ETargetPlatform::Linux },
{ "ETargetPlatform::Lumin", (int64)ETargetPlatform::Lumin },
{ "ETargetPlatform::LuminClient", (int64)ETargetPlatform::LuminClient },
};
#if WITH_METADATA
const UE4CodeGen_Private::FMetaDataPairParam Enum_MetaDataParams[] = {
{ "BlueprintType", "true" },
{ "ModuleRelativePath", "Public/ETargetPlatform.h" },
};
#endif
static const UE4CodeGen_Private::FEnumParams EnumParams = {
(UObject*(*)())Z_Construct_UPackage__Script_HotPatcherRuntime,
nullptr,
"ETargetPlatform",
"ETargetPlatform",
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;
}
PRAGMA_ENABLE_DEPRECATION_WARNINGS
#ifdef _MSC_VER
#pragma warning (pop)
#endif

枚举值的名字可以通过下列方法获得:

1
FString PlatformName = StaticEnum<ETargetPlatform>()->GetNameByValue((int64)Platform).ToString();

其得到的值是具有namespace的,如ETargetPlatform::WindowsNoEditor.

如果不想要namespace可以使用:

1
2
3
4
5
FString PlatformName;
{
FString EnumName;
StaticEnum<ETargetPlatform>()->GetNameByValue((int64)Platform).ToString().Split(TEXT("::"), &EnumName, &PlatformName,ESearchCase::CaseSensitive,ESearchDir::FromEnd);
}

UE4:创建存储文件的提示

如下图这种效果:

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
FString SaveToFile = FPaths::Combine(ExportReleaseSettings->GetSavePath(), ExportReleaseSettings->GetVersionId() + TEXT(".json"));
bool runState = UFLibAssetManageHelperEx::SaveStringToFile(SaveToFile,SaveToJson);
if (runState)
{
auto Message = LOCTEXT("ExportReleaseSuccessNotification", "Succeed to export HotPatcher Release Version.");
FNotificationInfo Info(Message);
Info.bFireAndForget = true;
Info.ExpireDuration = 5.0f;
Info.bUseSuccessFailIcons = false;
Info.bUseLargeFont = false;

const FString HyperLinkText = SaveToFile;
Info.Hyperlink = FSimpleDelegate::CreateStatic(
[](FString SourceFilePath)
{
FPlatformProcess::ExploreFolder(*SourceFilePath);
},
HyperLinkText
);
Info.HyperlinkText = FText::FromString(HyperLinkText);

FSlateNotificationManager::Get().AddNotification(Info)->SetCompletionState(SNotificationItem::CS_Success);
}

UE4:获取工程所有的Map

Developer/LauncherService/GameProjectHelper.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
TArray<FString> UFlibPatchParserHelper::GetAvailableMaps(FString GameName, bool IncludeEngineMaps, bool Sorted)
{
TArray<FString> Result;
TArray<FString> EnginemapNames;
TArray<FString> ProjectMapNames;

const FString WildCard = FString::Printf(TEXT("*%s"), *FPackageName::GetMapPackageExtension());

// Scan all Content folder, because not all projects follow Content/Maps convention
IFileManager::Get().FindFilesRecursive(ProjectMapNames, *FPaths::Combine(*FPaths::RootDir(), *GameName, TEXT("Content")), *WildCard, true, false);

// didn't find any, let's check the base GameName just in case it is a full path
if (ProjectMapNames.Num() == 0)
{
IFileManager::Get().FindFilesRecursive(ProjectMapNames, *FPaths::Combine(*GameName, TEXT("Content")), *WildCard, true, false);
}

for (int32 i = 0; i < ProjectMapNames.Num(); i++)
{
Result.Add(FPaths::GetBaseFilename(ProjectMapNames[i]));
}

if (IncludeEngineMaps)
{
IFileManager::Get().FindFilesRecursive(EnginemapNames, *FPaths::Combine(*FPaths::RootDir(), TEXT("Engine"), TEXT("Content"), TEXT("Maps")), *WildCard, true, false);

for (int32 i = 0; i < EnginemapNames.Num(); i++)
{
Result.Add(FPaths::GetBaseFilename(EnginemapNames[i]));
}
}

if (Sorted)
{
Result.Sort();
}

return Result;
}

UE4:AssetRegistry的Asset概念

UE中的Asset在使用时有以下三个概念:

  • PackagePath:/Game/TEST/BP_Actor.BP_Actor
  • LongPackageName:/Game/TEST/BP_Actor
  • AssetName: BP_Actor

UE4:获取所有支持的平台

在ModuleTargetPlatform中可以获取:

1
TArray<ITargetPlatform*> Platforms = GetTargetPlatformManager()->GetTargetPlatforms();

但是注意,TargetPlatform是属于Developer的模块,不要在Runtime的模块中使用,否则会打包失败。

所以用宏简单裹了一下:

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
TArray<FString> UFlibAssetManageHelper::GetAllTargetPlatform()
{
#ifdef __DEVELOPER_MODE__
TArray<ITargetPlatform*> Platforms = GetTargetPlatformManager()->GetTargetPlatforms();
TArray<FString> result;

for (const auto& PlatformItem : Platforms)
{
result.Add(PlatformItem->PlatformName());
}

#else
TArray<FString> result = {
"AllDesktop",
"MacClient",
"MacNoEditor",
"MacServer",
"Mac",
"WindowsClient",
"WindowsNoEditor",
"WindowsServer",
"Windows",
"Android",
"Android_ASTC",
"Android_ATC",
"Android_DXT",
"Android_ETC1",
"Android_ETC1a",
"Android_ETC2",
"Android_PVRTC",
"AndroidClient",
"Android_ASTCClient",
"Android_ATCClient",
"Android_DXTClient",
"Android_ETC1Client",
"Android_ETC1aClient",
"Android_ETC2Client",
"Android_PVRTCClient",
"Android_Multi",
"Android_MultiClient",
"HTML5",
"IOSClient",
"IOS",
"TVOSClient",
"TVOS",
"LinuxClient",
"LinuxNoEditor",
"LinuxServer",
"Linux",
"Lumin",
"LuminClient"
};

#endif
return result;
}

UE4:递归扫描目录

与直接使用IFileManager::Get().FindFiles不同,IFileManager::Get().FindFiles只能由获取指定目录下的所有文件而无法递归扫描,可以使用IFileManager::Get().IterateDirectoryRecursively来解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class FFillArrayDirectoryVisitor : public IPlatformFile::FDirectoryVisitor
{
public:
virtual bool Visit(const TCHAR* FilenameOrDirectory, bool bIsDirectory) override
{
if (bIsDirectory)
{
Directories.Add(FilenameOrDirectory);
}
else
{
Files.Add(FilenameOrDirectory);
}
return true;
}

TArray<FString> Directories;
TArray<FString> Files;
};
// usage
FFillArrayDirectoryVisitor FileVisitor;
IFileManager::Get().IterateDirectoryRecursively(*InStartDir, FileVisitor);

其实就是要创建一个继承自IPlatformFile::FDirectoryVisitor的筛选类。

UE4:获取系统环境变量

可以使用FPlatformMisc::GetEnvironmentVariable来拿:

1
FString FindEnvGitPath = FPlatformMisc::GetEnvironmentVariable(TEXT("GIT_PATH"));

UE4: 运行时获取git diff的内容

做热更新有用到:

UE4: 获取Asset的依赖关系

在UE中想要获取一个资源对其他资源的依赖关系可以通过AsserRegistryModule来拿:

1
2
3
4
5
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
FStringAssetReference InAssetRef = TEXT("/Game/Pak/Cube.Cube");
FString InTargetLongPackageName = InAssetRef.GetLongPackageName();

bool bSuccessed = AssetRegistryModule.Get().GetDependencies(FName(*InTargetLongPackageName), local_Dependencies, EAssetRegistryDependencyType::Packages);

完整的函数如下:

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
void UFlibAssetManageHelper::GetAssetDependencies(const FString& InAsset, FAssetDependenciesInfo& OutDependInfo)
{
if (InAsset.IsEmpty())
return;

FStringAssetReference AssetRef = FStringAssetReference(InAsset);
if (!AssetRef.IsValid())
return;
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));

FStringAssetReference InAssetRef = InAsset;
FString TargetLongPackageName = InAssetRef.GetLongPackageName();
UE_LOG(LogTemp, Log, TEXT("TargetLongPackageName is %s."), *TargetLongPackageName);
if (FPackageName::DoesPackageExist(TargetLongPackageName))
{
{
TArray<FAssetData> AssetDataList;
bool bResault = AssetRegistryModule.Get().GetAssetsByPackageName(FName(*TargetLongPackageName), AssetDataList);
if (!bResault || !AssetDataList.Num())
{
UE_LOG(LogTemp, Error, TEXT("Faild to Parser AssetData of %s, please check."), *TargetLongPackageName);
return;
}
if (AssetDataList.Num() > 1)
{
UE_LOG(LogTemp, Warning, TEXT("Got mulitple AssetData of %s,please check."), *TargetLongPackageName);
}
}
UFlibAssetManageHelper::GatherAssetDependicesInfoRecursively(AssetRegistryModule, TargetLongPackageName, OutDependInfo.InContent, OutDependInfo.InOther);
}

}

可以写个递归获取的函数:

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
void UFlibAssetManageHelper::GatherAssetDependicesInfoRecursively(
FAssetRegistryModule& InAssetRegistryModule,
const FString& InTargetLongPackageName,
TArray<FString>& OutDependInContent,
TArray<FString>& OutDependInOther
)
{
TArray<FName> local_Dependencies;
bool bGetDependenciesSuccess = InAssetRegistryModule.Get().GetDependencies(FName(*InTargetLongPackageName), local_Dependencies, EAssetRegistryDependencyType::Packages);
if (bGetDependenciesSuccess)
{
for (auto &DependItem : local_Dependencies)
{
FString LongDependentPackageName = DependItem.ToString();
if (LongDependentPackageName.StartsWith(TEXT("/Game")))
{
if (OutDependInContent.Find(LongDependentPackageName) == INDEX_NONE)
{
OutDependInContent.Add(LongDependentPackageName);
GatherAssetDependicesInfoRecursively(InAssetRegistryModule, LongDependentPackageName, OutDependInContent, OutDependInOther);

}
}
else
{
if (OutDependInOther.Find(LongDependentPackageName) == INDEX_NONE)
{
OutDependInOther.Add(LongDependentPackageName);
GatherAssetDependicesInfoRecursively(InAssetRegistryModule, LongDependentPackageName, OutDependInContent, OutDependInOther);

}
}
}
}
}

使用方法:

运行结果:

UE4: Android屏幕方向

Project Setting-Platforms-Android-APK Packageing-Orientation

UE4: SoftClassReference

在UE中可以使用SoftClassReference保持资源的相对引用,其存储的是资源的路径而不是直接对类的引用,如果直接使用UClass是硬引用,如果需要动态加载某些资源,如果之前使用的是硬引用则会Crash。

  • /Game代表工程文件夹的Content目录
  • /Engine代表引擎目录下的Content目录
  • C++类的资源路径是/Script/MODULE_NAME.CLASS_NAME

  1. AActor的SoftrClassReference的路径是:/Script/Engine.Actor
  2. AActorController的SoftClassReference的路径是/Script/AIModule.AIController
  • BP类的资源路径和C++不同,是资源相对于Content的路径+BP_CLASS_NAME_C

如:有一个蓝图Content/Pak/Cube.uasset,其的SoftClassReference路径为/Game/Pak/Cube.Cube_C

UE4: Http下载文件

可以使用UE4的HTTP模块使用GET方法来从网络获取文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// FlibHttpHeler.h
#pragma once

#include "CoreMinimal.h"
#include "IHttpRequest.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "FlibHttpHelper.generated.h"

DECLARE_DYNAMIC_DELEGATE_OneParam(FOnRequestSuccessed,const TArray<uint8>&,ResponseContent);
DECLARE_DYNAMIC_DELEGATE_TwoParams(FOnRequestFailed, FString, ErrorText, int32, ErrorCode);

UCLASS()
class GWORLD_API UFlibHttpHelper : public UBlueprintFunctionLibrary
{
GENERATED_BODY()

UFUNCTION(BlueprintCallable)
static void HttpDownloadRequest(const FString& URL, FOnRequestSuccessed OnSuccessed, FOnRequestFailed OnFaild);

static void OnRequestContentReady(FHttpRequestPtr Request,FHttpResponsePtr Response,bool Successed,FOnRequestSuccessed OnSuccessed, FOnRequestFailed OnFaild);

};
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
// FlibHttpHelper.cpp
#include "FlibHttpHelper.h"
#include "HttpModule.h"
#include "IHttpRequest.h"
#include "IHttpResponse.h"

void UFlibHttpHelper::HttpDownloadRequest(const FString& URL, FOnRequestSuccessed OnSuccessed, FOnRequestFailed OnFaild)
{
TSharedRef<class IHttpRequest> HttpRequest = FHttpModule::Get().CreateRequest();
HttpRequest->OnProcessRequestComplete().BindStatic(UFlibHttpHelper::OnRequestContentReady, OnSuccessed, OnFaild);
HttpRequest->SetURL(*URL);
HttpRequest->SetVerb(TEXT("Get"));
HttpRequest->ProcessRequest();
}

void UFlibHttpHelper::OnRequestContentReady(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Successed, FOnRequestSuccessed OnSuccessed, FOnRequestFailed OnFaild)
{
if (!Successed || !Response.IsValid())
{
OnFaild.ExecuteIfBound(TEXT("Faild"), -1);
return;
}
int32 ResponseCode = Response->GetResponseCode();
if (!EHttpResponseCodes::IsOk(ResponseCode))
{
OnFaild.ExecuteIfBound(FString::Printf(TEXT("HttpDownloadRequest faild, Respose Code is %d."),ResponseCode), ResponseCode);
return;
}
TArray<uint8> ResponseContent = Response->GetContent();
OnSuccessed.ExecuteIfBound(ResponseContent);
return;

}

UE4:Mount pak in Runtime

前面的笔记中提到,UE的项目打包后会自动加载三个路径下的Paks/下的所有Pak文件,为了热更新的需求,需要在运行时自己指定加载Pak,翻了一下代码,可以写了个挂载的函数。

注意!注意!注意!在编辑器模式下运行无作用,没有任何逻辑,因为编辑器模式也不需要加载Pak,引擎里部分逻辑在编辑器模式下不执行,如果非要在编辑器下Mount引擎会Crash,以Standalone模式运行也一样,都属于编辑器模式。

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
bool MountPak(const FString& PakPath, int32 PakOrder, const FString& InMountPoint)
{
bool bMounted = false;
#if !WITH_EDITOR
FPakPlatformFile* PakFileMgr=(FPakPlatformFile*)FPlatformFileManager::Get().GetPlatformFile(FPakPlatformFile::GetTypeName());
if (!PakFileMgr)
{
UE_LOG(LogTemp, Log, TEXT("GetPlatformFile(TEXT(\"PakFile\") is NULL"));
return false;
}

PakOrder = FMath::Max(0, PakOrder);

if (FPaths::FileExists(PakPath) && FPaths::GetExtension(PakPath) == TEXT("pak"))
{
const TCHAR* MountPount = InMountPoint.GetCharArray().GetData();
if (PakFileMgr->Mount(*PakPath, PakOrder,MountPount))
{
UE_LOG(LogTemp, Log, TEXT("Mounted = %s, Order = %d, MountPoint = %s"), *PakPath, PakOrder, !MountPount ? TEXT("(NULL)") : MountPount);
bMounted = true;
}
else {
UE_LOG(LogTemp, Error, TEXT("Faild to mount pak = %s"), *PakPath);
bMounted = false;
}
}

#endif
return bMounted;
}

UE4:Patch的挂载和资源加载

这两天在看UE打包Patch作为热更的方案,UE打包Patch的逻辑是这样的:

  1. 如果在版本1.0中,场景Scene01中对资源A有直接引用(放在场景中),在修改了A资源之后,打包Patch0.1,想要让场景Scene01中资源A的改动也生效,则需要把Scene01也需要打到Patch中,因为是直接放置在场景中的,场景记录了该资源A的引用信息,这个不会随着资源A的更新而更新,加载时会产生AsyncLoading.cpp中的报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
[2019.11.13-06.03.27:766][ 68]LogLinker: Warning: The file '../../../GWorld/Content/PAK/Cube.uasset' contains unrecognizable data, check that it is of the expected type.
[2019.11.13-06.03.27:770][ 68]LogStreaming: Error: ****DumpDependencies [Dependencies]:
[2019.11.13-06.03.27:771][ 68]LogStreaming: Error: Export 8 /Game/Map2.Map2:PersistentLevel.Cube_2
[2019.11.13-06.03.27:772][ 68]LogStreaming: Error: Linker is ../../../GWorld/Content/Map2.umap
[2019.11.13-06.03.27:774][ 68]LogStreaming: Error: Dep C_BEFORE_S Export 38 /Game/Map2.Map2:PersistentLevel.Cube_2.Cube (class StaticMeshComponent)
[2019.11.13-06.03.27:774][ 68]LogStreaming: Error: Dep C_BEFORE_S Export 29 /Game/Map2.Map2:PersistentLevel.Cube_2.DefaultSceneRoot (class SceneComponent)
[2019.11.13-06.03.27:775][ 68]LogStreaming: Error: Dep S_BEFORE_C Import 3 /Game/PAK/Cube.Cube_C
[2019.11.13-06.03.27:776][ 68]LogStreaming: Error: Dep S_BEFORE_C Import 42 /Game/PAK/Cube.Default__Cube_C
[2019.11.13-06.03.27:776][ 68]LogStreaming: Error: Dep C_BEFORE_C Export 23 /Game/Map2.Map2:PersistentLevel (class Level)
[2019.11.13-06.03.27:777][ 68]LogStreaming: Error: Missing Dependency, request for /Game/PAK/Cube.Cube_C but it hasn't been created yet.
[2019.11.13-06.03.27:778][ 68]LogStreaming: Error: Could not find class Cube_C to create Cube_2
[2019.11.13-06.03.27:778][ 68]LogStreaming: Error: Could not find outer Cube_2 to create DefaultSceneRoot
[2019.11.13-06.03.27:779][ 68]LogStreaming: Error: Could not find outer Cube_2 to create Cube

解决这个问题的办法是通过SoftClassReference动态加载资源,比如直接SpawnActorFromClass直接指定Class为具体的类也是会产生上面的错误,但是用SoftClassReference可以避免这个错误(因为SoftClassReference本质就是存了个路径):

  1. 如果在版本1.0中,资源A引用了其他资源B,在Patch1.0中将资源A中引用的B换为了资源C,则该Patch的pak中会有资源A/B/C三个,因为资源的引用关系变了,但是如果不改动引用关系只是改动B中的信息,然后只Patch资源B是可以的。

直接使用Unreal.pakCooked的资源打包为pak文件需要注意一点:修改Mount的路径。
如果说直接使用下列命令:

1
$ UnrealPak.exe New_0_P.pak -create=D:\GWorldClient\Saved\Cooked\WindowsNoEditor\GWorld\Content\PAK

则打出来的pak的Mount路径为:

1
D:\GWorldClient\Saved\Cooked\WindowsNoEditor\GWorld\Content\PAK

而查看使用引擎打包的Pak和patch的pak都是相对路径的,这个性格对路径就是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
D:\GWorldPackage\WindowsNoEditor\GWorld\Content\Paks>UnrealPak.exe GWorld-WindowsNoEditor.pak -list
LogPaths: Warning: No paths for game localization data were specifed in the game configuration.
LogPakFile: Display: Using command line for crypto configuration
LogPakFile: Display: Added 0 entries to add to pak file.
LogPakFile: Display: Mount point ../../../
LogPakFile: Display: "Engine/Content/BasicShapes/BasicShapeMaterial.uasset" offset: 0, size: 749 bytes, sha1: E028879C8856192DC648EA4C422C145E38951DA9, compression: Zlib.
LogPakFile: Display: "Engine/Content/EditorMaterials/PreviewShadowIndicator.uasset" offset: 819, size: 496 bytes, sha1: 80BCDD2FF2674FED35763CD6A8C93C5A0EC27442, compression: Zlib.
// ....
LogPakFile: Display: "GWorld/AssetRegistry.bin" offset: 16928768, size: 2759 bytes, sha1: 35E6A5C7667C23FDDFD1F3BDC5535FA4E3BFF24F, compression: Zlib.
LogPakFile: Display: "GWorld/Config/DefaultEngine.ini" offset: 16932864, size: 2378 bytes, sha1: 7E33CA6CC805CC1A6E2FF84EF3010CEACBF99E04, compression: Zlib.
LogPakFile: Display: "GWorld/Config/DefaultGame.ini" offset: 16935312, size: 523 bytes, sha1: DDD788C5E2C9446AB11E2C8BC7D83EA24CB85AA9, compression: Zlib.
LogPakFile: Display: "GWorld/Config/DefaultInput.ini" offset: 16935905, size: 753 bytes, sha1: 6C03DCDAE9605BEB9CB2EB250631D6AA2C2A4336, compression: Zlib.
LogPakFile: Display: "GWorld/Content/Map2.uexp" offset: 16936960, size: 339962 bytes, sha1: 6605D83E8355CC2B4D20DE31C3D6182324BA556C, compression: Zlib.
LogPakFile: Display: "GWorld/Content/Map2.umap" offset: 17278976, size: 5541 bytes, sha1: A400A39FD85529E832750CB481D9F845E4C4A74B, compression: Zlib.
LogPakFile: Display: "GWorld/Content/Map2_BuiltData.uasset" offset: 17285120, size: 795 bytes, sha1: 1388AE2CCFA55587DD0A3120CA9679AAF8FC9336, compression: Zlib.
LogPakFile: Display: "GWorld/Content/Map2_BuiltData.ubulk" offset: 17287168, size: 8637 bytes, sha1: 7D8E7AE0D100415553DF6F222B9C35EBACF4BE28, compression: Zlib.
LogPakFile: Display: "GWorld/Content/Map2_BuiltData.uexp" offset: 17297408, size: 586603 bytes, sha1: 3B776E46005F6A4BEECA505674ED7FB0B8C432D6, compression: Zlib.
// ...
LogPakFile: Display: "GWorld/Content/ShaderArchive-GWorld-PCD3D_SM5.ushaderbytecode" offset: 23377920, size: 1207165 bytes, sha1: 44474138CD033FEF89EDFA86A480E569615A8850, compression: None.
LogPakFile: Display: "GWorld/GWorld.uproject" offset: 24585135, size: 323 bytes, sha1: D2E183A541A10DEEC5C87533400FFC0E89CA3B83, compression: Zlib.
LogPakFile: Display: "GWorld/Plugins/GWorld.upluginmanifest" offset: 24586240, size: 5022 bytes, sha1: CFDB721323DABBA1C7C7ABE6CB967852EA2AC23B, compression: Zlib.
LogPakFile: Display: "GWorld/Plugins/LowEntryExtStdLib/LowEntryExtStdLib.uplugin" offset: 24591332, size: 434 bytes, sha1: 5CD75BB5212731BEA8E0A552C99B69B259FA489C, compression: Zlib.
LogPakFile: Display: "GWorld/Plugins/UIFramework/UIFramework.uplugin" offset: 24591836, size: 236 bytes, sha1: C9426AD575AA6ADECEF5C1AE79CF4A49304C3350, compression: Zlib.
LogPakFile: Display: "GWorld/Plugins/VaRestPlugin/VaRestPlugin.uplugin" offset: 24592384, size: 393 bytes, sha1: 28B9C2F3CEB767F4A8D5D466CB8C6EEF772CDA43, compression: Zlib.
LogPakFile: Display: 1257 files (24050765 bytes), (0 filtered bytes).
LogPakFile: Display: Unreal pak executed in 1.746391 seconds

解决方案是可以通过UnrealPak.exe-create来指定一个txt文件来指定某个资源的绝对路径换算为相对路径:

1
2
"D:\GWorldClient\Saved\Cooked\WindowsNoEditor\GWorld\Content\PAK\BasicShapeMaterial_3.uasset" "../../../GWorld/Content/PAK/BasicShapeMaterial_3.uasset" -compress
"D:\GWorldClient\Saved\Cooked\WindowsNoEditor\GWorld\Content\PAK\BasicShapeMaterial_3.uexp" "../../../GWorld/Content/PAK/BasicShapeMaterial_3.uexp" -compress

可以看作有数列的表,第一列是资源的绝对路径,第二列是该绝对路径资源对应的相对路径,后面是参数(可以是-compress/-encrypt),生成的代码在Source/Programs/AutomationTool/Script/CopyBuildToStagingDirectory.Automation.cs中。

1
2
3
4
5
6
7
8
9
10
11
12
D:\>UnrealPak.exe D:\NEW_6_P.pak -create="D:\GWorldClient\Saved\Cooked\WindowsNoEditor\GWorld\Content\New.txt"
LogPaths: Warning: No paths for game localization data were specifed in the game configuration.
LogPakFile: Display: Using command line for crypto configuration
LogPakFile: Display: Loading response file D:\GWorldClient\Saved\Cooked\WindowsNoEditor\GWorld\Content\New.txt
LogPakFile: Display: Added 2 entries to add to pak file.
LogPakFile: Display: Collecting files to add to pak file...
LogPakFile: Display: Collected 2 files in 0.00s.
LogPakFile: Display: Encrypting using embedded key
LogPakFile: Display: Added 2 files, 8549 bytes total, time 0.00s.
LogPakFile: Display: Compression summary: 13.27% of original size. Compressed Size 7981 bytes, Original Size 60159 bytes.
LogPakFile: Display: Encryption - DISABLED
LogPakFile: Display: Unreal pak executed in 0.010672 seconds

检查打包出来的New_6_P.pak

1
2
3
4
5
6
7
8
9
D:\>UnrealPak.exe NEW_6_P.pak -list
LogPaths: Warning: No paths for game localization data were specifed in the game configuration.
LogPakFile: Display: Using command line for crypto configuration
LogPakFile: Display: Added 0 entries to add to pak file.
LogPakFile: Display: Mount point ../../../GWorld/Content/PAK/
LogPakFile: Display: "BasicShapeMaterial_3.uasset" offset: 0, size: 753 bytes, sha1: 63E00757694E91070403390CFB808829E13D01FF, compression: Zlib.
LogPakFile: Display: "BasicShapeMaterial_3.uexp" offset: 823, size: 7228 bytes, sha1: BD3C0B302FC49790327CF804F2B644F01A14EFCD, compression: Zlib.
LogPakFile: Display: 2 files (7981 bytes), (0 filtered bytes).
LogPakFile: Display: Unreal pak executed in 0.001987 seconds

可以看到变成了相对路径了。

UE4:引擎启动时Pak的加载

当在UE的Project SettingProject-Packaging-UsePakFile启用时,会打包出来pak文件,以Windows平台为例,打包出来的pak路径为:

1
WindowsNoEditor/PROJECT_NAME/Content/Paks

该目录下的pak文件在游戏启动时会自动加载,在引擎的FEngineLoop::PreInit中调用LaunchCheckForFileOverride(LaunchEngineLoop.cpp)又调用ConditionallyCreateFileWrapper来加载PakFilePlatformFile,但是在ConditionallyCreateFileWrapper的代码中做了一层WrapperFile->ShouldBeUsed判断:

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
// Runtime/Launch/Private/LaunchEngineLoop.cpp
static IPlatformFile* ConditionallyCreateFileWrapper(const TCHAR* Name, IPlatformFile* CurrentPlatformFile, const TCHAR* CommandLine, bool* OutFailedToInitialize = nullptr, bool* bOutShouldBeUsed = nullptr )
{
if (OutFailedToInitialize)
{
*OutFailedToInitialize = false;
}
if ( bOutShouldBeUsed )
{
*bOutShouldBeUsed = false;
}
IPlatformFile* WrapperFile = FPlatformFileManager::Get().GetPlatformFile(Name);
if (WrapperFile != nullptr && WrapperFile->ShouldBeUsed(CurrentPlatformFile, CommandLine))
{
if ( bOutShouldBeUsed )
{
*bOutShouldBeUsed = true;
}
if (WrapperFile->Initialize(CurrentPlatformFile, CommandLine) == false)
{
if (OutFailedToInitialize)
{
*OutFailedToInitialize = true;
}
// Don't delete the platform file. It will be automatically deleted by its module.
WrapperFile = nullptr;
}
}
else
{
// Make sure it won't be used.
WrapperFile = nullptr;
}
return WrapperFile;
}

如果为false不会对该PlatformFile调用Initialize,而FPakPlatformFile的一些成员就是在这里被设置的。FPakPlatformFile::ShoubleBeUsed的定义如下(UE_4.22.3):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Runtime/PakFile/Private/IPlatformFilePak.cpp
bool FPakPlatformFile::ShouldBeUsed(IPlatformFile* Inner, const TCHAR* CmdLine) const
{
bool Result = false;
#if !WITH_EDITOR
if (!FParse::Param(CmdLine, TEXT("NoPak")))
{
TArray<FString> PakFolders;
GetPakFolders(CmdLine, PakFolders);
Result = CheckIfPakFilesExist(Inner, PakFolders);
}
#endif
return Result;
}

编辑器模式下直接就是false,即编辑器模式下不可以使用pak的mount操作,因为mount中需要用到LowerLevel成员,而该成员在FPakPlatformFile::Initialize中被设置,所以不可以调用mount,否则必Crash。

Mount所有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
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
// Runtime\PakFile\Private\IPlatformFilePak.cpp
bool FPakPlatformFile::Initialize(IPlatformFile* Inner, const TCHAR* CmdLine)
{
LLM_SCOPE(ELLMTag::FileSystem);
SCOPED_BOOT_TIMING("FPakPlatformFile::Initialize");
// Inner is required.
check(Inner != NULL);
LowerLevel = Inner;

#if EXCLUDE_NONPAK_UE_EXTENSIONS
// Extensions for file types that should only ever be in a pak file. Used to stop unnecessary access to the lower level platform file
ExcludedNonPakExtensions.Add(TEXT("uasset"));
ExcludedNonPakExtensions.Add(TEXT("umap"));
ExcludedNonPakExtensions.Add(TEXT("ubulk"));
ExcludedNonPakExtensions.Add(TEXT("uexp"));
#endif

#if DISABLE_NONUFS_INI_WHEN_COOKED
IniFileExtension = TEXT(".ini");
GameUserSettingsIniFilename = TEXT("GameUserSettings.ini");
#endif

// signed if we have keys, and are not running with fileopenlog (currently results in a deadlock).
bSigned = GetPakSigningKey().IsValid() && !FParse::Param(FCommandLine::Get(), TEXT("fileopenlog"));;

// Find and mount pak files from the specified directories.
TArray<FString> PakFolders;
GetPakFolders(FCommandLine::Get(), PakFolders);
MountAllPakFiles(PakFolders);

#if !UE_BUILD_SHIPPING
GPakExec = MakeUnique<FPakExec>(*this);
#endif // !UE_BUILD_SHIPPING

FCoreDelegates::OnMountAllPakFiles.BindRaw(this, &FPakPlatformFile::MountAllPakFiles);
FCoreDelegates::OnMountPak.BindRaw(this, &FPakPlatformFile::HandleMountPakDelegate);
FCoreDelegates::OnUnmountPak.BindRaw(this, &FPakPlatformFile::HandleUnmountPakDelegate);

#if !(IS_PROGRAM || WITH_EDITOR)
FCoreDelegates::OnFEngineLoopInitComplete.AddLambda([this] {
FPlatformMisc::LowLevelOutputDebugStringf(TEXT("Checking Pak Config"));
bool bUnloadPakEntryFilenamesIfPossible = false;
GConfig->GetBool(TEXT("Pak"), TEXT("UnloadPakEntryFilenamesIfPossible"), bUnloadPakEntryFilenamesIfPossible, GEngineIni);

if (bUnloadPakEntryFilenamesIfPossible)
{
// With [Pak] UnloadPakEntryFilenamesIfPossible enabled, [Pak] DirectoryRootsToKeepInMemoryWhenUnloadingPakEntryFilenames
// can contain pak entry directory wildcards of which the entire recursive directory structure of filenames underneath a
// matching wildcard will be kept.
//
// Example:
// [Pak]
// DirectoryRootsToKeepInMemoryWhenUnloadingPakEntryFilenames="*/Config/Tags/"
// +DirectoryRootsToKeepInMemoryWhenUnloadingPakEntryFilenames="*/Content/Localization/*"
TArray<FString> DirectoryRootsToKeep;
GConfig->GetArray(TEXT("Pak"), TEXT("DirectoryRootsToKeepInMemoryWhenUnloadingPakEntryFilenames"), DirectoryRootsToKeep, GEngineIni);

FPakPlatformFile* PakPlatformFile = (FPakPlatformFile*)(FPlatformFileManager::Get().FindPlatformFile(FPakPlatformFile::GetTypeName()));
PakPlatformFile->UnloadPakEntryFilenames(&DirectoryRootsToKeep);
}

bool bShrinkPakEntriesMemoryUsage = false;
GConfig->GetBool(TEXT("Pak"), TEXT("ShrinkPakEntriesMemoryUsage"), bShrinkPakEntriesMemoryUsage, GEngineIni);
if (bShrinkPakEntriesMemoryUsage)
{
FPakPlatformFile* PakPlatformFile = (FPakPlatformFile*)(FPlatformFileManager::Get().FindPlatformFile(FPakPlatformFile::GetTypeName()));
PakPlatformFile->ShrinkPakEntriesMemoryUsage();
}
});
#endif

return !!LowerLevel;
}

UE自动加载的pak路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Runtime\PakFile\Private\IPlatformFilePak.cpp
void FPakPlatformFile::GetPakFolders(const TCHAR* CmdLine, TArray<FString>& OutPakFolders)
{
#if !UE_BUILD_SHIPPING
// Command line folders
FString PakDirs;
if (FParse::Value(CmdLine, TEXT("-pakdir="), PakDirs))
{
TArray<FString> CmdLineFolders;
PakDirs.ParseIntoArray(CmdLineFolders, TEXT("*"), true);
OutPakFolders.Append(CmdLineFolders);
}
#endif

// @todo plugin urgent: Needs to handle plugin Pak directories, too
// Hardcoded locations
OutPakFolders.Add(FString::Printf(TEXT("%sPaks/"), *FPaths::ProjectContentDir()));
OutPakFolders.Add(FString::Printf(TEXT("%sPaks/"), *FPaths::ProjectSavedDir()));
OutPakFolders.Add(FString::Printf(TEXT("%sPaks/"), *FPaths::EngineContentDir()));
}

在非Shipping打包的时候可以通过才命令行加启动参数-pakdir来添加额外的pak路径。

引擎默认添加的路径为:

1
2
3
4
5
# relative to Project Path
Content/Paks/
Saved/Paks/
# relative to Engine Path
Content/Paks

之后又调用了FPakPlatformFile::MountAllPakFiles来把挂载所有pak(默认对pak的名字进行了个降序排序,但是这里的排序没用),在该函数中mount的时候会给不同的路径加载的pak设置不同的Order,其函数在:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Runtime\PakFile\Private\IPlatformFilePak.cpp
int32 FPakPlatformFile::GetPakOrderFromPakFilePath(const FString& PakFilePath)
{
if (PakFilePath.StartsWith(FString::Printf(TEXT("%sPaks/%s-"), *FPaths::ProjectContentDir(), FApp::GetProjectName())))
{
return 4;
}
else if (PakFilePath.StartsWith(FPaths::ProjectContentDir()))
{
return 3;
}
else if (PakFilePath.StartsWith(FPaths::EngineContentDir()))
{
return 2;
}
else if (PakFilePath.StartsWith(FPaths::ProjectSavedDir()))
{
return 1;
}

return 0;
}

概括来说:

1
2
3
4
5
6
7
# relative to project path
4 Content/Paks/PROJECT_NAME-*.pak
3 Content/Paks/
1 Saved/Paks

# relative to engine path
2 Content/Paks/

可以看到Saved/Paks下的pak文件加载的优先级是最低的。

Mount的时候如果上述路径中有打出来Patch包,以_Num_P.pak结尾的文件,其中Num是数字,Patch包的优先级高于普通的pak,在IPlatformFilePak.cpp中默认给_P.pakPakOrder加了100,_P.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
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
// Runtime\PakFile\Private\IPlatformFilePak.cpp
bool FPakPlatformFile::Mount(const TCHAR* InPakFilename, uint32 PakOrder, const TCHAR* InPath /*= NULL*/)
{
bool bSuccess = false;
TSharedPtr<IFileHandle> PakHandle = MakeShareable(LowerLevel->OpenRead(InPakFilename));
if (PakHandle.IsValid())
{
FPakFile* Pak = new FPakFile(LowerLevel, InPakFilename, bSigned);
if (Pak->IsValid())
{
if (InPath != NULL)
{
Pak->SetMountPoint(InPath);
}
FString PakFilename = InPakFilename;
if (PakFilename.EndsWith(TEXT("_P.pak")))
{
// Prioritize based on the chunk version number
// Default to version 1 for single patch system
uint32 ChunkVersionNumber = 1;
FString StrippedPakFilename = PakFilename.LeftChop(6);
int32 VersionEndIndex = PakFilename.Find("_", ESearchCase::CaseSensitive, ESearchDir::FromEnd);
if (VersionEndIndex != INDEX_NONE && VersionEndIndex > 0)
{
int32 VersionStartIndex = PakFilename.Find("_", ESearchCase::CaseSensitive, ESearchDir::FromEnd, VersionEndIndex - 1);
if (VersionStartIndex != INDEX_NONE)
{
VersionStartIndex++;
FString VersionString = PakFilename.Mid(VersionStartIndex, VersionEndIndex - VersionStartIndex);
if (VersionString.IsNumeric())
{
int32 ChunkVersionSigned = FCString::Atoi(*VersionString);
if (ChunkVersionSigned >= 1)
{
// Increment by one so that the first patch file still gets more priority than the base pak file
ChunkVersionNumber = (uint32)ChunkVersionSigned + 1;
}
}
}
}
PakOrder += 100 * ChunkVersionNumber;
}
{
// Add new pak file
FScopeLock ScopedLock(&PakListCritical);
FPakListEntry Entry;
Entry.ReadOrder = PakOrder;
Entry.PakFile = Pak;
PakFiles.Add(Entry);
PakFiles.StableSort();
}
bSuccess = true;
}
else
&