PENDING WIKI

等待发布的 Wiki 页面,基于 Gist 可以方便地在线编辑内容。



离线渲染 Sequence 命令

在渲染的时候,虚幻引擎提供了基于命令行的接口,用户在命令行中指定项目的路径、地图、Sequencer,以及渲染参数(例如分辨率、质量等)后,虚幻引擎即可离屏渲染出图像。

1
2
3
4
5
6
7
8
9
10
11
UE4Editor.exe
DummyProject.uproject
/Game/NewMap
-game
-MovieSceneCaptureType="/Script/MovieSceneCapture.AutomatedLevelSequenceCapture"
-LevelSequence="/Game/NewLevelSequence"
-MovieFrameRate=30 -noloadingscreen
-resx=1920 -resy=1080 -forceres
-MovieFormat=PNG -MovieQuality=100
-notexturestreaming -MovieCinematicMode=yes
-MovieWarmUpFrames=60 -opengl

输出图像之后,可以使用 ffmpeg 进行合成,之前有一篇采集的文章:使用 Unreal Engine 4 采集 360° 全景视频

FDebugText

有些需求是在场景中打印不根据透视变化的调试信息,如 RecastNavMesh 的预览 TileLabel:

Runtime\NavigationSystem\Private\NavMesh\NavMeshRenderingComponent.cpp
1
2
3
4
if (bGatherTileLabels)
{
DebugLabels.Add(FDebugText(NavLocation.Location + NavMeshDrawOffset, FString::Printf(TEXT("(%d,%d:%d)"), X, Y, Layer)));
}

在以下代码中使用:

Runtime\NavigationSystem\Private\NavMesh\NavMeshRenderingComponent.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
#if WITH_RECAST && !UE_BUILD_SHIPPING && !UE_BUILD_TEST
void FNavMeshDebugDrawDelegateHelper::RegisterDebugDrawDelgate()
{
ensureMsgf(State != RegisteredState, TEXT("RegisterDebugDrawDelgate is already Registered!"));
if (State == InitializedState)
{
DebugTextDrawingDelegate = FDebugDrawDelegate::CreateRaw(this, &FNavMeshDebugDrawDelegateHelper::DrawDebugLabels);
DebugTextDrawingDelegateHandle = UDebugDrawService::Register(TEXT("Navigation"), DebugTextDrawingDelegate);
State = RegisteredState;
}
}

void FNavMeshDebugDrawDelegateHelper::UnregisterDebugDrawDelgate()
{
ensureMsgf(State != InitializedState, TEXT("UnegisterDebugDrawDelgate is in an invalid State: %i !"), State);
if (State == RegisteredState)
{
check(DebugTextDrawingDelegate.IsBound());
UDebugDrawService::Unregister(DebugTextDrawingDelegateHandle);
State = InitializedState;
}
}
#endif
// ...
#if WITH_RECAST && !UE_BUILD_SHIPPING && !UE_BUILD_TEST
void FNavMeshDebugDrawDelegateHelper::DrawDebugLabels(UCanvas* Canvas, APlayerController*)
{
if (!Canvas)
{
return;
}

const bool bVisible = (Canvas->SceneView && !!Canvas->SceneView->Family->EngineShowFlags.Navigation) || bForceRendering;
if (!bVisible || bNeedsNewData || DebugLabels.Num() == 0)
{
return;
}

const FColor OldDrawColor = Canvas->DrawColor;
Canvas->SetDrawColor(FColor::White);
const FSceneView* View = Canvas->SceneView;
UFont* Font = GEngine->GetSmallFont();
const FNavMeshSceneProxyData::FDebugText* DebugText = DebugLabels.GetData();
for (int32 Idx = 0; Idx < DebugLabels.Num(); ++Idx, ++DebugText)
{
if (View->ViewFrustum.IntersectSphere(DebugText->Location, 1.0f))
{
const FVector ScreenLoc = Canvas->Project(DebugText->Location);
Canvas->DrawText(Font, DebugText->Text, ScreenLoc.X, ScreenLoc.Y);
}
}

Canvas->SetDrawColor(OldDrawColor);
}
#endif

/Game/Map.Map:PersistentLevel.None

1
2
3
4
5
[2021.06.26-09.08.17:941][518]LogGarbage: Warning: Spent more than 30.00s on routing FinishDestroy to objects (objects in queue: 2)
[2021.06.26-09.08.17:941][518]LogGarbage: Warning: [0]: SphereComponent /Game/Map.Map:PersistentLevel.None.None, IsReadyForFinishDestroy: false
[2021.06.26-09.08.17:941][518]LogGarbage: Warning: [1]: SpectatorPawn /Game/Map.Map:PersistentLevel.None, IsReadyForFinishDestroy: false
[2021.06.26-09.08.17:941][518]LogGarbage: Warning: Spent to much time waiting for FinishDestroy for 2 object(s) (last object: SpectatorPawn /Game/Map.Map:PersistentLevel.None), check log for details
[2021.06.26-09.08.17:941][518]LogObj: Warning: UObject::AbortInsideMemberFunction called on object SpectatorPawn /Game/Map.Map:PersistentLevel.None.

出现这个报错是因为场景里有选择一个无效的场景对象:

这种形式的路径引用是选择场景中的对象:

1
/Game/Maps/VFXLight.VFXLight:PersistentLevel.DirectionalLight_1

如果有引用,但是场景中的对象没了,PersistentLevel.DirectionalLight_1这就是一个引用错误,可能会导致 Crash。

监听 Actor 创建事件

可以通过监听 UWorldOnActorSpawned实现:

1
2
3
UWorld* MyWorld = GetWorld();
FOnActorSpawned::FDelegate ActorSpawnedDelegate = FOnActorSpawned::FDelegate::CreateRaw(this, &FWaitForLoadingToFinish::OnActorSpawned);
FDelegateHandle ActorSpawnedDelegateHandle = MyWorld->AddOnActorSpawnedHandler(ActorSpawnedDelegate)

监听 Cook 完成事件

有些操作需要等待 Cook 的任务完成,可以通过监听 UPackage::PackageSavedEvent 实现:

1
UPackage::PackageSavedEvent.AddRaw(this,&FCookManager::OnPackageSavedEvent);

使用 GEditor->Save 进行 Cook 时即可在回调中监听:

1
2
3
4
5
6
7
8
GIsCookerLoadingPackage = true;
UPackage::PackageSavedEvent.AddLambda([PackageSavedCallback](const FString& InFilePath,UObject* Object){PackageSavedCallback(InFilePath);});

// UE_LOG(LogHotPatcherEditorHelper,Display,TEXT("Cook Assets:%s save to %s"),*Package->GetName(),*CookedSavePath);
FSavePackageResultStruct Result = GEditor->Save(Package, nullptr, CookedFlags, *CookedSavePath,
GError, nullptr, false, false, SaveFlags, Platform,
FDateTime::MinValue(), false, /*DiffMap*/ nullptr,CurrentPlatformPackageContext);
GIsCookerLoadingPackage = false;

地图的最大尺寸

UE 中支持的最大 World 尺寸通过WORLD_MAX 宏定义:Runtime/Engine/Public/EngineDefines.h#L27

Source/Runtime/Engine/Public/EngineDefines.h
1
2
3
4
5
6
/*-----------------------------------------------------------------------------
Size of the world.
-----------------------------------------------------------------------------*/
#define WORLD_MAX 2097152.0 /* Maximum size of the world */
#define HALF_WORLD_MAX (WORLD_MAX * 0.5) /* Half the maximum size of the world */
#define HALF_WORLD_MAX1 (HALF_WORLD_MAX - 1.0) /* Half the maximum size of the world minus one */

超出范围 Actor 就被销毁了:Runtime/Engine/Private/Components/InterpToMovementComponent.cpp#L394

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bool UInterpToMovementComponent::CheckStillInWorld()
{
// ....
// Check if box has poked outside the world
else if (UpdatedComponent && UpdatedComponent->IsRegistered())
{
const FBox& Box = UpdatedComponent->Bounds.GetBox();
if (Box.Min.X < -HALF_WORLD_MAX || Box.Max.X > HALF_WORLD_MAX ||
Box.Min.Y < -HALF_WORLD_MAX || Box.Max.Y > HALF_WORLD_MAX ||
Box.Min.Z < -HALF_WORLD_MAX || Box.Max.Z > HALF_WORLD_MAX)
{
UE_LOG(LogInterpToMovementComponent, Warning, TEXT("%s is outside the world bounds!"), *ActorOwner->GetName());
ActorOwner->OutsideWorldBounds();
// not safe to use physics or collision at this point
ActorOwner->SetActorEnableCollision(false);
FHitResult Hit(1.f);
StopSimulating(Hit);
return false;
}
}
return true;
}

UProperty 到 FProperty 的优化

从 4.25 开始 UProperty 正式修改为了 FProperty,避免了额外的 UProperty 的消耗。

官方介绍:

  • Loading performance, especially when loading a large number Blueprints
  • Garbage collection performance, especially when a project has a large number of Blueprints
  • Memory savings, tests in Fortnite show a memory savings of over 30 megabytes.
  • Performance improvements include:
  • UObject iteration (there are fewer objects to check)
  • FProperty casts are three times as fast as UObject casts.
  • FProperty iteration is almost twice as fast as UProperty iteration.

从 Target 获取编译器版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private static void CheckCompilerVersion(ref string Version, WindowsCompiler Compiler, string LongVersionName, string ShortVersionName)
{
try
{
if (Compiler == (WindowsCompiler)Enum.Parse(typeof(WindowsCompiler), LongVersionName))
Version = ShortVersionName;
}
catch{}
}

private string GetVisualStudioVersion()
{
string VSVersion = "vc150";
var Compiler = Target.WindowsPlatform.Compiler;
CheckCompilerVersion(ref VSVersion, Compiler, "VisualStudio2019", "vc160");
CheckCompilerVersion(ref VSVersion, Compiler, "VisualStudio2017", "vc150");
CheckCompilerVersion(ref VSVersion, Compiler, "VisualStudio2015", "vc140");
CheckCompilerVersion(ref VSVersion, Compiler, "VisualStudio2013", "vc120");
return VSVersion;
}

Localization Error

IOS 加载出现以下错误:

1
No paths for game localization data were specifed in the game configuration.

产生错误的代码为:

Paths.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const TArray<FString>& FPaths::GetGameLocalizationPaths()
{
FStaticData& StaticData = TLazySingleton<FStaticData>::Get();
if(!StaticData.bGameLocalizationPathsInitialized)
{
if(GConfig && GConfig->IsReadyForUse())
{
GConfig->GetArray(TEXT("Internationalization"), TEXT("LocalizationPaths"), StaticData.GameLocalizationPaths, GGameIni );
if(!StaticData.GameLocalizationPaths.Num()) // Failed to find localization path.
{
UE_LOG(LogPaths, Warning, TEXT("No paths for game localization data were specifed in the game configuration."));
}
StaticData.bGameLocalizationPathsInitialized = true;
}
}
return StaticData.GameLocalizationPaths;
}

会从 GGameIni 中读取(BaseGame.ini中已配置):

BaseGame.ini
1
2
[Internationalization]
+LocalizationPaths=%GAMEDIR%Content/Localization/Game

获取引用的对象

可以获取 UObject 引用的对象信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static int32 URamaStaticFunctionLib::GetObjReferenceCount(UObject* Obj, TArray<UObject*>* OutReferredToObjects = nullptr)
{
if(!Obj || !Obj->IsValidLowLevelFast())
{
return -1;
}

TArray<UObject*> ReferredToObjects; //req outer, ignore archetype, recursive, ignore transient
FReferenceFinder ObjectReferenceCollector(ReferredToObjects, Obj, false, true, true, false);
ObjectReferenceCollector.FindReferences(Obj);

if(OutReferredToObjects)
{
OutReferredToObjects->Append(ReferredToObjects);
}
return OutReferredToObjects.Num();
}

AutomationTool Project

可以在项目中创建一个 Automotion 的项目,类似 UGCExample 中的那样,在 Build/Script 下创建 UAT 项目:Build/Scripts

UPL 修改 gradle.properties

修改 Gradle 的堆大小:

1
2
3
<gradleProperties>
org.gradle.jvmargs=-Xmx9216M
</gradleProperties>

引擎中默认的为:UEDeployAndroid.cs#L3092

1
2
3
4
5
6
7
8
9
10
GradleProperties.AppendLine("org.gradle.daemon=false");
GradleProperties.AppendLine("org.gradle.jvmargs=-XX:MaxHeapSize=4096m -Xmx9216m");
GradleProperties.AppendLine("android.injected.testOnly=false");
GradleProperties.AppendLine(string.Format("COMPILE_SDK_VERSION={0}", CompileSDKVersion));
GradleProperties.AppendLine(string.Format("BUILD_TOOLS_VERSION={0}", BuildToolsVersion));
GradleProperties.AppendLine(string.Format("PACKAGE_NAME={0}", PackageName));
GradleProperties.AppendLine(string.Format("MIN_SDK_VERSION={0}", MinSDKVersion.ToString()));
GradleProperties.AppendLine(string.Format("TARGET_SDK_VERSION={0}", TargetSDKVersion.ToString()));
GradleProperties.AppendLine(string.Format("STORE_VERSION={0}", StoreVersion.ToString()));
GradleProperties.AppendLine(string.Format("VERSION_DISPLAY_NAME={0}", VersionDisplayName));

Timer 的回调中不能析构 handle

在 Timer 的回调里不能释放掉 handle 对象的内存,因为在回调之后,FTimeManager::Tick中在执行 Delegate 之后还有用到:

IniPlatformName

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

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

可以通过以下方式转换:

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

IOS 内存阈值

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

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

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

ensure

在 shipping 时不会 Break。

