Unreal Engine 4 版本控制的工作原理以及一些可优化项
UE4 的版本迭代
Unreal Engine 在保证 Editor 以及 cooked 内容能在多个不同引擎版本间通用方面表现出色。同时,为方便程序员在不同版本间更改数据结构,Unreal Engine 提供了相应机制。本文将通过 GetLinkerUE4Version()
及其相关函数,深入分析版本间的相关内容。
首先,我们要了解引擎不同种类的版本:
- UE4Version:每当 Epic 对原有代码进行修改,该数字就会增加。此版本号只能由 Epic 改动。
- UE4LicenseeVersion:代码改动时,该数字同样会增加。这是一个“安全”的版本号,开发者可以在此基础上进行代码更改,该数字可由开发者(Licensees)修改。
- CustomVersion:这个版本号可用于不同工程师间的迭代,且不会引发冲突。
如何使用 UE4 的版本迭代
通常,我们使用 GetLinkerUE4Version()
函数和 GetLinkerLicenseeUE4Version()
来处理不同版本间的问题。
下面给出一个 Epic 使用 GetLinkerUE4Version()
函数修正内容的例子:
在某个时间段,有人添加了一个用于设定 Visibility 的 Blueprint 变量,但出现拼写错误,丢失了 “visibility” 中的第三个 ‘i’。后来,有人发现了这个问题,在 UWidgetBlueprint::PostLoad()
函数中加入如下代码来修正错误:
if (GetLinkerUE4Version() < VER_UE4_RENAME_WIDGET_VISIBILITY)
{
static const FName Visiblity(TEXT("Visiblity"));
static const FName Visibility(TEXT("Visibility"));
for (FDelegateEditorBinding& Binding : Bindings)
{
if (Binding.PropertyName == Visiblity)
{
Binding.PropertyName = Visibility;
}
}
}
然后,在 ObjectVersion.h
头文件中增加了针对此修改的宏:
// Rename Visiblity on widgets to Visibility
VER_UE4_RENAME_WIDGET_VISIBILITY,
整个工作流程如下:
- 当一个较老版本且有拼写错误的包被载入时,该包的版本号会小于
VER_UE4_RENAME_WIDGET_VISIBILITY
。 - 此时,上述代码会被激活,用于修正拼写错误。
- 如果该包被保存,它将使用正确的拼写,并将版本号设定为最新版本号。
这种做法在很多场景都有应用,例如:
- 在类中增加或废除新变量。
- 增加需要改变的 Patch。
版本号对应宏
在头文件 ObjectVersion.h
中,记录了 Epic 处理版本迭代的方式。首先是 EUnrealEngineObjectUE4Version
:
enum EUnrealEngineObjectUE4Version
{
VER_UE4_OLDEST_LOADABLE_PACKAGE = 214,
// Removed restriction on blueprint-exposed variables from being read-only
VER_UE4_BLUEPRINT_VARS_NOT_READ_ONLY,
// Added manually serialized element to UStaticMesh (precalculated nav collision)
VER_UE4_STATIC_MESH_STORE_NAV_COLLISION,
// Changed property name for atmospheric fog
VER_UE4_ATMOSPHERIC_FOG_DECAY_NAME_CHANGE,
...
// -----<new versions can be added before this line>-------------------------------------------------
// - this needs to be the last line (see note below)
VER_UE4_AUTOMATIC_VERSION_PLUS_ONE,
VER_UE4_AUTOMATIC_VERSION = VER_UE4_AUTOMATIC_VERSION_PLUS_ONE - 1
};
同样,在同一个头文件中还有 EUnrealEngineObjectLicenseeUE4Version
:
enum EUnrealEngineObjectLicenseeUE4Version
{
VER_LIC_NONE = 0,
// - this needs to be the last line (see note below)
VER_LIC_AUTOMATIC_VERSION_PLUS_ONE,
VER_LIC_AUTOMATIC_VERSION = VER_LIC_AUTOMATIC_VERSION_PLUS_ONE - 1
};
EUnrealEngineObjectLicenseeUE4Version
有以下注意事项:
- 如前文所述,开发者只能在
EUnrealEngineObjectLicenseeUE4Version
列表中进行迭代。 - 只能在该列表的最下方添加内容。
- 使用 Perforce 等代码管理工具时,务必注意处理包保存后的情况,否则包容易出现问题。最简单的解决办法是,当版本号改变时,不允许 check in。
我们为何只能使用 UE4LicenseeVersion
呢?考虑以下情况:
- 一个项目从 UE4.11 开始开发。
- 我们对 UE4Version 进行迭代,添加了一些自认为很酷的功能。
- 我们合并了 UE4.12 版本,却发现出现冲突 —— Epic 也在 4.12 版本迭代了这部分功能。此时,我们有两种选择:
- 将 Epic 的新版本号插入列表末尾,保持自己的迭代版本号不变。这意味着我们可以使用自己的内容,但 Epic 提供的一些新功能可能无法使用。
- 将我们的版本号移到最后,但这样之前实现的功能可能会出现严重问题。
上述问题可能是致命的,我们在 UE3 开发时就犯过类似错误,花费了大量时间进行重新打补丁。因此,再次提醒 —— 只使用 UE4LicenseeVersion
。
当版本号出现问题
在长期的开发过程中,我经历了许多因版本号冲突导致的崩溃问题,希望这些经验能给读者带来启发。
要理解这些问题的根源,我们首先要了解版本号的运作方式。假设一个包使用很久以前的版本号 256 进行存储,而当前版本号是 259。当这个包被载入时,它需要经过 257、258 和 259 三个版本的迭代处理。
以下是我总结的最可能导致版本号出现问题的情况:
- 合并迭代逻辑但未提交版本控制:合并了其他迭代逻辑,但尚未提交版本控制逻辑,而团队其他程序员还未合并这些迭代逻辑。
- 在本地测试时没有问题。
- 其他开发者开始抱怨崩溃。因为其他开发者已经更新了迭代逻辑,在他们的版本中,这个迭代有不同的版本号。而你将自己的迭代放到了列表末尾,系统出现混乱。当包被载入时,引擎认为你的迭代已经载入(但实际上没有)。
- 集成新的版本迭代:从 Epic 集成了一个新的版本迭代,并在版本列表中加入了新的宏定义。
- 然而,在 Epic 的
ObjectVersion.h
头文件中,在你集成的位置前后,Epic 做了一些新的改动。 - 之后进行了完整集成,添加了 Epic 新增的宏定义。此时,由于某些 patch 的顺序混乱,开始出现崩溃和 bug。似乎唯一“正确”的方法是只添加集成之后的内容,但只要 Epic 提供的官方内容在其他地方被保存,这个包仍可能出现问题。
我们发现的一些可优化项
项目内容 cooked 完成后,Saved 文件夹下所有包的版本号都会被设为最大。基于此,我有了以下优化思路。
即使游戏在载入已 cooked 的内容,引擎也不会默认其为最新版本。这样,当改变某些包的版本号时,无需重新 cooked 所有内容。但频繁检查内容是否需要更新的操作开销较大,尤其对于 Shipping 包,这并非必要。因此,我们做如下假设:
- Shipping 包仅在已 cooked 的内容下运行。
- Shipping 包中只有完全 cooked 到最新版本的内容。
如果这些假设成立,我们可以进行优化,告知编译器无需运行 patching 代码。即让编译器每次检查 if (GetLinkerUE4Version() < VER_…)
之类的代码都失败,每次检查 if (GetLinkerUE4Version() >= VER_…)
之类的代码都成功。最简单的方法是让每个 Get—Version()
类型的函数都返回当前最新的版本号。
我们在头文件中进行处理,确保编译器将其设为内联。具体操作如下:
在 UObjectBaseUtility.h
头文件的开头添加:
#define ASSUME_UE4VERSIONS_ARE_LATEST (UE_BUILD_SHIPPING && !WITH_EDITORONLY_DATA)
需要注意,项目中的设定是 Shipping 包只能在运行 cooked builds 时运行,不同项目需进行相应调整。
在 GetLinkerUE4Version()
前加入如下代码:
#if ASSUME_UE4VERSIONS_ARE_LATEST
FORCEINLINE int32 GetLinkerUE4Version() const { return VER_LATEST_ENGINE_UE4; }
FORCEINLINE int32 GetLinkerLicenseeUE4Version() const { return VER_LATEST_ENGINE_LICENSEEUE4; }
FORCEINLINE int32 GetLinkerCustomVersion(FGuid CustomVersionKey) const { return MAX_int32; }
#else // ASSUME_UE4VERSIONS_ARE_LATEST
/**
* Returns the UE4 version of the linker for this object.
在 GetLinkerCustomVersion()
后面加入如下代码:
int32 GetLinkerCustomVersion(FGuid CustomVersionKey) const;
#endif // ASSUME_UE4VERSIONS_ARE_LATEST
现在开始重写版本控制函数的 cpp 代码,在 ObjectBaseUtility.cpp
文件开头,include
语句之后加入:
#include "CoreUObjectPrivate.h"
#if !ASSUME_UE4VERSIONS_ARE_LATEST
在文件末尾加入:
#endif // !ASSUME_UE4VERSIONS_ARE_LATEST
完成以上操作后,编译器应该能够完全去掉 patching 代码,使整个代码更加简洁。
其他的意见(给 Epic)
我见过很多开发者直接在 EUnrealEngineObjectUE4Version
中加入自己的宏定义,建议尽量避免这种做法。如果是我,会添加一些注释,将:
// -----<new versions can be added before this line>-------------------------------------------------
// - this needs to be the last line (see note below)
VER_UE4_AUTOMATIC_VERSION_PLUS_ONE,
VER_UE4_AUTOMATIC_VERSION = VER_UE4_AUTOMATIC_VERSION_PLUS_ONE - 1
};
改为:
// LICENSEES SHOULD NOT ADD CODE HERE!
// PLEASE USE EUnrealEngineObjectLicenseeUE4Version INSTEAD!
// ADDING CODE HERE WILL MAKE FUTURE INTEGRATIONS HARD!
// -----<new versions can be added before this line>-------------------------------------------------
VER_UE4_AUTOMATIC_VERSION_PLUS_ONE,
VER_UE4_AUTOMATIC_VERSION = VER_UE4_AUTOMATIC_VERSION_PLUS_ONE - 1
};
另外,将如下代码:
enum EUnrealEngineObjectLicenseeUE4Version
{
VER_LIC_NONE = 0,
// - this needs to be the last line (see note below)
VER_LIC_AUTOMATIC_VERSION_PLUS_ONE,
VER_LIC_AUTOMATIC_VERSION = VER_LIC_AUTOMATIC_VERSION_PLUS_ONE - 1
};
改为:
enum EUnrealEngineObjectLicenseeUE4Version
{
VER_LIC_NONE = 0,
// -----<new versions can be added before this line>-------------------------------------------------
VER_LIC_AUTOMATIC_VERSION_PLUS_ONE,
VER_LIC_AUTOMATIC_VERSION = VER_LIC_AUTOMATIC_VERSION_PLUS_ONE - 1
};
还可以尝试将版本控制相关的名字从 EUnrealEngineObjectUE4Version
重命名为 EUnrealEngineObjectPleasePleaseOnlyForEpicChangesUE4Version
等,务必让用户了解直接更改这部分内容的危害,我甚至见过经验丰富的开发者在此栽跟头。
点击“泰课资讯”查看更多相关技术文章。