Features Plugins Releases

隆重推出适用于 TeamCity 的 Unreal Engine 插件

Read this post in other languages:

TeamCity 是游戏开发的热门选择,大家选择它的原因包括支持 Perforce、可以进行本地安装,并提供了多种配置选项。 除了将它主要用作通用 CI/CD 解决方案之外,我们还努力为各种构建工具提供专门支持。 我们已提供 Unity 插件数年,现在,我们很高兴正式推出 Unreal Engine 支持插件

在这款新插件的开发过程中,我们一直与多位游戏开发客户保持密切联系,以确保它符合从事实际 Unreal Engine 项目工作的 DevOps 团队的需求。

为 Unreal Engine 游戏设置合适的构建管道可能是一项艰巨的任务,特别是对于那些在平台独特性方面经验有限的人来说。 在这篇博文中,我们将带您了解基本设置,同时会展示该插件的功能,并演示分布式 BuildGraph 支持等高级功能。

新 Unreal Engine 插件生成的示例构建管道

主要功能

以下是此版插件所提供功能的快速概览:

  • 专为最常见用例(BuildCookRun、BuildGraph 和自动化测试)定制的专用运行程序。
  • 基于 BuildGraph 描述构建发行版。
  • 即时自动化测试报告。
  • 自动发现构建计算机上的 Unreal Engine 安装。
  • 兼容最新的 5.x 版 Unreal Engine,还支持 4.x 版本。

演示

在本演示中,我们将为随 Unreal 提供的两款入门级游戏 CropoutLyra 设置管道: 我们将为构建代理和安装了 Unreal Engine 插件的 TeamCity Cloud 使用 AWS 基础架构(EC2 和用于 FSx for OpenZFS)。

我们将介绍两个场景:引擎已安装在代理上,以及引擎与游戏一起通过源代码构建。 目前,TeamCity Cloud 不为代理提供预装的 Unreal Engine,但您可以随时添加您自己的包含所有必需 SDK 的自托管代理。 我们将在这篇博文中采用这种方式。

使用 BuildCookRun 构建 Cropout

构建 Unreal 项目通常涉及多个步骤,例如:

  • 采用指定配置针对目标平台进行编译。
  • 资源烘焙(将所有资源转换为可在目标平台上读取的资源)。
  • 将项目封装为合适的分发格式。
  • 等等。

当然,之后还要进行测试!

首先,我们来看一个简单的场景,并使用标准 BuildCookRun 命令在一台计算机上按顺序运行所有阶段。

对于此场景,我们将使用撰写这篇博文时可以获取的最新普通版 Unreal Engine,并通过 EGL(Epic Games 启动器)进行安装。

将代理成功连接到 TeamCity Cloud 服务器后,我们可以在 Agents(代理)标签页中看到它。

现在,我想简单探讨一下代理的一些属性。

查看上图,我们可以看到代理发现了引擎及其版本(如您所见,我们的计算机上安装了多个引擎)。 请记下此信息,因为稍后我们会将此信息用于构建步骤配置。

现在,常用的方式是将您所有的配置以代码形式存储在源代码控制系统下(也就是“配置即代码”)。 在 TeamCity 中,您可以使用 Kotlin DSL 执行此操作。 当然,您还可以使用传统 UI 配置,但今天我们将使用第一种方式(由于 YAML 已得到广泛使用,并且已成为事实上的标准,我们已将它添加到最近发布的 TeamCity Pipelines – 如果您还没有查看,请查看)。

用于在 TeamCity 中构建 Cropout 的 Kotlin DSL 配置如下所示:

unrealEngine {
    engineDetectionMode = automatic {
        identifier = "5.4"
    }
    command = buildCookRun {
        project = "cropout/CropoutSampleProject.uproject"
        buildConfiguration = standaloneGame {
            configurations = "Development+Shipping"
            platforms = "Mac"
        }
        cook = cookConfiguration {
            maps = "Village+MainMenu"
            cultures = "en"
            unversionedContent = true
        }
        stage = stageConfiguration {
            directory = "./staged"
        }
        archive = archiveConfiguration {
            directory = "./archived"
        }
        pak = true
        compressed = true
        prerequisites = true
    }
    additionalArguments = "-utf8output -buildmachine -unattended -noP4 -nosplash -stdout -NoCodeSign"
}

所有设置的作用都一目了然,但有几项设置需要进一步说明:

  • engine detection mode

    我们可以在此处指定“automatic”或“manual”。 前者假定您已经在代理上安装引擎并在系统中注册。

    我借此机会澄清一下“注册 ”的含义:当您通过 EGL 安装 Unreal Engine 或通过源代码构建 Unreal Engine 时,此操作会写入目标计算机上的某些文件(如果使用的是 Windows,则会写入注册表)。 有关详情,请参阅此处

    这基本上就是自动检测模式的用途。 我们读取这些文件并发布标识符作为代理属性,这样一来,您稍后便可使用这些属性为构建选择合适的代理。

    “手动”模式允许您设置 Unreal Engine 根文件夹的准确路径,当您通过源代码构建时可能会用到该路径。 稍后我们将介绍这部分内容。

  • build configuration

    通过调整此参数,我们可以指定所创建构建的类型,它是单机游戏、客户端、服务器,还是既是客户端又是服务器组件。

    根据所选的值,插件将应用 -client-server-noserver 标志,并相应地管理 -clientconfig-serverconfig-config-targetplatform-servertargetplatform 参数。

到现在为止都没问题。 现在,该添加一些自动化测试并运行最终管道了。

unrealEngine {
    engineDetectionMode = automatic {
        identifier = "5.4"
    }
    command = runAutomation {
        project = "cropout/CropoutSampleProject.uproject"
        execCommand = runTests {
            tests = """
                StartsWith:JsonConfig
                Input.Triggers.Released
            """.trimIndent()
        }
        nullRHI = true
    }
    additionalArguments = "-utf8output -buildmachine -unattended -noP4 -nosplash -stdout -NoCodeSign"
}

如您所知,使用 Unreal Engine 自动化框架时,通常使用以下自动化“子命令”之一执行测试:

  • RunAll – 这是一个非常简单的命令,能够运行所有必需的测试。
  • RunFilter – 借助此命令,可以执行使用一个指定筛选器(包括 EngineStressSmoke 等值)标记的测试。
  • RunTests – 这是最通用的命令,因为您可以使用该命令指定要运行的测试列表(包括使用 StartsWith 的前缀筛选器、运行组筛选器和简单的子字符串匹配)。

我们尝试在 Kotlin DSL 中保留相同的用词,以便 Unreal 用户对此感到熟悉。

还要指出的是,插件会即时解析测试结果,并以 TeamCity 自带的正确显示的格式呈现测试结果。

以下是构建结果示例:

在下方,我们可以看到发布到工件存储的游戏二进制文件。 默认情况下,工件会发布到内置存储;但 TeamCity 还支持多种外部存储选项。 在云环境中,可以配置发布到 S3。

UI 中的最终配置将如下所示:

下面是测试运行的步骤:

由于我们已启用 Kotlin DSL 配置并禁止在 UI 中对其进行编辑,所有设置均被禁用。

使用 BuildGraph 构建 Lyra

我们将逐步过渡到使用 Lyra 的更复杂示例。 由于它是一款多人游戏,我们来构建 Linux 服务器(据我们所知,一切高性能游戏都应在 Linux 上运行!)以及 Windows 和 Linux 游戏客户端。

在前面的示例中,我们在一台计算机上按顺序运行了所有步骤。 这一过程通常需要大量时间。 但还有一种更好的方式,即 BuildGraph。 这是一种 Unreal 方法,以声明方式(或几乎以声明方式)将构建描述为一组具有依赖关系的任务。 随后,构件图的不同部分可以共同执行,或拆分到不同的计算机上。 后一种方式可以显著加快整个构建过程的速度,特别适合大型复杂项目。