Runtime\Core\Public\Misc\AssertionMacros.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/**
* ensure() can be used to test for *non-fatal* errors at runtime
*
* Rather than crashing, an error report (with a full call stack) will be logged and submitted to the crash server.
* This is useful when you want runtime code verification but you're handling the error case anyway.
*
* Note: ensure() can be nested within conditionals!
*
* Example:
*
* if (ensure(InObject != nullptr))
* {
* InObject->Modify();
* }
*
* This code is safe to execute as the pointer dereference is wrapped in a non-nullptr conditional block, but
* you still want to find out if this ever happens so you can avoid side effects. Using ensure() here will
* force a crash report to be generated without crashing the application (and potentially causing editor
* users to lose unsaved work.)
*
* ensure() resolves to just evaluate the expression when DO_CHECK is 0 (typically shipping or test builds).
*
* By default a given call site will only print the callstack and submit the 'crash report' the first time an
* ensure is hit in a session; ensureAlways can be used instead if you want to handle every failure
*/

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

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

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

#else // DO_CHECK

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

#endif // DO_CHECK

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

自动化测试框架

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

Cooker 优化

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

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

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

Device Profile

在线迭代

RootComponent 警告

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

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

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

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

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

就像:

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

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

ConfigLayer

引擎 Config 的文件路径规则:

Runtime\Core\Private\Misc\ConfigCacheIni.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* Structure to define all the layers of the config system. Layers can be expanded by expansion files (NoRedist, etc), or by ini platform parents
* (coming soon from another branch)
*/
struct FConfigLayer
{
// Used by the editor to display in the ini-editor
const TCHAR* EditorName;
// Path to the ini file (with variables)
const TCHAR* Path;
// Path to the platform extension version
const TCHAR* PlatformExtensionPath;
// Special flag
EConfigLayerFlags Flag;

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

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

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

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

Mac 生成的 xocde 工程

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

Unreal Insight 默认端口

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

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

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

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

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

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

可以传递给 Unreal Insight 的参数:

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

加密 PakIndex 造成的无法挂载 shaderbytecode

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

运行时给 Enum 添加值

通过 UEnum 的 SetEnums 可以实现:

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

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

遍历 Enum 的所有值

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

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

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

IoStore 的生成分析

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

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

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

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

Programs\AutomationTool\Scripts\CopyBuildToStagingDirectory.Automation.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
private static bool ShouldCreateIoStoreContainerFiles(ProjectParams Params, Platform StageTargetPlatform)
{
if (Params.CookOnTheFly)
{
return false;
}

if (Params.SkipIoStore)
{
return false;
}

if (Params.IoStore)
{
return true;
}

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

return false;
}

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

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

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

}

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

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

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

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

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

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

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

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

IoStoreCommandlet 的调用方式:

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

最小执行命令:

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

IoStoreCommands.txt 的内容为:

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

创建 IO Store 的 Log:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
LogIoStore: Display: ==================== IoStore Utils ====================
LogIoStore: Display: Parsing crypto keys from a crypto key cache file 'C:\Users\lipengzha\Documents\Unreal Projects\ThirdPerson_UE5\Saved\Cooked\Windows\ThirdPerson_UE5\Metadata\Crypto.json'
LogIoStore: Display: Container signing - DISABLED
LogIoStore: Display: Directory index - ENABLED
LogIoStore: Display: Using memory mapping alignment '16384'
LogIoStore: Display: Using compression block size '65536'
LogIoStore: Display: Using compression block alignment '2048'
LogIoStore: Display: Using max partition size '0'
LogIoStore: Display: Using command list file: 'C:\Users\lipengzha\AppData\Roaming\Unreal Engine\AutomationTool\Logs\C+Program+Files+Epic+Games+UE_5.0ea\IoStoreCommands.txt'
LogIoStore: Display: Using target platform 'Windows'
LogIoStore: Display: Searching for cooked assets in folder 'C:\Users\lipengzha\Documents\Unreal Projects\ThirdPerson_UE5\Saved\Cooked\Windows'
LogIoStore: Display: Found '1342' files
LogIoStore: Display: Loaded Bulk Data manifest 'C:\Users\lipengzha\Documents\Unreal Projects\ThirdPerson_UE5\Saved\Cooked\Windows/ThirdPerson_UE5/Metadata/BulkDataInfo.ubulkmanifest'
LogIoStore: Display: Creating container targets...
LogIoStore: Display: Parsing packages...
LogIoStore: Display: Reading package assets...
LogIoStore: Display: Parsing package assets...
LogIoStore: Display: Parsing 0/556: 'C:/Users/lipengzha/Documents/Unreal Projects/ThirdPerson_UE5/Saved/Cooked/Windows/ThirdPerson_UE5/Content/ThirdPersonCPP/Chunks/BP_LightInstance.uasset'
LogIoStore: Display: Creating global script objects...
LogIoStore: Display: Creating global imports and exports...
LogIoStore: Display: Converting export map import indices...
LogIoStore: Display: Conforming localized packages...
LogIoStore: Display: Adding localized import packages...
LogIoStore: Display: Conforming localized imports...
LogIoStore: Display: Adding preload dependencies...
LogIoStore: Display: Building bundles...
LogIoStore: Display: Finalizing name maps...
LogIoStore: Display: Finalizing package headers...
LogIoStore: Display: Creating disk layout...
LogIoStore: Display: Ordered 0/556 packages using game open order
LogIoStore: Display: Ordered 551/556 packages using cooker open order
LogIoStore: Display: Ordered 5 packages using fallback bundle order
LogIoStore: Display: Finalizing initial load...
LogIoStore: Display: Serializing global meta data
LogIoStore: Display: Saving global name map to container file
LogIoStore: Display: Serializing container(s)...
LogIoStore: Display: Hashed, Compressed, Serialized: 790, 790, 790 / 790
LogIoStore: Display: Calculating stats...
LogIoStore: Display: --------------------------------------------------- IoDispatcher --------------------------------------------------------
LogIoStore: Display:
LogIoStore: Display: Container Flags TOC Size (KB) TOC Entries Size (MB) Compressed (MB)
LogIoStore: Display: -------------------------------------------------------------------------------------------------------------------------
LogIoStore: Display: global -/-/-/- 0.52 3 1.10 -
LogIoStore: Display: ThirdPerson_UE5-Windows -/-/-/I 189.89 787 571.22 -
LogIoStore: Display: TOTAL 190.41 790 572.32 -
LogIoStore: Display:
LogIoStore: Display: ** Flags: (C)ompressed / (E)ncrypted / (S)igned) / (I)ndexed) **
LogIoStore: Display:
LogIoStore: Display: Compression block padding: 0.55 MB
LogIoStore: Display:
LogIoStore: Display: -------------------------------------------- Container Directory Index --------------------------------------------------
LogIoStore: Display: Container Size (KB)
LogIoStore: Display: global 0.00
LogIoStore: Display: ThirdPerson_UE5-Windows 33.53
LogIoStore: Display:
LogIoStore: Display: ---------------------------------------------- Container Patch Report ---------------------------------------------------
LogIoStore: Display: Container Total (count) Modified (count) Added (count) Modified (MB) Added (MB)
LogIoStore: Display: global 3 0 0 0.00 0.00
LogIoStore: Display: ThirdPerson_UE5-Windows 787 0 787 0.00 570.67
LogIoStore: Display:
LogIoStore: Display:
LogIoStore: Display: --------------------------------------------------- PackageStore (KB) ---------------------------------------------------
LogIoStore: Display:
LogIoStore: Display: Container Store Size Packages Localized
LogIoStore: Display: -------------------------------------------------------------------------------------------------------------------------
LogIoStore: Display: ThirdPerson_UE5 22 556 0
LogIoStore: Display: TOTAL 22 556 0
LogIoStore: Display:
LogIoStore: Display:
LogIoStore: Display: --------------------------------------------------- PackageHeader (KB) --------------------------------------------------
LogIoStore: Display:
LogIoStore: Display: Container Header Summary Graph ImportMap ExportMap NameMap
LogIoStore: Display: -------------------------------------------------------------------------------------------------------------------------
LogIoStore: Display: ThirdPerson_UE5 911 35 15 35 165 662
LogIoStore: Display: TOTAL 911 35 15 35 165 662
LogIoStore: Display:
LogIoStore: Display:
LogIoStore: Display: Input: 132.91 MB UExp
LogIoStore: Display: Input: 1.12 MB UAsset
LogIoStore: Display: Input: 0.10 MB FPackageFileSummary
LogIoStore: Display: Input: 556 Packages
LogIoStore: Display: Input: 641 Imported package entries
LogIoStore: Display: Input: 327 Packages without imports
LogIoStore: Display: Input: 26088 Name map entries
LogIoStore: Display: Input: 10631 PreloadDependencies entries
LogIoStore: Display: Input: 4480 ImportMap entries
LogIoStore: Display: Input: 2342 ExportMap entries
LogIoStore: Display: Input: 712 Public exports
LogIoStore: Display:
LogIoStore: Display: Output: 558 Export bundles
LogIoStore: Display: Output: 4684 Export bundle entries
LogIoStore: Display: Output: 641 Export bundle arcs
LogIoStore: Display: Output: 18041 Public runtime script objects
LogIoStore: Display: Output: 1.10 MB InitialLoadData
LogInit: Display:
LogInit: Display: Warning/Error Summary (Unique only)
LogInit: Display: -----------------------------------

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

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

运行时 Log:

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

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

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

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

TextureStreaming

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

引擎中的默认配置:

Engine/Config/BaseScalability.ini
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
[[email protected]]
; Must be used with r.streaming.usepertexturebias set to 1. Otherwise, all textures will have a constant 16 mip bias
r.Streaming.MipBias=16
r.Streaming.AmortizeCPUToGPUCopy=1
r.Streaming.MaxNumTexturesToStreamPerFrame=1
r.Streaming.Boost=0.3
r.MaxAnisotropy=0
r.VT.MaxAnisotropy=4
r.Streaming.LimitPoolSizeToVRAM=1
r.Streaming.PoolSize=400
r.Streaming.MaxEffectiveScreenSize=0

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

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

[[email protected]]
r.Streaming.MipBias=0
r.Streaming.AmortizeCPUToGPUCopy=0
r.Streaming.MaxNumTexturesToStreamPerFrame=0
r.Streaming.Boost=1
r.MaxAnisotropy=8
r.VT.MaxAnisotropy=8
r.Streaming.LimitPoolSizeToVRAM=0
r.Streaming.PoolSize=1000
r.Streaming.MaxEffectiveScreenSize=0

[[email protected]]
r.Streaming.MipBias=0
r.Streaming.AmortizeCPUToGPUCopy=0
r.Streaming.MaxNumTexturesToStreamPerFrame=0
r.Streaming.Boost=1
r.MaxAnisotropy=8
r.VT.MaxAnisotropy=8
r.Streaming.LimitPoolSizeToVRAM=0
r.Streaming.PoolSize=3000
r.Streaming.MaxEffectiveScreenSize=0

PackagePath 获取资源磁盘路径

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

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

Viveport 选中 Actor 按 F 调用函数

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

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

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

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

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

Searchable Names

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


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

基础包过滤 config 中的 ini 文件

打包时会有以下 Log:

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

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

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

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

Source\Programs\AutomationTool\Scripts\CopyBuildToStagingDirectory.Automation.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
/// <summary>
/// Determines if an individual config file should be staged
/// </summary>
/// <param name="SC">The staging context</param>
/// <param name="ConfigDir">Directory containing the config files</param>
/// <param name="ConfigFile">The config file to check</param>
/// <returns>True if the file should be staged, false otherwise</returns>
static Nullable < bool > ShouldStageConfigFile(DeploymentContext SC, DirectoryReference ConfigDir, FileReference ConfigFile, string PlatformExtensionName) {
StagedFileReference StagedConfigFile = SC.GetStagedFileLocation(ConfigFile);
if (SC.WhitelistConfigFiles.Contains(StagedConfigFile)) {
return true;
}
if (SC.BlacklistConfigFiles.Contains(StagedConfigFile)) {
return false;
}

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

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

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

return ShouldStageConfigSuffix(SC, ConfigFile, ShortName);
}

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

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

return ShouldStageConfigSuffix(SC, ConfigFile, ShortName);
}

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

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

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

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

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

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

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

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

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

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

}
return null;
}

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

t.MaxFPS 启动时无效

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

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

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

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

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

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

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

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

UE 执行 py 的 Commandlet

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

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

一个脚本例子:

1
2
3
4
5
import unreal
import sys

if __name__ == '__main__':
do_something()

设置 ConsoleVariables 值的几种方式

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

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

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

Set有几个重载:

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

第二个参数为EConsoleVariableFlags

HAL/IConsoleManager.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
enum EConsoleVariableFlags
{
ECVF_FlagMask = 0x0000ffff,
ECVF_Default = 0x0,
ECVF_Cheat = 0x1,
ECVF_ReadOnly = 0x4,
ECVF_Unregistered = 0x8,
ECVF_CreatedFromIni = 0x10,
ECVF_RenderThreadSafe = 0x20,
ECVF_Scalability = 0x40,
ECVF_ScalabilityGroup = 0x80,
ECVF_SetFlagMask = 0x00ff0000,
ECVF_Set_NoSinkCall_Unsafe = 0x00010000,
ECVF_SetByMask = 0xff000000,
ECVF_SetByConstructor = 0x00000000,
ECVF_SetByScalability = 0x01000000,
ECVF_SetByGameSetting = 0x02000000,
ECVF_SetByProjectSetting = 0x03000000,
ECVF_SetBySystemSettingsIni = 0x04000000,
ECVF_SetByDeviceProfile = 0x05000000,
ECVF_SetByConsoleVariablesIni = 0x06000000,
ECVF_SetByCommandline = 0x07000000,
ECVF_SetByCode = 0x08000000,
ECVF_SetByConsole = 0x09000000,
}

debug shader compile

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

移动平台纹理压缩格式

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

控制打包的 log level

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

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

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

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

