Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
A dockable build switcher and defines manager. Edit `Defines` and `AllBuilds` in the code to fit your project.
/// OdinBuildSwitcher
/// 5argon - Exceed7 Experiments
/// ,
using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;
using System.Collections.Generic;
using System.Linq;
using Sirenix.OdinInspector;
using Sirenix.Utilities;
using Sirenix.OdinInspector.Editor;
using Sirenix.Utilities.Editor;
public class OdinBuildSwitcher : PreSetupOdinEditorWindow<OdinBuildSwitcher>
#region Your configurations
/// <summary>
/// All of your game's custom defines here. Please type the enum as if they are preprocessor strings.
/// </summary>
public enum Defines
LOG_NETWORK = 1 << 1,
CHEAT_BUILD = 1 << 2,
STRIP_CODE = 1 << 3,
TESTING = 1 << 4
/// <summary>
/// Your desired platform presets here. They must be in pairs of BuildTargetGroup and BuildTarget used for Apply buttons, and optinally build path for that preset + any numbers of BuildOptions used for Build buttons.
/// </summary>
private static Dictionary<string, BuildConfiguration> AllBuilds = new Dictionary<string, BuildConfiguration>
["Android"] = new BuildConfiguration(BuildTargetGroup.Android, BuildTarget.Android, "./AndroidBuild/AndroidBuild.apk", BuildOptions.AutoRunPlayer, BuildOptions.StrictMode, BuildOptions.Development),
["iOS"] = new BuildConfiguration(BuildTargetGroup.iOS, BuildTarget.iOS),
["Standalone"] = new BuildConfiguration(BuildTargetGroup.Standalone, BuildTarget.StandaloneWindows)
#region Odin
[EnumToggleButtons, LabelText("Select Defines")]
public Defines selectedDefines;
[ValueDropdown("AllBuildKeys"), LabelText("Select Build")]
public string selectedBuildKey;
[Button(ButtonSizes.Medium), GUIColor(1.0f, 0.4f, 0.4f)]
public void Apply() => Apply(selectedBuildKey, selectedDefines);
[Button(ButtonSizes.Medium), GUIColor(1.0f, 0.9f, 0.5f), PropertyTooltip("Will switch to selected preset and defines, build, and switch everything back. You still have to wait for a project recompilation afterwards but at least you don't have to babysit the build just to change everything back when it finishes.")]
public void BuildWithSelectedSettings() => ApplyBuildRevert(selectedBuildKey, selectedDefines);
[ShowInInspector, ListDrawerSettings(Expanded = true), PropertyOrder(10)]
public static string[] CurrentDefines => PlayerSettings.GetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup).Trim().Split(';');
[ShowInInspector, HorizontalGroup("BuildTarget", Width = 0.66f), LabelText("Current Build Target"), PropertyOrder(11)]
public static BuildTargetGroup CurrentBuildTargetGroup => EditorUserBuildSettings.selectedBuildTargetGroup;
[ShowInInspector, HorizontalGroup("BuildTarget", Width = 0.33f), HideLabel, PropertyOrder(12)]
public static BuildTarget CurrentBuildTarget => EditorUserBuildSettings.activeBuildTarget;
protected override string Title => "Builds";
protected override EditorIcon Icon => EditorIcons.GridBlocks;
[MenuItem("Window/Your Game/Build Switcher")]
private static void Open() => OpenWindow();
private struct BuildConfiguration
public BuildTargetGroup buildTargetGroup { get; }
public BuildTarget buildTarget { get; }
public string buildPath { get; }
public BuildOptions[] buildOptions { get; }
/// <param name="buildPath">Default is your previous build location you manually build before.</param>
public BuildConfiguration(BuildTargetGroup buildTargetGroup, BuildTarget buildTarget, string buildPath = default(string), params BuildOptions[] buildOptions)
this.buildTargetGroup = buildTargetGroup;
this.buildTarget = buildTarget;
this.buildPath = buildPath;
this.buildOptions = buildOptions;
private static IEnumerable<string> AllPossibleDefines => System.Enum.GetNames(typeof(Defines));
private static IEnumerable<string> AllBuildKeys => AllBuilds.Keys;
private static BuildTargetGroup TargetGroup(string buildKey) => AllBuilds[buildKey].buildTargetGroup;
private static BuildTarget Target(string buildKey) => AllBuilds[buildKey].buildTarget;
private static string BuildPath(string buildKey) => AllBuilds[buildKey].buildPath;
private static IEnumerable<BuildOptions> GetBuildOptions(string buildKey) => AllBuilds[buildKey].buildOptions;
/// <param name="withDefines">Use bitwise OR to merge multiple defines</param>
public static void Apply(string buildKey, Defines withDefines)
Debug.Log("Changing defines...");
List<string> currentDefines = CurrentDefines.ToList();
List<string> newDefines = new List<string>();
foreach (string oneOfCurrentDefines in currentDefines)
if (!AllPossibleDefines.Contains(oneOfCurrentDefines))
//Don't touch other defines we don't know about
//Add new defines
foreach (Defines d in System.Enum.GetValues(typeof(Defines)))
if ((d & withDefines) != 0)
PlayerSettings.SetScriptingDefineSymbolsForGroup(TargetGroup(buildKey), string.Join(";", newDefines));
Debug.Log("Switching build target...");
bool switchingResult = EditorUserBuildSettings.SwitchActiveBuildTargetAsync(TargetGroup(buildKey), Target(buildKey));
Debug.Log(switchingResult ? $"Switched to {Target(buildKey)}." : $"Switching to {Target(buildKey)} failed!");
/// <param name="withDefines">Use bitwise OR to merge multiple defines</param>
public static void ApplyBuildRevert(string buildKey, Defines buildWithDefines)
string[] rememberDefines = CurrentDefines;
BuildTarget rememberBuildTarget = CurrentBuildTarget;
BuildTargetGroup rememberBuildTargetGroup = CurrentBuildTargetGroup;
Apply(buildKey, buildWithDefines);
EditorUserBuildSettings.SwitchActiveBuildTargetAsync(rememberBuildTargetGroup, rememberBuildTarget);
PlayerSettings.SetScriptingDefineSymbolsForGroup(rememberBuildTargetGroup, string.Join(";", rememberDefines));
/// <summary>
/// You can call this from somewhere else as you like.
/// </summary>
public static void BuildWithCurrentDefines(string buildKey)
BuildPlayerOptions bpo = new BuildPlayerOptions();
bpo.scenes = EditorBuildSettings.scenes.Select(scene => scene.path).ToArray();
bpo.targetGroup = TargetGroup(buildKey); = Target(buildKey);
if(BuildPath(buildKey) == default(string))
bpo.locationPathName = EditorUserBuildSettings.GetBuildLocation(Target(buildKey));
bpo.locationPathName = BuildPath(buildKey);
bpo.options = BuildOptions.None;
foreach (BuildOptions bo in GetBuildOptions(buildKey))
bpo.options |= bo;
var report = BuildPipeline.BuildPlayer(bpo);
Debug.Log($"[{report.summary.buildStartedAt}~{report.summary.buildEndedAt}] Build result : {report.summary.result}, Output : {report.summary.outputPath}");
/// <summary>
/// Gives a good template for adding title and icon that persists.
/// After subclassing, your class can just call base.OpenWindow() on the code with [MenuItem]
/// </summary>
public abstract class PreSetupOdinEditorWindow<T> : OdinEditorWindow where T : PreSetupOdinEditorWindow<T>
protected abstract EditorIcon Icon { get; }
protected abstract string Title { get; }
private static T window;
protected static void OpenWindow()
window = GetWindow<T>();
window.position = GUIHelper.GetEditorWindowRect().AlignCenter(700, 700);
protected override void OnEnable()
window = GetWindow<T>("", false);
private static void RefreshTitle()
window.titleContent = new GUIContent(window.Title, window.Icon?.Active);
private static void RegisterRefreshTitle()
UnityEditor.SceneManagement.EditorSceneManager.activeSceneChangedInEditMode -= RefreshTitle;
UnityEditor.SceneManagement.EditorSceneManager.activeSceneChangedInEditMode += RefreshTitle;
private static void RefreshTitle(Scene from, Scene to) => RefreshTitle();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.