Press "Enter" to skip to content

Automatizujeme Unity v AWS: Sestavujeme naší hru

Zechy 0

Předchozí díl: Automatizujeme Unity v AWS: Příprava prostředí

V předchozí části jsme si připravili S3 Bucket pro zdrojové kódy, CodeBuild s Docker Image od GameCI, kde jsme vyzkoušeli aktivovat licenci, a nyní je čas sestavit naší aplikaci!

Na poprvé si to celé ještě připravíme nanečisto, což znamená, že k našemu projektu doplníme veškeré požadované skripty, ovšem nahrání zdrojových kódů, a následný build, provedeme ručně. Ověříme si tak, že ze strany AWS vše poběží tak, jak má.

Postup

  1. Příprava projektu
    1. BuildCommand
      1. public static void BuildFromEditor()
      2. public static void PerformBuild()
      3. private static void BuildAddressables()
    2. Sestavení hry
      1. Buildspec
      2. Artefakty
  2. Spuštění buildu
    1. Délka sestavení

Příprava projektu

V předchozím díle jsme si připravili skripty request-license.sh, activate.sh a samozřejmě *.ulf, kterým ověřujeme platnost licence pro Unity. Pro toto vše máme připravenou složku .ci, tu teď můžeme vzít a nakopírovat do kořenového adresáře našeho projektu. Nesmíme samozřejmě zapomenout na buildspec.yml pro CodeBuild.

BuildCommand

První důležitou součástí je samozřejmě možnost spustit nějak náš build. K tomu si musíme napsat vlastní statickou třídu. Tu si uložíme do složky Editor, aby tak byla k dispozici pouze pro něj. V mém případě volím strukturu Editor/Tools, čemuž i odpovídá namespace.

Následující kód je převzatý z příkladu Game.CI a následně upraven pro vlastní potřeby, chybí tak například podpora Android buildu. Originální BuildCommand.cs si lze zobrazit zde.

using System;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.AddressableAssets.Settings;
using UnityEngine;

namespace Editor.Tools
{
    /// <summary>
    /// Příkaz pro vykonání buildu.
    /// </summary>
    public static class BuildCommand
    {
        /// <summary>
        /// Vlastní argumenty pro PerformBuild.
        /// </summary>
        private static string[] _args;

        /// <summary>
        /// Provede sestavení z editoru rutinou pro CI.
        /// </summary>
        /// <exception cref="Exception">Při chybě.</exception>
        [MenuItem("Tools/Build/Build Project")]
        public static void BuildFromEditor()
        {
            var rootPath = Path.GetDirectoryName(Application.dataPath);
            if (rootPath == null)
            {
                throw new Exception("Cannot determine Application data path");
            }

            var buildPath = Path.Combine(rootPath, "Build/");
            _args = new[]
            {
                "buildPath", buildPath,
                "buildTarget", BuildTarget.StandaloneWindows64.ToString(),
                "buildName", "SestavenaHra"
            };

            ClearBuildDirectory(buildPath);
            PerformBuild();
        }

        /// <summary>
        /// Spustí build aplikace.
        /// </summary>
        public static void PerformBuild()
        {
            var start = DateTime.Now;

            Console.Write(":: Performing build");
            var buildTarget = GetBuildTarget();
            SetTargetBuild(buildTarget);

            var buildPath = GetArgument("buildPath");
            Console.WriteLine(":: Building Addresables");
            BuildAddressables();
            Console.WriteLine(":: Addressables build");
            Build(buildTarget, buildPath, GetArgument("buildName"));

            var done = DateTime.Now.Subtract(start).TotalSeconds;
            var seconds = Mathf.RoundToInt((float) (done % 60));
            var minutes = Mathf.RoundToInt((float) (done / 60));

            Console.WriteLine($":: Build done in {minutes}m {seconds}s.");
        }

        /// <summary>
        /// Nastaví BuildTarget.
        /// </summary>
        /// <param name="buildTarget">Zvolený build target.</param>
        private static void SetTargetBuild(BuildTarget buildTarget)
        {
            EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTargetGroup.Standalone, buildTarget);
            EditorUserBuildSettings.selectedStandaloneTarget = buildTarget;
        }

        /// <summary>
        /// Sestaví Addressables.
        /// </summary>
        private static void BuildAddressables()
        {
            AddressableAssetSettings.CleanPlayerContent();
            AddressableAssetSettings.BuildPlayerContent();
        }

