Как запустить питон в blender
Перейти к содержимому

Как запустить питон в blender

  • автор:

Основы Python API для Blender

Приветствую всех блендерщиков, не блендерщиков и других, кто решил сюда заглянуть. Сегодня я попробую доступным образом объяснить, как писать простенькие скрипты для блендера и не только Язык, который мы будем использовать — Python. Он простенький, с необычным синтаксисом лично для меня, человека, который начал и в течении двух лет учил С++. В этой статье я не буду делать урок по питону, она рассчитана на то, что вы хотя бы поверхностно знаете его. Этого можно легко добиться, посмотрев пару уроков на ютубе или прочитав в интернете. Если возникнет какая-то проблема, то пишите в комментарии Кстати, если заметили ошибку, знаете решение оптимальнее, то не стесняйтесь писать об этом (естественно, адекватно, а без наездов в стиле «автор — ламер-гумманитарий» и т.п.). Я бы не назвал себя прям таким мидл питон программистом, или человеком, который очень глубоко знает API для блендера, но моих знаний хватает, чтобы писать скрипты, которые очень могут облегчить жизнь, или те же интерфейсы. Ну чтож, мы что-то отвлеклись на отступление, пожалуй, начнем!

Подготовка

Мы будем писать код прямо в блендере, так как это будет удобнее. Чтобы использовать модули API вне блендера, требуются определенные махинации, которые в том числе включают компиляцию блендера из исходников. Однако в 3.4 в этой области ожидается улучшение, я пока что не тестировал, да и урок будет в 3.3 Для начала стоит изменить настройки. Переходим в Edit->Preferences->Interface. Ставим галочки у пунктов Developer Extras и Python Tooltips
Первое откроет оператор Edit source при правом клике на определенные элементы интерфейса. Он открывает файл в текст едиторе по пути , который содержит код, определяющий этот элемент. Почти весь интерфейс блендера написан на питоне, и лежит в папках по пути выше. Так как python — интерпретируемый язык, то текст лежит в .py файлах (однако стоит заметить, что код можно скомпилировать, тогда его не получится отредактировать, но такая возможность не используется в угоду легкой редактируемости). Можно легко залезть в каталоги, и что-то поправить в UI. Но и тут есть подвох: по какой-то причине часть элементов реализована в ядре блендера, то есть написана на С/С++, закомпилирована в бинарник (т.е. в blender.exe или же просто blender для Линукса, исполняемые файлы не носят расширения там), и отредактировать их без перекомпиляции блендера не получится. Если вы наткнетесь на такое, то оператор известит вас, что исходники недоступны Второй оператор добавляет Python подсказки, когда вы оставляете курсор неподвижным на элементе. Увы, мой скриншотер сбивает фокус курсора, поэтому сплывашка изчезает, и заскринить ее не удалось:( После этого переходим в такую непримечательную рабочую область в самом конце под названием Scripting. если у вас ее нет, но нажимаете на плюсик в конце, потом оттуда выбираем General->Scripting
Там практически все необходимое для того, чтоб писать скрипты. Ну чтож, пора познакомится с нашими инструментами
Окно по середине — это Text Editor, редактор, в котором будет производится основное действие, и который был упомянут ранее. По сути — это обычный блокнот с подсветкой синтаксиса, у которого есть кнопка запуска скрипта, и всякие опции, типа автоматического запуска скрипта при открытии файла
Второй по значимости редактор — это Python Console. Это встроенная консоль с питоном и определенными преднастройками, которая позволяет быстро запускать функции без написания скрипта, или просматривать содержимое классов/модулей
Третий, не особо часто используемый, но определенно полезный редактор — Info. Просто список операторов, которые подразумевались под выполнением каких-то действий пользователем (нажатие хоткея, кнопки и т.п.). По сути — аналог майской строки внизу, где указывается MEL команда, когда что-то происходит, ни больше, ни меньше
остальные редакторы — это все уже знакомое, вьюпорт, панель свойств, оутлайнер в обычном режиме и режиме отображения структуры текущего blend файла Мы будем работать с консолью, так как это легче в начале. Для открытия консоли в Windows надо нажать Window->Toggle System Console, для Линукса необходимо запускать блендер через консоль На этом необходимый сетап выполнен, можно приступать к кодингу!

Использование инструментов

Для начала напишем hello world, чтобы понять, как работать с инструментами Напишем во внутренней консоли

print("hello world")

И получим ожидаемый ответ
Как уже было ранее сказано, консоль внутри блендера идентична консоли, в которой был запущен Python. Однако в системной консоли вывода от принта не будет, так как он ушел в виртуальную консоль Теперь напишем код, который будет выводить hello world уже в системную консоль. Создаем новый текстовый файл в текст едиторе, пишем идентичный код, нажимаем на кнопку запуска, и смотрим в консоль

Стоит заметить, что инфо редактор отреагировал на нажатие кнопки запуска, выдав оператор, который скрывается за кнопкой

Помимо исполнения кода, внутренняя консоль может отображать документацию к функциям и члены модуля/класса

Чтобы так сделать, надо написать точку/скобку (в зависимости от того, что из себя представляет сущность), и потом нажать Tab. В консоли появится лаймовый текст с документацией или членами сущности. Tab — это на самом деле функция автодополнения, так что помимо быстрого доступа к документации этот хоткей можно использовать по его прямому назначению Существует опция Register в пункте Text заголовка. Этот флажок говорит о том, что необходимо запускать скрипт, когда загружается blend файл. Она может очень часто использоваться, а почему — мы поговорим позже
Теперь, когда мы научились пользоваться инструментами, пора уже перейти к самому API

Собственно API

Для того, чтоб как-то взаимодействовать с блендером, нам надо импортировать модуль под названием bpy. Он идет вместе с блендером. Чтобы начать работать с ним, необходимо написать в начале скрипта:

import bpy

Теперь нам доступны все элементы bpy, которых не так уж и много. Их можно посмотреть через Tab в консоли
Мы будем разбирать их в порядке важности/частоты использования

Контекст (bpy.context)

Это один из важнейших классов API. Он содержит в себе различные объекты и коллекции, которые отображают то, что сейчас происходит в блендере. именно через него можно получить активный объект, или все выделенные например. Список того, что можно получить через него — довольно огромен. Мы можете посмотреть сами, используя выше описанный трюк с автодополнением Практически все элементы контекста — read only, то есть вы не можете их изменять напрямую. Однако это возможно путем изменения данных объектов или других функций API Часть контекста напрямую зависит от того, где запускается код. В документации расписано, какие элементы подчиняются этому правилу Лишь малая часть контекста независима, и везде будет одинакова В пример приведу коллекцию, относящуюся к анимации — editable_fcurves. Она содержит в себе редактируемые анимационные кривые. Если вы запустите такой код в консоли (кстати, вот и та самая изначальная настройка консоли — заранее импортирована часть модулей + для сокращения времени печатания есть парочку переменных, например С -это bpy.context в консоли)

print(C.editable_fcurves)

То вы неожиданно обнаружите, что коллекция имеет тип None, т.е. там ничего нету, даже если в редакторе графов будет хоть одна кривая. Как же так?! — спросите вы. Это и есть пример зависимости контекста от того, где запускается код. Но, если вы обернете функцию в оператор (о чем пониже), и запустите в граф едиторе через F3, то вместо None будет напечатана коллекция, все будет нормально

Операторы (bpy.ops)

Я очень часто упоминаю их, так что пора бы рассказать, да и тем более это очень важная часть программной экосистемы блендера Оператор — это по сути функция, которая что-то делает, но оформлена в виде класса, из-за того что для оператора приходится хранить кучу дополнительных данных

bpy.types.Operator

Каждый оператор — это класс, который наследуется от класса выше. Код обязательно должен быть обернут в класс оператора, если надо, чтобы он запускался из интерфейса (будь то кнопка, или пункт в меню), а не только вызывался другим кодом Пример того, как должен быть реализован оператор, есть в Templates-Python->Operator Simple
Данный оператор может быть сложен для первого раза, поэтому упрощу код, убрав все лишнее
Для начала импортируем модуль bpy, все как обычно. Потом создаем класс, и в качестве родителя указываем bpy.types.Operator Текст в тройных кавычках — то, что будет отображаться во встроенной консоли в качестве документации
bl_idname — это встроенное поле. Оно должно быть обязательно указано. Это по сути ID оператора, то, как к нему будет обращаться остальная система (включая вас и остальных скриптеров/прогеров). Оно включает имя подмодуля в bpy.ops и само имя оператора. Если имя подмодуля уже существует, то оператор станет членом этого модуля, иначе — создастся новый подмодуль bl_label — это имя оператора уже в UI, то есть то, что будет отображаться в том же меню метод poll — это способ ограничить применение оператора, если существует риск появления контекста, где оператор завершится с ошибкой, либо бесполезен по самой логике. True — оператор будет доступен, False — нет. В нашем примере оператор будет запускаться, только если есть активный объект. Можно создать любое условие, например запуск в определенном режиме объекта, или если выделены определенные элементы, все. что вам позволяет делать контекст execute — это собственно метод, где находится то, что делает оператор. Метод обязательно должен вернуть словарь (т.е. фигурные скобки, «< >«) со строкой, которая означает статус завершения работы оператора. Обычно используют два значения — ‘FINISHED’, что значит, что все хорошо, и ‘CANCELLED’ — значит работа оператора была прекращена по какой-то причине

def execute(self, context): . return

Естественно, это не все, что можно сказать по операторам, это очень обширная тема. Например, существуют метод invoke, который позволяет проинициализировать оператор перед запуском, модальные операторы (их пример — это GRS операторы. Подробнее о модальных операторах можно почитать тут) и многое много другое. Все это рассказывать очень долго, но есть то, что хотелось бы мне добавить У операторов существует огромное количество разных свойств, помимо bl_idname и bl_label. Они все помогают настроить поведение оператора, но есть одно очень важное, которое почему-то не было вынесено в шаблон оператора, и без которого оператор даже не сможет отменяться — это bl_options. Оно принимает словарь из ключевых слов, которые указывают определенные опции оператору. Например ‘UNDO’ — оператор может отменяться, или ‘REGISTER’ — оператор будет иметь панель повтора снизу. В основном это самые популярные используемые опции, их стоит запомнить

bl_options =

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

Регистрация классов

Однако после написания класса блендер не будет видеть оператор. Для того, чтобы это исправить, необходимо зарегистрировать его. В начале своего запуска блендер вообще ничего не знает ни о операторах (кроме тех, что реализованы в ядре), ни о различных панельках, менюшках, которые реализованы на питоне. Каждый такой элемент должен быть зарегистрирован при помощи функции bpy.utils.register_class(). Практически каждый оператор, каждая менюшка, которая реализована в , так или иначе проходит процедуру регистрации через register_class В нашем примере есть код, который регистрирует класс. Он расположен в функции register. далее идет код, который значит, что при запуске скрипта будет выполнена функция register(). Также есть функционал для отмены регистрации класса, это соответственно bpy.utils.unregister_class(). Она нужна например аддонам, чтобы когда ты выключал аддон, от них ничего не оставалось Стоит заметить, что порядок регистрации имеет очень важную роль. Если сначала зарегистрировать панельку, которая использует оператор, а только потом сам оператор, то будет ошибка Чтобы наконец-то увидеть результат нашего оператора, нажимаем кнопку запуска, и вуаля — в системной консоли отобразится информация о классе объекта, а через F3 появится возможность запускать оператор. Галочка Register у текста нужна именно за этим — чтобы каждый раз самому не запускать скрипт, тем самым запуская регистрацию

UI (bpy.types.UILayout и другие)

Думаю, многие этого ждали В блендере очень много UI элементов — панельки, менюшки, кнопки, и т.п. и т.п. С каждым можно так или иначе работать через API Начнем пожалуй с панелек. думаю, каждый бы хотел что-то прикрутить справа во вьюпорте. Для этого нам надо Создать класс, производный от bpy.types.Panel. Я возьму пример от моего проекта
У класса панели имя желательно должно строится образом — имя_пространства_PT_свой_текст bl_space_type — пространство или редактор, в котором будет отображаться панель bl_region_type — тип региона, в котором будет отображаться панель. У окна есть несколько регионов, мы указываем ‘UI’ bl_category — категория, в которой будет панелька. Если указываем существующее, то панель будет там же вместе с остальными панелями, которые указали такой регион, если такой категории не будет — то она создастся. Ну и bl_label — это само название панели У панели также есть метод poll, чтобы избегать запуска в неправильном контексте, аналогично оператору Вместо метода execute у панели и другого UI есть метод draw — это метод, отвечающий за рисование внутри сущности. В блендере UI описывается функциональным подходом — то есть хотим поместить оператор — вызываем функцию, которая создаст оператор, потом настраиваем элемент, передавая функции соответствующие аргументы Модернизируем наш код, чтобы теперь он отображал панель в 3д вьюпорте
Пока что она пустая, надо бы ее наполнить элементами Для того, чтобы начать работать с UI — берем у self поле layout

layout = self.layout

У лайяута ОЧЕНЬ много методов и всяких полей, всех их можно прочитать тут. Я же перечислю часто используемые

row = layout.row()

создает ряд с элементами, которые идут по горизонтали

column = layout.column()

Это собрат row(), но уже по вертикали. Это колонка элементов Значение, которое возвращает метод, необходимо принять в переменную, иначе не получится получить доступ к ним Хочу заметить, что у каждого вышеописанного метода возвращаемое значение имеет тип UILayout, т.е. все, что было и будет сказано для лайяута, применимо ко всем перечисленным элементам: вам ничто не мешает сделать ряд, а в нем столбцы Очень важные методы — это layout.prop и layout.operator

layout.operator('module_name.op_name')

Дает доступ к оператору из UI. Может быть и кнопкой с текстом, и с иконкой Используем полученные знания, чтобы наполнить нашу панельку
Теперь у нас появилась кнопка с нашим оператором. Если при вызове row.operator() задать параметр text, то можно задавать кастомный текст
Для того, чтобы оставить только иконку, необходимо выставить text как », а также указать ID иконки. Айдишник можно взять пи помощи встроенного аддона Icon Viewer, что мы и сделаем
Если использовать несколько row.operator() подряд, то можно заметить, чем row отличается от column
Аналогично можно сделать и с column Теперь следующий наиболее важный метод

layout.prop(object, 'path_to_data')

Позволяет вынести уже свойство объекта в UI. Сначала пишется то, с чего берется свойство, потом путь до свойства в виде строки относительно объекта. Чтобы узнать путь, можно просто навестись на свойство в UI. Как я понял, блендер не воспринимает вложенные объекты. Т.е. если у класса Scene есть поле под класс SceneEEVEE, который хранит настройки eevee, то для того, чтобы допустим вынести количество семплов, надо задавать объектом scene.eevee, а путь — просто ‘taa_samples’. В UI в зависимости от настроек свойства будет отображаться соответствующий виджет
И последнее, что хотелось бы сказать по UI — это меню. Они делаются как и все UI классы, но чтобы прицепить это меню к существующим, необходимо использовать коллбек — функцию, которая передается в параметр другой функции, которая потом где-то вызывает первую В нашем случае мы пишем сначала класс менюшки

class VIEW3D_MT_test(bpy.types.Menu): bl_label = "test" def draw(self, context): layout = self.layout layout.operator(. )

А потом коллбек функцию, подобную draw(self, context). Единственное условие — параметры должны совпадать, на этом — все. Теперь берем и пишем код, используя метод layout.menu(). Думаю, вы догадались, что оно сделает

def draw_func(self, context): layout = self.layout layout.menu(menu_name) .

После написания функции ее надо прицепить туда, куда нам надо. Вот тут и пригодится оператор Edit Source. Выясняете, в каком месте рисуется меню, и тем самым находите класс, который представляет меню. Например, для менюшек в заголовке вьюпорта это будет

bpy.types.VIEW3D_MT_editor_menus

Далее необходимо вызвать у класса метод append()

bpy.types.VIEW3D_MT_editor_menus.append(draw_func)

Кстати, не забываем про регистрацию. И меню, и панели надо сначала регистрировать, а только потом использовать

Свойства (bpy.props)

Еще одна очень важная тема, чтобы полноценно писать то же UI для ригов
bpy.props — это по сути обертка для более низкоуровневой и сложной системы — RNA Свойства есть так и у объектов, так и у операторов Есть немало разных свойств, но я перечислю самые часто используемые: IntProperty (целое число), FloatProperty (число с плавающей запятой), BoolProperty (булево значение) Существует также StringProperty, EnumProperty и куча других. Обо всех свойствах можно прочитать тута Синтаксис для операторов и обычных обычных объектов различается Для операторов

var : Property(. )

Для объектов

bpy.types.Type.new_prop = bpy.props.Property(. )

Для операторов свойства можно использовать через self

. prop : IntProperty(name='rbgrb') . def execute(self, context): . a = self.prop

Их можно настраивать через layout.operator()

layout.operator('object.my_op').prop = 1

В нашем случае оператор запустится со значением prop в виде 1 Можно прокинуть свойство в оператор, повесив его на объект

bpy.types.Scene.prop = bpy.props.StringProperty(name='sas') . def execute(self, context): . a = context.scene.prop

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

У свойств огромная куча комбинаций использования. Какой метод будет лучше — зависит от задачи

Остальные Модули

  • bpy.data — все данные текущего blend файла
  • bpy.app — модуль позволяет получить информацию о самом блендере, дает доступ к обработчикам, таймерам, и прочим штукам для чего-то сложнее панельки или оператора
  • bpy.types — содержит все когда-либо зарегистрированные классы
  • bpy.utils — содержит различные вспомогательные функции, вроде register_class
  • bpy.path — необходим при работе с путями
  • bpy.msgbus — модуль, который нужен, когда необходимо отслеживать изменения данных

Помимо bpy есть и другие модули. Их необходимо импортировать отдельно

  • mathutils — математика: вектора, матрицы, все, что пожелаете
  • aud — модуль работы со звуком
  • bgl — устаревший модуль для низкоуровневого взаимодействия с GPU. Не надо его использовать
  • bl_math — если вам было мало mathutils, то вот вам еще)
  • blf — для низкоуровневой работы со шрифтами
  • bmesh — модуль для работы с мешем. Перед применением лучше прочитать мануал
  • bpy_extras — различные дополнительные функции
  • freestyle — модуль для работы с фристайл рендером
  • gpu — как bgl, но лучше, и не будет выпилен
  • gpu_extras — небольшой модуль со всякими доп штуками для GPU
  • idprop.types — крайне мало документации. Что-то связанное с данными
  • imbuf — модуль для работы с изображениями

Послесловие

Естественно, я не могу описать все на свете. Вы сами видели, сколько одних только модулей, и насколько может рознится сфера их применения. Для того, чтобы нормально писать скрипты, достаточно bpy. А дальше изучаете в зависимости от сферы, в которой работают ваши скрипты

Я оставлю ссылки на различные материалы, которые могут вам помочь

  • официальная документация
  • Канал в блендер чате, где могут помочь
  • На форуме разработчиков тоже могут подсобить
  • Плейлист Андрея Соколова, откуда можно подчеркнуть информацию, которую я мог не рассказать, а также небольшой курс по питону

Спасибо всем за прочтение!

748 0 850 5

Спасибо, бро. Для тех, кто в стадии освоения скриптов под названием «ни-бум-бум» – самое то. 😀
Давненько подспудно что-то такое высматривал.

Василий Горожанин Спасибо, бро. Для тех, кто в стадии освоения скриптов под названием «ни-бум-бум» – самое то. 😀
Давненько подспудно что-то такое высматривал.

Рад помочь. Я как раз и рассчитывал в основном на «ни-бум-бум»=) Я думаю, что в принципе после этой статьи будет достаточная база, чтобы дальше самому плясать

Аргументы командной строки¶

-b , —background Run in background (often used for UI-less rendering). -a , —render-anim Render frames from start to end (inclusive). -S , —scene Set the active scene for rendering. -f , —render-frame

Render frame and save it.

  • + start frame relative, — end frame relative.
  • A comma separated list of frames can also be used (no spaces).
  • A range of frames can be expressed using .. seperator between the first and last frames (inclusive).

Set the render path and file name. Use // at the start of the path to render relative to the blend-file.

Символы # заменяются номером кадра, и используются для выравнивания номера нулями слева.

  • ani_##_test.png станет ani_01_test.png
  • test-######.png станет test-000001.png

Если имя файла не содержит символов # , по умолчанию к имени добавится суффикс #### .

Номер кадра будет добавлен в конец имени файла, т.е.:

blender -b foobar.blend -o //render_ -F PNG -x 1 -a

//render_ станет //render_#### , для которого будут записаны фреймы вида //render_0001.png

-E , —engine Specify the render engine. Use -E help to list available engines. -t , —threads Use amount of for rendering and other operations [1-64], 0 for systems processor count.

Опции формата¶

-F , —render-format

Set the render format. Valid options are TGA RAWTGA JPEG IRIS IRIZ AVIRAW AVIJPEG PNG BMP

Formats that can be compiled into Blender, not available on all systems: HDR TIFF EXR MULTILAYER MPEG FRAMESERVER CINEON DPX DDS JP2

-x , —use-extension Set option to add the file extension to the end of the file.

Опции проигрывания анимации¶

Проигрывает файлы , но только когда программа работает не в фоновом режиме.

-p Open with lower left corner at , . -m Read from disk (Do not buffer). -f Specify FPS to start with. -j Set frame step to . -s Play from . -e Play until .

Опции окна¶

-w , —window-border Force opening with borders. -W , —window-borderless Force opening without borders. -p , —window-geometry Open with lower left corner at , and width and height as , . -con , —start-console Start with the console window open (ignored if -b is set), (Windows only). —no-native-pixels Do not use native pixel size, for high resolution displays (MacBook Retina ).

Отдельные опции игрового движка¶

-g Отдельные опции игрового движка

fixedtime Run on 50 hertz without dropping frames. vertexarrays Use Vertex Arrays for rendering (usually faster). nomipmap No Texture Mipmapping. linearmipmap Linear Texture Mipmapping instead of Nearest (default).

Опции Python¶

Опции отладки¶

Turn debugging on.

  • Включает обнаружение ошибок памяти
  • Отключает захват мыши для взаимодействия с отладчиком
  • Сохраняет sys.stdin Python (не устанавливает его в None)

Различные опции¶

—factory-startup Skip reading the startup.blend in the user’s home directory. —env-system-datafiles Set the BLENDER_SYSTEM_DATAFILES environment variable. —env-system-scripts Set the BLENDER_SYSTEM_SCRIPTS environment variable. —env-system-python Set the BLENDER_SYSTEM_PYTHON environment variable. -nojoystick Disable joystick support. -noglsl Disable GLSL shading. -noaudio Force sound system to None. -setaudio Force sound system to a specific device. NULL SDL OPENAL JACK . -h , —help Print this help text and exit. -R Register blend-file extension, then exit (Windows only). -r Silently register blend-file extension, then exit (Windows only). -v , —version Print Blender version and exit. — End option processing, following arguments passed unchanged. Access via Python’s sys.argv .

Experimental Features¶

—enable-new-depsgraph Use new dependency graph. —enable-new-basic-shader-glsl Use new GLSL basic shader.

Прочие опции¶

/? Print this help text and exit (windows only). —debug-freestyle Enable debug messages for FreeStyle. —debug-gpu Разрешает отладочный контекст GPU и информацию для OpenGL 4.3+. —disable-abort-handler Disable the abort handler. —verbose Установить уровень логирования

Разбор аргументов¶

Аргументы должны быть разделены пробелами, т.е.

blender -ba test.blend

…will ignore the a .

blender -b test.blend -f8

…will ignore 8 because there is no space between the -f and the frame value.

Порядок аргументов¶

Аргументы обрабатываются в том порядке, в котором они заданы, т.е.

blender --background test.blend --render-frame 1 --render-output '/tmp' 

…will not render to /tmp because —render-frame 1 renders before the output path is set.

blender --background --render-output /tmp test.blend --render-frame 1 

…will not render to /tmp because loading the blend-file overwrites the render output that was set.

blender --background test.blend --render-output /tmp --render-frame 1 

… работает как это и указано.

Переменные окружения¶

BLENDER_USER_CONFIG:
Каталог для конфигурационных файлов пользователя
BLENDER_USER_SCRIPTS:
Каталог для пользовательских скриптов
BLENDER_SYSTEM_SCRIPTS:
Каталог для общесистемных скриптов
BLENDER_USER_DATAFILES:
Каталог для пользовательских файлов данных (иконы, переводы и пр.)
BLENDER_SYSTEM_DATAFILES:
Каталог для общесистемных файлов данных
BLENDER_SYSTEM_PYTHON:
Directory for system Python libraries.
TEMP: Сохранять временные файлы сюда
TMP: или $TMPDIR Сохранять временные файлы сюда
SDL_AUDIODRIVER:
Аудиодрайвер LibSDL — alsa, esd, dma.
PYTHONHOME: Path to the Python directory, eg. /usr/lib/python.

© Copyright : This page is licensed under a CC-BY-SA 4.0 Int. License.

Настройка Blender 3D для работы со скриптами

Окинув взглядом серию статей, что перевожу, решил написать заметку про настройки Блендера. На всякий случай: вижу в тех статьях руководство, помогающее никогда не программировавшему моделлеру преодолеть барьер между тягаю полигончики и автоматизирую задачи. Думаю, это руководство непо́лно без списка вещей, облегчающих работу с кодом и скриптами в Блендере.

Версия, для которой эта заметка пишется: Blender 3.0

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

Настроим рабочее пространство

Рабочее пространство: Scripting

Лейауты, они же рабочие пространства: преднастроенный интерфейс под наборы действий. Переключение меж ними измененяет контекст работы: переключившись на Modeling, сразу окажемся в режиме редактирования выделенного объекта, в Layout наоборот, выйдем в объектный режим. А в рабочем пространстве Scripting Блендер сразу откроет важные штуки: командное окно, текстовый редактор и окошко с логом выполненных команд.

Если готовых лейаутов недостаточно, настройте свой.

Переключившись на Scripting, увидим:

Лог команд

Почти любое действие в Блендере отображается в логе. Удобно: сделайте руками что-либо, а потом скопируйте выхлоп из лога в код. А для проверки, как оно сработает, скопируйте выхлоп в командное окно. Если действие не отразилось, придётся читать мануал, или воспользоваться подсказками Блендера. Про них чуть позже.

Клац мышкой по команде выделяет её целиком, а дальше по старинке: Ctrl+C Ctrl+V

Командное окно

Встроенное командное окошко Блендера помогает проверять строчки кода по отдельности. Как напроверяемся, записываем в текстовый редактор.

Хоткеи для этого окошка:

  • Стрелки вверх и вниз прокучивают историю команд
  • Tab — автозаполнение
Текстовый редактор

Первым делом жмякаем New, и строчим аддоны прямо в Блендере. Стоит сохраняться перед каждым запуском скрипта, потому что риск закинуть Блендер в бесконечный цикл, да или просто крашнуть велик. А беспокоиться о выгрузке скрипта не стоит: в бленд-файле скрипт сохранится (если галочка с Fake User не снята).

Кстати о выгрузке. Если душа к встроенному редактору не лежит, сохраните об него пустой файл скрипта, и откройте в любимой IDE: Блендер постоянно сравнивает открытую версию с сохранённой. Alt+R перезагрузит сохранённую версию, Alt+S — сохранит её.

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

Хоткеи для текстового редактора:

  • Ctrl+Wheel отмасштабирует написанное
  • Alt+P запустит скрипт
  • Ctrl+/ закомментирует строку или выделенный блок
  • та же комбинация снимет комментарии
  • Shift+Tab снимает табуляцию со строки или блока по одной за раз
  • Tab накидывает табуляцию и на строку и на блок
  • Alt+S сохранит скрипт отдельным файлом

А ещё в нём есть шаблоны: их стоит изучить, многое станет понятнее.

Системная консоль

Windows → Toggle System Console

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

Если вы (как и я) любите бахнуть сабдив на три тысячи, включите консоль сразу по открытию Блендера: прерывание процессов быстрее переоткрывания бленд-файла.

Хоткей прерывания: Ctrl+C в консоли.

Автозаполнение в консоли Блендера

Конструкции вроде bpy.context.selected_editable_objects[0] неудобны для печати, а с непривычки и для осмысления. С осмыслением сложнее, а вот с печатью — жмяк табом, и Блендер выдаст зелёный список, какие существуют возможности продолжить фразу.

Подсказки

По умолчанию включены пользовательские подсказки: при наведении на элемент мышки появляется окно с описанием того, что этот элемент делает. А ещё есть подсказки для будущих скриптописателей: тогда в них окажется кусок кода, заменяющий физическое взаимодействие с элементом. Таким образом можно писать макросы в духе импортировать файл → отрендерить → удалить объект → повторить n раз. Написать ремешер будет дивно сложнее.

Включить подсказки: Edit → Preferences → Interface → Display → Python Tooltips
Скопировать подсказку: Ctrl+Alt+Shift+C (мой любимый хоткей, без гитары не справился бы).

Режим разработчика

Полезен тем, что даёт визуальный доступ к индексам вершин: пригодятся для постройки мешей.

Включить: Edit → Preferences → Interface → Display → Developer Extras

После чего в режиме редактирования появится нужная галочка:

Дополнительный экран

Windows → New Window откроет второе окно блендера с этим же файлом.

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

Развернуть окно во весь экран

Ctrl+Space разворачивает внутреннее окно, над которым мышь, во весь экран. Эта же комбинация разворачивает рабочее пространство обратно.

Коротенько:

  • системная консоль подскажет ошибки и спасёт нервы
  • включите подсказки
  • переключите рабочее пространство на Scripting
  • автодополнение: поменьше печатать, получше понимать структуру модулей
  • режим разработчика: пересчитай свои вертексы
  • доролнительные окна и разворот целевого помогут организовать работу удобно
  • сохраняйтесь чаще!

Туториалы, которые перевожу:

  • Меши с Python & Blender: двумерная сетка
  • Меши с Python & Blender: кубы и матрицы
  • Меши с Python & Blender: икосферы
  • Меши с Python & Blender: скруглённый куб
  • Меши с Python & Blender: круги и цилиндры

Blender 2.6 + Python 3.2 – задействуем устройства ввода в собственной игре

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

Однако, если мы хотим разработать полноценную игру, то нам нужно более серьезное решение – современная игровая индустрия предлагает гораздо более широкий выбор устройств ввода, чем одна только клавиатура, а список возможных действий и событий в игре не исчерпывается движением единственного игрового объекта.
В данном топике мы попробуем создать законченную, расширяемую, универсальную систему взаимодействия с устройствами ввода в игре при помощи программирования на Python 3.2 в Blender 2.6.

Лирическое вступление

— Blender – программа не только для создания трехмерной графики. С некоторых пор Blender имеет все необходимые инструменты для разработки игр. Например, на Блендере сделаны Yo Frankie, Dead Cyborg (обе игры работают на Linux, Windows, Mac). Кстати, это игры с открытым исходным кодом. Кроме того, сам Блендер бесплатен, его исходный код открыт.

— По желанию хабражителей в данном топике будет использоваться последняя версия Блендера. На момент написания текста это версия 2.60a, которая уже содержит Python 3.2. Скачать Blender 2.60a для всех поддерживаемых платформ можно по этой ссылке: http://download.blender.org/release/Blender2.60/ (скачать именно для Windows).
В прошлом топике использовалась версия 2.49b, обозванная «одной из самых стабильных и проверенных» из всех версий. Увы, поработав с Бледрером версии 2.60a, можно сказать, что это была чистая правда – «крахи» программы теперь стали обыденным делом, хотя раньше практически не случались. В целом, от перехода на новую версию ощущения такие, словно кто-то чужой убрался в квартире: когда заходишь в первый раз, вроде очень приятно, красиво, закругленные края, новый API, порядок и чистота… Но как только надо быстро найти какую-то вещь, то нихера ничего найти нельзя. В прошлом «беспорядке» и то быстрее ориентировался. Конечно, это нормально, главное – привыкнуть.

— в прошлом топике было много скриншотов, но вот Logic Bricks были описаны поверхностно, а принципы работы скриптов в Блендере не упоминались почти совсем. В данном топике важнее всего понимание Logic Bricks и функционирования Питона, поэтому больше будут раскрываться именно эти вопросы. Полностью раскрываться.

— ссылка на архив готового проекта, который описывается в статье — http://ifolder.ru/27335587

Цель

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

Как? Blender, Blender Game Engine, Logic Bricks

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

Особо не вдаваясь в подробности, условимся, что файл проекта в Блендере имеет расширение .blend, содержит в себе сцены, в которых, в свою очередь, размещаются игровые объекты. Уровни, экраны загрузки, игровое меню — всё в игре может быть реализовано именно как сцены. То есть каждый уровень – отдельная сцена, экран загрузки – отдельная сцена, игровое меню – отдельная сцена, и все они при этом находятся в одном файле .blend (вообще, если уровней много, то можно раскидать их по разным файлам .blend, затем импортируя нужные в определенный момент).

Игровая система, то есть манипуляция объектами, их поведение, изменение состояний игры, реакция на события и многое другое в Блендере реализуется с помощью Logic Bricks. По сути, это есть визуальное программирование. Система держится на трех китах: сенсоры, контроллеры, актуаторы.

1. Сенсоры – это «раздражители», события, ситуации: нажатие клавиши, загрузка сцены, движение мышью, столкновение объектов, приближение объекта к другому объекту и так далее.

Грубо говоря, сенсоры могут работать так: произошло (началось) нужное событие (враг появился в поле зрения игрока, произошла коллизия объектов и т.п.) – сенсор посылает один положительный сигнал, «становится в положительное» состояние (sensor.positive = 1) и не посылает больше сигналов. Когда событие прекратилось (враг исчез из поля зрения игрока, коллизия объектов исчезла и т.п.) – сенсор посылает один отрицательный сигнал и «становится в отрицательное» состояние (sensor.positive = 0). Важно понять, что в промежутке между «произошло нужное событие» и «событие прекратилось» сенсор хоть и имеет положительное состояние, но специально об это никого не уведомляет – сигналы отправляются только при наступлении и прекращении события. Можно сделать так, чтобы сенсор отправлял сигналы на протяжении всего времени, пока он положителен (от начала и до конца события, например, пока враг виден игроку или пока объекты прикасаются друг к другу) – нужно поставить сенсор в режим Pulse Mode, в результате он будет посылать сигналы беспрерывно пока сам положителен.
Но кому посылаются все эти сигналы? Контроллерам.

2. Контроллеры – это механизмы, которые получают сигнал от сенсоров, и решают, будет ли далее активирован какой-либо актуатор и какой именно.

Грубо говоря, контроллеры могут быть булевыми операторами — AND, OR и т.п. — что позволяет группировать и соотносить их между собой. А могут быть скриптом (или модулем) на Питоне – то есть, когда сигнал сенсора будет отправлен контроллеру, выполнится конкретный скрипт (или функция модуля). Подробнее этот вопрос будет рассмотрен ниже.
Сенсоры срабатывают по наступлению событий, отправляют сигнал контроллерам и те, если решат, что все хорошо, активируют актуаторы.

3. Актуаторы – это «действия»: придать скорость игровому персонажу, сделать объект невидимым, запустить анимацию, выйти из игры и так далее.

Как-как… Каком кверху!

Теперь о скриптах. Как можно запустить скрипт? Способов много: есть колбэки; есть фоновый режим; в Блендере есть интерактивная конслоль; в Блендере есть отдельная панель редактирования кода с нумерованием строк, подсветкой и прочим, в которой можно запустить скрипт по нажатию ALT + P; есть Script Operators; в конце концов, у нас всегда есть exec(), который на полном серьезе предлагается в качестве рабочего варианта.

А есть сенсор Always (и много других сенсоров имеется, на каждый случай свой), к которому можно «подцепить» контроллер со скриптом, чтобы исполнять его в начале или даже на протяжении всего игрового времени.

Можно потратить немало времени, изучая все вышеперечисленные способы, каждый из них подарит пытливому исследователю свой собственный, индивидуальный букет особенностей, контекстно-зависимое поведение, свою область видимости и вообще – у кого нет недостатков в наше время? Хотя правильнее было бы говорить о предназначении и «уместности» применения тех или иных средств. Так вот, судя по всему, кроме варианта с сенсором Always ни один другой способ не предназначен для постоянного, системного использования для контроля игры, событий, игровых объектов, их поведения и взаимодействия и прочего. Поэтому для этих целей мы будем использовать именно данный метод, то есть сенсоры, контроллеры (Logic Bricks) + скрипты (Python).

Logic Bricks + Python

Как же будет работать подобная система?

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

Рассмотрим ситуацию, когда мы хотим заставить врага бежать, пока он в поле зрения игрока. Допустим, мы создали сенсор, который реагирует на появление врага в поле зрения игрока и посылает сигнал на контроллер типа Python, на который мы «повесили» скрипт runLolaRun.py. Этот скрипт придает врагу скорость 5 км/ч. Итак, враг попадает на глаза игроку, срабатывает сенсор и посылает один положительный сигнал котроллеру; котроллер запускает на исполнение скрипт runLolaRun.py и врагу придается скорость. Что дальше? А дальше врага ждет незавидная участь: ведь он остановится и останется один на один с игроком. Почему он остановился? Потому что скрипт исполнился только одни раз – по срабатыванию сенсора. Если мы хотим это преодолеть, то должны поставить сенсор в режим Pulse Mode – благодаря этому сигнал от сенсора будет посылаться на контроллер «непрерывно» на протяжении всего события, и скрипт будет исполняться «непрерывно», поддерживаю скорость на нужном значении. «Непрерывно» в данном случае означает с частотой N тиков в секунду, не важно, сколько именно, наша проблема в другом.

А проблема в том, что каждый раз выполняется весь код скрипта (по новой объявляются функции, переменные), после выполнения все они бесследно «отмирают». То есть, если враг будет находиться в поле зрения игрока 10 секунд, и сенсор будет на протяжении этого времени посылать положительный сигнал на котроллер с частотой N тиков в секунду, то не трудно подсчитать, сколько раз будет по новой исполнен весь скрипт. Это звучит довольно… неприятно, если мы хоть немного думаем о производительности. Вообще, это не так страшно, если скрипт не велик. Но в серьезной игре скриптов много, функций много, строк кода тоже.

Так и запишем: вопрос производительности.

Это еще не всё. Допустим, у нас есть скрипт movement.py, отвечающий за движение любого рода в игре – в нем содержатся все соответствующие функции; еще есть скрипт keybind.py – отвечающий за назначение и обработку клавиш; есть скрипт systemfunctions.py – в котором содержатся «системные» функции (log, initGame, cleanSrting и прочее); есть скрипт savegame.py – он отвечает за сохранения.
Если нам нужно использовать движение в игре, мы создаем сенсор и присоединяем его к контроллеру, к которому «привязан» скрипт movement.py. Объект будет двигаться. Но что, если во время движения мне нужно вызвать «системную» функцию или сохраниться? Ведь все эти функции определены в соответствующий скриптах (system.py и savegame.py ), а к контроллеру «привязан» только скрипт movement.py и только он исполняется по сигналу сенсора. Как вызвать нужные функции из других скриптов в скрипте movement.py? Неужели придется объединять все скрипты в один? (Но тогда этот скрипт будет огромен и выполнять его по N тиков в секунду будет весьма затратно.) Или скопировать только нужные функции из других скриптов в скрипт movement.py? (Но тогда придется копировать эти функции каждый раз, когда они изменятся.)

Так и запишем: вопрос повторного использования кода.

Решение. Что я могу Вам предложить, мсье?

Вообще, Блендер любезно предоставляет «суперглобальный» словарь bge.logic.globalDIct. Он сохраняет переменные между загрузками сцен и даже между загрузками .blend файлов и эти переменные доступны из любого скрипта в любой момент времени. Можно попытаться решить вопрос производительности и повторного использования кода так: при инициализации игры один раз «записать» в bge.logic.globalDIct все нужные функции, чтобы затем их использовать из любого скрипта в любой момент (либо даже в версиях Блендера 2.4* «записать» в GameLogic.myvar, как советуют некоторые источники в сети).
В Блендере версии 2.49b попытка проделать такой фокус с функциями давал фейл. В переменной содержался адрес памяти, где хранилась функция, даже вызвать её было можно, но исполнялась она в каком то своём загадочном контексте, где не было даже built-in функций Питона (хотелось бы узнать, как такое вообще возможно? вместо «суперглобальной» функции мы получили какую то «суперанонимную»). И это правильно, ведь в документации написано:

Note: only python built in types such as int/string/bool/float/tuples/lists can be saved, GameObjects, Actuators etc will not work as expected.

Правда, в Блендере версии 2.60a такой способ не приводит к ошибке. Но строить с использованием этого костыля всю игровую систему – большой риск, получится колосс на глиняных ногах.

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

Только у нас не просто будут модулями те самые файлы movement.py, keybind.py, savegame.py. Сделай мы их модулями, это решило бы лишь вопрос повторного использования кода. Но вопрос производительности все равно остался бы полуоткрытым – хоть мы и имели бы «скомпилированную» версию модулей, тем не менее, весь код (нужный и не нужный) исполнялся бы по N тиков в секунду.

Мы же используем возможность Блендера, которая появилась в версиях выше 2.49. Начиная с этой версии котроллер типа Python может быть «завязан» не только на скрипт на Питоне, но и на модуль, точнее — на конкретную функцию модуля. То есть, если раньше мы прописывали в контроллере ссылку на файл movement.py, то теперь мы можем написать movement.go. Это значит, что каждый раз, когда сработает сенсор и на котроллер будет отправлен сигнал, будет выполнена функция go модуля movement.py.Тут важно понять, что остальная часть кода (всё, кроме функции go) исполняется только при первом вызове, затем уже повторно не исполняется – при этом все переменные и функции, объявленные при первом вызове, доступны всегда. Это решение вопроса производительности. Подробно в документации, самые последние абзацы.

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

Лирическое отступление

Люди задаются многими бессмысленными вопросами: что важнее – душа или тело? Человек – он англел или демон? Кто лучше — мужчина или женщина? Табличная или дивная?

В Блендере подобным притянутым за уши вопросом мог бы быть вопрос: Logics Bricks или Python?

На самом деле, истина всегда где-то в середине: душа пребывает в теле все время своих перерождений; человек – не демон и не ангел, а человек; в природе нет мужчин и женщин, есть «мужское» и «женское», причем «никогда ни в одном индивиде одно качество не составляет 100% пола»; табличная верстка имеет свою область применения… кхе-кхе.

Разработчики Блендера поступили столь же мудро: невозможно на Блендере создать серьезную игру исключительно на Logic Bricks или только с помощью программирования на Питоне – их нужно совмещать.

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

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

Любые крайности в этом вопросы чреваты. Если попытаться создать полноценную игру только на Logic Bricks, то результат будет очень сильно смахивать на вермишель с кубиками лего (и дело совсем не в эстетическом восприятии, а в том, что такую систему трудно поддерживать). Если же вы видите окружающий мир нулями и единицами, словно Нео в Матрице, и будете настаивать на отказе от «визуального программирования», то рано или поздно дойдете до изменения исходников Blendera и компилирования своих билдов.

Моя игра

Наконец то мы получили достаточно знаний и понимания и можем приступить к игре.

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

Проект состоит из папки /mygame, в которой:
— game.blend – файл Блендера (о том, как он устроен, написано в начале топика)
— game.py и events.py – модули Питона, которые мы написали для нашей игры (они будут подробно описаны позже)
— папка /data – в ней будут храниться сериализованные данные о настройках клавиатуры (файл actskeys), настройках игры (файл settings) и прочие данные (actskeys_default, settings_default – это файлы с данными «по умолчанию», которые нам нужны на случай, если основные файлы с данными игрока «испортятся»)
— папка /__pycache__ — появится после первого запуска игры, содержит «скомпилированные» версии наших модулей

Вспомним, что должна уметь наша система взаимодействия с устройствами ввода: назначать действия на клавиши (в том числе несколько действий на одну клавишу), сохранять настройки, переназначать клавиши по усмотрению игрока. Как именно будет всё это реализовано?

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

С мышью всё немного сложнее. Хорошо, что разработчики Блендера не стали создавать отдельный тип сенсора для каждой кнопки на клавиатуре – есть один сенсор типа «Keyborad», который можно назначить любой клавише или всем сразу. Но с мышью все иначе – есть один тип сенсора «Mouse», но назначить его одного на все события мыши нельзя. Необходимо создавать отдельные сенсоры на движение мышью, на правую кнопку мыши, на левую, на среднюю и даже на колесикоВверх и колесикоВниз. Поэтому, если с клавиатурой мы обошлись элегантно: один сенсор на все клавиши + один Python-контроллер, то на мышь мы потратим пять сенсоров (правая кнопка, левая кнопка, средняя, колесикоВверх, колесикоВниз) и привяжем их всех к тому же самому, что и на клавиатуре, контроллеру с модулем на Питоне.

Но неужели нам придется писать в коде что-то вроде «if buttons(‘W’).isPressed():vpered() else if buttons(‘S’).isPressed(): Nazad()». Нет, конечно, ведь нам нужна универсальная, расширяемая, гибкая система.

Возьмем такую ситуацию. В игре у нас на клавиши W/S назначены шаг вперед/назад соответственно. Так же у нас есть момент, когда игрок подходит к панели управления и берет на себя управление роботом, при этом движение робота назад/вперде так же назначены на W/S. После того, как управление роботом будет завершено, нам нужно снова двигать игрока по тем же клавишам. Хоть это и простой пример, в игре может быть много подобных «пересечений». Как это организовать?

Для возможности непротиворечивого назначения и обработки действий, в проекте использована следующая схема: Контекст->Процессы->Действия

Легче всего объяснить это на примере.
Пример контекстов: «загрузка игры», «Игровой процесс», «управление транспортным средством», «просмотр игрового меню» и т.д.
Пример процессов: «движение», «стрельба», «управление роботом» и т.д.
Пример действий: шаг вперед, шаг назад, приседание, выстрел, приказ роботу двигаться вперед, приказ роботу кружиться вокруг своей оси и так далее.

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

Например, если в игре текущий контекст «Загрузка игры», то нажатие на любую клавишу ни к каким действиям приводить не должно. Следовательно, этот контекст не будет содержать в себе никаких процессов.
Допустим, в игре текущий контекст это «Игровой процесс». Мы хотим, чтобы игрок двигался, стрелял. Тогда мы «наследуем» от контекста «Игровой процесс» контекст «Управление игроком». В этот контекст «Управление игроком» включаем процессы «Движение» и «Стрельба». В процесс «Движение» включаем действия «Шаг вперед», «Шаг назад», а в процесс «Стрельба» включаем действие «Выстрел». На каждое действие у нас заранее назначена нужная клавиша, а значит, теперь каждый раз, когда игра будет иметь текущий контекст «Управление игроком», по нажатию на соответствующую клавишу игрок будет ходить назад или вперед, и стрелять.

Вернемся к примеру с управлением роботом. Как реализовать передачу управления? Достаточно создать новый контекст «Контроль объекта», создать процесс «Управление роботом», создать действия «робот вперед» и «робот назад» (которым назначенные клавиши W/S). Контекст «Контроль объекта» наследуется от контекста «Игровой процесс». В контекст «Контроль объекта» включается процесс «Управление роботом», а в процесс – действия. Когда игрок подходит к панели управления, нам достаточно поменять контекст игры с «Управление игроком» на «Управление роботом» — теперь по нажатию на те же клавиши W/S будут исполняться другие действия.

Зачем наследовать один контекст от другого? В примере мы наследуем контекст «Управление игроком» от контекста «Игровой процесс». Вообще, наследование нужно, чтобы можно было не объявлять напрямую в каждом контексте все процессы. Допустим, у нас есть контекст «Игровой процесс» и в нем 10 процессов и есть контекст «Управление игроком», в котором те же 10 процессов плюс еще 1. Тогда можно один раз объявить все процессы при создании контекста «Игровой процесс», потом при создании контекста «Управление игроком» объявить в нем только 1 дополнительный контекст, а остальные 10 унаследовать от контекста «Игровой процесс».

Программный код, ура!

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

В нашем проекте два модуля (скрипта):
— game.py – в нем реализованы функции инициализации игры, загрузки игровых данных, сохранения настроек и прочие. Этот модуль (точнее, его конкретная функция initialize) вызывается один раз в момент загрузки уровня, чтобы инициализировать игру (объявить контексты, процессы и т.д.) и загрузить настройки (назначения клавиш и т.д.). После этого он уже используется только как импортируемый модуль в модуле events.py
— events.py – модуль, в котором реализованы функции обработки изменений состояний клавиш, функция работы с контекстом и процессами, все процессы и прочее

Рассмотрим подробнее каждый модуль и его функции.

Модуль game

Все начинается с импорта модуля bge, который является модулем игрового движка Блендера (blender game engine).

import bge 

Далее по функциям.
— Функция initialize вывается при первой загрузке модуля.

def initialize(): # initialize game data if not 'GAME_STATE' in bge.logic.globalDict: # that means we just started the game bge.logic.globalDict['GAME_STATE'] = None bge.logic.globalDict['SETTINGS'] = <> bge.logic.globalDict['CURRENT_CONTEXT'] = > bge.logic.globalDict['CONTEXTS'] = < 'loadingScreen':<'parents':[], 'processes':<>>, 'inGame': <'parents':[], 'processes' : < 'gameControlling':True, 'doNothing':True >>, 'secondView': >, 'objectControlling': > > bge.logic.globalDict['PROCESSES'] = < 'playerMovement':>, 'playerShooting':>, 'robotKeying':>, 'gameControlling':>, 'doNothing':> > # here we define dictionary with all keys bge.logic.globalDict['KEYS'] = <> bge.logic.globalDict['MOUSEKEYS'] = [] # MOUSEKEYS - is a list of mouse keys names for key in bge.events.__dict__: if key.isupper(): bge.logic.globalDict['KEYS'][key] = if key.find('MOUSE') != -1 and key in getObject('player').sensors: bge.logic.globalDict['MOUSEKEYS'].append(key) #load game data from files and initialize default context if (initGameData() and initContext(bge.logic.globalDict['SETTINGS']['context'])): bge.logic.globalDict['GAME_STATE'] = 'INITIALIZED' bge.logic.setGravity(bge.logic.globalDict['SETTINGS']['gravity']) # show debug info into console # -1 - write nothing into console # 0 - errors only # 1 - errors and alerts # 2 - errors, alerts and all other # please keep in mind writing all debug info in console will slow game bge.logic.globalDict['SETTINGS']['debug'] = 1 else: log('We could not initialize the game!', 0)

Сначала мы проверяем, есть ли в «суперглобальном» словаре элемент «GAME_STATE», и если его нет это значит мы запускаем игру в первый раз. Надо понимать, что данная функция исполняется при загрузке каждого уровня в игре, но при загрузке второго и последующих уровней нам уже не надо назначать абсолютно все данные (конексты, процессы, клавиши — они уже есть). Именно для этих целей и нужна проверка GAME_STATE.
Далее в словарь добавляются все контексты, процессы.
Далее нам нужно создать словарь со всеми клавишами (клавиатуры и мыши) с их кодами и строковыми именами. Для этого мы используем словарь bge.events.__dict__ ю
Далее уже мы загружаем из папки /data настройки, сохраненные в файлы и инициализируем контекст по умолчанию.
Далее, если все прошло удачно, можно изменить настройки, загруженные из файлов.
Например, такую настройку как вывод отладочной информации в консоль. Для этого мы изменяем переменную bge.logic.globalDict[‘SETTINGS’][‘debug’]. Установите её в -1 чтобы не выводить никакую информацию в консоль; 0 — чтобы выводить только ошибки; 1 — чтобы выводить ошибки и алерты; 2 — чтобы выводить всю отладочную информацию (даже каждый тик сенсора клавиатуры). Важно помнить, что вывод всей информации в консоль сильно тормозит игру.

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

# validate all game data loaded from files and initialize def initGameData(): log('initGameData()', 1) if not (loadGameData('ACTSKEYS') and loadGameData('SETTINGS')): log('initGameData() failed!', 0) return False for act in bge.logic.globalDict['ACTSKEYS']: key = bge.logic.globalDict['ACTSKEYS'][act] if len(key) > 0 and not key in bge.logic.globalDict['KEYS']: bge.logic.globalDict['ACTSKEYS'][act] = '' log('Key "' + key + '" wich assigned to act "' + act + '" doesnot exist in KEYS' , 0) unknownActs = <> for process in bge.logic.globalDict['PROCESSES']: bge.logic.globalDict['PROCESSES'][process]['conditionals']['keys'] = [] for act in bge.logic.globalDict['PROCESSES'][process]['conditionals']['acts']: if not act in bge.logic.globalDict['ACTSKEYS']: log('Process: ' + process + ', act: ' + act + ' - does not exist in ACTSKEYS', 0) if process in unknownActs: unknownActs[process].append(act) else: unknownActs[process] = [act] elif len(bge.logic.globalDict['ACTSKEYS'][act]) > 0: bge.logic.globalDict['PROCESSES'][process]['conditionals']['keys'].append(bge.logic.globalDict['ACTSKEYS'][act]) for process in unknownActs: for act in unknownActs[process]: bge.logic.globalDict['PROCESSES'][process]['conditionals']['acts'].remove(act) return True 

Как видно из кода, сначала вызывается «loadGameData(‘ACTSKEYS’) and loadGameData(‘SETTINGS’)» — на самом деле это и есть функции, которые работают «на земле» непосредственно с файлами, но о них в следующем шаге.
Здесь же мы должны понять, что при назначении клавиш, действий, процессов и контекстов может произойти путаница, возникнуть противоречия: будет назначена несуществующая клавиша, использовано несуществующее действие и т.д. Именно эти случаи и проверяются в функции initGameData(): несуществующие клавиши просто удаляются из привязки, несуществующие действия так же удаляются.

— Функция loadGameData() принимает название файла с настройками, которые надо загрузить. Пока это только actskeys — назначение клавиш и settings — общие настройки игры.

# load game data from files in /data directory def loadGameData(dataName, attempt = 0): import os log('loadGameData(): ' + dataName, 1) if attempt > 2: log('To many run attempts of loadGameData()', 0) return False gameDataFileName = dataName.lower() gameDataFilePath = 'data/' + gameDataFileName gameDataDefaultFilePath = gameDataFilePath + '_default' if not os.path.exists(gameDataFilePath): log('There is no file with gamedata: ' + gameDataFilePath, 0) gameDataFileName = '' else: log('We try to eval content of gamedata file now') try: fileResource = open(gameDataFilePath) gameData = eval(cleanData(fileResource.read())) fileResource.close() log('We did and it gives: ' + str(gameData), 1) if isinstance(gameData, dict) != True: log('Eval gives invalid data', 0) gameDataFileName = '' else: bge.logic.globalDict[dataName] = gameData log('It looks like gamedata loaded!', 1) except: log('Eval was failed!', 0) gameDataFileName = '' if not gameDataFileName: log('Lets try to use default file of gamedata: ' + gameDataDefaultFilePath, 1) if not os.path.exists(gameDataDefaultFilePath): log('There is no default file of gamedata', 0) return False else: import shutil shutil.copyfile(gameDataDefaultFilePath, gameDataFilePath) return loadGameData(dataName, attempt + 1) return True 

Сначала функция пытается загрузить файл настроек игрока, потом, если его нет, ищется файл настроек по умолчанию. Если он есть — его содержимое копируется в файл настройки игрока и функция запускается по новой. Если же ни файла настроек игрока, ни файла настроек по умолчанию, то в консоль пишется ошибка и возвращается False.
Важно понять, что данные хранятся в файлах как «сериализованные»словари.
Перед загрузкой данных происходит их «очистка» функцией cleanData.

— Функция saveGameData() сохраняет настройки в файл в папку /data.

# save game data into files in /data directory def saveGameData(dataName): import os log('saveGameData(): ' + dataName) if not os.path.exists('data'): log('There is no directory to save gameData! Try to make it', 0) os.mkdir('data') gameData = cleanData(repr(bge.logic.globalDict[dataName.upper()])) #we must to try eval gameData we want to save just for be sure we will not save incorrect data try: if isinstance(eval(gameData), dict) != True: log('Evaling gameData before saving gives incorrect data!', 0) return False else: log('It looks like gamedata ready to save!') except: log('Evaling gameData before saving gives an error!', 0) return False gameDataFile = open('data/' + dataName.lower(), 'w') gameDataFile.write(gameData) gameDataFile.close() log('gameData saved!') return True 

— Далее мы рассмотрим важную функцию initContext() — которая инициализирует переданный контекст.

# initialize context def initContext(context): log('initContext(): ' + context, 1) if bge.logic.globalDict['CURRENT_CONTEXT']['name'] == context: return False if context in bge.logic.globalDict['CONTEXTS']: bge.logic.globalDict['CURRENT_CONTEXT']['name'] = context bge.logic.globalDict['CURRENT_CONTEXT']['processes'] = <> # extends all PROCESSES from parents for parent in bge.logic.globalDict['CONTEXTS'][context]['parents']: for process in bge.logic.globalDict['CONTEXTS'][parent]['processes']: if bge.logic.globalDict['CONTEXTS'][parent]['processes'][process] == True: bge.logic.globalDict['CURRENT_CONTEXT']['processes'][process] = bge.logic.globalDict['PROCESSES'][process] # extends all PROCESSES in initing context process = '' for process in bge.logic.globalDict['CONTEXTS'][context]['processes']: if bge.logic.globalDict['CONTEXTS'][context]['processes'][process] == True: bge.logic.globalDict['CURRENT_CONTEXT']['processes'][process] = bge.logic.globalDict['PROCESSES'][process] elif process in bge.logic.globalDict['CURRENT_CONTEXT']['processes']: bge.logic.globalDict['CURRENT_CONTEXT']['processes'].pop(process) log('This CONTEXT successfully inited: ' + context, 1) log(bge.logic.globalDict['CURRENT_CONTEXT'], 1) return True else: log('This CONTEXT doesnot exist: ' + context, 0) return False 

При инициализации контекста, сначала он наследует все процессы своих родителей. Затем проверяются его собственные процессы. Если какой-либо процесс выставлен в False, то он удаляется — это позволяет удалять один или несколько ненужных процессов, унаследованных от родителя.

— Функции log() и cleanData() занимаются записью отладочной информации в консоль и очисткой данных соответственно.

def log(message, type = 2): if bge.logic.globalDict['SETTINGS']['debug'] >= type: print(('ERROR: ', 'INFO: ', ' - ')[type] + str(message)) def cleanData(data): import re data = str(data); data = re.sub("[^a-zA-Z0-9_<>:,\s'\[\]\-.]|[\t\n]", '', data, flags = re.IGNORECASE); data = re.sub('^\s+|(\s+)|\s+$', '', data) return data 

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

Теперь мы подошли к непосредственно «игровым» функциям.

— Первая из низ — getObject()

def getObject(object): return bge.logic.getCurrentScene().objects[object] 

Она возвращает объект, имя которого передано фукнции.

— Важная функция getKeyStatusByAct() возвращает статус кнопки, привязанной к действию.

# get status of key binded to act def getKeyStatusByAct(act): #KX_INPUT_NONE = 0, KX_INPUT_JUST_ACTIVATED = 1, KX_INPUT_ACTIVE = 2, KX_INPUT_JUST_RELEASED = 3 keyString = None if act in bge.logic.globalDict['ACTSKEYS']: keyString = bge.logic.globalDict['ACTSKEYS'][act] if not keyString: log('Act: ' + act + ' is not belong to any key', 0) return False else: log('Act: ' + act + ' does not exist in ACTSKEYS', 0) return False keyCode = bge.logic.globalDict['KEYS'][keyString]['keyCode'] keyStatus = 0 if keyString.find('MOUSE') != -1: keyStatus = bge.logic.mouse.events[keyCode] else: keyStatus = bge.logic.keyboard.events[keyCode] return keyStatus 

Тут важно понять, что к действию может быть привязано любая клавиша. Поэтому функции передается имя действия (а не клавиши) и уже потом по настройкам назначений клавиш (которые мы уже загрузили) находится строковое имя и код клавиши. Затем необходимо проверить статус этой кнопки.
Важный момент — кнопки в Блендере могут иметь 4 состояния: не нажата = 0, только что нажата = 1, нажата =2, только что отпущена =3.

— Далее идет функция isTouched().

# returns true if objects collision exists def isTouched(object, secondObject): touched = False collisions = getObject(object).sensors['collisions'].hitObjectList for hitObject in collisions: if hitObject.name.startswith(secondObject): touched = True break return touched 

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

Модуль events

Здесь тоже в начале импортируется модуль bge (blender game engine), а так же наш модуль game. К его функциям мы теперь можем обращаться как game.initContext, game.saveGameData и т.д.

import bge, game

Далее по функциями модуля events.

— Функции playerMovement(), robotKeying(), gameControlling() — это ни что иное, как функции процессов, объявленных при загрузке игры. Именно в них реализовано поведение (действия), требуемое от процессов — движение игрока, управление роботом и контроль за состоянием игры.

def playerMovement(): # Y - forward/back # X - left/right # Z - up/down player = game.getObject('player') Y = X = Z = 0.0 coef = 1 # factor which will be more if we will run if game.getKeyStatusByAct('playerRun'): coef = 2.5 applyingForce = coef * bge.logic.globalDict['SETTINGS']['forceApplyingToPlayer'] if game.getKeyStatusByAct('playerMoveBack'): Y = -applyingForce if game.getKeyStatusByAct('playerMoveForward'): Y = applyingForce if game.getKeyStatusByAct('playerMoveLeft'): X = -applyingForce if game.getKeyStatusByAct('playerMoveRight'): X = applyingForce if game.getKeyStatusByAct('playerJump') and game.isTouched('player', 'ground'): Z = 250 if Y and X: Y = Y/1.5 X = X/1.5 player.applyForce([X, Y, Z], 1) def robotKeying(): robot = game.getObject('robot') applyingMovement = bge.logic.globalDict['SETTINGS']['robotMovementStep'] Y = X = 0 if game.getKeyStatusByAct('robotMoveBack'): X = applyingMovement if game.getKeyStatusByAct('robotMoveForward'): X = -applyingMovement if game.getKeyStatusByAct('robotMoveLeft'): Y = -applyingMovement if game.getKeyStatusByAct('robotMoveRight'): Y = applyingMovement if game.getKeyStatusByAct('robotDoBarrel'): robot.applyRotation([0, 0, 0.1], 0) if game.getKeyStatusByAct('robotReplaceMesh') == 1: robot.replaceMesh(game.getObject('robot_replacement').meshes[0], True, True) robot.applyMovement([X, Y, 0], 0) def gameControlling(): if game.getKeyStatusByAct('returnToDefaultContext') == 3: game.initContext(bge.logic.globalDict['SETTINGS']['context']) elif game.getKeyStatusByAct('changeKeyBind') == 3: keys = ['WKEY', 'SKEY', 'AKEY', 'DKEY'] if (bge.logic.globalDict['ACTSKEYS']['playerMoveForward'] == 'WKEY'): keys = ['UPARROWKEY', 'DOWNARROWKEY', 'LEFTARROWKEY', 'RIGHTARROWKEY'] bge.logic.globalDict['ACTSKEYS']['playerMoveForward'] = keys[0] bge.logic.globalDict['ACTSKEYS']['playerMoveBack'] = keys[1] bge.logic.globalDict['ACTSKEYS']['playerMoveLeft'] = keys [2] bge.logic.globalDict['ACTSKEYS']['playerMoveRight'] = keys [3] game.saveGameData('ACTSKEYS') game.initGameData() 

Функция playerMovement() применяет функция Блендера applyForce() для того, чтобы применить силу к объекту (у нас это главный игрок), после чего объект начинает двигаться.
Для разнообразия функция robotKeying() применяет другую функцию Блендера applyMovement() для того, чтобы уже точно указать, насколько мы хотим изменить положение объекта (у нас это робот), благодаря чему объект движется.
Функция gameControlling(), в свою очередь, позволяет нам возвращаться в контекст по умолчанию (действие returnToDefaultContex), переназначать клавиши и сохранять новые настройки.

— Следующая функция findProcessByKey() очень важна.

# find process which has touched key def findProcessByKey(keyString): game.log('\nfindProcessByKey(): ' + keyString + ', current context: ' + bge.logic.globalDict['CURRENT_CONTEXT']['name']) if not bge.logic.globalDict['CURRENT_CONTEXT']['processes']: game.log('Current CONTEXT does not have any processes', 0) return False game.log('Lets try to find PROCESS wich has touched key in conditionals') for process in bge.logic.globalDict['CURRENT_CONTEXT']['processes']: run = True game.log('Current PROCESS we check: ' + process) #HERE IS SOME PROBLEM if process in bge.logic.globalDict['CURRENT_CONTEXT']['processes'] and 'conditionals' in bge.logic.globalDict['CURRENT_CONTEXT']['processes'][process]: keys = bge.logic.globalDict['CURRENT_CONTEXT']['processes'][process]['conditionals']['keys'] if keyString in keys: game.log('Get it!') if process in processedProcesses: game.log('This process already checked in this event') continue processedProcesses[process] = True else: game.log('PROCESS does not have touched key in conditionals') run = False else: processedProcesses[process] = True game.log('This PROCESS does not have any conditionals') if run: game.log('All conditionals matched or no conditionals need, we can run this PROCESS now: ' + process) if process in globals(): globals()[process]() else: game.log('This process does not have assigned function: ' + process, 0) else: game.log('Process was not activated') 

Именно она ищет по заданной клавише (в функцию передается строковое имя клавиши) все процессы, которые „зависят“ от состояния этой клавиши и „запускает“ эти процессы.

— Функция key() вызывается при каждом изменении состояния любой клавиши.

# this function runs every time any key changes status def key(): globals()['processedProcesses'] = <> controller = bge.logic.getCurrentController() keyboard = controller.sensors['keyboard'] for key in keyboard.events: findProcessByKey(bge.events.EventToString(key[0])) for key in bge.logic.globalDict['MOUSEKEYS']: if controller.sensors[key].triggered: findProcessByKey(key) 

Далее для каждой клавиши, чье состояние изменилось в данном событии, вызвается фукнция findProcessByKey().

— Функция context() позволяет изменять контекст в игре.

# this function changes context def context(): controller = bge.logic.getCurrentController() owner = controller.owner context = bge.logic.globalDict['SETTINGS']['context'] for sensor in owner.sensors: if sensor.triggered and owner.get('context') and sensor.positive: context = owner.get('context') break game.initContext(context)

Это происходит при срабатывании сенсора на объекте, при это объект должен иметь свойство „context“, в котором должно содержаться имя нужного контекста.

— Функция simpleMouseLook(). Обзор мышью — это тема для отдельного топика, в котором надо будет вспомнить немного аналитическую геометрию и тригонометрию, поэтому в данном проекте была реализована самая простая функция обзора мышью (только по горизонтали).

# the simplest def simpleMouseLook(): Z = 0 if(bge.logic.mouse.position[0] > 0.5): Z = -0.05 elif(bge.logic.mouse.position[0] < 0.49): Z = 0.05 game.getObject('player').applyRotation([0, 0, Z], False) bge.render.setMousePosition(int(bge.render.getWindowWidth()/2),int(bge.render.getWindowHeight()/2)) return False 

Мы рассмотрели наши модули.
Рассмотрим файлы настроек.

Как уже было сказано, они хранятся как „сериализованные“ словари и имеют примерно такой вид:

#SETTINGS < 'debug': 1, 'context':'secondView', #default context 'gravity': [0, 0, -30], 'forceApplyingToPlayer': 30, 'robotMovementStep': 0.1 >#ACTSKEYS

Как видно, в файле SETTINGS хранятся основные настройки игры. А в файле ACTSKEYS — назначения клавиш.

Run Blender Run

Осталось только рассмотреть файл game.blend

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

Запустите файл game.blend. Нажмите клавишу P чтобы запустить игру.
Походите, осмотритесь мышью. Оружия у вас нет, но не переживайте — никто не нападет на вас из за угла (углов просто тоже нет). По умолчанию управление на W/S/A/D.
Затем подойдите к панели управления и коснитесь её. Управление автоматически перейдет на робота, который будет у вас перед глазами. Управление на тех же клавиш — W/S/A/D. Кроме этого, можете нажать левую кнопку мыши и правую…
Как закончите баловаться, вам нужно будет вернуть стандартное управление игроком. Для этого просто нажмите на клавиатуре Q — игрок снова будет свободно ходить.

Для демонстрации переназначения клавиш и сохранения новых настроек, во время управления игроком нажмите 1, после чего управление с клавиш W/S/A/D перейдет на стрелки ВВЕРХ/ВНИЗ/ВЛЕВО/ВПРАВО. После этого вы можете посмотреть файл data/actskeys — в нем будут новые настройки назначения клавиш, а это значит, что если вы сейчас закроете игру и снова запустите, то управление будет на ВВЕРХ/ВНИЗ/ВЛЕВО/ВПРАВО. Для обратной смены управления (на W/S/A/D) снова нажмите 1.

Итог

Это была одна из возможных реализаций системы взаимодействия с устройствами ввода в Блендере, созданная с помощью программирования на Питоне и с использованием Logic Brikcs. Данная система дает нам уверенность, что, насколько бы сложной и большой не была наша игра, любое изменение состояния клавиш и действий в игре будет обработано непротиворечиво.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *