一些篇幅短小待深挖的知识随笔罗列在这里。


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。

下面来具体分析一下实现。

替换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调用中,栈如下:

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

TryBindUnlua

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

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

Bind

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

  1. require对应的lua模块

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,还有
完整的调用栈:

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,不可以指定其他的名字。

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

UE4:为IOS添加Framawork

IOS上的Framework有点类似于静态链接库的意思,相当于把.a+.h+资源打包到一块的集合体。更具体的区别描述请看:iOS库 .a与.framework区别

在UE中以集成IOS上操作Keycahin的SSKeychain为例,在Module的build.cs中使用PublicAdditionalFrameworks来添加:

1
2
3
4
5
6
7
PublicAdditionalFrameworks.Add(
new Framework(
"SSKeychain",
"ThirdParty/IOS/SSKeychain.embeddedframework.zip",
"SSKeychain.framework/SSKeychain.bundle"
)
);

构造Framework的第一个参数是名字,第二个是framework的路径(相对于Module),第三个则是解压之后的Framework的bundle路径(如果framework没有bundle则可以忽略这个参数,而且就算有bundle,但是不写这第三个参数貌似也没什么问题)。

这个可以打开SSKeychain.embeddedframework.zip文件看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
SSKeychain.embeddedframework
└─SSKeychain.framework
│ Info.plist
│ SSKeychain

├─Headers
│ SSKeychain.h
│ SSKeychainQuery.h

├─Modules
module.modulemap

├─SSKeychain.bundle
│ └─en.lproj
│ SSKeychain.strings

└─_CodeSignature
CodeDirectory
CodeRequirements
CodeRequirements-1
CodeResources
CodeSignature

相对于.framework的路径,这个路径一定要填正确,不然是不能用的,因为打包时会把这个zip解压出来,然后拷贝到包体中,路径指定错了就无法拷贝了。

1
[2020.05.14-11.04.48:324][988]UATHelper: Packaging (iOS):     [2/183] sh Unzipping : /Users/zyhmac/UE4/Builds/ZHALIPENG/C/Users/imzlp/Documents/UnrealProjectSSD/MicroEnd_423/Plugins/PlatformUtils/Source/PlatformUtils/ThirdParty/IOS/SSKeychain.embeddedframework.zip -> /Users/zyhmac/UE4/Builds/ZHALIPENG/D/UnrealEngine/Epic/UE_4.23/Engine/Intermediate/UnzippedFrameworks/SSKeychain/SSKeychain.embeddedframework

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的参数来把文件写入、

在Windows上查看iOS设备log

Andorid的设备可以使用adb logcat来捕获log,在想要看iOS的log却十分麻烦,还要Mac。

但是经过一番查找,找到了一个工具,可以在Windows上实时地查看当前设备log:IOSLogInfo

下载之后解压,执行sdsiosloginfo.exe就可以看到类似logcat的日志输出了,如果装了Git bash环境也可以使用|来进行过滤。

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:枚举值与字符串的互相转换

