Надоело писать PropertyDrawer в Unity? Есть способ лучше

Не так давно я участвовал в разработке игры на Unity. Много времени уделял инструментам для коллег: редактор уровней для геймдизайнера, удобные базы данных для художников.


По понятным причинам, в программировании интерфейсов под Unity мы не всегда можем использовать автоматическую разметку (удобные средства GUILayout), и нередко приходится вручную нарезать прямоугольники и рисовать интерфейсы средствами класса GUI. Эта работа утомительна, связана с большим количеством ошибок, а код получается сложным в поддержке. Со временем, возникла привычная каждому программисту мысль: напишу свой велосипед! "Должен быть способ лучше!". За подробностями приглашаю под кат.



Картинка для привлечения внимания взята отсюда.


Первым делом, те самые понятные причины

GUILayout чтобы рассчитать размеры рисуемых полей, вызывает метод OnGUI дважды:


  • В первый проход с event.type == EventType.Layout ничего не рисует, а лишь собирает данные обо всех рисуемых компонентах, чтобы рассчитать размеры прямоугольника для каждого из них;
  • Во время второго прохода все элементы рисуются по-настоящему.

Не стоит также забывать и о накладных расходах за вычисление прямоугольников. Таким образом, понятные причины здесь — оптимизация.


Благодаря такой логике, разработчика ждут взрывающие мозг артефакты отрисовки и баги, когда в самописных редакторах (чаще всего это относится к наследникам EditorWindow) он смешивает использование методов как GUILayout, так и GUI. Например, если где-то используется GUILayoutUtility.GetLastRect() или GUILayoutUtility.GetRect() и результат сохраняется в поле, либо логика завязана на делегатах и колбеках.


Проблемы хорошо лечатся проверкой в критических местах:


if (Event.current.type != EventType.Layout) {
    ...
}

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

Еще в самый первый раз, когда я читал документацию к PropertyDrawer, я ужаснулся работе с прямоугольниками. Они предлагают делать это так:


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

Нередко в туториалах делают вот так (я немного изменил код, чтобы подчеркнуть особенности подхода к работе с прямоугольниками):


// Draw first field
position.width -=200;
EditorGUI.PropertyField (position, property.FindPropertyRelative("level"), GUIContent.none);

// Shift rect and draw second field
position.x = position.width + 20;
position.width = position.width - position.width - 10;
EditorGUI.PropertyField (position, property.FindPropertyRelative("type"), GUIContent.none );

Следует заметить, что в тот момент я еще не знал про возможностях GUILayout. Представляете мой ужас, когда я решил, что это единственный способ рисовать UI, который может предложить Unity?


Прошли годы, я написал немало интерфейсов используя как GUI, так и GUILayout, но так и не смог смириться с этой ситуацией. В своей следующей статье я собираюсь рассказать, как я перестал беспокоиться и начал резать прямоугольники правильно.


Договоримся о терминах


  • В статье я использую выражение стандартный инспектор, имея в виду стандартный механизм отрисовки полей в инспекторе (стандартный Editor);
  • я также использую выражение базы данных, имея в виду ScriptableObject, хотя весь последующий текст также применим и к MonoBehaviour;
  • говоря GUI, я не делаю различия между классами GUI и EditorGUI; с GUILayout та же история.

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


Когда новичок в Unity хочет вынести в инспектор те или иные настройки, он просто делает поле публичным добавляет к полю [SerializeField] и радуется простоте и удобству редактора. Но когда его база данных разрастется, его ждет суровая реальность: стандартный инспектор ужасен.


Плюсы:


  • Просто добавь воды атрибут (в лучших традициях POJO);
  • Программист больше не тратит драгоценные человеко-часы в попытках убедить ГД в том, что ему не нужно выносить в базу данных ту или иную фичу (знаю на собственном опыте с json базами данных на самописных движках).

Минусы:


  • необходимо постоянно разворачивать вложенные блоки в Инспекторе;
  • невозможно окинуть взглядом всю базу данных, что создает множество проблем в моменты "А не забыл ли я чего?" и "Нужно в последний раз все перепроверить!";
  • информация в таком виде воспринимается как сплошной текст, глазу не за что зацепиться, иногда сложно отделить все свойства одного объекта от свойств другого (частично лечится использованием декораторов вроде [Header], [Range] и т.д.);
  • пространство используется нерационально: под булево значение отводится столько же места, сколько и под строковое.

