Реалистичный рендер в 3ds max. V-Ray решение: теория. Как сохранять рендер каналы на batch render

Реалистичный рендер в 3ds max. V-Ray решение: теория. Как сохранять рендер каналы на batch render

Часть третья. Контроль текста.

16. Откройте скрипт TextControl и приготовьтесь писать.

17. Для начала давайте создадим переменную isNewGameButton, при которой объект будет загружать сцену.

var isNewGameButton = false;

Примечание: Почему false? Потому-что при true переменная будет срабатывать сразу.

18. Лучше не раскатывать тесто, и сразу выставить нужные переменные.

var isOptionsButton = false; var isQualityButtonFastest = false; var isQualityButtonFantastic = false; var isMainMenuButton = false; var isQuitButton = false;

19. Теперь давайте разберем самое интересное. При входе на объект курсора мыши, его материал моментально изменяется.

function OnMouseEnter() {

function OnMouseExit() {

Смена материала на серый цвет при входе курсора будет осуществляться таким образом:

function OnMouseEnter() { renderer.material.color = Color.gray; }

А возвращение материала в прежний вид при выходе курсора выглядит так:

function OnMouseExit() { renderer.material.color = Color.white; }

Примечание: Белый цвет, точнее Color.white, возвращает обычный цвет текстуре.

20. Сейчас мы начнем делать нажатие на объекты.

Нажатие осуществляется через:

function OnMouseUp() {

А чтобы занять чем-то нажатие, мы составим свойства для каждой переменной.

Для начала возьмем переменную IsQuitButton:

function OnMouseUp() { if (isQuitButton) { Application.Quit(); }

Примечание: Application.Quit() осуществляет выход из игры полностью.

Теперь возьмем переменную ужасной графики.

else if (isQualityButtonFastest) { QualitySettings.currentLevel = QualityLevel.Fastest; }

Примечание: else дает возможность работать в OnMouseUp() много раз. А QualitySettings.currentLevel = QualityLevel.Fastest изменяет графику на уровне.

else if (isQualityButtonFantastic) { QualitySettings.currentLevel = QualityLevel.Fantastic; }

Примечание: Тоже самое что и выше, но теперь с максимальной графикой.

Теперь возьмем переменную загрузки уровня.

else if (isNewGameButton) { Application.LoadLevel (1); }

Примечание: Application.LoadLevel () дает возможность загружать уровень. А число в скобках пишут для точной загрузки нужного левела.

21. Теперь поработаем с камерами. Создайте новые две переменные:

var camera1:Camera; var camera2:Camera;

А теперь продолжите таким образом:

else if (isOptionsButton) { camera1.enabled = false; camera2.enabled = true; }

Примечание: Мы работаем с нажатием, и таким образом, когда вы щелкаете на кнопке Options, первая камера отключается, а вторая начинает функционировать.

22. Ну и последнее что нам нужно, это возвращение из игры в меню.

else if (isMainMenuButton) { Application.LoadLevel (0); }

Добавьте в конце закрывающуюся скобку }.

23. Проверяем.

var isNewGameButton = false; var isOptionsButton = false; var isQualityButtonFastest = false; var isQualityButtonFantastic = false; var isMainMenuButton = false; var isQuitButton = false; var camera1:Camera; var camera2:Camera; function OnMouseEnter() { //color.X - any color that you like renderer.material.color = Color.gray; } function OnMouseExit() { //color.X - any color that you like renderer.material.color = Color.white; } function OnMouseUp() { if (isQuitButton) { Application.Quit(); } else if (isQualityButtonFastest) { QualitySettings.currentLevel = QualityLevel.Fastest; } else if (isQualityButtonFantastic) { QualitySettings.currentLevel = QualityLevel.Fantastic; } else if (isNewGameButton) { //it can be any level Application.LoadLevel (1); } else if (isOptionsButton) { camera1.enabled = false; camera2.enabled = true; } else if (isMainMenuButton) { //main menu level Application.LoadLevel (0); } }

Часть четвертая. Сборка меню.

24. Назначьте готовый скрипт ко всем кнопкам в сцене.
25. Теперь смотрим в Inspector --> Text Control (Script) и разбираем.

Для начала выберите кубик с New Game и поставьте галочку Is New Game Button.
Затем щелкните кубик с Options и поставьте галку Is Options Button. А еще в поле Camera 1 вставьте первую камеру, а в поле Camera 2, вторую.
На кубике Quit галку Is Quit Button.

Сейчас мы переходим на место с настройками. Выделите куб Fastest и поставьте галочку Is Quality Button Fastest. Так же Fantastic, но уже Is Quality Button Fantastic.
Выделите кнопку Cancel, поставье галочку Is Options Button, и вставьте в поле Camera 1 вторую камеру, а в поле Camera 2, первую.
Примечание: Почему? Потому-что нам нужен обратный процесс.

Для загрузки сцены (1), то есть, Is New Game Button, при компиляции, выставьте уровень под нужным вам числом. И не забудьте изменить значение в скрипте!

Так же, если потребуется выйти из уровня обратно в меню, используйте переменную Is Main Menu Button.

26. Теперь посмотрим что получилось.

Если все в порядке, значит я не зря писал данный урок:)
А если возникли какие-то проблемы, пишите, помогу всем, чем смогу.
Спасибо за внимание, с вами был Валентин. Желаю успехов в игрострое!

Ну а в следующем уроке я расскажу, как улучшить меню в плане возможностей;)

Этот туториал – моя первая «статья» по Unreal Engine 4. Сам я относительно недавно начал осваивать данный движок и разработку игр в общем и сейчас работаю над созданием более-менее простой игры. Недавно закончил базовую версию меню для своего проекта и решил описать свой опыт в этой статье.

Данная статья не требует каких-либо специальных навыков и вам нужно лишь установить сам движок . Я буду использовать последнюю на сей день стабильную версию: 4.16.2.

Что мы будем делать?


Это меню я сделал для игры над которой сейчас работаю. В результате туториала мы сделаем нечто похожее. (Сама игра не включена в туториал) .
  1. Главное меню – первый экран игры (данная статья).
  2. Меню «паузы» – то же самое меню, но с дополнительной кнопкой «Продолжить».
  3. Анимации и плавные переходы при открытии/закрытии меню.
Так как статья получилась длинной из-за скриншотов, части 2 и 3 пойдут отдельными статьями (надеюсь, в течение этой недели).

0. Создаём проект

Возьмём за основу шаблон для First Person Shooter. Вы можете взять любой другой или вообще ипользовать пустой (Blank) шаблон – для данной статьи это не имеет значения.

Я буду использовать Blueprint-шаблон, т.к. в любом случае меню удобнее создавать через UMG виджеты и визуальные скрипты, а работать с UMG из C++ не очень удобно (впрочем, зависит от сложности и предпочтений – в упомянутом мной выше проекте я использую смешанный подход ).

После запуска Unreal Engine вы увидите экран создания проекта:

Выбираем New Project -> Blueprint -> First Person .
Вводим путь для сохранения проекта и имя проекта, нажимаем Create Project .

После запуска Unreal Engine вы увидите примерно следующее:

Вы можете сразу же нажать Play , если интересно, что из себя представляет шаблон FPS-игры.

1. Главное меню – первый экран игры

Самый простой способ сделать главное меню – создать новый пустой уровень для меню и запускать игру именно с него.

Итак, создадим новый уровень!

Слева в Content Browser открываем папку Maps

Здесь на пустом месте вызываем контекстное меню и выбираем пункт Level

Назовём новый уровень MainMenu .

Делаем двойной клик на уровне и видим, что уровень представляет из себя ничего – просто чёрный viewport.

В данном случае, именно ничего нам и нужно!

В Content Browser возвращаемся на уровень выше и через то же контекстное меню создаём New Folder , называем его UI .

Открываем папку UI , через контекстное меню создаём новый виджет: Widget Blueprint

Назовём его снова оригинально: MainMenu . Открываем виджет двойным кликом и видим следующий экран. По-дефолту он открывается в новом окне, но вы можете перетащить вкладку в основное окно или просто развернуть его на весь экран.

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

Из панели Palette слева перетащим на основное поле элемент Text и введём туда какую-нибудь строку. Для автоматической установки размеров элемента в соответствии с текстом отметим опцию Size To Content .

И нажмём кнопку Save в панели инструментов.
UE4 любит неожиданно вылетать, а в случае вылета все несохранённые изменения вы потеряете.

Добавленный текст будет названием нашей игры. С помощью «ромашки» отцентрируем его по-горизонтали и на высоте 20% от верхнего края экрана. «Ромашка» позволяет выравнивать виджеты и привязывать их к нужным точкам, очень удобно при необходимости делать интерфейсы, которые будут работать на разных разрешениях экрана.

Если вы не видите «ромашку» – выберите наш текстовый элемент, кликнув на него левой кнопкой мыши.

Справа, в панели Details , опции Position X и Y определяют расположение элемента относительно точки привязки (центр «Ромашки») и считаются в соответствии со значениями опции Alignment , которая задаёт точку самого элемента в значениях от 0.0 до 1.0 .

Например, значение X 0.5 Y 0.0 задаёт точку посередине элемента по-горизонтали и на верхней границе элемента по вертикали.

К слову, управлять «ромашкой» можно через опцию Anchors .

В этой же панели Details установим размер шрифта побольше, например 60 единиц.

Создание самого меню

Для меню UE4 предлагает удобный элемент Vertical Box – он позволяет автоматически выравнивать несколько дочерних элементов; в нашем случае это будут кнопки (Button).

Vertical Box можно найти в панели Palette , в разделе Panel .

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

Внутрь Vertical Box поместите элемент Button , а внутрь Button – элемент Text . Слева, в Hierarchy , можно проверить, что всё расположено правильно. Если нет, то там же можно переместить элементы, чтобы получилась нужная вложенность.

Цвета в UE4 в основном задаются четырьмя переменными типа float (RGBA: red, green, blue, alpha), каждое значение может быть от 0.0 до 1.0 .
Сделаем фон кнопки прозрачным, выставив значение alpha в ноль в параметре Background color. Не забудьте предварительно выделить именно кнопку, а не текстовый элемент внутри неё.

У текстового элемента можно поставить размер шрифта побольше, например, 35 единиц. А сам текст поменяйте на " Начать Игру ".
Картинку, как это сделать, вставлять не буду, изменение размера шрифта мы уже проходили чуть ранее, при добавлении заголовка.

Добавим ещё несколько кнопок. Для этого в панели Hierarchy вызовите контекстное меню на элементе Button , выберите пункт Copy (либо нажмите Ctrl+C / ⌘+C), затем выберите Vertical Box и вставьте скопированную кнопку 3 раза.

Поменяем текст у новых кнопок: " Настройки ", " Выход ", " Продолжить ". Последняя расположена нелогично и надо бы поместить её на самый верх. Для этого выберем кнопку и воспользуемся кнопками перемещения, которые у нас есть благодаря Vertical Box .

На данном этапе меню должно выглядеть примерно так:

Отображение меню в игре

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

Перейдём на основную вкладку UE4, где у нас открыт пустой уровень с чёрным вьюпортом.

В верхней панели инструментов нажмём кнопку Blueprints и в появившемся меню выберем Open Level Blueprint . Откроется редактор блюпринтов, в котором можно создавать обработчики различных событий и вообще писать логику игры (не конкретно в блюпринте уровня, но в таком же интерфейсе).

Blueprints – мощный язык программирования, несмотря на то, что здесь вы не пишете код, а составляете его из блоков (нод). «Под капотом» там всё равно работает C++, но многие вещи намного проще и удобнее делать именно через Blueprints, вместо того, чтобы писать непосредственно код «вручную». Впрочем, верно и обратное: многие другие вещи удобнее/проще делать в коде. Поэтому при создании игры моя рекомендация – использовать комбинированный подход, а не зацикливаться на чём-то одном.

Если вы не видите у себя ноды BeginPlay , то добавьте её через контекстное меню и строку поиска. Расположение ноды на поле не имеет значения, но в случае сложных скриптов лучше стараться сразу располагать ноды так, чтобы потом самим не запутаться.

По аналогии с BeginPlay создадим рядом ноду Create Widget:

У нод событий (эвентов, events) обычно только один исходящий коннектор-пин – exec pin (от «execute» ), пин выполнения. У других нод могут быть ещё пины параметров и пины результата. Цвета пинов параметров и результатов показывают тип значения (boolean, int, string и т.д., очень много всевозможных вариантов).

Чтобы дать понять движку, что мы хотим выполнить ноду Create Widget при наступлении события BeginPlay , соединим исходящий exec-пин ноды BeginPlay со входящим exec-пином ноды Create Widget . Для этого левой кнопкой мыши нажмите на пин и не отпуская кнопку тащите до входящего пина.

В ноде Create Widget в параметре Class выберем наш виджет MainMenu

Из пина параметра Owning Player тянем линию в пустое место (да, так тоже можно), отпускаем и в меню ищем ноду Get Player Controller , добавляем её. Т.к. UE4 – движок многопользовательский, этот параметр определяет, какому игроку будет показан виджет. В случае одного игрока можно просто оставить Player Index равный нулю.

Теперь при запуске игры виджет будет создан, но мы ничего не увидим, т.к. его ещё надо отобразить. А также – передать управление от самой игры к интерфейсу.

Для этого из Return Value ноды Create Main Menu Widget тащим коннект в пустое место и в меню ищем ноду Add to Viewport . В данном случае, при создании ноды, exec-коннект должен подключиться автоматически.

Треугольник в нижней части новой ноды говорит о том, что есть какие-то скрытые параметры. Нажимаем его и видим опцию ZOrder – она устанавливает порядок, в котором виджеты будут наложены друг на друга, если мы добавляем сразу несколько виджетов на экран. Для меню логично будет поставить значение побольше, чтобы меню всегда было поверх остальных виджетов. Например, 9999 .

Последний штрих (на данном этапе) – нужно переключить режим ввода. Нам нужна нода Set Input Mode UI Only . В качестве параметра Target нужно указать тот же самый Player Controller , что и ранее, а в качестве виджета – объект, созданный нодой Create Widget .

В верхней панели инструментов нажимаем кнопку Compile . Возвращаемся в основную вкладку, сохраняем всё (меню File -> Save All) и в панели инструментов нажимаем большую кнопку Play !

Если всё было сделано правильно, то вы должны увидеть меню на чёрном фоне. Однако, есть проблема: курсор оказывается невидим. Ок, в панели инструментов нажимаем Stop (или просто Esc на клавиатуре).

Это исправляется просто. Идём обратно во вкладку Main Menu - Level Blueprint .
Из ноды Get Player Controller вытягиваем коннектор и создаём новую ноду Set Show Mouse Cursor .

Отмечаем галочкой параметр Show Mouse Cursor (тёмно-красный цвет пина – Boolean ; установка галочки равнозначна присвоению значения true , снятие галочки – false). Подключаем ноду между BeginPlay и Create Main Menu Widget и премещаем ноды так, чтоб они не запутывались.

Hint: Колесом мыши можно менять зум блюпринта.

Снова нажимаем Compile и возвращаемся в основную вкладку. Нажимаем Play .
На этот раз курсор должен быть виден, а кнопки должны нажиматься (хоть и без результата).

Создание обработчиков кнопок

Возвращаемся во вкладку нашего виджета MainMenu . Если вы её закрыли или потеряли – всегда можно открыть заново двойным кликом на нужном ассете в панели Content Browser .

Так как статья получается длинной, в этой части мы сделаем только кнопки " Начать игру " и " Выход ". А другие кнопки, пока что, отключим.

Выбираем кнопку " Продолжить " (удобнее сделать это в панели Hierarchy , чтобы выбрать саму кнопку, а не текстовый элемент) и справа в панели Details находим опцию Is Enabled . Чтобы не копаться в куче параметров, всегда можно воспользоваться строкой поиска вверху просто введите " enabled ". Снимаем галочку.
Аналогично поступаем с кнопкой " Настройки ".

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

Hint: элементы виджета можно переименовать, чтобы не путаться в нескольких одноимённых Button "ах. Для этого в панели Hierarchy выберите нужный элемент и кликните на нём один раз, либо в контекстном меню выберите Rename. Я переименовал кнопки в ButtonContinue , ButtonStart , ButtonOptions и ButtonExit соответственно.

Выбираем ButtonStart и прокручиваем панель Details в самый низ, до раздела Events . Там будет несколько зелёных кнопок.

Нажимаем кнопку OnClicked и попадаем в блюпринт нашего виджета со свежесозданой нодой эвента On Clicked (ButtonStart) . Создаём ноду Open Level , подключаем её к эвенту и в параметре Level Name указываем " FirstPersonExampleMap " (название дефолтного уровня, можно посмотреть в Content Browser).

Казалось бы, всё… но не совсем. Если помните, раньше мы переключили режим ввода на UI Only . Теперь надо сделать обратное – переключить на Game Only .

Для этого из ноды эвента вытягиваем коннектор и создаём ноду Set Input Mode Game Only . При этом нода Open Level автоматически переподключится к новой ноде, вам останется только выровнять их. Ну и не забудем, что надо указать параметр Target в новой ноде – подключим туда ноду Get Player Controller .

Нажимаем Compile и Save , запускаем, нажимаем " Начать игру "… Ура, мы в игре!

Создание обработчика ButtonExit оставляю на домашнее задание – оно даже проще: нужно просто использовать ноду Quit Game с дефолтными параметрами. Чтобы из блюпринта виджета вернуться в редактор UI можно воспользоваться переключателем справа вверху.

В один прекрасный момент вам приспичило создать меню. Контекстное или слитое c общим меню Unity. И тут вы поняли что ничего о том как это делается и не знаете. Или знаете не всё. Данная статья - обзор по способам построить меню в Unity. Этих способов благо достаточно - и каждый из них специфичен.

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

Итак, способы

ContextMenu

Самый первый способ, с которым, вы вероятно даже сталкивались - это ContextMenu .
Следующий пример демонстрирует принцип работы данного атрибута:

Using UnityEngine; public class MyClass: MonoBehaviour { void SayMessage() { Debug.Log ("I love you, DevTribe"); } }

В результате мы получим пункт меню в следующем месте:

При нажатии на данный пункт выполнится наш метод SayMessage() .

Однако, данный способ применяется в очень узком месте:

  • только в скриптах, наследуемых от класса Behaviour (всех сценариях и компонентах)

Способ применения: когда нужно быстренько вызвать какой-нибудь метод из компонента. В таких случаях переписывать инспектор объекта с целью добавить пару кнопок - нецелесообразно. Да и некоторые пунктики лучше иметь именно там.

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

ContextMenuItem

Данный способ похож на предыдущий - это тоже атрибут. Но пишется он не туда и не так.
Пример написания:

Using UnityEngine; public class MyClass: MonoBehaviour { public string ff; void SayMessage() { Debug.Log("I love you, DevTribe"); } }

Данный способ добавляет контекстное меню для нашего поля(в примере - поле ff). При щелчке правой кнопкой мыши мы увидим меню с данным пунктом:

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

  • используется только в скриптах, наследуемых от класса Behaviour (всех сценариях и компонентах)
  • только для методов экземпляра (никакого "static"!)
  • метод не должен иметь параметров

Способ применения: аналогично первому способу, но с учетом того, что выполнение метода относится к определенному полю, а не компоненту в целом.

Этот способ всё так же лепится в игровую сборку.
Для данного способа чуть ниже (в разделе "Параметры строки меню") будет расписано чуть подробней.

Лично по мне - данный способ весьма не очевиден, на практике я им ни разу и не пользовался. Однако раз уж статья про меню - странно было бы упустить этот способ, тем более что в документации нет примера на C# (хотя, он и очевиден, но принцип действия не очень).

Внимание
Все последующие способы являются расширением редактора и относятся только к сборке редактора. Это говорит о том, что скрипты, исполняющие данный код должны лежать в папке /Assets/Editor/ вашего проекта .

MenuItem

С атрибутами, однако, мы еще не закончили. Остался у нас в запасе один, о котором я умолчал. Это атрибут MenuItem , добавляющие пункты в основное меню редактора.

Пример кода:

Using UnityEditor; using UnityEngine; public static class MyClass { static void SayMessage() { Debug.Log("I love you, DevTribe"); } }

Данный способ приведет к следующему результату:

Естественно, у данного способа есть свои ограничения, но они отличаются от тех, что вы прочли ранее:

  • функция, которой принадлежит атрибут, обязательно должна быть статичной
  • функция не должна иметь параметров
Прошу заметить - в официальной документации Unity предоставлен ошибочный пример применения данной функции. Там она используется в компонентах MonoBehaviour , которые для редактора в принципе не нужны. Естественно, что пример безумно дурной и если вы в реальной ситуации заюзаете данный метод вот таким образом ваша игра просто не скомпилируется (нельзя использовать методы редактора в игровом билде, но если вы не удержались то должны предварить это соответствующей ограничивающей директивой).

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

Данный способ всегда должен указывать на раздел, то есть нельзя просто добавить пункт в меню, он обязательно должен находиться в каком-то месте. Помимо этого данный способ позволяет встраивать меню в существующие уже места. Это создает подобный пункт не только в основном меню, но и в контекстном меню в иерархии или проекте, если вы определили элемент туда.

Помимо этого данный атрибут может по желанию реализовывать несколько параметров:
isValidateFunction - помечает функцию как функцию-валидатор. Это значит, что данная функция не показывает пункт меню, а проверяет его доступность. Такая функция должна возвращать bool , говорящий, задисейблена данная кнопка или активна. Используется в паре с обычным пунктом меню (не валидатором с точно таким же путем, включая дополнительные параметры).
Пример:

Static bool CanBeShow() { return "ILove".Length == 5; } static void ShowMessage() { Debug.Log("I love you, DevTribe"); }

Опять же, про это почему то в документации особо-то и не расписывали.

priority - порядок пункта меню. Обычно стандартные элементы отсчитывают порядок от 0 и выше и, следовательно, указав "-1" мы установим наш пункт в самом верху списка.

Если вы укажете больше одного идентичного пути, то будет задействован только последний указанный. Таким образом вы можете подменять стандартные пункты меню Unity на свои.

Так же в рамках данного способа вы можете изменять контекстные меню инспектора, с таким же результатом как у способа ContextMenu . Однако, тут есть свой сахар - вы можете изменять пункты уже написанных компонентов, таких как Transform, Rigidbody и так далее.
Для этого способа зарезервировано меню CONTEXT, после которого вы вводите название нужного вам компонента:

Using UnityEditor; using UnityEngine; public static class MyClass { static void ShowMessage(MenuCommand command) { Debug.Log("I love you, DevTribe"); } }

Как вы могли заметить, я применил в функции параметр - он не обязателен, но возможен при таком раскладе. Данный параметр содержит в себе поле "context", содержащее ссылку на компонент.
У данного способа так же нет ограничений в виде конкретного типа - например в качестве пути вы можете указать CONTEXT/MonoBehaviour/ и прикрепить пункт сразу ко всем наследникам данного класса. Указывать можно любой тип, наследуемый от Component (включая сам тип Component ).
Помимо "CONTEXT/" вы так же можете использовать следующие "специальные пути":

  • "Assets/" - для добавления пункта в контекстное меню в окне ассетов
  • "Assets/Create/" - для добавления пункта в меню кнопки "Create" в окне с ассетами

GUI методы

Нельзя обойти стороной GUI методы, существующие для вызова попапов - однако среди стандартных это, зачастую, выборка значения.
Итак, попапы собственно есть следующие:

  • EditorGUILayout.Popup - строковый попап. Принцип работы: узнаем индекс строки из массива
  • EditorGUILayout.IntPopup - числовой попап. Принцип работы: узнаем индекс числа из массива, при том можно подменить числа "строковым отображением" (показывать текст, подразумевать число)
  • EditorGUILayout.EnumPopup - попап для перечислений. Принцип работы: указываем текущее значение перечислителя, получаем новое.
  • EditorGUILayout.MaskField - попап, позволяющий выбрать несколько значений, компонуя их в маску (обычно так делают выборки слоев/тегов)

В случае с отображением "строки" опять же применимы правила форматирования строки, такие как сепараторы и косая черта, о которых я писал выше.

Особо за них говорить нечего, всё можно узнать в документации.

ShowAsDropDown

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

Public class AutocompleteMenu: EditorWindow { public int visibleCount = 6; public static void Open(Vector2 position, OnControlEventDelegate onControlEvent, params GUIContent variants) { var window = CreateInstance(); window.variants = variants; window.onControlEvent = onControlEvent; window.ShowAsDropDown(new Rect(position.x, position.y, 1, 1), new Vector2(200, 20*window.visibleCount)); } public delegate void OnControlEventDelegate(Event @event); public OnControlEventDelegate onControlEvent; }

Соответственно далее в такое окно дописывается метод OnGUI где рисуется абсолютно кастомный попап под наши потребности.
Метод "ShowAsDropDown" просит указать место своего появления в абсолютных координатах. Часто нам нужно отображать такой попап на месте щелчка мышью. Такие координаты мыши можно достать следующей командой:

Var screenMousePos = GUIUtility.GUIToScreenPoint(Event.current.mousePosition);

Соль этого способа в том, что вид попапа может быть любым - вы можете сделать, например, выбор иконок или оформить окно по своему желанию.

PopupWindow

Данный способ по сути является автоматизацией предыдущего, предоставляя методы OnOpen /OnClose и заготовленный метод Show .
Суть в том чтобы пронаследовать наш класс от класса PopupWindow . Однако запомните - данная реализация ограничена в плане возврата обратного значения. Если вы используете попап для конкретных целей - данное окно вам подойдет, но для универсальных попапов желательно использовать ShowAsDropDown .

DisplayPopupMenu

Следующий способ - обычный попап, который используется во всех стандартных способах. По сути он лежит "в корне" работы GUI методов, которые я описывал немногим выше.
Этот способ использует делегат для обратного вызова.
Соль в вызове метода , при вызове которого мы указываем ссылку на метод. Если вы хотите запаковать вызов пункта в метод без делегата вам понадобится создать класс аля:

Public class PopupCallback { public static PopupCallback instance; public int controlId; public bool change; public delegate void SetValueDelegate(object data); public static void Init(int controlId) { instance = new PopupCallback { controlId = controlId }; } public static bool Its(int controlId) { return instance.w(x => x.change) && instance.controlId == controlId; } public object obj; public static EditorUtility.SelectMenuItemFunction SetValueStandart(int controlId) { Init(controlId); return instance._SetValue; } public static SetValueDelegate SetValue(int controlId) { Init(controlId); return instance._SetValue; } public void _SetValue(object userData, string options, int selected) { change = true; obj = selected; } public void _SetValue(object data) { change = true; obj = data; } public static object GetValue() { var result = instance.obj; instance = null; return result; } public static T GetValue() { var result = (T)instance.obj; instance = null; return result; } }

Данный класс содержит методы, необходимые для манипуляций с данными попапа. Если коротко - то в коде GUI вы должны завести id для вашего контрола и затем вызвать EditorUtility.DisplayPopupMenu . В данный метод вы подставляете на место делегата метод PopupCallback.SetValueStandart . После проверяете, равен ли id инстанса в PopupCallback вашему id (это уже идет код чтобы отловить обратный вызов). В положительном случае - достаете из инстанса данные.
Не совсем очевидно, но это именно то что делают Unity для формирования таких методов.

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

GenericMenu

Завершает данный список класс GenericMenu . Его синтаксис необычайно прост - вы создаете экземпляр этого класса и добавляете через методы новые пункты. По завершению вызываете ShowAsContext . Каждый пункт может иметь свой обратный вызов, что упрощает создание меню с командами.

Кастомный попап без фокуса

Отдельной темой является создание попапов без фокуса. Дело в том, что на данный момент это нельзя сделать напрямую. Несмотря на это - потребность в таком попапе есть, притом существенная - это кастомные подсказки со встроенными картинками, это "умная строка", не сбивающая ввод текста и предлагающая варианты для окончания и прочие случаи, при которых новое окно не должно влиять на поведение.
Для таких целей я портировал костыль. Чтобы его использовать скопируйте целиком следующий код:

PopupWindowWithoutFocus.cs

Using System; using System.Collections.Generic; using System.Reflection; using UnityEditor; using UnityEngine; internal class PopupWindowWithoutFocus: EditorWindow { // Fields private Rect m_ActivatorRect; private const float m_BorderWidth = 1f; private Vector2 m_LastWantedSize = Vector2.zero; private PopupLocationHelper.PopupLocation m_LocationPriorityOrder; private PopupWindowContent m_WindowContent; private static Rect s_LastActivatorRect; private static double s_LastClosedTime; private static PopupWindowWithoutFocus s_PopupWindowWithoutFocus; // Methods private PopupWindowWithoutFocus() { hideFlags = HideFlags.DontSave; } private void FitWindowToContent() { Vector2 windowSize = m_WindowContent.GetWindowSize(); if (m_LastWantedSize != windowSize) { m_LastWantedSize = windowSize; Vector2 minSize = windowSize + new Vector2(2f * m_BorderWidth, 2f * m_BorderWidth); Rect rect = PopupLocationHelper.GetDropDownRect(m_ActivatorRect, minSize, minSize, null, m_LocationPriorityOrder); var baseType = GetType().BaseType; if (baseType != null) { var fieldInfo = baseType.GetField("m_Pos", BindingFlags.Instance | BindingFlags.NonPublic); if (fieldInfo != null) fieldInfo.SetValue(this, rect); } var vector3 = new Vector2(rect.width, rect.height); maxSize = vector3; base.minSize = vector3; } } public static void Hide() { if (s_PopupWindowWithoutFocus != null) { s_PopupWindowWithoutFocus.Close(); } } private void Init(Rect activatorRect, PopupWindowContent windowContent, PopupLocationHelper.PopupLocation locationPriorityOrder) { m_WindowContent = windowContent; m_WindowContent.GetType() .GetProperty("editorWindow") .GetSetMethod(true) .Invoke(m_WindowContent, new object {this}); m_ActivatorRect = GUIToScreenRect(activatorRect); m_LastWantedSize = windowContent.GetWindowSize(); m_LocationPriorityOrder = locationPriorityOrder; Vector2 minSize = windowContent.GetWindowSize() + new Vector2(m_BorderWidth * 2f, m_BorderWidth * 2f); position = PopupLocationHelper.GetDropDownRect(m_ActivatorRect, minSize, minSize, null, m_LocationPriorityOrder); ShowPopup(); Repaint(); } internal static Rect GUIToScreenRect(Rect guiRect) { Vector2 vector = GUIUtility.GUIToScreenPoint(new Vector2(guiRect.x, guiRect.y)); guiRect.x = vector.x; guiRect.y = vector.y; return guiRect; } public static bool IsVisible() { return (s_PopupWindowWithoutFocus != null); } private void OnDisable() { s_LastClosedTime = EditorApplication.timeSinceStartup; if (m_WindowContent != null) { m_WindowContent.OnClose(); } s_PopupWindowWithoutFocus = null; } private void OnEnable() { s_PopupWindowWithoutFocus = this; } private static bool OnGlobalMouseOrKeyEvent(EventType type, KeyCode keyCode, Vector2 mousePosition) { if (s_PopupWindowWithoutFocus != null) { if ((type == EventType.KeyDown) && (keyCode == KeyCode.Escape)) { s_PopupWindowWithoutFocus.Close(); return true; } if ((type == EventType.MouseDown) && !s_PopupWindowWithoutFocus.position.Contains(mousePosition)) { s_PopupWindowWithoutFocus.Close(); return true; } } return false; } internal void OnGUI() { FitWindowToContent(); var rect = new Rect(m_BorderWidth, m_BorderWidth, position.width - (2f * m_BorderWidth), position.height - (2f * m_BorderWidth)); m_WindowContent.OnGUI(rect); GUI.Label(new Rect(0f, 0f, position.width, position.height), GUIContent.none, "grey_border"); } private static bool ShouldShowWindow(Rect activatorRect) { if (((EditorApplication.timeSinceStartup - s_LastClosedTime) < 0.2) && !(activatorRect != s_LastActivatorRect)) { return false; } s_LastActivatorRect = activatorRect; return true; } public static void Show(Rect activatorRect, PopupWindowContent windowContent) { Show(activatorRect, windowContent, null); } internal static void Show(Rect activatorRect, PopupWindowContent windowContent, PopupLocationHelper.PopupLocation locationPriorityOrder) { if (ShouldShowWindow(activatorRect)) { if (s_PopupWindowWithoutFocus == null) { s_PopupWindowWithoutFocus = CreateInstance(); } s_PopupWindowWithoutFocus.Init(activatorRect, windowContent, locationPriorityOrder); } } } internal static class PopupLocationHelper { private static Type _typePopupLocationHelper; private static Type typePopupLocationHelper { get { if (_typePopupLocationHelper == null) _typePopupLocationHelper = typeof(EditorGUI).Assembly.GetType("UnityEditor.PopupLocationHelper"); return _typePopupLocationHelper; } } private static Type _typeContainerWindow; private static Type typeContainerWindow { get { if (_typeContainerWindow == null) _typeContainerWindow = typeof(EditorGUI).Assembly.GetType("UnityEditor.ContainerWindow"); return _typeContainerWindow; } } private static MethodInfo _methodFitWindowRectToScreen; private static MethodInfo methodFitWindowRectToScreen { get { if (_methodFitWindowRectToScreen == null) _methodFitWindowRectToScreen = typeContainerWindow.GetMethod("FitWindowRectToScreen", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); return _methodFitWindowRectToScreen; } } private static MethodInfo _methodFitRectToScreen; private static MethodInfo methodFitRectToScreen { get { if (_methodFitRectToScreen == null) _methodFitRectToScreen = typeContainerWindow.GetMethod("FitRectToScreen", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); return _methodFitRectToScreen; } } // Methods private static Rect FitRect(Rect rect, object popupContainerWindow) { if (popupContainerWindow != null) { return (Rect)methodFitWindowRectToScreen.Invoke(popupContainerWindow, new object {rect, true, true}); } return (Rect) methodFitRectToScreen.Invoke(null, new object {rect, true, true}); } public static Rect GetDropDownRect(Rect buttonRect, Vector2 minSize, Vector2 maxSize, object popupContainerWindow) { return GetDropDownRect(buttonRect, minSize, maxSize, popupContainerWindow, null); } public static Rect GetDropDownRect(Rect buttonRect, Vector2 minSize, Vector2 maxSize, object popupContainerWindow, PopupLocation locationPriorityOrder) { if (locationPriorityOrder == null) { PopupLocation locationArray1 = new PopupLocation; locationArray1 = PopupLocation.Above; locationPriorityOrder = locationArray1; } List rects = new List(); PopupLocation locationArray = locationPriorityOrder; for (int i = 0; i < locationArray.Length; i++) { Rect rect; switch (locationArray[i]) { case PopupLocation.Below: if (!PopupBelow(buttonRect, minSize, maxSize, popupContainerWindow, out rect)) { break; } return rect; case PopupLocation.Above: if (!PopupAbove(buttonRect, minSize, maxSize, popupContainerWindow, out rect)) { goto Label_0079; } return rect; case PopupLocation.Left: if (!PopupLeft(buttonRect, minSize, maxSize, popupContainerWindow, out rect)) { goto Label_0099; } return rect; case PopupLocation.Right: if (!PopupRight(buttonRect, minSize, maxSize, popupContainerWindow, out rect)) { goto Label_00B9; } return rect; default: { continue; } } rects.Add(rect); continue; Label_0079: rects.Add(rect); continue; Label_0099: rects.Add(rect); continue; Label_00B9: rects.Add(rect); } return GetLargestRect(rects); } private static Rect GetLargestRect(IEnumerable rects) { var rect = new Rect(); foreach (Rect rect2 in rects) { if ((rect2.height * rect2.width) > (rect.height * rect.width)) { rect = rect2; } } return rect; } private static bool PopupAbove(Rect buttonRect, Vector2 minSize, Vector2 maxSize, object popupContainerWindow, out Rect resultRect) { Rect rect = new Rect(buttonRect.x, buttonRect.y - maxSize.y, maxSize.x, maxSize.y); float num = 0f; rect.yMin -= num; rect = FitRect(rect, popupContainerWindow); float a = Mathf.Max((buttonRect.y - rect.y) - num, 0f); if (a >= minSize.y) { float height = Mathf.Min(a, maxSize.y); resultRect = new Rect(rect.x, buttonRect.y - height, rect.width, height); return true; } resultRect = new Rect(rect.x, buttonRect.y - a, rect.width, a); return false; } private static bool PopupBelow(Rect buttonRect, Vector2 minSize, Vector2 maxSize, object popupContainerWindow, out Rect resultRect) { var rect = new Rect(buttonRect.x, buttonRect.yMax, maxSize.x, maxSize.y) { height = maxSize.y + k_SpaceFromBottom }; rect = FitRect(rect, popupContainerWindow); float a = Mathf.Max((rect.yMax - buttonRect.yMax) - k_SpaceFromBottom, 0f); if (a >= minSize.y) { float height = Mathf.Min(a, maxSize.y); resultRect = new Rect(rect.x, buttonRect.yMax, rect.width, height); return true; } resultRect = new Rect(rect.x, buttonRect.yMax, rect.width, a); return false; } private static bool PopupLeft(Rect buttonRect, Vector2 minSize, Vector2 maxSize, object popupContainerWindow, out Rect resultRect) { var rect = new Rect(buttonRect.x - maxSize.x, buttonRect.y, maxSize.x, maxSize.y); const float num = 0f; rect.xMin -= num; rect.height += k_SpaceFromBottom; rect = FitRect(rect, popupContainerWindow); float a = Mathf.Max((buttonRect.x - rect.x) - num, 0f); float width = Mathf.Min(a, maxSize.x); resultRect = new Rect(rect.x, rect.y, width, rect.height - k_SpaceFromBottom); return (a >= minSize.x); } private static bool PopupRight(Rect buttonRect, Vector2 minSize, Vector2 maxSize, object popupContainerWindow, out Rect resultRect) { var rect = new Rect(buttonRect.xMax, buttonRect.y, maxSize.x, maxSize.y); const float num = 0f; rect.xMax += num; rect.height += k_SpaceFromBottom; rect = FitRect(rect, popupContainerWindow); float a = Mathf.Max((rect.xMax - buttonRect.xMax) - num, 0f); float width = Mathf.Min(a, maxSize.x); resultRect = new Rect(rect.x, rect.y, width, rect.height - k_SpaceFromBottom); return (a >= minSize.x); } // Properties private static float k_SpaceFromBottom { get { if (Application.platform == RuntimePlatform.OSXEditor) { return 10f; } return 0f; } } // Nested Types public enum PopupLocation { Below, Above, Left, Right } }

В качестве примера используйте этот код:

Using UnityEditor; using UnityEngine; public class ExampleUnfocusWindow: PopupWindowContent { public static void Open() { Open(new Vector2()); } public static void Open(Vector2 position) { var locationPriorityOrder = new PopupLocationHelper.PopupLocation; locationPriorityOrder = PopupLocationHelper.PopupLocation.Left; locationPriorityOrder = PopupLocationHelper.PopupLocation.Right; PopupWindowWithoutFocus.Show(new Rect(position.x, position.y, 6, 6), new ExampleUnfocusWindow(), locationPriorityOrder); } public override Vector2 GetWindowSize() { return new Vector2(200, 200); } public override void OnOpen() { base.OnOpen(); } public override void OnClose() { base.OnClose(); } public override void OnGUI(Rect rect) { GUI.Box(rect, "Hello world"); } }

Настройка строки меню

Некоторые из указанных выше способов поддерживают расширенное форматирование для строки.
Если вы укажете в качестве пункта пустую строку, вместо неё будет задействован сепаратор (вертикальная черта).
Если вы разделите части строки дробной чертой / , вы создадите подпункт с иерархией.

Так же вы можете задать сочетание клавиш, при котором будет происходить вызов пункта меню. Для этого вам понадобится написать некоторую последовательность символов через пробел, после вашего пути.
В качестве спецсимволов зарезервированы:

% - ctrl
# - shift
& - alt
_ - отсутствие спецсимвола

Таким образом, если вы хотите назначить пункту меню клавишу G вы должны написать что-то подобное следующему:

Так же пункты можно комбинировать, чтобы добиться сочетания аля "ctrl+shift".

Естественно, что не всем клавиши можно представить буквой, потому для некоторых клавиш используются специальные ключевые слова LEFT, RIGHT, UP, DOWN, F1 .. F12, HOME, END, PGUP, PGDN.

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

Никогда не думал что чтобы рассказать про меню нужно столько написать. А ведь я еще кастомные меню особо не расписывал

Заготовки using UnityEditor; using UnityEngine; public class Popup: EditorWindow { public static void Open(Vector2 size) { var window = CreateInstance(); var screenMousePos = GUIUtility.GUIToScreenPoint(Event.current.mousePosition); window.ShowAsDropDown(new Rect(screenMousePos.x, screenMousePos.y, 1, 1), size); } }

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

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

    Using UnityEngine; using System.Collections; public class puse: MonoBehaviour { public float timer; public bool ispuse; public bool guipuse; void Update() { Time.timeScale = timer; if (Input.GetKeyDown(KeyCode.Escape) && ispuse == false) { ispuse = true; } else if (Input.GetKeyDown(KeyCode.Escape) && ispuse == true) { ispuse = false; } if (ispuse == true) { timer = 0; guipuse = true; } else if (ispuse == false) { timer = 1f; guipuse = false; } } public void OnGUI() { if (guipuse == true) { Cursor.visible = true;// включаем отображение курсора if (GUI.Button(new Rect((float)(Screen.width / 2), (float)(Screen.height / 2) - 150f, 150f, 45f), "Продолжить")) { ispuse = false; timer = 0; Cursor.visible = false; } if (GUI.Button(new Rect((float)(Screen.width / 2), (float)(Screen.height / 2) - 100f, 150f, 45f), "Сохранить")) { } if (GUI.Button(new Rect((float)(Screen.width / 2), (float)(Screen.height / 2) - 50f, 150f, 45f), "Загрузить")) { } if (GUI.Button(new Rect((float)(Screen.width / 2), (float)(Screen.height / 2), 150f, 45f), "В Меню")) { ispuse = false; timer = 0; Application.LoadLevel("Menu"); // здесь при нажатии на кнопку загружается другая сцена, вы можете изменить название сцены на свое } } } }

    Здесь все просто. Две переменные, одна из которых будет при нажатии на ESC останавливать время, а вторая, при остановке времени будет выводить на экран кнопки меню.

    Весь скрипт построен, по сути, на одной функции:

    Time.timeScale = timer

    TimerScale является, как бы, своеобразными часами которые при значении 1 работают в нормальном режиме и время проходит как обычно, при значении 0,5 время замедляется в 2 раза, а при значении 0 время вообще останавливается. Этим и воспользуемся для создания паузы.



    © 2024 beasthackerz.ru - Браузеры. Аудио. Жесткий диск. Программы. Локальная сеть. Windows