15 августа 2012 в 15:47

Unity3D 3.x Получение текущего активного окна

Недавно перед нашей командой встала довольно простая задача. Нам нужно было сделать перетаскивание вещи из инвентаря в другие окна (эквип, сундук). Если два окна находятся друг над другом, то вещь должна упасть в то окно, которое выше.

Бегло осмотрев список свойств в классе GUI я не нашел чего-либо подходящего, потом я осмотрел GUIUtility, и даже заглянул в GUILayout. Вообщем такого свойства нигде не было. Гугление по этому запросу выдает несколько вопросов в Q&A и пару скудных постов на офф. форуме которые заканчиваются ответами в стиле «так сделать нельзя, но можно вручную отслеживать по какому окну нажали мышкой и заполнять переменную активного окна самостоятельно».
Нам не подошло ничего из того что там предлагали, но один парень натолкнул меня на интересную мысль. Мы пишем код на C#, а значит можем пользоваться всеми плюсами этого языка, в том числе и С# Reflection

Кишки

Скачав мой любимый Dis#, я сразу полез в код функции GUI.Window

        public static Rect Window(int id, Rect clientRect, GUI.WindowFunction func, string text)
        {
            return GUI.DoWindow(id, clientRect, func, GUIContent.Temp(text), GUI.skin.window, true);
        }

        internal static Rect DoWindow(int id, Rect clientRect, GUI.WindowFunction func, GUIContent title, GUIStyle style, bool forceRectOnLayout)
        {
            GUIUtility.CheckOnGUI();
            GUI._Window _window = (GUI._Window)GUI._WindowList.instance.windows[id];
            if (_window == null)
            {
                _window = new GUI._Window(id);
                GUI._WindowList.instance.windows[id] = _window;
                GUI.s_LayersChanged = true;
            }
            if (!_window.moved)
                _window.rect = clientRect;
            _window.moved = false;
            _window.opacity = 1.0F;
            _window.style = style;
            _window.title.text = title.text;
            _window.title.image = title.image;
            _window.title.tooltip = title.tooltip;
            _window.func = func;
            _window.used = true;
            _window.enabled = GUI.enabled;
            _window.color = GUI.color;
            _window.backgroundColor = GUI.backgroundColor;
            _window.matrix = GUI.matrix;
            _window.skin = GUI.skin;
            _window.contentColor = GUI.contentColor;
            _window.forceRect = forceRectOnLayout;
            return _window.rect;
        }

Ага, значит есть список окон, осталось выяснить в какой последовательности они отрисовываются, для этого заглянем в функцию GUI.BringWindowToFront

        public static void BringWindowToFront(int windowID)
        {
            GUIUtility.CheckOnGUI();
            GUI._Window _window1 = GUI._WindowList.instance.Get(windowID);
            if (_window1 != null)
            {
                int i = 0;
                foreach (GUI._Window _window2 in GUI._WindowList.instance.windows.Values)
                {
                    if (_window2.depth < i)
                        i = _window2.depth;
                }
                _window1.depth = i - 1;
                GUI.s_LayersChanged = true;
            }
        }


Все понятно, в классе GUI есть синглтон класс _WindowList у которого есть список окон. У каждого окна есть Depth. Отрисовка происходит в порядке убывания Depth. Все что осталось узнать это какого типа этот список.

  internal sealed class _WindowList
  {
         internal Hashtable windows;
         internal static GUI._WindowList instance;
.......

Вот и узнали :)

Пишем функцию для выковыривания добра

Функция хорошо прокомментирована и надеюсь не нуждается в пояснении.
	/// <summary>
	/// Функция определяет самое верхнее окно из списка
	/// </summary>
	/// <returns>
	/// ID самого верхнего окна
	/// </returns>
	/// <param name='id_list'>
	/// Список ID окон
	/// </param>
	int GetTopmostId(List<int> id_list)
    {
		//Получаем тип GUI
		Type guiType = typeof(GUI);
		//Получаем тип списка окон
		Type windowListType = guiType.Assembly.GetType("UnityEngine.GUI+_WindowList");                                
		//Получаем поле instance списка, в котором хранится его экземпляр (это синглтон)
		FieldInfo windowListInstanceField = windowListType.GetField("instance", BindingFlags.NonPublic | BindingFlags.Static);
		//Получаем значение поля, теперь  нас есть экземпляр списка
		object windowListInstance = windowListInstanceField.GetValue(null);
		//Получаем поле спика с окнами
		FieldInfo windowsField = windowListType.GetField("windows", BindingFlags.NonPublic | BindingFlags.Instance);
		//Получаем сам список окон типа Hashtable
		Hashtable hashtable = windowsField.GetValue(windowListInstance) as Hashtable;
		//Осталось перебрать его и найти верхнее
		int min = -1;
		int window_id = -1;
		foreach(DictionaryEntry entry in hashtable)
		{
			int key = (int)entry.Key;
			if (id_list.Contains(key)) //сравнивать только если окно в нашем списке
			{
				//получаем значение поля глубина у окна
				int depth = (int)entry.Value.GetType().GetField("depth", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(entry.Value);
				if (min < 0 || depth < min)
				{
					min = depth;
					window_id = key;
				}
			}
		}
		
		return window_id;
   }

Примечание: если вы собираетесь вызывать функцию каждый OnGUI() event, то рекомендую разбить ее на две части, и хранить Hashtable в переменной класса, чтобы каждый раз не терять время на выяснении кучи типов и полей.

Для минусующих: альтернатив этому решению не существует, если нужно узнать на каком сейчас уровне находится окно, то это единственный способ
Alexandr @agasper
карма
26,0
рейтинг 0,0
Самое читаемое Разработка

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

  • 0
    Поздравляю, вы только что повесили себе ружье на стену.
  • 0
    Unity позволяет писать на JS, C# и Boo на выбор и прочитав эту статью хотелось бы задать вопрос — существует ли существенная разница в плане возможностей на чем писать? Может ли оказаться что есть вещи какие в Unity можно сделать только на одном из этих языков (к примеру C#) и в принципе невозможно сделать на другом (JS к примеру)? Или любую проблему можно решить на любом из языков, просто немного по разному? Ведь API Unity как я понимаю для всех языков одинаков (в плане возможностей)?
    • 0
      К сожалению я не знаю всех возможностей JavaScript, т.к. ничего серьезного на нем не писал.
    • 0
      На JS можно задать значение для координат трансформа (transform.position.x ,y, z) напрямую (transform.position.x = 50.0f), в C# же придется задавать через Vector3.

      Это из известной мне разницы :)
      • 0
        Ну это уже особенности синтаксиса и типов переменных, мне интереснее функциональные ограничения между языками.
        • 0
          Функциональных ограничений? Не знаю насчет JS/Boo, но C# позволяет «использовать себя полностью», выходя за рамки встроенных возможностей Unity3D.
    • +1
      Лучше использовать C#. JavaScript тут — это JScript от Microsoft, урезанный и стремный.
    • 0
      Для начала ссылка на официальную вики об особенностях местного JS.
      Из того с чем я сталкивался:
      — геттеры и сеттеры в UnityScript (будем его так называть) есть, но их использование иногда не удобно;
      — невозможно передавать в UnityScript объекты в параметрах (в отличии от классического JS).
  • 0
    Если это синглтон, то почему бы вам не сохранить куда-нибудь при старте ссылку на инстанс? Тогда и не нужно будет каждый раз рефлекшн дергать.

    Еще вариант… отслеживать интеракции с окнами и хранить стэк окон.

    П.С.: Ей Богу! Зря вы с рефлекшном завязываетесь! Лучше зайдите с другого бока к проблеме — наверняка есть решение много безопаснее.
    • 0
      почему бы вам не сохранить куда-нибудь при старте ссылку на инстанс? Тогда и не нужно будет каждый раз рефлекшн дергать.

      Я об этом написал в примечании, сама функция это пример.

      Еще вариант… отслеживать интеракции с окнами и хранить стэк окон.

      Это решение не подходит, ввиду того, что мы не знаем на каком слое окно после вызова. К тому же отследить все варианты изменения глубины окна не представляется возможным.

      Лучше зайдите с другого бока к проблеме — наверняка есть решение много безопаснее.

      Нету. Либо гугль, форум юнити, и их документация о нем не знают.
      Это решение не показало уменьшения производительности или стабильности клиента.
      • +1
        Небезопасность тут потоньше. Реализация может измениться и код вдруг начнет падать или работать не так.
        • 0
          Юнити не обновляется сама, так что с этим проблем не будет. В любом случае альтернатив этому решению нет.
        • 0
          Тоже хотел написать об этом… но вспомнил, что при билде либы рантайма встраиваются в дистриб приложения.
      • 0
        > Это решение не показало уменьшения производительности или стабильности клиента.
        До поры… до времени… Так всегда вначале кажется, что производительность не аффектается. А потом постепенно такие же костыли появляются в других местах либо на этот кастыль наложится другой, который сам по себе ничего не рушит. А вместе полная задница.

        П.С.: еще вариант — докинг без перекрытия. Уж выстроить то окна рядом друг с другом можно?
        См. тайловые менеджеры окон
        • 0
          Да, запретить перекрывать окна можно. Но для нашего интерфейса это не подходящий вариант.
  • 0
    А сделать классы обертки для окон с уникальным ID нельзя?
    • 0
      все равно мы не будем знать на каком слое оно сейчас находится
      • 0
        А если контролировать это изначально? То есть сделать свой z-buffer для них, свою переключалку по клику курсора?
        • 0
          Дело в том что после вывода окна на экран неизвестно на каком оно слое, еще до действий пользователя.
          • 0
            Понял :/ Вечно с этим встроенным ГУИ траблы. С 4ой версией надеюсь будет получше
            • +1
              Да, текущий Unity GUI доставил нам немало неприятностей.
  • +1
    Есть еще одна проблема — рефлекшн выпилен в мобильных платформах и, потенциально, в вебплеере (надо проверять). Т.е. получается непереносимость проекта, что ломает одну из основных «фич» юнити — сборка одного проекта под несколько платформ.
    • 0
      Ну этот метод точно работает для Standalone, а для остальных случаев приходится менять концепцию интерфейса.
  • 0
    Не придираюсь к конкретному проекту, просто хотел бы обсудить конепцию данного интерфейса.
    Придерживаюсь мнения, что перетаскиваемые окна не так удобны, как может показаться. Из примеров игр, в которых были реализованы «плавучий» оконный интерфейс вспоминаются Морровинд, ЕВЕ онлайн, Рагнарок онлайн. Из моих наблюдений во всех случаях игрок приходил к тому, что однажды распологал окна, а потом не менял эту схему на протяжении всей игровой «карьеры». Схема обычно придераживалась некоторых правил: окна не наслаивались друг на друга; схожие по функциональности окна (например, сундук и рюкзак) ставились рядом и подгонялись под одну высоту или ширину; чем больше информации в окне, тем больше окно. Словом, я считаю, что если бы за игрока все это сделал (конечно, с крайней вдумчивостью и отсветсвенностью) разработчик или дизайнер интерфейсов, то и игрок бы оказался в плюсе, и от многой лишней функциональности (перетаскивание окон) можно было бы отказаться.

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.