Немного боли

Наверное, каждый программист встречался c проблемой: добавил [SerializeField], а в инспекторе ничего не появилось? Вся магия в том, что класс поля обязательно должен иметь атрибут [Serializable], что далеко не всегда приходит в голову.


Для наглядности рассмотрим простую базу данных героев популярной саги (содержит наиболее важные характеристики персонажей):


Этот ужасный стандартный инспектор
using System;
using UnityEngine;

[CreateAssetMenu]
public class SimplePeopleDatabase : ScriptableObject {
    [SerializeField]
    private Human[] people;

    [Serializable]
    public class Human {
        [SerializeField]
        private string name;
        [SerializeField]
        private Gender gender;
        [SerializeField, Tooltip("Общее количество ушей")]
        private int earedness;
    }

    public enum Gender {
        Undefined = 0,
        Male = 1,
        Female = 2,
    }
}


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


Благо, разработчики Unity делают нам предложение, от которого мы просто не можем отказаться: написать свой редактор. Целый CustomEditor писать не будем, достаточно изменить отрисовку класса Human: пишем HumanPropertyDrawer. Помещаю его в том же файле для простоты изложения, однако в реальных проектах так делать не советую: не раз встречал ситуации, когда при достаточном уровне усталости вечером в пятницу в гит попадает using UnityEditor;, не обернутый в директивы препроцессора.


Красивый вариант базы данных
using System;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif

[CreateAssetMenu]
public class SimplePeopleDatabaseWithCustomEditor : ScriptableObject {
    [SerializeField]
    private Human[] people;

    [Serializable]
    public class Human {
        [SerializeField]
        private string name;
        [SerializeField]
        private Gender gender;
        [SerializeField, Tooltip("Общее количество ушей")]
        private int earedness;
    }

    public enum Gender {
        Undefined = 0,
        Male = 1,
        Female = 2,
    }

#if UNITY_EDITOR
    [CustomPropertyDrawer(typeof(Human))]
    public class HumanPropertyDrawer : PropertyDrawer {

        private const float space = 5;

        public override void OnGUI(Rect rect, 
                                   SerializedProperty property, 
                                   GUIContent label) {
            int indent = EditorGUI.indentLevel;
            EditorGUI.indentLevel = 0;

            var firstLineRect = new Rect(
                x: rect.x,
                y: rect.y,
                width: rect.width,
                height: EditorGUIUtility.singleLineHeight
            );
            DrawMainProperties(firstLineRect, property);

            EditorGUI.indentLevel = indent;
        }

        private void DrawMainProperties(Rect rect,
                                        SerializedProperty human){
            rect.width = (rect.width - 2*space) / 3; 
            DrawProperty(rect, human.FindPropertyRelative("name"));
            rect.x += rect.width + space;
            DrawProperty(rect, human.FindPropertyRelative("gender"));
            rect.x += rect.width + space;
            DrawProperty(rect, human.FindPropertyRelative("earedness"));
        }

        private void DrawProperty(Rect rect, 
                                  SerializedProperty property){
            EditorGUI.PropertyField(rect, property, GUIContent.none);
        }
    }
#endif
}


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


Сложность поддержки кода приводит к его инертности: каждый раз, когда базу данных нужно будет "немножко" изменить, возникнет множество проблем с разъехавшимися позициями, изменившимися именами полей и т.д. Когда в коде появляется рефлексия, о рефакторинге кода можно просто забыть: стандартные средства IDE уже не справляются. Появившиеся ошибки не выявляются на этапе компиляции, и чтобы заметить их, необходимо, чтобы кто-то заглянул в эту базу данных в инспекторе.


Плюсы:


  • никаких вложенных блоков и лишних кликов;
  • размер базы данных существенно сократился, пространство используется рационально;
  • каждый объект в базе данных имеет смысловое отделение от остальных;
  • невероятная гибкость подхода — программист может подстроиться к любым изменениям и сделать базу сколь угодно удобной.

