- PVSM.RU - https://www.pvsm.ru -

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

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

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

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

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

Долгое время я мирился с проблемой, просто пытаясь не делать слишком плохих вещей. Но когда занялся разработкой OneLine [2], параллельно написал и ряд расширений для класса 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);

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

Еще вариант из очень красивого туториала

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);

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

Все то же, но с сахаром

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"));

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

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

То же самое, но без магических чисел

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);

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

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

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

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

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

Пример из декомпилированных библиотек 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;
}

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

Еще один пример из 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);
}

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

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

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

Как это делают с 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"));

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

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

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

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

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

Автор: SlavniyTeo

Источник [12]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/unity3d/267923

Ссылки в тексте:

[1] предыдущей статье: https://habrahabr.ru/post/340536/

[2] OneLine: https://github.com/slavniyteo/one-line

[3] здесь: https://docs.unity3d.com/Manual/editor-PropertyDrawers.html

[4] здесь: http://25games.net/2017/02/01/custom-property-drawers/

[5] здесь: https://github.com/LightGive/PropertyDrawerTest/blob/master/PropertyDrawerTest/Assets/Scripts/PropertyDrawer/Editor/PiyoDrawer.cs

[6] лучшие практики: https://habrahabr.ru/post/309478/

[7] здесь: https://blogs.unity3d.com/ru/2012/09/07/property-drawers-in-unity-4/

[8] здесь: https://github.com/MattRix/UnityDecompiled/blob/master/UnityEditor/UnityEditor/EditorGUI.cs

[9] здесь: https://github.com/MattRix/UnityDecompiled/blob/master/UnityEditor.UI/UnityEditor.UI/ColorBlockDrawer.cs

[10] нарисовал: https://draw.io/

[11] Image: https://habrastorage.org/webt/0f/7x/k-/0f7xk-7de3qp5-r-qi0vcgyuqcc.png

[12] Источник: https://habrahabr.ru/post/340858/