├── BuildSettingsExtensions.cs
├── BuildSettingsExtensions.cs.meta
├── EditorGuiUtilities.cs
├── EditorGuiUtilities.cs.meta
├── GameBuilder.asmdef
├── GameBuilder.asmdef.meta
├── GameBuilderCompression.cs
├── GameBuilderCompression.cs.meta
├── GameBuilderModel.cs
├── GameBuilderModel.cs.meta
├── GameBuilderOsOperations.cs
├── GameBuilderOsOperations.cs.meta
├── GameBuilderWindow.cs
├── GameBuilderWindow.cs.meta
├── README.md
├── README.md.meta
├── StringUtils.cs
└── StringUtils.cs.meta
/BuildSettingsExtensions.cs:
--------------------------------------------------------------------------------
1 | using UnityEditor;
2 | using UnityEngine;
3 |
4 | namespace GameBuilderEditor
5 | {
6 | public static class BuildSettingsExtensions
7 | {
8 | public static BuildTarget GetBuildTarget(this GameBuilderModel.BuildSettings settings) => settings.buildingPlatform switch
9 | {
10 | BuildingPlatform.Windows => BuildTarget.StandaloneWindows64,
11 | BuildingPlatform.WindowsServer => BuildTarget.StandaloneWindows64,
12 | BuildingPlatform.Linux => BuildTarget.StandaloneLinux64,
13 | BuildingPlatform.LinuxServer => BuildTarget.StandaloneLinux64,
14 | BuildingPlatform.Android => BuildTarget.Android,
15 | BuildingPlatform.WebGL => BuildTarget.WebGL,
16 | _ => (BuildTarget)(-1)
17 | };
18 |
19 | public static int GetSubTarget(this GameBuilderModel.BuildSettings settings) => settings.buildingPlatform switch
20 | {
21 | BuildingPlatform.Windows => (int)StandaloneBuildSubtarget.Player,
22 | BuildingPlatform.WindowsServer => (int)StandaloneBuildSubtarget.Server,
23 | BuildingPlatform.Linux => (int)StandaloneBuildSubtarget.Player,
24 | BuildingPlatform.LinuxServer => (int)StandaloneBuildSubtarget.Server,
25 | BuildingPlatform.Android => 0,
26 | BuildingPlatform.WebGL => 0,
27 | _ => -1
28 | };
29 |
30 | public static BuildTargetGroup GetTargetGroup(this GameBuilderModel.BuildSettings settings) => settings.buildingPlatform switch
31 | {
32 | BuildingPlatform.Windows => BuildTargetGroup.Standalone,
33 | BuildingPlatform.WindowsServer => BuildTargetGroup.Standalone,
34 | BuildingPlatform.Linux => BuildTargetGroup.Standalone,
35 | BuildingPlatform.LinuxServer => BuildTargetGroup.Standalone,
36 | BuildingPlatform.Android => BuildTargetGroup.Android,
37 | BuildingPlatform.WebGL => BuildTargetGroup.WebGL,
38 | _ => (BuildTargetGroup)(-1)
39 | };
40 |
41 | public static string GetBuildPath(this GameBuilderModel.BuildSettings settings)
42 | {
43 | try
44 | {
45 | return settings.buildingPlatform switch
46 | {
47 | BuildingPlatform.Windows =>
48 | string.Format(settings.buildPath, Application.version, ".exe"),
49 | BuildingPlatform.WindowsServer =>
50 | string.Format(settings.buildPath, Application.version, ".exe"),
51 | BuildingPlatform.Linux =>
52 | string.Format(settings.buildPath, Application.version, string.Empty),
53 | BuildingPlatform.LinuxServer =>
54 | string.Format(settings.buildPath, Application.version, string.Empty),
55 | BuildingPlatform.Android =>
56 | string.Format(settings.buildPath, Application.version, ".apk"),
57 | BuildingPlatform.WebGL =>
58 | string.Format(settings.buildPath, Application.version, string.Empty),
59 | _ => string.Empty
60 | };
61 | }
62 | catch
63 | {
64 | return "invalid path";
65 | }
66 | }
67 |
68 | public static string GetCompressedFilePath(this GameBuilderModel.BuildSettings settings)
69 | {
70 | try
71 | {
72 | return settings.buildingPlatform switch
73 | {
74 | BuildingPlatform.Windows =>
75 | string.Format(settings.compressFilePath, Application.version, ".exe", ".zip"),
76 | BuildingPlatform.WindowsServer =>
77 | string.Format(settings.compressFilePath, Application.version, ".exe", ".zip"),
78 | BuildingPlatform.Linux =>
79 | string.Format(settings.compressFilePath, Application.version, string.Empty, ".zip"),
80 | BuildingPlatform.LinuxServer =>
81 | string.Format(settings.compressFilePath, Application.version, string.Empty, ".zip"),
82 | BuildingPlatform.Android =>
83 | string.Format(settings.compressFilePath, Application.version, ".apk", ".zip"),
84 | BuildingPlatform.WebGL =>
85 | string.Format(settings.compressFilePath, Application.version, string.Empty, ".zip"),
86 | _ => string.Empty
87 | };
88 | }
89 | catch
90 | {
91 | return "invalid path";
92 | }
93 | }
94 | }
95 | }
--------------------------------------------------------------------------------
/BuildSettingsExtensions.cs.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: 5a38073c2b478c74d93b5566fc374f0e
3 | MonoImporter:
4 | externalObjects: {}
5 | serializedVersion: 2
6 | defaultReferences: []
7 | executionOrder: 0
8 | icon: {instanceID: 0}
9 | userData:
10 | assetBundleName:
11 | assetBundleVariant:
12 |
--------------------------------------------------------------------------------
/EditorGuiUtilities.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using UnityEditor;
3 |
4 | namespace GameBuilderEditor
5 | {
6 | public static class EditorGuiUtilities
7 | {
8 | public readonly struct LabelWidth : IDisposable
9 | {
10 | private readonly float w;
11 | public LabelWidth(float width)
12 | {
13 | w = EditorGUIUtility.labelWidth;
14 | EditorGUIUtility.labelWidth = width;
15 | }
16 |
17 | public readonly void Dispose()
18 | {
19 | EditorGUIUtility.labelWidth = w;
20 | }
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/EditorGuiUtilities.cs.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: a92c576485b87824aaa0c6a910ae7578
3 | MonoImporter:
4 | externalObjects: {}
5 | serializedVersion: 2
6 | defaultReferences: []
7 | executionOrder: 0
8 | icon: {instanceID: 0}
9 | userData:
10 | assetBundleName:
11 | assetBundleVariant:
12 |
--------------------------------------------------------------------------------
/GameBuilder.asmdef:
--------------------------------------------------------------------------------
1 | {
2 | "name": "GameBuilder",
3 | "rootNamespace": "",
4 | "references": [],
5 | "includePlatforms": [
6 | "Editor"
7 | ],
8 | "excludePlatforms": [],
9 | "allowUnsafeCode": false,
10 | "overrideReferences": false,
11 | "precompiledReferences": [],
12 | "autoReferenced": true,
13 | "defineConstraints": [],
14 | "versionDefines": [],
15 | "noEngineReferences": false
16 | }
--------------------------------------------------------------------------------
/GameBuilder.asmdef.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: 0c39a6ac05bf5ea49a094659945b4ff1
3 | AssemblyDefinitionImporter:
4 | externalObjects: {}
5 | userData:
6 | assetBundleName:
7 | assetBundleVariant:
8 |
--------------------------------------------------------------------------------
/GameBuilderCompression.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.IO.Compression;
3 |
4 | namespace GameBuilderEditor
5 | {
6 | ///
7 | /// Helper class for compressing files into an archive.
8 | ///
9 | public static class GameBuilderCompression
10 | {
11 | ///
12 | /// compresses files (not folders) into the specified archive path
13 | ///
14 | ///
15 | public static void ZipFiles(string[] files, string outputPath, CompressionLevel compressionLevel)
16 | {
17 | if (files == null || files.Length == 0)
18 | {
19 | return;
20 | }
21 |
22 | // rename any previous file with the same name
23 | if (File.Exists(outputPath))
24 | {
25 | File.Delete(outputPath);
26 | }
27 |
28 | using var archive = ZipFile.Open(outputPath, ZipArchiveMode.Create);
29 | var outDir = Path.GetDirectoryName(outputPath);
30 | foreach (var file in files)
31 | {
32 | if (File.Exists(file))
33 | {
34 | archive.CreateEntryFromFile(file, Path.GetRelativePath(outDir, file), compressionLevel);
35 | }
36 | }
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/GameBuilderCompression.cs.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: ada9380e8900adf4bb2e44c764629f74
3 | MonoImporter:
4 | externalObjects: {}
5 | serializedVersion: 2
6 | defaultReferences: []
7 | executionOrder: 0
8 | icon: {instanceID: 0}
9 | userData:
10 | assetBundleName:
11 | assetBundleVariant:
12 |
--------------------------------------------------------------------------------
/GameBuilderModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 | using UnityEditor;
4 | using UnityEngine;
5 |
6 | namespace GameBuilderEditor
7 | {
8 | public sealed class GameBuilderModel : ScriptableObject
9 | {
10 | public BuildSettings[] buildSettings = Array.Empty();
11 |
12 | [Serializable]
13 | public class BuildSettings
14 | {
15 | public BuildingPlatform buildingPlatform;
16 | public SceneAsset[] scenes;
17 | public string[] scriptingDefines;
18 |
19 | [Tooltip(
20 | "{0}: version\n" +
21 | "{1}: platform-specific file extension")]
22 | public string buildPath = "Builds/{0}/Game{1}";
23 |
24 | public string label = "New Settings";
25 |
26 | public BuildOptions buildOptions;
27 |
28 | [Tooltip("opens build folder in terminal if succeeded")]
29 | public bool openInTerminal;
30 |
31 | [Tooltip("Instances to run if succeeded")]
32 | public int instancesToRun;
33 |
34 | [Tooltip("Whether or not to compress game files")]
35 | public bool compressFiles;
36 |
37 | [Tooltip(
38 | "{0}: version\n" +
39 | "{1}: platform-specific file extension\n" +
40 | "{2}: compression method file extension")]
41 | public string compressFilePath;
42 |
43 | [Tooltip("compression level for compressing the files.")]
44 | public System.IO.Compression.CompressionLevel compressionLevel;
45 |
46 | public string Info
47 | {
48 | get
49 | {
50 | StringBuilder sb = new();
51 | if (buildOptions.ContainsFast(BuildOptions.None))
52 | sb.AppendLine("None:\n Perform the specified build without any special settings or extra tasks.");
53 | if (buildOptions.ContainsFast(BuildOptions.Development))
54 | sb.AppendLine("Development:\n Build a development version of the player.");
55 | if (buildOptions.ContainsFast(BuildOptions.AutoRunPlayer))
56 | sb.AppendLine("AutoRunPlayer:\n Run the built player.");
57 | if (buildOptions.ContainsFast(BuildOptions.ShowBuiltPlayer))
58 | sb.AppendLine("ShowBuiltPlayer:\n Show the built player.");
59 | if (buildOptions.ContainsFast(BuildOptions.BuildAdditionalStreamedScenes))
60 | sb.AppendLine("BuildAdditionalStreamedScenes:\n Build a compressed asset bundle that contains streamed Scenes loadable with the UnityWebRequest class.");
61 | if (buildOptions.ContainsFast(BuildOptions.AcceptExternalModificationsToPlayer))
62 | sb.AppendLine("AcceptExternalModificationsToPlayer:\n Used when building Xcode (iOS) or Eclipse (Android) projects.");
63 | if (buildOptions.ContainsFast(BuildOptions.InstallInBuildFolder))
64 | sb.AppendLine("InstallInBuildFolder:\n ");
65 | if (buildOptions.ContainsFast(BuildOptions.CleanBuildCache))
66 | sb.AppendLine("CleanBuildCache:\n Clear all cached build results, resulting in a full rebuild of all scripts and all player data.");
67 | if (buildOptions.ContainsFast(BuildOptions.ConnectWithProfiler))
68 | sb.AppendLine("ConnectWithProfiler:\n Start the player with a connection to the profiler in the editor.");
69 | if (buildOptions.ContainsFast(BuildOptions.AllowDebugging))
70 | sb.AppendLine("AllowDebugging:\n Allow script debuggers to attach to the player remotely.");
71 | if (buildOptions.ContainsFast(BuildOptions.SymlinkSources))
72 | sb.AppendLine("SymlinkSources:\n Symlink sources when generating the project. This is useful if you're changing source files inside the generated project and want to bring the changes back into your Unity project or a package.");
73 | if (buildOptions.ContainsFast(BuildOptions.UncompressedAssetBundle))
74 | sb.AppendLine("UncompressedAssetBundle:\n Don't compress the data when creating the asset bundle.");
75 | if (buildOptions.ContainsFast(BuildOptions.ConnectToHost))
76 | sb.AppendLine("ConnectToHost:\n Sets the Player to connect to the Editor.");
77 | if (buildOptions.ContainsFast(BuildOptions.CustomConnectionID))
78 | sb.AppendLine("CustomConnectionID:\n Determines if the player should be using the custom connection ID.");
79 | if (buildOptions.ContainsFast(BuildOptions.BuildScriptsOnly))
80 | sb.AppendLine("BuildScriptsOnly:\n Only build the scripts in a Project.");
81 | if (buildOptions.ContainsFast(BuildOptions.PatchPackage))
82 | sb.AppendLine("PatchPackage:\n Patch a Development app package rather than completely rebuilding it. Supported platforms: Android.");
83 | if (buildOptions.ContainsFast(BuildOptions.ForceEnableAssertions))
84 | sb.AppendLine("ForceEnableAssertions:\n Include assertions in the build. By default, the assertions are only included in development builds.");
85 | if (buildOptions.ContainsFast(BuildOptions.CompressWithLz4))
86 | sb.AppendLine("CompressWithLz4:\n Use chunk-based LZ4 compression when building the Player.");
87 | if (buildOptions.ContainsFast(BuildOptions.CompressWithLz4HC))
88 | sb.AppendLine("CompressWithLz4HC:\n Use chunk-based LZ4 high-compression when building the Player.");
89 | if (buildOptions.ContainsFast(BuildOptions.ComputeCRC))
90 | sb.AppendLine("ComputeCRC:\n ");
91 | if (buildOptions.ContainsFast(BuildOptions.StrictMode))
92 | sb.AppendLine("StrictMode:\n Do not allow the build to succeed if any errors are reporting during it.");
93 | if (buildOptions.ContainsFast(BuildOptions.IncludeTestAssemblies))
94 | sb.AppendLine("IncludeTestAssemblies:\n Build will include Assemblies for testing.");
95 | if (buildOptions.ContainsFast(BuildOptions.NoUniqueIdentifier))
96 | sb.AppendLine("NoUniqueIdentifier:\n Will force the buildGUID to all zeros.");
97 | if (buildOptions.ContainsFast(BuildOptions.WaitForPlayerConnection))
98 | sb.AppendLine("WaitForPlayerConnection:\n Sets the Player to wait for player connection on player start.");
99 | if (buildOptions.ContainsFast(BuildOptions.EnableCodeCoverage))
100 | sb.AppendLine("EnableCodeCoverage:\n Enables code coverage. You can use this as a complimentary way of enabling code coverage on platforms that do not support command line arguments.");
101 | if (buildOptions.ContainsFast(BuildOptions.EnableDeepProfilingSupport))
102 | sb.AppendLine("EnableDeepProfilingSupport:\n Enables Deep Profiling support in the player.");
103 | if (buildOptions.ContainsFast(BuildOptions.DetailedBuildReport))
104 | sb.AppendLine("DetailedBuildReport:\n Generates more information in the BuildReport.");
105 | if (buildOptions.ContainsFast(BuildOptions.ShaderLivelinkSupport))
106 | sb.AppendLine("ShaderLivelinkSupport:\n Enable Shader Livelink support.");
107 |
108 | return sb.ToString();
109 | }
110 | }
111 | }
112 | }
113 |
114 | public enum BuildingPlatform
115 | {
116 | Windows,
117 | WindowsServer,
118 | Linux,
119 | LinuxServer,
120 | Android,
121 | WebGL
122 | }
123 |
124 | public static class BuildOptionsExtension
125 | {
126 | public static bool ContainsFast(this BuildOptions self, BuildOptions other) => (self & other) == other;
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/GameBuilderModel.cs.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: abca8dbe2c7cda94cbe9111117a3284d
3 | MonoImporter:
4 | externalObjects: {}
5 | serializedVersion: 2
6 | defaultReferences: []
7 | executionOrder: 0
8 | icon: {instanceID: 0}
9 | userData:
10 | assetBundleName:
11 | assetBundleVariant:
12 |
--------------------------------------------------------------------------------
/GameBuilderOsOperations.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 |
4 | namespace GameBuilderEditor
5 | {
6 | public static class GameBuilderOsOperations
7 | {
8 | public static void OpenTerminalAtDirectory(string path)
9 | {
10 | new Process
11 | {
12 | StartInfo = new()
13 | {
14 | CreateNoWindow = false,
15 | #if UNITY_EDITOR_WIN
16 | FileName = "cmd",
17 | Arguments = $"/k cd \"{path}\"",
18 | #else
19 | #warning not supported
20 | #endif
21 |
22 | WindowStyle = ProcessWindowStyle.Normal,
23 | }
24 | }.Start();
25 | }
26 |
27 | public static void OpenFile(string path)
28 | {
29 | new Process
30 | {
31 | StartInfo = new()
32 | {
33 | FileName = path
34 | }
35 | }.Start();
36 | }
37 |
38 | public static string ExecuteBatch(string command)
39 | {
40 | var proc = new Process();
41 | proc.StartInfo.FileName =
42 | #if UNITY_EDITOR_WIN
43 | "cmd.exe";
44 | #elif UNITY_EDITOR_LINUX || UNITY_EDITOR_OSX
45 | "/bin/bash";
46 | #else
47 | #warning not supported
48 | null;
49 | #endif
50 | proc.StartInfo.UseShellExecute = false;
51 | proc.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
52 | proc.StartInfo.RedirectStandardOutput = true;
53 | proc.StartInfo.RedirectStandardError = true;
54 | proc.StartInfo.RedirectStandardInput = true;
55 | proc.StartInfo.CreateNoWindow = true;
56 | proc.Start();
57 | using (var writer = proc.StandardInput)
58 | {
59 | if (writer.BaseStream.CanWrite)
60 | {
61 | var lines = command.Split(Environment.NewLine);
62 | foreach (var line in lines)
63 | {
64 | writer.WriteLine(line);
65 | }
66 | }
67 | }
68 | return proc.StandardOutput.ReadToEnd();
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/GameBuilderOsOperations.cs.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: 3dfa90312ead9644581496fa29eff213
3 | MonoImporter:
4 | externalObjects: {}
5 | serializedVersion: 2
6 | defaultReferences: []
7 | executionOrder: 0
8 | icon: {instanceID: 0}
9 | userData:
10 | assetBundleName:
11 | assetBundleVariant:
12 |
--------------------------------------------------------------------------------
/GameBuilderWindow.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 | using UnityEditor;
8 | using UnityEditor.Build.Reporting;
9 | using UnityEngine;
10 | using Debug = UnityEngine.Debug;
11 |
12 | namespace GameBuilderEditor
13 | {
14 | public class GameBuilderWindow : EditorWindow
15 | {
16 | [MenuItem("Assets/Game Builder")]
17 | private static void OpenWindow()
18 | {
19 | var window = GetWindow(c_useUtilityWindow, "Game Builder");
20 | window.minSize = new(600, 400);
21 | window.Show();
22 | }
23 |
24 | public CancellationTokenSource cts;
25 | public GameBuilderModel model;
26 | public SerializedObject serializedObject;
27 | public List business = new(1);
28 |
29 | public int SelectedBuildSettingsIndex
30 | {
31 | get => EditorPrefs.GetInt("gamebuilder.selectedBuildSettingsIndex", 0);
32 | set => EditorPrefs.SetInt("gamebuilder.selectedBuildSettingsIndex", value);
33 | }
34 |
35 | private const bool c_useUtilityWindow = false;
36 | private const string c_modelDir_0 = "Assets";
37 | private const string c_modelDir_1 = "Editor";
38 | private const string c_modelFilename = "GameBuilderModel.asset";
39 | private const string c_preLog = "[game builder] ";
40 | private const int c_createModel_retryDelay = 1000;
41 |
42 | private Vector2 _infoScrollPos;
43 | private Vector2 _mainScrollPos;
44 | private Vector2 _buildSettingsPresets_scrollPos;
45 |
46 | private void OnGUI()
47 | {
48 | var scriptsCompiling = EditorApplication.isCompiling;
49 | if (scriptsCompiling)
50 | {
51 | GUILayout.FlexibleSpace();
52 | using (new GUILayout.HorizontalScope())
53 | {
54 | GUILayout.FlexibleSpace();
55 | GUILayout.Label("cant use this window while scripts are compiling.");
56 | GUILayout.FlexibleSpace();
57 | }
58 | GUILayout.FlexibleSpace();
59 | return;
60 | }
61 |
62 | cts ??= new();
63 | using (var scroll = new GUILayout.ScrollViewScope(_mainScrollPos))
64 | {
65 | _mainScrollPos = scroll.scrollPosition;
66 | if (business.Count > 0)
67 | {
68 | GUILayout.Label("Busy...", EditorStyles.largeLabel);
69 | foreach (var b in business)
70 | {
71 | try
72 | {
73 | b.onGui();
74 | }
75 | catch { }
76 | EditorGUILayout.Separator();
77 | }
78 | return;
79 | }
80 |
81 | if (model == null)
82 | {
83 | CreateModel_Business();
84 | }
85 |
86 | if (model != null)
87 | {
88 | if (serializedObject == null)
89 | {
90 | serializedObject = new(model);
91 | if (serializedObject == null)
92 | {
93 | Debug.LogWarningFormat("{0}could not create {1} from {2}", c_preLog, nameof(SerializedObject), nameof(model));
94 | return;
95 | }
96 | }
97 |
98 | serializedObject.Update();
99 | DrawWindow();
100 | serializedObject.ApplyModifiedProperties();
101 | }
102 | }
103 | }
104 |
105 | private void DrawWindow()
106 | {
107 | var buildSettingsProp = serializedObject.FindProperty(nameof(GameBuilderModel.buildSettings));
108 | if (buildSettingsProp == null)
109 | {
110 | return;
111 | }
112 |
113 | using (new GUILayout.HorizontalScope(GUILayout.ExpandHeight(true)))
114 | {
115 | DrawBuildSettingsSelection(buildSettingsProp);
116 |
117 | SelectedBuildSettingsIndex = Mathf.Clamp(SelectedBuildSettingsIndex, 0, model.buildSettings.Length - 1);
118 |
119 | // show selected preset
120 | if (SelectedBuildSettingsIndex >= 0 && SelectedBuildSettingsIndex < buildSettingsProp.arraySize)
121 | {
122 | using (new GUILayout.VerticalScope())
123 | {
124 | DrawBuildSettingsConfiguration(SelectedBuildSettingsIndex);
125 | }
126 | }
127 | }
128 |
129 | DrawBuildLayout();
130 | }
131 |
132 | private void DrawBuildSettingsSelection(SerializedProperty buildSettingsProp)
133 | {
134 | using (new GUILayout.VerticalScope(EditorStyles.helpBox, GUILayout.Width(220)))
135 | {
136 | // draw build settings
137 | GUILayout.Label("Build Settings", EditorStyles.boldLabel);
138 | using (var scroll = new GUILayout.ScrollViewScope(_buildSettingsPresets_scrollPos))
139 | {
140 | _buildSettingsPresets_scrollPos = scroll.scrollPosition;
141 | for (int i = 0; i < buildSettingsProp.arraySize; i++)
142 | {
143 | var preset = buildSettingsProp.GetArrayElementAtIndex(i);
144 |
145 | var height = Mathf.Max(30,
146 | EditorStyles.wordWrappedLabel.CalcHeight(new(model.buildSettings[i].label), 70));
147 |
148 | var mainRect = EditorGUILayout.GetControlRect(GUILayout.Width(205), GUILayout.Height(height));
149 |
150 | if (SelectedBuildSettingsIndex == i)
151 | {
152 | EditorGUI.DrawRect(mainRect, new(0, 0, 1, 0.5f));
153 | }
154 |
155 | mainRect.x += 6;
156 | mainRect.width -= 12;
157 | var rect = mainRect;
158 |
159 | rect.width -= 20 + 20 + 40;
160 | EditorGUI.LabelField(rect,
161 | preset.FindPropertyRelative(nameof(GameBuilderModel.BuildSettings.label)).stringValue,
162 | EditorStyles.wordWrappedLabel);
163 |
164 | rect.height = 20;
165 | rect.y = mainRect.y + (mainRect.height - rect.height) / 2f;
166 |
167 | rect.x += rect.width;
168 | rect.width = 20;
169 | if (GUI.Button(rect, "↑"))
170 | {
171 | buildSettingsProp.MoveArrayElement(i, i - 1);
172 | serializedObject.ApplyModifiedProperties();
173 | break;
174 | }
175 | rect.x += rect.width;
176 | if (GUI.Button(rect, "↓"))
177 | {
178 | buildSettingsProp.MoveArrayElement(i, i + 1);
179 | serializedObject.ApplyModifiedProperties();
180 | break;
181 | }
182 | rect.x += rect.width;
183 | rect.width = 40;
184 | if (GUI.Button(rect, "Del"))
185 | {
186 | buildSettingsProp.DeleteArrayElementAtIndex(i);
187 | serializedObject.ApplyModifiedProperties();
188 | break;
189 | }
190 |
191 | // click
192 | if (Event.current.type == EventType.MouseDown && Event.current.button == 0 &&
193 | mainRect.Contains(Event.current.mousePosition))
194 | {
195 | SelectedBuildSettingsIndex = i;
196 | Repaint();
197 | }
198 | }
199 | }
200 |
201 | if (GUILayout.Button("New"))
202 | {
203 | buildSettingsProp.arraySize++;
204 | serializedObject.ApplyModifiedProperties();
205 | }
206 | }
207 | }
208 |
209 | private void DrawBuildSettingsConfiguration(int index)
210 | {
211 | var buildSettings = model.buildSettings[index];
212 | var buildSettingsProp =
213 | serializedObject.FindProperty(nameof(GameBuilderModel.buildSettings))
214 | .GetArrayElementAtIndex(index);
215 | var labelProp = buildSettingsProp.FindPropertyRelative(nameof(GameBuilderModel.BuildSettings.label));
216 | var buildingPlatformProp = buildSettingsProp.FindPropertyRelative(nameof(GameBuilderModel.BuildSettings.buildingPlatform));
217 | var scenesProp = buildSettingsProp.FindPropertyRelative(nameof(GameBuilderModel.BuildSettings.scenes));
218 | var scriptingDefinesProp = buildSettingsProp.FindPropertyRelative(nameof(GameBuilderModel.BuildSettings.scriptingDefines));
219 | var buildOptionsProp = buildSettingsProp.FindPropertyRelative(nameof(GameBuilderModel.BuildSettings.buildOptions));
220 | var openInTerminalProp = buildSettingsProp.FindPropertyRelative(nameof(GameBuilderModel.BuildSettings.openInTerminal));
221 | var instancesToRunProp = buildSettingsProp.FindPropertyRelative(nameof(GameBuilderModel.BuildSettings.instancesToRun));
222 | var compressFilesProp = buildSettingsProp.FindPropertyRelative(nameof(GameBuilderModel.BuildSettings.compressFiles));
223 | var compressFilePathProp = buildSettingsProp.FindPropertyRelative(nameof(GameBuilderModel.BuildSettings.compressFilePath));
224 | var compressionLevelProp = buildSettingsProp.FindPropertyRelative(nameof(GameBuilderModel.BuildSettings.compressionLevel));
225 | var buildPathProp = buildSettingsProp.FindPropertyRelative(nameof(GameBuilderModel.BuildSettings.buildPath));
226 |
227 | EditorGUILayout.PropertyField(labelProp);
228 | EditorGUILayout.PropertyField(buildingPlatformProp);
229 | using (new GUILayout.HorizontalScope(EditorStyles.helpBox))
230 | {
231 | EditorGUILayout.PropertyField(scenesProp);
232 | EditorGUILayout.PropertyField(scriptingDefinesProp);
233 | }
234 | EditorGUILayout.PropertyField(openInTerminalProp);
235 | EditorGUILayout.PropertyField(instancesToRunProp);
236 | EditorGUILayout.PropertyField(buildPathProp);
237 | EditorGUILayout.PropertyField(compressFilesProp);
238 | if (compressFilesProp.boolValue)
239 | {
240 | EditorGUILayout.PropertyField(compressFilePathProp);
241 | EditorGUILayout.PropertyField(compressionLevelProp);
242 | string compressedPath = buildSettings.GetCompressedFilePath();
243 | EditorGUILayout.LabelField($"compressed path: {compressedPath}");
244 | }
245 | EditorGUILayout.PropertyField(buildOptionsProp);
246 | using var scroll = new GUILayout.ScrollViewScope(_infoScrollPos);
247 | using (new EditorGUI.DisabledGroupScope(true))
248 | {
249 | _infoScrollPos = scroll.scrollPosition;
250 | GUILayout.Label(buildSettings.Info, EditorStyles.wordWrappedLabel);
251 | }
252 | }
253 |
254 | private void DrawBuildLayout()
255 | {
256 | SelectedBuildSettingsIndex = Mathf.Clamp(SelectedBuildSettingsIndex, 0, model.buildSettings.Length - 1);
257 | if (model.buildSettings.Length > 0)
258 | {
259 | var buildSettings = model.buildSettings[SelectedBuildSettingsIndex];
260 |
261 | using (new GUILayout.HorizontalScope(GUILayout.ExpandWidth(false)))
262 | {
263 | string path = buildSettings.GetBuildPath();
264 | GUILayout.Label(path);
265 |
266 | if (GUILayout.Button("Copy Full", GUILayout.Width(80)))
267 | {
268 | GUIUtility.systemCopyBuffer = Path.GetFullPath(path);
269 | }
270 |
271 | using (new EditorGuiUtilities.LabelWidth(60))
272 | PlayerSettings.bundleVersion = EditorGUILayout.TextField("version", PlayerSettings.bundleVersion);
273 | if (GUILayout.Button("↑", GUILayout.Width(15)))
274 | {
275 | PlayerSettings.bundleVersion = StringUtils.IncrementIntegerInString(PlayerSettings.bundleVersion, 1);
276 | }
277 | if (GUILayout.Button("↓", GUILayout.Width(15)))
278 | {
279 | PlayerSettings.bundleVersion = StringUtils.IncrementIntegerInString(PlayerSettings.bundleVersion, -1);
280 | }
281 | }
282 |
283 | if (buildSettings.buildingPlatform == BuildingPlatform.Android &&
284 | !buildSettings.buildOptions.ContainsFast(BuildOptions.Development))
285 | {
286 | using (new GUILayout.HorizontalScope())
287 | {
288 | PlayerSettings.Android.keystorePass = EditorGUILayout.PasswordField("Keystore Password", PlayerSettings.Android.keystorePass);
289 | PlayerSettings.Android.keyaliasPass = EditorGUILayout.PasswordField("Key Alias Password", PlayerSettings.Android.keyaliasPass);
290 | }
291 | }
292 |
293 | using (new GUILayout.HorizontalScope())
294 | {
295 | if (GUILayout.Button("Perform Build", GUILayout.Height(30), GUILayout.ExpandWidth(true)))
296 | {
297 | PerformBuild_Business().ConfigureAwait(false);
298 | }
299 | if (GUILayout.Button("Clean", GUILayout.Height(30), GUILayout.Width(50)))
300 | {
301 | CleanBuildDirectory();
302 | }
303 | }
304 | }
305 | }
306 |
307 | private void CleanBuildDirectory()
308 | {
309 | var buildSettings = model.buildSettings[SelectedBuildSettingsIndex];
310 | var buildPath = buildSettings.GetBuildPath();
311 | if (Directory.Exists(buildPath))
312 | {
313 | Directory.Delete(buildPath, true);
314 | }
315 | else if (File.Exists(buildPath))
316 | {
317 | File.Delete(buildPath);
318 | }
319 | }
320 |
321 | public async Task PerformBuild_Business()
322 | {
323 | if (model == null || model.buildSettings == null)
324 | {
325 | Debug.LogWarningFormat("{0}invalid model", c_preLog);
326 | return;
327 | }
328 |
329 | var buildSettings = model.buildSettings[SelectedBuildSettingsIndex];
330 |
331 | var buildTarget = buildSettings.GetBuildTarget();
332 | var subTarget = buildSettings.GetSubTarget();
333 | var targetGroup = buildSettings.GetTargetGroup();
334 | var buildPath = buildSettings.GetBuildPath();
335 | if (subTarget == -1 || (int)targetGroup == -1 || string.IsNullOrEmpty(buildPath))
336 | {
337 | Debug.LogWarningFormat("{0}invalid model", c_preLog);
338 | return;
339 | }
340 |
341 | var performBuildBusiness = new Business()
342 | {
343 | onGui = () => { GUILayout.Label("Building in progresss"); }
344 | };
345 | business.Add(performBuildBusiness);
346 | var r = await PerformBuild(
347 | buildOptions: buildSettings.buildOptions,
348 | scenes: buildSettings.scenes.Select(s => AssetDatabase.GetAssetPath(s)).ToArray(),
349 | target: buildTarget,
350 | subTarget: subTarget,
351 | targetGroup: targetGroup,
352 | extraScriptingDefines: buildSettings.scriptingDefines,
353 | buildPath: buildPath
354 | );
355 |
356 | if (r != null)
357 | {
358 | Debug.LogFormat("{0}build finished. \'{1}\' duration:\'{2} seconds\'", c_preLog, r.summary.result,
359 | r.summary.totalTime.TotalSeconds);
360 | }
361 |
362 | // post build
363 | if (r.summary.result == BuildResult.Succeeded)
364 | {
365 | var fileInfo = new FileInfo(r.summary.outputPath);
366 | var compressedFilePath = buildSettings.GetCompressedFilePath();
367 |
368 | // open in terminal
369 | if (buildSettings.openInTerminal)
370 | {
371 | GameBuilderOsOperations.OpenTerminalAtDirectory(fileInfo.Directory.FullName);
372 | }
373 |
374 | // run instances
375 | for (int i = 0; i < buildSettings.instancesToRun; i++)
376 | {
377 | GameBuilderOsOperations.OpenFile(fileInfo.FullName);
378 | }
379 |
380 | // compress
381 | if (buildSettings.compressFiles)
382 | {
383 | try
384 | {
385 | var files = Directory.GetFiles(fileInfo.Directory.FullName, "*.*", SearchOption.AllDirectories)
386 | .Where(f => !f.Contains("DoNotShip"))
387 | .ToArray();
388 | GameBuilderCompression.ZipFiles(files, compressedFilePath, buildSettings.compressionLevel);
389 | Debug.LogFormat("successfully compressed into {0}", compressedFilePath);
390 | }
391 | catch (Exception ex)
392 | {
393 | Debug.LogError("error while compressing. see the next log for exception");
394 | Debug.LogException(ex);
395 | }
396 | }
397 | }
398 |
399 | business.Remove(performBuildBusiness);
400 | }
401 |
402 | private async Task PerformBuild(BuildOptions buildOptions, string[] scenes,
403 | BuildTarget target, int subTarget, BuildTargetGroup targetGroup, string[] extraScriptingDefines, string buildPath)
404 | {
405 | BuildPlayerOptions options = new()
406 | {
407 | options = buildOptions,
408 | scenes = scenes,
409 | target = target,
410 | subtarget = subTarget,
411 | targetGroup = targetGroup,
412 | extraScriptingDefines = extraScriptingDefines,
413 | locationPathName = buildPath,
414 | };
415 | if (!BuildPipeline.IsBuildTargetSupported(targetGroup, target))
416 | {
417 | Debug.LogWarning("Build target is not installed");
418 | return null;
419 | }
420 | var result = BuildPipeline.BuildPlayer(options);
421 | await Task.Yield();
422 | return result;
423 | }
424 |
425 | private async void CreateModel_Business()
426 | {
427 | var content = new GUIContent("Creating new model",
428 | $"creating an instance of {nameof(GameBuilderModel)} at {Path.Combine(c_modelDir_0, c_modelDir_1, c_modelFilename)}");
429 | Business createModelBusiness = new()
430 | {
431 | onGui = () =>
432 | {
433 | using (new GUILayout.HorizontalScope())
434 | {
435 | GUILayout.FlexibleSpace();
436 | GUILayout.Label(content);
437 | GUILayout.FlexibleSpace();
438 | }
439 | }
440 | };
441 | business.Add(createModelBusiness);
442 | model = await CreateModel(cts.Token);
443 | business.Remove(createModelBusiness);
444 | }
445 |
446 | private async Task CreateModel(CancellationToken ct)
447 | {
448 | if (ct.IsCancellationRequested)
449 | {
450 | return null;
451 | }
452 |
453 | if (!AssetDatabase.IsValidFolder(Path.Combine(c_modelDir_0, c_modelDir_1)))
454 | {
455 | AssetDatabase.CreateFolder(c_modelDir_0, c_modelDir_1);
456 | AssetDatabase.Refresh();
457 | }
458 |
459 | string assetPath = Path.Combine(c_modelDir_0, c_modelDir_1, c_modelFilename);
460 | var asset = AssetDatabase.LoadAssetAtPath(assetPath);
461 | if (asset != null)
462 | {
463 | return asset;
464 | }
465 |
466 | Debug.LogFormat("{0}creating new model at {1}", c_preLog, assetPath);
467 | AssetDatabase.CreateAsset(CreateInstance(), assetPath);
468 | AssetDatabase.Refresh();
469 |
470 | asset = AssetDatabase.LoadAssetAtPath(assetPath);
471 | if (asset != null)
472 | {
473 | return asset;
474 | }
475 |
476 | // error
477 | Debug.LogWarningFormat("{0}error while fetching mode. retrying in {1} milliseconds", c_preLog,
478 | c_createModel_retryDelay);
479 | AssetDatabase.Refresh();
480 | await Task.Delay(c_createModel_retryDelay, ct);
481 |
482 | if (ct.IsCancellationRequested)
483 | {
484 | return null;
485 | }
486 |
487 | return await CreateModel(ct);
488 | }
489 |
490 | public class Business
491 | {
492 | public Action onGui;
493 | }
494 | }
495 | }
--------------------------------------------------------------------------------
/GameBuilderWindow.cs.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: d292ae0f54f1669498328fcd25685c2d
3 | MonoImporter:
4 | externalObjects: {}
5 | serializedVersion: 2
6 | defaultReferences: []
7 | executionOrder: 0
8 | icon: {instanceID: 0}
9 | userData:
10 | assetBundleName:
11 | assetBundleVariant:
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | ### Advantages
4 | - Separate Scene settings for each platform
5 | - Build pressets (includes dynamic build path, automating post-build commands, opening multiple instances etc.)
6 | - Simple & Open. You can edit the code and add new configs to pressets etc. to meet your needs.
7 | - Automation. If you're using git in your project, you can take advantage of build automation. it pulls the branch from remote and performs a new build.
8 | - Detailed build report right in the editor. You can view full build report in the logs, so you won't have to install 3rd parties or open logs file manually to view the build reports.
9 |
10 | ### Disadvantages
11 | - Only tested on Windows
12 | - Can only build for a limited number of platforms
13 | - Not many error reporting around. (This project is not for beginners. If you use it, you're expected to know how git and building works)
14 | - Bugs might lerk around unnoticed. It's a personal project, so it's not been fully tested on different machines and with different scenarios. If you feel adventurous and want to try hacks, expect bugs.
15 |
--------------------------------------------------------------------------------
/README.md.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: 66272db05a78fee45b74c3998b9392c2
3 | TextScriptImporter:
4 | externalObjects: {}
5 | userData:
6 | assetBundleName:
7 | assetBundleVariant:
8 |
--------------------------------------------------------------------------------
/StringUtils.cs:
--------------------------------------------------------------------------------
1 | using System.Text.RegularExpressions;
2 |
3 | namespace GameBuilderEditor
4 | {
5 | internal sealed class StringUtils
6 | {
7 | private static readonly string s_pattern = @"\d+";
8 |
9 | public static string IncrementIntegerInString(string str, int increment)
10 | {
11 | var matches = Regex.Matches(str, s_pattern);
12 | if (matches.Count > 0)
13 | {
14 | var lastMatch = matches[^1].Value;
15 | var integer = int.Parse(lastMatch);
16 | integer += increment;
17 | int index = str.LastIndexOf(lastMatch);
18 | str = str.Remove(index, lastMatch.Length).Insert(index, integer.ToString());
19 | }
20 | return str;
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/StringUtils.cs.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: 887c8dfd1a8863e4e811b0994c7bed1a
3 | MonoImporter:
4 | externalObjects: {}
5 | serializedVersion: 2
6 | defaultReferences: []
7 | executionOrder: 0
8 | icon: {instanceID: 0}
9 | userData:
10 | assetBundleName:
11 | assetBundleVariant:
12 |
--------------------------------------------------------------------------------