Минусы:


  • небходимость писать дополнительный код;
  • много проблем с поддержкой кода редактора;
  • из-за сложностей с поддержкой редактора рабочий код загнивает;
  • пропали подсказки с полей (те, которые с атрибута [Tootip]), чего никто не ожидал.

Все минусы первого варианта полностью разрешены, однако от его плюсов не осталось и следа.


Просто для примера добавим каждому персонажу еще одно поле 'Питомцы'
using System;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif

[CreateAssetMenu]
public class PeopleDatabaseWithCustomEditor : ScriptableObject {
    [SerializeField]
    private Human[] people;

    [Serializable]
    public class Human {
        [SerializeField]
        private string name;
        [SerializeField]
        private Gender gender;
        [SerializeField, Tooltip("Общее количество ушей")]
        private int earedness;
        [SerializeField]
        private Pet[] pets;
    }

    public enum Gender {
        Undefined = 0,
        Male = 1,
        Female = 2,
    }

    [Serializable]
    public class Pet {
        [SerializeField]
        private PetType type;
        [SerializeField]
        private string name;
        [SerializeField, Tooltip("Может ли быть использован в бою?")]
        private bool combat;
    }

    public enum PetType {
        Undefined = 0,
        Wolf = 1,
        Dragon = 2,
        Raven = 3,
    }

#if UNITY_EDITOR
    [CustomPropertyDrawer(typeof(Human))]
    public class HumanPropertyDrawer : PropertyDrawer {

        private const float space = 5;

        public override float GetPropertyHeight(SerializedProperty property, GUIContent label){
            return EditorGUIUtility.singleLineHeight + 
                   EditorGUI.GetPropertyHeight(property.FindPropertyRelative("pets"), null, true);
        }

        public override void OnGUI(Rect rect, 
                                   SerializedProperty property, 
                                   GUIContent label) {
            int indent = EditorGUI.indentLevel;
            EditorGUI.indentLevel = 0;

            var firstLineRect = new Rect(
                x: rect.x,
                y: rect.y,
                width: rect.width,
                height: EditorGUIUtility.singleLineHeight
            );
            DrawMainProperties(firstLineRect, property);

            var petsRect = new Rect(
                x: rect.x,
                y: rect.y + firstLineRect.height,
                height: rect.height - firstLineRect.height,
                width: rect.width
            );
            DrawPets(petsRect, property.FindPropertyRelative("pets"));

            // Separator
            var lastLine = new Rect(
                x: rect.x,
                y: rect.y + rect.height - EditorGUIUtility.singleLineHeight / 2,
                width: rect.width,
                height: 1
            );
            EditorGUI.DrawRect(lastLine, Color.gray);

            EditorGUI.indentLevel = indent;
        }

        private void DrawMainProperties(Rect rect,
                                        SerializedProperty human){
            rect.width = (rect.width - 2*space) / 3; 
            DrawProperty(rect, human.FindPropertyRelative("name"));
            rect.x += rect.width + space;
            DrawProperty(rect, human.FindPropertyRelative("gender"));
            rect.x += rect.width + space;
            DrawProperty(rect, human.FindPropertyRelative("earedness"));
        }

        private void DrawProperty(Rect rect, 
                                  SerializedProperty property){
            EditorGUI.PropertyField(rect, property, GUIContent.none);
        }

        private void DrawPets(Rect rect, SerializedProperty petsArray){
            var countRect = new Rect(
                x: rect.x,
                y: rect.y,
                height: EditorGUIUtility.singleLineHeight,
                width: rect.width
            );
            var label = new GUIContent("Pets");
            petsArray.arraySize = EditorGUI.IntField(countRect, label, petsArray.arraySize);

            var petsRect = new Rect(
                x: rect.x + EditorGUIUtility.labelWidth,
                y: rect.y,
                width: rect.width - EditorGUIUtility.labelWidth,
                height: 18
            );
            petsArray.isExpanded = true;

            for (int i = 0; i < petsArray.arraySize; i++){
                petsRect.y += petsRect.height;
                EditorGUI.PropertyField(petsRect, petsArray.GetArrayElementAtIndex(i));
            }
            petsArray.isExpanded = true;
        }
    }