有些需要序列化枚举值的需要,虽然我们可以通过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
template<typename ENUM_TYPE>
static FString GetEnumNameByValue(ENUM_TYPE InEnumValue, bool bFullName = false)
{
FString result;
{
FString TypeName;
FString ValueName;

UEnum* FoundEnum = StaticEnum<ENUM_TYPE>();
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
template<typename ENUM_TYPE>
static bool GetEnumValueByName(const FString& InEnumValueName, ENUM_TYPE& OutEnumValue)
{
bool bStatus = false;

UEnum* FoundEnum = StaticEnum<ENUM_TYPE>();
FString EnumTypeName = FoundEnum->CppType;
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:PC远程打包IOS

首先在MAC的系统偏好设置-共享中启用远程登录:

然后在Windows上对项目添导入mobileproversion和设置BundleNameBundle Identifier
之后继续往下拉找到IOS-Build下的Remote Build Options:

填入目标MAC机器的IP地址和用户名。

然后点击Generated SSH Key会弹出一个窗口:

按任意键继续。
会提示你输入一个密码,按照提示输入,之后会提示你输入MAC电脑的密码,输入之后会提示:

1
Enter passphrase (empty for no passphrase):

这是让你输入生成的ssh Key的密码,默认情况下可以不输,直接Enter就好。
按照提示一直Enter会提示你ssh key生成成功:

再继续会提示让你输入第一次设置的密码,和目标MAC机器的密码,执行完毕之后就会提示没有错误,就ok了:

生成的SSH Key的存放路径为:

1
C:\Users\imzlp\AppData\Roaming/Unreal Engine/UnrealBuildTool/SSHKeys/192.168.2.89/imzlp/RemoteToolChainPrivate.key

如果要将其共享给组内的其他成员,则把这个RemoteToolChainPrivate.key共享,然后让他们把IOS-Build-RemoteBuildOptions下的Override existing SSH Permissions file设置为RemoteToolChainPrivate.key的路径即可。

之后就可以像打包Windows或者在Win上打包IOS一样了:

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:地图的存储和加载

存储栈:

加载栈:

C++中delete[]的实现

注意:不同的编译器实现可能不一样,我使用的是Clang 7.0.0 x86_64-w64-windows-gnu

在C++中我们可以通过newnew[]在堆上分配内存,但是有没有考虑过下面这样的问题:

1
2
3
4
5
6
7
8
9
10
11
12
class IntClass{
public:
int v;
~IntClass(){}
};

int main()
{
IntClass *i = new IntClass[10];

delete[] i;
}

因为i就只是一个普通的指针,所以它没有任何的类型信息,那么delete[]的时候怎么知道要回收多少内存呢?

所以肯定是哪里存储了i的长度信息!祭出我们的IR代码:

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
; Function Attrs: noinline norecurse optnone uwtable
define dso_local i32 @main() #4 {
%1 = alloca i32, align 4
%2 = alloca %class.IntClass*, align 8
store i32 0, i32* %1, align 4
%3 = call i8* @_Znay(i64 48) #8
%4 = bitcast i8* %3 to i64*
store i64 10, i64* %4, align 8
%5 = getelementptr inbounds i8, i8* %3, i64 8
%6 = bitcast i8* %5 to %class.IntClass*
store %class.IntClass* %6, %class.IntClass** %2, align 8
%7 = load %class.IntClass*, %class.IntClass** %2, align 8
%8 = icmp eq %class.IntClass* %7, null
br i1 %8, label %21, label %9

; <label>:9: ; preds = %0
%10 = bitcast %class.IntClass* %7 to i8*
%11 = getelementptr inbounds i8, i8* %10, i64 -8
%12 = bitcast i8* %11 to i64*
%13 = load i64, i64* %12, align 4
%14 = getelementptr inbounds %class.IntClass, %class.IntClass* %7, i64 %13
%15 = icmp eq %class.IntClass* %7, %14
br i1 %15, label %20, label %16

; <label>:16: ; preds = %16, %9
%17 = phi %class.IntClass* [ %14, %9 ], [ %18, %16 ]
%18 = getelementptr inbounds %class.IntClass, %class.IntClass* %17, i64 -1
call void @_ZN8IntClassD2Ev(%class.IntClass* %18) #3
%19 = icmp eq %class.IntClass* %18, %7
br i1 %19, label %20, label %16

; <label>:20: ; preds = %16, %9
call void @_ZdaPv(i8* %11) #9
br label %21

; <label>:21: ; preds = %20, %0
ret i32 0
}

可以看到编译器给我们的new IntClass[10]通过@_Znay(i64 48)来分配了48个字节的内存!

但是按照sizeof(IntClass)*10来算其实之应该有40个字节的内存,多余的8个字节用来存储了数组的长度信息。

1
2
3
4
5
%3 = call i8* @_Znay(i64 48) #8
%4 = bitcast i8* %3 to i64*
store i64 10, i64* %4, align 8
%5 = getelementptr inbounds i8, i8* %3, i64 8
%6 = bitcast i8* %5 to %class.IntClass*

可以看到,它把数组的长度写入到了分配内存的前8个字节,在八个字节之后才可以分配真正的对象。

我们真正得到的i的地址就是偏移之后的,数组的长度写在第一个元素之前的64位内存中。

1
2
// 每个x代表一个byte,new IntClass[10]产生的内存布局
|xxxxxxxx|xxxx|xxxx|xxxx|xxxx|xxxx|xxxx|xxxx|xxxx|xxxx|xxxx|

既然知道了它存在哪里,所以我们可以修改它(在修改之前我们delete[] i;会调用10次析构函数):

1
2
3
4
IntClass *i = new IntClass[10];
int64_t *ArrayLength = (int64_t*)((char*)(i)-8);
*ArrayLength = 1;
delete[] i;

这样修改之后delete[] i;只会调用1次析构函数,也印证了我们猜想。

v2ray中大量的67错误

看到v2ray里有大量的下列错误:

1
2020/04/12 17:31:30 tcp:127.0.0.1:53920 rejected  v2ray.com/core/proxy/socks: unknown Socks version: 67

官方说这是因为应用里设置里http代理,排查了一下,这是因为在win10的设置里开启了代理,在Win10设置-网络-代理关掉系统代理即可。

Chrome阅读模式

chrome://flags/中有Enable Reader Mode,开启即可。

VS内存断点

在使用VS调试的时候有在有些情况下需要知道一些对象在什么时候被修改了,如果按照单步一点一点来调试的话很不方便,这时候就可以使用VS的Data Breakpoint来进行断点调试:

添加Data Breakpoint的操作为Debug-New BreakPoint-Data Breakpoint(或者在Breakpoint窗口下):

需要在Address处输入要断点的内存地址,可以输入对象名字使用取地址表达式(&Test),如果想要断点的对象不是全局对象可以通过直接输入内存地址。

获取一个对象的内存地址的方法为在Watch下添加一条该对象的取地址表达式(可以使用&ival或者&this->ival):

其中Value的就得到了该对象的内存地址。

拿到内存地址之后就可以填到Data BreakpointAddress中了,然后指定它的数据大小(可选1/2/4/8):

当该地址的数据被修改时会提示触发了内存断点:

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次幂的贴图大小会有性能问题。

UE4:IOS证书申请和打包

IOS证书申请

首先在Mac上导出一个证书:
打开软件钥匙串访问-证书助理-从证书颁发机构请求证书

选择存储到磁盘,会生成一个CertificateSigningRequest.certSigningRequest的文件。
然后登录Apple Developer,进入Account-Certificates

进去之后创建Apple Development或者iOS App Development

添加设备:

可以使用UE的IPhonePackager.exe来查看ios设备的uuid:

生成Provision:

生成之后要下载provision文件:

UE4打包IOS项目设置

最重要的是下面几点:

  1. 导入mobileprovision
  2. BundleNameBundle Identifier设置为在Apple开发者网站上设置的Bundle ID,格式为com.xxxxx.yyyyyy.

UE4:最多75根骨骼的限制

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

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

UE4:Mac和ios平台问题收录

安装CommandLinTool

Xcode's metal shader compiler was not found, verify Xcode has been installed on this Mac and that it has been selected in Xcode > Preferences > Locations > Command-line Tools.

相关问题:

离线安装XCodeCommand Line Tools for Xcode可以从苹果的开发者网站下载:More Downloads for Apple Developers

在安装完Command Line Tools之后,如果cook时还是提示这个错误,则需要执行下列命令(当然要首先确保/Library/Developer/CommandLineTools路径存在,一般Command Line Tools的默认安装路径是这个):

1
$ sudo xcode-select -s /Library/Developer/CommandLineTools

当设置CommandLineTools之后打包时可能会提示:

1
ERROR: Invalid SDK MacOSX.sdk, not found in /Library/Developer/CommandLineTools/Platforms/MacOSX.platform/Developer/SDKs

这是因为通过xcode-select设置为CommandLineTools之后,打包时找不到Xcode里的库了。
解决的办法是在CommandLineTools的目录下创建一个Xcode中的Platforms目录的软连接:

1
sudo ln -s /Applications/Xcode.app/Contents/Developer/Platforms /Library/Developer/CommandLineTools/Platforms

actool错误

1
2
UATHelper: Packaging (iOS):   xcrun: error: unable to find utility "actool", not a developer tool or in PATH
PackagingResults: Error: unable to find utility "actool", not a developer tool or in PATH

这是因为把CommandLinTool设置为默认的命令行工具之后,CommandLinTool/use/bin下并没有actool等工具。
这就是个十分坑爹的问题,用xcode作为默认的命令行工具导致Cook不过,用CommandLineTool又编译时有问题。
我的解办法是把/Applications/Xcode.app/Contents/Developer/usr/bin通过软连接方式链接到/Library/Developer/CommandLineTools/usr:

1
2
3
4
# 当然要先备份CommandLineTool/usr/bin
$ mv /Library/Developer/CommandLineTools/usr/bin /Library/Developer/CommandLineTools/usr/Command_bin
# 创建xcode的bin目录的软连接
$ sudo ln -s /Applications/Xcode.app/Contents/Developer/usr/bin /Library/Developer/CommandLineTools/usr/bin

MAC上的文件位置

MacOS上打开UE项目的Log位置为~/Library/Logs/Unreal Engine/ProjectNameLocating Project Logs

CookResults: Error: Package Native Shader Library failed for MacNoEditor.

这是因为Project Settings-Packing-Shared Material Native Library,关掉之后就可以了。

PS:我在UE的issus里看到了类似的bug提交,但是标记在4.18时就修复了:UE-49105,不知道为什么还有这个问题。

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

npm换源

把npm从官方换到淘宝源:

1
$ npm config set registry https://registry.npm.taobao.org

国内速度会快很多。

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标准支持度不同。

NDKclang version
r14bclang 3.8.275480 (based on LLVM 3.8.275480)
r17cclang version 6.0.2
r18bclang version 7.0.2
r20bclang version 8.0.7

UE4:根据枚举名字获取枚举值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// get enum value by name
{
FString EnumName = TEXT("ETargetPlatform::");
EnumName.Append(Platform->AsString());

UEnum* ETargetPlatformEnum = FindObject<UEnum>(ANY_PACKAGE, TEXT("ETargetPlatform"), 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;
}
}

三大运营商个人轨迹证明方法

一、电信手机用户证明方法

编辑短信“CXMYD#身份证号码后四位”到10001,授权回复Y后,实现“漫游地查询",可查询手机号近15日内的途径地信息。

二、联通手机用户证明方法

手机发送:“CXMYD#身份证后四位”至10010,查询近30天的全国漫游地信息,便于返工辅助排查。

三、移动用户证明方法

编写CXMYD,发送到10086,再依据回复短信输入身份证后四位,可查询过去一个月内去过的省和直辖市(无地市)。

每人免费一天查询10次。

npm install报错

在npm安装时遇到下列错误:

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
[email protected]:/mnt/c/Users/Administrator/Desktop/hexo# npm install hexo-cli -g
npm http GET https://registry.npmjs.org/hexo-cli
npm http GET https://registry.npmjs.org/hexo-cli
npm http GET https://registry.npmjs.org/hexo-cli
npm ERR! Error: CERT_UNTRUSTED
npm ERR! at SecurePair.<anonymous> (tls.js:1370:32)
npm ERR! at SecurePair.EventEmitter.emit (events.js:92:17)
npm ERR! at SecurePair.maybeInitFinished (tls.js:982:10)
npm ERR! at CleartextStream.read [as _read] (tls.js:469:13)
npm ERR! at CleartextStream.Readable.read (_stream_readable.js:320:10)
npm ERR! at EncryptedStream.write [as _write] (tls.js:366:25)
npm ERR! at doWrite (_stream_writable.js:223:10)
npm ERR! at writeOrBuffer (_stream_writable.js:213:5)
npm ERR! at EncryptedStream.Writable.write (_stream_writable.js:180:11)
npm ERR! at write (_stream_readable.js:583:24)
npm ERR! If you need help, you may report this log at:
npm ERR! <http://github.com/isaacs/npm/issues>
npm ERR! or email it to:
npm ERR! <[email protected]>

npm ERR! System Linux 3.4.0+
npm ERR! command "/usr/bin/nodejs" "/usr/bin/npm" "install" "hexo-cli" "-g"
npm ERR! cwd /mnt/c/Users/Administrator/Desktop/hexo
npm ERR! node -v v0.10.25
npm ERR! npm -v 1.3.10
npm ERR!
npm ERR! Additional logging details can be found in:
npm ERR! /mnt/c/Users/Administrator/Desktop/hexo/npm-debug.log
npm ERR! not ok code 0

解决办法,在bash执行下列命令:

1
$ npm config set strict-ssl false

之后重新执行安装即可。

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这样的形式。

Adb命令

首先先要下载Adb

Adb安装Apk

1
$ adb install APK_FILE_NAME.apk

Adb启动App

安装的renderdoccmd是没有桌面图标的,想要自己启动的话只能使用下列adb命令:

1
adb shell am start org.renderdoc.renderdoccmd.arm64/.Loader -e renderdoccmd "remoteserver"

adb启动App的shell命令模板:

1
adb shell am start PACKAGE_NAME/.ActivityName

这个方法需要知道App的包名和Activity名,包名很容易知道,但是Activity如果不知道可以通过下列操作获取:

首先使用一个反编译工具将apk解包(可以使用之前的apktools):

1
apktool.bat d -o ./renderdoccmd_arm64 org.renderdoc.renderdoccmd.arm64.apk

然后打开org.renderdoc.renderdoccmd.arm64目录下的AndroidManifest.xml文件,找到其中的Application项:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8" standalone="no"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.renderdoc.renderdoccmd.arm64" platformBuildVersionCode="26" platformBuildVersionName="8.0.0">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-feature android:glEsVersion="0x00030000" android:required="true"/>
<application android:debuggable="true" android:hasCode="true" android:icon="@drawable/icon" android:label="RenderDocCmd">
<activity android:configChanges="keyboardHidden|orientation" android:exported="true" android:label="RenderDoc" android:name=".Loader" android:screenOrientation="landscape">
<meta-data android:name="android.app.lib_name" android:value="renderdoccmd"/>
</activity>
</application>
</manifest>

其中有所有注册的Activity,没有有界面的apk只有一个Activity,所以上面的renderdoccmd的主Activity就是.Loader

如果说有界面的app,则会有多个,则可以从AndroidManifest.xml查找Category或者根据命名(名字带main的Activity)来判断哪个是主Activity。一般都是从lanucher开始,到main,或者有的进登陆界面。

PS:使用UE打包出游戏的主Activity是com.epicgames.ue4.SplashActivity,可以通过下列命令启动。

1
adb shell am start com.imzlp.GWorld/com.epicgames.ue4.SplashActivity

Adb传输文件

使用adb往手机传文件:

1
2
# adb push 1.0_Android_ETC2_P.pak /sdcard/Android/data/com.imzlp.TEST/files/UE4GameData/Mobile422/Mobile422/Saved/Paks
$ adb push FILE_NAME REMOATE_PATH

从手机传递到电脑:

1
2
# adb pull /sdcard/Android/data/com.imzlp.TEST/files/UE4GameData/Mobile422/Mobile422/Saved/Paks/1.0_Android_ETC2_P.pak A.Pak
$ adb pull REMOATE_FILE_PATH LOCAL_PATH

Adb:Logcat

使用logcast可以看到Android的设备Log信息。

1
$ adb logcat

会打印出当前设备的所有信息,但是我们调试App时不需要看到这么多,可以使用find进行筛选(注意大小写严格区分):

1
2
# adb logcat | find "GWorld"
$ adb logcat | find "KEY_WORD"

查看UE打包的APP所有的log可以筛选:

1
$ adb logcat | find "UE4"

如果运行的次数过多积累了大量的Log,可以使用清理:

1
adb logcat -c

Adb:从设备中提取已安装的APK

注意:执行下列命令时需要检查手机是否开放开发者权限,手机上提示的验证指纹信息要允许。

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
# 查看链接设备
$ adb devices
List of devices attached
b2fcxxxx unauthorized
# 列出手机中安装的所有app
$ adb shell pm list package
# 如果提示下问题,则需要执行adb kill-server
error: device unauthorized.
This adb servers $ADB_VENDOR_KEYS is not set
Try 'adb kill-server' if that seems wrong.
Otherwise check for a confirmation dialog on your device.
# 正常情况下会列出一堆这样的列表
C:\Users\imzlp>adb shell pm list package
package:com.miui.screenrecorder
package:com.amazon.mShop.android.shopping
package:com.mobisystems.office
package:com.weico.international
package:com.github.shadowsocks
package:com.android.cts.priv.ctsshim
package:com.sorcerer.sorcery.iconpack
package:com.google.android.youtube

# 找到指定app的的apk位置
$ adb shell pm path com.github.shadowsocks
package:/data/app/com.github.shadowsocks-iBtqbmLo8rYcq2BqFhJtsA==/base.apk
# 然后将该文件拉取到本地来即可
$ adb pull /data/app/com.github.shadowsocks-iBtqbmLo8rYcq2BqFhJtsA==/base.apk
/data/app/com.github.shadowsocks-iBtqbmLo8rYcq2BqFhJtsA==/...se.apk: 1 file pulled. 21.5 MB/s (4843324 bytes in 0.215s)

Adb刷入Recovery

下载Adb,然后根据具体情况使用下列命令(如果当前已经在bootloader就不需要执行第一条了)。

1
2
3
4
5
6
adb reboot bootloader
# 写入img到设备
fastboot flash recovery recovery.img
fastboot flash boot boot.img
# 引导img
fastboot boot recovery.img

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放到包路径下就可以了。

UE4:获取UEnum

在UE4.22+的版本中可以使用下列方法:

1
const UEnum* TypeEnum = StaticEnum<EnumType>();

在4.21及之前的版本就要麻烦一点:

1
const UEnum* TypeEnum = FindObject<UEnum>(ANY_PACKAGE, TEXT("EnumType"), true);

注意上面的TEXT("EnumType")其中要填想要获取的枚举类型名字。

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"
]
}

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