配置参数的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
------- Log conventions
[cat] = a category for the command to operate on, or 'global' for all categories.
[level] = verbosity level, one of: none, error, warning, display, log, verbose, all, default
At boot time, compiled in default is overridden by ini files setting, which is overridden by command line
------- Log console command usage
Log list - list all log categories
Log list [string] - list all log categories containing a substring
Log reset - reset all log categories to their boot-time default
Log [cat] - toggle the display of the category [cat]
Log [cat] off - disable display of the category [cat]
Log [cat] on - resume display of the category [cat]
Log [cat] only - enables [cat] and disables all other categories"));
Log [cat] [level] - set the verbosity level of the category [cat]
Log [cat] break - toggle the debug break on display of the category [cat]
------- Log command line
-LogCmds=\"[arguments],[arguments]...\" - applies a list of console commands at boot time
-LogCmds=\"foo verbose, bar off\" - turns on the foo category and turns off the bar category
------- Environment variables
Any command line option can be set via the environment variable UE-CmdLineArgs
set UE-CmdLineArgs=\"-LogCmds=foo verbose breakon, bar off\"
------- Config file
[Core.Log]
global=[default verbosity for things not listed later]
[cat]=[level]
foo=verbose break

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

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

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

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

PSO Caching 配置

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

DefaultEngine.ini
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
[ConsoleVariables]
;Sets the startup mode for the PSO cache, determining what the cache does after initialisation:
;0: Precompilation is paused and nothing will compile until a call to ResumeBatching().
;1: Precompilation is enabled in the 'Fast' mode.
;2: Precompilation is enabled in the 'Background' mode.
;Default is 1.
r.ShaderPipelineCache.StartupMode=1

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

DefaultGameUserSettings.ini:

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

MacSDK 下载

清理 Mac DDC

删除以下三个目录:

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

修改平台使用的 RHI

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

可以通过以下方式设置:

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

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

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

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

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

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

相关资料:

Standalone 模式的 ES3 预览

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

修改引擎的编译器参数

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

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

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

1
2
bOverrideBuildEnvironment = true;
AdditionalCompilerArguments = "-Wno-range-loop-analysis";

独立半透明

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

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

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

Slate 图片

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

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

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

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

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

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

创建项目设置中的选项

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

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

UPL 读取 ini

如有以下 ini 配置:

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

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

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

访问系统环境变量

获取环境变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdlib.h>
#include <string>
namespace
{
std::string StdString(FString UEString)
{
return std::string(TCHAR_TO_UTF8(*UEString));
}

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

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

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

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

return Value;
}

设置环境变量:

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

Xcode stdc++ 错误

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

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

会有以下错误:

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

AsyncTask

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

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

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

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

ENamedThread 的枚举定义:

Runtime\Core\Public\Async\TaskGraphInterfaces.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
namespace ENamedThreads
{
enum Type : int32
{
UnusedAnchor = -1,
/** The always-present, named threads are listed next **/
#if STATS
StatsThread,
#endif
RHIThread,
AudioThread,
GameThread,
// The render thread is sometimes the game thread and is sometimes the actual rendering thread
ActualRenderingThread = GameThread + 1,
// CAUTION ThreadedRenderingThread must be the last named thread, insert new named threads before it

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

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

MainQueue = 0x000,
LocalQueue = 0x100,

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

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

NormalTaskPriority = 0x000,
HighTaskPriority = 0x200,

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

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

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

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

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

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

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

MSVC 的警告 / 错误编号

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

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

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

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

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

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

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

按照名字规则加载模块

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

Source/Developer/TargetPlatform/Private/TargetPlatformManagerModule.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
virtual const TArray<const IShaderFormat*>& GetShaderFormats() override
{
static bool bInitialized = false;
static TArray<const IShaderFormat*> Results;

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

TArray<FName> Modules;

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

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

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

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

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

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

性能优化:设备分级

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

打印调用栈

使用 FDebug 中的函数:

1
FDebug::DumpStackTraceToLog();

会有以下输出:

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

Windows Metal Shader Compiler for IOS

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
Running: C:\Program Files\Epic Games\UE_4.26\Engine\Binaries\Win64\UE4Editor-Cmd.exe "C:\Users\lipengzha\Documents\Unreal Projects\StarterContent426\StarterContent426.uproject" -run=Cook  -TargetPlatform=IOS -fileopenlog -ddc=InstalledDerivedDataBackendGraph -unversioned -abslog="C:\Program Files\Epic Games\UE_4.26\Engine\Programs
\AutomationTool\Saved\Cook-2021.04.08-10.06.40.txt" -stdout -CrashForUAT -unattended -NoLogTimes -UTF8Output
LogInit: Display: Running engine for game: StarterContent426
LogHAL: Display: Platform has ~ 32 GB [34123063296 / 34359738368 / 32], which maps to Largest [LargestMinGB=32, LargerMinGB=12, DefaultMinGB=8, SmallerMinGB=6, SmallestMinGB=0)
LogTargetPlatformManager: Display: Loaded TargetPlatform 'AllDesktop'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'Android'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'Android_ASTC'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'Android_DXT'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'Android_ETC2'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'AndroidClient'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'Android_ASTCClient'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'Android_DXTClient'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'Android_ETC2Client'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'Android_Multi'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'Android_MultiClient'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'IOSClient'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'IOS'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'Linux'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'LinuxNoEditor'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'LinuxClient'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'LinuxServer'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'LinuxAArch64NoEditor'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'LinuxAArch64Client'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'LinuxAArch64Server'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'Lumin'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'LuminClient'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'MacNoEditor'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'Mac'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'MacClient'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'MacServer'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'TVOSClient'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'TVOS'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'WindowsNoEditor'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'Windows'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'WindowsClient'
LogTargetPlatformManager: Display: Loaded TargetPlatform 'WindowsServer'
LogTargetPlatformManager: Display: Building Assets For IOS
LogAudioDebug: Display: Lib vorbis DLL was dynamically loaded.
LogShaderCompilers: Display: Using Local Shader Compiler.
LogDerivedDataCache: Display: Max Cache Size: 512 MB
LogDerivedDataCache: Display: Loaded Boot cache: C:/Users/lipengzha/AppData/Local/UnrealEngine/4.26/DerivedDataCache/Boot.ddc
LogDerivedDataCache: Display: Pak cache opened for reading ../../../Engine/DerivedDataCache/Compressed.ddp.
LogDerivedDataCache: Display: Performance to C:/Users/lipengzha/AppData/Local/UnrealEngine/Common/DerivedDataCache: Latency=0.06ms. RandomReadSpeed=291.68MBs, RandomWriteSpeed=137.99MBs. Assigned SpeedClass 'Local'
LogMaterial: Display: Missing cached shader map for material Widget3DPassThrough, compiling.
LogMaterial: Display: Missing cached shader map for material Widget3DPassThrough, compiling.
LogMaterial: Display: Missing cached shader map for material Widget3DPassThrough, compiling.
LogMaterial: Display: Missing cached shader map for material Widget3DPassThrough, compiling.
LogMaterial: Display: Missing cached shader map for material Widget3DPassThrough, compiling.
LogMaterial: Display: Missing cached shader map for material Widget3DPassThrough, compiling.
LogMaterial: Display: Missing cached shader map for material DefaultSpriteMaterial, compiling.
LogAudioCaptureCore: Display: No Audio Capture implementations found. Audio input will be silent.
LogAudioCaptureCore: Display: No Audio Capture implementations found. Audio input will be silent.
LogCook: Display: CookSettings for Memory: MemoryMaxUsedVirtual 0MiB, MemoryMaxUsedPhysical 16384MiB, MemoryMinFreeVirtual 0MiB, MemoryMinFreePhysical 1024MiB
LogCook: Display: Mobile HDR setting 1
LogCook: Display: Creating asset registry
LogCook: Display: Discovering localized assets for cultures: en
LogCook: Display: Unable to read previous cook inisettings for platform IOS invalidating cook
LogCook: Display: Clearing all cooked content for platform IOS
LogCook: Display: Sandbox cleanup took 0.025 seconds for platforms IOS
LogMetalShaderCompiler: Display: Creating Native Library C:/Users/lipengzha/Documents/Unreal Projects/StarterContent426/Saved/Cooked/IOS/StarterContent426/Content/Global_SF_METAL.0.metallib
LogMetalShaderCompiler: Display: Archiving 685 shaders for shader platform: SF_METAL
LogZipArchiveWriter: Display: Closing zip file with 0 entries.
LogMetalShaderCompiler: Display: Post-processing archive for shader platform: SF_METAL
LogCook: Display: Cooked packages 0 Packages Remain 314 Total 314

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

Loaded a text shader (will be slower to load)

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

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

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

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

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

rust in unreal

uhtmanifest

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
"IsGameTarget":true,
"RootLocalPath":"C:\\build_agent\\workspace\\FGameEngine\\Engine",
"TargetName":"UE4Game",
"ExternalDependenciesFile":"C:\\build_agent\\workspace\\FGameEngine\\Engine\\Engine\\Intermediate\\Build\\Win64\\UE4Game\\Development\\UE4Game.deps",
"Modules":[
{
"Name":"CoreUObject",
"ModuleType":"EngineRuntime",
"BaseDirectory":"C:\\build_agent\\workspace\\FGameEngine\\Engine\\Engine\\Source\\Runtime\\CoreUObject",
"IncludeBase":"C:\\build_agent\\workspace\\FGameEngine\\Engine\\Engine\\Source\\Runtime",
"OutputDirectory":"C:\\build_agent\\workspace\\FGameEngine\\Engine\\Engine\\Intermediate\\Build\\Win64\\UE4\\Inc\\CoreUObject",
"ClassesHeaders":[

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

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

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

震动反馈

Incredibuild 对 FASTBuild 的影响

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

交叉编译工具链

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

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

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

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

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

IOS MobileProvision 路径

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

1
~/Library/MobileDevice/Provisioning\ Profiles

漏掉 PRAGMA_ENABLE_OPTIMIZATION 导致的编译错误

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

1
2
3
PRAGMA_DISABLE_OPTIMIZATION
// ...
PRAGMA_ENABLE_OPTIMIZATION

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

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

读取文件的一部分

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

Runtime\Core\Public\GenericPlatform\GenericPlatformFile.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/** 
* File handle interface.
**/
class CORE_API IFileHandle
{
public:
/** Destructor, also the only way to close the file handle **/
virtual ~IFileHandle()
{
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

PakBlackList

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

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

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

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

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

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

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

Programs/AutomationTool/Script/CopyBuildToStagingDirectory.Automation.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
/// <summary>
/// Creates a pak response file using stage context
/// </summary>
/// <param name="SC"></param>
/// <returns></returns>
private static Dictionary<string, string> CreatePakResponseFileFromStagingManifest(DeploymentContext SC, Dictionary<StagedFileReference, FileReference> FilesToStage)
{
// look for optional packaging blacklist if only one config active
List<string> Blacklist = null;
if (SC.StageTargetConfigurations.Count == 1)
{
FileReference PakBlacklistFilename = FileReference.Combine(SC.ProjectRoot, "Build", SC.PlatformDir, string.Format("PakBlacklist-{0}.txt", SC.StageTargetConfigurations[0].ToString()));
if (FileReference.Exists(PakBlacklistFilename))
{
LogInformation("Applying PAK blacklist file {0}. This is deprecated in favor of DefaultPakFileRules.ini", PakBlacklistFilename);
string[] BlacklistContents = FileReference.ReadAllLines(PakBlacklistFilename);
foreach (string Candidate in BlacklistContents)
{
if (Candidate.Trim().Length > 0)
{
if (Blacklist == null)
{
Blacklist = new List<string>();
}
Blacklist.Add(Candidate);
}
}
}
}

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

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

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

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

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

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

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

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

return UnrealPakResponseFile;
}

[/Script/BuildSettings.BuildSettings]

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

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

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

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

Unreal Insights 的初始化

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

Runtime\Launch\Private\LaunchEngineLoop.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int32 FEngineLoop::PreInitPreStartupScreen(const TCHAR* CmdLine)
{
FDelayedAutoRegisterHelper::RunAndClearDelayedAutoRegisterDelegates(EDelayedRegisterRunPhase::StartOfEnginePreInit);

#if UE_TRACE_ENABLED
{
Trace::Initialize();

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

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

SCOPED_BOOT_TIMING("FEngineLoop::PreInitPreStartupScreen");

// ...
}

UPackage::Save

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

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

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

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

OutstandingAsyncWrites.Decrement();
});
}

调用栈为:

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

配置 ConsoleVariable 默认值

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

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

引擎中的相关代码为:

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

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

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

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

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

UPROPERTY 的 ConsoleVariable

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

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

定义在其他文件中的 FAutoConsoleVariableRef:

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

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

CopyDirectory

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

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

UE4 快捷键

Core Redirects

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

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

打包时 Shader 的编译

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

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

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

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

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

.target 文件

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

UnrealHeaderTool.target
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
{
"TargetName": "UnrealHeaderTool",
"Platform": "Win64",
"Configuration": "Development",
"TargetType": "Program",
"Architecture": "",
"Launch": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool.exe",
"Version":
{
"MajorVersion": 4,
"MinorVersion": 25,
"PatchVersion": 1,
"Changelist": 0,
"CompatibleChangelist": 13144385,
"IsLicenseeVersion": 0,
"IsPromotedBuild": 0,
"BranchName": "++UE4+Release-4.25",
"BuildId": "b5ff8f70-3501-456c-bde4-438215a9b5c5",
"BuildVersion": ""
},
"BuildProducts": [
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool.exe",
"Type": "Executable"
},
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool.pdb",
"Type": "SymbolFile"
},
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool-BuildSettings.dll",
"Type": "DynamicLibrary"
},
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool-BuildSettings.pdb",
"Type": "SymbolFile"
},
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool-TraceLog.dll",
"Type": "DynamicLibrary"
},
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool-TraceLog.pdb",
"Type": "SymbolFile"
},
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool-Core.dll",
"Type": "DynamicLibrary"
},
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool-Core.pdb",
"Type": "SymbolFile"
},
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool-Json.dll",
"Type": "DynamicLibrary"
},
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool-Json.pdb",
"Type": "SymbolFile"
},
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool-Projects.dll",
"Type": "DynamicLibrary"
},
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool-Projects.pdb",
"Type": "SymbolFile"
},
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool-CoreUObject.dll",
"Type": "DynamicLibrary"
},
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool-CoreUObject.pdb",
"Type": "SymbolFile"
},
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool.version",
"Type": "RequiredResource"
},
{
"Path": "$(EngineDir)/Binaries/Win64/UnrealHeaderTool.modules",
"Type": "RequiredResource"
}
],
"RuntimeDependencies": [
{
"Path": "$(EngineDir)/Binaries/ThirdParty/DbgHelp/dbghelp.dll",
"Type": "NonUFS"
}
],
"AdditionalProperties": [
{
"Name": "SDK",
"Value": "Not Applicable"
}
]
}

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

IOS Crash 分析文档

材质 Sampler 超限制 Crash

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

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

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

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

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

LoadObject 加载磁盘文件栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
FFileManagerGeneric::CreateFileReaderInternal(const wchar_t *,unsigned int,unsigned int) FileManagerGeneric.cpp:47
FLinkerLoad::CreateLoader(TFunction<void __cdecl(void)> &&) LinkerLoad.cpp:1037
FLinkerLoad::Tick(float,bool,bool,TMap<TTuple<FName,FPackageIndex>,FPackageIndex,FDefaultSetAllocator,TDefaultMapHashableKeyFuncs<TTuple<FName,FPackageIndex>,FPackageIndex,0> > *) LinkerLoad.cpp:696
FLinkerLoad::CreateLinker(FUObjectSerializeContext *,UPackage *,const wchar_t *,unsigned int,FArchive *) LinkerLoad.cpp:459
GetPackageLinker(UPackage *,const wchar_t *,unsigned int,UPackageMap *,FGuid *,FArchive *,FUObjectSerializeContext **) Linker.cpp:745
LoadPackageInternal(UPackage *,const wchar_t *,unsigned int,FLinkerLoad *,FArchive *,FUObjectSerializeContext *) UObjectGlobals.cpp:1208
LoadPackage(UPackage *,const wchar_t *,unsigned int,FArchive *,FUObjectSerializeContext *) UObjectGlobals.cpp:1427
ResolveName(UObject *&,FString &,bool,bool,unsigned int,FUObjectSerializeContext *) UObjectGlobals.cpp:767
StaticLoadObjectInternal(UClass *,UObject *,const wchar_t *,const wchar_t *,unsigned int,UPackageMap *,bool,FUObjectSerializeContext *) UObjectGlobals.cpp:828
StaticLoadObject(UClass *,UObject *,const wchar_t *,const wchar_t *,unsigned int,UPackageMap *,bool,FUObjectSerializeContext *) UObjectGlobals.cpp:904
FSoftObjectPath::TryLoad(FUObjectSerializeContext *) SoftObjectPath.cpp:431
FSoftObjectPtr::LoadSynchronous() SoftObjectPtr.h:54
UPaperSprite::GetSourceTexture() PaperSprite.cpp:1474
UPaperSprite::PostLoad() PaperSprite.cpp:1842
UObject::ConditionalPostLoad() Obj.cpp:1067
FAsyncPackage::PostLoadObjects() AsyncLoading.cpp:6356
FAsyncPackage::TickAsyncPackage(bool,bool,float &,FFlushTree *) AsyncLoading.cpp:5533
FAsyncLoadingThread::ProcessAsyncLoading(int &,bool,bool,float,FFlushTree *) AsyncLoading.cpp:4178
FAsyncLoadingThread::TickAsyncThread(bool,bool,float,bool &,FFlushTree *) AsyncLoading.cpp:4819
FAsyncLoadingThread::TickAsyncLoading(bool,bool,float,FFlushTree *) AsyncLoading.cpp:4519
FAsyncLoadingThread::ProcessLoading(bool,bool,float) AsyncLoading.cpp:7057
StaticTick(float,bool,float) UObjectGlobals.cpp:464
UEditorEngine::Tick(float,bool) EditorEngine.cpp:1355
UUnrealEdEngine::Tick(float,bool) UnrealEdEngine.cpp:411
FEngineLoop::Tick() LaunchEngineLoop.cpp:4844
GuardedMain(const wchar_t *) Launch.cpp:171
WinMain(HINSTANCE__ *,HINSTANCE__ *,char *,int) LaunchWindows.cpp:257
__scrt_common_main_seh() 0x00007ff6ab4e140a
BaseThreadInitThunk 0x00007ffabeff7c24
RtlUserThreadStart 0x00007ffabfcad4d1

rebuild metadata

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

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

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

IPA 包最大为 4GB

自动化 Editor 的 Crash 上报

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

Build lighting from commandlet

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

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

AutomationTool 的 ErrorCode

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

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

使用 AssetRegistry 检测资源是否存在

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
namespace FileHelperPackageUtil
{
/**
* DoesPackageExist helper that rely on the AssetRegistry to validate if a package exists instead of hitting the FS
* Fallback to the FS if the asset registry initial scan isn't done or we aren't in Editor
*/
bool DoesPackageExist(UPackage* Package, FString* OutFilename = nullptr)
{
// Test using asset registry to figure out existence
IAssetRegistry& AssetRegistry = FAssetRegistryModule::GetRegistry();
if (!AssetRegistry.IsLoadingAssets() || !GIsEditor)
{
TArray<FAssetData> Data;
FAssetRegistryModule::GetRegistry().GetAssetsByPackageName(Package->GetFName(), Data, true);

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

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

分析打包的资源

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

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

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

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

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

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

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

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

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

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

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

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

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

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

实现分析:
[/Script/UnrealEd.ProjectPackagingSettings]DirectoriesToAlwaysStageAsUFS 值是在 Programs/AutomationTool/Scripts/CopyBuildToStagingDirectory.Automation.cs 中被读取的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
private static void StageAdditionalDirectoriesFromConfig(DeploymentContext SC, DirectoryReference ProjectContentRoot, StagedDirectoryReference StageContentRoot, ConfigHierarchy PlatformGameConfig, bool bUFS, string ConfigKeyName)
{
List<string> ExtraDirs;
if (PlatformGameConfig.GetArray("/Script/UnrealEd.ProjectPackagingSettings", ConfigKeyName, out ExtraDirs))
{
// Each string has the format '(Path="TheDirToStage")'
foreach (var PathStr in ExtraDirs)
{
string RelativePath = null;
var PathParts = PathStr.Split('"');
if (PathParts.Length == 3)
{
RelativePath = PathParts[1];
}
else if (PathParts.Length == 1)
{
RelativePath = PathParts[0];
}
if (RelativePath != null)
{
DirectoryReference InputDir = DirectoryReference.Combine(ProjectContentRoot, RelativePath);
StagedDirectoryReference OutputDir = StagedDirectoryReference.Combine(StageContentRoot, RelativePath);
if (bUFS)
{
List<FileReference> Files = SC.FindFilesToStage(InputDir, StageFilesSearch.AllDirectories);
Files.RemoveAll(x => x.HasExtension(".uasset") || x.HasExtension(".umap") || (SC.DedicatedServer && x.HasExtension(".mp4")));
SC.StageFiles(StagedFileType.UFS, InputDir, Files, OutputDir);
}
else
{
SC.StageFiles(StagedFileType.NonUFS, InputDir, StageFilesSearch.AllDirectories, OutputDir);
}
}
}
}
}

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

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

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

FDataTime::UtcNow

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

uexp 和 ubulk 的作用

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

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

UMG OnTouchStarted 不触发

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

Android not found uproject

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

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

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

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

提取 chunk 的 paklist 文件

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

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

paklist 相关的文件列表如下:

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

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

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

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

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

DS 产生 corefile

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

指定 SkeletalMesh 的 LOD 级别

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

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

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

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

HotPatcher 的自动化导出 Release 脚本

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

HotRelease.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import os 
import sys
import argparse
parser = argparse.ArgumentParser(description="used for build engine or project")
hot_release = parser.add_argument_group('HotRelease')
# project
hot_release.add_argument('--enginebin',help='UE4Editor-cmd.exe binary path')
hot_release.add_argument('--projectdir',help='project root directory')
hot_release.add_argument('--projectname',help='project name,match projectname.uproject')
hot_release.add_argument('--versiondir',help='client version file dir')
hot_release.add_argument('--versionid',help='current release version id')
hot_release.add_argument('--outdir',help='export release result save dir')
platform_list=[
"WindowsNoEditor",
"Android_ASTC",
"IOS",
]
def getPlatformPakListName(PlatformName,ProjectName):
return "PakList_%s-%s.txt" % (ProjectName,PlatformName)
def getProjectFullPath(ProjectDir,ProjectName):
return "%s\\%s.uproject" % (ProjectDir,ProjectName)
def get_platform_paklist(clientversion_path,versionid,project_name):
result_dict = {}
if os.path.exists(clientversion_path):
version_paklist_path = os.path.normpath(os.path.abspath(os.path.join(clientversion_path, versionid)))
print("versionid: %s" % versionid)
for platform in platform_list:
platform_paklist_path = os.path.join(version_paklist_path, getPlatformPakListName(platform,project_name))
if os.path.exists(platform_paklist_path):
result_dict[platform] = platform_paklist_path
print("platform:%s paklist:%s" % (platform,result_dict[platform]))
return result_dict
def ExportRelease(versionid,engine_bin_path,project_dir,project_name,platform_paklist_dict,savepath):
AddPlatformPakListCmd = "-AddPlatformPakList="
for key,value in platform_paklist_dict.items():
AddPlatformPakListCmd = "%s%s+%s," % (AddPlatformPakListCmd,key,value)
print(AddPlatformPakListCmd)
commands_tuple = [
engine_bin_path,
getProjectFullPath(project_dir,project_name),
"-run=HotRelease",
"-versionid=%s" % (versionid),
"-byPakList=true",
AddPlatformPakListCmd,
"-savepath.path=%s" % (savepath),
# "-wait"
]
final_cmd = ""
for param in commands_tuple:
final_cmd = "%s %s" % (final_cmd,param)
print(final_cmd)
os.system(final_cmd)
def GetArgByName(ParserArgs,ArgName):
ArgsPairs = ParserArgs.__dict__
for key,value in ArgsPairs.items():
if key == ArgName:
return value
def printSelectorHelp():
print("Args is invalid!")
def main():
ParserArgs = parser.parse_args()
engine_bin_path = GetArgByName(ParserArgs,"enginebin")
project_dir = GetArgByName(ParserArgs,"projectdir")
project_name = GetArgByName(ParserArgs,"projectname")
version_id = GetArgByName(ParserArgs,"versionid")
clientversion_path = GetArgByName(ParserArgs,"versiondir")
outdir = GetArgByName(ParserArgs,"outdir")
if engine_bin_path and project_dir and project_name and version_id and clientversion_path and outdir:
ExportRelease(
version_id,
engine_bin_path,
project_dir,
project_name,
get_platform_paklist(clientversion_path,version_id,project_name),
outdir
)
else:
printSelectorHelp()
if __name__ == "__main__":
main()

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

可以通过 FEngineVersion 来获取:

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

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 检测指针是否为 Null,对象是否被注册(UClass 是否存在)  
/**
* Checks to see if the object appears to be valid
* @return true if this appears to be a valid object
*/
bool IsValidLowLevel() const;
// 从内存布局上检测对象是否有效,并会检测对象的 CDO/UClass 是否存在
// 可以用于加载资源的检测
/**
* Faster version of IsValidLowLevel.
* Checks to see if the object appears to be valid by checking pointers and their alignment.
* Name and InternalIndex checks are less accurate than IsValidLowLevel.
* @param bRecursive true if the Class pointer should be checked with IsValidLowLevelFast
* @return true if this appears to be a valid object
*/
bool IsValidLowLevelFast(bool bRecursive = true) const;
// 检测指针是否为 null,以及是否被 GC 清理,如果指针是无效的内存地址会 Crash
/**
* Test validity of object
*
* @param Test The object to test
* @return Return true if the object is usable: non-null and not pending kill
*/
FORCEINLINE bool IsValid(const UObject *Test)
{
return Test && !Test->IsPendingKill();
}

M1 Mac 的 UE4.26 兼容性报告

UMG CanvasPanel 合批

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

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

引擎修改语言的配置存储

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

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

其值如下:

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

如果是英文的,则是en.

中文赋值给 FString

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

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

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

强引用 UClass

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

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

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

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

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

build.cs 添加宏定义的值

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

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

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

1
#define TEST_MACRO_NOT_VALUE 1

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

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

编译引擎的命令

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

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

编辑器检测 Actor 移动

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

1
virtual void PostEditMove(bool bFinished) override;

Actor Tick in Editor

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

1
PrimaryActorTick.bCanEverTick = true;

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

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

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

AActor 的类中,默认返回 false.

UE4 ERROR: Missing object file

在编译时遇到以下错误:

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 
public override List<FileItem> Compile(ReadOnlyTargetRules Target, UEToolChain ToolChain, CppCompileEnvironment BinaryCompileEnvironment, FileReference SingleFileToCompile, ISourceFileWorkingSet WorkingSet, IActionGraphBuilder Graph)
{
//UEBuildPlatform BuildPlatform = UEBuildPlatform.GetBuildPlatform(BinaryCompileEnvironment.Platform);

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

CppCompileEnvironment ModuleCompileEnvironment = CreateModuleCompileEnvironment(Target, BinaryCompileEnvironment);

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

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

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

分隔 String 为数组

可以使用 FString 的 ParserIntoArray 函数:

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

Win 和 Mac 出 IOS 包的区别

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

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

UE4 新地图的 Package 问题

发现 UE 中的一个问题:

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

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

C++ 加载 BP 的 Enum

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

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

效果:

DS 设置超时时间

修改 DefaultEngine.ini:

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

DDC 的资料

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

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

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

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

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

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

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

Mount Point 的作用

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Runtime/PakFile/Private/IPlatformFilePak.cpp
void FPakFile::LoadIndex(FArchive* Reader)
{
if (CachedTotalSize < (Info.IndexOffset + Info.IndexSize))
{
UE_LOG(LogPakFile, Fatal, TEXT("Corrupted index offset in pak file."));
}
else
{
if (Info.Version >= FPakInfo::PakFile_Version_FrozenIndex && Info.bIndexIsFrozen)
{
SCOPED_BOOT_TIMING("PakFile_LoadFrozen");

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

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// Runtime/PakFile/Public/IPlatformFilePak.h
/**
* Finds a file in the specified pak files.
*
* @param Paks Pak files to find the file in.
* @param Filename File to find in pak files.
* @param OutPakFile Optional pointer to a pak file where the filename was found.
* @return Pointer to pak entry if the file was found, NULL otherwise.
*/
static bool FindFileInPakFiles(TArray<FPakListEntry>& Paks,const TCHAR* Filename,FPakFile** OutPakFile,FPakEntry* OutEntry = nullptr)
{
FString StandardFilename(Filename);
FPaths::MakeStandardFilename(StandardFilename);

int32 DeletedReadOrder = -1;

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

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

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

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

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

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

引擎的 Splash 过程

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

Outer

获取 ContentBrowser 中选择的资源

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

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

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

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

1
bLegacyPublicIncludePaths = false;

添加资源右键菜单按钮

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

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

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

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

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

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

GenerateProjectFiles 指定 VS 版本

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

Module 的 WhitelistPlatforms

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

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

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

提取 PakList 文件

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

1
Engine\Programs\AutomationTool\Saved\Logs

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

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

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

绑定 DetailsView 的属性变化事件

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
TSharedPtr<IStructureDetailsView> SettingsView;

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

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

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

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

关键是这行代码:

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

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

从指定 Pak 加载文件

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
IFileHandle* FPakPlatformFile::OpenRead(const TCHAR* Filename, bool bAllowWrite)
{
IFileHandle* Result = NULL;
FPakFile* PakFile = NULL;
FPakEntry FileEntry;
if (FindFileInPakFiles(Filename, &PakFile, &FileEntry))
{
#if PAK_TRACKER
TrackPak(Filename, &FileEntry);
#endif

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

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

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

Cook 资源

可以看 CookOnTheFlyServer.cpp 中的代码:

Editor/UnrealEd/Private/CookOnTheFlyServer.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
uint32 UCookOnTheFlyServer::FullLoadAndSave(uint32& CookedPackageCount)
{
// ...
if (bCookPackage)
{
FString PlatFilename = Filename.Replace(TEXT("[Platform]"), *Target->PlatformName());

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

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

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

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

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

FPlatformAtomics::InterlockedIncrement(&ParallelSavedPackages);
}

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

// ...
}

Compile 对 Instanced 的替换调用栈

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

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

对资源点击 Compile 的调用栈

会执行到FKismetEditorUtilities::CompileBlueprint

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

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

UMG 的子控件引用热更问题

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

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

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

监听 Slate 的输入事件

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

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

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

IInputProcessor提供的接口:

Runtime/Slate/Public/Framework/Application/IInputProcessor.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* Interface for a Slate Input Handler
*/
class SLATE_API IInputProcessor
{
public:
IInputProcessor(){};
virtual ~IInputProcessor(){}

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

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

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

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

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

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

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

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

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

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

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

Rider 传递命令行参数

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

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

VirtualCamera

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

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

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

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

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

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

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

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

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

远程构建在 4.26 的问题

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

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

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

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

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

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

平台相关 Target

在项目的 Target.cs 中定义着项目的 TargetRules,但是也不是所有的平台都可以通用全部的参数,每个平台都有自己特定的属性,所以 UE 的 TargetRules 定义中中还包含各个平台的 Target:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/// <summary>
/// Android-specific target settings.
/// </summary>
[ConfigSubObject]
public AndroidTargetRules AndroidPlatform = new AndroidTargetRules();

/// <summary>
/// IOS-specific target settings.
/// </summary>
[ConfigSubObject]
public IOSTargetRules IOSPlatform = new IOSTargetRules();

/// <summary>
/// Lumin-specific target settings.
/// </summary>
[ConfigSubObject]
public LuminTargetRules LuminPlatform = new LuminTargetRules();

/// <summary>
/// Linux-specific target settings.
/// </summary>
[ConfigSubObject]
public LinuxTargetRules LinuxPlatform = new LinuxTargetRules();

/// <summary>
/// Mac-specific target settings.
/// </summary>
[ConfigSubObject]
public MacTargetRules MacPlatform = new MacTargetRules();

/// <summary>
/// PS4-specific target settings.
/// </summary>
[ConfigSubObject]
public PS4TargetRules PS4Platform = new PS4TargetRules();

/// <summary>
/// Switch-specific target settings.
/// </summary>
[ConfigSubObject]
public SwitchTargetRules SwitchPlatform = new SwitchTargetRules();

/// <summary>
/// Windows-specific target settings.
/// </summary>
[ConfigSubObject]
public WindowsTargetRules WindowsPlatform; // Requires 'this' parameter; initialized in constructor

/// <summary>
/// Xbox One-specific target settings.
/// </summary>
[ConfigSubObject]
public XboxOneTargetRules XboxOnePlatform = new XboxOneTargetRules();

/// <summary>
/// HoloLens-specific target settings.
/// </summary>
[ConfigSubObject]
public HoloLensTargetRules HoloLensPlatform;

当需要对某个平台进行特殊控制时,可以在 TargetRules 中访问特定平台的对象。

如在 IOS 平台生成 dSYM:

1
2
3
4
if(Target.Platform == UnrealTargetPlatform.IOS)
{
IOSPlatform.bGeneratedSYM = true;
}

TargetRule 获取项目路径

使用ProjectFile

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

UObject 获取资源路径

可以使用FStringAssetReference

1
2
FStringAssetReference ObjectPath(Object);
FString AssetPackagePath = ObjectPath.ToString();

获取的路径格式为:

1
/Game/StarterContent/Materials/M_Basic_Floor.M_Basic_Floor

如果想要获得类似编辑器 Copy Reference 的信息,则可以使用FAssetData

1
2
FAssetData AssetData(Object);
FString ReferenceInfo = AssetData.GetExportTextName();

获取的格式为:

1
Material'/Game/StarterContent/Materials/M_Basic_Floor.M_Basic_Floor'

也能够根据 UObject 获取到它的资源类型。

如果用 FAssetData 获取 C++ 类会得到下面的这样的信息(在运行时动态创建的对象):

1
Actor'/Game/StarterContent/Maps/UEDPIE_0_Minimal_Default.Minimal_Default:PersistentLevel.Actor_0'

编译引擎支持 Android 和 iOS

把源码版引擎导出为安装版引擎时可以使用 BuildGraph(Win+Android+iOS):

1
Engine/Build/BatchFiles/RunUAT.bat BuildGraph -Script=InstalledEngineBuild.xml -Target="Make Installed Build Win64" -set:WithDDC=false -set:WithWin32=false -std:HostPlatformEditorOnly=true -std:HostPlatformOnly=true -set:WithAndroid=true -set:WithIOS=true -set:WithLumin=false -set:WithLuminMac=false -set:WithTVOS=false -set:WithLinux=false -set:WithLinuxAArch64=false -set:WithHoloLens=false -set:EmbedSrcSrvInfo=false -set:WithFullDebugInfo=false -set:GameConfigurations=Development -set:VS2019=true -set:InstalledDir=D:\EngineBin -compile

但是编译 iOS 需要一台 Mac,类似项目的远程构建,引擎构建 iOS 支持时也需要。
在之前的笔记中写到,项目的远程构建 iOS 时可以在 DefaultEngine.ini 中加上远程机器的地址和 SSHKey:

1
2
3
4
[/Script/IOSRuntimeSettings.IOSRuntimeSettings]
RemoteServerName=xxx.xx.xx.xxx
RSyncUsername=machinename
SSHPrivateKeyOverridePath=D:\XXXX\RemoteToolChainPrivate.key

在构建引擎时需要把它们写到 Engine\Config\BaseEngine.ini 中,然后再使用上面的命令构建即可。

UPARAM

可以在 UFUNCTION 的函数中给指定的参数来指定它的 UPARAM,可以用来控制函数的参数属性。之前是用来指定 Ref(UPARAM(Ref)),其实也可以使用 UPARAM(meta=()) 来指定 meta 属性。

如在蓝图实现使用枚举 bitmask:

1
2
3
4
5
6
7
8
UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category="Audiokinetic|Actor", meta=(AdvancedDisplay="2", AutoCreateRefTerm = "PostEventCallback,ExternalSources"))
static int32 PostEvent(class UAkAudioEvent* AkEvent,
class AActor* Actor,
UPARAM(meta = (Bitmask, BitmaskEnum = EAkCallbackType)) int32 CallbackMask,
const FOnAkPostEventCallback& PostEventCallback,
const TArray<FAkExternalSourceInfo>& ExternalSources,
bool bStopWhenAttachedToDestroyed = false,
FString EventName = FString(""));

指定返回值的名字什么的都不在话下:

1
static UPARAM(DisplayName = "Bundle") FOSCBundle& AddMessageToBundle(const FOSCMessage& Message, UPARAM(ref) FOSCBundle& Bundle);

可以在 UPARAM 看到引擎代码中的各种用法。

无边框模式

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

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

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

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

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

C# 获取命令行参数

需要using system;.

1
2
3
4
5
6
string[] arguments = Environment.GetCommandLineArgs();

for (int index = 0;index<arguments.Length;++index)
{

}

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

有时我们需要在打包时给不同的平台添加不同目录下的外部资源(如 WWise),UE 提供了 Project Settings-Packaging-Additional Non-Asset Directories to Package,找了一下,并没有发现单独给某个平台指定添加路径的地方。
但是 Additional Non-Asset Directories to Package 本身是支持给不同平台添加不同目录的,只要在 Additional Non-Asset Directories to Package 添加的目录下根据不同的平台创建不同的目录。
如:

1
2
3
4
5
6
7
D:\EmptyProject\Content\WwiseAudio>tree /a
+---Android
| \---English(US)
+---iOS
| \---English(US)
\---Windows
\---English(US)

在项目的 Content\WwiseAudio 下,有 Android/iOS/Windows 等目录,在 Additional Non-Asset Directories to Package 中添加的是 WwiseAudio 目录, 打包时只会把对应平台的目录给打包进去,但是没有看到 UE 的文档里哪有写。

UStruct 的 json 序列化

因为 UStruct 在 UE 内是具有反射的,所以不用自己去解析编码就可以实现序列化和反序列化,UE 中提供了一个辅助模块:JsonUtilities,里面具有 FJsonObjectConverter 类,定义了一系列的操作。
我简单封装了一下,对 Ustrut 的序列化和反序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
template<typename TStructType>
static bool TSerializeStructAsJsonObject(const TStructType& InStruct,TSharedPtr<FJsonObject>& OutJsonObject)
{
if(!OutJsonObject.IsValid())
{
OutJsonObject = MakeShareable(new FJsonObject);
}
bool bStatus = FJsonObjectConverter::UStructToJsonObject(TStructType::StaticStruct(),&InStruct,OutJsonObject.ToSharedRef(),0,0);
return bStatus;
}

template<typename TStructType>
static bool TDeserializeJsonObjectAsStruct(const TSharedPtr<FJsonObject>& OutJsonObject,TStructType& InStruct)
{
bool bStatus = false;
if(OutJsonObject.IsValid())
{
bStatus = FJsonObjectConverter::JsonObjectToUStruct(OutJsonObject.ToSharedRef(),TStructType::StaticStruct(),&InStruct,0,0);
}
return bStatus;
}

template<typename TStructType>
static bool TSerializeStructAsJsonString(const TStructType& InStruct,FString& OutJsonString)
{
bool bRunStatus = false;

{
TSharedPtr<FJsonObject> JsonObject;
if (TSerializeStructAsJsonObject<TStructType>(InStruct,JsonObject) && JsonObject.IsValid())
{
auto JsonWriter = TJsonWriterFactory<>::Create(&OutJsonString);
FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter);
bRunStatus = true;
}
}
return bRunStatus;
}

template<typename TStructType>
static bool TDeserializeJsonStringAsStruct(const FString& InJsonString,TStructType& OutStruct)
{
bool bRunStatus = false;
TSharedRef<TJsonReader<TCHAR>> JsonReader = TJsonReaderFactory<TCHAR>::Create(InJsonString);
TSharedPtr<FJsonObject> DeserializeJsonObject;
if (FJsonSerializer::Deserialize(JsonReader, DeserializeJsonObject))
{
bRunStatus = TDeserializeJsonObjectAsStruct<TStructType>(DeserializeJsonObject,OutStruct);
}
return bRunStatus;
}

Redirector

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

检查引擎是否运行在 Commandlet

UE 中有检测的方法,定义在 CoreGlobals.hIsRunningCommandlet

1
2
3
4
5
6
7
8
9
10
11
/**
* Check to see if this executable is running a commandlet (custom command-line processing code in an editor-like environment)
*/
FORCEINLINE bool IsRunningCommandlet()
{
#if WITH_ENGINE
return PRIVATE_GIsRunningCommandlet;
#else
return false;
#endif
}

FSoftObjectPath 限定类型

默认 FSoftObjectPath 可以指定任何继承自 UObject 的资源类型,但有时候只想要指定某些类型,UE 提供了限定类型的功能,要使用 UPROPERTY:

1
2
UPROPERTY(config, EditAnywhere, Category=DefaultMaps, meta=(AllowedClasses="World"))
FSoftObjectPath EditorStartupMap;

meta 中使用 AllowedClasses 即可,AllowedClassess的值是实际类型去掉前缀 U,如USoundClass 要使用SoundClass

AllowedClasses可以指定多个,使用逗号分隔,使当前 FSoftObjectPath 可以指定多个限定类型的资源。而且还可以使用 ExactClass 来控制是否严格限定类型(是否允许继承层次中类型中的资源,如 UDataTableUCompositeDataTable),如果不指定,默认是严格限定类型的,如果 ExactClass=true 则可以使用继承层次中类型的资源。

编辑器 viewport 显示文字

UKismetSystemLibrary::PrintString 是运行时可以输出到 viewport,在编辑器下无效果,想要实现编辑器下的文本输出可以通过 FCanvasDrawItem来实现:

1
2
3
FCanvasTextItem TextItem(FVector2D(100, 200), LOCTEXT("OutOfTextureMemory", "RAN OUT OF TEXTURE MEMORY, EXPECT CORRUPTION AND GPU HANGS!"), GEngine->GetMediumFont(), FLinearColor::Red);
TextItem.EnableShadow(FLinearColor::Black);
Canvas->DrawItem(TextItem);

stat fps 等,都是通过 FCanvas 来实现的,如DrawMapWarnings:

以及RenderStatFPS:

FCanvas可以通过 FViewport::GetDebugCanvas() 来获得。

URL Encode/Decode

UE 提供了相关的 API:

1
2
static FString FGenericPlatformHttp::UrlDecode(const FString & EncodedString)
static FString FGenericPlatformHttp::UrlEncode(const FString & UnencodedString)

执行结果:

查看 APK 的签名信息

可以使用keytool.exe(在 JDK 的 bin 中),使用以下参数:

1
keytool.exe" -list -printcert -jarfile FGame-armv7.apk

会打印出 apk 的签名色所有者和发布者,以及证书的有效时间、证书指纹等等。

ASTC 设置压缩率

Project Settings-Engine-Cooker中有选项:

打包 Windows 以窗口模式启动

在项目的 Config 下新建 DefaultGameUserSettings.ini 文件,填入以下内容:

1
2
3
4
5
6
7
8
9
10
11
[/Script/Engine.GameUserSettings]
bUseVSync=False
ResolutionSizeX=1920
ResolutionSizeY=1080
LastUserConfirmedResolutionSizeX=1920
LastUserConfirmedResolutionSizeY=1080
WindowPosX=-1
WindowPosY=-1
FullscreenMode=2
LastConfirmedFullscreenMode=2
Version=5

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

指定地图 Cook 及打包

DefaultEditor.ini中添加以下项:

1
2
3
4
5
6
7
[AllMaps]
+Map=/Game/Assets/Scene/Map/Fb/v3/8r/xzzn/Fb_ThePoolOfTribute
+Map=/Game/Assets/Scene/Map/LightSpeed/LightSpeed

[AlwaysCookMaps]
+Map=/Game/Assets/Scene/Map/Fb/v3/8r/xzzn/Fb_ThePoolOfTribute
+Map=/Game/Assets/Scene/Map/LightSpeed/LightSpeed

C++ 访问 Collision Chanel

ECollisionChanel是定义在 EngineTypes.h 中的枚举类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
UENUM(BlueprintType)
enum ECollisionChannel
{

ECC_WorldStatic UMETA(DisplayName="WorldStatic"),
ECC_WorldDynamic UMETA(DisplayName="WorldDynamic"),
ECC_Pawn UMETA(DisplayName="Pawn"),
ECC_Visibility UMETA(DisplayName="Visibility" , TraceQuery="1"),
ECC_Camera UMETA(DisplayName="Camera" , TraceQuery="1"),
ECC_PhysicsBody UMETA(DisplayName="PhysicsBody"),
ECC_Vehicle UMETA(DisplayName="Vehicle"),
ECC_Destructible UMETA(DisplayName="Destructible"),

/** Reserved for gizmo collision */
ECC_EngineTraceChannel1 UMETA(Hidden),

ECC_EngineTraceChannel2 UMETA(Hidden),
ECC_EngineTraceChannel3 UMETA(Hidden),
ECC_EngineTraceChannel4 UMETA(Hidden),
ECC_EngineTraceChannel5 UMETA(Hidden),
ECC_EngineTraceChannel6 UMETA(Hidden),

ECC_GameTraceChannel1 UMETA(Hidden),
ECC_GameTraceChannel2 UMETA(Hidden),
ECC_GameTraceChannel3 UMETA(Hidden),
ECC_GameTraceChannel4 UMETA(Hidden),
ECC_GameTraceChannel5 UMETA(Hidden),
ECC_GameTraceChannel6 UMETA(Hidden),
ECC_GameTraceChannel7 UMETA(Hidden),
ECC_GameTraceChannel8 UMETA(Hidden),
ECC_GameTraceChannel9 UMETA(Hidden),
ECC_GameTraceChannel10 UMETA(Hidden),
ECC_GameTraceChannel11 UMETA(Hidden),
ECC_GameTraceChannel12 UMETA(Hidden),
ECC_GameTraceChannel13 UMETA(Hidden),
ECC_GameTraceChannel14 UMETA(Hidden),
ECC_GameTraceChannel15 UMETA(Hidden),
ECC_GameTraceChannel16 UMETA(Hidden),
ECC_GameTraceChannel17 UMETA(Hidden),
ECC_GameTraceChannel18 UMETA(Hidden),

/** Add new serializeable channels above here (i.e. entries that exist in FCollisionResponseContainer) */
/** Add only nonserialized/transient flags below */

// NOTE!!!! THESE ARE BEING DEPRECATED BUT STILL THERE FOR BLUEPRINT. PLEASE DO NOT USE THEM IN CODE

ECC_OverlapAll_Deprecated UMETA(Hidden),
ECC_MAX,
};

但是我们在项目设置中添加是可以取任意的名字的,而且创建的 Chanel 的数量有上限(18 个),这是因为 ECollisionChanel 是预先定义了 18 个供游戏创建的枚举值,假如我们创建了一个名字为 AAA 的 Chanel,会在当前项目的 Config/DefaultEngine.ini[/Script/Engine.CollisionProfile]中创建以下项:

1
+DefaultChannelResponses=(Channel=ECC_GameTraceChannel1,DefaultResponse=ECR_Overlap,bTraceType=False,bStaticObject=False,Name="AAA")

在这里跟枚举值做了绑定,在 C++ 代码中进行设置的时候就需要指定 ECC_GameTraceChannel1 的枚举值。

Todo:可以写一个方便从名字获取 ECollisionChanel 枚举值的库。

Actor 的延迟 Spawn

1
2
3
4
5
6
7
8
FTransform SpawnTransform(Rotation, Origin);
auto MyDeferredActor = Cast<ADeferredActor>(UGameplayStatics::BeginDeferredActorSpawnFromClass(this, DeferredActorClass, SpawnTransform));
if (MyDeferredActor != nullptr)
{
MyDeferredActor->Init(ShootDir);

UGameplayStatics::FinishSpawningActor(MyDeferredActor, SpawnTransform);
}

编译引擎的 WindowsDebugTools 错误

编译引擎出现以下错误:

1
2
3
4
ERROR: Unable to find installation of PDBCOPY.EXE, which is required to strip symbols. This tool is included as part of the 'Windows Debugging Tools' component of the Windows 10 SDK (https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk).
while executing task <Strip Platform="Win64" BaseDir="C:\BuildAgent\workspace\FGameEngine\FEngine" Files="#UE4Editor Win64 Unstripped" OutputDir="C:\BuildAgent\workspace\FGameEngine\FEngine\Engine\Saved" Tag="#UE4Editor Win64 Stripped" />
at Engine\Build\InstalledEngineBuild.xml(183)
(see C:\BuildAgent\workspace\FGameEngine\FEngine\Engine\Programs\AutomationTool\Saved\Logs\Log.txt for full exception trace)

这是因为没有安装 Windows Debugging Tools,在 这里 下载 Win10SDK 安装程序,在其中选择安装 Windows Debug Tools 安装即可。

BuildGraph 构建引擎

可以使用下列命令:
构建引擎的工具集:

1
Engine\Build\BatchFiles\RunUAT.bat BuildGraph -Script=Engine\Build\InstalledEngineBuild.xml -Target="Build Tools Win64" -set:HostPlatformOnly=true -set:WithWin64=true -set:WithWin32=false -set:WithDDC=false -clean

构建引擎:

1
2
Engine/Build/BatchFiles/RunUAT.bat BuildGraph -Script=Engine\Build\InstalledEngineBuild.xml -target=\"Make Installed Build Win64\" -set:WithDDC=false -set:WithWin64=true -set:WithWin32=false -set:WithAndroid=false -set:WithIOS=false -set:WithTVOS=false -set:WithLinux=false -set:WithLinuxAArch64=false -set:WithLumin=false -set:WithHoloLens=false

Game module could not be loaded

在引入其他模块的代码时,有时候会具有下面这样的情况:

Log 中的提示:

1
2
3
4
5
[2020.07.18-01.58.50:729][0]LogWindows: Failed to load 'E:/UnrealProjects/Examples/HotPatcherExample/Binaries/Win64/UE4Editor-HotPatcherExample.dll' (GetLastError=1114)
[2020.07.18-01.58.50:729][0]LogModuleManager: Warning: ModuleManager: Unable to load module 'E:/UnrealProjects/Examples/HotPatcherExample/Binaries/Win64/UE4Editor-HotPatcherExample.dll' because the file couldn't be loaded by the OS.
[2020.07.18-02.01.38:988][0]LogWindowsTextInputMethodSystem: Display: IME system now activated using TSF (微软拼音).
[2020.07.18-02.01.38:991][0]Message dialog closed, result: Ok, title: Message, text: The game module 'HotPatcherExample' could not be loaded. There may be an operating system error or the module may not be properly set up.
[2020.07.18-02.01.38:991][0]LogCore: Engine exit requested (reason: EngineExit() was called)

这个问题应该是项目中(或者插件中)加载 DLL 失败的问题,一般情况下是 DLL 文件存在问题或者 DLL 中逻辑的错误造成。

经过排查后发现我触发这个问题的方式为在使用 UnLua 注册 lua_Reg 函数时漏掉了置空最后一个元素:

1
2
3
4
5
6
7
8
9
10
static const luaL_Reg AMyActorLib[]=
{
{"ReceiveBytes",ReceiveBytes},
// {nullptr,nullptr}
};

BEGIN_EXPORT_REFLECTED_CLASS(AMyActor)
ADD_LIB(AMyActorLib)
END_EXPORT_CLASS(AMyActor)
IMPLEMENT_EXPORTED_CLASS(AMyActor)

因为 AMyActorLib 这个数组最后一个元素没有置空,在 UnLuaEx.inl 中的 AddLib 函数中:

1
2
3
4
5
6
7
8
9
10
11
12
template <bool bIsReflected>
void TExportedClassBase<bIsReflected>::AddLib(const luaL_Reg *InLib)
{
if (InLib)
{
while (InLib->name && InLib->func)
{
GlueFunctions.Add(new FGlueFunction(ANSI_TO_TCHAR(InLib->name), InLib->func));
++InLib;
}
}
}

如果 lua_Reg 数组的最后一个元素不为空就会导致这里出发 UB 行为(死循环),导致当前模块加载失败,也就触发了前面模块加载失败的问题。

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

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

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

UMG 资料文档

文章:

获取 UObject 的资源路径

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

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

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

BP to CPP

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

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

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

监听窗口关闭

可以通过监听 FSlateFontServices 里的OnSlateWindowDestroyed:

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

监听方法:

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

UnLua 的 EXPORT_PRIMITIVE_TYPE

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

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

宏展开之后为:

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

4.25 MountPak 没有材质

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Android 上 Arrow 组件的 crash

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

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

有时间在来具体分析。

target/build.cs 输出 Log

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

1
2
3
using System;

System.Console.WriteLine("12346");

TargetRules 的 BuildSettingsVersion

PS:UE4.24 之后才有。

可以在 Target.cs 中指定:

1
DefaultBuildSettings = BuildSettingsVersion.V2;

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

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

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

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

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

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

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

从 TargetRules 获取 Configuration

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

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

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

IOS CrashLog 分析

GC

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

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

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

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

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

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

SDetailsView 监听属性变化

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

接收参数是FPropertyChangedEvent

1
DECLARE_MULTICAST_DELEGATE_OneParam(FOnFinishedChangingProperties, const FPropertyChangedEvent&);

UObject serializer 的调用栈

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

Plugin 添加其他 Plugin 的模块

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

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

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

Build Configurations

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

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

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

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

GlobalShaderCache 的加载

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

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

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

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

ushaderbytecode 的加载

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

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

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

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

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

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

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

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

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

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

默认打包到 pak 里的资源

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

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

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

+Package=/Engine/EngineSounds/WhiteNoise

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

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

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

创建 Commandlet

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

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

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


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

public:

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

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

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

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

通过 Commandline 替换加载的 ini

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

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

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

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

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

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

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

获取当前平台信息

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

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

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

遍历 UCLASS 或 USTRUCT 的反射成员

可以通过 TFieldIterator 来遍历:

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

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

Lua:Metatable

控制打包时 ini 的拷贝

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

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

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

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

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

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

bUsesSlate

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

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

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

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

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

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

Unreal Plugin Language

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

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

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

打包时 Paklist 文件的生成

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

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

类似的路径下。

这个文件生成的地方为:

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

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

ShaderStableInfo*.scl.csv

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

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

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

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

uint32 KeyHash;
FSHAHash OutputHash;

FStableShaderKeyAndValue()
: KeyHash(0)
{
}
}

作用有时间再来分析。

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

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

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

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

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

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

UCLASS 的 config

如:

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

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

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

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

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

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

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

操作剪贴板 Clipboard

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

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

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

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

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

在场景中 Copy/Paste 的实现

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

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

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

Copy

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

1
2
3
4
5
6
7
8
9
10
/**
* Copies selected actors to the clipboard. Supports copying actors from multiple levels.
* NOTE: Doesn't support copying prefab instance actors!
*
* @param InWorld World to get the selected actors from
* @param bShouldCut If true, deletes the selected actors after copying them to the clipboard
* @param bIsMove If true, this cut is part of a move and the actors 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,符合上面的规则。

其他

相关资料:

工具:

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

编辑器 SpawnActor

可以使用 UEditorEngine 中的 SpawnActor 函数。

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

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

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.

获取资源依赖关系

最近有个需求要获取 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

地图的存储和加载

存储栈:

加载栈:

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

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

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

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都删了重新编译生成。

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

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

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 才行。

获取蓝图添加的所有接口

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

UnrealFrontEnd DeviceLog

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

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

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

获取某个类的所有子类

查找所有继承自某个 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;
}

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

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

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

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

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

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

PlatformMisc 的跨平台实现

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

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

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

FPlatformMisc只是一个类型定义 (typedef),通过平台宏来判断,当编译为不同的平台是会把FPlatformMisc 通过 typedef 为目标平台的类。
进行平台判断的代码在Runtime/Core/Public/HAL/PlatformMisc.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Runtime/Core/Public/HAL/PlatformMisc.h
// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.
#pragma once

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

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

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

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

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

Lua: 从内存加载 module

从内存执行:

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

添加到 PRELOAD 中:

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

直接添加到 LOADED 中:

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

gituhb 快捷代码搜索

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

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

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

控制写入文件 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);

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:

打包时添加外部文件

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^ 关键字

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

APK 包中 OBB 文件

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

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

1
2
3
4
5
6
7
8
9
10
\---FGame
\---Content
+---Movies
| logo.mp4
| SparkMore.mp4
|
\---Paks
pakchunk0-Android_ASTC.pak
pakchunk1-Android_ASTC.pak
pakchunk2-Android_ASTC.pak

UE 编译环境的 VS 安装配置

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
{
"version": "1.0",
"components": [
"Microsoft.VisualStudio.Workload.NativeDesktop",
"Microsoft.VisualStudio.Workload.Python",
"Microsoft.VisualStudio.Workload.Node",
"Microsoft.VisualStudio.Workload.NativeGame",
"Microsoft.VisualStudio.Workload.NativeCrossPlat",
"microsoft.visualstudio.component.debugger.justintime",
"microsoft.net.component.4.6.2.sdk",
"microsoft.net.component.4.6.2.targetingpack",
"microsoft.net.component.4.7.sdk",
"microsoft.net.component.4.7.targetingpack",
"microsoft.net.component.4.7.1.sdk",
"microsoft.net.component.4.7.1.targetingpack",
"microsoft.net.component.4.7.2.sdk",
"microsoft.net.component.4.7.2.targetingpack",
"microsoft.visualstudio.component.vc.diagnostictools",
"microsoft.visualstudio.component.vc.cmake.project",
"microsoft.visualstudio.component.vc.atl",
"microsoft.visualstudio.component.vc.testadapterforboosttest",
"microsoft.visualstudio.component.vc.testadapterforgoogletest",
"microsoft.visualstudio.component.winxp",
"microsoft.visualstudio.component.vc.cli.support",
"microsoft.visualstudio.component.vc.modules.x86.x64",
"component.incredibuild",
"microsoft.component.netfx.core.runtime",
"microsoft.component.cookiecuttertools",
"microsoft.component.pythontools.web",
"microsoft.visualstudio.component.classdesigner",
"microsoft.net.component.3.5.developertools",
"component.unreal.android",
"component.linux.cmake",
"microsoft.component.helpviewer",
"microsoft.visualstudio.component.vc.clangc2",
"microsoft.visualstudio.component.vc.tools.14.14"
]
}

下载地址:vs_installer.vsconfig

扫描资源引用时需注意 Redirector

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

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

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

Cook 执行代码

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

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

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

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 的对象进去。

移动设备的渲染预览

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

Android 项目设置

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

在构造函数中用 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

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

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

1
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 来接收程序的打印输出。

绑定 FTicker

在非 Actor 的对象上如果想要使用 Tick,可以使用下列代码:

1
FDelegateHandle TickHandle = FTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateRaw(this,&UGameExtensionSettings::Tick);

PS:FCoreDelegatesFCoreUObjectDelegates 中有很多有用的代理。

地图加载的 Delegate

FCoreUObjectDelegates中的这两个代理在地图加载时和地图加载完成时会调用,可以用来显示加载地图时的过场:

1
2
FCoreUObjectDelegates::PreLoadMap.AddUObject(this, &UMarinGameInstance::BeginLoadingScreen);
FCoreUObjectDelegates::PostLoadMapWithWorld.AddUObject(this, &UMarinGameInstance::EndL

StaticLoadClass

可以从通过一个字符串来加载 UClass:

1
UClass* UserWidgetClass = StaticLoadClass(UUserWidget::StaticClass(), NULL, TEXT("WidgetBlueprintGeneratedClass'/Game/TEST/BP_UserWidget.BP_UserWidget_C'"));

同理也有StaticLoadObject

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 来执行命令了。

在游戏项目中添加编辑器模块

首先创建一个空的 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

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>& 然后重新编译即可。

数据成员初始化顺序必须要与声明顺序一致

如:

1
2
3
4
5
6
7
8
9
class A
{
// error in UE4,ill in c++
A():dval(0.0f),ival(0){}
public:
int ival;
double dval;
};

会有下列错误:

1
error C5038: data member 'UTcpNetPeer::RecvMessageDataRemaining' will be initialized after data member 'UTcpNetPeer::ConnectionRetryTimes'

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 文件

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。

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)。

创建存储文件的提示

如下图这种效果:

代码:

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

获取工程所有的 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;
}

AssetRegistry 的 Asset 概念

UE 中的 Asset 在使用时有以下三个概念:

  • PackagePath:/Game/TEST/BP_Actor.BP_Actor
  • LongPackageName:/Game/TEST/BP_Actor
  • AssetName: BP_Actor

获取所有支持的平台

在 ModuleTargetPlatform中可以获取:

1
TArray<ITargetPlatform*> Platforms = GetTargetPlatformManager()->GetTargetPlatforms();

但是注意,TargetPlatform是属于 Developer 的模块,不要在 Runtime 的模块中使用,否则会打包失败。

所以用宏简单裹了一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
TArray<FString> UFlibAssetManageHelper::GetAllTargetPlatform()
{
#ifdef __DEVELOPER_MODE__
TArray<ITargetPlatform*> Platforms = GetTargetPlatformManager()->GetTargetPlatforms();
TArray<FString> result;

for (const auto& PlatformItem : Platforms)
{
result.Add(PlatformItem->PlatformName());
}

#else
TArray<FString> result = {
"AllDesktop",
"MacClient",
"MacNoEditor",
"MacServer",
"Mac",
"WindowsClient",
"WindowsNoEditor",
"WindowsServer",
"Windows",
"Android",
"Android_ASTC",
"Android_ATC",
"Android_DXT",
"Android_ETC1",
"Android_ETC1a",
"Android_ETC2",
"Android_PVRTC",
"AndroidClient",
"Android_ASTCClient",
"Android_ATCClient",
"Android_DXTClient",
"Android_ETC1Client",
"Android_ETC1aClient",
"Android_ETC2Client",
"Android_PVRTCClient",
"Android_Multi",
"Android_MultiClient",
"HTML5",
"IOSClient",
"IOS",
"TVOSClient",
"TVOS",
"LinuxClient",
"LinuxNoEditor",
"LinuxServer",
"Linux",
"Lumin",
"LuminClient"
};

#endif
return result;
}

递归扫描目录

与直接使用 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 的筛选类。

获取系统环境变量

可以使用 FPlatformMisc::GetEnvironmentVariable 来拿:

1
FString FindEnvGitPath = FPlatformMisc::GetEnvironmentVariable(TEXT("GIT_PATH"));

运行时获取 git diff 的内容

做热更新有用到:

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

}
}
}
}
}

使用方法:

运行结果:

Android 屏幕方向

Project Setting-Platforms-Android-APK Packageing-Orientation

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

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;

}

Mount pak in Runtime

前面的笔记中提到,UE 的项目打包后会自动加载三个路径下的 Paks/ 下的所有 Pak 文件,为了热更新的需求,需要在运行时自己指定加载 Pak,翻了一下代码,可以写了个挂载的函数。

注意!注意!注意!在编辑器模式下运行无作用,没有任何逻辑,因为编辑器模式也不需要加载 Pak,引擎里部分逻辑在编辑器模式下不执行,如果非要在编辑器下 Mount 引擎会 Crash,以 Standalone 模式运行也一样,都属于编辑器模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
bool MountPak(const FString& PakPath, int32 PakOrder, const FString& InMountPoint)
{
bool bMounted = false;
#if !WITH_EDITOR
FPakPlatformFile* PakFileMgr=(FPakPlatformFile*)FPlatformFileManager::Get().GetPlatformFile(FPakPlatformFile::GetTypeName());
if (!PakFileMgr)
{
UE_LOG(LogTemp, Log, TEXT("GetPlatformFile(TEXT(\"PakFile\") is NULL"));
return false;
}

PakOrder = FMath::Max(0, PakOrder);

if (FPaths::FileExists(PakPath) && FPaths::GetExtension(PakPath) == TEXT("pak"))
{
const TCHAR* MountPount = InMountPoint.GetCharArray().GetData();
if (PakFileMgr->Mount(*PakPath, PakOrder,MountPount))
{
UE_LOG(LogTemp, Log, TEXT("Mounted = %s, Order = %d, MountPoint = %s"), *PakPath, PakOrder, !MountPount ? TEXT("(NULL)") : MountPount);
bMounted = true;
}
else {
UE_LOG(LogTemp, Error, TEXT("Faild to mount pak = %s"), *PakPath);
bMounted = false;
}
}

#endif
return bMounted;
}

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

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

前面笔记中写到,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 生成的导航数据。

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

解决模块间的名字冲突

在写 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 掉,确保不被其他的代码造成影响。

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

在蓝图中使用参数确定返回类型

在使用诸如 SpawnActor 的时候,传进来的 UClass 是什么就会直接返回具体的类型,避免 Cast 的操作。

使用代码实现的话需要在 UFUNCTIONmeta参数中添加DeterminesOutputType,根据传入的参数确定返回的类型:

1
2
UFUNCTION(BlueprintCallable, meta = (DeterminesOutputType = "ObjectClass"))
static class UObject* CreateObject(class TSubclassOf<class UObject> ObjectClass);

SetMasterPoseComponent

当创建模块化角色的时候,需要将拆开的身体的各个部分跟随着一个主 Pose 运动。

但是当对一个 USkeletonMeshComponent 调用该方法时会关闭掉当前 Mesh 的物理资产(与物理相关的都会关闭)
可以看 Runtime/Engine/Private/Components/SkinnedMeshComponent.cpp 中的 SetMasterPoseComponent 实现,其中调用了RecreatePhysicsState();

这个问题再 4.1x 时代就有人给 Unreal Engine Issus 提过 bug,但是官方表示不是 bug 不会修复。

二元操作符的 AddPin

在 UE 中有很多这样的节点:

这个 AddPin,可以在蓝图中很方便地进行拼接,其本质就从前往后的项依次两两结合,组成新的项,再与后续的项两两结合。

其在代码里的写法为 (以Append 为例):

1
2
3
4
5
6
7
8
/**
* Concatenates two strings together to make a new string
* @param A - The original string
* @param B - The string to append to A
* @returns A new string which is the concatenation of A+B
*/
UFUNCTION(BlueprintPure, meta=(DisplayName = "Append", CommutativeAssociativeBinaryOperator = "true"), Category="Utilities|String")
static FString Concat_StrStr(const FString& A, const FString& B);

其重点就是 static 函数 +BlueprintPure标记 +CommutativeAssociativeBinaryOperator = "true"元标记。

图像质量级别

大多数游戏都提供了游戏画面参数的自定义设置,UE 自己内置了五个不同的渲染级别:

  • Low(0)
  • Medium(1)
  • Hight(1)
  • Epic(3)
  • Cinematic(4)

这个五个级别可以通过在 UGameUserSettings 中调用 FQualityLevels::SetFromSingleQualityLevel 来设置:

1
ScalabilityQuality.SetFromSingleQualityLevel(GraphicsLevel);

因为在 UGameUserSettings 中,它也可以被定义为UPROPERTY(config),实现存储设置的目的。

在 UGameUserSettings 中,使用 LoadSettings 函数会加载默认的 Quality 设置:

Runtime\Engine\Private\GameUserSettings.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
void UGameUserSettings::LoadSettings(bool bForceReload/*=false*/)
{
QUICK_SCOPE_CYCLE_COUNTER(GameUserSettings_LoadSettings);

if (bForceReload)
{
LoadConfigIni(bForceReload);
}
LoadConfig(GetClass(), *GGameUserSettingsIni);

// Note: Scalability::LoadState() should not be needed as we already loaded the settings earlier (needed so the engine can startup with that before the game is initialized)
ScalabilityQuality = Scalability::GetQualityLevels();

// Allow override using command-line settings
bool bDetectingResolution = ResolutionSizeX == 0 || ResolutionSizeY == 0;

if (bDetectingResolution)
{
ConfirmVideoMode();
}

// Update r.FullScreenMode CVar
SetPreferredFullscreenMode(PreferredFullscreenMode);
}

其中 Scalability::GetQualityLevels 获取了控制台变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
FQualityLevels GetQualityLevels()
{
FQualityLevels Ret;

// Only suggested way to get the current state - don't get CVars directly
if (!GScalabilityUsingTemporaryQualityLevels)
{
Ret.ResolutionQuality = CVarResolutionQuality.GetValueOnGameThread();
Ret.ViewDistanceQuality = CVarViewDistanceQuality.GetValueOnGameThread();
Ret.AntiAliasingQuality = CVarAntiAliasingQuality.GetValueOnGameThread();
Ret.ShadowQuality = CVarShadowQuality.GetValueOnGameThread();
Ret.PostProcessQuality = CVarPostProcessQuality.GetValueOnGameThread();
Ret.TextureQuality = CVarTextureQuality.GetValueOnGameThread();
Ret.EffectsQuality = CVarEffectsQuality.GetValueOnGameThread();
Ret.FoliageQuality = CVarFoliageQuality.GetValueOnGameThread();
Ret.ShadingQuality = CVarShadingQuality.GetValueOnGameThread();
}
else
{
Ret = GScalabilityBackupQualityLevels;
}

return Ret;
}

这些控制台变量,都是定义在 Runtime\Engine\Private\Scalability.cpp 文件中的:

Runtime\Engine\Private\Scalability.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
static TAutoConsoleVariable<float> CVarResolutionQuality(
TEXT("sg.ResolutionQuality"),
100.0f,
TEXT("Scalability quality state (internally used by scalability system, ini load/save or using SCALABILITY console command)\n")
TEXT(" 10..100, default: 100"),
ECVF_ScalabilityGroup);

static TAutoConsoleVariable<int32> CVarViewDistanceQuality(
TEXT("sg.ViewDistanceQuality"),
Scalability::DefaultQualityLevel,
TEXT("Scalability quality state (internally used by scalability system, ini load/save or using SCALABILITY console command)\n")
TEXT(" 0:low, 1:med, 2:high, 3:epic, 4:cinematic, default: 3"),
ECVF_ScalabilityGroup);

static TAutoConsoleVariable<int32> CVarAntiAliasingQuality(
TEXT("sg.AntiAliasingQuality"),
Scalability::DefaultQualityLevel,
TEXT("Scalability quality state (internally used by scalability system, ini load/save or using SCALABILITY console command)\n")
TEXT(" 0:low, 1:med, 2:high, 3:epic, 4:cinematic, default: 3"),
ECVF_ScalabilityGroup);

static TAutoConsoleVariable<int32> CVarShadowQuality(
TEXT("sg.ShadowQuality"),
Scalability::DefaultQualityLevel,
TEXT("Scalability quality state (internally used by scalability system, ini load/save or using SCALABILITY console command)\n")
TEXT(" 0:low, 1:med, 2:high, 3:epic, 4:cinematic, default: 3"),
ECVF_ScalabilityGroup);

static TAutoConsoleVariable<int32> CVarPostProcessQuality(
TEXT("sg.PostProcessQuality"),
Scalability::DefaultQualityLevel,
TEXT("Scalability quality state (internally used by scalability system, ini load/save or using SCALABILITY console command)\n")
TEXT(" 0:low, 1:med, 2:high, 3:epic, 4:cinematic, default: 3"),
ECVF_ScalabilityGroup);

static TAutoConsoleVariable<int32> CVarTextureQuality(
TEXT("sg.TextureQuality"),
Scalability::DefaultQualityLevel,
TEXT("Scalability quality state (internally used by scalability system, ini load/save or using SCALABILITY console command)\n")
TEXT(" 0:low, 1:med, 2:high, 3:epic, 4:cinematic, default: 3"),
ECVF_ScalabilityGroup);

static TAutoConsoleVariable<int32> CVarEffectsQuality(
TEXT("sg.EffectsQuality"),
Scalability::DefaultQualityLevel,
TEXT("Scalability quality state (internally used by scalability system, ini load/save or using SCALABILITY console command)\n")
TEXT(" 0:low, 1:med, 2:high, 3:epic, 4:cinematic, default: 3"),
ECVF_ScalabilityGroup);

static TAutoConsoleVariable<int32> CVarFoliageQuality(
TEXT("sg.FoliageQuality"),
3,
TEXT("Scalability quality state (internally used by scalability system, ini load/save or using SCALABILITY console command)\n")
TEXT(" 0:low, 1:med, 2:high, 3:epic, 4:cinematic, default: 3"),
ECVF_ScalabilityGroup);

static TAutoConsoleVariable<int32> CVarShadingQuality(
TEXT("sg.ShadingQuality"),
Scalability::DefaultQualityLevel,
TEXT("Scalability quality state (internally used by scalability system, ini load/save or using SCALABILITY console command)\n")
TEXT(" 0:low, 1:med, 2:high, 3:epic, 4:cinematic, default: 3"),
ECVF_ScalabilityGroup);

static TAutoConsoleVariable<int32> CVarViewDistanceQuality_NumLevels(
TEXT("sg.ViewDistanceQuality.NumLevels"),
5,
TEXT("Number of settings quality levels in sg.ViewDistanceQuality\n")
TEXT(" default: 5 (0..4)"),
ECVF_ReadOnly);

static TAutoConsoleVariable<int32> CVarAntiAliasingQuality_NumLevels(
TEXT("sg.AntiAliasingQuality.NumLevels"),
5,
TEXT("Number of settings quality levels in sg.AntiAliasingQuality\n")
TEXT(" default: 5 (0..4)"),
ECVF_ReadOnly);

static TAutoConsoleVariable<int32> CVarShadowQuality_NumLevels(
TEXT("sg.ShadowQuality.NumLevels"),
5,
TEXT("Number of settings quality levels in sg.ShadowQuality\n")
TEXT(" default: 5 (0..4)"),
ECVF_ReadOnly);

static TAutoConsoleVariable<int32> CVarPostProcessQuality_NumLevels(
TEXT("sg.PostProcessQuality.NumLevels"),
5,
TEXT("Number of settings quality levels in sg.PostProcessQuality\n")
TEXT(" default: 5 (0..4)"),
ECVF_ReadOnly);

static TAutoConsoleVariable<int32> CVarTextureQuality_NumLevels(
TEXT("sg.TextureQuality.NumLevels"),
5,
TEXT("Number of settings quality levels in sg.TextureQuality\n")
TEXT(" default: 5 (0..4)"),
ECVF_ReadOnly);

static TAutoConsoleVariable<int32> CVarEffectsQuality_NumLevels(
TEXT("sg.EffectsQuality.NumLevels"),
5,
TEXT("Number of settings quality levels in sg.EffectsQuality\n")
TEXT(" default: 5 (0..4)"),
ECVF_ReadOnly);

static TAutoConsoleVariable<int32> CVarFoliageQuality_NumLevels(
TEXT("sg.FoliageQuality.NumLevels"),
5,
TEXT("Number of settings quality levels in sg.FoliageQuality\n")
TEXT(" default: 5 (0..4)"),
ECVF_ReadOnly);

static TAutoConsoleVariable<int32> CVarShadingQuality_NumLevels(
TEXT("sg.ShadingQuality.NumLevels"),
5,
TEXT("Number of settings quality levels in sg.ShadingQuality\n")
TEXT(" default: 5 (0..4)"),
ECVF_ReadOnly);

可以把它们加到项目的 Config/DefaultGameUserSettings.ini 文件的 [ScalabilityGroups] 下:

1
2
[ScalabilityGroups]
sg.ViewDistanceQuality=4

引擎在启动时去调用了:

LaunchEngineLoop.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int32 FEngineLoop::PreInitPreStartupScreen(const TCHAR* CmdLine)
{
// ...
{
{
SCOPED_BOOT_TIMING("InitScalabilitySystem");
// Init scalability system and defaults
Scalability::InitScalabilitySystem();
}

{
SCOPED_BOOT_TIMING("InitializeCVarsForActiveDeviceProfile");
// Set all CVars which have been setup in the device profiles.
// This may include scalability group settings which will override
// the defaults set above which can then be replaced below when
// the game user settings are loaded and applied.
UDeviceProfileManager::InitializeCVarsForActiveDeviceProfile();
}

{
SCOPED_BOOT_TIMING("Scalability::LoadState");
// As early as possible to avoid expensive re-init of subsystems,
// after SystemSettings.ini file loading so we get the right state,
// before ConsoleVariables.ini so the local developer can always override.
// after InitializeCVarsForActiveDeviceProfile() so the user can override platform defaults
Scalability::LoadState((bHasEditorToken && !GEditorSettingsIni.IsEmpty()) ? GEditorSettingsIni : GGameUserSettingsIni);
}

if (FPlatformMisc::UseRenderThread())
{
GUseThreadedRendering = true;
}
}
// ...
}

Scalability::LoadState在非 Editor 模式下传递的是GGameUserSettingsIni,加载代码如下:

Runtime\Engine\Private\Scalability.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
void LoadState(const FString& IniName)
{
check(!IniName.IsEmpty());

// todo: could be done earlier
InitScalabilitySystem();

// Use existing quality levels - Defaults with device profile customization
FQualityLevels State = GetQualityLevels();

const TCHAR* Section = TEXT("ScalabilityGroups");

// looks like cvars but here we just use the name for the ini
GConfig->GetFloat(Section, TEXT("sg.ResolutionQuality"), State.ResolutionQuality, IniName);
GConfig->GetInt(Section, TEXT("sg.ViewDistanceQuality"), State.ViewDistanceQuality, IniName);
GConfig->GetInt(Section, TEXT("sg.AntiAliasingQuality"), State.AntiAliasingQuality, IniName);
GConfig->GetInt(Section, TEXT("sg.ShadowQuality"), State.ShadowQuality, IniName);
GConfig->GetInt(Section, TEXT("sg.PostProcessQuality"), State.PostProcessQuality, IniName);
GConfig->GetInt(Section, TEXT("sg.TextureQuality"), State.TextureQuality, IniName);
GConfig->GetInt(Section, TEXT("sg.EffectsQuality"), State.EffectsQuality, IniName);
GConfig->GetInt(Section, TEXT("sg.FoliageQuality"), State.FoliageQuality, IniName);
GConfig->GetInt(Section, TEXT("sg.ShadingQuality"), State.ShadingQuality, IniName);

// If possible apply immediately, else store in backup so we can re-apply later
if (!GScalabilityUsingTemporaryQualityLevels)
{
SetQualityLevels(State);
}
else
{
GScalabilityBackupQualityLevels = State;
}
}

使用物理资产优化动态阴影消耗

在 UE 中打开 SkeletalMesh,在AssetDetails 中找到Lighting-Shadow Physics Asset,将其指定为物理资产PhysicsAsset

然后打开使用这个 SkeletalMeshSkeletalMesh Component,将其 Lithing-Capsule Direct Shadow 选项打勾,这个 Skeletal Mesh 就会被使用 Physics Asset 的框来计算阴影了,会降低消耗。

GameUserSettings

在 UE 中,当我们需要保存玩家的一些数据时,比如玩家的游戏设置:分辨率、画面质量、按键等等。这些与游戏无关的数据可以使用 GameUserSettings 来存储,游戏的数据还是使用 SaveGame 来存储。

UE 默认使用的 GameUserSettings 类是 UGameUserSettings,我们可以在Config/DefaultEngine.ini 中的 /Script/Engine.Engine 这个 Section 下通过 GameUserSettingsClassName 指定:

1
2
[/Script/Engine.Engine]
GameUserSettingsClassName=/Script/MODULE_NAME.GAME_USER_SETTING_CLASS
  • MODULE_NAME指的是所要指定的类是定义在什么 Module 下的。
  • GAME_USER_SETTING_CLASS:指所指定的类的名字,忽略U
1
2
[/Script/Engine.Engine]
GameUserSettingsClassName=/Script/GWorld.SGGameUserSettings

GameUserSettingsClassNameUGameUserSettings::PreloadResolutionSettings 这个 static 成员函数中中被加载和读取,而它又在 FEngineLoop::PreInit 中被调用。真正的类对象是在 UEngine::InitializeObjectReferences(UnrealEngine.cpp) 中被加载。

我们指定的类必须是继承自 UGameUserSettings 的:

1
2
3
4
5
6
UCLASS(BlueprintType,Blueprintable)
class GWORLD_API USGGameUserSettings : public UGameUserSettings
{
GENERATED_UCLASS_BODY()
// ...
}

我们需要重写其中的几个函数:

1
2
virtual void ApplySettings(bool bCheckForCommandLineOverrides) override;
virtual void SetToDefaults()override;

在这两个函数中,根据所有的用户可配置信息做事情。

在我们自己的 GameUserSetting 类中,可以使用 UPROPERTY(config) 声明变量,标识其可以被存储到配置文件中。

当在游戏中选项设置完毕,调用 ApplySettings 应用设置之后,可以调用 SaveSettings 将我们的 GameUserSetting 中所有的标记为 UPROPERTY(Config) 的成员都存储到 GameUserSettings.ini 中。

注意:在项目和非 shipping 模式打包的项目,GameUserSettings.ini文件的路径为:

1
PROJECT_DIR/Saved/Config/Windows/GameUserSettings.ini

Shipping 模式打包的项目,Saved文件夹不在打包的目录下,而是在:

1
C:\Users\USER_NAME\AppData\Local\PROJECT_NAME

TAutoConsoleVariable

TAutoConsoleVariable可以让我们往 UE 的项目中添加 Console 命令:

1
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
// Runtime/Core/Pulic/HAL/IConsoleManager.h

template <class T>
class TAutoConsoleVariable : public FAutoConsoleObject
{
public:
/**
* Create a float, int or string console variable
* @param Name must not be 0
* @param Help must not be 0
* @param Flags bitmask combined from EConsoleVariableFlags
*/
TAutoConsoleVariable(const TCHAR* Name, const T& DefaultValue, const TCHAR* Help, uint32 Flags = ECVF_Default);

T GetValueOnGameThread() const
{
return Ref->GetValueOnGameThread();
}

T GetValueOnRenderThread() const
{
return Ref->GetValueOnRenderThread();
}

T GetValueOnAnyThread(bool bForceGameThread = false) const
{
return Ref->GetValueOnAnyThread(bForceGameThread);
}

/** Dereference back to a variable**/
FORCEINLINE IConsoleVariable& operator*()
{
return *AsVariable();
}