Форум программистов, компьютерный форум, киберфорум
Unity, Unity3D
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  
 
Рейтинг 4.88/40: Рейтинг темы: голосов - 40, средняя оценка - 4.88
 Аватар для ImAlexSmith
325 / 114 / 7
Регистрация: 01.05.2011
Сообщений: 283
Записей в блоге: 3

Оцените. Кастомные редакторы, которые мы используем в игре

17.02.2018, 21:58. Показов 8098. Ответов 2

Студворк — интернет-сервис помощи студентам
Unity - сказочный движок. Иногда эта сказка добрая, про пони и радугу. А иногда - про страшного бабайку. И очень важная часть данного движка, а именно расширения для редактора (custom inspectors) - как раз про бабайку. Ниже я бы хотел поделиться опытом использования собственных расширений на примере нашей игры. Буду рад если кому-то поможет или кто-нибудь даст дельное замечание.

В нашей, казалось бы, небольшой головоломке используется 22 собственных скрипта для расширений на ~2 500 строк кода в сумме. Приведу особо интересные примеры для каждого типа расширений (UnityEditor.PropertyDrawer, UnityEditor.EditorWindow, UnityEditor.Editor).

PropertyDrawer

Здесь, как раз, ничего сильно интересного у нас нет. Просто для примера - отображение свойства неактивным, если объект на сцене. И возможность редактировать свойство, если объект является префабом.

code
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//Скрипт в папке Editor
[CustomPropertyDrawer(typeof(ReadOnlyIfNotPersistentAttribute))]
public class ReadOnlyIfNotPersistentPropertyDrawer : PropertyDrawer
{
    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        return EditorGUI.GetPropertyHeight(property, label, true);
    }
 
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        if (EditorUtility.IsPersistent(property.serializedObject.targetObject))
        {
            EditorGUI.PropertyField(position, property, label, true);
        }
        else
        {
            GUI.enabled = false;
            EditorGUI.PropertyField(position, property, label, true);
            GUI.enabled = true;
        }
    }
}
 
//Использование в игровом скрипте
[AttributeUsage(AttributeTargets.Field)]
public class ReadOnlyIfNotPersistentAttribute : PropertyAttribute { }
 
public abstract class InteractableBase : MonoBehaviour
{
   [ReadOnlyIfNotPersistent]
   public string Identifier;
}


Эта функция используется для свойств-идентификаторов объектов. Что-то типа ИД, необходимого для сериализации/десериализации структуры уровня. Защита от самого себя, чтобы не поменять идентификатор во время сборки уровня в редакторе. Если так сделать - уровень сохранится, но загружаться не будет.

(см. изображение 1)

Еще есть PropertyDrawer для класса “Координаты X и Y” - вместо двух строчек выводит в одну. Еще один - для отображения коллекции string в виде выпадающего списка.

(см. изображение 2)

EditorWindow

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

Первый - получение картинки-превью префаба. Т.е. той картинки, которая отображается в самом редакторе Unity. Реализуется с помощью одного метода AssetPreview.GetAssetPreview(gameObject) и возвращает объект Texture2D, который может быть использован в качестве контента для GUILayout.Button.

C#
1
2
3
var buttonContent = new GUIContent(AssetPreview.GetAssetPreview(GOpref), GOpref.name);
if (GUILayout.Button(buttonContent, GUILayout.Width(ListButtonSize), GUILayout.Height(ListButtonSize)))
                            newCell = Instantiate(GOpref);
Второй - центрирование камеры в SceneView к выбранной ячейке. Здесь три метода.
C#
1
2
3
Selection.activeGameObject = cell;
SceneView.lastActiveSceneView.AlignViewToObject(cell.transform);
SceneView.lastActiveSceneView.rotation = Quaternion.identity;
(см. изображение 3)
А вот так это выглядит в движении:

Editor

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

Полный код редактора:

code
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
public abstract class CustomEditorBase<T> : Editor where T : Object
{
    // =======
    protected const float InfoboxScrollHeight = 90f;
    protected const float MessageIconWidth = 36f;
 
    public bool ShowMessage = true;
    public string ErrorMessage;
    public string WarningMessage;
    public string InformationMessageHeader = "Information:";
    public string InformationMessage;
 
    protected List<T> Targets = new List<T>();
    protected T Target
    {
        get { return Targets[0]; }
        set { Targets[0] = value; }
    }
 
