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,
  • 因此,整个的工作流程如下:
  1. 当一个比较老版本的有拼写错误的包被载入时,这个包的版本号会比VER_UE4_RENAME_WIDGET_VISIBILITY更小。
  2. 此时,上面的代码会被激活,用于修正拼写错误。
  3. 如果这个包被保存了,它会使用正确的拼写,并且把版本号设定为最新的版本号。

这种做法在很多地方都有使用,例如:

  • 在类里面增加/废除一些新的变量。
  • 增加一些我们想要改变的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有如下的注意事项:

  • 就如同上面提到的,licensees**只能**在EUnrealEngineObjectLicenseeUE4Version列表中进行迭代
  • 只能在该列表的最下面进行添加
  • 当使用例如Perforce之类的代码管理工具时,务必注意针对于包被保存后进行处理,否则这个包容易被玩坏。针对于这种情况,最简单的解决方案就是当版本号改变的时候,不允许check in

我们为何只能用UE4LicenseeVersion呢?考虑如下的状况:

  • 一个项目使用UE4.11开始
  • 我们对于UE4Version进行了迭代,加了一些我们觉得很酷的功能
  • 我们merge了UE4.12,然后发现我们出现了一个冲突 —— Epic也在4.12迭代了这一块的功能…在此情况下,我们只能: 
    • 把Epic的新版本号插入到列表里面的最后而将我们自己的迭代版本号设为不变 - 这样就意味着我们可以使用我们自己的内容,但是Epic提供的一些新的功能应该都用不了了。
    • 把我们的版本号移到最后 - 但是这样先前所做的功能便会出很大问题。

以上所出现的问题很可能是致命的,我们当初在UE3里面也犯过这种错误,我们花了好一阵时间进行re-patch。所以我再次提醒 —— 只使用UE4LicenseeVersion.

当版本号出现问题

在这么久的开发生涯中,我经历了不少的由于版本号冲突而导致的崩溃,我希望这些问题能够带给读者一些启发。

要弄懂这些问题到底从何二来,我们首先要了解版本号是如何运作的。现在假如说我们有一个包使用了很久以前的版本号256来进行储存,而现在的版本号是259.当这个包被载入的时候,它需要经过257、258和259三个版本的迭代处理。

以下是我总结出的最可能导致版本号出现问题的状况:

  • Merge了其他的迭代逻辑,但是还没有提交版本控制的逻辑。而团队的其他程序员还没有merge其他的迭代逻辑。 
    • 在本地测试,没有任何问题。
    • 对于其他的开发者,就开始抱怨崩溃了。 
      • 其他开发者已经更新了迭代的逻辑。
      • 在其他开发者的版本中,这个迭代就有了一个不同的版本号。
      • 但是你已经将自己的迭代放到了list的末尾,系统现在出现混乱。
      • 当这个包被载入的时候,引擎认为你自己的迭代已经载入(但是实际上没有)。
  • 你从Epic那里integrate了一个新的版本迭代,并且在版本列表中加入了一个新的宏定义。 
    • 然而,在Epic的ObjectVersion.h头文件中,在你integrate的地方之前之后,Epic做了一些新的改动。
    • 你之后进行了一个full integration,把Epic添加的额外宏定义进行了添加。 
      • Crashes/bugs开始出现,因为有一些patch现在的顺序已经乱了。
    • 看来似乎唯一“正确”的方法只有添加那些你integrate之后的东西了。 
      • 但是这依然会出事,只要Epic提供的官方内容在其他的地方被保存了,那么这个包依然可能出现问题。

我们发现的一些可优化项

当项目的内容被cook完后,Saved文件夹下的内容中,所有的包的版本号都会被设为最大。因此我才有了这个优化的想法。

即使游戏是在载入已被cooked的内容,引擎也不会默认它是最新的。这样可以使得当你改变一些包的版本号时,不需要重新cook所有的东西。但是,频繁的检查内容是否需要更新的操作是很昂贵的。尤其对于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)

我见过了太多licensees会直接在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之类的。请务必让用户了解到直接更改这一块的危害!我甚至见过非常有经验的开发者在这里栽过跟头。

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