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/