    [CustomPropertyDrawer(typeof(Pet))]
    public class PetPropertyDrawer : PropertyDrawer {

        private const float space = 2;

        public override void OnGUI(Rect rect, 
                                   SerializedProperty property, 
                                   GUIContent label) {
            int indent = EditorGUI.indentLevel;
            EditorGUI.indentLevel = 0;

            float combatFlagWidth = 20;
            rect.width = (rect.width - 2*space - combatFlagWidth) / 2; 
            DrawProperty(rect, property.FindPropertyRelative("type"));
            rect.x += rect.width + space;
            DrawProperty(rect, property.FindPropertyRelative("name"));
            rect.x += rect.width + space;
            rect.width = combatFlagWidth;
            DrawProperty(rect, property.FindPropertyRelative("combat"));

            EditorGUI.indentLevel = indent;
        }

        private void DrawProperty(Rect rect, 
                                  SerializedProperty property){
            EditorGUI.PropertyField(rect, property, GUIContent.none);
        }
    }
#endif
}


Хотя результат выглядит неплохо, сложно передать словами все эмоции относительно этого кода. А еще страшней представить, что же будет дальше.


Решение проблемы


Первое, что приходит в голову: кто-то уже сталкивался с такой проблемой, идем искать в AssetStore. И находим в нем множество ассетов, направленных на улучшение стандартного инспектора: они позволяют размещать в инспекторе кнопки (вызывающие методы объекта), превью текстур, добавляют возможность ветвления и многое другое. Но почти все они платные, да и большинство из них следуют парадигме "одно поле — одна линия", а добавляемые ими функции лишь еще сильней увеличивают вертикальные размеры базы данных.


В общем, хватит вступлений, пора рассказать о том, ради чего мы все здесь собрались. Я написал свой ассет: OneLine.


Что делает OneLine? Рисует в одну строку поле, помеченное атрибутом, вместе со всеми вложенными полями (будет понятно на примерах ниже).


Используем OneLine в первом примере

Добавляем атрибут [OneLine] для отрисовки в одну линию и [HideLabel], чтобы спрятать ненужный в нашем случае заголовок, съедающий треть длины строки.


using System;
using UnityEngine;
using OneLine;

[CreateAssetMenu]
public class SimplePeopleDatabaseWithOneLine : ScriptableObject {
    [SerializeField, OneLine, HideLabel]
    private Human[] people;

    [Serializable]
    public class Human {
        [SerializeField]
        private string name;
        [SerializeField]
        private Gender gender;
        [SerializeField, Tooltip("Общее количество ушей")]
        private int earedness;
    }

    public enum Gender {
        Undefined = 0,
        Male = 1,
        Female = 2,
    }
}


Используем OneLine во втором примере с животными

Растягивание полей по ширине в OneLine выглядит странно с динамическими массивами (а список питомцев как раз такой), так что жестко задаем всем полям ширину атрибутом [Width]. В этот раз картинка кликабельна.


using System;
using UnityEngine;
using OneLine;

[CreateAssetMenu]
public class PeopleDatabaseWithOneLine : ScriptableObject {
    [SerializeField, OneLine, HideLabel]
    private Human[] people;

    [Serializable]
    public class Human {
        [SerializeField, Width(130)]
        private string name;
        [SerializeField, Width(75)]
        private Gender gender;
        [SerializeField, Tooltip("Общее количество ушей"), Width(25)]
        private int earedness;
        [SerializeField]
        private Pet[] pets;
    }

    public enum Gender {
        Undefined = 0,
        Male = 1,
        Female = 2,
    }

    [Serializable]
    public class Pet {
        [SerializeField, Width(60)]
        private PetType type;
        [SerializeField, Width(100)]
        private string name;
        [SerializeField, Tooltip("Может ли быть использован в бою?")]
        private bool combat;
    }

    public enum PetType {
        Undefined = 0,
        Wolf = 1,
        Dragon = 2,
        Raven = 3,
    }
}


Получается довольно широко, однако при работе с базой данных, её вполне можно растянуть и на весь экран. В будущих версиях я планирую дать OneLine возможность рисовать слишком большие поля в несколько строк.


