Unreal Engine 4 版本控制的工作原理以及一些可优化项

2017年03月16日 17:10 0 点赞 1 评论 更新于 2020-01-14 19:42
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 等,务必让用户了解直接更改这部分内容的危害,我甚至见过经验丰富的开发者在此栽跟头。

点击“泰课资讯”查看更多相关技术文章。

作者信息

孟子菇凉

孟子菇凉

共发布了 1189 篇文章