Skip to content

Instantly share code, notes, and snippets.

@j-mai
Last active April 18, 2024 10:44
Show Gist options
  • Star 30 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save j-mai/4389f587a079cb9f9f07602e4444a6ed to your computer and use it in GitHub Desktop.
Save j-mai/4389f587a079cb9f9f07602e4444a6ed to your computer and use it in GitHub Desktop.
A guide for using Unity and Git

Using Git with Unity

What are the problems?

  • Noise: Unity editor has lots of temporary files that we don’t want git to track
  • Broken object references: Unity keeps track of objects created by using random GUIDs, and if they aren’t tracked using .meta files then there can be conflicts that really break your project
  • Unresolvable merge conflicts: files like scene files that are written in confusing languages (like YAML) that are supposed to be translations of Unity editor actions into code. Most likely, you cannot resolve using Git, and the only way to resolve merge conflicts is to open it in a text editor and resolve them manually while hoping you don't mess anything up because these files are confusing and not meant to be manipulated directly.
  • Large files: Sometimes assets are large and take up a lot of storage space

💻 Project Setup

A few things we can do to help prevent some of these problems:

  1. Add Unity specific .gitignore
  2. Configure Unity for version control
  3. Use GitHub For Unity
  4. Use Git Large File Storage

Before Starting: Make sure that your Unity project has a .git folder. If not, use Terminal or Command Prompt to go into the Unity project folder and run git init. Use git remote add origin <git url> to add the repo URL as a remote connection.

1. Adding a Unity Specific .gitignore

2. Configuring Unity for Version Control

  1. Open the editor settings window.
    • Edit > Project Settings > Editor
  2. Make .meta files visible to avoid broken object references.
    • Version Control / Mode: “Visible Meta Files”
  3. Use plain text serialization to avoid unresolvable merge conflicts.
    • Asset Serialization / Mode: “Force Text”
  4. Save your changes.
    • File > Save Project

3. Configuring Git to use Unity Smart Merge for merging

Unity's Smart Merge (also called UnityYAMLMerge) is a tool shipped with the Unity editor. It is a pretty decent tool to use for resolving merge conflicts that would be really hard to merge maually (e.g. scene files). You can read more about it at the following links.

Assuming Unity is installed in the standard location, the path to UnityYAMLMerge will be...

on Windows:

C:\Program Files\Unity\Editor\Data\Tools\UnityYAMLMerge.exe

or

C:\Program Files (x86)\Unity\Editor\Data\Tools\UnityYAMLMerge.exe

on Mac OSX:

/Applications/Unity/Unity.app/Contents/Tools/UnityYAMLMerge

🚀 Important: Add the following to your .git or .gitconfig file:

[merge]
tool = unityyamlmerge

[mergetool "unityyamlmerge"]
trustExitCode = false
cmd = '<path to UnityYAMLMerge>' merge -p "$BASE" "$REMOTE" "$LOCAL" "$MERGED"

Make sure that in your .gitattributes file, the following is present:

# For line ending normalization between Unix and Windows
* text=auto

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

*.mat -text merge=unityyamlmerge diff
*.anim -text merge=unityyamlmerge diff
*.unity -text merge=unityyamlmerge diff
*.prefab -text merge=unityyamlmerge diff
*.physicsMaterial2D -text merge=unityyamlmerge diff
*.physicMaterial -text merge=unityyamlmerge diff
*.asset -text merge=unityyamlmerge diff
*.meta -text merge=unityyamlmerge diff
*.controller -text merge=unityyamlmerge diff

This tells git to use UnityYAMLMerge when resolving conflicts for meta files, scenes, prefabs, etc. This is based off of https://gist.github.com/nemotoo/b8a1c3a0f1225bb9231979f389fd4f3f.

4. Using Git Large File Storage System (Git LFS)

Git LFS uses .gitattributes to track large files with git.

Instructions to install and use are here.

A good .gitattributes to use:

# 3D models
*.3dm filter=lfs diff=lfs merge=lfs -text
*.3ds filter=lfs diff=lfs merge=lfs -text
*.blend filter=lfs diff=lfs merge=lfs -text
*.c4d filter=lfs diff=lfs merge=lfs -text
*.collada filter=lfs diff=lfs merge=lfs -text
*.dae filter=lfs diff=lfs merge=lfs -text
*.dxf filter=lfs diff=lfs merge=lfs -text
*.fbx filter=lfs diff=lfs merge=lfs -text
*.jas filter=lfs diff=lfs merge=lfs -text
*.lws filter=lfs diff=lfs merge=lfs -text
*.lxo filter=lfs diff=lfs merge=lfs -text
*.ma filter=lfs diff=lfs merge=lfs -text
*.max filter=lfs diff=lfs merge=lfs -text
*.mb filter=lfs diff=lfs merge=lfs -text
*.obj filter=lfs diff=lfs merge=lfs -text
*.ply filter=lfs diff=lfs merge=lfs -text
*.skp filter=lfs diff=lfs merge=lfs -text
*.stl filter=lfs diff=lfs merge=lfs -text
*.ztl filter=lfs diff=lfs merge=lfs -text
# Audio
*.aif filter=lfs diff=lfs merge=lfs -text
*.aiff filter=lfs diff=lfs merge=lfs -text
*.it filter=lfs diff=lfs merge=lfs -text
*.mod filter=lfs diff=lfs merge=lfs -text
*.mp3 filter=lfs diff=lfs merge=lfs -text
*.ogg filter=lfs diff=lfs merge=lfs -text
*.s3m filter=lfs diff=lfs merge=lfs -text
*.wav filter=lfs diff=lfs merge=lfs -text
*.xm filter=lfs diff=lfs merge=lfs -text
# Fonts
*.otf filter=lfs diff=lfs merge=lfs -text
*.ttf filter=lfs diff=lfs merge=lfs -text
# Images
*.bmp filter=lfs diff=lfs merge=lfs -text
*.exr filter=lfs diff=lfs merge=lfs -text
*.gif filter=lfs diff=lfs merge=lfs -text
*.hdr filter=lfs diff=lfs merge=lfs -text
*.iff filter=lfs diff=lfs merge=lfs -text
*.jpeg filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.pict filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.psd filter=lfs diff=lfs merge=lfs -text
*.tga filter=lfs diff=lfs merge=lfs -text
*.tif filter=lfs diff=lfs merge=lfs -text
*.tiff filter=lfs diff=lfs merge=lfs -text

To reduce clutter on git when you are reviewing files on git (as YAML files are often not changeable or actionable on git), add this to the .gitattributes file:

# Collapse Unity-generated files on GitHub
*.asset linguist-generated
*.mat linguist-generated
*.meta linguist-generated
*.prefab linguist-generated
*.unity linguist-generated

🚀And so at the end of project setup, your entire .gitattributes should look like:

# For line ending normalization between Unix and Windows
* text=auto

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

*.mat -text merge=unityyamlmerge diff
*.anim -text merge=unityyamlmerge diff
*.unity -text merge=unityyamlmerge diff
*.prefab -text merge=unityyamlmerge diff
*.physicsMaterial2D -text merge=unityyamlmerge diff
*.physicMaterial -text merge=unityyamlmerge diff
*.asset -text merge=unityyamlmerge diff
*.meta -text merge=unityyamlmerge diff
*.controller -text merge=unityyamlmerge diff

# 3D models
*.3dm filter=lfs diff=lfs merge=lfs -text
*.3ds filter=lfs diff=lfs merge=lfs -text
*.blend filter=lfs diff=lfs merge=lfs -text
*.c4d filter=lfs diff=lfs merge=lfs -text
*.collada filter=lfs diff=lfs merge=lfs -text
*.dae filter=lfs diff=lfs merge=lfs -text
*.dxf filter=lfs diff=lfs merge=lfs -text
*.fbx filter=lfs diff=lfs merge=lfs -text
*.jas filter=lfs diff=lfs merge=lfs -text
*.lws filter=lfs diff=lfs merge=lfs -text
*.lxo filter=lfs diff=lfs merge=lfs -text
*.ma filter=lfs diff=lfs merge=lfs -text
*.max filter=lfs diff=lfs merge=lfs -text
*.mb filter=lfs diff=lfs merge=lfs -text
*.obj filter=lfs diff=lfs merge=lfs -text
*.ply filter=lfs diff=lfs merge=lfs -text
*.skp filter=lfs diff=lfs merge=lfs -text
*.stl filter=lfs diff=lfs merge=lfs -text
*.ztl filter=lfs diff=lfs merge=lfs -text
# Audio
*.aif filter=lfs diff=lfs merge=lfs -text
*.aiff filter=lfs diff=lfs merge=lfs -text
*.it filter=lfs diff=lfs merge=lfs -text
*.mod filter=lfs diff=lfs merge=lfs -text
*.mp3 filter=lfs diff=lfs merge=lfs -text
*.ogg filter=lfs diff=lfs merge=lfs -text
*.s3m filter=lfs diff=lfs merge=lfs -text
*.wav filter=lfs diff=lfs merge=lfs -text
*.xm filter=lfs diff=lfs merge=lfs -text
# Fonts
*.otf filter=lfs diff=lfs merge=lfs -text
*.ttf filter=lfs diff=lfs merge=lfs -text
# Images
*.bmp filter=lfs diff=lfs merge=lfs -text
*.exr filter=lfs diff=lfs merge=lfs -text
*.gif filter=lfs diff=lfs merge=lfs -text
*.hdr filter=lfs diff=lfs merge=lfs -text
*.iff filter=lfs diff=lfs merge=lfs -text
*.jpeg filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.pict filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.psd filter=lfs diff=lfs merge=lfs -text
*.tga filter=lfs diff=lfs merge=lfs -text
*.tif filter=lfs diff=lfs merge=lfs -text
*.tiff filter=lfs diff=lfs merge=lfs -text

# Collapse Unity-generated files on GitHub
*.asset linguist-generated
*.mat linguist-generated
*.meta linguist-generated
*.prefab linguist-generated
*.unity linguist-generated

5. Using GitHub For Unity

If you are uncomfortable with using git on the commandline/terminal, there is an asset called GitHub For Unity that can be downloaded and installed using Unity's Asset store.

A cool feature of GitHub For Unity that can help with Git Workflow is that you no longer need to use the command line. There is a nice visual editor inside Unity that will allow you to commit, push, pull, etc. Furthermore, with GitHub For Unity, you can lock specific files that you are working on (like scene or prefab files) so other people can't change them when you are working. (Wow the power!)

To install GitHub For Unity:

  1. Make sure you are in Unity
  2. Go to Window -> Asset Store
  3. Search for GitHub for unity, install it, and import it into your project.

GitHub For Unity Guide: https://github.com/github-for-unity/Unity/blob/master/docs/using/quick-guide.md

🚀 You can bring up GitHub For Unity in the Unity editor by going to Window->GitHub in the Unity editor.

🤝 Git Workflow

The best way to merge scene files is to avoid merge conflicts in the first place. Merge conflicts in YAML files are pretty much impossible to read and thus very, very difficult to manually resolve those conflicts. There are generally two ways we can avoid merge conflicts in scene files:

  1. Break up large scenes into prefabs
  2. Use Additive Scene Loading

1. Breaking up large scenes into prefabs

We can break up scenes into many smaller areas and make these areas prefabs. Then as long as there is only one person working on an area prefab at a time, we can have multiple people working on the scene at once.

A video on this approach: https://www.youtube.com/watch?v=YgoCp2tzRh0

2. Using additive scene loading

This approach is similar to the prefabs approach but instead of breaking up the scenes into different prefabs, we break up the scene into mini scenes and use additive scene loading to put them together at runtime. Scene loading may cause a dip in framerate temporarily though.

In this gist, there contains two scripts that will help you accomplish additive scene loading. sceneLoader.cs relies on SceneReference.cs, so you will need to put both scripts in your project. If you put an empty game object in your main scene and add sceneLoader.cs to the game object, you will be able to drag in the scenes you want to load into a list in the inspector for the script.

Note: SceneReference.cs is taken from this gist: https://gist.github.com/JohannesMP/ec7d3f0bcf167dab3d0d3bb480e0e07b.

GitHub Locking

A cool feature of GitHub For Unity is that you can lock files right inside the Unity editor. If you choose to have a workflow where only one person can edit a scene at a time, you can lock the scene.

🚀 Here is a guide on locks in GitHub: https://github.com/github-for-unity/Unity/blob/master/docs/using/locking-files.md.

But if you didn't want to go to a separate webpage, here are the instructions:

  • To view which scenes are locked, make sure the GitHub window is opened in Unity, then press the Locks tab in the GitHub window.
  • To lock a scene, right click on the scene and click Request Lock
  • To release a lock, there are 3 ways:
    • Right click on the locked file and click Release Lock
    • From the GitHub tab under the Locks view, right-click to open the context menu and select to Release Lock
    • Select the file to unlock and go to select the menu option Assets -> Release Lock

🚀 Note: There are 2 options to release locks: Release Lock and Release Lock (Forced). Always choose Release Lock first, as Release Lock (Forced) can release someone else's lock.

Make sure you release your locks after you are done working on them so other people can work on them too!

✏️ Tips for Workflow

  1. Communication: As with all collaborative projects, communication is important, but with development in Unity, communication is especially important. Tools like UnityYAMLMerge and GitHub For Unity should make collaboration much easier, but it is still really important to communicate which scenes each person is working on.
  2. Only Stage Actual Changes: When you are doing git add, only add the files that you actually changed!
  3. Making New Branches: When starting new features, to make sure that you are as up-to-date with master as possible, make new branches from the master branch and not other branches
  4. Smaller PRs: General rule is that each PR should only have one new feature. Features in Unity often already have so many different changes and assets added, so to make it easier to review and have less potential for merge conflicts, avoid giant PRs.

⚔️ Resolving Merge Conflicts

Hopefully with tools like UnityYAMLMerge and GitHub For Unity, manual merge conflict resolution will no longer be needed. But if things come to this, you can always open the file in Visual Studio or another text editor and manually resolve merge conflicts that way. This is highly not recommended because this method is error prone, as YAML files are difficult to understand and not meant to be manipulated directly.

Sources and More Information:

using System.Collections;
using System.Collections.Generic;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.SceneManagement;
#endif
using UnityEngine;
using UnityEngine.SceneManagement;
[ExecuteInEditMode]
public class sceneLoader : MonoBehaviour
{
[SerializeField] private SceneReference[] scenes;
public string scenesFolder;
public bool wipeScenesList = false;
// Start is called before the first frame update
public void Start()
{
if (Application.isEditor)
{
#if UNITY_EDITOR
if (EditorApplication.isPlayingOrWillChangePlaymode)
{
for (int j = 0; j < scenes.Length; j++)
{
LoadSceneParameters parameters = new LoadSceneParameters();
parameters.loadSceneMode = LoadSceneMode.Additive;
SceneManager.UnloadSceneAsync(scenes[j]);
EditorSceneManager.LoadSceneInPlayMode(scenes[j].ScenePath, parameters);
}
}
else
{
for (int j = 0; j < scenes.Length; j++)
{
EditorSceneManager.OpenScene(scenes[j].ScenePath, OpenSceneMode.Additive);
}
}
#endif
}
else
{
for (int j = 0; j < scenes.Length; j++)
{
SceneManager.LoadScene(scenes[j], LoadSceneMode.Additive);
}
}
}
}
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
// Author: JohannesMP (2018-08-12)
//
// A wrapper that provides the means to safely serialize Scene Asset References.
//
// Internally we serialize an Object to the SceneAsset which only exists at editor time.
// Any time the object is serialized, we store the path provided by this Asset (assuming it was valid).
//
// This means that, come build time, the string path of the scene asset is always already stored, which if
// the scene was added to the build settings means it can be loaded.
//
// It is up to the user to ensure the scene exists in the build settings so it is loadable at runtime.
// To help with this, a custom PropertyDrawer displays the scene build settings state.
//
// Known issues:
// - When reverting back to a prefab which has the asset stored as null, Unity will show the property
// as modified despite having just reverted. This only happens the fist time, and reverting again
// fixes it. Under the hood the state is still always valid, and serialized correctly regardless.
/// <summary>
/// A wrapper that provides the means to safely serialize Scene Asset References.
/// </summary>
[System.Serializable]
public class SceneReference : ISerializationCallbackReceiver
{
#if UNITY_EDITOR
// What we use in editor to select the scene
[SerializeField] private Object sceneAsset = null;
bool IsValidSceneAsset
{
get
{
if (sceneAsset == null)
return false;
return sceneAsset.GetType().Equals(typeof(SceneAsset));
}
}
#endif
// This should only ever be set during serialization/deserialization!
[SerializeField]
private string scenePath = string.Empty;
// Use this when you want to actually have the scene path
public string ScenePath
{
get
{
#if UNITY_EDITOR
// In editor we always use the asset's path
return GetScenePathFromAsset();
#else
// At runtime we rely on the stored path value which we assume was serialized correctly at build time.
// See OnBeforeSerialize and OnAfterDeserialize
return scenePath;
#endif
}
set
{
scenePath = value;
#if UNITY_EDITOR
sceneAsset = GetSceneAssetFromPath();
#endif
}
}
public static implicit operator string(SceneReference sceneReference)
{
return sceneReference.ScenePath;
}
// Called to prepare this data for serialization. Stubbed out when not in editor.
public void OnBeforeSerialize()
{
#if UNITY_EDITOR
HandleBeforeSerialize();
#endif
}
// Called to set up data for deserialization. Stubbed out when not in editor.
public void OnAfterDeserialize()
{
#if UNITY_EDITOR
// We sadly cannot touch assetdatabase during serialization, so defer by a bit.
EditorApplication.update += HandleAfterDeserialize;
#endif
}
#if UNITY_EDITOR
private SceneAsset GetSceneAssetFromPath()
{
if (string.IsNullOrEmpty(scenePath))
return null;
return AssetDatabase.LoadAssetAtPath<SceneAsset>(scenePath);
}
private string GetScenePathFromAsset()
{
if (sceneAsset == null)
return string.Empty;
return AssetDatabase.GetAssetPath(sceneAsset);
}
private void HandleBeforeSerialize()
{
// Asset is invalid but have Path to try and recover from
if (IsValidSceneAsset == false && string.IsNullOrEmpty(scenePath) == false)
{
sceneAsset = GetSceneAssetFromPath();
if (sceneAsset == null)
scenePath = string.Empty;
UnityEditor.SceneManagement.EditorSceneManager.MarkAllScenesDirty();
}
// Asset takes precendence and overwrites Path
else
{
scenePath = GetScenePathFromAsset();
}
}
private void HandleAfterDeserialize()
{
EditorApplication.update -= HandleAfterDeserialize;
// Asset is valid, don't do anything - Path will always be set based on it when it matters
if (IsValidSceneAsset)
return;
// Asset is invalid but have path to try and recover from
if (string.IsNullOrEmpty(scenePath) == false)
{
sceneAsset = GetSceneAssetFromPath();
// No asset found, path was invalid. Make sure we don't carry over the old invalid path
if (sceneAsset == null)
scenePath = string.Empty;
if (Application.isPlaying == false)
UnityEditor.SceneManagement.EditorSceneManager.MarkAllScenesDirty();
}
}
#endif
}
#if UNITY_EDITOR
/// <summary>
/// Display a Scene Reference object in the editor.
/// If scene is valid, provides basic buttons to interact with the scene's role in Build Settings.
/// </summary>
[CustomPropertyDrawer(typeof(SceneReference))]
public class SceneReferencePropertyDrawer : PropertyDrawer
{
// The exact name of the asset Object variable in the SceneReference object
const string sceneAssetPropertyString = "sceneAsset";
// The exact name of the scene Path variable in the SceneReference object
const string scenePathPropertyString = "scenePath";
static readonly RectOffset boxPadding = EditorStyles.helpBox.padding;
static readonly float padSize = 2f;
static readonly float lineHeight = EditorGUIUtility.singleLineHeight;
static readonly float paddedLine = lineHeight + padSize;
static readonly float footerHeight = 10f;
/// <summary>
/// Drawing the 'SceneReference' property
/// </summary>
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
var sceneAssetProperty = GetSceneAssetProperty(property);
// Draw the Box Background
position.height -= footerHeight;
GUI.Box(EditorGUI.IndentedRect(position), GUIContent.none, EditorStyles.helpBox);
position = boxPadding.Remove(position);
position.height = lineHeight;
// Draw the main Object field
label.tooltip = "The actual Scene Asset reference.\nOn serialize this is also stored as the asset's path.";
EditorGUI.BeginProperty(position, GUIContent.none, property);
EditorGUI.BeginChangeCheck();
int sceneControlID = GUIUtility.GetControlID(FocusType.Passive);
var selectedObject = EditorGUI.ObjectField(position, label, sceneAssetProperty.objectReferenceValue, typeof(SceneAsset), false);
BuildUtils.BuildScene buildScene = BuildUtils.GetBuildScene(selectedObject);
if (EditorGUI.EndChangeCheck())
{
sceneAssetProperty.objectReferenceValue = selectedObject;
// If no valid scene asset was selected, reset the stored path accordingly
if (buildScene.scene == null)
GetScenePathProperty(property).stringValue = string.Empty;
}
position.y += paddedLine;
if (buildScene.assetGUID.Empty() == false)
{
// Draw the Build Settings Info of the selected Scene
DrawSceneInfoGUI(position, buildScene, sceneControlID + 1);
}
EditorGUI.EndProperty();
}
/// <summary>
/// Ensure that what we draw in OnGUI always has the room it needs
/// </summary>
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
int lines = 2;
SerializedProperty sceneAssetProperty = GetSceneAssetProperty(property);
if (sceneAssetProperty.objectReferenceValue == null)
lines = 1;
return boxPadding.vertical + lineHeight * lines + padSize * (lines - 1) + footerHeight;
}
/// <summary>
/// Draws info box of the provided scene
/// </summary>
private void DrawSceneInfoGUI(Rect position, BuildUtils.BuildScene buildScene, int sceneControlID)
{
bool readOnly = BuildUtils.IsReadOnly();
string readOnlyWarning = readOnly ? "\n\nWARNING: Build Settings is not checked out and so cannot be modified." : "";
// Label Prefix
GUIContent iconContent = new GUIContent();
GUIContent labelContent = new GUIContent();
// Missing from build scenes
if (buildScene.buildIndex == -1)
{
iconContent = EditorGUIUtility.IconContent("d_winbtn_mac_close");
labelContent.text = "NOT In Build";
labelContent.tooltip = "This scene is NOT in build settings.\nIt will be NOT included in builds.";
}
// In build scenes and enabled
else if (buildScene.scene.enabled)
{
iconContent = EditorGUIUtility.IconContent("d_winbtn_mac_max");
labelContent.text = "BuildIndex: " + buildScene.buildIndex;
labelContent.tooltip = "This scene is in build settings and ENABLED.\nIt will be included in builds." + readOnlyWarning;
}
// In build scenes and disabled
else
{
iconContent = EditorGUIUtility.IconContent("d_winbtn_mac_min");
labelContent.text = "BuildIndex: " + buildScene.buildIndex;
labelContent.tooltip = "This scene is in build settings and DISABLED.\nIt will be NOT included in builds.";
}
// Left status label
using (new EditorGUI.DisabledScope(readOnly))
{
Rect labelRect = DrawUtils.GetLabelRect(position);
Rect iconRect = labelRect;
iconRect.width = iconContent.image.width + padSize;
labelRect.width -= iconRect.width;
labelRect.x += iconRect.width;
EditorGUI.PrefixLabel(iconRect, sceneControlID, iconContent);
EditorGUI.PrefixLabel(labelRect, sceneControlID, labelContent);
}
// Right context buttons
Rect buttonRect = DrawUtils.GetFieldRect(position);
buttonRect.width = (buttonRect.width) / 3;
string tooltipMsg = "";
using (new EditorGUI.DisabledScope(readOnly))
{
// NOT in build settings
if (buildScene.buildIndex == -1)
{
buttonRect.width *= 2;
int addIndex = EditorBuildSettings.scenes.Length;
tooltipMsg = "Add this scene to build settings. It will be appended to the end of the build scenes as buildIndex: " + addIndex + "." + readOnlyWarning;
if (DrawUtils.ButtonHelper(buttonRect, "Add...", "Add (buildIndex " + addIndex + ")", EditorStyles.miniButtonLeft, tooltipMsg))
BuildUtils.AddBuildScene(buildScene);
buttonRect.width /= 2;
buttonRect.x += buttonRect.width;
}
// In build settings
else
{
bool isEnabled = buildScene.scene.enabled;
string stateString = isEnabled ? "Disable" : "Enable";
tooltipMsg = stateString + " this scene in build settings.\n" + (isEnabled ? "It will no longer be included in builds" : "It will be included in builds") + "." + readOnlyWarning;
if (DrawUtils.ButtonHelper(buttonRect, stateString, stateString + " In Build", EditorStyles.miniButtonLeft, tooltipMsg))
BuildUtils.SetBuildSceneState(buildScene, !isEnabled);
buttonRect.x += buttonRect.width;
tooltipMsg = "Completely remove this scene from build settings.\nYou will need to add it again for it to be included in builds!" + readOnlyWarning;
if (DrawUtils.ButtonHelper(buttonRect, "Remove...", "Remove from Build", EditorStyles.miniButtonMid, tooltipMsg))
BuildUtils.RemoveBuildScene(buildScene);
}
}
buttonRect.x += buttonRect.width;
tooltipMsg = "Open the 'Build Settings' Window for managing scenes." + readOnlyWarning;
if (DrawUtils.ButtonHelper(buttonRect, "Settings", "Build Settings", EditorStyles.miniButtonRight, tooltipMsg))
{
BuildUtils.OpenBuildSettings();
}
}
static SerializedProperty GetSceneAssetProperty(SerializedProperty property)
{
return property.FindPropertyRelative(sceneAssetPropertyString);
}
static SerializedProperty GetScenePathProperty(SerializedProperty property)
{
return property.FindPropertyRelative(scenePathPropertyString);
}
private static class DrawUtils
{
/// <summary>
/// Draw a GUI button, choosing between a short and a long button text based on if it fits
/// </summary>
static public bool ButtonHelper(Rect position, string msgShort, string msgLong, GUIStyle style, string tooltip = null)
{
GUIContent content = new GUIContent(msgLong);
content.tooltip = tooltip;
float longWidth = style.CalcSize(content).x;
if (longWidth > position.width)
content.text = msgShort;
return GUI.Button(position, content, style);
}
/// <summary>
/// Given a position rect, get its field portion
/// </summary>
static public Rect GetFieldRect(Rect position)
{
position.width -= EditorGUIUtility.labelWidth;
position.x += EditorGUIUtility.labelWidth;
return position;
}
/// <summary>
/// Given a position rect, get its label portion
/// </summary>
static public Rect GetLabelRect(Rect position)
{
position.width = EditorGUIUtility.labelWidth - padSize;
return position;
}
}
/// <summary>
/// Various BuildSettings interactions
/// </summary>
static private class BuildUtils
{
// time in seconds that we have to wait before we query again when IsReadOnly() is called.
public static float minCheckWait = 3;
static float lastTimeChecked = 0;
static bool cachedReadonlyVal = true;
/// <summary>
/// A small container for tracking scene data BuildSettings
/// </summary>
public struct BuildScene
{
public int buildIndex;
public GUID assetGUID;
public string assetPath;
public EditorBuildSettingsScene scene;
}
/// <summary>
/// Check if the build settings asset is readonly.
/// Caches value and only queries state a max of every 'minCheckWait' seconds.
/// </summary>
static public bool IsReadOnly()
{
float curTime = Time.realtimeSinceStartup;
float timeSinceLastCheck = curTime - lastTimeChecked;
if (timeSinceLastCheck > minCheckWait)
{
lastTimeChecked = curTime;
cachedReadonlyVal = QueryBuildSettingsStatus();
}
return cachedReadonlyVal;
}
/// <summary>
/// A blocking call to the Version Control system to see if the build settings asset is readonly.
/// Use BuildSettingsIsReadOnly for version that caches the value for better responsivenes.
/// </summary>
static private bool QueryBuildSettingsStatus()
{
// If no version control provider, assume not readonly
if (UnityEditor.VersionControl.Provider.enabled == false)
return false;
// If we cannot checkout, then assume we are not readonly
if (UnityEditor.VersionControl.Provider.hasCheckoutSupport == false)
return false;
//// If offline (and are using a version control provider that requires checkout) we cannot edit.
//if (UnityEditor.VersionControl.Provider.onlineState == UnityEditor.VersionControl.OnlineState.Offline)
// return true;
// Try to get status for file
var status = UnityEditor.VersionControl.Provider.Status("ProjectSettings/EditorBuildSettings.asset", false);
status.Wait();
// If no status listed we can edit
if (status.assetList == null || status.assetList.Count != 1)
return true;
// If is checked out, we can edit
if (status.assetList[0].IsState(UnityEditor.VersionControl.Asset.States.CheckedOutLocal))
return false;
return true;
}
/// <summary>
/// For a given Scene Asset object reference, extract its build settings data, including buildIndex.
/// </summary>
static public BuildScene GetBuildScene(Object sceneObject)
{
BuildScene entry = new BuildScene()
{
buildIndex = -1,
assetGUID = new GUID(string.Empty)
};
if (sceneObject as SceneAsset == null)
return entry;
entry.assetPath = AssetDatabase.GetAssetPath(sceneObject);
entry.assetGUID = new GUID(AssetDatabase.AssetPathToGUID(entry.assetPath));
for (int index = 0; index < EditorBuildSettings.scenes.Length; ++index)
{
if (entry.assetGUID.Equals(EditorBuildSettings.scenes[index].guid))
{
entry.scene = EditorBuildSettings.scenes[index];
entry.buildIndex = index;
return entry;
}
}
return entry;
}
/// <summary>
/// Enable/Disable a given scene in the buildSettings
/// </summary>
static public void SetBuildSceneState(BuildScene buildScene, bool enabled)
{
bool modified = false;
EditorBuildSettingsScene[] scenesToModify = EditorBuildSettings.scenes;
foreach (var curScene in scenesToModify)
{
if (curScene.guid.Equals(buildScene.assetGUID))
{
curScene.enabled = enabled;
modified = true;
break;
}
}
if (modified)
EditorBuildSettings.scenes = scenesToModify;
}
/// <summary>
/// Display Dialog to add a scene to build settings
/// </summary>
static public void AddBuildScene(BuildScene buildScene, bool force = false, bool enabled = true)
{
if (force == false)
{
int selection = EditorUtility.DisplayDialogComplex(
"Add Scene To Build",
"You are about to add scene at " + buildScene.assetPath + " To the Build Settings.",
"Add as Enabled", // option 0
"Add as Disabled", // option 1
"Cancel (do nothing)"); // option 2
switch (selection)
{
case 0: // enabled
enabled = true;
break;
case 1: // disabled
enabled = false;
break;
default:
case 2: // cancel
return;
}
}
EditorBuildSettingsScene newScene = new EditorBuildSettingsScene(buildScene.assetGUID, enabled);
List<EditorBuildSettingsScene> tempScenes = EditorBuildSettings.scenes.ToList();
tempScenes.Add(newScene);
EditorBuildSettings.scenes = tempScenes.ToArray();
}
/// <summary>
/// Display Dialog to remove a scene from build settings (or just disable it)
/// </summary>
static public void RemoveBuildScene(BuildScene buildScene, bool force = false)
{
bool onlyDisable = false;
if (force == false)
{
int selection = -1;
string title = "Remove Scene From Build";
string details = string.Format("You are about to remove the following scene from build settings:\n {0}\n buildIndex: {1}\n\n{2}",
buildScene.assetPath, buildScene.buildIndex,
"This will modify build settings, but the scene asset will remain untouched.");
string confirm = "Remove From Build";
string alt = "Just Disable";
string cancel = "Cancel (do nothing)";
if (buildScene.scene.enabled)
{
details += "\n\nIf you want, you can also just disable it instead.";
selection = EditorUtility.DisplayDialogComplex(title, details, confirm, alt, cancel);
}
else
{
selection = EditorUtility.DisplayDialog(title, details, confirm, cancel) ? 0 : 2;
}
switch (selection)
{
case 0: // remove
break;
case 1: // disable
onlyDisable = true;
break;
default:
case 2: // cancel
return;
}
}
// User chose to not remove, only disable the scene
if (onlyDisable)
{
SetBuildSceneState(buildScene, false);
}
// User chose to fully remove the scene from build settings
else
{
List<EditorBuildSettingsScene> tempScenes = EditorBuildSettings.scenes.ToList();
tempScenes.RemoveAll(scene => scene.guid.Equals(buildScene.assetGUID));
EditorBuildSettings.scenes = tempScenes.ToArray();
}
}
/// <summary>
/// Open the default Unity Build Settings window
/// </summary>
static public void OpenBuildSettings()
{
EditorWindow.GetWindow(typeof(BuildPlayerWindow));
}
}
}
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment