Как я перестал беспокоиться и начал резать прямоугольники в Unity правильно

    В своей предыдущей статье я обещал рассказать, свой способ работы с прямоугольниками. Разрабатывая OneLine, я написал несколько расширений класса Rect, заметно упрощающих работу с GUI. Сейчас я выделил их в отдельную библиотеку: RectEx.


    Подробности под катом.


    Суть проблемы


    Когда мы пишем PropertyDrawer в Unity, мы вынуждены пользоваться классом GUI (вместо GUILayout), а значит работать с разметкой руками. Код обрастает множеством new Rect(...) и rect.y += rect.height + 5, усложняется для чтения и изменений. Когда в дело замешиваются магические числа (далее будут примеры с просторов интернета), код становится настолько инертным, что каждое новое изменение воспринимается программистом как издевательство со стороны геймдизайнера.


    Долгое время я мирился с проблемой, просто пытаясь не делать слишком плохих вещей. Но когда занялся разработкой OneLine, параллельно написал и ряд расширений для класса Rect, упрощающих рутинную работу.


    Как это делают люди


    На просторах интернета я нашел множество способов нарезать прямоугольники в туториалах и исходниках на гитхабе. Далее идет небольшая подборка. Найдете ли Вы среди них свой любимый? Если нет, напишите в комментариях свой вариант, я добавлю в статью.


    Найденные примеры я правил по своему усмотрению, чтобы убрать все лишнее и сделать их нагляднее.


    Готовим прямоугольники заранее


    Официальный вариант из документации
    // Calculate rects
    var amountRect = new Rect (position.x, position.y, 30, position.height);
    var unitRect = new Rect (position.x+35, position.y, 50, position.height);
    var nameRect = new Rect (position.x+90, position.y, position.width-90, position.height);
    
    // Draw fields - passs GUIContent.none to each so they are drawn without labels
    EditorGUI.PropertyField (amountRect, property.FindPropertyRelative ("amount"), GUIContent.none);
    EditorGUI.PropertyField (unitRect, property.FindPropertyRelative ("unit"), GUIContent.none);
    EditorGUI.PropertyField (nameRect, property.FindPropertyRelative ("name"), GUIContent.none);

    Источник здесь.


    Еще вариант из очень красивого туториала
    Rect minRect = new Rect(position.x,
                            position.y,
                            position.width * 0.4f - 5,
                            position.height);
    Rect mirroredRect = new Rect(position.x + position.width * 
                                 position.y,
                                 position.width * 0.2f,
                                 position.height);
    Rect maxRect = new Rect(position.x + position.width * 0.6f + 5,
                            position.y,
                            position.width * 0.4f - 5,
                            position.height);

    Источник здесь.


    Все то же, но с сахаром
    var firstRect = new Rect(position){
        width = position.width / 2
    };
    var secondRect = new Rect(position){
        x = position.x + position.width / 2,
        width = position.width / 2
    };
    
    EditorGUI.PropertyField(firstRect, property.FindPropertyRelative("first"));
    EditorGUI.PropertyField(secondRect, property.FindPropertyRelative("second"));

    Источник здесь.


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


    То же самое, но без магических чисел
    float curveWidth = 50;
    
    var sliderRect = new Rect (rect.x, rect.y, rect.width - curveWidth, rect.height)
    EditorGUI.Slider (sliderRect, scale, min, max, label);
    
    var curveRect = new Rect (rect.width - curveWidth, rect.y, curveWidth, rect.height);
    EditorGUI.PropertyField (curveRect, curve, GUIContent.none);

    Источник здесь.


    Такой код тяжело читать в случаях, когда рисуется большое количество свойств.


    Такой код тяжело поддерживать. Даже если мы рисуем три свойства и вдруг нужно добавить четвертое/пятое.


    Однако есть способ лучше!


    Один прямоугольник: нарисовал => подвинул


    Пример из декомпилированных библиотек Unity
    float count = labels.Length;
    float space = 2;
    float width = (position.width - (count - 1) * space) / count;
    position.width = num2;
    
    for (int i = 0; i < count; i++){
        EditorGUI.PropertyField(position, properties[i], labels[i]);
        position.x += count + space;
    }

    Источник здесь


    Еще один пример из Unity
    public override void OnGUI(Rect rect, SerializedProperty prop, GUIContent label) {
        Rect position = rect;
        float height = EditorGUIUtility.singleLineHeight;
        float space = EditorGUIUtility.standardVerticalSpacing;
        position.height = height;
    
        var property = prop.FindPropertyRelative("m_NormalColor");
        var property2 = prop.FindPropertyRelative("m_HighlightedColor");
        var property3 = prop.FindPropertyRelative("m_PressedColor");
        var property4 = prop.FindPropertyRelative("m_DisabledColor");
        var property5 = prop.FindPropertyRelative("m_ColorMultiplier");
        var property6 = prop.FindPropertyRelative("m_FadeDuration");
    
        EditorGUI.PropertyField(position, property);
        position.y += height + space;
        EditorGUI.PropertyField(position, property2);
        position.y += height + space;
        EditorGUI.PropertyField(position, property3);
        position.y += height + space;
        EditorGUI.PropertyField(position, property4);
        position.y += height + space;
        EditorGUI.PropertyField(position, property5);
        position.y += height + space;
        EditorGUI.PropertyField(position, property6);
    }

    Источник здесь.


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


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


    Как это делают с RectEx


    RectEx добавляет несколько методов, расширяющих класс Rect, но наиболее полезны два: Column и Row.


    Почему такие странные названия?


    Сначала я назвал их SplitVertically и SplitHorizontally. Оказалось слишком длинно, неудобно, да еще и не читалось.


    Я попробовал SplitV и SplitH. Получилось короче и удобней. Однако, постоянно забываешь, что же каждый из них делает? Один режет горизонтальными линиями, другой — вертикальными. Или один возвращает горизонтальный столбец, другой — вертикальный?


    На помощь как всегда пришла математика, а точнее господа Вектор-Столбец и Вектор-Строка (оба слова с большой, потому как господ фамилии двойные). Уж глядя на rect.Row(5) сразу понимаешь, что метод возвращает строку, а rect.Column(5) — столбец.


    Дальше идут демонстрации.


    Режем на три равные части вертикальными линиями
    var rects = rect.Row(3);
    
    EditorGUI.PropertyField(rects[0], property.FindPropertyRelative("first"));
    EditorGUI.PropertyField(rects[1], property.FindPropertyRelative("second"));
    EditorGUI.PropertyField(rects[2], property.FindPropertyRelative("third"));

    Режем на три равные части горизонтальными линиями

    Я добавил i++, чтобы было проще менять строки местами.


    var rects = rect.Column(3);
    
    int i = 0;
    EditorGUI.PropertyField(rects[i++], property.FindPropertyRelative("first"));
    EditorGUI.PropertyField(rects[i++], property.FindPropertyRelative("second"));
    EditorGUI.PropertyField(rects[i++], property.FindPropertyRelative("third"));

    Элементы разного размера

    В этом примере мы передаем методу Column относительные веса, на основе которых получим: второй элемент в два раза больше первого, а третий — в три.


    var rects = rect.Column(new float[]{1, 2, 3});
    
    EditorGUI.PropertyField(rects[0], property.FindPropertyRelative("first"));
    EditorGUI.PropertyField(rects[1], property.FindPropertyRelative("second"));
    EditorGUI.PropertyField(rects[2], property.FindPropertyRelative("third"));

    Для наглядности я нарисовал две симметричные картинки, на которых попытался показать пример использования методов Raw и Column (картинки кликабельные).


    Использование метода Row


    Использование метода Column

    Вторая картинка — просто транспонированная первая: строки стали столбцами. Но я решил, что стоит нарисовать и её.



    Где взять?


    Текущая версия: v0.1.0.
    Попробовать можно на гитхабе. В ридми описаны остальные методы.

    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 4
    • +1
      «Плюс» за глубину и подход исследователя в решении даже простых проблем.
      • 0

        Есть же GuiLayout.BeginArea(rect)/EndArea и поехали верстать гуй через GuiLayout. Разве нет?

        • 0
          Иногда нет.

          Например, когда пишешь PropertyDrawer. В этом случае методы GUILayout не работают вообще.

          Или когда необходимо рисовать элементы поверх друг друга. Думаю, этого можно добиться используя GUILayoutUtility.GetLastRect + GUILayout.BeginArea/EndArea. Но мне жаль людей, которые будут поддерживать этот код.
        • 0
          Bootstrap + Grid Layout для Unity3D
          Прикольненько

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