BuildGraph 的优点是它会自动管理作业之间的所有中间工件。 您只需要共享存储。 构建 Lyra 时,我们会将此共享存储用于两个目的:在单个构建中的节点之间共享数据,以及维护共享派生数据缓存 (DDC)。

我们之前提到过,我们还将通过源代码构建引擎。 您通过源代码构建引擎可能是出于多种原因,但就我们的特定情况而言,我们这样做是因为无法使用任何方式通过引擎(从 EGL 安装)构建 Linux 服务器(至少在撰写这篇博文时无法实现)。 这是因为此特定引擎版本未提供相应文件和 SDK。

好的,序言介绍完毕 — 我们来看看代码。

项目结构和 Perforce 中的相应流如下所示(假设您已从 Epic 的 Perforce 获取 Unreal Engine 源代码):

下面是我们将用于构建的 BuildGraph XML 脚本的节选。 脚本的这一特定部分给出了编辑器的构建方式以及编译所需的工具, 其中还包含几项测试。 在我们的简单示例中,这些测试采用硬编码。 在真实场景中,您可能会将测试列表传递给脚本并采用更复杂的逻辑。

...
<!-- Editors -->
  <ForEach Name="Platform" Values="$(EditorPlatforms)" Separator="+">
    <Property Name="Compiler" Value="$(AgentPrefixCompile)$(Platform)" />

    <Agent Name="Build Editor and tools $(Platform)" Type="$(Compiler)">
            
            ...

      <Property Name="ExtraToolCompileArguments" Value="$(ExtraToolCompileArguments) -architecture=arm64" If="'$(Platform)' == 'Mac'" />
      
      <Node Name="$(ToolsNodeName) $(Platform)" Requires="Setup Toolchain $(Platform)" Produces="#$(Platform)ToolBinaries">
        <Compile Target="CrashReportClient" Platform="$(Platform)" Configuration="Shipping" Arguments="$(ExtraToolCompileArguments)" Tag="#$(Platform)ToolBinaries"/>
        <Compile Target="CrashReportClientEditor" Platform="$(Platform)" Configuration="Shipping" Arguments="$(ExtraToolCompileArguments)" Tag="#$(Platform)ToolBinaries"/>
        <Compile Target="ShaderCompileWorker" Platform="$(Platform)" Configuration="Development" Arguments="$(ExtraToolCompileArguments)" Tag="#$(Platform)ToolBinaries"/>
        <Compile Target="UnrealLightmass" Platform="$(Platform)" Configuration="Development" Tag="#$(Platform)ToolBinaries"/>
        <Compile Target="InterchangeWorker" Platform="$(Platform)" Configuration="Development" Arguments="$(ExtraToolCompileArguments)" Tag="#$(Platform)ToolBinaries"/>
        <Compile Target="UnrealPak" Platform="$(Platform)" Configuration="Development" Arguments="$(ExtraToolCompileArguments)" Tag="#$(Platform)ToolBinaries"/>
        <Compile Target="BootstrapPackagedGame" Platform="$(Platform)" Configuration="Shipping" Arguments="$(ExtraToolCompileArguments)" Tag="#$(Platform)ToolBinaries" If="'$(Platform)' == 'Win64'"/>
      </Node>
      
      ...
      
      <Node Name="$(EditorNodeName) $(Platform)" Requires="$(ToolsNodeName) $(Platform)" Produces="#$(Platform)EditorBinaries">
        <Compile Target="$(ProjectName)Editor" Project="$(UProject)" Platform="$(Platform)" Configuration="Development" Arguments="$(ExtraEditorCompileArguments)" Tag="#$(Platform)EditorBinaries"/>
      </Node>

            <Property Name="AutomationTestsNodeName" Value="Run Tests $(Platform)" />
      <Node Name="$(AutomationTestsNodeName)" Requires="$(EditorNodeName) $(Platform)">
        <Property Name="TestArgs" Value="-Project=$(UProject) -NullRHI -Deterministic" />
        <Property Name="TestArgs" Value="$(TestArgs) -Test=UE.EditorAutomation -RunTest=&quot;StartsWith:Input&quot;" />
        <Property Name="TestArgs" Value="$(TestArgs) -Build=Editor -UseEditor" />
        <Command Name="RunUnreal" Arguments="$(TestArgs)" />
      </Node>
    </Agent>

    <Property Name="BuildNodes" Value="$(BuildNodes);$(EditorNodeName) $(Platform);$(AutomationTestsNodeName);" />
  </ForEach>
  
  ...

有关完整语法的描述,请参阅 Epic 网站。 这里,我们本质上要做的是通过选项 EditorPlatforms 迭代传递给脚本的平台列表,并为每个平台构建编辑器。 这部分代码的一个有趣的功能是代理节点的 Type 属性。

<Agent ... Type="">

文档中查看此表:

因此,为了在特定代理上运行一组任务,您可以修改此字段。 撰写这篇博文时,为了确保一切正常运行,本部分需要对构建代理进行一些配置。 也就是说,您应当在代理配置文件中定义两个属性:

  • unreal-engine.build-graph.agent.type:此属性可以是任意值(或由 ; 分隔的值列表)。 相应的任务集随后将仅在至少有一个匹配项的代理上运行。
  • unreal-engine.build-graph.agent.shared-dir:前文中讨论过,为了运行分布式 BuildGraph 构建,我们需要一个共享存储位置。 通过此属性,我们可以在特定代理上设置该存储位置的路径。

以下是我们的 EC2 构建代理设置:

  • 对于 Linux:
unreal-engine.build-graph.agent.type = CompileLinux;CookLinux;Linux
unreal-engine.build-graph.agent.shared-dir = /mnt/agent-shared-dir/intermediate
  • 对于 Windows(由于某些原因,将文件夹作为单独的驱动器进行装载导致一次 Windows 系统调用失败,因此我们在配置中选择了网络共享):
unreal-engine.build-graph.agent.type = CompileWin64;CookWin64;Win64
unreal-engine.build-graph.agent.shared-dir = \\\\fs-040b8d6dab476baf1.fsx.eu-west-1.amazonaws.com\\fsx\\

在真实场景中,可能需要区分执行烘焙的代理和执行编译的代理,并确保它们采用合适的规范。

我们来快速浏览一下游戏的编译、烘焙和封装过程。 在我们的脚本中,我们已将构建客户端的逻辑和构建服务器的逻辑分开(因为它们传递的标志不同):

<ForEach Name="Platform" Values="$(ClientPlatforms)" Separator="+">
    <!-- COMPILATION -->
    <Property Name="Compiler" Value="$(AgentPrefixCompile)$(Platform)" />
    <Agent Name="Compile $(Platform) Client" Type="$(Compiler)">

      <Property Name="CompileNodeName" Value="Compile $(Platform) Client" />  
      <Node Name="$(CompileNodeName)" Requires="$(ToolsNodeName) $(Platform)" Produces="#$(Platform) Client Binaries">
        <ForEach Name="TargetConfiguration" Values="$(TargetConfigurations)" Separator="+">    
          <Compile Target="$(ProjectName)Client" Project="$(UProject)" Platform="$(Platform)" Configuration="$(TargetConfiguration)" Arguments="$(ExtraProjectCompileArguments)" />
        </ForEach>
      </Node>

      <Property Name="BuildNodes" Value="$(BuildNodes);$(CompileNodeName);" />
    </Agent>

    <!-- COOKING -->
    <Property Name="Cooker" Value="$(AgentPrefixCook)$(Platform)" />

    <Property Name="CookPlatformNodeName" Value="Cook $(Platform) Client" />
    <Agent Name="Cook $(Platform) Client" Type="$(Cooker)">
      <Property Name="CookPlatform" Value="$(Platform)" />
      <Property Name="CookPlatform" Value="Windows" If="'$(Platform)' == 'Win64'" />

      <Node Name="$(CookPlatformNodeName)" Requires="$(EditorNodeName) $(Platform)" Produces="#Cook $(Platform) Client Complete">
        <Cook Project="$(UProject)" Platform="$(CookPlatform)Client"/>
      </Node>
    </Agent>

    <Property Name="BuildNodes" Value="$(BuildNodes);$(CookPlatformNodeName);" />

    <!-- PACKAGING -->
    <Agent Name="Package $(Platform) Client" Type="$(Platform)">
      <Property Name="BCRArgs" Value="-Project='$(UProject)' -Platform=$(Platform) -NoCodeSign -Client" />

        <!-- Stage -->
      <Node Name="Stage $(Platform) Client" Requires="Compile $(Platform) Client;Cook $(Platform) Client">
        <ForEach Name="TargetConfiguration" Values="$(TargetConfigurations)" Separator="+">
          <Command Name="BuildCookRun" Arguments="$(BCRArgs) -Configuration=$(TargetConfiguration) -SkipBuild -SkipCook -Stage -Pak" />
        </ForEach>
      </Node>

      <!-- Package -->
      <Node Name="Package $(Platform) Client" Requires="Stage $(Platform) Client">
        <ForEach Name="TargetConfiguration" Values="$(TargetConfigurations)" Separator="+">    
          <Command Name="BuildCookRun" Arguments="$(BCRArgs) -Configuration=$(TargetConfiguration) -SkipBuild -SkipCook -SkipStage -Package" />
        </ForEach>
      </Node>

      <!-- Publish (Packages) -->
      <Node Name="Archive $(Platform) Client" Requires="Package $(Platform) Client">
        <ForEach Name="TargetConfiguration" Values="$(TargetConfigurations)" Separator="+">    
          <Command Name="BuildCookRun" Arguments="$(BCRArgs) -Configuration=$(TargetConfiguration) -SkipBuild -SkipCook -SkipStage -SkipPak -SkipPackage -Archive" />
        </ForEach>
      </Node>

      <Node Name="Publish $(Platform) Client" Requires="Archive $(Platform) Client">
        <Property Name="PublishPlatform" Value="$(Platform)" />
        <Property Name="PublishPlatform" Value="Windows" If="'$(Platform)' == 'Win64'" />
        <Log Message="##teamcity[publishArtifacts 'game/ArchivedBuilds/$(PublishPlatform)Client=>$(PublishPlatform)Client.zip']" />
      </Node>
    </Agent>

    <Property Name="BuildNodes" Value="$(BuildNodes);Archive $(Platform) Client;Publish $(Platform) Client" />
  </ForEach>

脚本本身不需要过多解释。 我们会迭代希望客户端在其上运行的平台列表,该列表会再次作为选项传递。 我们针对每个平台进行编译、烘焙资源,最后将一切封装起来。 作为封装过程的最后一步,我们使用服务消息将构建的客户端作为 TeamCity 构建工件发布。

您可能已经注意到,在这部分脚本中,我们有三个代理代表构建过程的三个阶段(编译、烘焙和封装)。 编译需要使用属于编辑器的工具,但不需要编辑器本身:

<Node Name="$(CompileNodeName)" Requires="$(ToolsNodeName) $(Platform)" Produces="#$(Platform) Client Binaries">

同时,烘焙过程需要实际的编辑器:

<Node Name="$(CookPlatformNodeName)" Requires="$(EditorNodeName) $(Platform)" Produces="#Cook $(Platform) Client Complete">

由于这两个过程之间没有依赖关系,它们可以在不同的计算机上并行运行(在有足够多的计算机可用的情况下)。 这一简单示例演示了使用 BuildGraph 的强大功能:您只需描述共享依赖项的各项工作,然后它们就会同时运行。

为了简洁起见,我们不会显示包含服务器部分的脚本的其余部分,因为这部分内容与我们刚才描述的内容非常相似,只有标志集合不同。

最后,我们要介绍 TeamCity 中的插件配置。 由于大部分工作已在 BuildGraph XML 脚本中描述,DSL 配置相当简单:

