Что за флаг?

Кто устанавливал офицальные ассеты от Unity "Starter Assets - Third Person Character Controller" или "Starter Assets - First Person Character Controller" возможно замечал что в настройках проета (Project settings -> Player -> Other settings -> Script Compilation) появляется флаг STARTER_ASSETS_PACKAGES_CHECKED, но зачем он нужен? Давайте разбираться.

Исследуем скрипты

Для иследования был выбран ассет "Starter Assets - First Person Character Controller". Открываем скрипт "ThirdPersonController" и что мы видим:

...
namespace StarterAssets
{
    [RequireComponent(typeof(CharacterController))]
#if ENABLE_INPUT_SYSTEM && STARTER_ASSETS_PACKAGES_CHECKED
    [RequireComponent(typeof(PlayerInput))]
#endif
    public class ThirdPersonController : MonoBehaviour
    {
...
...
#if ENABLE_INPUT_SYSTEM && STARTER_ASSETS_PACKAGES_CHECKED
        private PlayerInput _playerInput;
#endif
...
...
#if ENABLE_INPUT_SYSTEM && STARTER_ASSETS_PACKAGES_CHECKED
            _playerInput = GetComponent<PlayerInput>();
#else
...

По всюду этот флаг используется в паре с флагом ENABLE_INPUT_SYSTEM, хм... интересно. Очевидно что флаг ENABLE_INPUT_SYSTEM отвечает за новую систему ввода, но вот второй флаг зачем он здесь и кто его устанавливает в настройках проекта? Смотрим дальше. Нашелся еще один флаг в скрипте "StarterAssetsDeployMenu.cs", но уже тут он используется один:

...
#if STARTER_ASSETS_PACKAGES_CHECKED
        private static void CheckCameras(Transform targetParent, string prefabFolder)
        {
            CheckMainCamera(prefabFolder);

            GameObject vcam = GameObject.Find(CinemachineVirtualCameraName);

...

Из кода становиться понятно что он включает работу с кинемашиной. Интересно, значит получается что этот флаг контролирует подключение кода который в свой очередь находиться в двух пакетах "com.unity.inputsystem" и "com.unity.cinemachine". С этим вроде немного разобрались, но всетаки кто устанавливает этот флаг в настройках проекта?

Исследуем файлы проекта

После тщательного обследования файлов ассета, выявлена подозрительная библиотека Assets\StarterAssets\Editor\PackageChecker\StarterAssetsPackageChecker.dll давайте ее отрефлектим:

Hidden text
// Decompiled with JetBrains decompiler
// Type: StarterAssetsPackageChecker.PackageChecker
// Assembly: StarterAssetsPackageChecker, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
// MVID: 2A478D25-B4D8-4B2D-BB34-CB7D710194F5
// Assembly location: D:\YandexDisk\Development\Games\secrets-from-unity\Assets\StarterAssets\Editor\PackageChecker\StarterAssetsPackageChecker.dll

using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEditor.PackageManager;
using UnityEditor.PackageManager.Requests;
using UnityEngine;

namespace StarterAssetsPackageChecker
{
  public static class PackageChecker
  {
    private static ListRequest _clientList;
    private static SearchRequest _compatibleList;
    private static List<PackageChecker.PackageEntry> _packagesToAdd;
    private static AddRequest[] _addRequests;
    private static bool[] _installRequired;
    private static PackageChecker.Settings _settings;

    [InitializeOnLoadMethod]
    private static void CheckPackage()
    {
      PackageChecker._settings = new PackageChecker.Settings();
      string[] files = Directory.GetFiles(Application.dataPath, "PackageCheckerSettings.json", SearchOption.AllDirectories);
      if (files.Length != 0)
        JsonUtility.FromJsonOverwrite(File.ReadAllText(files[0]), (object) PackageChecker._settings);
      if (PackageChecker.CheckScriptingDefine(PackageChecker._settings.PackageCheckerScriptingDefine))
        return;
      PackageChecker._packagesToAdd = new List<PackageChecker.PackageEntry>();
      PackageChecker._clientList = (ListRequest) null;
      PackageChecker._compatibleList = (SearchRequest) null;
      PackageChecker._packagesToAdd = new List<PackageChecker.PackageEntry>();
      foreach (string str in PackageChecker._settings.PackagesToAdd)
      {
        char[] chArray = new char[1]{ '@' };
        string[] strArray = str.Split(chArray);
        PackageChecker.PackageEntry packageEntry = new PackageChecker.PackageEntry()
        {
          Name = strArray[0],
          Version = strArray.Length > 1 ? strArray[1] : (string) null
        };
        PackageChecker._packagesToAdd.Add(packageEntry);
      }
      PackageChecker.SetScriptingDefine(PackageChecker._settings.PackageCheckerScriptingDefine);
      PackageChecker._compatibleList = Client.SearchAll();
      while (!PackageChecker._compatibleList.IsCompleted)
      {
        if ((PackageChecker._compatibleList.Status == StatusCode.Failure || PackageChecker._compatibleList.Error != null) && PackageChecker._compatibleList.Error != null)
        {
          Debug.LogError((object) PackageChecker._compatibleList.Error.message);
          break;
        }
      }
      PackageChecker._clientList = Client.List();
      while (!PackageChecker._clientList.IsCompleted)
      {
        if ((PackageChecker._clientList.Status == StatusCode.Failure || PackageChecker._clientList.Error != null) && PackageChecker._clientList.Error != null)
        {
          Debug.LogError((object) PackageChecker._clientList.Error.message);
          break;
        }
      }
      PackageChecker._addRequests = new AddRequest[PackageChecker._packagesToAdd.Count];
      PackageChecker._installRequired = new bool[PackageChecker._packagesToAdd.Count];
      for (int index = 0; index < PackageChecker._installRequired.Length; ++index)
        PackageChecker._installRequired[index] = false;
      List<UnityEditor.PackageManager.PackageInfo> packageInfoList1 = new List<UnityEditor.PackageManager.PackageInfo>();
      List<UnityEditor.PackageManager.PackageInfo> packageInfoList2 = new List<UnityEditor.PackageManager.PackageInfo>();
      foreach (UnityEditor.PackageManager.PackageInfo packageInfo in PackageChecker._compatibleList.Result)
        packageInfoList1.Add(packageInfo);
      foreach (UnityEditor.PackageManager.PackageInfo packageInfo in (IEnumerable<UnityEditor.PackageManager.PackageInfo>) PackageChecker._clientList.Result)
        packageInfoList2.Add(packageInfo);
      for (int index = 0; index < PackageChecker._packagesToAdd.Count; ++index)
      {
        if (PackageChecker._packagesToAdd[index].Version == null)
        {
          foreach (UnityEditor.PackageManager.PackageInfo packageInfo in packageInfoList1)
          {
            if (PackageChecker._packagesToAdd[index].Name == packageInfo.name && packageInfo.versions.verified != string.Empty)
            {
              PackageChecker._packagesToAdd[index].Version = packageInfo.versions.verified;
              PackageChecker._installRequired[index] = true;
            }
          }
        }
        foreach (UnityEditor.PackageManager.PackageInfo packageInfo in packageInfoList2)
        {
          if (PackageChecker._packagesToAdd[index].Name == packageInfo.name)
          {
            switch (PackageChecker.CompareVersion(PackageChecker._packagesToAdd[index].Version, packageInfo.version))
            {
              case -1:
                PackageChecker._installRequired[index] = (EditorUtility.DisplayDialog("Confirm Package Downgrade", "The version of \"" + PackageChecker._packagesToAdd[index].Name + "\" in this project is " + packageInfo.version + ". The latest verified version is " + PackageChecker._packagesToAdd[index].Version + ". " + packageInfo.version + " is unverified. Would you like to downgrade it to the latest verified version? (Recommended)", "Yes", "No") ? 1 : 0) != 0;
                Debug.Log((object) ("<b>Package version ahead</b>: " + packageInfo.packageId + " is newer than latest verified version " + packageInfo.versions.verified + ", skipped install"));
                continue;
              case 0:
                PackageChecker._installRequired[index] = false;
                Debug.Log((object) ("<b>Package version match</b>: " + packageInfo.packageId + " matches latest verified version " + packageInfo.versions.verified + ". Skipped install"));
                continue;
              case 1:
                PackageChecker._installRequired[index] = (EditorUtility.DisplayDialog("Confirm Package Upgrade", "The version of \"" + PackageChecker._packagesToAdd[index].Name + "\" in this project is " + packageInfo.version + ". The latest verified version is " + PackageChecker._packagesToAdd[index].Version + ". Would you like to upgrade it to the latest version? (Recommended)", "Yes", "No") ? 1 : 0) != 0;
                Debug.Log((object) ("<b>Package version behind</b>: " + packageInfo.packageId + " is behind latest verified version " + packageInfo.versions.verified + ". prompting user install"));
                continue;
              default:
                continue;
            }
          }
        }
      }
      for (int index = 0; index < PackageChecker._packagesToAdd.Count; ++index)
      {
        if (PackageChecker._installRequired[index])
          PackageChecker._addRequests[index] = PackageChecker.InstallSelectedPackage(PackageChecker._packagesToAdd[index].Name, PackageChecker._packagesToAdd[index].Version);
      }
      PackageChecker.ReimportPackagesByKeyword();
    }

    private static AddRequest InstallSelectedPackage(
      string packageName,
      string packageVersion)
    {
      if (packageVersion != null)
      {
        packageName = packageName + "@" + packageVersion;
        Debug.Log((object) ("<b>Adding package</b>: " + packageName));
      }
      AddRequest addRequest = Client.Add(packageName);
      while (!addRequest.IsCompleted)
      {
        if ((addRequest.Status == StatusCode.Failure || addRequest.Error != null) && addRequest.Error != null)
        {
          Debug.LogError((object) addRequest.Error.message);
          return (AddRequest) null;
        }
      }
      return addRequest;
    }

    private static void ReimportPackagesByKeyword()
    {
      AssetDatabase.Refresh();
      AssetDatabase.ImportAsset(PackageChecker._settings.EditorFolderRoot, ImportAssetOptions.ImportRecursive);
    }

    public static int CompareVersion(string latestVerifiedVersion, string projectVersion)
    {
      string[] strArray1 = latestVerifiedVersion.Split('.');
      string[] strArray2 = projectVersion.Split('.');
      int index1 = 0;
      for (int index2 = 0; index1 < strArray1.Length || index2 < strArray2.Length; ++index2)
      {
        int num1 = 0;
        int num2 = 0;
        if (index1 < strArray1.Length)
          num1 = Convert.ToInt32(strArray1[index1]);
        if (index2 < strArray2.Length)
          num2 = Convert.ToInt32(strArray2[index2]);
        if (num1 > num2)
          return 1;
        if (num1 < num2)
          return -1;
        ++index1;
      }
      return 0;
    }

    private static bool CheckScriptingDefine(string scriptingDefine) => PlayerSettings.GetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup).Contains(scriptingDefine);

    private static void SetScriptingDefine(string scriptingDefine)
    {
      BuildTargetGroup buildTargetGroup = EditorUserBuildSettings.selectedBuildTargetGroup;
      string defineSymbolsForGroup = PlayerSettings.GetScriptingDefineSymbolsForGroup(buildTargetGroup);
      if (defineSymbolsForGroup.Contains(scriptingDefine))
        return;
      string defines = defineSymbolsForGroup + ";" + scriptingDefine;
      PlayerSettings.SetScriptingDefineSymbolsForGroup(buildTargetGroup, defines);
    }

    public static void RemovePackageCheckerScriptingDefine()
    {
      BuildTargetGroup buildTargetGroup = EditorUserBuildSettings.selectedBuildTargetGroup;
      string defineSymbolsForGroup = PlayerSettings.GetScriptingDefineSymbolsForGroup(buildTargetGroup);
      if (!defineSymbolsForGroup.Contains(PackageChecker._settings.PackageCheckerScriptingDefine))
        return;
      string defines = defineSymbolsForGroup.Replace(PackageChecker._settings.PackageCheckerScriptingDefine, "");
      PlayerSettings.SetScriptingDefineSymbolsForGroup(buildTargetGroup, defines);
    }

    private class PackageEntry
    {
      public string Name;
      public string Version;
    }

    [Serializable]
    private class Settings
    {
      public string EditorFolderRoot = "Assets/StarterAssets/";
      public string[] PackagesToAdd = new string[2]
      {
        "com.unity.cinemachine",
        "com.unity.inputsystem"
      };

      public string PackageCheckerScriptingDefine => "STARTER_ASSETS_PACKAGES_CHECKED";
    }
  }
}

И что мы тут видим? Наш флажочек STARTER_ASSETS_PACKAGES_CHECKED)