        /// <summary>
        /// Vykonání samotné fáze buildu.
        /// </summary>
        /// <param name="buildTarget">Platforma pro build.</param>
        /// <param name="buildPath">Cesta, kde bude build uložen.</param>
        /// <param name="buildName">Název executable souboru.</param>
        /// <exception cref="Exception">Při chybě.</exception>
        private static void Build(BuildTarget buildTarget, string buildPath, string buildName)
        {
            var executable = GetExecutablePath(buildPath, buildName);
            var buildReport = BuildPipeline.BuildPlayer(GetEnabledScenes(), executable, buildTarget, BuildOptions.None);
            if (buildReport.summary.result != UnityEditor.Build.Reporting.BuildResult.Succeeded)
                throw new Exception($"Build ended with {buildReport.summary.result} status");
        }

        /// <summary>
        /// Získá argument se zadaným jménem.
        /// </summary>
        /// <param name="name">Název argument, který je hledán.</param>
        /// <returns>Hodnotu argumentu.</returns>
        /// <exception cref="ArgumentException">Pokud nebyl argument nalezen.</exception>
        private static string GetArgument(string name)
        {
            var args = _args ?? Environment.GetCommandLineArgs();
            for (var i = 0; i < args.Length; i++)
            {
                if (args[i].Contains(name))
                {
                    return args[i + 1];
                }
            }

            throw new ArgumentException($"Argument '{name}' was not found.");
        }

        /// <summary>
        /// Získá Enum hodnotu pro Build target.
        /// </summary>
        /// <returns>Definovaný target nebo NoTarget.</returns>
        private static BuildTarget GetBuildTarget()
        {
            var targetName = GetArgument("buildTarget");
            Console.WriteLine($":: Building for platform '{targetName}'");

            if (targetName.TryConvertToEnum(out BuildTarget value))
            {
                return value;
            }

            Console.WriteLine(
                $":: {nameof(targetName)} '{targetName}' is not defined on enum {nameof(BuildTarget)}, using {nameof(BuildTarget.NoTarget)}");

            return BuildTarget.NoTarget;
        }

        /// <summary>
        /// Vygeneruje cestu pro executable.
        /// </summary>
        /// <param name="buildPath">Cesta pro uložení sestavené aplikace.</param>
        /// <param name="buildName">Název buildu.</param>
        /// <returns>Cesta pro executable.</returns>
        private static string GetExecutablePath(string buildPath, string buildName)
        {
            return buildPath + buildName + ".exe";
        }

        /// <summary>
        /// Načte scény, které jsou k dispozici.
        /// </summary>
        /// <returns>Seznam dostupných scén.</returns>
        private static string[] GetEnabledScenes()
        {
            return (
                from scene in EditorBuildSettings.scenes
                where scene.enabled
                where !string.IsNullOrEmpty(scene.path)
                select scene.path
            ).ToArray();
        }

        /// <summary>
        /// Vyčistí složku, do které je ukládán build.
        /// </summary>
        /// <param name="buildPath">Cesta pro build.</param>
        private static void ClearBuildDirectory(string buildPath)
        {
            var directoryInfo = new DirectoryInfo(buildPath);
            foreach (var file in directoryInfo.GetFiles())
            {
                file.Delete();
            }

            foreach (var dir in directoryInfo.GetDirectories())
            {
                dir.Delete(true);
            }
        }
    }
}

Je to tentokrát trochu delší, i s mými vlastními komentáři, pojďme se ovšem zaměřit na to nejdůležitější. Vše ostatní by měli popisovat komentáře.

public static void BuildFromEditor()

Že všechno funguje na jedničku, co se buildu týče, si můžeme vyzkoušet i z Editoru, atributem [MenuItem] říkáme Unity, aby do horní nabídky v Editoru přidalo/rozšířilo položku Tools o Build > Build Project. Z editoru tak do složky projektu vytvoříme sestavenou hru se složkou Build. Tato metoda nakonec nedělá nic jiného, než že nasimuluje předané parametry, které následně zpracovává metoda PerformBuild().

public static void PerformBuild()

Tuto metodu budeme spouštět pomocí našeho build skriptu. Ta již nastaví námi požadovanou platformu, pro kterou budeme hru sestavovat, sestaví Addressables a následně provede už samotný build. K tomu všemu, jako bonus, si samozřejmě zapíšeme jak dlouho trval samotný build.

private static void BuildAddressables()

Používáte Addressables? Využívá je například nativní Unity lokalizace. Tento přístup má uplatnění pro dodatečné načítání obsahu, kdy se na něj odkazuje pomocí specifických adres. Je to tak například využitelné pro DLC nebo pokud musíte stahovat k vaší hře dodatečné soubory.

Addressables jsou trošku oříšek, protože před každým buildem aplikace musíte sestavit i aktualizovanou databázi a Unity to za vás řešit nebude. Je to ovšem snazší než se zdá a tato metoda je toho důkazem.

Sestavení hry

Pokud jsme si úspěšně vyzkoušeli jak funguje BuildCommand, můžeme se vrhnout na další skripty pro CodeBuild. Pojďme si tak připravit složku, do které se výsledná hra uloží.

#!/usr/bin/env bash

DIR="Build"

if [ -d "$DIR" ]; then
  rm -rf $DIR
fi

mkdir -p $DIR
if [ ! -d "$DIR" ]; then
  echo "Directory $(pwd)/$DIR could not be created."
  exit 1
else
  echo "Build directory ready."
  exit 0
fi

Pokud již složka Build bude existovat, smažeme ji s jejím obsahem a následně vytvoříme znova – čistou a prázdnou. Můžeme tedy pokračovat na další krok – samotné sestavení.

#!/usr/bin/env bash

PROJECT_PATH=$(pwd)
BUILD_TARGET=StandaloneWindows64
EXEC_METHOD=Editor.Tools.BuildCommand.PerformBuild
BUILD_PATH=./Build/
BUILD_NAME=SestavenaHra
LOG_OUTPUT=./unity-output.log

echo "Starting Unity Build"

OUTPUT=$(unity-editor -nographics -logFile /dev/stdout -quit -projectPath "$PROJECT_PATH" -buildTarget "$BUILD_TARGET" -buildPath "$BUILD_PATH" -buildName "$BUILD_NAME" -executeMethod "$EXEC_METHOD")
UNITY_EXIT_CODE=$?
echo "$OUTPUT" >> "$LOG_OUTPUT"

if [ $UNITY_EXIT_CODE -eq 0 ]; then
  echo "Build succeeded"
else
  echo "Build failed with exit code $UNITY_EXIT_CODE"
fi

Zde už si musíme připravit několik parametrů.

  • PROJECT_PATH říká, kde se projekt nachází. V našem případě current working directory, tedy příkaz $(pwd)
  • BUILD_TARGET nastavuje cílovou platformu, tedy x64 verzi pro Windows
  • EXEC_METHOD odkáže unity na to, který příkaz má spustit
  • BUILD_PATH specifikuje složku, do které je build uložen
  • BUILD_NAME definuje jak se bude jmenovat náš *.exe soubor
  • LOG_OUTPUT cesta k souboru, kam zapisujeme výstup z editoru

Pokud jste se na BuildCommand.cs dívali pozorně, můžete si všimnout, že většinu parametrů použité pro unity-editor zpracováváme v rámci tohoto skriptu. Následný postup už pak samozřejmě známe, uložíme si exit code a do našeho unity-output přidáme další výstup. Zde je rozdíl oproti operátoru „>“ a „>>“. Zatímco operátor > obsah do souboru zapíše tak, že jej kompletně přepíše, operátor >> nám náš řetězec do souboru pouze přidá již k tomu, co existuje. K výstupu z aktivace editoru se tak přidají detaily o tom, jak build proběhl.

To by ovšem nebylo ono, kdybychom si neověřili, že build proběhl. K tomu využijeme následující skript.

#!/usr/bin/env bash

echo "Checking build folder"
ls -la Build
if [ -n "$(ls -A Build)" ]; then
  echo "Build data confirmed"
else
  echo "Build data does not confimed"
fi

Vypíšeme si obsah složky build (který případně můžeme zkontrolovat ve výstupu z CodeBuild) a samozřejmě ověříme, že složka Build vůbec něco obsahuje. Pokud ano, můžeme předpokládat, že build proběhl jak má. Nyní je čas rozšířit náš buildspec.yml.

Buildspec

version: 0.2

phases:
  install:
    commands:
      - chmod +x ./.ci/activate.sh
      - chmod +x ./.ci/build.sh
      - chmod +x ./.ci/check-build.sh
      - chmod +x ./.ci/create-folder.sh
      - ./.ci/activate.sh
  pre_build:
    commands:
      - ./.ci/create-folder.sh
  build:
    commands:
      - ./.ci/build.sh
  post_build:
    commands:
      - ./.ci/check-build.sh