AndroidVersionSDK Version
Android 10(API level 29)
Android 9(API level 28)
Android 8.1(API level27)
Android 8.0(API level 26)
Android 7.1(API level 25)
Android 7.0(API level 24)
Android 6.0(API level 23)
Android 5.1(API level 22)
Android 5.0(API level 21)
Android 4.4W(API level 20)
Android 4.4(API level 19)
Android 4.3(API level 18)
Android 4.2(API level 17)
Android 4.1(API level 16)
Android 4.0.3(API level15)
Android 4.0(API level 14)
Android 3.2(API level 13)
Android 3.1(API level 12)
Android 3.0(API level 11)
Android 2.3.3(API level 10)
Android 2.3(API level 9)

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

UE4:Android项目设置

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

预处理使用##时需要注意编译器的不统一

下列代码在MSVC中编译的过:

1
2
3
4
5
6
7
8
9
10
11
12
13
#define DEFINE_GAME_EXTENSION_TYPE_VALUE_BY_KEY(ReturnType,InGetFuncName) \
bool GetGameExtension##ReturnType##ValueByKey(const FString& InKey,##ReturnType##& OutValue)\
{\
bool bLoadIniValueStatus = GConfig->##InGetFuncName##(\
GAME_EXTENSION_SETTINGS_SECTION,\
*InKey,\
OutValue,\
GAME_EXTENSION_SETTINGS_INI_FILE\
);\
return bLoadIniValueStatus;\
}

