Целью настоящего пособия является описание процесса того, как можно собрать небольшой прототип RPG-игры для движка GemRB. Кто не знает, GemRB (расшифровывается Game Engine Made with pre-Rendered Background) - это проект с открытым исходным кодом, направленный на создание клона движка Infinite Engine, того самого на котором в конце девяностых - начале нулевых были сделаны такие классические ролевые партийные игры как Baldur's Gate, Planescape: Torment, Icewind Dale и другие. Нынешнее состояние движка GemRB позволяет пройти все эти игры, используя, конечно, оригинальные ресурсы (графику, звук, тексты и прочее).
Одно из достоинств проекта GemRB в том, что под него можно сделать отдельную, совершенно независимую игру. В комплекте с ним идёт небольшое демо с иллюстрацией некоторых механик. Не всех, конечно, лишь самых базовых и простых. Чтобы поиграть в это демо, надо скачать дистрибутив (либо релизную версию, либо ежедневный билд). На момент написания этого текста только-только вышла версия 0.9.4. Распаковываем архив и запускаем файл gemrb.exe. По умолчанию как раз запустится демонстрационный проект.
Я разобрал как устроен этот проект, и в данной заметке хотел рассказать, как можно его повторить. Будем делать полностью с нуля. Уже подготовлены все необходимые ресурсы, так что будем конструировать игру из готовых ассетов. Графика нарисована самостоятельно, звуки (более или менее подходящие) набраны из интернета.
По своей сути создание игры для движка GemRB похоже на создание мода для игры Baldur's Gate (или другой подобной). В большинстве случаев используются те же механизмы и методы. Возникает вопрос, зачем городить огород, и не заняться модостроением. Всё правильно. Это оно почти и есть. Отличается лишь тем, что если делать мод для BG, то так или иначе вынужден вписываться в сеттинг и лор уже готовой игры. Да, это хорошо, так как уже готово много ресурсов и ассетов, которые можно переиспользовать. Плохо, если хочется рассказать независимую историю. А в нашем случае можно выбирать любой сеттинг, да и жанр не только партийная RPG. Хочешь киберпанк - пожалуйста, космооперу - на здоровье, нуарный детектив - завсегда пожалуйста. Правда, самому всё делать тогда надо. Ну так и хорошо.
Создание игры состоит в конфигурировании различных игровых сущностей, и редактировании таблиц с данными, управляющими игровым процессом (эти таблицы хранятся в 2da-файлах). Полноценного программирования при этом не происходит. Однако хоть в какой-то мере немного программировать придётся. Например, скрипты. Создание диалога по своей сути является программированием конечного автомата. Да и вообще алгоритмическое мышление необходимо. Как без него.
Нам понадобится:
Ассеты, скачать можно по ссылке.
Минимальный набор конфигурационных файлов, достаточных для запуска игры. Скачать можно по ссылке.
Помимо этого понадобятся три программы, которые используются для создания модов.
DLTCEP (расшифровывается The DragonLance Total Conversion Editor Pro). Скачать тут. Это будет основная программа для подготовки игровых сущностей.
Near Infinity. Скачать тут. Написана на Java. Но есть и портативная версия, уже с рантаймом. Нам в основном понадобится чтобы конвертировать графические ресурсы.
WeiDU. Скачать тут. Это консольная программа. Будем использовать её для подготовки текстовых данных, в частности, для создания диалога. Имеет смысл добавить путь до exe-файла в переменную окружения PATH, чтобы можно было его запускать из любого места.
Перед тем, как начать, следует добавить несколько замечаний. Во-первых, на текущий момент движок работает не идеально, есть некоторые шероховатости. Кое-что не работает, кое-что не удобно, но о всех проблемах разработчики знают. По крайней мере будем на это надеяться. Во-вторых, движок поддерживает довольно большое число функций, облегчающий дебаггинг проектов. Они перечислены вот здесь. Некоторые из тех, что могут понадобиться:
Ctrl+6 - показывает карты высот, освещённости и проходимости
Ctrl+5 - показывает силуэты стен
Ctrl+u - пишет в консоль значения системных переменных (тех, что были определены в скриптах)
Ctrl+x - пишет в консоль координаты курсора
Ctrl+j - телепортирует персонажа в точку, куда указывает курсор
1. Запуск движка
1.1. Подготовка папок
Скачиваем архив minimal с гитхаба, распаковываем его. Он содержит две папки: empty и GUIScripts. Папка empty содержит минимальную комплектацию необходимых файлов. В них нет никаких специальных данных, просто заготовки того, что может (и требуется) для работы движка. В папке GUIScripts содержатся пустые заготовки для Python-овских скриптов.
Итак, нам сейчас нужна папка empty. Переименовываем её во что-то более понятное, например назовём её expath. Это у нас игра так будет называться (типа ExPath).
Далее, открываем главный конфигурационный файл движка GemRB (называется GemRB.cfg и расположен в корневой директории). В нём указываем полный путь до нашей переименованной папки как значение переменной Gamepath. Пишем путь как есть, без кавычек.
Теперь Python-скрипты. Директория с движком уже содержит папку GUIScripts. Поэтому копируем содержимое нашей папки GUIScripts в ту, что уже есть, и переименовываем эту скопированную папку, тоже назвав её expath. После этого в конфигурационном файле GemRB.cfg указываем значение переменной GameType равное этому самому expath.
И тут же в файле GemRB.cfg заодно указываем размер игрового окна побольше. Хотя бы Width=1024 и Height=768. А может быть даже и ещё больше.
Если сейчас запустить gemrb.exe, то пойдёт запуск, а потом всё закроется. Это потому что нет точки входа в игру, да и самой игры как сущности тоже пока нет. Будем делать. Для этого нам надо сделать три вещи:
Создать тестовую локацию
Создать тестового персонажа
Написать запускающий всё это код
Сначала сделаем всё с использованием тестовых ассетов, чтобы быстрее было. Потом всё оформим по-нормальному. И до самого конца текущего первого параграфа мы не будем знать, делаем ли мы всё правильно или нет. Лишь в самом конце получится что-то запустить и проверить, работает ли.
1.2. Первый запуск DLTCEP
Вот тут-то нам первый раз и понадобится DLTCEP. Итак, запускам DLTCEP.exe. При первом запуске он попросит указать файл chitin.key, который содержится в нашей папке с будущей игрой expath. Указываем его. Помимо этого задаём имя пресета настоек и снимаем птицу с параметра Read Only (если сия птица была установлена).
Жмём Save & Back. Он навалит немного warning-ов. Игнорируем их. Если предложит исправить повреждённый dialog.tlk - соглашаемся. Можно, чтобы всё было чисто, в созданную автоматически папку override (внутри expath) переписать всё содержимое папки data. Никаких warning-ов тогда не будет, но папка override станет забитой всяким непонятным хламом. А вообще, так-то, именно в папку override мы и будем добавлять весь контент. Поэтому лучше её поддерживать как можно более пустой, и без того забьётся ресурсами. Никакой системы по разделению на директории и поддиректории тут нет. Движок берет данные либо из data, либо из override, а все остальные (в частности DLTCEP) - только из override. Вот так-то.
1.3. Тестовая локация
Теперь мы готовы создать нашу первую локацию. Жмём Edit - Area (ARE, WED). Вместо этого можно нажать кнопку Areas.
Появляется окно, в котором можно задавать параметры новой локации. Сейчас оно пустое, будем исправлять.
Пишем имя нашей новой локации: AREA00 вместо NEW AREA (имя должно быть не длиннее восьми символов), и нажимаем кнопку Edit wed. Появившееся окно предназначено, чтобы определить то, как выглядит геометрия локации. Ну то есть какая картинка используется в качестве фона. Вот для этого и нажимаем кнопку Set overlay.
В появившемся окне (да сколько их!) жмём Load external Tis.
Там используем формат *.bmp и выбираем файл resources\fast_start\location.bmp. Со всем соглашаемся. Появится окно с картой локации, разбитой на тайлы. Скучноватая, конечно, локация, но для теста пойдёт. Жмём Back для закрытия окна с тайлами, а потом Set overlay и сохраняем файл AREA00.tis.
После этого закрываем окно с редактированием wed-а (нажав Back) и сохраняем локацию, нажав Save Area As..., и указав имя AREA00.are. Короче, wed, tis и are должны иметь одно и то же имя AREA00.
Дальше, чтобы не было проблем, надо обновить список ресурсов, которые доступны DLTCEP. Для этого закрываем окно редактирования локации, соглашаемся, с тем, что нет каких-то дополнительных карт (скоро сделаем), и в основном окне нажимаем Reload chitin.
Теперь снова заходим в Edit - Area. Жмём Load Area и из списка выбираем AREA00.
Дальше снова Edit wed, потом Extract, и в появившемся окне жмём Minimap. Это создаст файл AREA00.mos, являющийся, собственно миникартой для локации.
Закрываем оба окна (жмём два раза Back). Переключаемся на вкладку Maps.
Здесь нам надо будет создать три карты
Карту высот (Height map)
Карту освещённости (Light map)
Карту проходимости (Search map)
Все эти карты будут тривиальными, у нас ведь тестовая локация.
Итак, карта высот задаёт, как следует из названия, высоту областей в пределах локации. Используется чтобы обозначить лестницы или что-то подобное. Высота меряется от 0 (самый низкий уровень) до 15 (самый высокий). Наш уровень плоский, поэтому поставим уровень высот везде одинаковый, равный 8 (как раз серединка). Для этого жмём сначала Create palette, потом выбираем из списка 8, и нажимаем Set to. Не очень естественный порядок расположения кнопок в интерфейсе, но что уж поделать. Всё, карта высот готова.
Теперь очередь карты освещённости. Она добавляет к персонажам на локации оттенок, который призван лучше вписать их поверх статичного фона. С этой картой действия аналогичные. Переключаемся на Light map, жмём Create palette, выбираем 0, и жмём Set to.
Впрочем, ситуация немного отличается от предыдущего случая. Вообще каждый пиксель карты освещённости может иметь значение от 0 до 255. Но это вовсе не оттенки серого. Это индексы пикселей в палитре. Её можно редактировать, если нажать кнопку Edit palette. И там же видно, что использованный нами индекс 0 соответствует цвету (225, 225, 225). Но таким способом редактировать пиксели не удобно. Позже, когда дойдём до настоящей локации, используем уже заранее подготовленную картинку с автоматическим созданием подходящей палитры.
Ну и теперь карта проходимости. Она показывает где можно ходить персонажам, а где нельзя. Как и раньше переключаемся на Search map, жмём Create palette, выбираем 4 - Stone 1, и, наконец, Set to. Смысл такой, что теперь вся локация будет считаться проходимой, и тип поверхности - камень.
На этом с созданием карт покончено. Сохраняем локацию, нажав Save Are As... В общем-то и с созданием локации покончено тоже.
1.3. Тестовый персонаж
Чтобы создать нового персонажа выбираем Edit - Creature (CRE), или жмём кнопку Creatures.
Появится окно, в котором много всяких нулей. Тут нам надо будет исправить всего несколько значений. Большинство параметров для нас не имеет значений. Они описывают персонажа среди всего их многообразия в играх по D&D. Нам нужна лишь малая часть этого разнообразия.
В качестве уровней указываем единички. Текущее и максимальное число жизней - 10 (для определённости). Параметр Reaction выставляем 0x2. Это означает, что персонаж является управляемым, у него будет рисоваться зелёный кружок. И последнее здесь - задаём параметр Creature animation. Можно выбрать любое число в 16-ричной системе счисления, например 0xa000 (тоже для определённости). Этот параметр задаёт идентификатор, используя который можно будет задать картинки для отображения персонажа на экране.
На второй вкладке Stats & Skills выставляем основные параметры, равные 1. Можно написать любые числа, для нас это не важно (опять же, ведь мы не будем использовать никакую ролевую систему). Главное, чтобы эти параметры были больше 0. Иначе персонаж будет считаться мёртвым.
Сохраняем созданного персонажа (жмём Save Creature As...) под именем player.cre.
Дальше надо подготовить изображение, которые будет использоваться нашим персонажем. Все графические ресурсы сохраняются в специальном формате *.bam. Воспользуемся Near Infinity. Запускаем его, и аналогично DLTCEP, указываем файл chitin.key.
Выбираем Tools - Convert - BAM Converter...
Откроется окно для преобразования графических ресурсов (png-изображений в большинстве случаев) в bam-формат.
Жмём кнопку Add... и добавляем один единственный файл resources\fast_start\character.png. Этот, с позволения сказать, бочонок и будет играть роль персонажа (пока, по крайней мере). Выставляем центр (40, 60), активизируем птицу Compress BAM и переходим на вкладку Cycles.
Создаём новый цикл (кнопка Add cycle), добавляем в него единственное изображение, дублируем этот цикл аж 79 раз.
Тут надо пояснить, что всё это значит. Чтобы bam-файл можно было использовать для отображения персонажа, он должен содержать кадры разного типа анимаций. Каждый цикл - это анимация одного из движений с одного из ракурсов. Есть несколько различных форматов того, как всё это может быть организовано. Мы для простоты сейчас будем использовать формат, когда всё сохраняется в один файл. Этот файл должен содержать пять типов движений, у каждого 16 ориентаций (каждая ориентация отличается поворотом на 22.5 градусов. Понимаете, да? 360 / 16 = 22.5). Итого, значит, 80 циклов. У нас всего одна картинка, поэтому дублируем её на все эти циклы.
Сохраняем файл char00.bam в папку override.
Теперь надо созданный bam-файл использовать для отображения нашего персонажа. Для этого из папки data копируем файл avatars.2da в папку override. Открываем его в любом текстовом редакторе и вместо звёздочек в первой строке пишем
0xa000 char00 char00 char00 char00 1 2 1 *
Разделители между словами можно использовать любые: хоть табы, хоть пробелы в любом количестве. Указанная строчка как раз привяжет к идентификатору 0xa000 файл с анимациями char00. Первое значение 1 здесь - это типа анимации, 2 - это размер персонажа, его вроде как радиус (это значение может быть только целым), а вторая 1 носит технический характер (что-то там про используемую палитру).
И последнее, зададим скорость перемещения персонажа. Для этого из папки data копируем файл moverate.2da в папку override. В нём пишем
0xa000 8 player
Смысл понятен. Сначала указывается идентификатор анимации, потом скорость, и потом для какого персонажа использовать эту скорость.
По идее персонаж готов.
1.4. Запускающий код
Ну и последнее, осталось указать стартовую локацию, которая будет грузиться самой первой при запуске игры. Для этого из папки data копируем файлы startpos.2da и startare.2da в папку override. В файле startare.2da указываем значение параметра START_AREA равным AREA00, а в файле startpos.2da в первом столбце указываем стартовую позицию персонажа (где он появится в самый первый момент), что-то вроде START_XPOS = 320, START_YPOS = 190.
Теперь, собственно, сам запускающий код. Для этого нам кое-чего дописать в Python-овские скрипты в папке GUIScripts\expath. Всего немного.
Тут такая структура. Сначала запускается скрипт Start.py. Он должен вызвать SetupGame.py, и потом вызывается Game.py. Вот это и надо сделать.
Итак, в файле Start.py пишем
def OnLoad():
GemRB.LoadGame(None)
GemRB.SetNextScript("SetupGame")
Тут всего дел, что запустить SetupGame.py. В нём, в свою очередь пишем
def OnLoad():
GemRB.CreatePlayer("player", 1)
GemRB.SetPlayerName(1, "Player", 0)
GemRB.GameSetScreenFlags(GS_PARTYAI, OP_OR)
GemRB.SetVar("CHAPTER", 1)
GemRB.EnterGame()
Тут уже побольше содержания. Сначала создаётся персонаж, используя player.cre. Далее назначается его имя Player. Потом еще две строчки ерунды, и, наконец, запускаем игру. Как только игра запустится внутри движка, он сам вызовет скрипт Game.py. Вот в нём уже написано
def EnterGame():
GemRB.GamePause(0, 0)
Ничего кроме этого не надо. Эта единственная команда снимает игру с паузы.
По идее всё. Момент истины. Запускаем gemrb.exe, и всё должно забегать.
2. Создание локации
Теперь, когда у нас есть работающий билд, можно начать делать нормальную локацию. Ну как нормальную, в меру возможностей, конечно же. Я заранее подготовил задний фон для нашей локации. Вот его схема
Фактически один коридор с несколькими дверьми. Есть необязательный обходной путь, запертая дверь открывается отдельным рычагом. Чтобы открыть последнюю дверь надо будет поговорить с NPC и решить его небольшую загадку (чисто символическую).
Как принципиально делать локацию мы уже знаем. Повторяем все те предыдущие шаги.
Обзываем новую область AREA01. В качестве фона используем картинку из ресурсов resources\art\location\area_open_doors.bmp. Размер будет 55 на 28 тайлов. Не забываем после создания area01.tis переиндексировать ресурсы для DLTCEP и создать миникарту.
2.1. Карты для локации
Теперь карты высот, освещенности и проходимости. Уровень плоский, поэтому с высотами ничего отдельного не делаем, генерируем однотонную карту.
Для карты освещённости тоже генерируем однотонную текстуру. Сохраняем локацию. В папке override появились два bmp-файла, как раз с картами высот и освещённости. Открываем текстуру для освещённости в графическом редакторе (например в Фотомагазине от Адоба). Меняем режим на RGB Color.
Потом берём картинку resources\art\location\lightmap.png и вставляем её поверх этой. Масштабируем, чтобы подогнать размеры. После этого возвращаем обратно режим в Indexed Color.
Ну и пересохраняем с прежним именем. Всё, карта освещённости готова.
Наконец, карта проходимости. Вот её надо будет делать самостоятельно. Для этого сначала, как и раньше, создаём однотонную карту (из постоянных значений), но в качестве значения выбираем 0 - Solid obstacle
Далее жмём кнопку Edit map. Откроется окно с нашей картой, сплошь заполненное нулями.
Это показывает, что сейчас на локации нет проходимых участков. Выбираем 4 - Stone 1.
И после этого кликаем мышкой на точки локации и меняем нолики на четвёрочки. Вот так вот по одному. В целом цель состоит в том, чтобы все проходимые места закрасить четвёрками, а непроходимые оставить нулями. Можно немного ускорить процесс. Если не отпуская левой клавиши мыши провести линию между двумя точками, потом отпустить и снова кликнуть, то участок между начальной и конечной точкой заполнится четвёрками.
И ещё один ускоряющий tip-and-trick. Если окружить замкнутую область четвёрками, то можно выбрать опцию Floodfill, кликнуть внутри огороженной области, и вся она заполнится четвёрками. Если где-то будет дырка в контуре, то вообще всё заполнится чётверками, и придётся волосы на голове (или где они у кого есть) рвать от досады.
Ну вот так всё и заполняем. Совета тут два. Почаще сохраняться, и не прижиматься сильно к стенкам. Уж лучше пусть области вдоль стен будут непроходимыми, чем залазить на них.
По поводу дверей. Открытые створка надо игнорировать. Непроходимость через них мы оформим чуть позже, когда будем настраивать двери. В результате получается нечто подобное.
На этом будем считать, что карта проходимости закончена.
Можно всё протестировать в игре. Для этого надо только сменить имя стартовой локации в файле startare.2da, а также подобрать более удачное начальное местоположение персонажа (указать в файле startpos.2da значения 3035 и 1500).
2.2. Стены
Следующий этап - это определение стен. Стена - это участок локации, который может закрывать персонажа. У нас это в основном колонны на переднем плане и арки в обходных путях. Обозначение этих областей позволит движку рисовать участок фона как будто он полупрозрачно наложен поверх персонажа. Это задаёт глубину сцены.
Итак, заходим в режим Edit wed.
Там нам нужен раздел Wallgroup.
Жмём Add polygon. Полигоны - это и есть как раз те области, которые нам нужны. Потом нажимаем Edit polygon.
Откроется окошко для задания вершин полигона. Жмём Preview. Откроется карта нашей локации. Потом включаем режим Insert.
Теперь, если кликать на карте локации, то будут последовательно добавляться точки в полигон. Обводим как-то так.
Здесь важно откуда начинать. Первый сегмент полигона (вот это синее ребро) задаёт базовую линию (base line). Если персонаж находится выше этой линии, то стена накладывается поверх персонажа, если ниже - то нет. С помощью стрелочек можно менять порядок рёбер, выбирая нужное ребро начальным.
Если отжать режим Insert, то клики на локации будут устанавливать местоположение выделенной вершины (а не создавать каждый раз новую). Удобно, если с первого раза нарисовали всё криво. Чтобы удалить вершину - по ней достаточно кликнуть два раза в списке. Да, тут естественно рекомендуется использовать какое-нибудь приложение, которое позволяет приближать область экрана. Чтобы удобнее было кликать на контур объекта.
И ещё, у каждого полигона надо активировать две птицы: Wall и Cover animations. Без них в некоторых случаях тоже будет работать,но лучше установить, чтобы всё чётко было.
Сохраняем локацию, и проверяем, что получилось в игре.
Всё работает. Ну и вот теперь, короче, обводим так каждую колонну, арку, и вообще любую часть, которая может закрывать персонажа. Тут таких участков много. Для дверей обводим только неподвижную часть. Створки пока игнорируем. Их оформим позже, когда доберёмся до задания дверей.
Вот пример арки. Её не обязательно обрисовывать всю. Достаточно лишь той области, что потенциально может перекрывать персонажа.
Да и вообще, лучше каждый полигон проверить в игре, как отображается. Нет ли где ненужных наслоений. Если есть, то тогда рекомендуется разделять полигон на несколько, и двигать у них базовые линии.
2.3. Двери
У нас на уровне всего пять дверей. Для каждой из них надо:
Задать на какие тайлы подменять фон в закрытом состоянии
Расставить точки и области для открытия/закрытия
Разметить непроходимые области в закрытом и открытом состоянии
Задать стены в закрытом и открытом состоянии
Начнём с самой первой двери, что у старта локации. Переходим на вкладку Doors и жмём кнопку Add door.
Сейчас мы можем задать точки для открывания/закрывания этой двери. Это две точки на локации. Предполагается, что если кликнуть на дверь, то персонаж побежит к ближайшей точке и откроет из неё эту дверь. Координаты указываются в разделе Open location front и Open location back.
Жмём Set. В появившемся окне кликаем перед дверью. Заметим, как сверху окна появятся координаты точки клика. Потом нажимаем Set.
Повторяем то же самое для позиции с другой стороны двери. Если дверь предполагается открывать с двух разных сторон, то надо указать две точки. Если с одной - то можно их оставить одинаковыми. Здесь пока всё.
Теперь нам надо подготовить тайлы, которые будут рисоваться на месте закрытой двери. Для этого мы создадим отдельный tis-файл, и импортируем его в нашу локацию. Итак, закрываем окно редактирования локации и выбираем Edit - Tileset (TIS).
Появится окно. В нём жмём Load external Tis и указываем файл resources\art\location\area_close_doors.bmp.
Нам надо извлечь тайлы из загруженной картинки, поэтому жмём кнопку Extract tiles.
Появится новое окно. В нём жмём Preview selection, чтобы показать загруженную ранее картинку всю целиком. Активируем Show grid, чтобы видеть сами тайлы.
Дальше нам надо выбрать левый верхний тайл куска, нажать Select top left, а потом правый нижний и нажать Select bottom right. Кусок должен с запасом покрывать изменяемый фрагмент локации. Но не сильно.
Жмём, наконец, Save as TIS. Сохраняем где угодно под каким угодно именем, например d01.tis.
Закрываем всё, возвращаемся к редактированию локации. Жмём Edit wed.
В окне выбираем единственную дверь (она уже выбрана) и жмём Edit tiles.
Жмём Preview и начинаем выделять ровно те же тайлы, что были извлечены в d01.tis. Прямо вот так по одному нажимаем, соглашаемся и нажимаем кнопку Add tile, потом следующий, и так далее.
В файл d01.tis мы извлекли участок 6x8, вот и здесь отмечаем все 48 тайлов. Далее выделяем самый первый тайл списка и нажимаем External tiles.
Выбираем d01.tis. Готово. Теперь, если нажимать копку Draw closed/Draw open, то будем видеть как двери открываются/закрываются.
Теперь файл d01.tis можно удалить. Он больше не нужен.
Возвращаемся обратно на вкладку Doors в основном окне редактирования локации. Теперь будем задавать области проходимости/непроходимости для разных состояний двери. Для этого жмём кнопку Edit blocks при активированном режиме Open.
Рисуем единички там, где находятся открытые створки двери (ну, или рядом).
Закрываем окно, переключаемся в режим Close и снова жмём Edit blocks.
Рисуем единички вдоль закрытой двери.
Дальше задаём полигон для открывания и закрывания двери. То есть ту область, нажимая на которую в игре дверь будет открываться и закрываться.
В режиме Open жмём Edit polygon.
Обрисовываем открытые створки. Как мы умеем.
Потом то же самое для режима Close.
Ну и последнее - задание стен для дверей. Возвращаемся в Edit wed. Там в режиме Closed жмём Edit polygon.
Обводим контур закрытой двери. Тут не надо это путать с предыдущим шагом. Хоть форма и похожая, но здесь мы обозначаем не то место, на которое надо нажимать, чтобы открыть дверь, а ту область, что будет накладываться на персонажа.
В режиме open у нас две створки, поэтому надо будет использовать два полигона. Первый.
Второй.
Проверяем в игре. Работает.
Вот аналогичным образом надо сделать и все остальные двери. Прямо по той же схеме. Надо только не боятся область пошире для подмены тайлов указывать. Чтобы красивые полутени от открытых/закрытых створок не обрезались. Дополнительно отметим пару нюансов.
Сначала про дверь перед выходом. Мы не хотим, чтобы игрок мог за так просто её открыть. Поэтому надо сделать так, чтобы не было области, на которую можно нажать и дверь откроется. Для этого достаточно не обрисовывать контур двери для нажимания, а просто поставить значения 0 в графе для Bounding box.
Теперь про четвёртую дверь - решётку. Она должна открываться при повороте рычага в боковой комнате. Фактически это обычная дверь, только разнесённая в пространстве. Область для нажимания в одном месте.
Створки (решётка, конечно) - в другом.
Это вот те самые тайлы, что надо подменять. Так вот, можно извлечь эти два набора тайлов в два разных tis-файла. А потом для задания тайлов закрытой двери, сначала выделить те, что для рычага, заменить их. Потом удалить их из списка, добавить те, что для решётки, и подменить их. И в конце просто добавить те, что для рычага. Они уже будут с подменёнными.
Вопрос о том, надо ли создавать одну общую стену для решётки, или лучше по одной маленькой стенки для каждого прута решётки - дискуссионный. Первый способ быстрее, но зато халтурный. Второй кропотливее, но зато более аккуратно получится. Наверное.
Ещё в настройках решётки как двери имеет смысл активировать параметр Transp. В результате в закрытом состоянии она не будет блокировать область видимости. Всё-таки так ведь и в реальности происходит.
2.4. Окружение
Добавим на нашу локации немного декораций, чтобы она не выглядела слишком статичной. Декорации простые - огонь для факелов на стенах и в больших чашах на полу, а также лучи света из окон в начале уровня.
Для этого снова расчехляем BAM Converter из Near Infinity. Для маленького огонька факелов на стенах используем файлы из папки resources\art\props\torch_small. Назначаем центр (35, 55).
Создаём единственный цикл и все кадры закидываем в него. Прямо по-порядку.
Сохраняем под именем torchsm.bam.
В настройках локации переходим во вкладку Animations. Там жмём Add animation. Указываем имя TORCHSM, активируем птицу Blend colors. Этот параметр означает, что картинку надо накладывать в аддитивном режиме. Так как раз нормально будет. У нас там фон был чёрный, значит при наложении он никаких новых цветов не даст.
После этого жмём Find place и в появившемся окне выбираем место для первого огонька.
Смотрим, что получается. Вроде горит.
Точно так же добавляем ещё пять огоньков. Для простоты можно использовать кнопки Copy animation и Paste animation. Один раз копируем, потом создаём новые анимации, вставляем в них скопированный набор параметров. Всё, что остаётся - это задать новое местоположение. Да, чтобы выглядело поживее, стоит активировать птицу Random startframe. Смысл понятен. Каждая анимация будет начинаться не с первого кадра (одинакового для всех), а со случайного.
Ну, поживее теперь выглядит.
Теперь большой огонёк. Всё то же самое, только используем файлы из папки resources\art\props\torch_big. Выставляем центр (35, 68).
Сохраняем под именем torchbg.bam. Размещаем на локации над двумя большими чашами (первой и третьей). А над второй чашей, той, что за колонной - не размещаем.
Для второй чаши будем использовать другую анимацию. Дело тут в следующем. Текущая версия движка GemRB содержит ошибку, известную разработчикам. По идее должна быть возможность перекрывать стенами подобные анимации на локации. Но это не работает. Анимация рисуется поверх всех стен, даже если у стены активирована птица Cover animations. Поэтому, если нет возможности избежать перекрытия стенами анимаций, можно использовать анимации, уже обрезанные по профилю стены. Вот так и будем делать.
Создаём файл torchbgc.bam из файлов, лежащих в папке resources\art\props\torch_big_cutoff. Это тот же самый большой огонь, но как будто бы его перекрывает колонна. Ровно такая, какая расположена перед второй чашей на локации. Выставляем центр как и раньше, а вот местоположение указываем (1363, 1331). Это специальное такое местоположение, чтобы всё стояло ровно и никуда не наползало.
Вот результат.
Дальше очередь лучей света из окон.
Тут всё в целом точно так же. Используем картинки из папки resources\art\props\window_light. Устанавливаем центр где-нибудь в удобном месте, например в верхнем перекрестии рамы.
Сохраняем как window.bam. Добавляем анимацию на локацию, и видим, что работает не совсем как надо.
Хотелось бы, чтобы лучи света рисовались всегда поверх персонажа, а они тут почему-то позади. Почему - как раз понятно. Центр этой анимации находится выше, чем центр персонажа, поэтому персонаж заслоняет лучи. Можно было бы поставить центр лучей в самый низ картинки, но мы сделаем иначе. У каждой анимации есть параметр Height.
Этот параметр позволяет как-бы виртуально сдвинуть центр, чтобы поменять порядок накладывания изображений. У нас картинка с лучами имеет высоту 310, так что поставив Height = 300, эти лучи будут всегда рисоваться поверх персонажа.
То, что лучи рисуются поверх и колонны - это результат уже упомянутой проблемы о том, что GemRB пока с ошибкой обрабатывает (а точнее не обрабатывает совсем) стены поверх анимаций. Ну всё, расставляем подобные лучи из всех окон. Графика локации готова.
3. Персонажи
Пришло время задать нормальную модельку для нашего основного персонажа, а также добавить NPC на локацию.
3.1. Основной персонаж
Необходимые картинки находятся в папке resources\art\characters\green_man. Там есть две подпапки: iddle с анимацией, когда персонаж стоит спокойно, и run, когда он бежит. Других анимаций не заготовлено. Да нам и не нужно.
Каждая анимация отрендерена с 16-ти ракурсов. Так что нам надо будет использовать формат анимации, который поддерживает все 16 разных ориентаций. Об этом стоит упоминать отдельно, так как есть форматы, которые предполагают только 8 ориентаций. Или 16, но те ориентации, что смотрят вправо создаются процедурно отзеркаливанием тех, что смотрят влево. Делается это, понятно дело, для экономии места, которые занимают ресурсы. Мы же экономить не будет. Гулять-так-гулять!
Опишем формат, который будем использовать. Вообще тут, конечно, возникает естественный вопрос: а где взять описание допустимых форматов? Отвечаем: есть сайт в интернете: IESDP. На нём содержится описание форматов разных файлов, которые используются оригинальным движком Infinity Engine. В частности, там есть и про анимации персонажей, вот тут. Раньше мы использовали формат, который называется Type 4000, теперь будем использовать Type A000.
В общем снова достаём BAM Converter из Near Infinity. Добавляем файлы из папки resources\art\characters\green_man\run, которые начинаются префиксом от 00 до 09. То есть первые десять ориентаций бега. Выставляем у всех центр (30, 70).
Создаём 10 циклов по порядку, и в каждый кладём картинки для своей ориентации (то есть в цикл 0 картинки 00_001 - 00_0030, ну и так далее).
Перед тем как сохранять надо убедиться, что выбрана вторая версия bam-файла.
Объясню в чём смысл. Старая (первая) версия не поддерживает alpha-канал в изображениях. Поэтому если он не нужен, то лучше использовать эту первую версию. Но у нас другой случай. У персонажа есть мягкая тень, которая должна красиво смешиваться со статичным фоном локации. Поэтому нужен alpha-канал. Вот для этого и используем более современную версию bam-файлов. Его структура такова, что вся разметка сохраняется непосредственно в bam-файле, а пиксели используемых картинок - в отдельном наборе файлов с расширением *.pvrz. Эти файлы всегда имеют одно и то же название MOSxxxx.pvrz. Какие числа подставлять вместо xxxx определяется параметром PVRZ index start. Так что за этим индексом надо следить отдельно и менять его у каждого нового bam-файла самостоятельно.
Короче, сохраняем всё под именем greeng1.bam. Будут созданы два дополнительных файла: MOS1000.pvrz и MOS1001.pvrz. Обязательно приписать вот этот суффикс g1. Это требование формата хранения анимаций.
Теперь удаляем все файлы, и добавляем картинки с префиксами 10 - 15. Центр у них тот же самый.
Создаём сначала 10 пустых циклов, а потом в циклы с номерами 10 - 15 добавляем кадры анимации. Сохраняем файл с именем greeng1e.bam (используем суффикс g1e). Сохраняем, конечно, с использованием второй версии формата bam-файлов. Начало индексации pvrz-файлов указываем 2000.
Всё, анимация бега сделана.
Теперь переходим к анимации покоя. Все необходимые картинки лежат в папке resources\art\characters\green_man\iddle. Как и раньше, сначала используем ориентации 0 - 9. Центр прежний, начало индексации pvrz-файлов - 3000.
Сначала заполняем 10 циклов. Потом создаём 6 пустых (с номерами 10 - 15). И после этого ещё 10 циклов (с номерами 16 - 25) с теми же самыми анимациями, что и первые 10. Можно просто их скопировать. В Near Infinity есть соответствующие кнопки.
Сохраняем результат под именем greeng2.bam (суффикс g2). По идее формат предполагает и другие анимации, их кадры должны располагаться в последующих циклах. Но мы никаких других анимаций использовать не будем, поэтому ничего больше не добавляем.
Оставшиеся ориентации сохраняем в greeng2e.bam, начало индексации 4000. В этом файле, аналогично ранее, сначала идёт 10 пустых циклов, потом с 10-го по 15-ый - с кадрами анимации, потом ещё 10 пустых (с номерами 16 - 25), и после 6 циклов (с номерами 26 - 31), копирующих первую шестёрку.
Осталось теперь заменить у нашего основного персонажа анимацию. Открываем файл avatars.2da и там в качестве имени bam-файла везде указываем green (без всяких суффиксов), а значение Type = 8.
И последнее, надо немного подкрутить скорость анимации. По умолчанию все анимации воспроизводятся со скоростью 15 кадров в секунду. Это может быть и было нормально в лихие 90-ые, но сейчас уже маловато. Тем более, что все подготовленные анимации отрендерены в 30 fps. Так вот, чтобы указать частоту кадров каждой конкретной анимации, из папки data копируем в папку override файл animfps.2da. И в нём пишем строчки
greeng1 30
greeng1e 30
greeng2 30
greeng2e 30
В итоге вот так должно получиться.
Проверяем в игре.
Красиво.
3.2. NPC
Для NPC будем использовать картинки из папки resources\art\characters\red_man\iddle. Так как наш NPC не будет никуда ходить, то для него можно использовать простой (упомянутый уже ранее) тип анимации Type 4000. Для этого всего-то и надо, что заполнить картинками циклы 16 - 31. Центр у них у всех ставим в той же точке (30, 70). Bam-файл сохраняем с именем red.bam, начало индексации указываем следующее - 5000. Потребуется аж восемь pvrz-файлов. Картинки не показываю, делаем всё как раньше.
В файл avatars.2da добавляем новую строчку. Тут 0xa001 будет идентификатором анимации.
0xa001 red red red red 1 2 1 *
В файл animfps.2da тоже новую строчку
red 30
Заодно, кстати, можно задать 30 fps для уже расположенных на локации анимаций огоньков и лучей света из окон. Точно также, добавив строчки.
Перед тем, как добавить NPC на локацию, надо сначала создать его как персонажа. Это мы уже проходили. При создании нового персонажа надо указать правильный идентификатор анимации (0xa001 в нашем случае), а также значение параметра Reaction = 0x80. Почему так? Этот параметр задаёт агрессивность NPC по отношению к игроку. Измеряется от 1 до 256. Серединка - это 128. В шестнадцетиричной системе счисления - в точности 80.
Не забываем выставить произвольное ненулевое число жизней, а также на второй вкладке Stats & Skills указать ненулевые параметры.
Сохраняем персонажа под именем redman.cre.
Перезагружаем chitin и открываем настройки локации. Во вкладке Actors добавляем нового участника (нажав кнопку Add actor). Задаём имя персонажа REDMAN. С помощью кнопки Set position указываем точку на карте, где должен стоять наш NPC. Жмём кнопку Stay in place, ведь он никуда не должен ходить, а должен, сами понимаете, стоять на месте.
Проверяем в игре. Стоит где надо. Поговорить с ним пока нельзя. Попозже получится.
4. Звуки
Нам на локацию нужно добавить следующие звуки:
Открывания/закрывания дверей
Шаги основного персонажа
Треск огня в больших чашах на полу (для атмосферности)
Приступим.
4.1. Звуки дверей
В паке resources\sound\door есть несколько файлов: doorbg.ogg для больших дверей (они расположены в начале и в конце), doorsm.ogg для маленьких дверей (они огораживают обходной путь), doorarm.ogg (для рычага, поднимающего решётку). Копируем все три файла в папку override.
Открываем настройки локации и переходим во вкладку с дверьми. Для первой двери пишем в полях Opening sound и Closing sound имя файла DOORBG.
То же самое повторяем для пятой двери. Для второй и третьей в этих полях пишем имя файла DOORSM. Ну а для четвёртой двери (это как раз и есть рычаг) - имя DOORARM. Всё просто.
4.2. Звуки шагов
Копируем все файлы (в количестве 10-ти штук) из папки resources\sound\footsteps в папку override. Помимо этого копируем файлы terrain.2da и walksnd.2da из папки data в папку override. Это два конфигурационных файла, с помощью которых задаются звуки шагов персонажей.
В файле terrain.2da в строке TERRAIN на месте пятой звёздочки пишем имя файла шагов без всяких суффиксов, то есть fs_tomb. Должно вот так получиться.
Смысл понятен. Если поверхность под персонажем имеет тип 4 (а у нас на уровне все проходимые места имеют тип 4), то надо проигрывать соответствующие файлы.
Теперь файл walksnd.2da. Строчку DEFAULT заменяем на такую
DEFAULT terrain 0xa000 0xa000 10
Должно вот так получиться.
Смысл тут такой. Значение RESREF = terrain показывает, из какого файла брать ресур для звуков, MIN = 0xa000 задаёт начальный диапазон анимаций, для которых использовать этот звук, а MAX = 0xa000 - конечный. Нам звук шагов нужен только для основного персонажа, так что указываем только идентификатор его анимации. Значение RANGE = 10 означает, сколько у нас есть файлов для звуков. Движок каждый раз выбирает случайным образом звук из этого набора. У нас как раз 10 файлов.
4.3. Атмосферные эффекты
Копируем файлы из паки resources\sound\fire в папку override.
Заходим в настрйки локации, переходим на вкладку Ambients. Жмём кнопку Add ambient, после чего добавляем 4 звуковых ресурса, просто указывая имена только что скопированных файлов: FIREA, FIREB, FIREC и FIRED. Задаём значения Radius = 200, Volume = 150, Period = 1, а также активируем птицы Looping и Random sound selection. Смысл параметров тоже более или менее ясен исходя из названия. Будет создана область указанного радиуса, внутри которой периодически с промежутком в одну секунду будут воспроизводиться случайным образом выбранные звуки из списка. Слышно будет, только когда центр экрана будет в пределах этой области. Само собой задаём центр этой области с помощью кнопки Set center. Устанавливаем центр там, где находится большая чаша с огнём посреди коридора.
Создаём ещё два аналогичных звука окружения, поменяв только центры областей. Указываем их там, где располагаются две другие чаши с большими огоньками.
5. Интерфейс
Пора приступать к созданию игрового интерфейса. Он будет состоять из следующих компонентов:
Стартовое меню для запуска игры
Игровой интерфейс с двумя кнопками для показа текстового окна и меню выхода
Меню, которое появляется когда игра пройдена
5.1. Курсор
Снова используем Near Infinity для создания bam-файла. Добавляем три картинки для разного типа курсоров из папки resources\art\ui\cursor. Выставляем их центры: для иконки действия (кулак) в (18, 18), для диалога (облачко) в (7, 29), и просто для стрелки в (4, 4).
Так как у курсоров есть мягкая тень, то сохранять будем в формате V2, начало индексации 6000. Формат курсора такой. Должно быть 48 циклов, по одному кадру в каждом цикле. Пары соседних циклов используются для разных целей. Внутри каждой пары первый цикл - это изображение не нажатого курсора, а второй - нажатого. У нас всего три типа курсора, так что почти все циклы заполняем обчной стрелкой. Кулак устанавливаем в циклах 2, 3, 22, 23, 30 - 33. Иконка диалога в циклах 18, 19.
Сохраняем файл cursors.bam. Проверяем в игре. Теперь хоть курсор нормальный, а не то недоразуменее, что было раньше.
5.2. Стартовое меню
Вот тут уже никуда не деться, понадобится Python. Хорошо хоть, что нам от него надо лишь одно название. Он используется только как интерфейс для вызова функций движка. Но всё равно, синтаксис ведь Python-а используется.
Но сначала надо подготовить разметку окон интерфейса .Подобные разметки хранятся в файлах с расширением *.chu. Каждый из таких файлов может содержать несколько наборов окон. Каждое окно - набор виджетов. Виджетов не очень много, нам понадобятся кнопки, метки (labels по аглицки), текстовое поле и скролл (scroll bar). Да, картинки - это кнопки без обработчика события нажатий.
Стартовое меню будет иметь вот такой вид.
Тут всего одна фоновая картинка и две кнопки (пока без текста). Верхняя кнопка будет начинать игру, нижняя - закрывать приложение.
Итак, идём в Edit - UI (CHU).
Жмём Add window. Указываем размер нового окна (640, 480). Мы его будем программно центрировать в зависимости от размера игрового окна, поэтому Position не трогаем.
Теперь подготавливаем bam-файлы для этого окна. Используем файл resources\art\ui\menus\start.png чтобы сделать start.bam. Он содержит всего один цикл с одним кадром. Базовую точку оставляем в (0, 0).
Файлы из папки resources\art\ui\button_menu используем, чтобы сделать button.bam. В нём будет один цикл с двумя кадрами. Первый кадр - изображение не нажатой кнопки, второй - нажатой.
Теперь используем их в нашем окне. Создаём три виждета, нажав кнопку Add control. По умолчанию создаются кнопки, как нам и надо.
Последовательность будет такая: верхние две кнопки (с ID = 0 и 1) - это собственно кнопки, а нижняя (c ID = 2) - это фоновая картинка. Размеры кнопок 198 x 48. Такие их и указываем в параметре Dimensions. Position не трогаем, так как кнопки будем располагать тоже программно. В качестве Button BAM используем BUTTON. И ещё указываем, что для не нажатого состояния стоит использовать нулевой кадр цикла, а для нажатого - первый.
У последнего третьего виджета указываем размер 640 x 480. В качестве bam-файла - START. Сохраняем всё под именем guis.chu.
Теперь будем использовать подготовленный файл guis.chu с разметкой стартового окна.
Открываем файл GUIScripts\expath\Start.py из директории с установленным движком. Мы его раньше использовали, чтобы написать запускающий код. Теперь будем расширять. Метод OnLoad пишем таким:
def OnLoad():
window = GemRB.LoadWindow(0, "GUIS")
windows_width = GemRB.GetSystemVariable(SV_WIDTH)
windows_height = GemRB.GetSystemVariable(SV_HEIGHT)
window.SetSize(windows_width, windows_height)
window.SetPos(0, 0)
window.SetBackground({"r": 235, "g": 235, "b": 235, "a": 255})
back_image = window.GetControl(2)
back_size = back_image.GetSize()
pivot = ((windows_width - back_size[0]) // 2, (windows_height - back_size[1]) // 2)
back_image.SetPos(*pivot)
start_button = window.GetControl(0)
start_button.SetPos(pivot[0] + 221, pivot[1] + 146)
quit_button = window.GetControl(1)
quit_button.SetPos(pivot[0] + 221, pivot[1] + 286)
Поясним, что тут делается. Сначала загружается окно с индексом 0 из файла guis.chu. С помощью GetSystemVariable считываются размеры игрового окна. Устанавливается этот размер у окна интерфейса. Потом задаётся светлый (почти белый) фон.
Дальше с помощью метода window.GetControl получается доступ к виждетам. У них устанавливается местоположение. Ну да, используется несколько магических констант. Кому не нравится, может вынести их в самый верх. Делов-то.
Дальше надо добавить события, которые будут срабатывать по нажатию на кнопки. Для второй кнопки всё просто. Она должна закрывать приложение, поэтому пишем
quit_button.OnPress(lambda: GemRB.Quit())
Для первой кнопки чуть похитрее. Она должна запускать игру. Поэтому добавляем функцию
def on_new_game():
GemRB.LoadGame(None)
GemRB.SetNextScript("SetupGame")
И потом в функции OnLoad
start_button.OnPress(lambda: on_new_game())
Проверяем в игре. Работает.
Так как стартовое меню работает нормально, то для ускорения тестирования сделаем, чтобы при запуске приложения оно автоматически пропускалось. Для этого в конфигурационном файле GemRB.cfg движка есть специальный параметр SkipIntroVideos. Будем его использовать. Если этот параметр активирован, то сразу будем начинать игру, не показывая стартовое меню. Делается это просто. В самом начале функции OnLoad пишем
if GemRB.GetVar("SkipIntroVideos"):
on_new_game()
return None
5.3. Игровой интерфейс
Во время игры на экране будут показываться всего две кнопки в нижнем левом углу: для вызова текстового окна (с предыдущими диалогами), и для вызова меню настроек. Как и раньше, сначала подготовим разметку всех нужных окон в отдельном chu-файле.
В BAM Converter Near Infinity закидываем два файла из папки resources\art\ui\button_settings.
Как и раньше, формируем один цикл с этими кадрами. Сохраняем под именем buttons.bam. Это будет кнопка для вызова настроек.
То же самое делаем с файлами из папки resources\art\ui\button_text. Это будет кнопка для вызова текстового окна. Сохраняем под именем buttont.bam.
И точно так же для файлов из папки resources\art\ui\button_dialog. Это будет кнопка для завершения/продолжения диалога. Сохраняем под именем buttond.bam.
Единственную картинку resources\art\ui\menus\settings.png сохраняем в bam-файле под именем settings.bam.
Ещё надо подготовить скролл. Его будем использовать в текстовом окне, чтобы пролистывать содержимое. Для этого закидываем в Near Infinity файлы из папки resources\art\ui\scrollbar. Там всего пять файлов. Две пары стрелочек (вниз и вверх, нажатые и не нажатые), ползунок скролла, и фон по которому этот ползунок должен ездить.
Создаём один цикл, в котором располагаем кадры в следующем порядке:
Не нажатая стрелка вверх
Нажатая стрелка вверх
Не нажатая стрелка вниз
Нажатая стрелка вниз
База
Ползунок
Сохраняем под именем scroll.bam.
Чтобы потом не забыть, сделаем здесь же фон для всплывающей подсказки (так называемый tooltip). Добавляем в Near Infinity файлы из папки resources\art\ui\tooltip. Там три файла: центральная часть, а также левый и правый край. Выставляем у них центры. У центральной части в центре, у левой в центре у левого края, а у правой в центре у правого края.
Создаём три цикла. Первый содержит только центральную часть. Второй - левую. Третий - правую. Сохраняем под именем toolscrl.bam. Именно таким, так как именно такое имя указано в файле data/gemrb.ini для фона подсказок. Впрочем, можно имя изменить (и в ini-файле, и имя bam-файла).
Ну и последнее, копируем файл resources\art\ui\text_area\background.png в папку override, и переименовываем во что-нибудь покороче, например textbg. Эта картинка будет напрямую задаваться как фон текстового окна, поэтому конвертировать её в bam-файл не надо. Вроде с ресурсами здесь всё.
Переходим к созданию разметки окон. Создаём новый chu-файл. В нём будет четыре окна. Первое (ID = 0) для текстового поля, второе (ID = 1) для кнопки продолжения/окончания диалога, третье (ID = 2) для кнопок в правом нижнем углу, и четвёртое (ID = 3) для окна настроек.
Текстовое поле. Размер 1024 x 512. Фоновая картинка textbg. Содержит два виджета. Первый - собственно текстовое поле (text area). Начало в (18, 18), размер 966 x 454. Шрифт - normal (потом его сделаем). Кто-то спросит: "Чё за числа вообще?". Отвечаем. Во-первых, не чё, а что, а во-вторых, размеры ведь всех картинок известны, поэтому аккуратно в пикселях вычисляем откуда что должно начинаться, и какой размер иметь. Координаты виджетов вычисляются относительно левого верхнего угла окна. Координаты самих окон не трогаем, их будем располагать программно.
Второй виджет - скролл типа Scrollbar. Начало в точке (988, 13), размер 16 x 464. Bam-файл - SCROLL. Не забываем указать номера кадров из единственного цикла этого файла, отвечающие разным компонентам скролла.
Втрое окно. Размер 184 x 36, содержит всего один виджет - кнопку точно такого же размера. Использует bam-файл BUTTOND.
Третье окно. Размер 168 x 58. Содержит две кнопки. Первая размера 79 x 48 начинается в (0, 0) и использует файл buttont.bam. Вторая того же размера, но начинается в точке (79, 0) и использует файл buttons.bam.
Ну и четвёртое окно. Окно с настройками. Там будет всего две кнопки: продолжить и выйти. Размер 420 x 280. Обе кнопки имеют размер 198 x 48 и используют button.bam. Только первая начинается в точке (111, 46), а вторая - в точке (111, 186). Ну и фоновая картинка имеет размер 420 x 280 и использует settings.bam.
Сохраняем все сделанные окна в файл guiw.chu. Время Python-а.
Создаём в папке GUIScripts\expath внутри папки с движка GemRB новый файл setup_ui.py. И в нём доабвляем пока пустую функцию
import GemRB
def init_game_ui():
pass
В файле Game.py сначала делаем импорт
import setup_ui
И потом в функции EnterGame перед снятием игры с паузы вызываем
setup_ui.init_game_ui()
Теперь, собственно, настройка UI. Начнём с текстового окна. Сначала создаём файл MessageWindow.py и в него пишем заглушку одной функции
def UpdateControlStatus():
pass
Это нужно для следующего. В папке GUIScripts уже содержится много разных py-файлов, которые используются для нормальных игр. Они там сами вызываются когда надо, что-то в них происходит, в общем какие-то дела делаются. Если хочется переопределить поведение какого-то элемента, то достаточно создать файл с нужным именем в папке для текущей игры, и переопределить содержимое функций. Нам ничего от стандартного MessageWindow не надо. Вот и переопределяем его пустым файлом.
Теперь в файле setup_ui.py добавляем пару импортов
from GUIDefines import WINDOW_VCENTER, WINDOW_HCENTER, WF_BORDERLESS, IE_GUI_VIEW_IGNORE_EVENTS, IE_GUI_TEXTAREA_AUTOSCROLL, IE_GUI_VIEW_RESIZE_ALL, OP_OR
В функции init_game_ui пишем
text_window = GemRB.LoadWindow(0, "GUIW", WINDOW_VCENTER | WINDOW_HCENTER)
text_window.SetFlags(WF_BORDERLESS | IE_GUI_VIEW_IGNORE_EVENTS, OP_OR)
text_window.AddAlias("MSGWIN")
text_window.AddAlias("HIDE_CUT", 0)
text_area = text_window.GetControl(0)
text_area.SetFlags(IE_GUI_TEXTAREA_AUTOSCROLL)
text_area.SetResizeFlags(IE_GUI_VIEW_RESIZE_ALL)
text_area.AddAlias("MsgSys", 0)
text_area.AddAlias("MTA", 0)
Прокомментируем, что тут делается. Сначала загружается окно из файла guiw.chu, имеющее ID = 0. Оно тут же размещается по центру экрана. Потом для него добавляются два алиаса. Алиасы - это названия, которые отождествляются с тем или иным элементом, чтобы потом его можно было по этому названию получить. После этого берём виджет с ID = 0. Это как раз текстовое поле. Задаём разные флаги, и тоже назначаем алиасы. Это нужно, в частности, чтобы все тексты писались именно в это текстовое окно, а не какое другое. Делается это как раз по алиасам.
Можно даже проверить, что всё работает. Возьмём кусок Lorem-Ipsum-а, и вставим его в текстовое поле
text_area.Append("Lorem ipsum...")
Вот результат. На уродский шрифт пока не смотрим, всё поправим через некоторое время.
Подкрутим немного цвет текста. Пишем
text_area.SetColor({"r": 120, "g": 120, "b": 120}, TA_COLOR_NORMAL)
text_area.SetColor({"r": 235, "g": 235, "b": 235}, TA_COLOR_BACKGROUND)
text_area.SetColor({"r": 233, "g": 75, "b": 54}, TA_COLOR_OPTIONS)
text_area.SetColor({"r": 246, "g": 143, "b": 59}, TA_COLOR_HOVER)
text_area.SetColor({"r": 233, "g": 75, "b": 54}, TA_COLOR_SELECTED)
Конечно, не забываем использованные константы импортировать из GUIDefines. С помощью указанных команд задаётся цвет текста в разных случаях:
NORMAL - это обычный текст.
BACKGROUND - это цвет фона для создания эффекта антиалиасинга. Работает только с bam-шрифтами. Мы как раз такой сделаем через некоторое время.
OPTIONS - цвет реплик, которые можно выбирать в диалогах
HOVER - цвет реплики диалога, на которую указывает курсор
SELECTED - цвет реплики диалога, на которую курсор таки нажал
Дальше мы хотим сделать так, чтоб это текстовое окно можно было скрывать и показывать по нажатию специальной кнопки. Можно это делать по-разному. Предлагается не мудрствовать сильно и скрытое окно просто перемещать за пределы экрана. Для этого создаём две константы (за пределами функции init_game_ui)
TEXT_HIDE_POSITION = 0
TEXT_SHOW_POSITION = 0
Пока эти константы нулевые. А вот теперь их и вычисляем
game_height = GemRB.GetSystemVariable(SV_HEIGHT)
text_window_size = text_area.GetSize()
global TEXT_HIDE_POSITION
global TEXT_SHOW_POSITION
TEXT_HIDE_POSITION = game_height * 2
TEXT_SHOW_POSITION = (game_height - text_window_size[1]) // 2
text_window_pos = text_window.GetPos()
text_window.SetPos(text_window_pos[0], WINDOW_HIDE_POSITION)
Не забываем импортировать константу SV_HEIGHT. Смысл действий понятен. Окно показывается либо по центру, либо далеко за пределами экрана. Всё, настройка текстового поля закончена.
Теперь кнопки на экране. Продолжаем писать в функции init_game_ui
btn_window = GemRB.LoadWindow(2, "GUIW", WINDOW_BOTTOM | WINDOW_RIGHT)
btn_window.SetFlags(WF_BORDERLESS | IE_GUI_VIEW_IGNORE_EVENTS, OP_OR)
text_btn = btn_window.GetControl(0)
settings_btn = btn_window.GetControl(1)
Продолжаем не забывать импортировать константы из файла GUIDefines. Располагаем окно с кнопками в правом нижнем углу. Так как его размер на 10 пикселей больше, чем изображения кнопок (всё рассчитано!), то отступ от краёв составит как раз эти 10 пикселей. Вот результат.
Создаём две пока пустых функции
def activate_settings():
pass
def toggle_text_area():
pass
И вызываем их при нажатии на наши две кнопки
text_btn.OnPress(lambda: toggle_text_area())
settings_btn.OnPress(lambda: activate_settings())
Теперь функция activate_settings. В ней надо открывать новое окно. Делаем это.
settings_window = GemRB.LoadWindow(3, "GUIW")
settings_window.AddAlias("Settings", 0)
game_width = GemRB.GetSystemVariable(SV_WIDTH)
game_height = GemRB.GetSystemVariable(SV_HEIGHT)
settings_window.SetSize(game_width, game_height)
settings_window.SetPos(0, 0)
settings_window.SetFlags(WF_BORDERLESS | WF_ALPHA_CHANNEL, OP_OR)
settings_window.SetBackground({"r": 255, "g": 255, "b": 255, "a": 200})
back_image = settings_window.GetControl(2)
back_size = back_image.GetSize()
pivot = ((game_width - back_size[0]) // 2, (game_height - back_size[1]) // 2)
back_image.SetPos(*pivot)
continue_button = settings_window.GetControl(0)
continue_pos = continue_button.GetPos()
continue_button.SetPos(pivot[0] + continue_pos[0], pivot[1] + continue_pos[1])
quit_button = settings_window.GetControl(1)
quit_pos = quit_button.GetPos()
quit_button.SetPos(pivot[0] + quit_pos[0], pivot[1] + quit_pos[1])
GemRB.GamePause(1, 2)
Не забываем импортировать константы! Прокомментируем, что тут происходит из того, чего не происходило ранее. У всего окна устанавливается размер, совпадающий с окном приложения. Далее задаётся белый полупрозрачный фон. Потом фоновое изображение центрируется, а обе кнопки располагаются в соответствии с их местами, сохранёнными в chu-файле. В конце игра ставится на паузу.
В этом окне всего две кнопки. Верхняя будет продолжать игру, а нижняя - выходить в начальное меню. Пишем две функции
def settings_on_quit():
GemRB.QuitGame()
GemRB.SetNextScript("Start")
def settings_on_continue():
settings_window = GemRB.GetView("Settings")
settings_window.Close()
GemRB.GamePause(0, 0)
Назначаем их на кнопки.
continue_button.OnPress(lambda: settings_on_continue())
quit_button.OnPress(lambda: settings_on_quit())
Теперь функция toggle_text_area. Логика более или менее очевидна.
def show_text_area(window):
win_pos = window.GetPos()
window.SetPos(win_pos[0], TEXT_SHOW_POSITION)
def hide_text_area(window):
flags = GemRB.GetGUIFlags()
dialog_flag = flags & GS_DIALOG
if dialog_flag == 0:
win_pos = window.GetPos()
window.SetPos(win_pos[0], TEXT_HIDE_POSITION)
def toggle_text_area():
text_area_window = GemRB.GetView("MSGWIN")
text_area_pos = text_area_window.GetPos()
if text_area_pos[1] == TEXT_SHOW_POSITION:
hide_text_area(text_area_window)
else:
show_text_area(text_area_window)
Не забываем импортировать константу GS_DIALOG. Поясним, что особенного делается в функции hide_text_area. Да, собственно, одна единственная вещь. Проверяется, чтобы игрок был не в режиме диалога. В диалоге нажатие на кнопку закрытия текстового окна должно игнорироваться. А в остальных случаях просто перекидываем местоположение окна с текстовым полем туда-сюда.
И последнее здесь - кнопка продолжения/завершения диалога. Сделаем её, но правда протестировать не получится. У нас ведь нет ещё никаких диалогов.
Копируем файл GUIWORLD.py из папки GUIScripts в папку GUIScripts\expath. Будем переопределять содержимое функций.
В функции OpenDialogButton удаляем фрагмент
frame = MsgWin.GetFrame()
offset = 0
if GameCheck.IsGemRBDemo ():
offset = window.GetFrame()['h']
window.SetPos(frame['x'], frame['y'] + frame['h'] - offset)
Вместо этого пишем по-проще
frame = MsgWin.GetFrame()
window.SetPos(frame["x"] + (504 - 178 // 2), frame["y"] + frame["h"] - 36)
Идея в том, чтобы расположить кнопку внизу окна с текстом по центру. Ну вот эти волшебные числа как раз и делают, что надо.
Дальше в самом начале функции DialogStarted пишем
game_height = GemRB.GetSystemVariable(SV_HEIGHT)
text_window = GemRB.GetView("MSGWIN")
text_window_frame = text_window.GetFrame()
text_pos = text_window.GetPos()
if text_pos[1] > game_height:
text_window.SetPos(text_pos[0], (game_height - text_window_frame["h"]) // 2)
Тут смысл такой, что если окно с текстом за пределами экрана, то сдвигаем его в центр. В конце функции DialogStarted заменяем
ContinueWindow = OpenDialogButton(9)
на
ContinueWindow = OpenDialogButton(1)
То есть меняем ID с 9 на 1. Это потому что у нас в файле guiw.chu окно с кнопкой имеет ID = 1, а не 9.
Дальше функция DialogEnded. В самом начале пишем
game_height = GemRB.GetSystemVariable(SV_HEIGHT)
text_window = GemRB.GetView("MSGWIN")
text_pos = text_window.GetPos()
text_window.SetPos(text_pos[0], game_height * 2)
Ну то есть по завершению диалога закрываем текстовое окно.
Пока здесь всё. Надо будет попозже внести ещё два изменения, чтобы задать текст на кнопке в зависимости от того, для чего она предназначена. Но пока у нас никаких текстов нет, поэтому пропускаем.
5.4. Окончание игры
Когда уровень будет пройден, то будем показывать окошко с поздравлением и одной единственной кнопкой - перейти в начальное меню. Сначала подготавливаем ресурсы для этого окна. Как и раньше, с помощью Near Infinity создаём файл end.bam с картинкой resources\art\ui\menus\end.png.
Теперь создаём новый файл разметки интерфейса. В него добавляем новое окно. Размер 640 x 480. В нём три виджета:
Стандартная кнопка размера 198 x 48, которая начинается в точке (221, 266)
Метка (Label) размера 420 x 166, которая начинается в точке (110, 100). У неё задаём центрирование как по вертикали, так и по горизонтали, указываем шрифт NORMAL, а также задаём цвет текста: 160 для цвета букв и 235 для цвета фона
Картинка для фона размера 640 x 480
Добавляем ещё одно окно размера 1 x 1. Его программно растянем на весь экран. Никаких виджетов на нём не надо.
Если DLTCEP не сохраняет пустое окно без виджетов, то можно добавить какой-нибудь, сохранить, а потом удалить его и снова сохранить. Должно помочь.
Теперь Python-овский код. Копируем из папки GUIScripts файл TextScreen.py в папку GUIScripts\expath. Его содержимое нам по большому счёту не нужно. Оставляем только функцию ToggleAmbients и глобальную переменную AmbientVolume. Удаляем все остальные функции и глобальные переменные, и заново пишем функцию StartTextScreen
GemRB.GamePause(1, 3)
ToggleAmbients(0)
GemRB.HardEndPL()
windows_width = GemRB.GetSystemVariable(SV_WIDTH)
windows_height = GemRB.GetSystemVariable(SV_HEIGHT)
back_window = GemRB.LoadWindow(1, "GUIE")
back_window.SetSize(windows_width, windows_height)
back_window.SetPos(0, 0)
back_window.SetFlags(WF_BORDERLESS | WF_ALPHA_CHANNEL, OP_OR)
back_window.SetBackground({"r": 235, "g": 235, "b": 235, "a": 255})
window = GemRB.LoadWindow(0, "GUIE")
window.AddAlias("End")
window.SetSize(windows_width, windows_height)
window.SetPos(0, 0)
window.SetFlags(WF_BORDERLESS | WF_ALPHA_CHANNEL, OP_OR)
window.SetBackground({"r": 235, "g": 235, "b": 235, "a": 255})
back_image = window.GetControl(2)
back_size = back_image.GetSize()
pivot = ((windows_width - back_size[0]) // 2, (windows_height - back_size[1]) // 2)
back_image.SetPos(*pivot)
button = window.GetControl(0)
button_pos = button.GetPos()
button.SetPos(pivot[0] + button_pos[0], pivot[1] + button_pos[1])
button.SetColor({"r": 120, "g": 120, "b": 120}, TA_COLOR_NORMAL)
button.SetColor({"r": 235, "g": 235, "b": 235}, TA_COLOR_BACKGROUND)
button.MakeDefault()
label = window.GetControl(1)
label_pos = label.GetPos()
label.SetPos(pivot[0] + label_pos[0], pivot[1] + label_pos[1])
import GUICommonWindows
GUICommonWindows.CloseTopWindow()
window.ShowModal(MODAL_SHADOW_NONE)
Мы уже более или менее знакомы с тем, что означают все эти команды.
Добавляем новую функцию end_on_press
def end_on_press():
window = GemRB.GetView("End")
if window:
window.Close()
GemRB.HardEndPL()
GemRB.PlaySound(None, CHAN_GUI, 0, 0, SND_SPEECH)
ToggleAmbients(1)
GemRB.GamePause(0, 3)
Вызываем её по нажатию на единственную кнопку
button.OnPress(end_on_press)
Протестируем чуть попозже, когда будем оформлять этап окончания уровня.
5.5. Звук кнопок
Здесь уместно добавить звук нажатия на кнопки. Делается это легко. Копируем файл resources\sound\ui\btnclick.ogg в папку override. Также копируем файл data\defsound.2da в папку override. Открываем его и напротив строки BUTTON пишем
Всё бы ничего, да только теперь при нажатии на все фоновые картинки (которые ведь тоже кнопки) раздаётся звук клика. А это совсем не нужно. Исправляется тоже просто. Надо у каждого такого элемента выключить соответствующий флаг с помощью команды
back_image.SetFlags(IE_GUI_BUTTON_SOUND, OP_XOR)
Это, значит, сделали в файле Start.py. Аналогично в файле setup_ui.py в функции activate_settings, и в файле TextScreen.py в функции StartTextScreen.
6. Шрифт
Мы до последнего оттягивали упоминание и написание текста. Дальше тянуть нет никакой возможности. В общем тут есть ряд проблем. Ну как проблем - неудобств. Это касается того, как отображать тексты на экране. Есть две возможности. Первая - с помощью ttf-шрифтов. Для нас проще, но выглядит плохо и не особо настраивается. Вторая - использовать так называемые bam-шрифты. Каждый такой шрифт представляет собой обычный bam-файл, в котором кадры - это разные символы, расположенные в определённом порядке. Ну и когда на экране нужно изобразить слово на букву ж... (жизнь, например), то движок смотрит, в каких кадрах располагаются нужные буквы, и рисует соответствующие картинки на экране. Получается слово. Такой подход допускает большей гибкости в настройках, поэтому будем его использовать.
Нам понадобится два разных шрифта. По начертанию они одинаковые, отличаются только размером. В папке resources\font содержатся две папки, в которых уже заранее отрендерены необходимые символы с использованием полужирного шрифта Verdana размером 16 и 10. Начнём с размера 16.
Закидываем все символы из папки resources\font\verdana_16 в BAM Converter Near Infinity.
Теперь предстоит трудоёмкий этап. Надо пройтись по каждому кадру и выставить его базисную точку. Правило такое: по вертикали она располагается в самом низу (то есть можно выделить все кадры и установить Center Y = 19), а по горизонтали по середине. Так, например, если размер картинки 13 x 19, то центр должен находиться в точке (6, 19). То есть берём целую часть от деления на 2. В общем, проходимся и вручную выставляем центры.
Кто честно прошёлся, то заметил, что у нас тут есть английские буквы, русские буквы и ещё немного спец-символов. И нет буквы "ё". А тех, кто не прошёлся, тех мы попросим не лениться.
Теперь создаём циклы. По одному на каждый символ. Имена файлов сделаны такими, чтобы было видно в каком цикле должен располагаться тот или иной символ. Для всех остальных используем 000_unknown.png. Например, первые 31 цикл (0 - 30) содержат именно эту заглушку, 32-й (31-ый, если нумеровать с нуля) - символ пробела 031_space.png и так далее. Там дальше всё по порядку. Потом заглушки идут в циклах 126 - 190 (кроме 170 и 186).
Сохраняем файл verdana_16.bam.
Теперь надо сказать движку, чтобы использовал этот файл. Для этого копируем файл data\fonts.2da в папку override. В нём в строке NORMAL вместо шрифта verdana_bold пишем verdana_16.
Этого достаточно. Проверяем в игре, выведя в текстовое поле Lorem-Ipsum подлиннее.
Вот теперь, по-моему, красиво. И, что важно, читабельно.
Повторяем всё то же самое для шрифта размером 10pt. Используем для этого картиночки из папки resources\font\verdana_10. Сохраняем файл под именем verdana_10.bam. Мы этот шрифт используем для текста на подсказках (tooltip-ах). Для этого в файле fonts.2da в строке FLOATTXT пишем FONT_NAME = verdana_10.
Но надо ещё настроить цвет этого текста. Для этого копируем файл data\colors.2da в папку override. И в нём заменяем строчки
TOOLTIP 0x787878ff
TOOLTIPBG 0xebebebff
И в том же файле задаём цвет сообщений в диалогах.
DIALOG 0x787878ff
Проверяем в игре.
Работает.
7. Строки
Все текстовые данные хранятся в файле dialog.tlk. Будь это одно слово, предложение или фраза диалога, каждому из них назначается свой числовой идентификатор (указатель на строку), который используется при подготовке ресурсов. Для имён персонажей, названий вещей, надписей на кнопках, фразах в диалогах и так далее. Все текстовые данные должны храниться в этом файле dialog.tlk. Это удобно тем, что для локализации можно открыть этот файл, перевести все фразы - и готово, вот вам вся игра уже не на английском, а на русском (или наоборот).
7.1. Создание строк
Всё бы ничего, но когда мы хотим использовать русский текст начинаются проблемы. Принципиально dialog.tlk может хранить его в любой кодировке, но надо ведь, чтобы ещё движок GemRB правильно преобразовывал те байты, что там хранятся в индексы буковок из bam-шрифта. Поэтому надо использовать одну единственную правильную кодировку - cp1251. С ней нет проблем, а с utf-8 - есть. Что, конечно же, очень огорчает.
Ещё одна проблема связана с тем, как добавить строку, закодированную cp1251, в этот самый dialog.tlk. Для маленьких кусков текста (вроде имён персонажа) можно использовать DLTCEP. Только перед загрузкой строк надо не забывать почаще нажимать кнопку Reload dialog. Для надёжности. А то наслоится там что-нибудь, обрывки текста в результате останутся.
Давайте добавим имя нашему красному NPC. Назовём его на западный манер Рэдмэн. Заходим в редактирование персонажа Redman. Жмём кнопку New string, а потом в текстовое поле просто пишем нужное имя. Сохраняем. Жмём Exit, и программа спросит, хотим ли мы сохранить что-то новое в файл dialog.tlk. Говорим, что хотим.
Точно так же открываем настройки основного персонажа, и указываем ему имя Игрок. Эта строка будет идти по указателю 1.
Что делать в более сложных случаях? Я попробовал разные способы, но самый удобный (и тем не менее не очень удобный) - это через диалоговый файл для WeiDU.
В общем делаем так. Создадим отдельный текстовый файл, назовём его, к примеру strings.d. Где угодно. Надо только убедиться, что его кодировка cp1251. В нём пишем
BEGIN ~STRINGS~
IF ~~ THEN BEGIN 1
SAY ~Пауза~
END
Это пример простейшего диалогового файла. Чуть позже обсудим их структуру подробнее. Но тут нам нужно слово (или фраза), которые стоят после ключевого слова SAY между тильдами (знак ~). Это и есть то слово, что будем загружать в файл dialog.tlk. Для этого в терминале (в обычном системном терминале) пишем
weidu.exe --game .\expath\ .\strings.d
Эта команда запускает консольное приложение WeiDU, в качестве параметра указывается папка с игрой (полный путь до файла dialog.tlk получается expath\dialog.tlk). И потом указывается диалоговый файл. В результате будет создан файл STRINGS.dlg. Он нам даром не нужен, можно сразу удалить. Или оставить, всё равно потом снова появится. Но важнее вывод, который пишется в результате этого запуска
[.\expath\dialog.tlk] claims to be writeable.
[.\expath\dialog.tlk] claims to be a regular file.
14 characters, 1 entries added to DIALOG.TLK
[.\expath\dialog.tlk] created, 3 string entries
Это означает, что добавилась новая строка, их там теперь 3. Значит добавленная имеет указатель 2.
Точно так же добавляем строчку "Снято с паузы". Она будет иметь указатель 3. Кто-то спросит: зачем нам нужны эти строчки? Отвечаем: сейчас сделаем так, чтобы когда игра ставится на паузу, в текстовом окне появлялось слово "Пауза", а когда снимается - "Снято с паузы". Для этого копируем файл data\strings.2da в папку override. Открываем его и напротив PAUSED пишем 2 (то есть указатель на строку "Пауза"), а напротив UNPAUSED - 3. Проверяем в игре.
Строчки появляются. Это я последовательно нажимаю клавишу "Пробел". Ну, отлично же!
7.2. Надписи в UI
Теперь мы готовы добавить надписи на кнопках в интерфейсе.
Итак, записываем в dialog.tlk следующие строки: "Выйти", "Начать", "Продолжить", "Заново", "Уровень пройден!", "Закончить", "Дальше". Для каждого из них записываем указатель.
Теперь в файле setup_ui.py в функции activate_settings добавляем
continue_button.SetFlags(IE_GUI_BUTTON_CAPS, OP_XOR)
continue_button.SetText(6)
continue_button.SetColor({"r": 120, "g": 120, "b": 120}, TA_COLOR_NORMAL)
continue_button.SetColor({"r": 235, "g": 235, "b": 235}, TA_COLOR_BACKGROUND)
Это добавит строку по указателю 6 на кнопку продолжения. Тут же задаём цвет шрифта и фона для антиалиасинга. Первая команда отключает установленный по умолчанию режим, когда все надписи на кнопках рисуются заглавными буквами.
То же самое делаем с кнопкой выхода в начальное меню
quit_button.SetFlags(IE_GUI_BUTTON_CAPS, OP_XOR)
quit_button.SetText(4)
quit_button.SetColor({"r": 120, "g": 120, "b": 120}, TA_COLOR_NORMAL)
quit_button.SetColor({"r": 235, "g": 235, "b": 235}, TA_COLOR_BACKGROUND)
И ещё в файле fonts.2da в строчке BUTTON меняем шрифт на verdana_16. Проверяем в игре.
То же самое повторяем для кнопок start_button и quit_button в файле Start.py, а также для кнопки button в файле TextScreen.py. Чтобы задать текст метки на завершающем игру экране можно воспользоваться интерфейсом DLTCEP. Открываем файл guie.chu, и в нём для метки задаём Label strref = 8.
Теперь кнопка продолжения/завершения диалога. В файле GUIWORLD.py меняем функцию OpenEndMessageWindow на такую
Button = ContinueWindow.GetControl(0)
Button.SetVisible(True)
Button.SetDisabled(False)
Button.SetFlags(IE_GUI_BUTTON_CAPS, OP_XOR)
Button.SetText(9)
Button.SetColor({"r": 180, "g": 180, "b": 180}, TA_COLOR_NORMAL)
Button.SetColor({"r": 235, "g": 235, "b": 235}, TA_COLOR_BACKGROUND)
ContinueWindow.SetVisible(True)
Button.OnPress(CloseContinueWindow)
Button.SetFlags(IE_GUI_BUTTON_NO_TOOLTIP, OP_OR)
Button.MakeDefault(True)
А функцию OpenContinueMessageWindow на такую
Button = ContinueWindow.GetControl(0)
Button.SetVisible(True)
Button.SetDisabled(False)
Button.SetFlags(IE_GUI_BUTTON_CAPS, OP_XOR)
Button.SetText(10)
Button.SetColor({"r": 180, "g": 180, "b": 180}, TA_COLOR_NORMAL)
Button.SetColor({"r": 235, "g": 235, "b": 235}, TA_COLOR_BACKGROUND)
ContinueWindow.SetVisible(True)
Button.OnPress(CloseContinueWindow)
Button.SetFlags(IE_GUI_BUTTON_NO_TOOLTIP, OP_OR)
Button.MakeDefault(True)
И ещё в самый конец функции CloseContinueWindow добавляем
ContinueWindow.SetVisible(False)
7.3. Подсказки в игре
Теперь, когда у нас есть строки, можно добавить подсказку о запертой решётке на нашей локации. Дескать, открывается она не здесь. Для этого открываем свойство нашей локации, переходим на вкладку Regions, там с помощью кнопки Add region создаём новую область, устанавливаем её тип Info.
Жмём кнопку Edit Polygon и обрисовываем примерную область, нажав на которую должно появиться сообщение.
Жмём кнопку Edit strings и пишем в первой графе text что-то вроде "Чтобы открыть решетку нужно поискать рычаг. Тот рычаг..." (понимаете шутку, да? Кто не понимает, то делает вид, что ничего не происходит)
Сохраняем локацию, закрываем окно редактирования локации и соглашаемся обновить файл dialog.tlk.
Последний штрих здесь - сменить иконку курсора, когда он наводится на область этой подсказки. По умолчанию стоит 42, надо поставить 2 (у нас это иконка кулака).
Проверяем в игре.
Тут есть небольшая недоделка. Дело в том, что эта область подсказки не пропадает, даже если дверь открыта, и подсказка не нужна. Чтобы починить это - нужно использовать скрипты. Самое время.
7.4. Скрипты
Каждый скрипт представляет собой набор кусков вида
IF
[условие]
THEN
RESPONSE #100
[действие]
END
Условие проверяется 15 раз в секунду, и если оно выполняется, то совершается действие. Вот это вот RESPONSE #100 задаёт вес по которому будет выбрано то или иное действие. Обычно пишут 100, и никакого случайного выбора не происходит. Возможные условия перечислены в файле data\trigger.ids, а действия в data\action.ids. Что каждое из них значит написано на IESDP. Чтобы всё работало нормально копируем оба файла в папку override, а помимо этого ещё и файлы data\boolean.ids и data\object.ids. Лучше после этого перезапустить DLTCEP.
Создаём новый скрипт.
Пишем
IF
OpenState("DOOR0004",TRUE)
Global("SWITCHARMOFF","GLOBAL",1)
THEN
RESPONSE #100
Deactivate("INFOPOINT01")
SetGlobal("SWITCHARMOFF","GLOBAL",0)
END
IF
OpenState("DOOR0004",FALSE)
Global("SWITCHARMOFF","GLOBAL",0)
Global("SWITCHARMINIT","GLOBAL",1)
THEN
RESPONSE #100
Activate("INFOPOINT01")
SetGlobal("SWITCHARMOFF","GLOBAL",1)
END
IF
Global("SWITCHARMINIT","GLOBAL",0)
THEN
RESPONSE #100
SetGlobal("SWITCHARMINIT","GLOBAL",1)
SetGlobal("SWITCHARMOFF","GLOBAL",1)
END
Надо прокомментировать, что тут происходит. OpenState - это триггер, который возвращает True, если состояние двери DOOR0004 такое, как указано вторым аргументом. TRUE - дверь открыта, FALSE - закрыта. Триггер Global("SWITCHARMOFF","GLOBAL",1) возвращает True если значение глобальной переменной SWITCHARMOFF = 1. В самом начале эта переменная вообще не объявлена, поэтому её значение нулевое. Вот и получается, что в самом начале выполняется третий фрагмент. А после этого либо первый, либо второй в зависимости от того, открыта дверь или нет. Если она открыта, то подсказка с именем INFOPOINT01 отключается, а если закрыта - то включается. Переменная SWITCHARMOFF нужна для того, чтобы действие выполнять только один раз.
Сохраняем этот скрипт под именем arehnscr.bcs. Потом открываем настройки локации, и присоединяем этот скрипт к четвёртой двери. На самом деле можно в любое место, но так хоть не потеряем его потом.
Ещё стоит прокомментировать, что в скрипте используются два имения объектов локации: имя двери и имя области подсказки. Так вот это те имена, что в интерфейсе DLTCEP пишутся после номера с точкой.
Добавим ещё текстовое сообщение, которое должно появляться в момент открывания/закрывания решётки. Для этого добавляем новую строку "За углом раздается лязг решетки". У меня указатель на эту строку равен 12. Потом зафиксируем имя переменной, которая будет указывать на основного персонажа. Для этого открываем настройки этого самого персонажа и во вкладке Icons & Scripts задаём Scripting name = PLAYER.
Теперь в самом скрипте добавляем строчку
DisplayStringHead("PLAYER",12)
в конце обоих сегментов, срабатывающих в момент переключения состояния двери.
Вот результат.
7.5. Скрипт окончания игры
Окончанием игры будем считать момент, когда основной персонаж зайдёт на лестницу за пятой дверью. Пока, для теста, размести область для окончания где-нибудь поближе, куда не надо так далеко бежать. Создаём на локации область типа ловушка (Trap).
Располагаем где-нибудь недалеко от входа.
Теперь создаём новый скрипт и пишем в нём
IF
Entered([0])
THEN
RESPONSE #100
IncrementChapter("")
SmallWait(1)
EndCredits()
END
Сохраняем под именем endscr.bcs. Назначаем этот скрипт на только что сделанную ловушку. Активируем её, задав значение Trapped = 1.
Смысл тут в том, что как только кто-то заходит в область ловушки, вызывается команда IncrementChapter(""), из-за чего происходит смена главы. При этом вызывается функция StartTextScreen из файла TextScreen.py. Мы там жмём кнопку, окно закрывается. После этого выполняется вторая команда SmallWait(1), которая ждёт один тик (1/15 секунды). Потом EndCredits(), из-за чего игра вываливается в начальное меню, и там дальше по установленной процедуре.
Всё работает. Перемещаем область действия ловушки за пятую дверь.
8. Диалог
Пришло время написать диалог с нашим единственным NPC. Пусть это будет ворчливый старикашка, вечно раздражённый и недовольный. И он немного ку-ку, зациклен на двери, рядом с которой стоит. И пусть он всё время считает, что она красного цвета. Так про неё и говорит, дескать, красная дверь.
Диалог будет содержать четыре состояния. Вначале, когда персонаж подходит первый раз, у NPC можно спросить, как пройти дальше. Диалог перейдёт во вторую стадию, в которой NPC предлагает ответить на загадку. При согласии диалог переходит в третью стадию, когда NPC загадывает, собственно, загадку. Так как он ку-ку, то конечно же задаёт загадку про свою дверь. При правильном ответе диалог переходит в последнюю четвёртую стадию, когда NPC открыл дверь и бормочет что-то невнятное. При неправильном ответе - во вторую. И снова - спрашивает, готов ли отгадывать загадку, задаёт её, и так далее.
Все диалоги пишутся в d-файле, после чего компилируются в dlg-файл с помощью WeiDU. Структура диалогового файла такая:
BEGIN ~имя диалога~
IF ~условие~ [метка 0]
SAY ~реплика NPC~
IF ~~ THEN REPLY ~ответ игрока~ GOTO [метка 1]
IF ~~ THEN REPLY ~другой ответ игрока~ GOTO [метка 2]
END
IF ~условие~ [метка 1]
SAY ~реплика NPC~
IF ~~ THEN EXIT // это просто завершит диалог
END
В общем там есть разные ключевые слова. Но суть в том, что каждый диалог состоит из кусков. У каждого куска есть условие его использования и метка. Ответы игрока указывают на какую метку переходить дальше. После ответов и до перехода по метке можно совершить некое действие, аналогично тому, как это делалось в скриптах. Условия в тильдах подобны триггерам скриптов и используются для того, чтобы определить, какие реплики NPC и игрока допустимы в каждый текущий момент.
Итак, создаём где угодно файл redman.d. Текстовый, в кодировке cp1251. Сначала он ничего не содержит
BEGIN ~REDMAN~
Сначала добавим реплику, которая будет показываться, если игрок оказался в последней комнате, не открыв решётку. Такого, по идее, не может произойти, но мало ли. Вдруг кто с помощью Ctrl+J по локации скакал.
IF ~OpenState("Door0004", FALSE)~ 0.0
SAY ~Ты как тут оказался? Иди выйди и зайди нормально!~
IF ~~ THEN EXIT
END
Компилируем dlg-файл с помощью команды
weidu.exe --game .\expath\ .\redman.d
Копируем созданный файл redman.dlg в папку override. Присоединяем его к NPC, задав значение параметра Dialog = REDMAN на вкладке Icons & scripts.
Проверяем в игре.
У NPC всего одна реплика, и он её говорит. Всё работает пока как надо.
Теперь пишем реплику, которую говорит NPC в первом состоянии диалога.
IF ~OpenState("Door0004", TRUE)
Global("RMState", "GLOBAL", 0)~ 0.0
SAY ~Ну что ты тут ходишь все время?! Туда-сюда, туда-сюда. И на мою КРАСНУЮ дверь смотришь! Тебе чего надо?!~
IF ~~ THEN REPLY ~Ты знаешь как дальше пройти? За вот эту дверь, что за тобой. Она не открывается.~
DO ~SetGlobal("RMState", "GLOBAL", 1)~
GOTO 1.0
IF ~~ THEN REPLY ~Раскричался тут. Пойду я, пожалуй.~ EXIT
END
Видно, что эта реплика показывается, если решётка открыта и значение глобальной переменной RMState = 0. Мы эту переменную ещё не определяли, так что она именно такая. На эту реплику есть два ответа. Первый ответ переводит диалог в следующее состояние (назначает значение RMState = 1), и показывает следующую фразу по метке 1.0. Второй ответ просто заканчивает диалог. Он остаётся в прежнем состоянии.
Следующая фраза.
IF ~~ 1.0
SAY ~Знать-то знаю, но почему я тебе это должен говорить?! Ты тут ходить будешь, больше одного собираться, на дверь мою КРАСНУЮ смотреть! А я тебе ее, значит, открвывай! Да?.. Хорошо.~
= ~Тебе надо ответить на один вопрос. Ответишь правильно, открою мою КРАСНУЮ дверь, нет - давай досвиданье! Понял?!~
IF ~~ THEN REPLY ~Господи-Иисусе... Понял. Давай свой вопрос.~
DO ~SetGlobal("RMState", "GLOBAL", 2)~
GOTO 2.0
IF ~~ THEN REPLY ~Все-таки кричишь ты много. Что за манера так с людьми разговаривать. Пойду я, сам разберусь.~ EXIT
END
Здесь используется специальный синтаксис фразы NPC. Если разделить фразу на несколько частей символом =, то он не зараз вывалит всё это на экран, а покажет по частям. Ну и снова два ответа. Первый ответ переводит диалог в следующее состояние, и переходит к следующей фразе. Второй ответ заканчивает диалог, значение переменной RMState остаётся равным 1.
Теперь фраза, которую говорит NPC, если диалог в состоянии RMState = 1.
IF ~Global("RMState", "GLOBAL", 1)~ 0.1
SAY ~Ну, разобрался?! Будешь отвечать на мой вопрос?~
IF ~~ THEN REPLY ~Давай свой вопрос.~
DO ~SetGlobal("RMState", "GLOBAL", 2)~
GOTO 2.0
IF ~~ THEN REPLY ~Нет.~ EXIT
END
Теперь фраза в состоянии RMState = 2. Собственно, вопрос.
IF ~Global("RMState", "GLOBAL", 2)~ 2.0
SAY ~Какого цвета моя К... Красивая дверь?~
IF ~~ THEN REPLY ~КРАСНАЯ!~
DO ~SetGlobal("RMState", "GLOBAL", 3)
OpenDoor("DOOR0005")~
GOTO 3.0
IF ~~ THEN REPLY ~Белая.~
DO ~SetGlobal("RMState", "GLOBAL", 1)~
GOTO 4.0
IF ~~ THEN REPLY ~Зеленая.~
DO ~SetGlobal("RMState", "GLOBAL", 1)~
GOTO 4.0
IF ~~ THEN REPLY ~Нет тут никакой двери.~
DO ~SetGlobal("RMState", "GLOBAL", 1)~
GOTO 4.0
END
Первый ответ правильный. Поэтому назначается RMState = 3, пятая дверь открывается, и диалог переходит к следующей реплике. Остальные ответы неправильные, поэтому состояние откатывается к предыдущему, и диалог переходит к другой реплике.
Вот эти две завершающие реплики.
IF ~~ 3.0
SAY ~Э-э-э... Правильно... Это что же получается, мне ее открыть надо... Вот, извольте пройти...~
IF ~~ THEN REPLY ~Спасибо тебе, беспокойный человек.~ EXIT
IF ~~ THEN REPLY ~Слава Господу нашему Иисусу. Аминь.~ EXIT
IF ~~ THEN REPLY ~Вот спокойно нельзя было, человече?~ EXIT
END
IF ~~ 4.0
SAY ~Неправильно! Давай досвиданье!~
IF ~~ THEN EXIT
END
И ещё одна реплика, которую NPC говорит, если к нему подойти после того, как дверь открыта.
IF ~Global("RMState", "GLOBAL", 3)~ 3.1
SAY ~Уйди, христа-ради, а... Ты не видишь, что ли, что КРАСНАЯ дверь открыта?!!! Никогда такого не было, и вот опять...~
IF ~~ THEN EXIT
END
По идее с диалогом всё. Компилируем, переписываем результат в папку override, и должно всё работать.
9. Последние штрихи
Фактически всё готово. Осталось подкрутить пару-тройку моментов.
Первый - повысим частоту игровых кадров. По умолчанию все действия происходят 15 раз в секунду. Это касается не только вызовов скриптов, но и перемещений персонажа. У нас анимации все проигрываются с частотой 30 кадров в секунду. Поэтому видна некоторая дёрганность движений. Всего-то и надо сделать, что в файле game.ini в коневой папке с игрой задать значение Maximum Frame Rate = 60. Частота обновлений игрового мира будет половина от этой величины, как раз 30 раз в секунду. И надо понизить скорость передвижения персонажа. В файле moverate.2da выставляем значение SPEED = 4.
Второй момент - поведение камеры. Сейчас персонаж бегает независимо от камеры обзора, которую можно перемещать либо средней клавишей мыши, либо скролить экран, подводя мышь к его границам. Сделаем так, чтобы камера всегда своим центром смотрела на персонажа. Для этого в файле Game.py в самом начале функции EnterGame добавим одну-единственную строчку
GemRB.GameControlSetScreenFlags(SF_CENTERONACTOR | SF_ALWAYSCENTER, OP_OR)
Конечно, не забываем импортировать константы.
Третий момент - это цвет имён участников диалога. Сейчас цвета имён как основного игрока, так и NPC оранжевого цвета.
Чтобы задать цвет имени персонажа, надо сделать следующее. В папке data есть файл pal16.png. Так вот цвет имени персонажа берётся из него. Делается это так. Один из параметров персонажа - это Major colour.
Движок берёт значение этого параметра (на приведённой выше картинке это 57) и использует цвет, который находится в 5-ом столбце (имеет индекс 4, если считать с нуля) и 58-ой строке (имеет индекс 57, если тоже считать от нуля).
В палитре pal16.png есть начальный сегмент (первые 8 строк) в который можно записывать любые пиксели. Так что если не получается найти подходящий цвет в 5-м столбце, то допустимо какие-то из первых пикселей этого столбца сделать нужными. Но для порядку сначала этот файл копируется в папку override. Для наших целей вполне подойдёт пиксель в строке с индексом 64 (для основного персонажа), и 18 для NPC. Эти числа и указываем в качестве значения параметра Major colour.
Проверяем в игре.
Четвёртое. Для игры можно использовать любой размер игрового окна. Но у нас в интерфейсе текстовое окно имеет размер 1024 x 512. При маленьком разрешении оно не будет помещаться на экран. Поэтому можно установить минимальный допустимый размер игрового окна. Для этого в файле data\gemrb.ini устанавливаем MinWidth = 1024 (а на самом деле в файле, скопированном в override).
И последнее. Для порядку все файлы из папки override перемещаем в папку data. С заменой. Теперь можно начинать создавать следующий уровень.
Ну что-ж, на этом, пожалуй, всё.
Комментарии (2)
green_bag94
15.01.2025 14:43Изометрическая графика - очень красиво! Да ещё со светом, с тенями! Что интересно: здесь персонаж движется по произвольному вектору. Довольно часто в изометрии движение происходит по ограниченному набору (вероятно, 8) направлений.
CBET_TbMbI
Вот уж истинно: хорошего гайда должно быть много.