Как я перестал беспокоиться и начал резать прямоугольники в 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

0f7xk-7de3qp5-r-qi0vcgyuqcc.png


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

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


8jbfu7fb2z5c3casdtb0kcioyus.png


Где взять?

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

© Habrahabr.ru