Skip to content

Instantly share code, notes, and snippets.

@LotteMakesStuff
Last active April 28, 2023 09:49
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save LotteMakesStuff/1d01621393e2c36f96ba13128c726588 to your computer and use it in GitHub Desktop.
Save LotteMakesStuff/1d01621393e2c36f96ba13128c726588 to your computer and use it in GitHub Desktop.
Since Unity 2018.3 we've had this cool new tool called [SettingsProvider] that we can use to add custom settings menu into Unitys settings screens. This short example shows how i use [SettingsProvider] and some [MenuItems] to help build and run test clients when im developing a multiplayer game. My usecase is that i want a quick way to build and…
using System.Collections.Generic;
using System.Diagnostics;
using UnityEditor;
using UnityEditor.Build.Reporting;
using UnityEngine;
using Debug = UnityEngine.Debug;
public static class NetworkingTools
{
private static string multiplayerBuildSettingsPath = "Project/MultiplayerBuildSetting";
private static string multiplayerBuildPathKey = "MultiplayerBuildLoc";
private static string multplayerBuildSecenesKey = "MultiplayerBuildScenes";
private static List<SceneAsset> sceneAssets;
// Add a Multiplayer menu to the Unity menu bar with shortcuts to open our custom build setting and run builds...
// NOTE we have such a high priority value to make sure its at the bottom of the Multiplayer menu! the Unity.Transport
// package also uses this menu, we want our stuff at the bottom to make it easier to find~
[MenuItem("Multiplayer/Open Multiplayer Build Settings", false, 2999)]
public static void OpenBuildSettings()
{
SettingsService.OpenProjectSettings(multiplayerBuildSettingsPath);
}
[MenuItem("Multiplayer/Build Test Client", false, 3000)]
public static void BuildTestClient()
{
// Ask unity to do a custom build for us.. first we need to pull the settings out of editor preferences...
string buildPath = EditorPrefs.GetString(multiplayerBuildPathKey);
string scenes = EditorPrefs.GetString(multplayerBuildSecenesKey);
if (string.IsNullOrWhiteSpace(buildPath) || string.IsNullOrWhiteSpace(scenes))
{
// if either of the settings strings are not filled in, ask unity to open the settings panel for us
SettingsService.OpenProjectSettings(multiplayerBuildSettingsPath);
return;
}
// plug in our build settings
BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions();
buildPlayerOptions.scenes = scenes.Split(';');
buildPlayerOptions.locationPathName = buildPath + "/MultiplayerTest.exe";
buildPlayerOptions.target = BuildTarget.StandaloneWindows64;
buildPlayerOptions.options = BuildOptions.None;
// actually run the build and store off the results!
BuildReport report = BuildPipeline.BuildPlayer(buildPlayerOptions);
BuildSummary summary = report.summary;
if (summary.result == BuildResult.Succeeded)
{
Debug.Log("Build succeeded: " + summary.totalTime);
// ask unity to open a file exporer window with the client exe selected
EditorUtility.RevealInFinder(buildPath + "/MultiplayerTest.exe");
}
else if (summary.result == BuildResult.Failed)
{
Debug.Log("Build failed");
}
}
[MenuItem("Multiplayer/Run Latest Client", false, 3001)]
public static void RunTestClient()
{
string buildPath = EditorPrefs.GetString(multiplayerBuildPathKey);
Process process = new Process
{
StartInfo = {FileName = buildPath + "/MultiplayerTest.exe"}
};
process.Start();
}
[MenuItem("Multiplayer/Run Latest Client", true, 3001)]
public static bool CanRunTestClient()
{
// validation function for the "Run Latest Client" menu entry. Lets check if MultiplayerTest.exe exists
// if it dosnt that means we dont have a build to run, so we return false so the run menu item is grayed out
string buildPath = EditorPrefs.GetString(multiplayerBuildPathKey);
return System.IO.File.Exists(buildPath + "/MultiplayerTest.exe");
}
[SettingsProvider]
public static SettingsProvider CreateMultiplayerBuildLocationSettingsProvider()
{
var provider = new SettingsProvider(multiplayerBuildSettingsPath, SettingsScope.Project)
{
label = "Multiplayer Build",
deactivateHandler = () =>
{
// get the list of scenes in the build...
sceneAssets = null;
},
guiHandler = (searchContext) =>
{
// Lets actually draw our settings window!
// First off were going to draw a textbox for the build path.
using (var check = new EditorGUI.ChangeCheckScope())
{
string loc = EditorPrefs.GetString(multiplayerBuildPathKey);
loc = EditorGUILayout.TextField("Build folder", loc);
// add in a button for opening a folder picker...
if (GUILayout.Button("Pick folder..."))
{
loc = EditorUtility.OpenFolderPanel("Multiplayer Build Location", "", "");
}
if (check.changed)
{
// if the path string was saved, lets store it off into editor prefrences, so we can easily
// query for it in the build process.
EditorPrefs.SetString(multiplayerBuildPathKey, loc);
}
}
GUILayout.Space(5);
// Next, lets draw a list of scenes we want to be included in the buld
// first, check if the static scene asset list is null... if its null that means this is the first time
// were drawing the settings UI, and we need to load up any previously saved list of scenes
if (sceneAssets == null)
{
sceneAssets = new List<SceneAsset>();
// the list is stored in editor prefs -
// each scene path in the string is separated with a ; so lets split with that
var scenes = EditorPrefs.GetString(multplayerBuildSecenesKey);
var scenePaths = scenes.Split(';');
foreach (var scenePath in scenePaths)
{
// load each scene asset up from the asset database and add it to the list
sceneAssets.Add(AssetDatabase.LoadAssetAtPath<SceneAsset>(scenePath));
}
}
using (var check = new EditorGUI.ChangeCheckScope())
{
EditorGUILayout.LabelField("Scenes included in build", EditorStyles.boldLabel);
int deleteItem = -1;
for (int i = 0; i < sceneAssets.Count; ++i)
{
EditorGUILayout.BeginHorizontal();
sceneAssets[i] =
(SceneAsset) EditorGUILayout.ObjectField(sceneAssets[i], typeof(SceneAsset), false);
if (GUILayout.Button("X", GUILayout.Width(30)))
{
// draw a button for removing the scene from the list. if its clicked, lets store of its
// index so we can remove it after we finish itterating over the scene asset list
deleteItem = i;
}
EditorGUILayout.EndHorizontal();
}
if (deleteItem != -1)
{
// we can now remove the scene safely
sceneAssets.RemoveAt(deleteItem);
}
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Clone main build list", GUILayout.Width(140)))
{
sceneAssets = new List<SceneAsset>();
foreach (var scene in EditorBuildSettings.scenes)
{
sceneAssets.Add(AssetDatabase.LoadAssetAtPath<SceneAsset>(scene.path));
}
}
if (GUILayout.Button("Add"))
{
sceneAssets.Add(null);
}
EditorGUILayout.EndHorizontal();
// if the list has changed at all, save it out
if (check.changed)
{
Debug.Log("Changed");
string scenes = "";
bool first = true;
foreach (var asset in sceneAssets)
{
if (!first)
scenes += ";";
scenes += AssetDatabase.GetAssetPath(asset);
first = false;
}
EditorPrefs.SetString(multplayerBuildSecenesKey, scenes);
}
EditorGUILayout.Space(5);
if (GUILayout.Button("Build..."))
{
BuildTestClient();
}
}
}
};
return provider;
}
}

Always thankful for your support, Lotte💖

Buy Me a Coffee at ko-fi.com Become a Patron!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment