Добавлен , опубликован
Последние дни очень много кропчу над редактором. И естественно, какой редактор под юнькой без стандартного GUI? Да, по сути - большая часть работы - это сказка о том как сделать так, чтобы стандартный GUI работал как мне нужно.
А пишу я сейчас иерархию, точнее инструмент для ее создания. Да, долго, но это стандартный GUI, и чтобы сделать эту работу качественно - нужно изрядно попотеть и написать с десяток костылей. Вообще, можно сказать - это нативный GUI и обычно дальше объяснять про сроки не приходится.

Как я ошибку в GUI нашел

Позавчера, надо сказать, я наконец-то научился избавляться от ошибки, которая ранее часто меня бесила. Не то чтобы она мешала, именно бесила - выскакивает время от времени при рисовании.
А содержание ошибки следующее:
ArgumentException: Getting control 0's position in a group with only 0 controls when doing Repaint
Цифра - произвольная, событие - тоже. И указывает туда, где как оказалось собака зарыта не была. Потому и было сложно эту ошибку найти.
        GUILayout.BeginVertical(Styles.nullStyle, GUILayout.ExpandHeight(true));
        GUILayout.FlexibleSpace();
        GUILayout.EndVertical();

        if (Event.current.type == EventType.Repaint)
            fullRect = GUILayoutUtility.GetLastRect();

        var itemsCount = (int)fullRect.height / height;
        var elems = GetVisibleElements(itemsCount);

        for (int i = 0; i < elems.Count; i++)
        {
            GUILayout.BeginArea(new Rect(fullRect.x, fullRect.y + height * i, fullRect.width, height));
            DrawElement(elems[i]);
            GUILayout.EndArea();
        }
Кто не понимает стандартный гуй, поясню что здесь:
  1. Мы рисуем через GUILayout произвольный прямоугольник, заполняющий максимально возможное пространство (GUILayout тем и отличается от просто класса GUI, что позволяет рисовать абстрактно, а не по точным координатам). На выходе все само растягивается и считается как нужно.
  2. Далее в fullRect мы записывает последний получившийся прямоугольник (область из первого пункта). Этот метод может срабатывать только когда происходит Repaint.
  3. По полученному прямоугольнику мы узнаем максимально возможное к отображению количество элементов
  4. Берем элементы для отображения
  5. Рисуем каждый элемент в нужной области (BeginArea и EndArea)
Вот примерно такой код и содержит ошибку. Указывает на строчку с BeginArea.
На самом же деле ошибка была в GetLastRect
Чтобы понять ее принцип нужно понимать, что для отрисовки Layout функция OnGUI прогоняется дважды.
  1. Сначала с событием Layout
  2. Затем с событием Repaint
И конкретно сама ошибка возникает при ресайзе. Что же происходит под капотом?
  1. На стадии Layout у нас хранились старая область прямоугольника. Так как функция прогонялась в этом режиме, было зафиксировано, что метод нарисует элемент Area некоторое количество раз (например 5).
  2. На стадии Repaint код прогнался еще раз, в fullRect попали новые координаты и результат в itemsCount получился уже не 5, а допустим 6.
  3. При прорисовке обнаружается такое недоразумение и это и сообщается в лог в виде ошибки.
Как же избавиться от ошибки? Да очень просто, поставить строчки с расчетом itemsCount перед получением нового прямоугольника:
        GUILayout.BeginVertical(Styles.nullStyle, GUILayout.ExpandHeight(true));
        GUILayout.FlexibleSpace();
        GUILayout.EndVertical();

        var itemsCount = (int)fullRect.height / height;
        var elems = GetVisibleElements(itemsCount);

        if (Event.current.type == EventType.Repaint)
            fullRect = GUILayoutUtility.GetLastRect();

        for (int i = 0; i < elems.Count; i++)
        {
            GUILayout.BeginArea(new Rect(fullRect.x, fullRect.y + height * i, fullRect.width, height));
            DrawElement(elems[i]);
            GUILayout.EndArea();
        }
В этом случае сначала посчитается число, а затем примется новый прямоугольник. Не будет разницы между возвратом значения в этих двух прогонах метода - все будет в шоколаде.
Кстати наблюдательный может подумать что после GetLastRect возможно дальше продолжить условие, но это тоже приведет к ошибкам, так как не произойдет нужный для GUILayout-функций просчет координат при событии Layout.
И что я понял из этого, так это то, что понимать как внутри устроен движок очень даже нужное дело - иначе - ничего серьезного сделать не получится. И ведь это лишь один подводный камень в этом море.

Делаем события во времени для редактора

Стало быть вчера понадобилось мне сделать таймер, так как при прокрутке иерархии нужно было время от времени обновлять содержимое окна.
Именно такой - зажал мышку - обновляет, отжал - перестал.
К несчастью сделать точный способ средствами юнити не представляется возможным. Но доступ к Update все же есть, просто нельзя точно отследить количество прошедшего времени - в документации просто написано, что он вызывается приблизительно 100 раз в секунду.
Ну и собственно код для таймера в редакторе, обновляющего окно каждые полсекунды:
    public void StartUpdate()
    {
        timeDeltaCount = 0;
        EditorApplication.update += DoUpdate;
    }

    public int timeDeltaCount = 0;
    public void DoUpdate()
    {
        timeDeltaCount++;
        if (timeDeltaCount == 50)
        {
            window.Repaint(); //Здесь могут быть любые другие действия
            timeDeltaCount = 0;
        }
    }

    public void EndUpdate()
    {
        EditorApplication.update -= DoUpdate;
    }
Вот такие дела.

Табуляция и активные контролы

Поначалу может показаться, что описывание контрола - лишняя и ненужная работа, однако, именно она позволяет создать табуляцию и вообще делать вызовы для конкретного элемента.
Есть много рычагов в Unity, для реализации разных фич связаных с контролами, но я пожалуй расскажу самую основу.
Вот минимальный код для того, чтобы описать нажимаемый контрол, аля кнопка:
var id = GUIUtility.GetControlID("MyControl".GetHashCode(), FocusType.Native, rect);  //создаем id для "MyControl"
var eventType = Event.current.GetTypeForControl(id); //достаем его событие
if (eventType == EventType.MouseDown)                 //Если нажата кнопка мыши
{
    if (rect.Contains(Event.current.mousePosition) //и мы внутри контрола
    {
        GUIUtility.hotControl = id; //Сделать контрол активным
        Event.current.Use();    //И пометить событие как использованное
    }
}
if (eventType ==  EventType.MouseUp) //Если отжата кнопка мыши
{
    if (GUIUtility.hotControl == id) //И выбран текущий контрол
    {
        GUIUtility.hotControl = 0; //Очистить активный контрол
        Event.current.Use(); //И пометить событие как использованное
    }
}
Раздельный if умышленен, так как обычно туда нужно еще что-то дописывать.
Собственно этот простой код решит все проблемы, связанные с тем, от чьего лица мы отпускаем и нажимаем кнопки и совершаем какие-либо действия.
Всем крепким ребятам, дочитавшим сей текст до конца, и хотя бы попытавшимся понять принципы работы ГУИ в редакторе, о которых я говорил здесь, - большое спасибо за прочтение.
`
ОЖИДАНИЕ РЕКЛАМЫ...