Scene View в Unity3D является одним из самых необходимых элементов интерфейса. Каждый, кто хоть раз запускал Unity3D пользовался Scene View для визуальной расстановки объектов на сцене, а также для их настройки. Расширение функционала Scene View может понадобиться для создания собственного редактора уровней, редактирования mesh’а, создания собственных gizmos и много другого. Стоит заметить, что при использовании Terrain в вашем проекте, его редактирование (рисование текстур, изменение высот, а также посадка деревьев и растительности) осуществляется при помощи Scene View.

Для того, чтобы иметь возможность писать скрипты работающие в Scene view в первую очередь класс с которым вы работаете должен быть унаследован от Editor или EditorWindow, что подразумевает подключение namespace UnityEditor. Это дает доступ к нескольким «магическим» методам Unity3D, таким как OnGUI() и OnSceneGUI(). Метод OnSceneGUI дает возможность Editor'у управлять событиями Scene View.


Небольшое лирическое отступление. Среди разработчиков бытует мнение, что «магические» методы (Update, Start, OnSceneGUI и другие) реализованы по средствам System.Reflection в C#, однако есть информация, что это не так, и за их работу стоит благодарить C++ ядро Unity3D. В целом статья будет полезна всем, кто грешит множественными Update() в своем коде.

Вернемся к перехвату событий. Идея заключается в том, чтобы обозначить объект, для которого мы разрабатываем функционал, как элемент интерфейса Unity, на равне с Label или Button из IMGUI.

В api существует класс Event, который используется для различных GUI событий, например нажатия клавиш, кнопок мыши, событий рендеринга и layout’а Unity.

Помимо Event для перехвата кликов понадобится такая вещь как control. В качестве control в Unity может выступать любой элемент IMGUI. К примеру Button или Label. Для того, чтобы ваш объект перехватывал клик, необходимо сказать Unity, что он тоже является control и собирается участвовать в обработке событий. Для этого воспользуемся классом GUIUtility.
Каждый control должен обладать собственным уникальным id, с помощью которого Unity получит всю необходимую информацию о нем. id представляет собой int, однако стоит заметить, что он должен быть уникальным. Вы можете сгенерировать случайное число (отличное от 0) и считать его id вашего control, но при этом нет гарантии, что в системе уже нет control с таким же id, и в таком случае клик уйдет не к тому элементу, на который вы рассчитывали. Для создания уникального id используем метод из класса GUIUtility
public static int GUIUtility.GetControlID(FocusType focus) 

Параметр, принимаемый на вход этим методом отвечает за возможность control получать какой-либо ввод с клавиатуры. Информацию о нем, можно получить в документации. Так как ввод с клавиатуры в случае использовании Scene View не нужен, подойдет значение FocusType.Passive.
 int controlId = GUIUtility.GetControlID(FocusType.Passive); //получение уникального id для нашего элемента интерфейса.

Теперь, когда мы разобрались с получением id, настало время разобраться с перехватом событий. Мы можем получить информацию о текущем событии с помощью свойства Event.current. Чтобы получить тип события, которое произошло в Unity нужно сделать следующее
int controlId = GUIUtility.GetControlID(FocusType.Passive);
Event.current.GetTypeForControl(controlId)?

Данный вызов вернет значение типа EventType для нашего control. Далее нужно только определиться какие именно события вам нужны и перехватить их. Непосредственно перехват события осуществляется следующим образом
GUIUtility.hotControl = controlId;

В случае с кликом мыши, этим вы говорите что ваш control является «горячим», то есть он в первую очредь будет реагировать на события мыши, и они не пойдут дальше к другим conltrol.
Данный аспект подробно описан в документации. Чтобы вернуть доступ к событиям мыши к другим control необходимо сделать
GUIUtility.hotControl = 0;

Этим вы скажете, что следующие события мыши может перехватить любой другой control. Также события будут доступны для других control, если вы их не используете. То есть не пометите как Used
Event.current.Use();

Приведу полный пример скрипта, обрабатывающего клики по Scene View
using UnityEngine;?
using UnityEditor;?

[CustomEditor(typeof(MyCustomComponent))]?
public class MyCustomEditor : Editor
?{?
    void OnSceneGUI()?    
{
            int controlId = GUIUtility.GetControlID(FocusType.Passive);??
            switch (Event.current.GetTypeForControl(controlId))? 
           {?               
                    case EventType.MouseDown:??    
                   GUIUtility.hotControl = controlId;
				
		   //Ваша логика использования события MouseDown

		   //Левая кнопка мыши
		   if(Event.current.button == 0)? 
                   {
 ?			// . . .
		   }

	           //Правая кнопка мыши
		   if(Event.current.button == 1)
		   {
			// . . .
	           }?

		  //Используем событие?
                  Event.current.Use();? 
                  break;?

		 case EventType.MouseUp:??
 	         //Возвращаем другим control доступ к событиям мыши?? 
                 GUIUtility.hotControl = 0;?   
                 Event.current.Use();?
                 break;
	}?
}

У кого-то может возникнуть вопрос о надобности описанного выше материала. В качестве примера можно привести создание редактора 2d тайловых уровней. Можно написать подобный кастомный редактор для класса карты, а при перехвате событий получать координаты мыши для определения конкретного дочернего элемента, по которому был произведен клик. Определить конкретный элемент можно сделав Raycast, который в Scene View отработает абсолютно также, как и в ходе работы вашей игры.

На этом разрешите откланяться, надеюсь, что данная статья окажется полезной для кого-нибудь.
Поделиться с друзьями
-->

Комментарии (29)


  1. Ichimitsu
    13.07.2016 07:43

    Среди разработчиков бытует мнение, что «магические» методы (Update, Start, OnSceneGUI и другие) реализованы по средствам System.Reflection в C#, однако есть информация, что это не так, и за их работу стоит благодарить C++ ядро Unity3D


    Странное какое-то получается мнение о том, что Unity не использует рефлексию для методов Start и etc., а неким интересным способом Mono получает эту информацию и кэширует. Причем способ явно не описан и данное мнение ничем не подтверждается. Получить данные о приватном методе в C# можно только через рефлексию. То что Unity кэширует ссылки на найденные методы это понятно без слов. Я надеюсь, что никто не думает, что каждый вызов Update происходит через поиск MethodInfo по имени.

    PS: при компиляции проекта под VS (Windows Store платформа), там тоже Mono выдает каким-то образом информацию о приватных методах?


    1. kreol_dev
      13.07.2016 11:51

      Методы-события не обязательно должны быть приватными. Unity на самом деле по барабану как вы их объявите до тех пор пока они имеют правильное название и сигнатуру.

      >Получить данные о приватном методе в C# можно только через рефлексию.
      Из C#. Когда у вас ядро на плюсах, а шарп — скрипты, то проблема решается иначе.

      А учитывая то, что на многих платформах теперь вообще ILtoCPP, то рефлекшн там и не может использоваться. Равно как он не используется, судя по всему, ядром Unity. Я могу конечно ошибаться, но насколько я понимаю, из C++ кода вы отражения .NET никак не можете использовать.


      1. Ichimitsu
        13.07.2016 12:08

        Методы-события не обязательно должны быть приватными.

        В этом то и суть) по барабану может быть только из-за рефлексии.

        Из C#. Когда у вас ядро на плюсах, а шарп — скрипты, то проблема решается иначе.

        Объясните механизм, как получить доступ к методу (публичные не берем в счет) из C++, при как вы называете C# скриптах. Вы же понимаете, что C# скрипты, это не скрипты в понимании настоящем (аля Lua), когда компиляция идет на этапе исполнения. В Unity все скрипты компилируются автоматически при изменении оных, до того как вы что-либо запустите, иначе у вас ни инспектор поля не отобразит да и в принципе ничего работать не будет.

        А учитывая то, что на многих платформах теперь вообще ILtoCPP, то рефлекшн там и не может использоваться. Равно как он не используется, судя по всему, ядром Unity. Я могу конечно ошибаться, но насколько я понимаю, из C++ кода вы отражения .NET никак не можете использовать.


        IL2CPP это плюсовый интерпретатор C# кода по сути, то что делает CLR (точне CIL/MSIL) в виндовз. Цель простая С++ код работает быстрее. При этом использование рефлексии в коде С# дозволено даже при использовании IL2CPP.


        1. vmchar
          13.07.2016 17:00

          IL2CPP использует AOT компиляцию, тогда как рефлексии требуется JIT. Так что скорее в этом замешан C++


          1. Ichimitsu
            13.07.2016 17:37

            Вы хотите сказать, что если я заюзаю IL2CPP, то мой код рефлексии не будет работать? Или я не совсем все понял, что вы имели ввиду?


            1. vmchar
              13.07.2016 17:41

              Будет работать. Это к вопросу о том, что там скорее всего не C# рефлексия используется, а как-то обходится через с++. И скорее всего с Update/Start подобная техника используется.
              Например при сборке проекта с рефлексией под iOS проект очень сильно раздуется, потому что сама платформа не может JIT. Тоже самое верно и с Generic'ами


              1. Ichimitsu
                13.07.2016 18:12

                Допустим это появилось в IL2CPP, а как это работало до него в 3 и 4 юнити, где было чисто Mono. Как бы можно было бы понять это всего дело, но C# код это не скрипты, он не разбирается на части через строки, так как это делается с lua. C# код компилируется и при сборке проекта запихивается весь в dll-ку CLI сборка. Из этой dll уже можно конечно вытащить методы, но я не совсем понимаю, как вытащить приватные метод из класса завернутого в dll. Если вы знаете механизм, буду рад услышать. Просто уже реально интересно.


                1. kreol_dev
                  13.07.2016 18:37

                  http://stackoverflow.com/questions/30334178/using-mono-to-call-c-sharp-from-c-c
                  Как вариант. Как вы предлагаете рефлекшн использоваться из плюсов в шарп? Вам в любом случае рано или поздно придётся иначе как-то вызывать ваш код. Если погуглите, то найдёте порядка 5 методов, вроде P/Invoke.

                  AOT появился не в IL2CPP, эти требования были уже давно на iOS. Например, вы не можете генерить код в рантайме, это не будет работать совсем.
                  А рефлекшн работает там совсем иначе опять же, от того, как уже говорили, проекты сильно раздуваются.

                  >как вытащить приватные метод из класса завернутого в dll.
                  Так же как публичные, плюсам неважно какие у вас там методы.


                  1. Ichimitsu
                    13.07.2016 18:57

                    Так же как публичные, плюсам неважно какие у вас там методы.

                    а можно пример? если не затруднит, давно на С++ кодил, лет 10 назад.


                    1. kreol_dev
                      15.07.2016 13:17

                      Я так-то ссылку привёл, как вариант. Метод ещё назвал. И если вы погуглите немного относительно интероп'а C# и C++, то много такого добра найдёте. Я тоже на плюсах уже давно ничего не писал, но знать обязан :D


    1. TheShock
      14.07.2016 22:17

      Получить данные о приватном методе в C# можно только через рефлексию

      Кстати, никогда не понимал, зачем там рефлексия? Почему нельзя было обьявить пустые виртуальные методы в классе-предке (MonoBehavior, он все-авно необходим), а тут делать им оверрайд (только тем, которые должны иметь поведение)? Все было бы статически и, при этом, приблизительно с тем же синтаксисом.


      1. Ichimitsu
        15.07.2016 07:14
        +1

        представьте что у вас 1000 объектов в сцене, и только 100 из них оверрайдят метод Update. Вызов пустых методов для 90% объектов, несмотря на то, что они пустые, приведет к ощутимой потере производительности. Собственно оно и сейчас не рекомендуется (я лично по рукам бью) оставлять пустые методы Start и Update в скриптах.


  1. WeslomPo
    13.07.2016 09:02
    +2

    Какая-то пустая и не очень интересная статья. Три с половиной метода которые можно в документации легко найти. Ни картинок, ничего. Не поймёшь о чем речь, если не в курсе. Картинок бы, пример по реальнее. Про CustomEditor — забыли рассказать, зачем он, как его использовать, про OnInspectorGUI — ни слова, про ярчайший пример Handles — тоже ни слова.
    image
    Вот, например, редактор mesh-а внутри unity3d написанный с нуля при помощи описанных в статье техник а также небольшой магии OnInspectorGUI и Handles.


  1. DyadichenkoGA
    13.07.2016 14:16

    Я не совсем понял смысл, а зачем перехватывать события в данном случае? Обрабатывать события ввода с мыши можно и без таких конструкций. Лучше бы была бы где-нибудь статья, как в редакторе юнити настроить кастомные хоткей Вот это не так просто делается, если я правильно помню то, по какому принципу отрабатывает OnSceneGUI и OnInspectorGUI. Когда-то мне было проще сделать редактор просто отдельной сценой и запускать, чтобы с хоткеями работать (хотя это немного костыльненько, но удобно, в случае когда редактируемые объекты хранятся в виде файлов никак не связанных с Unity)


    1. WeslomPo
      13.07.2016 15:31

      Кастомные хоткеи на свои функции делаются через атрибут MenuItem например:

      // Alt+Shift+G, # - shift, & - alt, % - ctrl/cmd
      [MenuItem("Edit/Test #&g")]
      static public void Abc() {
      	Debug.Log("Pressed!");
      }
      

      А вот если нужно, например, при удержании какой-то клавиши, выполнять какие-то изменения на сцене, и желательно прямо во время update — то тут подойдёт описанный в статье способ. Например, когда хочется собственный handles написать.


      1. DyadichenkoGA
        13.07.2016 15:57

        Я видел в документации такое, но работало оно как-то странно. И хоткеи мне нужны были, чтобы соединять на сцене точку А с точкой Б верёвкой, которая генерировалась скриптом (ну то есть был метод, который принимал параметрами трансформ начала и трансформ конца, и по данной информации генерировалась верёвка между ними) Update не работает в Editor моде, а OnInspectorGUI() и OnSceneGUI() работает довольно странно (вроде там завязано на том, когда мышь движется внутри гуя инспектора или сцены) Использовать [ExecuteInEditor] не хотелось уже не помню почему, но в данном случае Update() вроде тоже работает как OnSceneGUI(), но вот тут я могу ошибаться, не помню точно.

        Поэтому я сделал просто сцену с редактором, где Update() работает в каждом кадре и создал класс Keyboard, в котором была скрыта логика работы событий ввода и явно описана логика работы необходимых методов. Так же хоткеи мне нужны были не для меню итемов, а для созданных мной интерфейсов, которые были в виде EditorWindow для левелдизайна и отвечали за инстанцирования префабов на сцену (просто менюшка с кнопочками). Причём префабы брались рекурсивно из папок (чтобы не прописывать каждый раз новый созданный объект) и кнопки создавались динамически. А тот вариант, который предложили вы хороший, но для простейших задач.


      1. DyadichenkoGA
        13.07.2016 16:01

        Сцену с редактором — имеется ввиду сцену с редактором уровней, которую просто запускал. А получившийся уровень у меня сохранялся то ли в формате XML, то ли в YAML. Поэтому апдейт и прочее отрабатывало в каждом кадре.


        1. WeslomPo
          13.07.2016 16:25

          Вот указанный в статье метод как раз вам и нужен был, чтобы события отлавливать. Потому что OnScene (по ощущениям) вызывается по несколько раз за кадр, на каждое событие. А чтобы в редакторе было нечто подобное Update, нужно подписаться на события вот так:

          // .. Editor class
          // Built-in method
          public void OnEnable() { 
          	SceneView.onSceneGUIDelegate = scene.UpdateScene; // Точно не помню что это за переменная, но у меня в этом методе вся отрисовка сцены написана
          	EditorApplication.update = Redraw; // Вот это событие вызывается около 20 раз в секунду
          }
          // Your methods
          public void UpdateScene(SceneView sceneView) {}
          public void Redraw() {
          	// Принудительно вызовет методы OnInspectorGUI
          	EditorUtility.SetDirty(target);
          	Repaint();
          }
          

          И тогда появится подобие Update, примерно 20FPS


          1. DyadichenkoGA
            13.07.2016 16:28

            Прикольно, не знал, спасибо.


          1. vmchar
            13.07.2016 16:55

            Где-то в недрах документации было, что OnScene как и OnEditorGUI вызовутся только когда что-то измениться на SceneView и Editor соответственно. По этому они и не работают как Update, а сделано так исключительно для экономии ресурсов.

            // Принудительно вызовет методы OnInspectorGUI
            	EditorUtility.SetDirty(target);

            Сейчас кстати данный метод может не сработать, так как поменялась система внутренней сериализации в Unity.
            Отсюда и вытекает экономия ресурсов. Если бы onSceneGUI/OnInspectorGUI вызывались по 60 раз за кадр, то столько бы раз происходила сериализация редактируемого объекта во внутренний формат Unity, что крайне сильно может ударить по производительности.
            А от EditorUtility.SetDirty(target); советуют переходить к использованию SerializedProperty и даже грозят сделать SetDirty depricated


            1. WeslomPo
              13.07.2016 20:10

              OnInsectorGUI/OnSceneGUI вызывается каждый раз когда проводишь мышкой над параметрами, handles, кликаешь по инспектору и т.д. Т.е. довольно часто, сомневаюсь что в этот момент происходит сериализация/десериализация, но полная перерисовка — конечно происходит. На этом
              видео отрисовки видно что здесь не 60 кадров а гораздо меньше (около 20, окошко справа).

              SerializedObject, Undo.RecordObject, etc — это конечно здорово, но муторно — когда пишешь какое-то быстрое решение, то лучше взять технический долг, а потом его компенсировать, когда будет готовое представление того как всё должно работать.

              Вообще метод не об этом, и он костыльный, конечно (EditorUtility.SetDirty(target); — вызывается чтобы принудительно вызвать перерисовку а не для сохранения). Но пока другого не придумал и не нашел. Если есть, об этом, было бы интересно почитать.


              1. DDDENISSS
                14.07.2016 18:09

                За такое как SerializedObject надо бы по рукам бить, ибо имя переменной указывается строкой и легко допустить ошибку.
                Не понимаю зачем они отказались от EditorUtility.SetDirty. Это удар ниже пояса.
                Сами юнити, кстати, во всю используют SerializedObject т.к. это единственный способ получить доступ к приватному полю.


                1. WeslomPo
                  14.07.2016 23:11

                  Во первых, конечно же, как вы заметили, получить доступ к переменной если она приватная — не получится (через Reflections можно).
                  Во вторых, например, у вас есть скачиваемый AssetBundle со ScriptableObject. в первой версии этого объекта были поля А и Б. Во второй добавились еще В и Г. Третья версия предложила именовать поле А как Я (при этом не забыв указать параметр FormerlySerializedAs), а тип Б поменяла с int на float. Если бы мы представляли это не текстом, а классом, то все наши приложения падали при десериализации отличной версии от той что содержат в скомпилированном виде.
                  В общем стоит просто понять что все MonoBehaviour и ScriptableObject хранятся в десериализованном виде, грубо говоря в виде текста, и по большому счёту SerializedObject создано чтобы их связывать между собой. А тексту свойственно сохранять всё так как задумал разработчик когда-то. Больше плюсов, чем просто ошибка с именем переменной. Можно построить редактор который учитывает такие вещи.
                  Вообще, ИМХО дико неудобно получилось с этими SerializedProperty/SerializedObject работать >____<.
                  От EditorUtility.SetDirty отказались из-за не очевидного поведения, когда редактируется несколько сцен одновременно то в старой реализации постоянно выскакивало сообщение «Хотите сохранить сцену?», теперь изменения просто игнорируются если саму сцену не сохранить перед переходом\выходом. В мануале подробно об этом написано, в каких случаях это работает а в каких нет. Для ScriptableObject — ничего не изменилось, и для него можно спокойно использовать EditorUtility.SetDirty.


                  1. DDDENISSS
                    16.07.2016 11:22

                    В последний версиях C# появился оператор nameof, который был бы очень полезным в нашем случае.
                    Unity обещают скоро обновить компилятор, может скоро сможем его заюзать.

                    Вообще мне кажется сериализация и редактор никак не связаны. FormerlySerializedAs должен работать вполне независимо от, использую ли я в редакторе SerializedObject или нет.

                    Я не пойму, а что хорошего в том, что изменения просто игнорируются? С несколькими сценами еще не работал, поэтому тоже не пойму, как эта фича повлияла на SetDirty.


                    1. WeslomPo
                      16.07.2016 12:27

                      Без сериализации редактор просто невозможен. Нужно как-то хранить данные, загружать, обновлять. Хранить дамп памяти? Так разработка на ПК/Мак а работать будет на андроид/айос.
                      В документации подробно описано как это повлияло на SetDiry.


                    1. vmchar
                      16.07.2016 16:21

                      Тот факт, что изменения игнорируются используется еще и в Play Mode. Когда все данные сериализуются, вы можете изменять их пока включен Player Mode, а после выключения все изменения пропадают, так как объекты серилиализуются заново. В этом есть свое удобство или фича.


  1. programmer403
    14.07.2016 17:00

    Расширение функционала Scene View может понадобиться для создания собственного редактора уровней, редактирования mesh’а, создания собственных gizmos и много другого.
    Вот можно побольше пример на эту тему


    1. vmchar
      14.07.2016 17:04

      Для примера редактора уровней внутри Scene View можно рассмотреть редактор для 2d изометрии. Генерировать gameobject для поля, разбитый на тайлы, как на картинке

      отслеживать клики внутри тайлов и «рисовать» на них нужные спрайты или объекты

      Что касается создания собственных gizmo, то в качестве примера можно посмотреть статью где автор создал собственные gizmo для удобства перемещения объектов


  1. DDDENISSS
    14.07.2016 18:53

    В каких случаях Event.current.GetTypeForControl(controlId)? и Event.current.type могут иметь разные значения?
    Всегда использовал просто Event.current.type и работало.

    Вы понимаете как работает данный код?
    HandleUtility.AddDefaultControl( GUIUtility.GetControlID( FocusType.Passive ) );
    Давно использую этот код, чтобы отключить переключение кликом мыши на другой объект, но как это работает так и не понял.