artifacts:
  files:
    - '**/*'
  base-directory: ./Build
  name: sestaveno-latest.zip
  secondary-artifacts:
    buildVersion:
      files:
        - '**/*'
      base-directory: ./Build
      name: sestaveno-$CODEBUILD_BUILD_NUMBER.zip
    unityLog:
      files:
        - ./unity-output.log
      name: unity-output-$CODEBUILD_BUILD_NUMBER.zip
    unityLatest:
      files:
        - ./unity-output.log
      name: unity-output-latest.zip

Tentokrát si už těch fází nastavíme více. Fázi install rozšíříme o nastavení chmod +x (executable) pro naše nové skripty a opět zde budeme provádět aktivaci editoru.

Následuje fáze pre_build, spouštějící create-folder.sh. Dále build spouštějící build.sh a nakonec post_build spouštějící check-build.sh. Jednotlivé fáze jsou závislé na té předchozí, pokud tedy například selže pre_build, už se dál nic neřeší a build selhal. Pokud by nám selhal post_build, CodeBuild nebude řešit artefakty.

Artefakty

Tentokrát si těch artefaktů přidáme o něco více. Náš primární bude latest build, tento archiv bude obsahovat vždy poslední build naší hry. Pro tento artefakt se projede vše, co se skrývá ve složce Build a bude uloženo do zip souboru.

V sekundárních artefaktech si připravíme archivy pro samostatné buildy a unity logy, opět podobnou mechanikou, akorát pro soubory, které označují tyto buildy využijeme proměnnou $CODEBUILD_BUILD_NUMBER, která obsahuje číslo buildu, který právě proběhl.

V našem projektu tentokrát pomocí tlačítka Edit konečně nastavíme na tvrdo naše artefakty tak, jak jsme to do teď dělali při Build with overrides. Ovšem s jistou změnou.

Nastavení artefaktu
Nastavení artefaktu

Vidíte rozdíl? První změnou je použití checkboxu Enable semantic versioning, což znamená, že pro vygenerování názvu našeho artefaktu se použije položka name z buildspec. V tomto případě se tak na cílovou složku musíme odkázat pomocí pole Path. Jistým bonusem je zde i Disable artifact encryption, pokud budete sdílet výsledný artefakt s ostatními. Tuto možnost využívám u primárního artefaktu, aby testeři měli k dispozici pod jedním odkazem vždy poslední verzi hry.

A u těch ostatních to samozřejmě nastavíme podobným způsobem.

Spuštění buildu

Nyní už pouze stačí zabalit náš projekt. Abychom to měli co nejvěrnější realitě toho, co se bude následně kopírovat z GitLabu, stačí zabalit pouze následující složky a soubory:

  • .ci
  • Assets
  • Packages
  • ProjectSettings
  • buildspec.yml

Je tedy pravda, že pokud necháme následně zabalit projekt GitLab, ocitnou se tam i věci jako .git či .gitignore nebo .gitattributes, pokud je využíváte. Bez toho se ale náš build obejde.

Nahrávání proběhne opět stejným způsobem jako jsme si ukazovali v posledním díle, náš archiv pojmenovaný jako codebuild.zip nahrajeme opět do S3 Bucket na své místo a přepneme se do CodeBuild, zde už nám stačí kliknout na tlačítko Start Build a počkat na výsledek.

Délka sestavení

Jak dlouho takový build trvá? V předchozím díle jsme zjistili, že nějakou minutku si ukousne PROVISIONING, v této fázi CodeBuild stahuje Docker Image a připravuje náš operační systém pro spuštění. Stěžejní je ovšem fáze Build. Na nejnižším možném prostředí, se kterým operujeme pro tento tutoriál, trvá celkové sestavení cca 150MB hry téměř ±13 minut. Nejedná se ovšem o samotnou fázi sestavení hry, ale samozřejmě o to, že se do unity editoru importuje celý projekt. Samotné sestavení je otázkou poměrně kratší doby.

Tento proces je samozřejmě možné snížit využitím lepšího prostředí. Druhá nejlepší varianta je schopná délka buildu srazit na ±7 minut.

Pokud nám build skončí zelenou, najdeme v naší složce artifacts sestavenou hru tak, jak jsme si definovali v buildspec.yml.

A jelikož nic nebudeme dělat ručně, v příštím díle si ukážeme, jak to vše zautomatizujeme již na našem GitLabu!

Předchozí díl: Automatizujeme Unity v AWS: Příprava prostředí

Napsat komentář