Как все это работает? В основном, благодаря возможности написать PropertyDrawer для атрибута. На чем висит атрибут, то Drawer и рисует. Плюс немного рефлексии, помощь декомпилированного кода библиотек Unity на гитхабе и чуточку воображения. Об основных трудностях на пути напишу чуть позже.


Возможности OneLine:


  • Все работает прямо из коробки: добавляем один атрибут на поле и все, что внутри него отрисуется в одну линию;
  • Если "из коробки" выглядит не очень, есть возможность кастомизации (подсветка полей, управления шириной и т.д.);
  • Нормально обрабатыват подсказки ([Tooltip]) в коде;
  • Бесплатный, отзывчивое сообщество (на гитхабе) из одного человека, готовое помочь;

Ограничения OneLine:


  • Если его повесить на массив, он продолжит рисоваться как обычно, а вот элементы его отрисуются каждый в своей линии;
  • Библиотека молодая, не успела обрасти множеством фич, а вот баги ловить еще только предстоит.

Как это работает:


Сначала все казалось просто: рекурсивно пройти по всем полям и нарисовать все, что встретится на пути, в одну линию. В жизни все оказалось сложней:


  • для расчета прямоугольников проходим по графу детей вглубь, определяем у каждого его относительную ширину на основе веса (weight) и дополнительной ширины (width);
  • для этого разбираемся, как из SerializedProperty узнать, что за поле перед нами: лист, узел или массив (что является особым случаем узла). Кстати, вы знали, что SerializedProperty, указывающее на строку, возвращает property.isArray == true? Вот я не знал;
  • заодно разбираемся, как с помощью property.propertyPath и рефлексии добыть все атрибуты, висящие на поле (и снова костыли, связанные с массивами);
  • пробираемся через дебри особых случаев и исключений;
  • приправляем украшениями вроде [HideLabel] и [Highlight];
  • оптимизируем;
  • ...
  • наслаждаемся результатом.

Препятствия на пути


Следует отметить, что многие проблемы на пути являются преградами, которые перед нами ставит сама Unity. В следствии чего, решать проблемы приходится некоторого рода обходными путями, в том числе опираясь на знание реализации тех или иных частей редактора и рефлексию.


Такой подход приводит к хрупкости кода, ведь в следующей версии редактора OneLine может вдруг прекратить работать корректно. С одной стороны, я надеюсь, что этого не случится. С другой стороны, проблемы решать в любом случае нужно, а необходимого для этого открытого API нет и не предвидится.


Пропадающая подсказка

Когда я писал базы данных для художников, я постоянно расставлял развернутые подсказки к полям с помощью [Tooltip], заменяющие нам документацию. С этим атрибутом в инспекторе достаточно навести мышку на поле, и всплывающее облачко все тебе о нем расскажет.


