{"id":473406,"date":"2024-05-14T15:08:01","date_gmt":"2024-05-14T14:08:01","guid":{"rendered":"https:\/\/blog.jetbrains.com\/?post_type=teamcity&#038;p=473406"},"modified":"2025-02-25T18:31:19","modified_gmt":"2025-02-25T17:31:19","slug":"unreal-engine-plugin-for-teamcity","status":"publish","type":"teamcity","link":"https:\/\/blog.jetbrains.com\/zh-hans\/teamcity\/2024\/05\/unreal-engine-plugin-for-teamcity","title":{"rendered":"Introducing the Unreal Engine Plugin for TeamCity"},"content":{"rendered":"\n<p>TeamCity is a popular choice in <a href=\"https:\/\/www.jetbrains.com\/teamcity\/use-cases\/game-development\/\" data-type=\"link\" data-id=\"https:\/\/www.jetbrains.com\/teamcity\/use-cases\/game-development\/\" target=\"_blank\" rel=\"noopener\">game development<\/a> for several reasons, including its support for Perforce, its ability to be installed on premises, and its numerous configuration options. In addition to its main application as a general-purpose CI\/CD solution, we\u2019re also striving to bring dedicated support for various<a href=\"https:\/\/www.jetbrains.com\/teamcity\/integrations\/build-tools\/\" target=\"_blank\" rel=\"noopener\"> build tools<\/a>, too. Having already provided a Unity plugin for several years, we&#8217;re now excited to officially introduce our <a href=\"https:\/\/plugins.jetbrains.com\/plugin\/22679-unreal-engine-support\" target=\"_blank\" rel=\"noopener\">Unreal Engine Support plugin<\/a>!<\/p>\n\n\n\n<p>We\u2019ve been in close contact with several of our game development customers throughout the development of this new plugin to ensure it meets the needs of DevOps teams working on real-world Unreal Engine projects.<\/p>\n\n\n\n<p>Setting up a proper build pipeline for an Unreal Engine game can be a daunting task, especially for those with limited experience of the platform\u2019s unique peculiarities. In this blog post, we&#8217;ll walk you through a basic setup, while showcasing the plugin&#8217;s features and demonstrating advanced capabilities such as distributed BuildGraph support.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1999\" height=\"639\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2024\/05\/sample-build-pipeline.png\" alt=\"\" class=\"wp-image-473421\"\/><figcaption class=\"wp-element-caption\">A sample build pipeline generated by the new Unreal Engine plugin<\/figcaption><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Key features<\/strong><\/h2>\n\n\n\n<p>Here&#8217;s a quick overview of what you can expect from this version of the plugin:<\/p>\n\n\n\n<ul>\n<li>A dedicated runner tailored for most common use cases (BuildCookRun, BuildGraph, and automation tests).<\/li>\n\n\n\n<li>Build distribution based on your BuildGraph description.<\/li>\n\n\n\n<li>On-the-fly automation test reporting.<\/li>\n\n\n\n<li>Automatic discovery of Unreal Engine installations on the build machines.<\/li>\n\n\n\n<li>Compatibility with the latest 5.x versions of Unreal Engine, plus support for 4.x.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>The demo<\/strong><\/h2>\n\n\n\n<p>In this demo, we\u2019ll be setting up a pipeline for two starter games shipped with Unreal:<em> Cropout<\/em> and <em>Lyra<\/em>. We\u2019ll use AWS infrastructure (EC2 and FSx for OpenZFS) for build agents and TeamCity Cloud with the Unreal Engine plugin installed.<\/p>\n\n\n\n<p>We\u2019ll look at two scenarios: when the engine is already installed on an agent and when it\u2019s built from source along with the game. At the moment, TeamCity Cloud doesn\u2019t offer agents with the preinstalled Unreal Engine, but you can always add your own <a href=\"https:\/\/www.jetbrains.com\/help\/teamcity\/cloud\/install-and-start-teamcity-agents.html\" target=\"_blank\" rel=\"noopener\">self-hosted agent<\/a> with all the required SDKs. This is the approach we\u2019ll stick to in this blog post.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Building <\/strong><strong><em>Cropout<\/em><\/strong><strong> with BuildCookRun<\/strong><\/h2>\n\n\n\n<p>Building an Unreal project usually involves<a href=\"https:\/\/dev.epicgames.com\/documentation\/en-us\/unreal-engine\/build-operations-cooking-packaging-deploying-and-running-projects-in-unreal-engine\" target=\"_blank\" rel=\"noopener\"> many steps<\/a>, such as:<\/p>\n\n\n\n<ul>\n<li>Compilation for the target platforms in the specified configurations.<\/li>\n\n\n\n<li>Asset cooking (converting all of the assets into ones that can be read on the target platforms).<\/li>\n\n\n\n<li>Packaging of the project into a proper distribution format.<\/li>\n\n\n\n<li>And many more.<\/li>\n<\/ul>\n\n\n\n<p>Then, of course, there\u2019s the testing!<\/p>\n\n\n\n<p>To begin with, let\u2019s start with a simple scenario and run all of these stages sequentially on a single machine utilizing the standard BuildCookRun command.<\/p>\n\n\n\n<p>For this scenario, we\u2019ll use the latest vanilla Unreal Engine version available at the moment of writing, installed from EGL (Epic Games Launcher).<\/p>\n\n\n\n<p>After successfully connecting the agent to our TeamCity Cloud server, we can see it in the <em>Agents<\/em> tab.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1420\" height=\"719\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2024\/05\/teamcity-cloud-agents-pool.png\" alt=\"\" class=\"wp-image-473444\"\/><\/figure>\n\n\n\n<p>At this point, I\u2019d like to take a moment to briefly explore some of the agent\u2019s properties.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1420\" height=\"398\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2024\/05\/agents-properties.png\" alt=\"\" class=\"wp-image-473456\"\/><\/figure>\n\n\n\n<p>Here, looking at the picture above, we see that the agent discovered the engine and its version (we have more than one on our machine, as you can see). Keep a note of this information, as we\u2019ll need it later for a build step configuration.<\/p>\n\n\n\n<p>Nowadays, the common approach is to store all your configs as code under source control (i.e. configuration as code). In TeamCity, you can do this using <a href=\"https:\/\/www.jetbrains.com\/teamcity\/features\/configuration-as-code\/\" data-type=\"link\" data-id=\"https:\/\/www.jetbrains.com\/teamcity\/features\/configuration-as-code\/\" target=\"_blank\" rel=\"noopener\">Kotlin DSL<\/a>. Of course, you can also go with the traditional UI configuration, but today, we\u2019ll use the first approach (due to the popularity of YAML and the fact that it has become a de-facto standard, we\u2019ve added it as a part of our recent release of<a href=\"https:\/\/blog.jetbrains.com\/zh-hans\/teamcity\/2024\/03\/meet-teamcity-pipelines\"> TeamCity Pipelines \u2013<\/a> check it out if you haven\u2019t already).<\/p>\n\n\n\n<p>The Kotlin DSL configuration for building<em> Cropout <\/em>in TeamCity looks like this:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\">unrealEngine {\n    engineDetectionMode = automatic {\n        identifier = &quot;5.4&quot;\n    }\n    command = buildCookRun {\n        project = &quot;cropout\/CropoutSampleProject.uproject&quot;\n        buildConfiguration = standaloneGame {\n            configurations = &quot;Development+Shipping&quot;\n            platforms = &quot;Mac&quot;\n        }\n        cook = cookConfiguration {\n            maps = &quot;Village+MainMenu&quot;\n            cultures = &quot;en&quot;\n            unversionedContent = true\n        }\n        stage = stageConfiguration {\n            directory = &quot;.\/staged&quot;\n        }\n        archive = archiveConfiguration {\n            directory = &quot;.\/archived&quot;\n        }\n        pak = true\n        compressed = true\n        prerequisites = true\n    }\n    additionalArguments = &quot;-utf8output -buildmachine -unattended -noP4 -nosplash -stdout -NoCodeSign&quot;\n}<\/pre>\n\n\n\n<p>All of the settings are pretty self-descriptive, but there are a couple worth paying closer attention to:<\/p>\n\n\n\n<ul>\n<li><code>engine detection mode<\/code><br><br>Here, we can specify either \u201cautomatic\u201d or \u201cmanual\u201d. The former assumes you already have the engine installed on your agent and registered in the system.<br><br>Let me take this opportunity to clarify what \u201cregistered\u201d means: Whenever you install the Unreal Engine from EGL or build it from source, it writes to certain files on the target machine (or registry in the case of Windows). More information on that can be found<a href=\"https:\/\/dev.epicgames.com\/documentation\/en-us\/unreal-engine\/installed-build-reference-guide-for-unreal-engine#registeringaninstalledbuild\" target=\"_blank\" rel=\"noopener\"> here<\/a>.<br><br>This is essentially what the automatic detection mode is for. We read those files and publish identifiers as agent properties, so you can use them later to select the proper agent for your build.<br><br>The \u201cmanual\u201d mode allows you to set the exact path to the Unreal Engine root folder, which might be useful when you build from source. We&#8217;ll touch on that later.<br><\/li>\n\n\n\n<li><code>build configuration<\/code><br><br>By adjusting this parameter, we can specify the type of build we&#8217;re creating, whether it&#8217;s a standalone game, client, server, or both client and server components.<br><br>Depending on the selected value, the plugin will apply <code>-client<\/code>, <code>-server<\/code>, <code>-noserver <\/code>flags and manage <code>-clientconfig<\/code>, <code>-serverconfig<\/code>, <code>-config<\/code>, <code>-targetplatform<\/code>, and <code>-servertargetplatform<\/code> parameters accordingly.<\/li>\n<\/ul>\n\n\n\n<p>So far, so good. Now, it\u2019s time to add some automation tests and run the final pipeline.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\">unrealEngine {\n    engineDetectionMode = automatic {\n        identifier = &quot;5.4&quot;\n    }\n    command = runAutomation {\n        project = &quot;cropout\/CropoutSampleProject.uproject&quot;\n        execCommand = runTests {\n            tests = &quot;&quot;&quot;\n                StartsWith:JsonConfig\n                Input.Triggers.Released\n            &quot;&quot;&quot;.trimIndent()\n        }\n        nullRHI = true\n    }\n    additionalArguments = &quot;-utf8output -buildmachine -unattended -noP4 -nosplash -stdout -NoCodeSign&quot;\n}<\/pre>\n\n\n\n<p>As you may know, when working with the Unreal Engine Automation Framework, tests are usually executed using one of the following automation \u201csubcommands\u201d:<\/p>\n\n\n\n<ul>\n<li><code>RunAll<\/code> \u2013 this is a pretty straightforward command that has the ability to run all required tests.<\/li>\n\n\n\n<li><code>RunFilter <\/code>\u2013 with this command, tests tagged with one of the specified filters (which includes such values as <code>Engine<\/code>, <code>Stress<\/code>, <code>Smoke<\/code>, etc.) can be executed.<\/li>\n\n\n\n<li><code>RunTests<\/code> \u2013 this is the most versatile command, as it allows you to specify the list of tests to run (including prefix filters with <code>StartsWith<\/code>, run group filters, and simple substring matches).<\/li>\n<\/ul>\n\n\n\n<p>We tried to preserve the same wording in Kotlin DSL to keep it familiar for Unreal folks.<\/p>\n\n\n\n<p>It&#8217;s also worth mentioning that the plugin parses the results of the tests on the fly and presents them in a nicely formatted manner native to TeamCity.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1420\" height=\"809\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2024\/05\/teamcity-test-results.png\" alt=\"\" class=\"wp-image-473479\"\/><\/figure>\n\n\n\n<p>Here\u2019s what the result of the build looks like:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"2000\" height=\"466\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2024\/05\/build-results-teamcity.png\" alt=\"\" class=\"wp-image-473490\"\/><\/figure>\n\n\n\n<p>Below, we can see the game binaries published to the <a href=\"https:\/\/www.jetbrains.com\/help\/teamcity\/configuring-artifacts-storage.html\" target=\"_blank\" rel=\"noopener\">Artifacts Storage<\/a>. By default, artifacts are published to <a href=\"https:\/\/www.jetbrains.com\/help\/teamcity\/configuring-artifacts-storage.html#Built-in+Artifacts+Storage\" target=\"_blank\" rel=\"noopener\">built-in storage<\/a>; however, TeamCity also supports various<a href=\"https:\/\/www.jetbrains.com\/help\/teamcity\/configuring-artifacts-storage.html#external-artifacts-storage\" target=\"_blank\" rel=\"noopener\"> external storage<\/a> options. In the context of the cloud, it makes sense to configure publishing to S3.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"2000\" height=\"1185\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2024\/05\/external-storage-options-teamcity.png\" alt=\"\" class=\"wp-image-473501\"\/><\/figure>\n\n\n\n<p>The final configuration in the UI would then look like this:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1736\" height=\"1744\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2024\/05\/ui-configuration-option-step1.png\" alt=\"\" class=\"wp-image-473512\"\/><\/figure>\n\n\n\n<p>And here\u2019s the step with the test run:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1736\" height=\"1532\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2024\/05\/ui-configuration-option-step2.png\" alt=\"\" class=\"wp-image-473523\"\/><\/figure>\n\n\n\n<p>All the settings are disabled since we&#8217;ve enabled Kotlin DSL configuration and disabled editing it in the UI.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Building <\/strong><strong><em>Lyra<\/em><\/strong><strong> with BuildGraph<\/strong><\/h2>\n\n\n\n<p>We&#8217;re slowly moving towards a more complex example with Lyra. Since it&#8217;s a multiplayer game, let&#8217;s build a Linux server (as we know that everything high-performance should be run on Linux!), along with Windows and Linux game clients.<\/p>\n\n\n\n<p>In the previous example, we ran all the steps sequentially on a single machine. Usually, this takes a significant amount of time. But there\u2019s a better way, and it&#8217;s called BuildGraph. This is Unreal&#8217;s method for describing your build declaratively (or almost) as a set of tasks with dependencies among them. The different parts of the graph can then be executed together or split across different machines. The latter approach allows you to dramatically speed up the entire build process, especially for a big and complex project.<\/p>\n\n\n\n<p>The nice thing about BuildGraph is it manages all of the intermediate artifacts between jobs automatically. All you need is shared storage. While building <em>Lyra<\/em>, we\u2019ll be using this shared storage for two purposes: sharing data between nodes within a single build and maintaining a shared derived data cache (DDC).<\/p>\n\n\n\n<p>As we mentioned earlier, we\u2019ll also build the engine from the source. There are multiple reasons why you might want to do that, but in our specific case, it\u2019s because there\u2019s no way to build a Linux server from the engine installed from EGL (at least at the time this post was written). This is due to the lack of files and SDKs provided with this particular engine version.<\/p>\n\n\n\n<p>Okay, that\u2019s enough preamble \u2013 let\u2019s get to grips with the code.<\/p>\n\n\n\n<p>The structure of the project and corresponding streams in Perforce look like this (we assume that you already have the Unreal Engine source code from Epic\u2019s Perforce):<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full is-resized\"><img decoding=\"async\" loading=\"lazy\" width=\"1491\" height=\"1999\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2024\/05\/lyra-perforce.png\" alt=\"\" class=\"wp-image-473534\" style=\"aspect-ratio:0.7458729364682342;width:424px;height:auto\"\/><\/figure><\/div>\n\n\n<p>Here\u2019s the excerpt of the BuildGraph XML script that we\u2019ll use for our build. This particular part of the script shows how we\u2019ll build the editor along with the tools required for the compilation. It also contains a few tests, too. In our simple example, these are hardcoded. In a real-world scenario, you\u2019d likely pass a list of tests to the script and employ more complicated logic.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\">...\n&lt;!-- Editors --&gt;\n  &lt;ForEach Name=&quot;Platform&quot; Values=&quot;$(EditorPlatforms)&quot; Separator=&quot;+&quot;&gt;\n    &lt;Property Name=&quot;Compiler&quot; Value=&quot;$(AgentPrefixCompile)$(Platform)&quot; \/&gt;\n\n    &lt;Agent Name=&quot;Build Editor and tools $(Platform)&quot; Type=&quot;$(Compiler)&quot;&gt;\n\t\t    \n\t\t    ...\n\n      &lt;Property Name=&quot;ExtraToolCompileArguments&quot; Value=&quot;$(ExtraToolCompileArguments) -architecture=arm64&quot; If=&quot;&#039;$(Platform)&#039; == &#039;Mac&#039;&quot; \/&gt;\n      \n      &lt;Node Name=&quot;$(ToolsNodeName) $(Platform)&quot; Requires=&quot;Setup Toolchain $(Platform)&quot; Produces=&quot;#$(Platform)ToolBinaries&quot;&gt;\n        &lt;Compile Target=&quot;CrashReportClient&quot; Platform=&quot;$(Platform)&quot; Configuration=&quot;Shipping&quot; Arguments=&quot;$(ExtraToolCompileArguments)&quot; Tag=&quot;#$(Platform)ToolBinaries&quot;\/&gt;\n        &lt;Compile Target=&quot;CrashReportClientEditor&quot; Platform=&quot;$(Platform)&quot; Configuration=&quot;Shipping&quot; Arguments=&quot;$(ExtraToolCompileArguments)&quot; Tag=&quot;#$(Platform)ToolBinaries&quot;\/&gt;\n        &lt;Compile Target=&quot;ShaderCompileWorker&quot; Platform=&quot;$(Platform)&quot; Configuration=&quot;Development&quot; Arguments=&quot;$(ExtraToolCompileArguments)&quot; Tag=&quot;#$(Platform)ToolBinaries&quot;\/&gt;\n        &lt;Compile Target=&quot;UnrealLightmass&quot; Platform=&quot;$(Platform)&quot; Configuration=&quot;Development&quot; Tag=&quot;#$(Platform)ToolBinaries&quot;\/&gt;\n        &lt;Compile Target=&quot;InterchangeWorker&quot; Platform=&quot;$(Platform)&quot; Configuration=&quot;Development&quot; Arguments=&quot;$(ExtraToolCompileArguments)&quot; Tag=&quot;#$(Platform)ToolBinaries&quot;\/&gt;\n        &lt;Compile Target=&quot;UnrealPak&quot; Platform=&quot;$(Platform)&quot; Configuration=&quot;Development&quot; Arguments=&quot;$(ExtraToolCompileArguments)&quot; Tag=&quot;#$(Platform)ToolBinaries&quot;\/&gt;\n        &lt;Compile Target=&quot;BootstrapPackagedGame&quot; Platform=&quot;$(Platform)&quot; Configuration=&quot;Shipping&quot; Arguments=&quot;$(ExtraToolCompileArguments)&quot; Tag=&quot;#$(Platform)ToolBinaries&quot; If=&quot;&#039;$(Platform)&#039; == &#039;Win64&#039;&quot;\/&gt;\n      &lt;\/Node&gt;\n      \n      ...\n      \n      &lt;Node Name=&quot;$(EditorNodeName) $(Platform)&quot; Requires=&quot;$(ToolsNodeName) $(Platform)&quot; Produces=&quot;#$(Platform)EditorBinaries&quot;&gt;\n        &lt;Compile Target=&quot;$(ProjectName)Editor&quot; Project=&quot;$(UProject)&quot; Platform=&quot;$(Platform)&quot; Configuration=&quot;Development&quot; Arguments=&quot;$(ExtraEditorCompileArguments)&quot; Tag=&quot;#$(Platform)EditorBinaries&quot;\/&gt;\n      &lt;\/Node&gt;\n\n\t\t\t&lt;Property Name=&quot;AutomationTestsNodeName&quot; Value=&quot;Run Tests $(Platform)&quot; \/&gt;\n      &lt;Node Name=&quot;$(AutomationTestsNodeName)&quot; Requires=&quot;$(EditorNodeName) $(Platform)&quot;&gt;\n        &lt;Property Name=&quot;TestArgs&quot; Value=&quot;-Project=$(UProject) -NullRHI -Deterministic&quot; \/&gt;\n        &lt;Property Name=&quot;TestArgs&quot; Value=&quot;$(TestArgs) -Test=UE.EditorAutomation -RunTest=&amp;quot;StartsWith:Input&amp;quot;&quot; \/&gt;\n        &lt;Property Name=&quot;TestArgs&quot; Value=&quot;$(TestArgs) -Build=Editor -UseEditor&quot; \/&gt;\n        &lt;Command Name=&quot;RunUnreal&quot; Arguments=&quot;$(TestArgs)&quot; \/&gt;\n      &lt;\/Node&gt;\n    &lt;\/Agent&gt;\n\n    &lt;Property Name=&quot;BuildNodes&quot; Value=&quot;$(BuildNodes);$(EditorNodeName) $(Platform);$(AutomationTestsNodeName);&quot; \/&gt;\n  &lt;\/ForEach&gt;\n  \n  ...<\/pre>\n\n\n\n<p>The full syntax is described in full on Epic\u2019s website. Here, what we\u2019re essentially doing is iterating over the list of platforms passed to the script via an option <code>EditorPlatforms<\/code> and building the <em>Editor<\/em> for each. One interesting feature of this part of the code is the <code>Type<\/code> property of the agent node.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\">&lt;Agent ... Type=&quot;&quot;&gt;<\/pre>\n\n\n\n<p>See this table from <a href=\"https:\/\/dev.epicgames.com\/documentation\/en-us\/unreal-engine\/buildgraph-script-elements-reference-for-unreal-engine#agent\" data-type=\"link\" data-id=\"https:\/\/dev.epicgames.com\/documentation\/en-us\/unreal-engine\/buildgraph-script-elements-reference-for-unreal-engine#agent\" target=\"_blank\" rel=\"noopener\">the documentation<\/a>:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1398\" height=\"476\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2024\/05\/Screenshot-2024-05-14-at-14.18.51.png\" alt=\"\" class=\"wp-image-473552\"\/><\/figure>\n\n\n\n<p>So, in order to run a set of tasks on a particular agent, you can manipulate this field. At the time of writing, to make everything work, this part requires a little configuration of your build agents. Namely, you should define two properties in the<a href=\"https:\/\/www.jetbrains.com\/help\/teamcity\/configure-agent-installation.html\" target=\"_blank\" rel=\"noopener\"> agent\u2019s configuration file<\/a>:<\/p>\n\n\n\n<ul>\n<li><code>unreal-engine.build-graph.agent.type<\/code>: This could be any value (or a list of values separated by <strong>;<\/strong>). The corresponding set of tasks will then be run only on an agent with at least one match.<\/li>\n\n\n\n<li><code>unreal-engine.build-graph.agent.shared-dir<\/code>: As discussed earlier, in order to run distributed BuildGraph builds, we need a shared storage location. With this property, we set a path to this storage location on a specific agent.<\/li>\n<\/ul>\n\n\n\n<p>This is what our setup for our EC2 build agents looks like:<\/p>\n\n\n\n<ul>\n<li>For Linux:<\/li>\n<\/ul>\n\n\n\n<pre class=\"EnlighterJSRAW\">unreal-engine.build-graph.agent.type = CompileLinux;CookLinux;Linux\nunreal-engine.build-graph.agent.shared-dir = \/mnt\/agent-shared-dir\/intermediate<\/pre>\n\n\n\n<ul>\n<li>And for Windows (for some reason, mounting the folder as a separate drive caused one of the Windows system calls to fail, so we opted for a network share in the configuration):<\/li>\n<\/ul>\n\n\n\n<pre class=\"EnlighterJSRAW\">unreal-engine.build-graph.agent.type = CompileWin64;CookWin64;Win64\nunreal-engine.build-graph.agent.shared-dir = \\\\\\\\fs-040b8d6dab476baf1.fsx.eu-west-1.amazonaws.com\\\\fsx\\\\<\/pre>\n\n\n\n<p>For real-world scenarios, it might make sense to differentiate between agents that perform cooking and those that perform compilation and ensure they have the appropriate specs.<\/p>\n\n\n\n<p>Let\u2019s quickly take a look at what the processes of compilation, cooking and packaging of the game look like. In our script, we\u2019ve separated the logic for building a client and building a server (as they differ in terms of passed flags):<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\">&lt;ForEach Name=&quot;Platform&quot; Values=&quot;$(ClientPlatforms)&quot; Separator=&quot;+&quot;&gt;\n    &lt;!-- COMPILATION --&gt;\n    &lt;Property Name=&quot;Compiler&quot; Value=&quot;$(AgentPrefixCompile)$(Platform)&quot; \/&gt;\n    &lt;Agent Name=&quot;Compile $(Platform) Client&quot; Type=&quot;$(Compiler)&quot;&gt;\n\n      &lt;Property Name=&quot;CompileNodeName&quot; Value=&quot;Compile $(Platform) Client&quot; \/&gt;  \n      &lt;Node Name=&quot;$(CompileNodeName)&quot; Requires=&quot;$(ToolsNodeName) $(Platform)&quot; Produces=&quot;#$(Platform) Client Binaries&quot;&gt;\n        &lt;ForEach Name=&quot;TargetConfiguration&quot; Values=&quot;$(TargetConfigurations)&quot; Separator=&quot;+&quot;&gt;    \n          &lt;Compile Target=&quot;$(ProjectName)Client&quot; Project=&quot;$(UProject)&quot; Platform=&quot;$(Platform)&quot; Configuration=&quot;$(TargetConfiguration)&quot; Arguments=&quot;$(ExtraProjectCompileArguments)&quot; \/&gt;\n        &lt;\/ForEach&gt;\n      &lt;\/Node&gt;\n\n      &lt;Property Name=&quot;BuildNodes&quot; Value=&quot;$(BuildNodes);$(CompileNodeName);&quot; \/&gt;\n    &lt;\/Agent&gt;\n\n    &lt;!-- COOKING --&gt;\n    &lt;Property Name=&quot;Cooker&quot; Value=&quot;$(AgentPrefixCook)$(Platform)&quot; \/&gt;\n\n    &lt;Property Name=&quot;CookPlatformNodeName&quot; Value=&quot;Cook $(Platform) Client&quot; \/&gt;\n    &lt;Agent Name=&quot;Cook $(Platform) Client&quot; Type=&quot;$(Cooker)&quot;&gt;\n      &lt;Property Name=&quot;CookPlatform&quot; Value=&quot;$(Platform)&quot; \/&gt;\n      &lt;Property Name=&quot;CookPlatform&quot; Value=&quot;Windows&quot; If=&quot;&#039;$(Platform)&#039; == &#039;Win64&#039;&quot; \/&gt;\n\n      &lt;Node Name=&quot;$(CookPlatformNodeName)&quot; Requires=&quot;$(EditorNodeName) $(Platform)&quot; Produces=&quot;#Cook $(Platform) Client Complete&quot;&gt;\n        &lt;Cook Project=&quot;$(UProject)&quot; Platform=&quot;$(CookPlatform)Client&quot;\/&gt;\n      &lt;\/Node&gt;\n    &lt;\/Agent&gt;\n\n    &lt;Property Name=&quot;BuildNodes&quot; Value=&quot;$(BuildNodes);$(CookPlatformNodeName);&quot; \/&gt;\n\n    &lt;!-- PACKAGING --&gt;\n    &lt;Agent Name=&quot;Package $(Platform) Client&quot; Type=&quot;$(Platform)&quot;&gt;\n      &lt;Property Name=&quot;BCRArgs&quot; Value=&quot;-Project=&#039;$(UProject)&#039; -Platform=$(Platform) -NoCodeSign -Client&quot; \/&gt;\n\n        &lt;!-- Stage --&gt;\n      &lt;Node Name=&quot;Stage $(Platform) Client&quot; Requires=&quot;Compile $(Platform) Client;Cook $(Platform) Client&quot;&gt;\n        &lt;ForEach Name=&quot;TargetConfiguration&quot; Values=&quot;$(TargetConfigurations)&quot; Separator=&quot;+&quot;&gt;\n          &lt;Command Name=&quot;BuildCookRun&quot; Arguments=&quot;$(BCRArgs) -Configuration=$(TargetConfiguration) -SkipBuild -SkipCook -Stage -Pak&quot; \/&gt;\n        &lt;\/ForEach&gt;\n      &lt;\/Node&gt;\n\n      &lt;!-- Package --&gt;\n      &lt;Node Name=&quot;Package $(Platform) Client&quot; Requires=&quot;Stage $(Platform) Client&quot;&gt;\n        &lt;ForEach Name=&quot;TargetConfiguration&quot; Values=&quot;$(TargetConfigurations)&quot; Separator=&quot;+&quot;&gt;    \n          &lt;Command Name=&quot;BuildCookRun&quot; Arguments=&quot;$(BCRArgs) -Configuration=$(TargetConfiguration) -SkipBuild -SkipCook -SkipStage -Package&quot; \/&gt;\n        &lt;\/ForEach&gt;\n      &lt;\/Node&gt;\n\n      &lt;!-- Publish (Packages) --&gt;\n      &lt;Node Name=&quot;Archive $(Platform) Client&quot; Requires=&quot;Package $(Platform) Client&quot;&gt;\n        &lt;ForEach Name=&quot;TargetConfiguration&quot; Values=&quot;$(TargetConfigurations)&quot; Separator=&quot;+&quot;&gt;    \n          &lt;Command Name=&quot;BuildCookRun&quot; Arguments=&quot;$(BCRArgs) -Configuration=$(TargetConfiguration) -SkipBuild -SkipCook -SkipStage -SkipPak -SkipPackage -Archive&quot; \/&gt;\n        &lt;\/ForEach&gt;\n      &lt;\/Node&gt;\n\n      &lt;Node Name=&quot;Publish $(Platform) Client&quot; Requires=&quot;Archive $(Platform) Client&quot;&gt;\n        &lt;Property Name=&quot;PublishPlatform&quot; Value=&quot;$(Platform)&quot; \/&gt;\n        &lt;Property Name=&quot;PublishPlatform&quot; Value=&quot;Windows&quot; If=&quot;&#039;$(Platform)&#039; == &#039;Win64&#039;&quot; \/&gt;\n        &lt;Log Message=&quot;##teamcity&#091;publishArtifacts &#039;game\/ArchivedBuilds\/$(PublishPlatform)Client=&gt;$(PublishPlatform)Client.zip&#039;]&quot; \/&gt;\n      &lt;\/Node&gt;\n    &lt;\/Agent&gt;\n\n    &lt;Property Name=&quot;BuildNodes&quot; Value=&quot;$(BuildNodes);Archive $(Platform) Client;Publish $(Platform) Client&quot; \/&gt;\n  &lt;\/ForEach&gt;<\/pre>\n\n\n\n<p>The script is pretty self-explanatory. We iterate over the list of platforms on which we want our client to run, which is again passed as an option. We compile for each of the platforms, cook the assets, and finally package everything. As the last step of the packaging process, we use a service message to publish the built client as a TeamCity build artifact.<\/p>\n\n\n\n<p>As you might have noticed, in this part of the script, we have three agents representing three stages of the build process (compilation, cooking, and packaging). The compilation requires tools belonging to the editor, but not the editor itself:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\">&lt;Node Name=&quot;$(CompileNodeName)&quot; Requires=&quot;$(ToolsNodeName) $(Platform)&quot; Produces=&quot;#$(Platform) Client Binaries&quot;&gt;<\/pre>\n\n\n\n<p>Meanwhile, the cooking process requires the actual editor:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\">&lt;Node Name=&quot;$(CookPlatformNodeName)&quot; Requires=&quot;$(EditorNodeName) $(Platform)&quot; Produces=&quot;#Cook $(Platform) Client Complete&quot;&gt;<\/pre>\n\n\n\n<p>And since there is no dependency between these two processes, they can run in parallel on different machines (if you have enough machines available). This simple example demonstrates the power of using BuildGraph: You simply describe pieces of work that share dependencies, and then they run simultaneously.<\/p>\n\n\n\n<p>In the interest of brevity, we won\u2019t show the rest of the script that includes the server part, as it looks very similar to what we just described, with only the set of flags differing.<\/p>\n\n\n\n<p>Finally, we&#8217;ve reached the part with the plugin configuration in TeamCity. Since most of the work is described in the BuildGraph XML script, the DSL configuration is fairly brief:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\">params {\n    param(&quot;env.UE_SharedDataCachePath&quot;, &quot;\/mnt\/agent-shared-dir\/ddc&quot;)\n    param(&quot;env.UE-SharedDataCachePath&quot;, &quot;\\\\\\\\fs-040b8d6dab476baf1.fsx.eu-west-1.amazonaws.com\\\\fsx\\\\ddc&quot;)\n}\n\nsteps {\n    unrealEngine {\n        id = &quot;Unreal_Engine&quot;\n        name = &quot;Build&quot;\n        engineDetectionMode = manual {\n            rootDir = &quot;engine&quot;\n        }\n        command = buildGraph {\n            script = &quot;game\/BuildProject.xml&quot;\n            targetNode = &quot;BuildProject&quot;\n            options = &quot;&quot;&quot;\n                ProjectPath=%teamcity.build.checkoutDir%\/game\n                ProjectName=Lyra\n                ClientPlatforms=Linux+Win64\n                ServerPlatforms=Linux\n                EditorPlatforms=Linux+Win64\n                TargetConfigurations=Shipping\n            &quot;&quot;&quot;.trimIndent()\n            mode = UnrealEngine.BuildGraphMode.Distributed\n        }\n        additionalArguments = &quot;-utf8output -buildmachine -unattended -noP4 -nosplash -stdout -NoCodeSign&quot;\n    }\n}<\/pre>\n\n\n\n<p>Here, we specify that we\u2019d like to run the given BuildGraph script in distributed mode. Nevertheless, there\u2019s always an option to run all the nodes sequentially on a single machine if we want to. We\u2019ve also specified<a href=\"https:\/\/dev.epicgames.com\/documentation\/en-us\/unreal-engine\/using-derived-data-cache-in-unreal-engine#howtosetupasharedddc\" target=\"_blank\" rel=\"noopener\"> a couple of environment variables<\/a> that are required for the engine to enable the usage of a shared DDC. Those folders should already be mounted to the agents connected to TeamCity.<\/p>\n\n\n\n<p>Now, we can pass the following list of three platform options for our game: <code>ClientPlatforms<\/code>, <code>ServerPlatforms<\/code>, and <code>EditorPlatforms<\/code>.<\/p>\n\n\n\n<p>In the UI, it looks like this:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1766\" height=\"1654\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2024\/05\/build-step-unreal-engine.png\" alt=\"\" class=\"wp-image-473563\"\/><\/figure>\n\n\n\n<p>When we launch the build, we get this build chain:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"2000\" height=\"1178\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2024\/05\/build-chain-unreal-engine.png\" alt=\"\" class=\"wp-image-473574\"\/><\/figure>\n\n\n\n<p>So, as we can see, the plugin has transformed the graph into a proper TeamCity build chain.<\/p>\n\n\n\n<p>We can check the published artifacts on the corresponding tab of the specific build. Here&#8217;s what it looks like for the Linux server:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"2000\" height=\"984\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2024\/05\/linux-server-unreal-engine-teamcity-1.png\" alt=\"\" class=\"wp-image-473596\"\/><\/figure>\n\n\n\n<p>Before finishing up, we&#8217;d like to emphasize some important points to bear in mind:<\/p>\n\n\n\n<ul>\n<li>In order for the distribution process of BuildGraph to work, your build configuration should contain exactly one active build step.<\/li>\n\n\n\n<li>Currently, the plugin does not manage the retention period of the produced artifacts in the shared storage in any way. It&#8217;s your responsibility to set it up properly.<\/li>\n\n\n\n<li>Examples in this blog post are by no means production-ready. Your real-world scenarios will likely be more complex. However, they could serve as a good starting point.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Try it out!<\/strong><\/h2>\n\n\n\n<p>You can get the demo code shown in this blog post from the<a href=\"https:\/\/github.com\/lesley29\/unreal-teamcity-demo\" target=\"_blank\" rel=\"noopener\"> GitHub repository<\/a>.<\/p>\n\n\n\n<p>If you\u2019d like to try out the Unreal Engine plugin for TeamCity, you can download it from <a href=\"https:\/\/plugins.jetbrains.com\/plugin\/22679-unreal-engine-support\" target=\"_blank\" rel=\"noopener\">JetBrains Marketplace<\/a> and install it on a TeamCity On-Premises server.<\/p>\n\n\n\n<p>For TeamCity Cloud users, the Unreal Engine plugin has been pre-installed, allowing you to use it simply by adding an Unreal Engine build step. As a reminder, TeamCity Cloud agents don\u2019t have Unreal Engine pre-installed, so you will need to use a self-hosted agent to run an Unreal Engine build.<\/p>\n\n\n\n<p>An important note for those who have been using a preview version of the plugin (any version that starts with 0.x.x): Please take into account that your existing configurations will be broken, as we have made changes and redesigned several features in the 1.0.0 version. You may need to recreate those configurations using the new version of the plugin.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>What\u2019s next?<\/strong><\/h2>\n\n\n\n<p>First off, we\u2019re looking forward to receiving your feedback. Please feel free to get in touch by submitting a ticket via our <a href=\"https:\/\/youtrack.jetbrains.com\/issues\/TW\" target=\"_blank\" rel=\"noopener\">issue tracker<\/a> or by commenting on this blog post.<\/p>\n\n\n\n<p>There are also some ideas we have in mind for further work, including:<\/p>\n\n\n\n<ul>\n<li>Providing a more in-depth build log analysis.<\/li>\n\n\n\n<li>Including integration with UnrealGameSync (UGS), with features like build status publication and the potential provision of an out-of-the-box metadata server.<\/li>\n\n\n\n<li>Adding detection of the SDKs available on build machines, possibly using Turnkey.<\/li>\n\n\n\n<li>Looking into Gauntlet Automation Framework and what we can do there.<\/li>\n\n\n\n<li>Looking into how we can leverage recent advancements in the build process introduced by Epic Games, namely, checking out Unreal Build Accelerator (UBA) and coordinating it on the agents from TeamCity.<\/li>\n\n\n\n<li>Open-sourcing the plugin. We believe that this will increase transparency and allow for the creation of a better plugin overall. Furthermore, going open source can be a lot of fun.<\/li>\n\n\n\n<li>Anything else you can think of! If you have an idea of something that would be nice to have, please feel free to reach out via any of the channels we mentioned above or simply by leaving a comment on this blog post.<\/li>\n<\/ul>\n\n\n\n<p>?Read our latest blog post &#8220;<a href=\"https:\/\/blog.jetbrains.com\/teamcity\/2024\/11\/unreal-engine-plugin-ugs-integration-and-open-sourcing\/\" data-type=\"link\" data-id=\"https:\/\/blog.jetbrains.com\/teamcity\/2024\/11\/unreal-engine-plugin-ugs-integration-and-open-sourcing\/\">Updates on Unreal Engine Support in TeamCity: UGS Integration and Open-Sourcing the Plugin<\/a>&#8221; to learn more about what&#8217;s new with the plugin.<\/p>\n\n\n\n<p>? See also:\u00a0<a href=\"https:\/\/www.jetbrains.com\/help\/teamcity\/what-s-new-in-teamcity.html\" target=\"_blank\" rel=\"noreferrer noopener\">What\u2019s New in TeamCity<\/a><\/p>\n","protected":false},"author":1494,"featured_media":473624,"comment_status":"closed","ping_status":"closed","template":"","categories":[808,1065,907],"tags":[1817,477],"cross-post-tag":[],"acf":[],"_links":{"self":[{"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/teamcity\/473406"}],"collection":[{"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/teamcity"}],"about":[{"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/types\/teamcity"}],"author":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/users\/1494"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/comments?post=473406"}],"version-history":[{"count":10,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/teamcity\/473406\/revisions"}],"predecessor-version":[{"id":549434,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/teamcity\/473406\/revisions\/549434"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/media\/473624"}],"wp:attachment":[{"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/media?parent=473406"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/categories?post=473406"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/tags?post=473406"},{"taxonomy":"cross-post-tag","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/cross-post-tag?post=473406"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}