Эффективное программирование 3D-приложений с помощью Irrlicht и Jython. Часть 11

В прошлый раз мы научились проектировать интерфейс приложения, используя стандартные компонентов GUI: кнопки, списки, диалоговые окна. Сегодня мы завершаем эту тему. Нам осталось рассмотреть методику обработки событий от этих компонентов, а кроме того попробуем загрузить в среду irrlicht уровень от quake3.

Напоминаю: очень-очень важно при проектировании интерфейса дать всем используемым компонентам уникальные идентификаторы. В противном случае вы не сможете определить, какой именно элемент был активирован пользователем. Для знакомых с идеями delphi/cbuilder/.net методика обработки событий в irrlicht может показаться очень примитивной. При создании конкретного элемента управления мы не можем привязать к нему обработчик события, а должны внутри уже знакомой нам функции "def OnEvent (self, e)" определить, к какому классу относится произошедшее событие. Если оно относится к семейству GUI Events, то уже следует определить, что за элемент был активирован, и какое специфическое для него событие произошло. Для тех, кто когда-то писал программы под классическое winapi (без позднее появившихся MFC), есть отличная возможность вернуться в босоногое детство. Отчасти подобное ретроградство можно объяснить не слишком богатым набором компонентов, отчасти тем, что поддержка GUI в irrlicht, да и почти во всех остальных 3d-движках, никогда не была приоритетным направлением. Большей частью такая методика обработки событий не представляет никаких сложностей, разве что больший объем кода, но в определенных ситуациях возникает настоящая угроза идеям ООП: я говорю об обработке событий от всплывающих окон. В примере ниже я показал это с помощью MessageBox — окна вопроса с двумя вариантами выбора. Дело в том, что события от диалогового окна (даже если он модальный) все равно приходят и анализируются единой для всего приложения функцией обработки сообщений. Если вы будете активно использовать irrlicht gui, рекомендую предварительно потратить время на создание собственной надстройки над моделью событий. Наиболее просто будет реализовать нечто подобное картам событий в MFC. Если вы знакомы с паттернами проектирования, то сможете подобрать сами наиболее подходящий паттерн поведения.

В примере ниже создается список, текстовое поле и кнопки. При нажатии на кнопку "Append" содержимое текстового поля добавляется в конец списка. При этом выполняется проверка, чтобы текстовое поле было не пустым (в случае необходимости выводится окно сообщения ошибки). При нажатии на кнопку "Choose" появляется окно выбора из двух вариантов (YES|NO). Какая бы кнопка ни была нажата, в любом случае сообщение добавляется в список "lst_log". Кнопка "File Dialog" приводит к появлению диалога выбора файла. И, наконец, кнопка "Color Dialog" приводит к появлению диалога выбора цвета (поддержка данной возможности появилась в irrlicht начиная с версии 1.2). Для отображения окна выбора файла используется функция: addFileOpenDialog(title, modal, parent, id)

Параметрами этой функции служат (по порядку) текст заголовка сообщения, признак того, будет ли окно выбора файла модальным или нет, родительское окно и привычный уже нам уникальный идентификатор окна диалога. Для того, чтобы показать окно выбора цвета, используется метод addColorSelectDialog, параметры которого идентичны тем, которые принимает и метод addFileOpenDialog. Общим для всех диалоговых окон является то, что после их отображения на экране irrlicht начинает генерировать слишком большое количество сообщений — например, простое перемещение мыши над окном диалога уже приводит к генерации событий-сообщений. Нам нужно грамотно отбрасывать информационный мусор и реагировать только на события закрытия диалогового окна с помощью любой из управляющих кнопок. В примере я также создаю меню окна irrlicht. Для добавления меню достаточно использовать метод addMenu() без параметров. Возможно строить многоуровневые меню. Это достигается за счет того, что при вызове метода добавления пункта addItem можно указать специальный флаг "hasSubMenu". Затем следует получить ссылку на добавленный пункт c помощью getSubMenu и от его имени делать вызовы функции добавления новых подпунктов. Если вы хотите отделить пункты меню с помощью линии-разделителя, то используйте метод addSeparator.

import java
import net.sf.jirr
from net.sf.jirr import dimension2di
from net.sf.jirr import position2di
from net.sf.jirr import recti
from net.sf.jirr import SColor
from net.sf.jirr import IEventReceiver

# стандартно класс обработчика событий наследуется от IEventReceiver
class EvtHandler (IEventReceiver):
# в этих двух переменных будут находиться ссылки на открытые диалоговые окна — выбора файлов и выбора цвета
cdialog = None
fdialog = None

# конструктор класса обработки событий
def __init__ (self):
IEventReceiver.__init__ (self)

def OnEvent (self, e):
if e.getEventType() == net.sf.jirr.EEVENT_TYPE.EET_GUI_EVENT:
# определяем, что событие относится к семейству событий GUI
if GUI_MESSAGEBOX_DIALOG == e.getGUIEventCaller ().getID():
# теперь проверяем: если инициатор события MsgBox — окно сообщения с двумя кнопками YES и NO, то проверяем уточняющий тип сообщения — какая именно кнопка была нажата
if net.sf.jirr.EGUI_EVENT_TYPE.EGET_MESSAGEBOX_YES == e.getGUIEventType ():
lst_log.addItem ("MsgBox YES")
if net.sf.jirr.EGUI_EVENT_TYPE.EGET_MESSAGEBOX_NO == e.getGUIEventType ():
lst_log.addItem ("MsgBox NO")

if GUI_FILEOPEN_DIALOG == e.getGUIEventCaller ().getID():
# событие закрытие диалога выбора файла
if net.sf.jirr.EGUI_EVENT_TYPE.EGET_FILE_SELECTED == e.getGUIEventType ():
lst_log.addItem ("File: " + self.fdialog.getFilename () )

if e.getGUIEventType () == net.sf.jirr.EGUI_EVENT_TYPE.EGET_BUTTON_CLICKED:
# если подтип события — это нажатие кнопки, то я проверяю, какая из четырех кнопок была активирована, и выполняю соответствующие действия if e.getGUIEventCaller ().getID() == GUI_BUTTON_ADD:
if txt_fio.getText() == '':
# сообщение об ошибке, если поле пустое
guienv.addMessageBox ("Error", "Text Field Is Empty, cannot add")
return False
lst_log.addItem (txt_fio.getText())
if e.getGUIEventCaller ().getID() == GUI_BUTTON_CHOOSE:
# при нажатии на кнопку Choose следует показать диалоговое окно выбора из двух вариантов,
# Первый параметр — это заголовок диалогового окна, второй — текст сообщения, третий параметр отвечает за то, будет ли наше окно сообщения модальным или нет, последний параметр — это набор отображаемых кнопок окна диалога,
обратите внимание на то, как именно я выполняю сборку набора — через оператор битового или "|"
guienv.addMessageBox ("Question", "What Do Your Want ?", True, net.sf.jirr.EMESSAGE_BOX_FLAG.EMBF_YES.swigValue() |
net.sf.jirr.EMESSAGE_BOX_FLAG.EMBF_NO.swigValue(), None, GUI_MESSAGEBOX_DIALOG)
lst_log.addItem ("MsgBox Showed")
if e.getGUIEventCaller ().getID() == GUI_BTN_DIALOG_FILE:
self.fdialog = guienv.addFileOpenDialog ("Select File", True, None, GUI_FILEOPEN_DIALOG)
if e.getGUIEventCaller ().getID() == GUI_BTN_DIALOG_COLOR:
self.cdialog = guienv.addColorSelectDialog ("Select Color", True, None, GUI_COLOR_DIALOG)
return False

java.lang.System.loadLibrary ('irrlicht_wrap')
device = net.sf.jirr.Jirr.createDevice(net.sf.jirr.E_DRIVER_TYPE.EDT_DIRECT3D9, dimension2di(800, 600), 32)
driver = device.getVideoDriver()
guienv = device.getGUIEnvironment()

# объявляем список идентификаторов элементов управления
GUI_BUTTON_ADD = 103
GUI_BUTTON_CHOOSE = 104
GUI_MESSAGEBOX_DIALOG = 105
GUI_FILEOPEN_DIALOG = 106
GUI_COLOR_DIALOG = 107
GUI_BUTTON_SAVE = 108
GUI_LIST_SOURCE = 109
GUI_BTN_DIALOG_FILE = 110
GUI_BTN_DIALOG_COLOR = 111
GUI_TEXT_SOURCE = 112

# эти идентификаторы будут служить для управления пунктами меню
GUI_MNN_APPLE = 113
GUI_MNN_ORANGE = 114
GUI_MNN_ORANGE_1 = 115
GUI_MNN_ORANGE_2 = 116

# создаем кнопки
btn_add = guienv.addButton(recti(120,70,190,90), None, GUI_BUTTON_ADD, "Append")
btn_remove = guienv.addButton(recti(120,100,190,120), None, GUI_BUTTON_CHOOSE, "Choose")
btn_move_left = guienv.addButton(recti(120,200,190,220), None, GUI_BTN_DIALOG_FILE, "File Dialog")
btn_move_left = guienv.addButton(recti(120,230,190,250), None, GUI_BTN_DIALOG_COLOR, "Color Dialog")

# создаем список
lst_log = guienv.addListBox(recti(10,100,110,400), None, GUI_LIST_SOURCE)
# создаем текстовое поле
txt_fio = guienv.addEditBox("Potatoes", recti(10,70,110,90), True, None, GUI_TEXT_SOURCE)

# создаем меню
menus = guienv.addMenu ()
# добавляем к меню первый пункт — это обычный пункт без дочерних подпунктов
menus.addItem ("Apple", GUI_MNN_APPLE)
# добавим разделитель между пунктами меню
menus.addSeparator()
# а теперь при вызове addItem мы укажем больше параметров, в том числе признак того, что этот пункт меню будет содержать дочерние подпункты # предпоследний параметр отвечает за то, будет ли данный пункт меню доступным или нет (т.е. заблокированным)
# последний параметр — это как раз и есть признак того, что этот подпункт раскрывается
menus.addItem ("Orange", GUI_MNN_ORANGE, True, True)
# теперь получаем ссылку на созданное подменю и наполняем его новыми подпунктами
# следует указать как параметр порядковый номер этого пункта меню
menus_orange = menus.getSubMenu (2)
menus_orange.addItem ("Orange_1", GUI_MNN_ORANGE_1)
menus_orange.addItem ("Orange_2", GUI_MNN_ORANGE_2)

evt = EvtHandler ()
device.setEventReceiver (evt)

# дальнейший код обычен — организуется цикл отрисовки
while device.run():
driver.beginScene(1, 1, SColor(255,220,241,240))
guienv.drawAll()
driver.endScene()

device.drop

Теперь мы переходим ко второй части нашего сегодняшнего урока. Мы попробуем загрузить карту уровня игры quake3. Если у вас под руками нет дистрибутива, то ничего страшного. В поставке irrlicht идет множество примеров, необходимые файлы (изображений, звука и других ресурсов) для которых находятся в папке media. В том числе там есть и файл map-20kdm2.pk3. Сначала я приведу пример исходного текста программы, а затем мы его проанализируем.

import java
import net.sf.jirr
from net.sf.jirr import dimension2di
from net.sf.jirr import position2di
from net.sf.jirr import SColor
from net.sf.jirr import vector3df
from net.sf.jirr import SKeyMap

java.lang.System.loadLibrary ('irrlicht_wrap')
device = net.sf.jirr.Jirr.createDevice(net.sf.jirr.E_DRIVER_TYPE.EDT_DIRECT3D9, dimension2di(640, 480), 32)
driver = device.getVideoDriver()
smgr = device.getSceneManager()

device.setWindowCaption("1.7 quake 3")
# для того, чтобы irrlicht смог загрузить некоторую модель уровня quake 3, необходимо добавить ссылку на его местоположение
device.getFileSystem().addZipFileArchive("E:\Program_Files_2\jirr_0.8\media\map-20kdm2.pk3", True, True)

mesh = smgr.getMesh("20kdm2.bsp")
node = smgr.addOctTreeSceneNode(mesh, None, -1, 128);
node.setPosition(vector3df(-1300,-144,-1249))
smgr.addCameraSceneNodeFPS(None,100,500,-1,SKeyMap(),0)
# прячем курсор
device.getCursorControl().setVisible(False)

while(device.run()):
driver.beginScene(True, True, SColor(0,100,100,100))
smgr.drawAll();
driver.endScene();
device.drop

Прежде всего, я должен указать местоположение файлов, образующих уровень — непосредственно модель уровня и все связанные с ним ресурсы, те же текстуры. Для этого служит вызов функции addZipFileArchive, получающей на вход первым параметром путь к архиву (файлы pk3 — это обычные zip- архивы ресурсных файлов игры). Второй параметр функции addZipFileArchive отвечает за возможность игнорировать регистр символов при загрузке уровня или связанных с ним ресурсов, третий же задает признак того, что можно использовать короткие имена файлов. Так, в архиве есть подпапки: levelshots, maps, scripts, textures. Если данный флаг не установлен (значение третьего параметра False), то при последующих загрузках ресурсов необходимо указывать путь целиком, например:

device.getFileSystem().addZipFileArchive("map-20kdm2.pk3", False, False)
mesh = smgr.getMesh("maps/20kdm2.bsp")

В случае, если загрузка ресурса была не успешна (скажем, уровень 20kdm2.bsp не был найден), то переменная mesh будет равна специальному значению None — ничего нет. Затем мы присоединяем модель к специальному узлу. Дело в том, что все объекты, размещенные в виртуальном мире irrlicht, представляют собой node — узлы различных видов. Каждый вид узла наилучшим образом оптимизирован для представления какой-то разновидности информации. Так, есть еще узлы вида: AnimatedMeshSceneNode — для представления анимированной модели персонажа, есть BillboardSceneNode — узел для представления спрайта (плоской 2d-картинки, всегда повернутой к камере лицом), есть CameraSceneNode — узел камеры и т.д. В irrlicht-узлы организованы в некоторое подобие дерева, у многих узлов есть родительский узел, один узел имеет несколько дочерних и, в свою очередь, может принадлежать некоторому родительскому узлу. Вообще идея с представлением виртуального мира в виде деревьев узлов очень часто применяется, это позволяет реализовывать иерархические модификации персонажей. Например, есть узел Дед_Мороз, с дочерними узлами Мешок_Подарков, Посох. Если вы перемещаете или вращаете узел Дед_Мороз, то также перемещаются и вращаются все его дочерние узлы, сохраняя относительное расстояние и ориентацию относительно своего родительского узла. Проще говоря, если ДедМороз сделал шаг влево, то его Мешок_Подарков и Посох не остались висеть на старом месте. Кроме того, грамотное проектирование иерархии узлов позволяет ускорить отрисовку 3d-сцены. Дело в том, что перед непосредственно рендерингом модели происходит определение того, видим ли данный узел или нет. И если это не так, то он и все вложенные в его состав узлы не рисуются. Следующий шаг — смещение узла (сами модели не способны перемещаться в виртуальном пространстве — еще одна причина существования узлов). Дело в том, что начало координат уровня не совпадает с началом координат мира irrlicht. И если вы не хотите увидеть модельку уровня где- то очень далеко в углу, то ее нужно передвинуть поближе. Конкретные значения цифр я аккуратно переписал из примера jirr, вам же в случае использования иных моделей следует воспользоваться методом "научного тыка" или взять редактор уровней quake — например radiant, — чтобы разобраться с системой координат конкретного уровня.

Теперь, когда у нас есть уровень, надо разместить камеру, глазами которой мы будем видеть мир irrlicht. Каждая камера характеризуется положением, направлением взгляда, а также широтой взгляда FOV — угол зрения. Также важны параметры ближней и дальней плоскостей отсечения. Представьте себе камеру в виде четырехугольной пирамиды. Ее вершина задает местоположение камеры. Центральная точка в основании — то, куда мы смотрим. Угол в основании пирамиды связан с FOV. А если теперь у пирамиды-камеры обрезать вершину так, чтобы получилась усеченная пирамида, то получим ближайшую плоскость отсечения. Основание же пирамиды даст нам дальнюю плоскость отсечения. Видим же мы только то, что находится в середине этой усеченной пирамиды. Да, есть такая камера в irrlicht, ее имя — addCameraSceneNode. Есть и другая камера — CameraSceneNodeFPS, использованная нами в примере, которая предоставляет встроенную возможность реагирования на действия пользователя с перемещением камеры и изменением направления взгляда. В случае же с CameraSceneNode нам бы пришлось вручную перемещать камеру и вращать ее. Irrlicht умеет загружать модели не только в формате quake3 — для импорта моделей объектов возможно использовать следующие известные форматы:

3D Studio (.3ds) — этот формат использовался в старых версиях 3dsmax еще во времена dos, сейчас это фактический стандарт для создания переносимых моделей во многих других приложениях 3d-моделирования.
DirectX (.x) — платформонезависимый формат с поддержкой анимации персонажей.
Maya (.obj) — очень известная программа 3d-моделирования.
Milkshape (.ms3d) — формат, используемый достаточно неплохой программой Milkshape — есть поддержка анимации моделей (что самое приятное, она умеет импортировать и экспортировать модели из ресурсов различных современных игр).
OCT (.oct) — один из форматов, которые понимает не очень известная в нашей стране программа 3d-моделирования blender. За рубежом она более популярна, т.к., хотя и проигрывает в возможностях лидерам рынка вроде 3dsmax, maya, но зато бесплатна и удовлетворяет большинство потребностей любительского 3d-моделирования.
OGRE Meshes (.mesh) — помните в главе, посвященной обзору 3d-движков, я упоминал о занимающем первое место в рейтинге devmasters движке 3d- рендеринга OGRE? Именно в формате .mesh OGRE загружает модели в сцену.
Quake 2 models (.md2) — анимированные модели персонажей из quake 2.

В следующий раз мы продолжим работу с 3d-функциями irrlicht — попробуем создавать собственные уровни и модели игрового окружения с помощью инструмента irrEdit и 3dsmax/MilkShape. Также на очереди рассмотрение средств работы с шейдерами.

black zorro, black-zorro@tut.by


Компьютерная газета. Статья была опубликована в номере 18 за 2007 год в рубрике программирование

©1997-2024 Компьютерная газета