    private Vector2 _infoboxScroll;
    private float _infoboxMessageHeight;
    private float _infoboxViewportWidth;
    private Texture2D _errorIcon;
    private Texture2D _warningIcon;
 
 
    // =======
    protected virtual void OnEnable()
    {
        Targets = serializedObject.targetObjects.Cast<T>().ToList();
        _errorIcon = (Texture2D)EditorGUIUtility.Load("icons/d_console.erroricon.png");
        _warningIcon = (Texture2D)EditorGUIUtility.Load("icons/d_console.warnicon.png");
    }
 
    public override void OnInspectorGUI()
    {
        GUI.enabled = false;
        EditorGUILayout.PropertyField(serializedObject.FindProperty("m_Script"), true);
        GUI.enabled = true;
 
        if (!ShowMessage) return;
 
        _infoboxScroll = EditorGUILayout.BeginScrollView(_infoboxScroll, GUI.skin.box, GUILayout.Height(InfoboxScrollHeight));
 
        if (!string.IsNullOrEmpty(ErrorMessage))
        {
            _infoboxMessageHeight = EditorStyles.wordWrappedLabel.CalcHeight(new GUIContent(ErrorMessage), _infoboxViewportWidth);
 
            EditorGUILayout.BeginHorizontal();
            GUILayout.Label(_errorIcon, GUILayout.Width(MessageIconWidth));
            EditorGUILayout.SelectableLabel(ErrorMessage, CES.RichWordWrappedLabelStyle, GUILayout.MinHeight(_infoboxMessageHeight));
            if (Event.current.type == EventType.Repaint)
                _infoboxViewportWidth = GUILayoutUtility.GetLastRect().width;
            EditorGUILayout.EndHorizontal();
        }
        else if (!string.IsNullOrEmpty(WarningMessage))
        {
            _infoboxMessageHeight = EditorStyles.wordWrappedLabel.CalcHeight(new GUIContent(WarningMessage), _infoboxViewportWidth);
 
            EditorGUILayout.BeginHorizontal();
            GUILayout.Label(_warningIcon, GUILayout.Width(MessageIconWidth));
            EditorGUILayout.SelectableLabel(WarningMessage, CES.RichWordWrappedLabelStyle, GUILayout.MinHeight(_infoboxMessageHeight));
            if (Event.current.type == EventType.Repaint)
                _infoboxViewportWidth = GUILayoutUtility.GetLastRect().width;
            EditorGUILayout.EndHorizontal();
        }
        else
        {
            _infoboxMessageHeight = EditorStyles.wordWrappedLabel.CalcHeight(new GUIContent(InformationMessage), _infoboxViewportWidth);
 
            EditorGUILayout.BeginVertical();
            EditorGUILayout.LabelField(InformationMessageHeader, EditorStyles.boldLabel);
            EditorGUILayout.SelectableLabel(InformationMessage, CES.RichWordWrappedLabelStyle, GUILayout.MinHeight(_infoboxMessageHeight));
            if (Event.current.type == EventType.Repaint)
                _infoboxViewportWidth = GUILayoutUtility.GetLastRect().width;
            EditorGUILayout.EndVertical();
        }
 
        EditorGUILayout.EndScrollView();
 
        ErrorMessage = "";
        WarningMessage = "";
        InformationMessage = "";
    }
}
 
//Использование в OnInspectorGUI дочернего класса
public override void OnInspectorGUI()
{
   base.OnInspectorGUI();
   serializedObject.Update();
   InformationMessage += "Build new level with given options.\nThe left-bottom cell has coordinates (0,0).";
   serializedObject.ApplyModifiedProperties();
}


Кстати, текст вверху эдитора можно выбрать мышкой и копировать, благодаря SelectableLabel.
Дальше от него наследуются все остальные кастомные эдиторы. Самые “толстые” у нас - это ContentPackEditor, LevelEditor, LocalizationEditor, GameFieldEditor. Каждый от 200 до 300 строк кода. Опять же, приводить код каждого смысла не имеет. Расскажу только про интересные моменты.

Для ContentPackEditor нужен был функционал визуальной сортировки элементов массива. И такой функционал в Unity есть, это ReorderableList в UnityEditorInternal. Вот статья по основам использования http://va.lent.in/unity-make-y... rablelist/
От себя я добавил сворачивание/разворачивание списка при клике на заголовок.

code
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
private const float ToggleWidth = 13f;
 
    private static bool _showHeroes = true;
 
    private SerializedProperty _heroesProperty;
 
    private ReorderableList _heroesList;
 
    // =======
    private static void MakeHeaderFoldout(ReorderableList list, Rect rect, ref bool value)
    {
        var e = Event.current;
        if (e.type == EventType.Repaint)
        {
            EditorStyles.foldout.Draw(new Rect(rect.x, rect.y, ToggleWidth, rect.height), false, false, value, false);
        }
 
        if (e.type == EventType.MouseDown && rect.Contains(e.mousePosition))
        {
            value = !value;
            SwitchFoldout(list, value);
            e.Use();
        }
    }
 
    private static void MakeHeaderLabel(ReorderableList list, Rect rect)
    {
        EditorGUI.LabelField(new Rect(rect.x + ToggleWidth, rect.y, rect.width - ToggleWidth, rect.height),
            new GUIContent(string.Format("[{0}] {1}", list.count, list.serializedProperty.name), ""),
            EditorStyles.boldLabel);
    }
 
    private static void SwitchFoldout(ReorderableList list, bool value)
    {
        list.displayAdd = value;
        list.displayRemove = value;
        list.draggable = value;
        list.elementHeight = value ? EditorGUIUtility.singleLineHeight : 0f;
        list.footerHeight = value ? EditorGUIUtility.singleLineHeight : 0f;
    }
 
    protected override void OnEnable()
    {
        base.OnEnable();
 
        _heroesProperty = serializedObject.FindProperty("Heroes");
       
        _heroesList = new ReorderableList(serializedObject, _heroesProperty, true, true, true, true);
        _heroesList.drawElementCallback += (rect, index, active, focused) =>
        {
            if (_showHeroes)
                EditorGUI.PropertyField(rect, _heroesProperty.GetArrayElementAtIndex(index));
        };
        _heroesList.drawHeaderCallback += rect =>
        {
            MakeHeaderFoldout(_heroesList, rect, ref _showHeroes);
            MakeHeaderLabel(_heroesList, rect);
        };
        SwitchFoldout(_heroesList, _showHeroes);
    }


(см. изображение 4)

В LocalizationEditor надо было сделать что-то вроде таблицы, чтобы четные и нечетные сроки были разным цветом. Решается это заведением двух разных стилей, примерно так:

code
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public static class CustomEditorStyles
{
    public static Texture2D MakeColoredTexture(int width, int height, Color color)
    {
        var pix = new Color[width * height];
 
        for (int i = 0; i < pix.Length; i++)
            pix[i] = color;
 
        var result = new Texture2D(width, height);
        result.SetPixels(pix);
        result.Apply();
 
        return result;
    }
 
    private static GUIStyle _oddListStyle;
    public static GUIStyle OddListStyle
    {
        get
        {
            if (_oddListStyle == null)
            {
                _oddListStyle = new GUIStyle(GUI.skin.box);
                _oddListStyle.margin = new RectOffset(0, 0, 0, 0);
                _oddListStyle.padding = new RectOffset(2, 2, 4, 4);
                _oddListStyle.normal.background = MakeColoredTexture(1, 1, new Color(0.87f, 0.92f, 0.95f));
            }
 
            return _oddListStyle;
        }
    }
 
    private static GUIStyle _evenListStyle;
    public static GUIStyle EvenListStyle
    {
        get
        {
            if (_evenListStyle == null)
            {
                _evenListStyle = new GUIStyle(GUI.skin.box);
                _evenListStyle.margin = new RectOffset(0, 0, 0, 0);
                _evenListStyle.padding = new RectOffset(2, 2, 4, 4);
                _evenListStyle.normal.background = MakeColoredTexture(1, 1, new Color(0.67f, 0.72f, 0.75f));
            }
 
            return _evenListStyle;
        }
    }
}
 
//Использование в кастомном редакторе
public override void OnInspectorGUI()
{
   for (int i = 0; i < entriesList.Count; i++)
   {
      EditorGUILayout.BeginHorizontal(i % 2 == 0 ? CustomEditorStyles.EvenListStyle : CustomEditorStyles.OddListStyle);
      // бла бла бла, рисование строчки таблицы
      EditorGUILayout.EndHorizontal();
   }
}


Вторым проблемным моментом было сделать поиск в уже добавленных ключах локализации и мгновенный вывод результатов в виде выпадающего списка под строкой поиска. Вот для этого момента найти решение было действительно непросто. В итоге нашелся метод на каком-то китайском сайте с иероглифами. Хорошо, что код - он и в африке код. http://www.clonefactor.com/wordpress/public/1769/

Путем небольшой доработки напильником стало выглядеть все вот так:

Собственно на этом все.
В качестве вывода могу сказать, что расширения для редактора Unity - это очень и очень мощная штука. Пользоваться ей однозначно стоит. Правда перед этим нужно запастись терпением и навыком гуглить информацию на иностранных языках. Ну и картинка со всеми более менее интересными расширениями (см. изображение 5)
Задавайте вопросы, критикуйте - буду рад обсудить
Миниатюры
Оцените. Кастомные редакторы, которые мы используем в игре   Оцените. Кастомные редакторы, которые мы используем в игре   Оцените. Кастомные редакторы, которые мы используем в игре  

Оцените. Кастомные редакторы, которые мы используем в игре   Оцените. Кастомные редакторы, которые мы используем в игре  
1
IT_Exp
Эксперт
34794 / 4073 / 2104
Регистрация: 17.06.2006
Сообщений: 32,602
Блог
17.02.2018, 21:58
Ответы с готовыми решениями:

Какие вы знаете бесплатные визуальные редакторы, которые не тормозят работу сайта?
Пробовал Page Builder, сайты на нем жутко тормозят - каждая страница загружается по 2.5-3 секунды, даже когда на ней совсем немного...

Есть ли какие-нибудь редакторы HTML, которые позваляют без всяких заморочек привязывать БД к страницам?
Есть ли какие-нибудь редакторы HTML, которые позваляют без всяких заморочек привязывать БД к страницам? На основе ASP, PHP, Perl и т.д.

Оцените макет диалогового окна в игре
Мотороллер не мой, я просто разместил объяву знакомый дизайнер хочет получить объективную критику на свою работу окно покупки...

2
58 / 57 / 15
Регистрация: 15.09.2012
Сообщений: 557
18.02.2018, 21:33
ImAlexSmith, похвально, что вы делитесь с участниками форума подобного рода информацией. Нет спора, что эти все расширения для вашего проекта полезны, но решая делать или не делать какое то расширения - нужно взвесить, востребовано ли оно насколько, что окупит время на внедрение этого расширения, не дублирует ли функционал уже предоставляемый движком, намного ли облегчит работу с проектом, кроме того новым участникам проекта даже хорошо знающим движок нужно будет еще вникнуть в особенности работы с расширением.
При обновлении версии движка расширения может поломаться, может от неправильной роботы пропасть ссылки назначенные через расширения итд) В общем случае все эти расширения ведут к еще большей зависимости проекта от движка, что считаю не есть хорошо.
Посмотрев все эти окошка из кучей ссылок, всегда вспоминаю ситуацию, когда после обновления движка или не вовремя закоммиченого проекта и вылета в движке - могут слетать эти ссылки и потом, если проект большой приходится все вспоминать.
По этому мое мнения, особенно для больших проектов - минимум связей между обьектами делать через инспектор и всякие расширения, делая эти связи кодом с правильной архитектурой проект становится намного гибче, более устойчив к обновлению движка и при любом сбое логи все прекрасно показывают.
0
 Аватар для ImAlexSmith
325 / 114 / 7
Регистрация: 01.05.2011
Сообщений: 283
Записей в блоге: 3
19.02.2018, 12:20  [ТС]
Цитата Сообщение от ASDFD12 Посмотреть сообщение
При обновлении версии движка расширения может поломаться
Такого не было с собственными расширениями) Было с инспектором Anima2D. Но как раз умение в инспекторы помогло поправить баг за пару минут. А без сторонних дополнений (с собственными инспекторами почти у каждого) особо каши не сваришь.

Цитата Сообщение от ASDFD12 Посмотреть сообщение
В общем случае все эти расширения ведут к еще большей зависимости проекта от движка, что считаю не есть хорошо.
Наши взгляды в данном вопросе в корне расходятся. Я считаю, что нет ничего плохого в зависимости от движка, конкретно Юнити. Как раз в силу его хорошей расширяемости и достойного Asset Store. Плюс... А как сделать игру, и не зависеть от движка?)

Цитата Сообщение от ASDFD12 Посмотреть сообщение
вылета в движке - могут слетать эти ссылки и потом, если проект большой приходится все вспоминать.
Бывало такое. Если не сохранить проект - все изменения в ScriptableObject слетали. Решается более частным сохранением проекта, что тоже полезная привычка

Цитата Сообщение от ASDFD12 Посмотреть сообщение
По этому мое мнения, особенно для больших проектов - минимум связей между обьектами делать через инспектор и всякие расширения, делая эти связи кодом с правильной архитектурой проект становится намного гибче, более устойчив к обновлению движка и при любом сбое логи все прекрасно показывают.
Да не факт, на самом деле... Тут слишком большое разнообразие возможных ситуаций, чтобы утверждать однозначно. В каждой ситуации нужно принимать решение индивидуально, исходя из потребностей и возможностей.
0
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
BasicMan
Эксперт
29316 / 5623 / 2384
Регистрация: 17.02.2009
Сообщений: 30,364
Блог
19.02.2018, 12:20
Помогаю со студенческими работами здесь

Как открыть файл которые используется в html5 игре
Файл из игры на html5.Нашёл в интернете интересный файл,но ничем не могу открыть.Файл с расширение rsx .Но профильной программой он не...

Используем IE )
Всем привет , нужно решить проблему , есть ie , нужно переместится в окне на нужную позицию ( как бы пролистать скроллингом как вниз...

Используем Heap
В этой задаче от вас требуется реализовать структуру данных heap. Ваша программа должна обрабатывать следующие запросы. «1» — сделать...

зачем мы используем цикл?
вот код: program st; var s,s1: string; i,k: integer; kol: integer; begin cls; readln(s); s:=s+' ';

Математический пример, используем if, switch
Задание: x*x Если 0&lt;=x&lt;=3 f(x)= { 4 В противном случае #include &quot;stdafx.h&quot; ...


Искать еще темы с ответами

Или воспользуйтесь поиском по форуму:
3
Ответ Создать тему
Новые блоги и статьи
SDL3 для Web (WebAssembly): Реализация движения на Box2D v3 - трение и коллизии с повёрнутыми стенами
8Observer8 20.02.2026
Содержание блога Box2D позволяет легко создать главного героя, который не проходит сквозь стены и перемещается с заданным трением о препятствия, которые можно располагать под углом, как верхнее. . .
Конвертировать закладки radiotray-ng в m3u-плейлист
damix 19.02.2026
Это можно сделать скриптом для PowerShell. Использование . \СonvertRadiotrayToM3U. ps1 <path_to_bookmarks. json> Рядом с файлом bookmarks. json появится файл bookmarks. m3u с результатом. # Check if. . .
Семь CDC на одном интерфейсе: 5 U[S]ARTов, 1 CAN и 1 SSI
Eddy_Em 18.02.2026
Постепенно допиливаю свою "многоинтерфейсную плату". Выглядит вот так: https:/ / www. cyberforum. ru/ blog_attachment. php?attachmentid=11617&stc=1&d=1771445347 Основана на STM32F303RBT6. На борту пять. . .
Камера Toupcam IUA500KMA
Eddy_Em 12.02.2026
Т. к. у всяких "хикроботов" слишком уж мелкий пиксель, для подсмотра в ESPriF они вообще плохо годятся: уже 14 величину можно рассмотреть еле-еле лишь на экспозициях под 3 секунды (а то и больше),. . .
И ясному Солнцу
zbw 12.02.2026
И ясному Солнцу, и светлой Луне. В мире покоя нет и люди не могут жить в тишине. А жить им немного лет.
«Знание-Сила»
zbw 12.02.2026
«Знание-Сила» «Время-Деньги» «Деньги -Пуля»
SDL3 для Web (WebAssembly): Подключение Box2D v3, физика и отрисовка коллайдеров
8Observer8 12.02.2026
Содержание блога Box2D - это библиотека для 2D физики для анимаций и игр. С её помощью можно определять были ли коллизии между конкретными объектами и вызывать обработчики событий столкновения. . . .
SDL3 для Web (WebAssembly): Загрузка PNG с прозрачным фоном с помощью SDL_LoadPNG (без SDL3_image)
8Observer8 11.02.2026
Содержание блога Библиотека SDL3 содержит встроенные инструменты для базовой работы с изображениями - без использования библиотеки SDL3_image. Пошагово создадим проект для загрузки изображения. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru