Всем привет! Меня зовут Григорий Дядиченко, я занимаюсь продюсированием digital проектов. Сегодня хотелось бы поговорить про возможности расширения редактора Unity, и как вы можете упростить себе работу на примере включения-выключения nginx из Unity. Мы пройдёмся по теме сборки AssetBundles и работы с процессами в C#.
Я довольно много работаю с виртуальной и дополненной реальностью. Да и в целом с тестированием чего-то на устройствах, где не всё можно сделать в редакторе Unity. И поэтому я задался вопросом: «Как бы упростить себе процесс работы?». Основная машина у меня на Windows, и для сборки билдов на IOS приходится перегонять через shared folder по Wifi сборку на макбук. Можно было бы настроить CI&CD, но с точки зрения итераций это медленнее такого пути. Но помимо существует такая задача, как тестирование контента, где в пересборке билда мало художественного смысла. И можно просто заливать на устройство контент в локальной сети через механизм Asset Bundles. Чтож, ну, а тут как не потратить пол дня на такую задачку?
Но чтобы бандлы доставлять нужен веб-сервер. Сначала я попробовал сделать это с помощью сервера из этой статьи, но он слишком топорный, и там будет тяжело поддержать gzip. Поэтому в голову сразу пришёл nginx. Но так как хочется сделать и себе, и людям, то надо бы упростить запуск nginx для Unity разработчиков, и ещё чтобы он выключался с выключением редактора и в целом из редактора им управлять (ну в рамках нашей задачи). Чтож, решение придумано — пора делать.
Интеграция NGINX в Unity проект
В целом навык запускать из редактора всякие консольные утилиты или exe файлы — штука довольно полезная. Таким образом можно быстро без сложной разработки расширять возможности редактора, но помимо редактора, если проект под десктопную платформу навык работать с классом Process, позволяет вам так же обращаться к нужным утилитам уже в билде. Мы же разберём случай с редактором. Чтож, напишем класс для запуска нашего nginx.
Для начала напишем просто запуск процесса и разберём, что он делает:
Код запуска процесса
private void ExecuteCommand (string pathToExe, string args) { Process process = new Process(); ProcessStartInfo startInfo = new ProcessStartInfo(); startInfo.WindowStyle = ProcessWindowStyle.Hidden; startInfo.FileName = pathToExe; startInfo.Arguments = args; startInfo.UseShellExecute = false; var path = Path.GetDirectoryName(NginxPath) ?? string.Empty; if (!string.IsNullOrEmpty(path)) { startInfo.WorkingDirectory = path; } startInfo.CreateNoWindow = true; process.StartInfo = startInfo; process.Start(); Debug.Log($"Success {pathToExe} {args}"); }
В данном блоке кода мы:
1. Создаём новый процесс
2. Задаём параметры запуска. Из интересного:
startInfo.WindowStyle = ProcessWindowStyle.Hidden; — чтобы у нас не появлялась консоль.
startInfo.WorkingDirectory = path; — некоторые процессы зависят от рабочей директории.
startInfo.CreateNoWindow = true; — нужно ли запускать процесс в новом окне.
3. Запускаем процесс
Запуск процесса написан, теперь нам нужен путь до исполняемого файла. Можно конечно его захардкодить, но в Unity есть инструмент в разы удобнее. Дело в том, что UnityEngine.Object задать в качестве поля в инспекторе, и при этом все файлы и папки проекта являются наследником UnityEngine.Object. Дальше через AssetDatabase.GetAssetPath можно получить полный путь до объекта. И использовать его для наших целей. Вот полный код нашего объекта настройки nginx:
Код NGINXSettings
using System; using System.Diagnostics; using System.IO; using UnityEditor; using UnityEngine; using Debug = UnityEngine.Debug; using Object = UnityEngine.Object; [CreateAssetMenu(fileName = "NGINXSettings", menuName = "NGINX/Settings")] public class NGINXSettings : ScriptableObject { public const int ServerPort = 10020; public const string LogPath = "logs"; public const string PidFileName = "nginx.pid"; public Object Nginx; public string NginxPath => Path.GetFullPath(AssetDatabase.GetAssetPath(Nginx)); public void StartNginx() { var dir = Path.GetDirectoryName(NginxPath) ?? string.Empty; if (!File.Exists(Path.Combine(dir, LogPath, PidFileName))) { ExecuteCommand(NginxPath , ""); } else { Debug.Log("Nginx already started!"); } } public void StopNginx() { ExecuteCommand(NginxPath, "-s quit"); } private void ExecuteCommand (string pathToExe, string args) { Process process = new Process(); ProcessStartInfo startInfo = new ProcessStartInfo(); startInfo.WindowStyle = ProcessWindowStyle.Hidden; startInfo.FileName = pathToExe; startInfo.Arguments = args; startInfo.UseShellExecute = false; var path = Path.GetDirectoryName(NginxPath) ?? string.Empty; if (!string.IsNullOrEmpty(path)) { startInfo.WorkingDirectory = path; } startInfo.CreateNoWindow = true; process.EnableRaisingEvents = true; process.StartInfo = startInfo; process.Start(); Debug.Log($"Success {pathToExe} {args}"); } }
В данном случае для удобства работы в редакторе мы сделаем Scriptable Object с кастомным инспектором. Более подробно SO и кастомные инспекторы я разбирал тут. Правда тогда я делал чуть иначе, поэтому тут стоит сказать, что делает аттрибут CreateAssetMenu. Он позволяет вам создавать SO по правой кнопке мыши в редакторе.
А вот кастомные инспекторы там разобраны неплохо, поэтому следущий код примем за данность, чтобы у нас появились кнопки. Тем более он элементарный:
Код NGINXSettingsCustomEditor
using UnityEditor; using UnityEngine; [CustomEditor(typeof(NGINXSettings))] public class NGINXSettingsCustomEditor : Editor { public override void OnInspectorGUI() { var nginxSettings = target as NGINXSettings; base.OnInspectorGUI(); if (GUILayout.Button("Start Nginx")) { nginxSettings.StartNginx(); } if (GUILayout.Button("Stop Nginx")) { nginxSettings.StopNginx(); } } }
Всё, теперь мы можем создать объект, положить в проект nginx и назначить nginx.exe в качестве поля в инспекторе.
Если вы собираетесь это паковать в билд, я бы делал это через StreamingAssets и расширение функциональности определения пути, но работать это всё равно будет только на десктопных платформах.
Стоит сказать, что сейчас у вас nginx работать не будет под Windows за пределами вашей машины, так как Unity редактор заблокирован в Windows Firewall на входящие соединения. Как это настроить правильно можно прочитать тут.
Едем дальше, теперь нам хочется удобно собирать бандлы, чтобы они сразу «заливались на сервер». Пусть и локальный.
Сборка AssetBundles для доступа по сети
Собирать ассет бандлы довольно просто. Для этого в Юнити есть метод:
BuildPipeline.BuildAssetBundles(string, BuildAssetBundleOptions, BuildTarget);
Сделаем это чуть удобнее, задав некоторый набор методов. Он будет позволять нам создавать определённые бандлы или все, назначать платформы которые мы собираем и т.п.
Код AssetBundlesBuilder
using System.Collections.Generic; using System.IO; using System.Linq; using UnityEditor; using UnityEngine; public class AssetBundlesBuilder { public static void BuildAllAssetBundles(AssetBundlesBuildSettings settings) { BuildCustomAssetBundles( settings.AssetBundleDirectory, null, settings.Platforms); } public static void BuildSpecifiedAssetBundles(AssetBundlesBuildSettings settings) { BuildCustomAssetBundles( settings.AssetBundleDirectory, settings.AssetBundleNamesToBuild, settings.Platforms); } private static void BuildCustomAssetBundles( string path, string[] assetBundleNames, BuildTarget[] platforms) { if(platforms == null) { Debug.LogError("Set at least one platform!"); return; }; if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } var builds = new List<AssetBundleBuild>(); if (assetBundleNames != null && assetBundleNames.Length != 0) { assetBundleNames = assetBundleNames.Distinct().ToArray(); foreach (var assetBundle in assetBundleNames) { var assetPaths = AssetDatabase.GetAssetPathsFromAssetBundle(assetBundle); var build = new AssetBundleBuild { assetBundleName = assetBundle, assetNames = assetPaths }; builds.Add(build); Debug.Log($"[Asset Bundles] Build bundle: {build.assetBundleName}"); } } for (int i = 0; i < platforms.Length; i++) { var platform = platforms[i]; BuildAssetBundlesForTarget(path, platform, GetPlatformDirectory(platform),builds.ToArray()); } } private static void BuildAssetBundlesForTarget(string path, BuildTarget target, string targetPath, AssetBundleBuild[] bundles = null) { var directory = Path.Combine(path, targetPath); if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } if (bundles == null || bundles.Length == 0) { BuildPipeline.BuildAssetBundles(directory, BuildAssetBundleOptions.None, target); } else { BuildPipeline.BuildAssetBundles(directory, bundles.ToArray(), BuildAssetBundleOptions.None, target); } } public static string GetPlatformDirectory(BuildTarget target) { switch (@target) { default: return "standalone"; case BuildTarget.Android: return "android"; case BuildTarget.iOS: return "ios"; case BuildTarget.StandaloneWindows: return "standalone"; case BuildTarget.StandaloneWindows64: return "standalone64"; } } }
Все эти методы нам пригодятся для нашего второго объекта настроек:
Код AssetBundlesBuildSettings
using UnityEditor; using UnityEngine; [CreateAssetMenu(fileName = "AssetBundleBuildSettings", menuName = "Asset Bundles/AssetBundleBuildSettings")] public class AssetBundlesBuildSettings : ScriptableObject { public Object AssetBundleDirectoryObject; public string AssetBundleDirectory => AssetDatabase.GetAssetPath(AssetBundleDirectoryObject); public BuildTarget[] Platforms; public string[] AssetBundleNamesToBuild; }
Код AssetBundlesBuildSettingsCustomEditor
using UnityEditor; using UnityEngine; [CustomEditor(typeof(AssetBundlesBuildSettings))] public class AssetBundlesBuildSettingsCustomEditor : Editor { public override void OnInspectorGUI() { base.OnInspectorGUI(); GUILayout.Space(40); if (GUILayout.Button("Build All")) { var settings = target as AssetBundlesBuildSettings; AssetBundlesBuilder.BuildAllAssetBundles(settings); } GUILayout.Space(40); if (GUILayout.Button("Build From Names")) { var settings = target as AssetBundlesBuildSettings; AssetBundlesBuilder.BuildSpecifiedAssetBundles(settings); } } }
В этом примере мы собственно будем передавать не объект, а папку в качестве UnityEngine.Object, что так же удобно, чтобы не хардкодить пути. Очень советую использовать такой механизм в проектах, так как это в разы удобнее, гибче и не требует времени на рекомпиляцию проекта.
Теперь осталось назначить папку для сборки bundles в качестве html папки nginx (как на картинке выше), и бандлы можно спокойно качать по локальной сети.
Спасибо за внимание! Полный код репозитория можно найти тут. Там есть и код клиентского приложения. Результат решения бандлы можно быстро протестировать на разных платформах.
ссылка на оригинал статьи https://habr.com/ru/post/661363/
Добавить комментарий