Pull to refresh

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

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

Однако, если мы хотим разработать полноценную игру, то нам нужно более серьезное решение – современная игровая индустрия предлагает гораздо более широкий выбор устройств ввода, чем одна только клавиатура, а список возможных действий и событий в игре не исчерпывается движением единственного игрового объекта.
В данном топике мы попробуем создать законченную, расширяемую, универсальную систему взаимодействия с устройствами ввода в игре при помощи программирования на 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'] = {'name':'', 'processes':{}}  
        bge.logic.globalDict['CONTEXTS'] = {
                                            
                        'loadingScreen':{'parents':[], 'processes':{}},
                        
                        'inGame': {'parents':[],
                                  'processes' : {
                                                 'gameControlling':True,
                                                 'doNothing':True
                                                 }},
                                            
                        'secondView': {'parents':['inGame'],
                                  'processes' : {
                                                 'playerMovement':True,
                                                 'playerShooting':True,
                                                 }},
                                            
                        'objectControlling': {'parents':['inGame'], 
                                    'processes': {
                                                  'robotKeying':True,
                                                  'doNothing':False
                                                  }}
                        }
            
        bge.logic.globalDict['PROCESSES'] = {
                                             
                       'playerMovement':{'conditionals':{
                                        'acts':[
                                                'playerMoveForward',
                                                'playerMoveLeft',
                                                'playerMoveRight',
                                                'playerMoveBack',
                                                'playerRun',
                                                'playerJump'
                                                ]}},
                                             
                        'playerShooting':{'conditionals':{
                                        'acts':[]}},
                                             
                        'robotKeying':{'conditionals':{
                                        'acts':[
                                                'robotMoveForward',
                                                'robotMoveBack',
                                                'robotMoveLeft',
                                                'robotMoveRight',
                                                'robotDoBarrel',
                                                'robotReplaceMesh'
                                                ]}},
                        
                        'gameControlling':{'conditionals':{
                                        'acts':[
                                                'returnToDefaultContext',
                                                'changeKeyBind'
                                                ]}},
                                             
                        'doNothing':{'conditionals':{
                                        'acts':[]}}                                     
                       }    
            
        # 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] = {'keyString':key, 'keyCode':bge.events.__dict__[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+){2,}|\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
{
                        'playerMoveForward':'WKEY',
                        'playerMoveBack':'SKEY',
                        'playerMoveLeft':'AKEY',
                        'playerMoveRight':'DKEY',
                        'playerRun':'LEFTSHIFTKEY',                
                        'playerJump':'SPACEKEY',
                        'robotMoveForward':'WKEY',
                        'robotMoveBack':'SKEY',
                        'robotMoveLeft':'AKEY',
                        'robotMoveRight':'DKEY',
                        'robotDoBarrel':'LEFTMOUSE',
                        'robotReplaceMesh':'RIGHTMOUSE',
                        'returnToDefaultContext':'QKEY',
                        'changeKeyBind':'ONEKEY'                
                        }


Как видно, в файле 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. Данная система дает нам уверенность, что, насколько бы сложной и большой не была наша игра, любое изменение состояния клавиш и действий в игре будет обработано непротиворечиво.
Tags:
Hubs:
Total votes 42: ↑39 and ↓3 +36
Views 13K
Comments Comments 4