 [Serializable]
    private class Settings
    {
      public string EditorFolderRoot = "Assets/StarterAssets/";
      public string[] PackagesToAdd = new string[2]
      {
        "com.unity.cinemachine",
        "com.unity.inputsystem"
      };

      public string PackageCheckerScriptingDefine => "STARTER_ASSETS_PACKAGES_CHECKED";
    }

Так, я чувствую что мы уже близко к истине.

Разбираем потроха StarterAssetsPackageChecker.dll

Изучив код этой библиотеки, я пришол к выводу что это - автоматический инсталятор пакетов Unity. Очень интересно! Давайте расскажу как эта штука работает.

Главный метод запускается каждый раз, при "перезагрузки" редактора, о чем говорит аттрибут[InitializeOnLoadMethod]в этом методе идет поиск файлов с именем "PackageCheckerSettings.json", а затем настройки из этого файла мапятся на PackageChecker._settings.

...
 [InitializeOnLoadMethod]
    private static void CheckPackage()
    {
      PackageChecker._settings = new PackageChecker.Settings();
      string[] files = Directory.GetFiles(Application.dataPath, "PackageCheckerSettings.json", SearchOption.AllDirectories);
      if (files.Length != 0)
        JsonUtility.FromJsonOverwrite(File.ReadAllText(files[0]), (object) PackageChecker._settings);
...

Давайте взглянем что находиться в файле "PackageCheckerSettings.json", и тут мы видим опять наши пакеты, а также какой-то "EditorFolderRoot":

{
    "EditorFolderRoot": "Assets/StarterAssets/",
    "PackagesToAdd": [
        "com.unity.cinemachine",
        "com.unity.inputsystem"
    ]
}

Вернемся в нашу dll. Далее по коду идет сравнение версий пакетов и их установка с помощью метода private static AddRequest InstallSelectedPackage. И тут же видим нашу заветную строчку, которая задает флаг STARTER_ASSETS_PACKAGES_CHECKED на уровне проекта:

PackageChecker.SetScriptingDefine(PackageChecker._settings.PackageCheckerScriptingDefine);
...
 private static void SetScriptingDefine(string scriptingDefine)
    {
      BuildTargetGroup buildTargetGroup = EditorUserBuildSettings.selectedBuildTargetGroup;
      string defineSymbolsForGroup = PlayerSettings.GetScriptingDefineSymbolsForGroup(buildTargetGroup);
      if (defineSymbolsForGroup.Contains(scriptingDefine))
        return;
      string defines = defineSymbolsForGroup + ";" + scriptingDefine;
      PlayerSettings.SetScriptingDefineSymbolsForGroup(buildTargetGroup, defines);
    }

Теперь стало все понятно, этот флаг фиксирует установку пакетов и при дальнейшем вызове метода CheckPackage() идет проверка, что если флаг установлен то установку пакетов уже не производим. Вауля!!!

 if (PackageChecker.CheckScriptingDefine(PackageChecker._settings.PackageCheckerScriptingDefine))
        return;

А что насчет строчки "EditorFolderRoot": "Assets/StarterAssets/" из конфига? А тут все просто она указывает на папку с ассетами которым нужно сделать реимпорт после установки пакетов

  private static void ReimportPackagesByKeyword()
    {
      AssetDatabase.Refresh();
      AssetDatabase.ImportAsset(PackageChecker._settings.EditorFolderRoot, ImportAssetOptions.ImportRecursive);
    }

Что в итоге?

Мы можем использовать библиотеку StarterAssetsPackageChecker.dll в паре с файлом PackageCheckerSettings.json в своем проекте для автоматической установки пакетов Unity. Просто закидываем их к себе в папку Editor и добавляем необходимые пакеты в файл конфигурации.

Чтобы я улучшил в библиотеке StarterAssetsPackageChecker.dll так это сделал бы свойство public string PackageCheckerScriptingDefine => "STARTER_ASSETS_PACKAGES_CHECKED" доступным для записи, чтобы можно было задавать произвольное имя флага в своих ассетах. Еще бы добавил итерацию по всем файлам PackageCheckerSettings.json находящимся в проекте, чтобы установить все зависимости, а не производить установку только по первому попавшемуся файлу.

Могу предположить что у каманды Unity это своего рода "заготовка" для будущей автоматизации установки пакетов, поэтому будем надеяться и верить что работа с пакетами станет еще проще и удобней. А также пожелаем Unity чтобы она добавила возможность добавлять scope в файлы манифеста с помощью кода.


Присоединяйтесь к моим соц сетям:

YouTube: https://www.youtube.com/channel/UC8Pm1hZfQMKE8nfSdYqKugg

VK: https://vk.com/stupenkovanton

GitHub: https://github.com/stupenkov

Linkedin: https://www.linkedin.com/in/stupenkov/

Комментарии (7)


  1. Styrbo
    07.09.2022 09:47

    В целом такая автоматизация уже существует - в зависимостях к пакету можно прописать нужные пакеты https://docs.unity3d.com/Manual/upm-dependencies.html


    1. stupenkov Автор
      07.09.2022 09:51

      Это то все понятно, но вы похоже не читаете название статьи "Зачем нужен флаг STARTER_ASSETS_PACKAGES_CHECKED в стартовых ассетах". Смысл статьи добраться до истины для чего нужен этот флаг, а вывод этой статьи о том что у Unity есть заготовка библиотеки для автоматизации установки пакетов. Т.е. если вы создаете .packageunity то зависимости туда не падают а с помощью этой dll можно автоматизировать установку всех зависимостей.


      1. dm_bondarev
        07.09.2022 19:07

        на этот вопрос давно ответили разработчики, первая ссылка в гугле https://forum.unity.com/threads/say-hello-to-the-new-starter-asset-packages.1123051/#post-7260505


    1. Suvitruf
      07.09.2022 10:10

      С этим проблема в том, что если ставить пакет из файла, то эти зависимости не установятся.


  1. Topmult
    07.09.2022 13:06
    +1

    Очень познавательная статья, спасибо автору


    1. Topmult
      07.09.2022 13:08

      Спасибо!


    1. stupenkov Автор
      07.09.2022 13:12

      И Вам Спасибо!