DEFINE_GAME_EXTENSION_TYPE_VALUE_BY_KEY(FString,GetString);

但是在GCC/Clang中会又如下错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Preprocess.cpp:16:1: error: pasting formed ',FString', an invalid preprocessing token
DEFINE_GAME_EXTENSION_TYPE_VALUE_BY_KEY(FString,GetString);
^
Preprocess.cpp:5:69: note: expanded from macro 'DEFINE_GAME_EXTENSION_TYPE_VALUE_BY_KEY'
bool GetGameExtension##ReturnType##ValueByKey(const FString& InKey,##ReturnType##& OutValue)\
^
Preprocess.cpp:16:1: error: pasting formed 'FString&', an invalid preprocessing token
Preprocess.cpp:5:81: note: expanded from macro 'DEFINE_GAME_EXTENSION_TYPE_VALUE_BY_KEY'
bool GetGameExtension##ReturnType##ValueByKey(const FString& InKey,##ReturnType##& OutValue)\
^
Preprocess.cpp:16:1: error: pasting formed '->GetString', an invalid preprocessing token
Preprocess.cpp:7:39: note: expanded from macro 'DEFINE_GAME_EXTENSION_TYPE_VALUE_BY_KEY'
bool bLoadIniValueStatus = GConfig->##InGetFuncName##(\
^
Preprocess.cpp:16:1: error: pasting formed 'GetString(', an invalid preprocessing token
Preprocess.cpp:7:54: note: expanded from macro 'DEFINE_GAME_EXTENSION_TYPE_VALUE_BY_KEY'
bool bLoadIniValueStatus = GConfig->##InGetFuncName##(\
^
4 errors generated.

这是由于GCC/Clang要求预处理之后的的结果必须是一个已定义的符号,MSVC在这方面和它们不一样,解决办法为在非拼接顺序字符的地方删掉##

1
2
3
4
5
6
7
8
9
10
11
12
#define DEFINE_GAME_EXTENSION_TYPE_VALUE_BY_KEY(ReturnType,InGetFuncName) \
bool GetGameExtension##ReturnType##ValueByKey(const FString& InKey, ReturnType& OutValue)\
{\
OutValue = ReturnType{};\
bool bLoadIniValueStatus = GConfig->InGetFuncName(\
GAME_EXTENSION_SETTINGS_SECTION,\
*InKey,\
OutValue,\
GAME_EXTENSION_SETTINGS_INI_FILE\
);\
return bLoadIniValueStatus;\
}

相关文章:

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

Chome79在Win10中崩溃

Chrome自动更新之后所有的页面都变成了这个样子:

解决办法是在chrome的快捷方式中添加-no-sandbox参数,但会提示您使用的是不受支持的命令行标记:-no-sandbox。稳定性和安全性会有所下降。,暂时我还没找到根本解决的办法,暂时先这样。

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
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又继承自TScriptInterface,所以动态代理可以绑定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;
}
};

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

重置Bitnami Gitlab管理员权限账户密码

安装的bitnami后gitlab的管理员账号密码默认为:

1
2
3
4
# account
user: Administrator
email: [email protected]
default password: 5iveL!fe

登录bitnami gitlab镜像的账户密码:

1
2
user: bitnami
password: bitnami

修改gitlab的默认管理员权限的账号密码可以在安装的bitnami镜像的环境中执行下列命令:

1
2
3
4
5
6
7
8
$ sudo gitlab-rails console production
irb(main):001:0> u = User.where(id:1).first
=> #<(User) id:1 @root>
irb(main):001:1> u.password = 'newpassword'
=> "newpassword"
irb(main):001:2> u.password_confirmation = 'newpassword'
=> "newpassword"
irb(main):001:2> u.save!

这样就修改完成了。

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
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
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
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
{
if (Pak->GetInfo().EncryptionKeyGuid.IsValid())
{
UE_LOG(LogPakFile, Log, TEXT("Deferring mount of pak \"%s\" until encryption key '%s' becomes available"), InPakFilename, *Pak->GetInfo().EncryptionKeyGuid.ToString());

check(!GetRegisteredEncryptionKeys().HasKey(Pak->GetInfo().EncryptionKeyGuid));
FPakListDeferredEntry& Entry = PendingEncryptedPakFiles[PendingEncryptedPakFiles.Add(FPakListDeferredEntry())];
Entry.Filename = InPakFilename;
Entry.Path = InPath;
Entry.ReadOrder = PakOrder;
Entry.EncryptionKeyGuid = Pak->GetInfo().EncryptionKeyGuid;
Entry.ChunkID = Pak->ChunkID;

delete Pak;
PakHandle.Reset();
return false;
}
else
{
UE_LOG(LogPakFile, Warning, TEXT("Failed to mount pak \"%s\", pak is invalid."), InPakFilename);
}
}
}
else
{
UE_LOG(LogPakFile, Warning, TEXT("Pak \"%s\" does not exist!"), InPakFilename);
}
return bSuccess;
}

另外,腾讯的和平精英的热更新的pak就是放在Saved/Paks里面。

而且,和平精英的apk的大小是1.6G,我下载完之后又热更新了1.2G左右的内容,总共占了3个多G...这样的apk里面obb的数据估计和外部的pak中的内容都是覆盖的,但是apk内容又没办法改,只能越更新越多了(但是可以在用户更新APK之后删除多余的pak)。

注:腾讯的吃鸡用sluaunreal,脚本的热更也是通过pak方式来加载的,这点在sluaunreal中有写:sluaunreal增量打包问题

如果从省空间的角度考虑,最好的方式就是基础apk+更新的方式修改游戏内容,但是玩家下载完apk之后还需要再更新可能会造成用户流失,这是个要考虑的问题。

UE4:自定义蓝图节点

在蓝图中类似于SpawnActor或者GetClassDefaults之类的函数,可以根据参数的变化来改变节点的信息(如增加引脚,修改返回值类型等等)。

其实在蓝图中这样的节点都是继承自UK2Node的类,每一个节点是一个类,如SpawnActor它就是定义在Editor/BlueprintEditor下的K2Node_SpawnActor类。
UK2Node提供了很多的方法供继承类重写,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 鼠标指到节点上的提示信息
virtual FText GetTooltipText()const;
// 节点在蓝图中显示的名字
virtual FText GetNodeTitle(ENodeTitleType::Type TitleType)const;
// 将节点添加至上下文菜单
virtual void GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegister)const;
// 设置节点在编辑器中右键菜单的分类
virtual FText GetMenuCategory()const;
// 在编译时扩展节点,可以添加或移除节点的Pin
// 在这个函数中需要绑定真正需要执行的函数,注意绑定的函数要是BlueprintCallable的
virtual void ExpandNode(FKismetCompilerContext & CompilerContext, UEdGraph * SourceGraph);
// 当节点中的Pin信息被改变
virtual void PinDefaultValueChanged(UEdGraphPin* ChangedPin);
// 给节点分配默认的Pin,如初始添加InExec和OutExec,声明在UEdGraphNode中
// 默认在ReallocatePinsDuringReconstruction中调用,也可以自己在ExpandNode中调用
virtual void AllocateDefaultPins();

上面列出的就是创建一个自定义的蓝图节点需要实现的最重要的几个函数。

当在蓝图中点击节点右键刷新的时候会调用到ExpandNode函数,如果在其中调用了AllocateDefaultPins要考虑从序列化中读取已有配置的问题。

一个例子,实现的效果为根据枚举值修改节点的参数类型:

代码放在了Gist:CustomK2Node.cpp

UE4:UENUM标记的反射

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

如果要获取一个UENMU的元数据可以通过StaticEnum<ETypeName>()或者通过UEnum* const MethodEnum = FindObjectChecked<UEnum>(ANY_PACKAGE, TEXT("ETypeName"), true);来拿。

UE4:Plugin加载时机太晚在蓝图中的错误

如果一个Runtime的模块具有在蓝图中可用的函数,加载时机一定要先于Default,可以是PreDefault,不然每次打开是都会有该模块的蓝图节点找不到的错误。

C++:static的链接问题

global/namespace scope的static函数/变量,仅在定义它的翻译单元(translation unit)可用,在其他的翻译单元不可用。

如有三个文件:

1
2
3
// file.h
#pragma once
static void func();
1
2
3
4
5
// file.cpp

void func()
{
}
1
2
3
4
5
6
7
// main.cpp
#include "file.h"

int main()
{
func();
}

使用下列命令编译:

1
2
# 注意此处有两个翻译单元 main.cpp/file.cpp
$ clang++ main.cpp file.cpp -o main.exe

会产生链接错误:

1
2
3
4
5
6
7
8
9
10
In file included from main.cpp:2:
./file.h:3:13: warning: function 'func' has internal linkage but is not defined [-Wundefined-internal]
static void func();
^
main.cpp:6:5: note: used here
func();
^
1 warning generated.
C:\Users\imzlp\AppData\Local\Temp\main-70cbfd.o:(.text+0x10): undefined reference to `func()'
clang++.exe: error: linker command failed with exit code 1 (use -v to see invocation)

这是因为func是个static函数,而且定义在file.cpp的翻译单元,因为static对象的internal linkage性质,而main.cpp的翻译单元不包含func的定义,所以会产生上面的链接错误。

知道了原因,那么解决办法有两个:

  1. 去掉func的static;
  2. 在所有用到func的翻译单元中包含func的定义。

C++:placement-new编译时的错误

1
'operator new' : function does not take 2 arguments

这个错误是因为没有包含new.h/new.

在命令行启动资源管理器打开文件夹

在Windows上的可以通过cmd命令来在资源管理器中打开一个文件夹,使用explorer命令:

1
explorer.exe /e,/root, F:\Test

注意,最后指定的路径必须是由\组成的路径,如果是反斜杠的则无法打开正确的目录(打开的是user/documents目录)。

在UE中使用时我写了一个路径转换的函数,把一个路径中的所有/替换为\\

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
FString ConvPath_Slash2BackSlash(const FString& InPath)
{
FString ResaultPath;
TArray<FString> OutArray;
InPath.ParseIntoArray(OutArray, TEXT("\\"));
if (OutArray.Num() == 1 && OutArray[0] == InPath)
{
InPath.ParseIntoArray(OutArray, TEXT("/"));
}
for (const auto& item : OutArray)
{
if (FPaths::DirectoryExists(ResaultPath + item))
{
ResaultPath.Append(item);
ResaultPath.Append(TEXT("\\"));
}
}
return ResaultPath;
}

UE4:NavigationSystem获取NavData

前面笔记中写到,UE4的寻路也是使用Recast来创建寻路网格的。所以如果要从ANavigationSystemV1获取到RecastdtNavMesh可以通过下列方法:

1
2
3
4
5
6
// 首先获取NavigationSystem
UNavigationSystemV1 NavSys=FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
// 通过NavigationSytstemV1可以获取到UE封装的管理NavData的类对象ANavigationData
ANavigationData NavData=NavSys->GetDefaultNavDataInstance(FNavigationSystem::DontCreate);
ARecastNavMesh* NavMesh=Cast<ARecastNavMesh>(NavData);
dtNavMesh* RecastNavMesh = NavMesh->GetRecastMesh();

注意:在UNavigationSystemV1中,通过GetMainNavData获取的对象与GetDefaultNavDataInstance是同一个。

然后ARecastNavMesh是继承自ANavigationData的,最后可以通过ARecastNavMesh::GetRecastMesh()获取到引擎中真正使用的dtNavMesh对象。

编译RecastNavigation

UE使用的是Recast创建的导航网格实现AI的寻路,相关的模块为Navmesh(修改之后的recastnavigation,RecaseDemo等也在此模块下)/NavigationSystem
如果想要在非UE的服务端使用,也可以自己编译recastnavigation,Github上的源码地址为:recastnavigation.

里面写了各个平台的编译流程,我在这里简单概述一下Windows下编译流程。

  1. 首先下载premake5,并将其添加到系统PATH路径
  2. clone RecastNavigation的代码
  3. 下载SDL2(选择Development Libraries),并将其解压到recastnavigation\RecastDemo\Contrib目录下,将文件夹改名为SDL,目录结构为:
1
2
3
4
5
6
D:\recastnavigation\RecastDemo\Contrib\SDL>tree /a
+---docs
+---include
\---lib
+---x64
\---x86
  1. recastnavigation\RecastDemo目录下执行命令premake5 vs2017(vs201x取决于你当前系统中安装的版本),它会在RecastDemo目录下创建build/vs2017目录,里面是VS项目的解决方案。
  2. 打开RecastDemo\Build\vs2017\recastnavigation.sln,编译即可。

  3. 编译出来RecastDemo.exe位置在RecastDemo\Bin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
C:\Users\imzlp\source\repos\recastnavigation\RecastDemo\Bin>tree /a /f
文件夹 PATH 列表
卷序列号为 000002D0 ECDB:6872
C:.
| .gitignore
| DroidSans.ttf
| RecastDemo.exe
| RecastDemo.pdb
| SDL2.dll
| Tests.exe
| Tests.pdb
|
+---Meshes
| dungeon.obj
| nav_test.obj
| undulating.obj
|
\---TestCases
movement_test.txt
nav_mesh_test.txt
raycast_test.txt

其中关键的几个文件:DroidSans.ttf/RecastDemo.exe/SDL2.dll/Meshs/

注意:必须把obj文件放到Meshs/目录下才可以被RecastDemo识别。

之后就可以打开RecastDemo.exe在默认提供的三个obj的模型上进行导航数据生成的测试了,通过Build生成,然后Save保存会在RecastDemo.exe所在的目录产生一个.bin文件,即使用recast生成的导航数据。

Windows tree命令

在Windows的cmd下可以使用tree命令来列出当前文件夹的目录结构。其命令格式为:

1
tree [drive][path] [/F] [/A]

/F:显示每个文件夹中文件的名称
/A:使用ASCII字符,而不使用扩展字符

For example:

1
2
3
4
5
6
D:\recastnavigation\RecastDemo\Contrib\SDL>tree /a
+---docs
+---include
\---lib
+---x64
\---x86
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
D:\recastnavigation\RecastDemo\Bin>tree /a /f
文件夹 PATH 列表
卷序列号为 000002D0 ECDB:6872
C:.
| .gitignore
| DroidSans.ttf
| RecastDemo.exe
| RecastDemo.pdb
| SDL2.dll
| Tests.exe
| Tests.pdb
|
+---Meshes
| dungeon.obj
| nav_test.obj
| undulating.obj
|
\---TestCases
movement_test.txt
nav_mesh_test.txt
raycast_test.txt

The Next Big Thing:C++20

这篇文章简单介绍了C++标准的历史和新标准的动向。

快速运行一个http文件下载服务

可以使用nodejsserve,安装完node之后在目录中执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ npx serve
WARNING: Checking for updates failed (use `--debug` to see full error)

┌──────────────────────────────────────────────────┐
│ │
│ Serving! │
│ │
│ - Local: http://localhost:5000 │
│ - On Your Network: http://192.168.2.83:5000 │
│ │
│ Copied local address to clipboard! │
│ │
└──────────────────────────────────────────────────┘

浏览器即可访问。

另一种方法是下载hfs,启动之后添加一个目录即可,很简单。

Windows编译最新版本的Lua

首先去lua.org下载最新的lua代码并解压(目前是v5.3.5):

1
2
curl -R -O http://www.lua.org/ftp/lua-5.3.5.tar.gz
tar zxf lua-5.3.5.tar.gz

在windows上启动MSVC的开发者工具命令:开始菜单-x64 Native Tools Command Prompt,将路径切换到Lua源码的Source目录吗,使用cd /d命令。
然后依次执行下面几条命令:

1
2
3
4
5
6
7
cl /MD /O2 /c /DLUA_BUILD_AS_DLL *.c
ren lua.obj lua.o
ren luac.obj luac.o
link /DLL /IMPLIB:lua535-import.lib /OUT:lua535.dll *.obj
lib /OUT:lua535.lib *.obj
link /OUT:luac.exe luac.o lua535.lib
link /OUT:lua.exe lua.o *.obj

最终编译出lua.exe/luac.exe/lua535.dll/lua535-import.lib/lua535.lib,这一堆的东西。

Windows环境变量长度超限制

今天往系统的PATH中添加一个新的路径,结果居然提示我太长不能保存。

这岂不是要了老命了,研究了一下,发现可以在系统的Path中添加一个另外的环境变量值。

即,现在系统中随便新建一个环境变量,如%ExternPATH%,然后将该环境变量添加至PATH中,ExternPATH中的值也都可以被查找到。

UE4 Plugin:ExportNav

最近有个需求需要导出UE里的寻路数据,研究了一下代码,写了一个插件:ue4-export-nav-data

UE4使用的是Recast游戏寻路引擎,所以UE的寻路数据数据是RecastNavMesh的数据格式。

可以使用Recast Navigation提供的RecastDemo来打开本插件导出的Obj文件(需要clone Recast的仓库自己编译),并构建出寻路的导航网格(使用Recast),之后可以使用Detour利用前面生成的导航网格做寻路操作。

附上我编译好的RecastDemo

What is this?

This is a Unreal Engine 4 Plugin that export ue4 navigation mesh data(recast mesh) to outside.

How do use?

  1. add the plugin to the project and enable it.
  2. Launch the Project in Editor, Click the ExportNav button.

  1. Open The Plugin Source/ExportNav/ThirdParty/RecastDemoBin
  2. copy .obj to RecastDemoBin/Meshes
  3. run RecastDemo.exe

UE4:解决模块间的名字冲突

在写hxhb/ue4-jwt插件的时候发现,在包含ThridParty/OpenSSL模块时,会有与Core模块下的名字冲突错误。

1
CompilerResultsLog:Error: Error \Engine\Source\ThirdParty\OpenSSL\1.0.2g\include\Win64\VS2015\openssl/ossl_typ.h(172) : error C2365: 'UI': redefinition; previous definition was 'namespace' CompilerResultsLog:Error: Error \engine\source\runtime\coreuobject\public\UObject/ObjectMacros.h(752) : note: see declaration of 'UI'

这是因为其他的模块定义了UI这个标识符,而在OpenSSL中也定义了一个同名的描述符的不同定于,这里一个是namespace一个是typedef,在加载外部的库的时候经常会有这样的问题。可以通过下列办法解决:

1
2
3
4
5
6
7
8
9
#define UI UI_ST
THIRD_PARTY_INCLUDES_START
#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <openssl/pem.h>
#include <openssl/ec.h>
#include <openssl/err.h>
THIRD_PARTY_INCLUDES_END
#undef UI

将具有标识符冲突的使用宏定义为其他的,然后将第三方库包含,最后在把宏给undef掉,确保不被其他的代码造成影响。

C++函数的后置&与&&修饰符

考虑下面例子代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename T>
class A{

public:
T& Get() &
{
return value;
}
T&& Get() &&
{
return std::move(value);
}
public:
T value;
};

这个A::Get函数后面的&&&修饰符是什么作用呢?

其实主要是因为这两个函数的原型只有返回值类型不同,但是,类成员函数的签名不包含返回值类型,所以他们会又重定义错误,而加这两个修饰的目的是让他们的签名不同。

C++类成员函数的签名组成:

  • name
  • parameter type list
  • class of witch the function is a member
  • cv-qualifiers(if any)
  • ref-qualifer(if any)

声明部分的语法描述在[IOS/IEC 14882:2014 § 8 Declarators]

UE4:CreateProc阻塞执行

在使用FPlatformProcess::CreateProc创建进程时是异步的,创建出来的进程和当前进程执行时没有顺序关系,有时我们需要等待进程执行完毕之后再执行后续逻辑,也就是阻塞行为。

因为FPlatformProcess::CreateProc本质也是使用Windows API中的CreateProcess创建的,所以使用WaitForSingleObject也可以在UE中使用,需要包含synchapi.h

1
2
3
FProcHandle ProtocProcIns=FPlatformProcess::CreateProc(*pProtocExe, *CommandParams, true, false, NULL, NULL, NULL, NULL, NULL);
WaitForSingleObject(ProtocProcIns.Get(), INFINITE);
CloseHandle(ProtocProcIns.Get());

WaitForSingleObject第一个参数需要接收PROCESS_INFORMATION中的hProcess参数,在UE中FProcHandleProcInfo.hProcess做了一层封装,通过FProcHandle对象上调用Get()即可获得。
WaitForSingleObject第二个参数是一个时间,为等待的时间,单位为毫秒,如果参数为0,则函数立即返回,如果为INFINITE则为无限等待下去直到程序退出。
如果等待超时,该函数返回WAIT_TIMEOUT。如果该函数失败,返回WAIT_FAILED

UE4:网络质量测试

可以使用几个命令参数模拟比较差的网络环境,从而测试UE联机的网络质量和bug检测。

SettingDescription
PktLagDelays the sending of a packet by the amount of time specified in milliseconds
PktLagVarianceProvides some randomness to the amount of time a packet is delayed, +/- the amount specified in milliseconds
PktLossSpecifies a percentage chance of an outbound packet being discarded to simulate packet loss
PktDupSpecifies a percentage chance to send a duplicate packet
PktOrderSends packets out of order when enabled (1 = enabled, 0 = disabled)

可以通过三种不同的方式来控制:命令行参数、控制台命令以及引擎ini配置。

在命令行启动:

1
SettingName=Value

在控制台设置:

1
Net SettingName=Value

以及在引擎配置(DefaultEngine.ini)中:

1
2
3
4
5
6
[PacketSimulationSettings]
PktLag=0
PktLagVariance=0
PktLoss=0
PktOrder=0
PktDup=0

Epic账号切换国家区域

Epic商城是锁国区的,有些内容国内账号没有,而且现在Epic商城要求付款账单地址信息与Epic账户信息一致,意味着,美元账户付不了国区。如果在Epic商城绑定支付信息和购买内容之后是没办法直接在网页上修改的,需要联系人工客服。

首先去Epic Games | Support Center提交联系客服的申请,问题描述就用英文说要修改账户的国家和地区从中国转为美国,之后客服会给你邮箱发邮件要求你提供几个信息。

  1. Email address associated with your Epic Games account
  2. Current Epic Games account display name
  3. First and last names of Epic Games account holder
  4. External account currently connected to your Epic Games account, if applicable.
  • This includes Google, Facebook, Twitch, PSN, Xbox, and Switch accounts.
  • Include the platform AND display name of any external accounts.

在邮件里直接回复这些信息之后,他们会给Epic账号绑定的邮箱发送一个验证链接,验证之后,再给他们回复邮件说验证成功,之后他们就会给你修改了。

注意:修改国家之后,之前账户绑定的支付信息都会清空,如Paypal账号之类的。

PS:Epic的客服联系渠道还真是超难找...

UE4集成Protobuf

我之前的文章Build protobuf with MSVC on Windows中介绍了在Windows上使用MSVC编译Protobuf,最近的项目中有用到Protobuf,就把Protobuf集成UE4做了一个插件,无需在系统和项目设置中添加任何环境变量以及文件包含,也不需要考虑protobuflib的链接,方便在项目中使用,并且对UE不兼容cc格式写了编辑器插件,.pb.cc都会被转换为pb.cpp

代码我放在了github上:hxhb/ue4-protobuf

下面是简单的使用介绍:

What is this?

This is an Unreal Engine 4 plugin that integrates Protobuf into the project without requiring you to add system PATH or anything else.

How do use?

  1. add the plugin to the project and enable it.
  2. add the following property to build.cs of the project :
1
2
3
4
PublicDependencyModuleNames.Add("Protobuf");
bEnableUndefinedIdentifierWarnings = false;
bUseRTTI = true;
bEnableExceptions = true;
  1. Create .proto file into project source code folder
  2. Launch the Project in Editor, Click the Protoc button.

Protobuf Version

  • Protobuf v3.9.1 build with MSVC 15 64bit (Visual Studio 2017).

protoc处理目录中的.proto文件

1
$ protoc --proto_path=PROTO_FILE_PATH PROTO_FILE --cpp_out=OUT_FILE_PATH PROTO_FILE_PATH\*.proto

如:

1
$ protoc --proto_path="d:\protoc" MyName.proto --cpp_out="d:\"

CMD添加环境变量

cmd查看系统的环境变量,在cmd窗口中输入set

将执行路径添加至系统的PATH路径,使用setx

1
setx Path "%PATH%;C:\BuildPath\Adb";

添加自定义的环境变量:

1
setx ANDROID_HOME "%current_dir_name%\android-sdk-windows"

cmd中获取当前的目录:

1
set "current_dir_name=%cd%"

附上添加AndroidSDK环境的bat:

1
2
3
4
5
6
7
8
9
10
11
12
@echo off
set "current_dir_name=%cd%"
setx JAVA_HOME "%current_dir_name%\jdk18077"
setx ANDROID_HOME "%current_dir_name%\android-sdk-windows"
setx ANDROID_NDK_ROOT "%current_dir_name%\android-ndk-r14b"
setx ANT_HOME "%current_dir_name%\apache-ant-1.8.2"
setx GRADLE_HOME "%current_dir_name%\gradle-4.1"
setx NDK_ROOT "%current_dir_name%\android-ndk-r14b"
setx NDKROOT "%current_dir_name%\android-ndk-r14b"
setx NVPACK_NDK_TOOL_VERSION "4.9"
setx NVPACK_NDK_VERSION "android-ndk-r14b"
setx NVPACK_ROOT "%current_dir_name%"

UE4: Was only expecting C++ files to have CachedCPPEnvironments!

写了个插件在UE中使用protobuf,在从proto文件生成.cc/.h之后编译报这个错误,分析了一下是UBT的代码文件类型不支持.cc

1
2
3
4
1>------ Build started: Project: GWorld, Configuration: Development x64 ------
1>Using 'git status' to determine working set for adaptive non-unity build (C:\Users\imzlp\Documents\Unreal Projects\GWorld).
1>UnrealBuildTool : error : Was only expecting C++ files to have CachedCPPEnvironments!
1>(see ../Programs/UnrealBuildTool/Log.txt for full exception trace)

在引擎中搜索代码,发现这个错误是在UnrealBuildTools\System\ActionGraph.csIsActionOutdated里的:

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
public bool IsActionOutdated(BuildConfiguration BuildConfiguration, UEBuildTarget Target, CPPHeaders Headers, Action RootAction, bool bIsAssemblingBuild, bool bNeedsFullCPPIncludeRescan, Dictionary<Action, bool> OutdatedActionDictionary, ActionHistory ActionHistory, Dictionary<UEBuildTarget, List<FileItem>> TargetToOutdatedPrerequisitesMap)
{
// ...
// @todo ubtmake: we may be scanning more files than we need to here -- indirectly outdated files are bIsOutdated=true by this point (for example basemost includes when deeper includes are dirty)
if (bIsOutdated && RootAction.ActionType == ActionType.Compile)// @todo ubtmake: Does this work with RC files? See above too.
{
Log.TraceVerbose("Outdated action: {0}", RootAction.StatusDescription);
foreach (FileItem PrerequisiteItem in RootAction.PrerequisiteItems)
{
if (PrerequisiteItem.CachedIncludePaths != null)
{
if (!IsCPPFile(PrerequisiteItem))
{
throw new BuildException("Was only expecting C++ files to have CachedCPPEnvironments!");
}
Log.TraceVerbose(" -> DEEP include scan: {0}", PrerequisiteItem.AbsolutePath);

List<FileItem> OutdatedPrerequisites;
if (!TargetToOutdatedPrerequisitesMap.TryGetValue(Target, out OutdatedPrerequisites))
{
OutdatedPrerequisites = new List<FileItem>();
TargetToOutdatedPrerequisitesMap.Add(Target, OutdatedPrerequisites);
}

OutdatedPrerequisites.Add(PrerequisiteItem);
}
else if (IsCPPImplementationFile(PrerequisiteItem) || IsCPPResourceFile(PrerequisiteItem))
{
Log.TraceVerbose(" -> WARNING: No CachedCPPEnvironment: {0}", PrerequisiteItem.AbsolutePath);
}
}
}
// ...
}

重点就是IsCPPFile这个函数,报这个错误是因为UBT检测到了项目中不能识别的文件。

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