Unity + git = дружба: часть 1 джентльменский набор

  • Tutorial
image
Система контроля версий git уже давно стала стандартом де-факто в мире разработки, но для большинства разработчиков на Unity не секрет, что существует ряд трудностей связанных с особенностями Unity, которые мешают эффективно использовать ее совместно с git.

Вот список типичных проблем:

  1. в репозиторий попадают ненужные файлы или наоборот не попадают нужные
  2. множество больших файлов раздувает размер репозитория
  3. проблема с мерджем yaml файлов Unity
  4. в репозиторий добавлен только сам файл или только meta
  5. в проекте присутствуют пустые папки
  6. сложность автоматической нумерации версий и билдов
  7. неудобство использования кода между несколькими проектами

О решение этих проблем, связанных с совместным использованием git и Unity, вы можете прочитать в моем цикле статей.

В этой статье будет описано решение первых трех проблем

Попробуем по шагам расписать методы решения каждой из проблем

Первое, что нам нужно будет сделать после создания репозитория для вашего проекта, это настроить исключения, не буду очень подробно на этом останавливаться, вот хороший пример.

Единственное замечание, что скорее всего в конец стоит дописать пару исключений
!*.dll — так как, если вы будете использовать плагины или сторонние ассеты, то dll вам придется хранить в репозитории а в Windows git игнорирует dll по умолчанию;
!*.obj — если используете модели в этом формате, опять же в windows obj файлы могут по умолчанию игнорироваться

моя версия .gitignore
# This .gitignore file should be placed at the root of your Unity project directory
#
# Get latest from https://github.com/github/gitignore/blob/master/Unity.gitignore
#
/[Ll]ibrary/
/[Tt]emp/
/[Oo]bj/
/[Bb]uild/
/[Bb]uilds/
/[Ll]ogs/
/[Uu]ser[Ss]ettings/

# MemoryCaptures can get excessive in size.
# They also could contain extremely sensitive data
/[Mm]emoryCaptures/

# Asset meta data should only be ignored when the corresponding asset is also ignored
!/[Aa]ssets/**/*.meta

# Uncomment this line if you wish to ignore the asset store tools plugin
# /[Aa]ssets/AssetStoreTools*

# Autogenerated Jetbrains Rider plugin
/[Aa]ssets/Plugins/Editor/JetBrains*

# Visual Studio cache directory
.vs/

# Gradle cache directory
.gradle/

# Autogenerated VS/MD/Consulo solution and project files
ExportedObj/
.consulo/
*.csproj
*.unityproj
*.sln
*.suo
*.tmp
*.user
*.userprefs
*.pidb
*.booproj
*.svd
*.pdb
*.mdb
*.opendb
*.VC.db

# Unity3D generated meta files
*.pidb.meta
*.pdb.meta
*.mdb.meta

# Unity3D generated file on crash reports
sysinfo.txt

# Builds
*.apk
*.unitypackage

# Crashlytics generated file
crashlytics-build.properties

# Packed Addressables
/[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin*

# Temporary auto-generated Android Assets
/[Aa]ssets/[Ss]treamingAssets/aa.meta
/[Aa]ssets/[Ss]treamingAssets/aa/*

# Exceptions
!*.dll
!*.obj


Вторым шагом мы попробуем решить проблему роста репозитория от больших файлов. Этим решением является LFS

подробнее про LFS
LFS — это надстройка над git, которая сохраняет в git репозиторий вместо бинарного файла его идентификатор, а сам файл кладет в специальное key-value хранилище.

Таким образом, в самом git репозитории хранится только LFS заглушка файла а сам файл после checkout скачивается из хранилища.

Неплохая статья правда на английском:
www.atlassian.com/git/tutorials/git-lfs

Чтобы настроить lfs типы файлов для нашего репозитори добавим в файл .gitattributes в корне проекта несколько строчек (возможно вам придется его создать, причем Windows может не дать создать файл с таким имеем в Explorer)

## git-lfs ##

#Image
*.jpg filter=lfs diff=lfs merge=lfs -text
*.jpeg filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.gif filter=lfs diff=lfs merge=lfs -text
*.psd filter=lfs diff=lfs merge=lfs -text
*.ai filter=lfs diff=lfs merge=lfs -text
*.tif filter=lfs diff=lfs merge=lfs -text
*.tga filter=lfs diff=lfs merge=lfs -text
*.cubemap filter=lfs diff=lfs merge=lfs -text
*.svg filter=lfs diff=lfs merge=lfs -text

#Audio
*.mp3 filter=lfs diff=lfs merge=lfs -text
*.wav filter=lfs diff=lfs merge=lfs -text
*.ogg filter=lfs diff=lfs merge=lfs -text

#Video
*.mp4 filter=lfs diff=lfs merge=lfs -text
*.mov filter=lfs diff=lfs merge=lfs -text
*.webm filter=lfs diff=lfs merge=lfs -text

#3D Object
*.FBX filter=lfs diff=lfs merge=lfs -text
*.fbx filter=lfs diff=lfs merge=lfs -text
*.blend filter=lfs diff=lfs merge=lfs -text
*.obj filter=lfs diff=lfs merge=lfs -text

#ETC
*.a filter=lfs diff=lfs merge=lfs -text
*.exr filter=lfs diff=lfs merge=lfs -text
*.pdf filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text
*.dll filter=lfs diff=lfs merge=lfs -text
*.unitypackage filter=lfs diff=lfs merge=lfs -text
*.aif filter=lfs diff=lfs merge=lfs -text
*.ttf filter=lfs diff=lfs merge=lfs -text
*.rns filter=lfs diff=lfs merge=lfs -text
*.reason filter=lfs diff=lfs merge=lfs -text
*.lxo filter=lfs diff=lfs merge=lfs -text

здесь перечислены большинство файлов который являются бинарными и могут быть достаточно большого размера. немного поясню:

строки, начинающиеся с # — это комментарии;
filter=lfs diff=lfs merge=lfs — это волшебные слова, заставляющие git использовать lfs для этих типов файлов; -text означает что файл бинарный и мерджить его не надо.

Если в вашем проекте используются еще какие-то большие бинарные файлы, допишите их сюда в дальнейшем поменять тип хранения файла (перенести в lfs или вынести оттуда) будет довольно сложно.

Следующим шагом попробуем немного немного улучшить ситуацию со сложными мерджами.
В составе Unity есть утилита UnityYAMLMerge, которая позволяет эффективно мерджить yaml файлы. Добавим в файл .gitattributes еще несколько строчек:

*.cs diff=csharp text
*.cginc text
*.shader text

*.mat merge=unityyamlmerge
*.anim merge=unityyamlmerge
*.unity merge=unityyamlmerge
*.prefab merge=unityyamlmerge
*.physicsMaterial2D merge=unityyamlmerge
*.physicsMaterial merge=unityyamlmerge
*.asset merge=unityyamlmerge
*.meta merge=unityyamlmerge
*.controller merge=unityyamlmerge

Поясню, что мы сделали:
для .cs файлов подсказали, что там будет текст являющийся C# кодом;
для файлов cginc и shader тоже выбрали текстовое представление
для большинства Unity yaml файлов выбрали кастомный драйвер слияния (custom merge driver) unityyamlmerge

Также необходимо его настроить: добавим следующий код в любой .gitconfig, проще всего в локальный, находящийся по пути .git/config от корня репозитория:

[merge "unityyamlmerge"]
	name = Unity SmartMerge (UnityYamlMerge)
	driver = \"{путь к папке с Unity}/Editor/Data/Tools/UnityYAMLMerge.exe\" merge -h -p --force --fallback none %O %B %A %A
	recursive = binary

Флаг -p заставляет UnityYamlMerge менять содержимое файлов даже если полностью конфликт разрешить не удалось, и сильно упрощает его дальнейшее решение руками. Например при слияние двух веток где было изменена одна и та же сцена, при использовании стандартного механизма слияния git, мы увидим множество изменений. При использование кастомного драйвера даже если было изменено одно и тоже поле одного и того же компонента, в конфликте будет ровно 1 строчка.

Для удобства я создал небольшой скрипт, который позволит провести установку unityyamlmerge автоматически при первом открытие проекта версией Unity. Его можно положить в любое место внутри папки Assets (требует чтобы git был установлен в системе и был прописан в переменной PATH т.е. был доступен по команде git);

этот класс
#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;
using System;

namespace GitIntegration
{
    [InitializeOnLoad]
    public class SmartMergeRegistrator
    {
        const string SmartMergeRegistratorEditorPrefsKey = "smart_merge_installed";
        const int Version = 1;
        static string VersionKey = $"{Version}_{Application.unityVersion}";

        public static string ExecuteGitWithParams(string param)
        {
            var processInfo = new System.Diagnostics.ProcessStartInfo("git");

            processInfo.UseShellExecute = false;
            processInfo.WorkingDirectory = Environment.CurrentDirectory;
            processInfo.RedirectStandardOutput = true;
            processInfo.RedirectStandardError = true;
            processInfo.CreateNoWindow = true;

            var process = new System.Diagnostics.Process();
            process.StartInfo = processInfo;
            process.StartInfo.FileName = "git";
            process.StartInfo.Arguments = param;
            process.Start();
            process.WaitForExit();

            if (process.ExitCode != 0)
                throw new Exception(process.StandardError.ReadLine());

            return process.StandardOutput.ReadLine();
        }

        [MenuItem("Tools/Git/SmartMerge registration")]
        static void SmartMergeRegister()
        {
            try
            {
                var UnityYAMLMergePath = EditorApplication.applicationContentsPath + "/Tools" + "/UnityYAMLMerge.exe";
                ExecuteGitWithParams("config merge.unityyamlmerge.name \"Unity SmartMerge (UnityYamlMerge)\"");
                ExecuteGitWithParams($"config merge.unityyamlmerge.driver \"\\\"{UnityYAMLMergePath}\\\" merge -h -p --force --fallback none %O %B %A %A\"");
                ExecuteGitWithParams("config merge.unityyamlmerge.recursive binary");
                EditorPrefs.SetString(SmartMergeRegistratorEditorPrefsKey, VersionKey);
                Debug.Log($"Succesfuly registered UnityYAMLMerge with path {UnityYAMLMergePath}");
            }
            catch (Exception e)
            {
                Debug.Log($"Fail to register UnityYAMLMerge with error: {e}");
            }
        }

        //Unity calls the static constructor when the engine opens
        static SmartMergeRegistrator()
        {
            var instaledVersionKey = EditorPrefs.GetString(SmartMergeRegistratorEditorPrefsKey);
            if (instaledVersionKey != VersionKey)
                SmartMergeRegister();
        }
    }
}
#endif

Принцип работы: каждый раз, когда Unity запускается или перекомпилирует скрипты, мы проверяем совпадения ключа в EditorPrefs с нашим «актуальным» ключом, если нет(либо поменялась версия нашего скрипта, либо версия Unity, либо это первый запуск) мы через команды git дописываем в локальный gitconfig настройки драйвера.

финальная версия .gitattributes
## Unity ##

*.cs diff=csharp text
*.cginc text
*.shader text

*.mat merge=unityyamlmerge
*.anim merge=unityyamlmerge
*.unity merge=unityyamlmerge
*.prefab merge=unityyamlmerge
*.physicsMaterial2D merge=unityyamlmerge
*.physicsMaterial merge=unityyamlmerge
*.asset merge=unityyamlmerge
*.meta merge=unityyamlmerge
*.controller merge=unityyamlmerge


## git-lfs ##

#Image
*.jpg filter=lfs diff=lfs merge=lfs -text
*.jpeg filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.gif filter=lfs diff=lfs merge=lfs -text
*.psd filter=lfs diff=lfs merge=lfs -text
*.ai filter=lfs diff=lfs merge=lfs -text
*.tif filter=lfs diff=lfs merge=lfs -text
*.tga filter=lfs diff=lfs merge=lfs -text
*.cubemap filter=lfs diff=lfs merge=lfs -text
*.svg filter=lfs diff=lfs merge=lfs -text

#Audio
*.mp3 filter=lfs diff=lfs merge=lfs -text
*.wav filter=lfs diff=lfs merge=lfs -text
*.ogg filter=lfs diff=lfs merge=lfs -text

#Video
*.mp4 filter=lfs diff=lfs merge=lfs -text
*.mov filter=lfs diff=lfs merge=lfs -text
*.webm filter=lfs diff=lfs merge=lfs -text

#3D Object
*.FBX filter=lfs diff=lfs merge=lfs -text
*.fbx filter=lfs diff=lfs merge=lfs -text
*.blend filter=lfs diff=lfs merge=lfs -text
*.obj filter=lfs diff=lfs merge=lfs -text

#ETC
*.a filter=lfs diff=lfs merge=lfs -text
*.exr filter=lfs diff=lfs merge=lfs -text
*.pdf filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text
*.dll filter=lfs diff=lfs merge=lfs -text
*.unitypackage filter=lfs diff=lfs merge=lfs -text
*.aif filter=lfs diff=lfs merge=lfs -text
*.ttf filter=lfs diff=lfs merge=lfs -text
*.rns filter=lfs diff=lfs merge=lfs -text
*.reason filter=lfs diff=lfs merge=lfs -text
*.lxo filter=lfs diff=lfs merge=lfs -text


После выполнения этих шагов настоятельно рекомендую закомитить текущее состояние репозитория.

Готовый проект https://github.com/newnon/UnityGitHabr1
еще раз напомню для корректной работы, git должен быть уставлен в системе и доступен по команде git.

Если хотите поэкспериментировать
В репозитории на github есть бренчи с именами test1 test2 test3
Без установленного кастомного merge драйвера в test1 без конфликта не удастся вмерджить ни test2 ни test3
При установленном драйвере test2 вливается в test1 без конфликта а test3 с конфликтом в одну строчку в измененном m_LocalPosition
Eстановить удалить драйвер можно в любой момент через меню Unity Tools/Git/
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 15

    +1

    Спасибо за статью!
    Я тут так-же разбирался с тем, как нормально подружить Unity и Git. Пусть будет как дополнительная информация.


    Клиент для гита использую SmartGit (Недавно перешел на него с SourceTree)


    Использование Unity + Git + Yaml (Smart Merge) в SmartGit — без правки .gitattributes


    Настройка SmartGit

    Go to: Preferences -> Conflict Solvers:


    File Pattern: "*.meta, *.unity, *.asset, *.prefab"
    Command: "..\Unity\Editor\Data\Tools\UnityYAMLMerge.exe"
    Arguments: "merge -p ${baseFile} ${rightFile} ${leftFile} ${mergedFile}"


    image


    Настройка Unity YAML (Smart Merge) для Meld Merge

    Add this code to mergespecfile.txt file (line 20):


    ..\Unity\Editor\Data\Tools\mergespecfile.txt

    # Meld merge (Recommended)
    * use "..\Meld\Meld.exe" "%r" "%b" "%l" -o "%d" --auto-merge
      0
      Да это рекомендованный Unity метод но он позволяет разруливать уже случившийся конфликт, мой метод позволяет конфликту не случиться.
        0

        Да, все верно — это в случае если мерж не прошел успешно (по методу описанному в статье).

          0
          но благодаря флагу -p Smart Merge частично смердживает что может без конфликтов, и конфликты остаются уже явно только по существу, так что при таком раскладе устанавливать Smart Merge в качестве утилиты решения конфликтов уже не имеет смысла.
            0

            В настройках SmartGit стоит этот же ключ "-p", по этому он также автоматически мержит такие изменения (не конфликтные)

      0
      А как добавить поддержку *.m файлов?
      (для исходников iOS-компонент)
        0
        Никакой дополнительной настройки не требуется. Но если очень хочется можно попробовать в .gitattributes добавить строку *.m diff=objc text но работать будет и без этого, и я не готов точно сказать насколько это позволит эффективней мерджить такие файлы
          0
          Да вот что-то гит не видит их ни со стройкой, ни без строки…
            0
            попробовал у себя локально создав файл test.m в корне асетов все ок
            либо на компьютере есть файл .gitignore из за которого файлы .m игнорируются либо этот файл лежит в папке которая игнорируется
        0
        Кстати, для юнити есть официальный GitHub-клиент.
        assetstore.unity.com/packages/tools/version-control/github-for-unity-118069
          0
          да я видел этот проект, к сожалению он сильно заточен на API github и не подходит если проект хостится не там
          0

          Спасибо за статью, будем ждать продолжения.

            0
            У тебя в .gitignore написано:
            *.unitypackage

            И в .gitattributes тоже
            *.unitypackage

            Выходит что ты и игнориишь все файлы с расширением .unitypackage и вносишь их в LFS.
              0
              Да я для себя решил что unitypackage хранить в репозитории не буду, но вот из .gitattributes убрать забыл.
              С другой стороны уж если их хранить то лучше в LFS
                0

                Иногда бывают моменты, что некоторые пакеты (из плагинов) надо наоборот включить в билд в обход основного правила .gitignore:


                !/SomePlugin/*.unitypackage

                В таком случае — эта конфигурация является верной.

              Only users with full accounts can post comments. Log in, please.