1

关于我的具体用例:我有一个旨在与虚幻引擎项目集成的插件,为了向该插件的用户演示如何做到这一点,我将它与虚幻的免费示例游戏之一集成举个例子。集成是非常特定于游戏的,因为它会修改菜单以允许用户轻松与我的插件交互。

但是,在理想的世界中,我希望能够:

  1. 跨多个不同的虚幻引擎版本提供与示例游戏的集成。至少这将包括 3 个当前存在的 Unreal 版本(4.24、4.25 和 4.26),但可能会扩展到 N 个不同的未来版本。这基本上使集成代码成为“样板”,因为它是每个示例游戏版本中的功能所必需的,但在不同版本之间完全没有变化。
  2. 能够从一个地方维护大部分集成代码。我不想每次更改某些内容时都必须在每个示例游戏集成中进行相同的修改,因为像这样处理多个并行代码库需要大量工作并且会增加错误的可能性。

这几乎是一个可以通过代码补丁解决的问题:无论我使用的是哪个版本的示例游戏,集成代码都适合相同文件中的相同函数/类。但是,示例游戏文件本身的内容在不同引擎版本中并不完全相同,因此“在这一行将这个大块插入到这个文件中”的补丁并不总是正确的。还有一种理论上的可能性,即未来将在示例游戏中引入更实质性的更改,这可能需要我在这种情况下更改我的集成(尽管这还没有发生 - 跨次要引擎版本的更改似乎很小)。

解决这个问题的最佳方法是什么?我能想到的一种特别可怕的方法(但一种展示了这个概念的方法)是将集成的每一块分离到一个单独的文件中,然后#include "Chunk1.inc"直接#include "Chunk2.inc"进入...示例游戏的每个版本中的相关类和函数中。

void ExistingClass::PerformOperationA()
{
    // ... existing implementation ...

    // Horrible hack:
    #include "IntegrationChunk1.inc"
}

我确信这会编译,但对我来说编写或最终用户阅读并不容易,因为当在这样的片段中隔离时,代码本身将缺乏所有上下文。

另一个例子可能是将集成代码的每个部分保存在一个单独的类中,该类被声明为代码通常所在的类的朋友。朋友类有一个指向主类的指针,因此可以访问任何所需的私有成员,并具有在适当位置调用的相关函数。这种模仿 C# 的partial类概念,并且会使原始代码和修改代码之间的关系明确,因为它都是纯透明的 C++。

例如:

class ExistingClass
{
    // Additional declaration of the friend integration class:
    friend class ExistingClass_Integration;
    ExistingClass_Integration m_Integration;

public:
    ExistingClass() :
        // ... existing initialisers ...
        m_Integration(this)

    void PerformOperationA()
    {
        // ... existing code ...
        
        // Additional bits that need to happen as part of the integration:
        m_Integration.PerformOperationA();
    }
};

class ExistingClass_Integration
{
    ExistingClass* m_Owner = nullptr;

public:
    ExistingClass_Integration(ExistingClass* owner) :
        m_Owner(owner)
    {
    }
    
    void PerformOperationA()
    {
        // Do some things here, potentially using m_Owner->...
    }
};

除了关于这是否是推荐做法的问题外,这里还有一个可读性权衡。尽管集成代码修改的内容定义明确且易于遵循,但它并不能真正代表真实用户将如何与他们的游戏集成 - 他们只是直接编写代码。额外的表达只是为了我自己的维护目的,这可能会使集成看起来比实际需要的更复杂。

这是思考这个问题的正确方法,还是我需要退后一步尝试不同的方法?有人有建议吗?


本着提供更多关于我实际使用的代码的细节的精神,我的集成包括以下内容:

现有源文件中的额外包含

#include "Engine/GameInstance.h"
#include "Engine/NetworkDelegates.h"
#include "MyPlugin/MyClass.h" // <- NEW

现有课程的扩充

class ExistingClass
{
public:
    void Function1();
    void Function2();
    
private:
    int m_Var1;
    int m_Var2;
    
// BEGIN INTEGRATION
public:
    void ExtraFunction();
    
private:
    int m_ExtraVar;
// END INTEGRATION
};

挂钩现有功能

这里的集成旨在简单地调用一个新函数并返回,以尽量减少对现有代码的差异。

void ExistingClass::CreateMainMenu()
{
    // Existing implementation, eg.:
    MainMenuUI = MakeShareable(new FShooterMainMenu());
    MainMenuUI->Construct(this, Player);
    MainMenuUI->AddMenuToGameViewport();

    SetUpExtrasAfterMenuCreated() // <- NEW

    // ... further existing implementation ...
}
4

1 回答 1

0

像这样处理多个并行代码库需要大量工作,并且会增加出现错误的可能性。.. 解决这个问题的最佳方法是什么?

没有最好的办法。通用修补需要手动工作,并且在某些公司中,有专职员工致力于此。这就是为什么拥有任何产品(软件或其他任何东西,真的)的多个受支持版本需要大量资金的原因。

最好的方法是以最小化支持旧版本的成本的方式编写软件。通常,这意味着最大限度地降低测试和验证旧版本的成本,而不是使用在许多情况下根本不可能的自动修补。有时,如果可以修改基本代码以使其尽可能容易打补丁,则可能会有更好的运气,但这似乎不是您的情况。

即使某些案例的子集可以自动化,有时出于多种原因这样做甚至没有意义。其中一些您已经说过:它可能不适用于未来的版本,可能无法保证可靠,用户不期望这样的代码等。

TL;DR:向后移植和维护多个软件分支并不便宜。

于 2021-01-19T12:27:28.033 回答