params {
    param("env.UE_SharedDataCachePath", "/mnt/agent-shared-dir/ddc")
    param("env.UE-SharedDataCachePath", "\\\\fs-040b8d6dab476baf1.fsx.eu-west-1.amazonaws.com\\fsx\\ddc")
}

steps {
    unrealEngine {
        id = "Unreal_Engine"
        name = "Build"
        engineDetectionMode = manual {
            rootDir = "engine"
        }
        command = buildGraph {
            script = "game/BuildProject.xml"
            targetNode = "BuildProject"
            options = """
                ProjectPath=%teamcity.build.checkoutDir%/game
                ProjectName=Lyra
                ClientPlatforms=Linux+Win64
                ServerPlatforms=Linux
                EditorPlatforms=Linux+Win64
                TargetConfigurations=Shipping
            """.trimIndent()
            mode = UnrealEngine.BuildGraphMode.Distributed
        }
        additionalArguments = "-utf8output -buildmachine -unattended -noP4 -nosplash -stdout -NoCodeSign"
    }
}

在这里,指定我们希望在分布式模式下运行提供的 BuildGraph 脚本。 但我们可以根据自己的意愿随时选择在一台计算机上按顺序运行所有节点。 我们还指定了一些环境变量,如果引擎要启用共享 DDC,则需要使用这些变量。 这些文件夹应当已装载到连接至 TeamCity 的代理。

现在,您可以为我们的游戏传递以下包含三个平台选项的列表:ClientPlatformsServerPlatformsEditorPlatforms

UI 中的显示如下:

启动构建时,我们会获得此构建链:

因此,如我们所见,插件已将构建图转换为适当的 TeamCity 构建链。

我们可以在指定构建的相应标签页上查看已发布的工件。 Linux 服务器的显示如下:

在结束之前,我们想强调一些要点,请您务必牢记:

  • 为了确保 BuildGraph 分发过程正常运行,您的构建配置应仅包含一个有效构建步骤。
  • 目前,该插件不会以任何方式管理共享存储中生成的工件的保留期。 您负责正确设置保留期。
  • 这篇博文中的示例绝不可用于生产。 您的真实场景可能会更加复杂。 但这些示例可作为一个良好的起点。

立即试用!

您可以从 GitHub 仓库中获取这篇博文中展示的演示代码。

如果您想尝试用于 TeamCity 的 Unreal Engine 插件,可从 JetBrains Marketplace 下载,并在 TeamCity On-Premises 服务器上安装。

对于 TeamCity Cloud 用户,我们已预装了 Unreal Engine 插件,您只需添加 Unreal Engine 构建步骤便可使用此插件。 需要提醒您的是,TeamCity Cloud 代理没有预装 Unreal Engine,因此您需要使用自托管代理运行 Unreal Engine 构建。

针对一直使用预览版插件(从 0.x.x 开始的任何版本)的用户的重要说明:请注意,由于我们在 1.0.0 版中进行了更改并重新设计了多个功能,您的现有配置将被破坏。 您可能需要使用新版插件重新创建这些配置。

后续计划

首先,我们期待收到您的反馈。 您可以随时联系我们:通过我们的问题跟踪器提交工单或对这篇博文发表评论。

我们对今后的工作还有一些想法,包括:

  • 提供更加深入的构建日志分析。
  • 包含与 UnrealGameSync (UGS) 的集成,添加构建状态发布等功能,以及提供开箱即用的元数据服务器。
  • 添加对构建计算机上可用 SDK 的检测,可能会使用整体解决方案。
  • 了解 Gauntlet 自动化框架以及我们可以在其中执行的操作。
  • 了解我们如何利用 Epic Games 推出的构建过程中的最新进展,即查看 Unreal Build Accelerator (UBA) 并通过 TeamCity 在代理上进行协调。
  • 将插件开源。 我们相信这将提高透明度,并从整体上创建更好的插件。 此外,开源会带来很多乐趣。
  • 您能想到的其他任何想法! 如果您有任何好的想法,请随时通过上述任何渠道联系我们,或直接在这篇博文下发表评论。

本博文英文原作者:

Sue

Vladislav Grinin

image description