Проблема: как я писал ранее, простой вызов `EditorGUI.PropertyField(rect, property, content) не отрисовывает подсказки. Что интересно, SerializedProperty содержит свойство tootlip (обратите внимание на исчерпывающую документацию), но оно всегда оказывалось пустым, когда я к нему обращался. ЧЯДНТ?


Решение: берем SerializedProperty.propertyPath и с рефлексией в руках ползем по пути с самого начала (обращаем внимание на массивы), когда доползем до конца, можем узнать все атрибуты поля. Этот метод получения атрибутов поля я использую не только для работы с подсказками.


public static T[] GetCustomAttributes<T>(this SerializedProperty property) where T : Attribute {
    string[] path = property.propertyPath.Split('.');

    bool failed = false;
    var type = property.serializedObject.targetObject.GetType();
    FieldInfo field = null;
    for (int i = 0; i < path.Length; i++) {
        field = type.GetField(path[i], BindingFlags.Public 
                                       | BindingFlags.NonPublic
                                       | BindingFlags.DeclaredOnly 
                                       | BindingFlags.FlattenHierarchy
                                       | BindingFlags.Instance);
        type = field.FieldType;

        // Обычное поле выглядит так: .fieldName
        // Элемент массива выглядит так: .fieldName.Array.data[0]
        int next = i + 1;
        if (next < path.Length && path[next] == "Array") {
            i += 2;
            if (type.IsArray) {
                type = type.GetElementType();
            }
            else {
                type = type.GetGenericArguments()[0];
            }
        }
    }

    return field.GetCustomAttributes(typeof(T), false)
                .Cast<T>()
                .ToArray();
}



Моргающий EditorGUI.DrawRect

В большой базе данных длинная линия, содержащая множество полей, может выглядеть слишком "ровной", то есть снова проявляется проблема "не за что глазу зацепиться". Передо мной встала задача обеспечить механизм выделения особо важных полей в линии.
В соответствии с политикой партии, выделение поля будет производить атрибутом [Highlight].


Реализация очень проста:


  • тем же инструментом, что и в случае с подсказкой, находим на поле атрибут [Highlight];
  • перед отрисовкой поля вызываем EditorGUI.DrawRect(rect, color); и подкрашиваем прямоугольник нужным цветом.

Проблема: в результате известного бага Unity (который помечен Won't fix) подсветка то появляется, то пропадает.


Решение: описано здесь. Выглядит просто. Интересно, по какой причине разработчики Unity отказались его чинить?




Не пропадающие декораторы

Основная фишка OneLine в том, что она должна работать из коробки: добавил один атрибут и все рисуется в одну линию. Но жизнь постоянно устраивает нам разного рода подлянки. И одна из них: декораторы. Это атрибуты [Header], [Space] (и любые другие, которые вы можете написать сами, расширяя DecoratorDrawer). Оказалось, что при обычном вызове `EditorGUI.PropertyField(rect, property, content) все декораторы тоже рисуются.


Проблема:


Решение: Сначала я пытался найти обходной путь и даже спрашивал на Unity Answers, но без результата. Тогда я порылся в декомпилированных исходниках UnityEditor.dll (здесь), и получил следующее решение:


typeof(EditorGUI)
     .GetMethod("DefaultPropertyField", BindingFlags.NonPublic | BindingFlags.Static)
     .Invoke(null, new object[]{rect, property, GUIContent.none});

Где скачать


Текущая версия OneLine v0.2.0.
За ситуацией Вы можете следить на гитхабе. В ридми функционал описан более подробно.

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

Подробнее
Реклама
Комментарии 13
  • 0
    Большое спасибо за ваши труды
    • 0
      Что поделаешь… приходится много чего дописывать самому. Без своих и 3-х лиц екстеншенов Юнити почти не стоит ничего — большой катящийся ком, наследие начала развития 3д-движков.
      • 0
        Спасибо за статью и инструмент, обязательно попробую его, как раз стоял вопрос об удобном способе представить базу в редакторе.
        • 0
          Не самый удачный способ по ряду причин(нет подписи у полей, при большом количестве переменных нужных геймдизу линия не будет помещаться на экране). Сам стараюсь делать кастомный эдитор и в каждом классе описывать его GUI. Если кому интересно расскажу.
          • 0

            Согласен с тем, что OneLine не нужно использовать абсолютно везде. Я специально выбрал пример с массивом: он хорошо показывает, как быстро увеличивается длина строки. Однако многим базам данных в играх OneLine придает вполне сносный и удобный вид.


            Мой опыт разработки лежит в области мобильных игр, и на одну базу данных, которая не вместилась бы в экран, приходится несколько мелких, где у каждого элемента лишь 2-3 простых поля.


            К тому же, я думаю о возможности рисовать поле в несколько строк (хоть это и странно с таким названием). Но пока не знаю, как сделать это простым и удобным. К сожалению, не всегда легко найти компромисс между простотой и функциональность.

            • 0
              Очень даже интересно. Не знаю как тут принято, но буду благодарен за уделенное время
            • 0
              Но… зачем? Гораздо проще использовать для этого google sheets (с combobox-ами и листами для выбора, с первичной проверкой данных), а из редактора сливать их через csv / json. Там и комментарии можно прикрутить и все, что угодно, включая сборку в один список из нескольких листов.
              • 0
                Каким образом в Google Sheets Вы проставите ссылки на наследников UnityEngine.Object вроде префабов, шейдеры, анимации и т.д?

                Вещи, вроде списков строк для локализаций, вполне удобно импортировать из гуглтаблиц, но ими мир баз данных в разработке игр не ограничивается.
                • 0
                  А в чем проблема? Дизайнер вообще не должен напрямую что-то указывать и создавать в проекте — это гарантированно ведет к появлению статей «как почистить проект, чтобы он не занимал столько места». Дизайнеру достаточно оперировать сущностями, которые он может выбрать из фиксированного списка через combobox в гугло-таблицах. Эти списки забиваются программером после реализации их в проекте, у дизайнера просто нет доступа на запись в них.
                  Примеры:
                  habrastorage.org/webt/59/ec/b4/59ecb40202b82038774726.png
                  habrastorage.org/webt/59/ec/b4/59ecb4e1755d1237482621.png
                  Это все дампится в csv, потом прокатывается скриптом в редакторе и гонится в json. Первая строка определяет имя поля, вторая — тип кавычек вокруг, либо игнорирование поля (например, комментарии).
                  • 0
                    Когда мы писали на самописном движке, мы также работали через гугл доки. Но какой смысл выносить разработку из Unity в браузер? Только чтобы оградить мозг геймдизайнера от базовых знаний о редакторе Unity?

                    Я работал в команде, где художники хотели стать лучше и учились. И они разбирались в том, кто такие шейдеры, и зачем нужны батчинг мешей и спрайт пакер.

                    Хотя я разрабатывал в небольшой команде из 15 человек, и даже там (и в соседних проектах) встречались члены, не желающие учиться и ломающие процесс разработки (и увеличивающие размеры билдов, если уж на то пошло). Но с единичными случаями обломовщины лучше бороться иными способами, чем просто «посадить всех не-программистов» в песочницу и пусть там копошатся).

                    UPD: не ответил на вопрос «в чем проблема»: проблема в опечатках (в Ваших примерах столбцы Name, Description, Image, Script — не выпадающие списки, а строки, набранные от руки) и постоянной синхронизации дока и скрипта.
                    • 0
                      К сожалению, чем дальше использую юнити, тем больше убеждаюсь, что чем меньше используется ее средств — тем лучше. Внутренние форматы могут меняться, апи может меняться — все это ведет к некорректному поведению тулзов (необходимости постоянно мониторить, что отпало в новом апдейте юнити) и потенциальной потере данных. Поэтому чем больше внешних данных, которые можно перегонять простейшими тулзами без UI в те же собственные текстовые / бинарные форматы внутри юнити для ручной загрузки — тем лучше / стабильнее оно работает (пайплайн переносим между проектами / версиями юнити без боли).
                      проблема в опечатках (в Ваших примерах столбцы Name, Description, Image, Script — не выпадающие списки

                      Поля в данном случае забиты не дизайнером и фиксированы как заголовки + все данные проверяются в editor-скрипте импорта с быстрым остановом на любой ошибке и дампом в лог. Смысл в том, чтобы дать инструмент для модификации контента вне юнити и не парить никому мозг с потенциально некорректным поведением кастомных инспекторов в юнити / неработающим undo и т.п спецификой. Внешние редакторы дают воспроизводимость данных, в случае google sheets — откат каждой операции с логированием в истории.
              • 0
                он просто делает поле публичным добавляет к полю [SerializeField]
                Кстати об этом. Официальные доки юнити по поводу SerializeField пишут «You will almost never need this.», как бы намекая нам, что движок предполагает ненормальное программирование.

                GUILayout чтобы рассчитать размеры рисуемых полей, вызывает метод OnGUI дважды
                Это еще что. Совершенно замечательная ситуация вырисовывается, когда нужно сделать перекрывающиеся элементы интерфейса (например панели). А порядок отрисовки и проверки событий ввода у виджетов в юнити совпадает, следовательно первым обработает событие виджет который на самом дальнем плане.
                • 0
                  Тоже не раз сталкивался с порядком отрисовки в интерфейсах Unity. С какого-то момента я просто перестал думать «Сейчас быстренько сделаю эту фичу», а перешел к более пессимистичному «Интересно, что нового я узнаю об особенностях Unity?»

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