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

в 11:47, , рубрики: game development, GUI, reflection, unity3d, метки: , , ,

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

Автор: agasper

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js