Ранняя альфа-версия игры Galaxy Boom mini, декабрь 2017: Скачать (8mb)

14 июля 2019

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

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

Для сцен, которые необходимо отображать постоянно (ранее — «Главная сцена», с отладочной информацией, консолью и т.д.), используется специальное свойство Всегда активна — такие сцены не затрагиваются при смене состояний. Вот пример работы с состояниями:

manager.addState('first', ['scene1']);
manager.addState('second', ['scene2']);
manager.setState('first');

Ранее все сообщения между элементами интерфейса рассылались от родителя ко вложенным (в прямом порядке), что привело к проблемам с передачей сообщений ввода. Представим, что есть два перекрывающих друг друга элемента — obj1 и obj2. Второй элемент должен выводиться на экран после первого (перекрывать его). Однако, в случае с сообщением mouseDown такая последовательность обработки приведет к тому, что obj1 (который рисуется снизу) перехватит это сообщение, и оно не дойдет до верхнего obj2. Для избежания этой проблемы мы разделили все сообщения по методу рассылки на прямые и обратные. В примере выше, сообщение mouseDown должно обработаться в обратном порядке, а draw — в прямом.

uiManager

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

Добавлено событие onError для описания действий, которые необходимо выполнить при возникновении ошибки. В событии имеется дополнительный параметр, позволяющий пометить сообщение как критическое.

Убраны некоторые неиспользуемые функции.

uiElement

Значительные изменения коснулись модуля uiElement. Доработан механизм обработки элементов под фокусом и наведенных (hovered) элементов. Элемент, помеченный как focused, может быть только один — теперь это же условие относится и к hovered-элементу. При установке этих флагов элемент обращается к самому верхнему родителю с просьбой снять флаг focused/hovered с текущего отмеченного им элемента и затем устанавливает его себе. Если с фокусом такая ситуация видится полностью логичной, то в случае с наведением вносит некоторые ограничения, которые, однако, пока не мешают работе системы.

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

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

Напомню, что ранее был реализован метод animate() для плавного изменения значений свойств типа Integer и Single. Теперь есть возможность анимировать текстовые свойства (если они содержат числовые значения). Это может быть полезно для анимации количества здоровья персонажа при нанесении ему урона. Также, метод animate() теперь принимает callBack-функцию в качестве последнего параметра — эта функция будет вызвана после окончания анимации. Помимо прочего, это можно использовать для реализации нескольких анимаций подряд, воспроизводящихся одна за другой.

Добавлен еще один метод для работы с анимацией — animateBack(), который принимает в качестве параметра отклонение от текущего значения свойства и затем анимирует свойство от нового значения к текущему. Вот пример кода, реализующего тряску панели при помощи метода animateBack() и callBack-функции для воспроизведения нескольких анимаций:

nowPanel.animateBack('y', 5, 100,
  procedure(element: TUiVisualElement)
  begin
    element.animateBack('y', -5, 100);
  end
);

Также, добавлен метод animateStop(), останавливающий все воспроизводящиеся в данный момент анимации.

В класс TUiVisualElement добавлено свойство opacity, для обработки которого введено новое сообщение msgParentChangeOpacity, инициирующее смену непрозрачности у всех вложенных элементов.

В класс TUiTextElement добавлена возможность указать цвета текста для определенных состояний элемента: normal, hovered, pressed, focused, disabled — все эти цвета можно задать в файле стилей при помощи специальных свойств. Также, в этот класс добавлено свойство autoWidth, которое позволяет автоматически определять его ширину при задании однострочного текста.

В класс TUiEdit добавлено свойство markerColor, определяющее цвет маркера.

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

  • 9 частей — по краям область элемента заполняется рамкой, а внутри — центральным изображением
  • 3 части — левая, средняя и правая картинки при горизонтальном расположении; также, доступно вертикальное расположение
  • 1 часть — элемент полностью заполняется одним изображением

Для реализации автоматического заполнения области текстурой (с повторениями), в движок Perfect Engine добавлен специальный параметр, задающийся при загрузке текстуры. Таким образом, для вывода рамки в TUiBorderedElement, рисуется только один прямоугольник, заполненный повторяющейся (или растягивающейся) текстурой. Будет текстура повторяться или растягиваться — полностью зависит от параметра, переданного в движок при ее загрузке.

Изображение, выводимое в TUiBorderedElement, может быть тонировано при помощи параметра tone — технически, для этого используется специальный шейдерный фильтр движка:

if (FTone.a <> 0) then
  pe.addFilter('tone', FTone.r, FTone.g, FTone.b, FTone.a);

Добавлен модуль uiComposite для описания стандартных составных элементов. В данный момент этот модуль содержит класс TUiCheckBox, состоящий из кнопки и текста, при нажатии на который, также, переключается состояние компонента.

Структура файлов

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

В дополнение к функции rgba() в стили добавлена новая функция hex(), которая переводит цвет из 16-ричного формата (используемого в Photoshop) в TRGBA.

Также, в процессе работы над этой библиотекой, было внесено много исправлений и изменений в сам графический движок. Некоторые из них описаны выше, а остальные изменения, думаю, удостоятся отдельного сообщения, когда движок будет доработан. Во вложении к этому сообщению я прикреплю тестовую программу, отображающую основные возможности библиотеки UI, а также, изменения, описанные выше. Тестовая программа представлена в двух разрешениях для наглядного отображения ‘резиновости’ библиотеки графического интерфейса.

Тестовая программа (4.3mb)

- 12 -
14 мая 2019

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

Но и по текущему проекту тоже было внесено немало доработок. Я не буду описывать их все, так как сейчас мы готовим крупное обновление для этого блога, в котором будет описана структура вообще всего проекта вместе со схемами — все, что есть на данный момент, и этого уже достаточно. Хотя, на вид все очень просто — квадратик бегает по экрану и стреляет в другой квадратик, который стоит на месте. Казалось бы, работы на день, но, вместе с тем, есть очень широкий функционал, который уже сейчас позволяет быстро реализовать довольно сложные вещи: Р — Расширяемость.

1. Появился наш первый составной элемент управления — консоль (какая-никакая, она еще требует некоторых доработок). Она просто появлялась на экране и исчезала при повторном нажатии клавиши «ё», а мне очень хотелось, чтобы консоль выезжала сверху и уезжала обратно при отключении. Как я это сделал? Реализовал в классе TUiVisualElement метод animate(), который принимает на вход имя свойства, требуемое значение и время, а затем средствами RTTI производит анимацию указанного свойства в течение указанного времени. Притом, анимироваться одновременно может сколько угодно свойств (типа Integer или Single, разумеется). Вот, как теперь производится вызов консоли:

FConsole.animate('y', 0)//Третий параметр - 200ms по умолчанию

Анимируем значение свойства «y» до 0 (изначально оно равно -50% от высоты экрана) — консоль плавно выезжает сверху. Если потребуется, могу предоставить код метода animate(), там его немного. Использовать можно много где; самое первое, что приходит на ум, — позиция всплывающего текста в игре, например, количество нанесенного урона рядом с целью атаки. Если объединить с параметром «Время жизни», который, также, доступен для всех элементов управления, — эта возможность реализуется, можно сказать, «из коробки».

2. Ничего особенного, просто сравните код:

physicCmp := ecs.getComponent('TPhysicComponent', FControlEntity) as TPhysicComponent;//Так было
physicCmp := ecs.getComponent<TPhysicComponent>(FControlEntity);//Так стало

Не понимаю, как сразу не додумались, но теперь эта оплошность исправлена — метод «getComponent()» теперь сам приводит тип, и код получения компонента выглядит намного короче, чем раньше.

- 11 -
6 мая 2019: Перевод статьи о хронологической модели программирования

Занимаясь изучением вопросов реализации сетевой поддержки для игры, а также, сопутствующими темами о лагокомпенсации и оптимизации, мы нашли англоязычную статью авторов Cheryl Savery и T. C. Nicholas Graham, в которой описана инновационная хронологическая модель программирования. По заверениям авторов, такая модель программирования обеспечивает встраивание времени как некоторой основополагающей сущности в сам принцип построения программы (или в данном случае многопользовательской игры), что дает широкие возможности для программирования сетевых взаимодействий между клиентами и значительно упрощает реализацию множества алгоритмов лагокомпенсации.

Данная модель была разработана специально для многопользовательских сетевых игр. В ходе её проектирования авторы реализовали некий фреймворк, так называемый Janus Toolkit, название которого аллегорически представляет возможности программистов получать значение игровых параметров из любого момента времени — настоящего, прошедшего или будущего, подобно персонажу римской мифологии Янусу, видящему прошлое и будущее. Этот фреймворк написан на языке С# с использованием сетевой библиотеки Lidgren.

Мы пока не имеем опыта работы с сетью, а тем более, — с алгоритмами лагокомпенсации, и сейчас только начинаем изучение данной темы. По словам авторов статьи, многие разработчики, использовавшие Janus Toolkit в своих проектах, очень хорошо отозвались о его удобстве для реализации сетевых возможностей в играх. Некоторые принципы лагокомпенсации уже заложены в хронологическую модель изначально, такие как расчет пути (dead reconing) и методы отложенного ввода (delayed input techniques), а некоторые сложные алгоритмы, такие как плавные исправления (smooth corrections) или удаленная задержка (remote lag) реализуются с помощью простых выражений.

Сама статья привлекла меня, в первую очередь, тем, что в ней описаны основные методы лагокомпенсации, применяемые в сетевых играх. И даже не только методы, но и принципы, — как вообще возникают ошибки, связанные с сетевыми задержками и как их можно избежать. Далее, по ходу прочтения и перевода статьи, я углубился в идею хронологической модели программирования, увидел её положительные черты и на показанных в статье примерах оценил сильные стороны этой модели.

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

Исходный текст статьи можно посмотреть здесь. А наш перевод — в прикрепленном файле.

- 10 -
15 апреля 2019: Иллюстрация «Битва у Магистрали»

Нарисовал недавно тематическую картинку (Битва у Магистрали), но нет уверенности в том, что она войдет в игру. Однако, получилось неплохо. Правда, раскрасить пока не вышло — не хочется портить красивый контур посредственной покраской. Научусь лучше рисовать — тогда и раскрашу.

- 9 -
9 апреля 2019: Иллюстрация-заготовка для главного меню

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

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

Что ж, отбросим временно все разногласия — ради фотографии для главного меню можно ненадолго объединить усилия. Хотя, даже приблизительно знающим историю Galaxy Boom, один из них, явно, покажется лишним…

- 8 -
6 апреля 2019

Система UI

При описании системы UI в одном из первых сообщений забыл упомянуть об одной интересной ее особенности. Еще с самого первого класса (TUiElement) элементы интерфейса имеют параметр lifeTime — время жизни, по прошествии которого элемент удаляется со всеми вложенными в него. Это может быть полезно для динамически создаваемых элементов, например, всплывающий текст с количеством нанесенного урона рядом с целью атаки.

В данном обновлении этот параметр отвязан от глобального игрового времени и привязан к системному (во избежание передачи дополнительного параметра «текущее время»).

Менеджер ECS

Система получила новый параметр — active (флаг активности).

События в менеджере событий разделены на Моментальные и Отложенные (выполняемые после всех остальных действий). Ранее были доступны только отложенные события; пока нет четкого представления по их использованию, решение об этом мы примем при тестировании ECS на реальных задачах.

Модуль Input

Один из написанных нами модулей, позволяет отслеживать нажатие заданных клавиш с указанной частотой:

input.addKey('num1', 49, ktOnce);//Одиночное срабатывание клавиши '1'

В данном обновлении модуль стал поддерживать события мыши — mouseMove и mousePressed, необходимые для функционирования системы графического интерфейса.

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

- 7 -
31 марта 2019

Выполнены последние доработки по библиотеке UI:

1. В класс TUiElement добавлен метод iterate() — данный метод проходит по всем вложенным элементам и выполняет переданную в него callBack-функцию. Первый параметр определяет, выполнять ли функцию для корневого элемента. Так теперь реализована стилизация элементов интерфейса:

scene.iterate(
    false,
    procedure(element: TUiElement) begin
        FStyles.stylize(element);
    end
);

2. Реализован алгоритм постстилизации — стилизация всех элементов разом после парсинга разметки. Небольшой задел на будущее: когда библиотека будет поддерживать составные элементы, которые создаются в конструкторе родительского, а не считываются из файла с разметкой — такие элементы тоже будут затронуты в процессе общей стилизации.

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

Однако, появилась и довольно серьезная проблема: постстилизация применяет к элементам стили после завершения парсинга разметки, таким образом, стили, заданные прямо в разметке, имеют меньший приоритет. Для решения этой проблемы набор стилей из разметки добавляется в общий список с уникальным именем:

//Добавление списка стилей в общий набор
FStyles.addStyle('tmp_' + owner.name, TJson.toArray(params));

//Добавление имени стиля к элементу (в самый конец)
if (owner is TUiVisualElement) then
    TUiVisualElement(owner).style.add(',tmp_' + owner.name);

Таким образом, восстановлен изначальный приоритет стилизации.

Остальные доработки по библиотеке UI было решено оставить на потом — прежде всего, это доработки по классу TUiEditElement. Основа библиотеки уже имеется, далее будем дополнять ее только по мере необходимости (в том числе, это зависит и от дизайна UI, который пока отсутствует). В планах на ближайшее будущее — доработка движка (система фильтров постобработки) и завершение внедрения ECS-библиотеки в тестовый проект.

- 6 -
27 марта 2019

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

1. В менеджер UI добавлен метод showPopup(), который ищет элемент с указанным именем в глобальной сцене и отображает его. Пока все всплывающие окна планируется записывать в глобальную сцену и вызывать этим методом. К таким окнам может относиться, например, меню паузы, если оно расположено не на отдельной сцене (под ним отображается игра). Насколько этот подход удобен, определим позже, по опыту его использования.

2. Исправлены проблемы с флагами isVisible и isActive. По логике библиотеки, элементы с отключенным isVisible не обрабатывают и не передают принятые сообщения. Проблема была в том, что список стилей при стилизации не подвергается сортировке, и если среди стилей попадется «isVisible = false», то это свойство может записаться раньше остальных, среди которых присутствуют и те, правильная установка которых требует обработки и передачи сообщений. В результате, установка таких свойств обрабатывалась неправильно. Проблема решена выносом isVisible/isActive за пределы стилей, в глобальную секцию разметки элемента.

3. Добавлен класс TUiEditElement — однострочный элемент с возможностью ввода текста. Движок пока не умеет обрезать выводимые примитивы (glScissor(), если говорить об OpenGL), что представляло самую большую трудность при разработке данного класса. Текст переносится посимвольно, а символы могут иметь разную ширину, причем, это различие может быть довольно ощутимым («i», «W»). В результате, при вводе или стирании текста, у правой границы элемента курсор постоянно прыгал, что выглядело очень некрасиво. Еще одна проблема: вместо исчезнувшего с левой стороны широкого символа, справа могли появиться сразу два-три узких.

Обе проблемы были решены одновременно добавлением понятия «Направление вывода» (FDirection) и соответствующих полей FStart и FFinish, от которых отсчитываются видимые на экране символы. Текст, выводимый слева (от FStart), прижимается к левому краю элемента, а текст, выводимый справа — к правому краю, в результате чего курсор перестает прыгать при вводе символов разной ширины. Правильно регулируя значение свойства FDirection при вводе и стирании текста, удалось добиться наибольшего удобства в использовании элемента, даже несмотря на отсутствие возможности обрезки выводимого текста при разной ширине символов.

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

4. Добавлены классы TUiImageElement и TUiBorderedElement. Первый выводит изображение, а второй — разбивает указанное изображение на 9 частей и выводит их по всей своей площади, что будет полезно, например, при создании всплывающих окон с рамкой. Для правильной работы TUiBorderedElement дописана функция img в стилях — теперь она может принимать параметры — размеры кадра при загрузке спрайта с несколькими кадрами:

image: img(folder/image.png)//Так задается стиль для TUiImageElement
image: img(folder/image.png, 16)//А так - для TUiBorderedElement

Данные классы пока, также, находятся на стадии доработки. В прикрепленном архиве можно посмотреть работу TUiBorderedElement (двойная рамка вокруг всех элементов интерфейса).

5. Добавлена возможность менять курсор при наведении на элементы управления. Для этого в классе TUiInputElement созданы поле FCursor и событие onCursorChange, вызываемое при наведении на элемент указателя мыши. Одноименное событие добавлено в менеджер UI, на которое автоматически устанавливается перенаправление с onCursorChange всех элементов, считанных из разметки. В функции, подписанной на событие менеджера в основной программе, производится смена курсора. В прикрепленном архиве можно увидеть, как меняется указатель мыши при наведении на TUiEditElement.

6. Немного доработан движок: в структуру с описанием изображения (TTexture) добавлены поля frameWidth и frameHeight.

Планы на будущее

В ближайшем будущем планируется доработать класс TUiEditElement и менеджер UI. Также, в данный момент ведется работа над движком: мы стараемся сделать более удобной работу с фильтрами постобработки и искажения кадра. Надеюсь в следующем обновлении написать об этом подробнее и, возможно, уже поделиться примерами работы этих механизмов.

Тестовая программа (1.2mb)

- 5 -
8 марта 2019: Описание ECS-библиотеки

Это последняя из трех моих вводных статей, здесь повествуется о реализации игровой системы. Впервые в своей практике мы столкнулись с ECS-подходом к написанию игр и решили использовать его, так как такой подход обещал нам возможность избежать некоторых структурных проблем при проектировании. Также, говорят, он позволяет обеспечить хорошую расширяемость.

Общие сведения

ECS — Entity-Component-System — шаблон проектирования, который позволяет разделить всю игровую логику на три части: Сущность, Компонент и Система. При проектировании с помощью ECS-подхода в игре перестают существовать объекты в нашем привычном понимании: боевые единицы, здания, декорации, а вместе с тем, уходит на второй план и привычное наследование, дерево классов. Все это заменяется набором сущностей и компонентов, которые обрабатываются системами.

Компонент — это набор данных без возможности управления ими. Для того, чтобы вычленить из общего списка компонентов разных классов определенные «объекты», используются сущности: компоненты с одним ID сущности относятся к одному «объекту». Системы, в свою очередь, занимаются обработкой всех игровых процессов и, по необходимости, имеют доступ к списку компонентов для работы с ними.

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

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

Итак, наша реализация.

В своей реализации мы разбили всю библиотеку на 5 модулей:

  • ecsEntity
  • ecsComponent
  • ecsSystem
  • ecsEvent
  • ecsManager

Сущность

В модуле ecsEntity описан класс-прототип всех игровых сущностей TEcsEntity, у которого всего одно свойство — id и один метод — конструктор. Все классы игровых сущностей будут наследоваться от этого класса и в собственном уникальном методе конструктора создавать весь необходимый набор компонентов. Это сделано для того, чтобы собрать код создания компонентов для разного типа объектов в модулях с описанием этих типов. Вот, к примеру, код конструктора для сущности «figureEntity» (прототип боевой единицы в нашей игре):

constructor TFigureEntity.create(x, y: Single; color: Cardinal; control: Boolean = false);
var
    transformComponent: TTransformComponent;
    renderComponent: TRenderComponent;
    damageComponent: TDamageComponent;
    moveComponent: TMoveComponent;
    physicComponent: TPhysicComponent;
    controlComponent: TControlComponent;
begin
    transformComponent := TTransformComponent.create(ecs.curEntity);
    transformComponent.x := x;
    transformComponent.y := y;
    transformComponent.width := 50;
    transformComponent.height := 50;
    ecs.addComponent(transformComponent);

    renderComponent := TRenderComponent.create(ecs.curEntity);
    renderComponent.color := color;
    ecs.addComponent(renderComponent);

    damageComponent := TDamageComponent.create(ecs.curEntity);
    ecs.addComponent(damageComponent);

    moveComponent := TMoveComponent.create(ecs.curEntity);
    moveComponent.speed := 5;
    ecs.addComponent(moveComponent);

    ...

    if (control) then begin
        controlComponent := TControlComponent.create(ecs.curEntity);
        ecs.addComponent(controlComponent);
    end;
end;

Мы видим, что в конструктор TFigureEntity передается набор параметров, уникальный для этого типа объектов. Причем, есть статичные параметры, задающиеся прямо в коде, а есть динамические — передающиеся в конструктор извне.

Также, в модуле ecsEntity описан менеджер сущностей, занимающийся хранением и обработкой списка сущностей: добавление, удаление и т.д.

Компонент

В модуле ecsComponent, также, описаны базовый класс компонента и менеджер компонентов. Базовый класс TEcsComponent содержит только одно свойство «entity» — id сущности, к которой привязан данный компонент. Менеджер представляет собой особый интерес: помимо прочих, он имеет методы получения компонента по типу и ID сущности и списка всех компонентов определенного типа (для обхода системой):

function getComponent(cmpType: String; entity: Integer): TEcsComponent;
function getComponents(cmpType: String): TDictionary<Integer, TEcsComponent>;

Сам список компонентов представлен в виде сложной структуры, основанной на стандартном классе TDictionary (хэш-таблица):

TComponentsList = TDictionary<String, TDictionary<Integer, TEcsComponent>>;

Таблица в таблице. Здесь ключ в первой таблице — имя типа компонентов (например, TRenderComponent), а значение — список компонентов этого типа, где, в свою очередь, ключами выступают ID сущностей, к которым они привязаны.

Системы и события

Как и в предыдущих двух модулях, в ecsSystem описаны классы системы и менеджера систем. Причем, первый из них имеет только виртуальную процедуру update(), но он, также, является наследником класса TEcsListener. Что это за класс?

Его можно найти в модуле ecsEvent, который содержит всего три класса: TEcsListener, TEcsEvent и TEcsEventManager. Любой наследник класса TEcsListener может быть «слушателем» события — таким образом, события рассылаются менеджером всем слушателям, что подразумевает возможность наличия нескольких слушателей для одного события.

Метод addListener(eventType, listener) добавляет слушателя в список для конкретного типа события (обычно вызывается при старте программы). Каждый раз при возникновении нового события (например gameStartEvent — начало игры) вызывается метод addEvent(event), куда передается объект — наследник TEcsEvent со своим набором параметров (уникальным для каждого типа события).

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

Общий менеджер

Для упрощения работы с системой реализован класс общего менеджера TEcsManager со статичным методом инициализации, создающим глобальный объект «ecs», к которому можно обратиться из любого модуля библиотеки. Этот класс является оберткой над остальными модулями: хранит в себе объекты вышеописанных менеджеров и реализует удобное обращение к ним посредством специальных методов.

Вот, пожалуй, и все. Наверняка, в этой реализации найдется достаточно подводных камней, так как это наш первый опыт написания подобной системы, и мы пока не опробовали ее на практике. Если такое случится — будем делиться информацией о возникших проблемах и о способах их решения в этом блоге. Примера здесь не будет, так как пример к данному сообщению — и есть сама мини-игра — прототип Galaxy Boom mini, проект, собранный из модулей, описанных в этой и предыдущих статьях. Обо всех новостях я постараюсь сообщать в этом блоге.

- 4 -
5 марта 2019: Описание системы графического интерфейса

Библиотека пользовательского интерфейса состоит из двух модулей: uiElement (реализация классов-прототипов будущих элементов интерфейса) и uiManager (менеджер интерфейса). Данная библиотека является дополнением к движку Perfect Engine 3.

uiElement

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

TUiMessage = (
    msgUpdate,
    msgDraw,
    msgParentChangePos,
    msgParentChangeWidth,
    msgParentChangeHeight,
    msgMouseMove,
    msgMouseDown,
    msgMouseUp,
    msgKeyDown,
    msgKeyUp
);

Система интерфейса задумана как «резиновая», координаты и размеры ее элементов могут задаваться либо в пикселях, либо, в процентах. Эта возможность реализована в корневом классе TUiElement, отвечающем за расчет координат и размеров элементов. В конструктор класса передаются специальные параметры: isPrcX, isPrcY, isPrcWidth, isPrcHeight, показывающие единицы измерения, в которых задаются данные параметры элемента.

Свойства x, y, width, height предназначены для получения и установки одноименных параметров в заданных единицах измерения. Для получения пиксельных вариантов (если параметры задаются в процентах) существуют свойства pxX, pxY, pxWidth и pxHeight. Более того, для вывода элементов и взаимодействия с ними, нужно иметь информацию о реальных координатах элемента на экране. Эта информация кэшируется при каждом изменении позиции и размеров в дереве элементов; получить ее можно при помощи свойств scrX и scrY.

Также, присутствуют свойства offsetX, offsetY и origin: первые два задают пиксельное отклонение от текущей позиции, а последнее — смещает точку вывода элемента следующим образом:

Если требуется вывести элемент у правой границы родителя с отступом сверху и справа в 10 пикселей, то такой набор свойств поможет это сделать:

isPrcX := true
x := 100
origin := 1
offsetX := -10
offsetY := 10

Класс TUiInputElement реализует базовое взаимодействие с мышью и клавиатурой, а также, предоставляет потомкам доступ к полям FIsHovered, FIsPressed и FIsFocused. Основываясь на этих данных, можно удобно реализовать смену отображения при наведении мыши, нажатии или при получении элементом фокуса.

Ко всему прочему, данный класс имеет поле FLink, указывающее имя сцены для автопереключения сцен, реализованного в менеджере.

Класс TUiVisualElement является общим предком для всех визуальных элементов. Среди его свойств — isUseCamera, определяющее, использовать ли камеру движка при выводе элемента и isVisible. Невидимые элементы, как и неактивные, не обрабатывают и не распространяют сообщений по вложенным элементам.

Класс TUiTextElement реализует базовый функционал вывода текста, его свойства очень похожи на аналогичные css-свойства в веб-разработке:

padding: TUiRect;//Отступ текста от границ элемента
xPadding, yPadding: Single;//Сумма отступов по одной из осей
fontName: String;//Имя шрифта
fontSize: Integer;//Размер шрифта
fontColor: TRGBA;//Цвет шрифта
textAlign: Byte;//Выравнивание текста
lineHeight: Single;//Междустрочный интервал
autoHeight: Boolean;//Используется ли автовычисление высоты
formatted: Boolean;//Используется ли форматированный текст

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

И, наконец, класс TUiScene наследуется от TUiInputElement и реализует сцену — корневой элемент дерева.

uiManager

Данный модуль является оберткой над uiElement и реализует работу со сценами, а также, загрузку интерфейса из json-файла и его стилизацию при помощи файла стилей. Вот набор файлов, с которыми работает менеджер:

  • markup.json — файл с разметкой и базовыми стилями
  • styles.css — файл со стилями
  • const.ini — файл с текстовыми константами

Файл с разметкой содержит в себе дерево объектов, представленное в формате json, где каждый объект имеет следующие поля:

  • class — класс элемента для автоматического создания через rtti
  • style — стиль элемента из таблицы стилей
  • scene — имя сцены-родителя, только для корневых элементов
  • isPrcX, isPrcY, isPrcWidth, isPrcHeight — описано выше, необязательные параметры
  • params — объект с перечислением свойств элемента и их значений
  • elements — объект с перечислением вложенных элементов такой же структуры

Файл стилей имеет следующую структуру:

.имя стиля
    свойство = функция(значение)
    свойство = значение
    ...

При стилизации параметры, указанные в разделе «params» json-файла, перезаписывают одноименные параметры из таблицы стилей, если таковые имеются. Сам процесс стилизации реализован за счет методов встроенной библиотеки rtti (автоматически), благодаря чему, при появлении новых свойств в дочерних классах, не требуется доработки функции записи. Свойства могут быть записаны как напрямую, так и с использованием специальной функции. На данный момент доступны следующие специальные функции:

  • rgba(r, g, b, a) — устанавливает цвет из четырехкомпонентного параметра
  • rect(left, right, top, bottom) — устанавливает свойство типа TUiRect
  • img(path) — вызывает функцию движка для загрузки изображения и записывает в свойство полученный индекс текстуры
  • font(name) — вызывает функцию движка для загрузки шрифта и записывает в свойство его имя
  • const(name) — загружает текстовую константу по имени

Вот пример работы функции:

procedure TUiStyles.setRgba(obj: TUiElement; prop: TRttiProperty; params: String);
var
    rgba: TRGBA;
    value: TValue;
    arr: TArray<String>;
begin
    arr := params.explode(',');
    rgba := TRGBA.make(
        arr[0].float(),
        arr[1].float(),
        arr[2].float(),
        arr[3].float()
    );

    TValue.make(@rgba, typeInfo(TRGBA), value);
    prop.setValue(obj, value);
end;

Также, менеджер имеет следующие публичные методы:

  • getElement(sceneName, elementName) — получение элемента
  • getConst(name) — получение текстовой константы
  • sendMessage(…) — отправка сообщения текущей и глобальной сценам
  • load(constFile, stylesFile, markupFile) — загрузка интерфейса из файлов
  • selectScene(name) — выбор сцены (создание новой при отсутствии)

Создание сцен происходит автоматически при парсинге разметки. Помимо общего набора сцен, в менеджере присутствует глобальная сцена (main), элементы которой видны всегда. В игре такая сцена может содержать, например, окно консоли, которое может быть вызвано в любой момент (как в самой игре, так и в меню). При работе со сценами, пользователю доступны специальные события: onSceneShow и onSceneHide, где могут выполняться загрузка и освобождение игровых ресурсов, а также, другие необходимые действия.

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

Планы на будущее

На ближайшее будущее планируется реализация класса TUiEditElement с базовым функционалом для элементов с возможностью редактирования текста — на нем в дальнейшем будет основан элемент TEdit. Также, подумаем над возможностью подключения нескольких стилей к одному элементу.

В представленной ниже тестовой программе на примере класса TUiButton, унаследованного от TUiTextElement, реализованы основные функции библиотеки, вот их примерный список:

  • Автоопределение высоты текстового элемента
  • Определение позиции и размеров в пикселях и процентах
  • Свойства origin и offset (элемент «приклеен» к правому краю родителя с отступом)
  • Форматированный текст
  • Использование специальных функций (например, загрузка изображения)
  • Автопереключение сцен
  • Глобальная сцена (элемент виден на всех остальных сценах)

Тестовая программа (1.2mb)

- 3 -
4 марта 2019: Краткое описание Perfect Engine 3

В третьей версии движок Perfect Engine и рендер для него были полностью переписаны, код проекта перенесен на Delphi XE и использует новые возможности языка. В данный момент доступен только OpenGL-рендер, его функциональность, как и функциональность самого движка, сильно расширена. Сейчас Perfect Engine 3 находится на стадии тестирования и доработки

Изменения коснулись не только основных файлов движка, но и дополнительных модулей и объектов. В частности, доработан объект камеры, упразднен тип TAlphaColor, добавлены типы TRect и TRGBA и многое другое. Начнем по порядку.

Камера

Самое незаметное изменение здесь — в методе «shake» (тряска): теперь камера плавно перемещается в выбранную точку при раскачивании (вместо резкого перемещения в PE2). Также, появились новые удобные свойства — slideInfo и shakeInfo с информацией о текущем состоянии перемещения (скольжения) и раскачивания, что дает возможности, к примеру, применять размытие кадра в движении при быстром перемещении камеры:

TCameraSlide = record
    targX, targY: Single;//Положение цели
    speed: Integer;//Скорость (в процентах, от 0 до 100)
end;

TCameraShake = record
    shiftX, shiftY: Single;//Максимальное смещение
    curShiftX, curShiftY: Single;//Текущее смещение
    steps: Integer;//Общее количество раскачиваний
    curStep: Integer;//Текущий шаг раскачивания
    strength: Integer;//Сила раскачивания
    isShaking: Boolean;//Качается ли камера в данный момент?
end;

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

move(x, y: Single);//Моментально переместить в указанную позицию
slide(x, y: Single);//Плавно переместить в указанную позицию
shake(strength, steps: Integer);//Трясти камеру

При помощи свойства «isActive» можно отключить камеру для последующих выводимых объектов (например, графический интерфейс). Метод «apply» используется движком для преобразования координат всех выводимых объектов с учетом координат камеры и ее смещения при раскачивании

Рендер

OpenGL-рендер, используемый в движке, теперь работает намного быстрее и имеет огромное количество новых возможностей. Вместо старых glBegin/glEnd здесь используются буферные объекты (VBO), есть возможность рендера в текстуру и смены буфера вывода, присутствуют шейдеры и шейдерные эффекты (цветокоррекция, размытие, размытие в движении, резкость, …). Вот список всех фильтров, доступных на данный момент:

  • Тонирование изображения (tone). Цветные участки изображения окрашиваются в указанный цвет.
  • Смена цвета (recolor). Красный цвет на изображении перекрашивается в указанный.
  • Цветокоррекция (correction). Коррекция цвета с учетом параметров смещения компонентов цвета в системе HSV (цветовой тон, насыщенность, яркость).
  • Заливка (fill). Полная заливка текстуры указанным цветом с сохранением прозрачных участков и указанием степени заливки.
  • Размытие (blur, motionBlur, fullBlur). Различные варианты размытия.
  • Резкость (sharp).
  • Рассеянное свечение (bloom).
  • Наложение узора (scanline). По умолчанию это эффект чересстрочной развертки (горизонтальные линии), но можно установить любое изображение, заменив его в папке с шейдерами (например, шум).
  • Искажение (displace). Искажение финального кадра в соответствии с картой искажений (например, для имитации взрывов).

В данный момент система фильтров дорабатывается для более удобного использования. Не лишним будет сказать, что теперь объект рендера наследуется от специального абстрактного класса TAbstractRender; этим решением избегается необходимость в куче callback-функций движка, назначаемых при инициализации рендера, как это было в PE2.

Движок

Сам движок тоже претерпел значительные изменения. В функцию инициализации теперь передается объект рендера

TPerfectEngine.init(handle, clientWidth, clientHeight, TOpenGlRender.create());

Perfect Engine, по-прежнему, может выводить как одиночные изображения, так и кадры анимации (спрайта), причем:

  • Процесс разбиения на кадры происходит при загрузке изображения, а не перед каждым выводом, как было раньше.
  • Теперь поддерживаются прямоугольные кадры (PE2 работал только с квадратными).

Что касается шрифта — он, как и прежде, может либо браться из системы, либо, загружаться из .ttf-файла в папке с программой: рендер сгенерирует в памяти спрайт с символами шрифта, который будет использоваться при выводе текста, подстраиваясь под нужные размеры с помощью технологии Mipmap. Помимо стандартных методов вывода (textOut) и подсчета ширины строки (getTextWidth), теперь появились методы для работы с форматированными строками: textOutF() и getTextWidthF():

pe.textOutF('Perfect [#ffff00ff]Engine[#]');//Здесь слово «Engine» будет покрашено в желтый цвет

Также, движок теперь умеет считать FPS — информацию о количестве кадров в секунду можно получить, обратившись к специальному одноименному свойству. Работа с Perfect Engine, по-прежнему, проста и удобна:

const
    MAP: TArray<TArray<Byte>> = [
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [1, 2, 2, 2, 3, 9, 9, 1, 2, 2],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 10, 8, 8, 8, 8, 10, 0],
        [0, 0, 0, 4, 12, 12, 12, 12, 4, 0],
        [0, 0, 0, 5, 12, 12, 12, 12, 5, 0],
        [0, 0, 0, 5, 0, 13, 13, 0, 5, 0],
        [0, 0, 0, 6, 0, 0, 0, 0, 6, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    ];

...

procedure TFrmMain.draw();
var
    i, j: Integer;
begin
    for i := 0 to high(MAP) do begin
        for j := 0 to high(MAP[i]) do
            pe.drawFrame(land, MAP[j][i], i * 64, j * 64, 64, 64);
    end;
end;

procedure TfrmMain.formCreate(sender: TObject);
begin
    TPerfectEngine.init(handle, clientWidth, clientHeight, TOpenGlRender.create());
    pe.onDraw := draw;
    land := pe.loadTexture(appPath + '/images/land.png', 64, 64);
end;

procedure TfrmMain.FormDestroy(Sender: TObject);
begin
    TPerfectEngine.remove();
end;

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

По ссылке ниже — тестовая программа (без исходника), демонстрирующая некоторые возможности движка.

Тестовая программа (1.92mb)

- 2 -
3 марта 2019: Небольшое вступление

Уже 3 года прошло с момента появления на свет GBM и почти 5 лет нашему графическому движку, но время не было потрачено впустую: все эти годы ушли на подготовку и получение знаний, необходимых для разработки проекта такой сложности. А самому миру Galaxy Boom и всем его основным персонажам уже около 10 лет, но это уже совсем другая история.

Итак, пара слов о том, что планируется в итоге. Сетевая 2D-игра (возможно, с сюжетной кампанией) с роботами, бомбами и интересными, на наш взгляд, геймплейными решениями. Технически: разрабатывается на Delphi XE; сетевая часть, скорее всего, будет написана с использованием библиотеки Synapse и TCP-протокола; сама игра будет реализована с использованием нового для нас ECS (Entity Component System)-шаблона проектирования. Вот некоторые из модулей, включенных в проект, над которыми в данный момент ведется работа

  • Графический движок Perfect Engine 3 (новая версия).
  • Система «резинового» графического интерфейса.
  • Менеджер ECS.

Теперь вкратце о каждом из них.

Perfect Engine 3: с момента разработки второй версии движка многое изменилось. Теперь здесь есть полноценный OpenGL-рендер с буферными объектами (против GLBegin/GLEnd в предыдущей версии), рендером в текстуру и шейдерными эффектами. Сам движок стал более структурным, а место кучи callback-функций в нем занял абстрактный рендер со своим набором свойств и методов, от которого и наследуются остальные рендеры.

Система UI: в данный момент, как раз, ведется ее разработка. Это модуль с классами — прототипами будущих элементов графического интерфейса и модуль менеджера UI с управлением сценами, а также, возможностью загрузки интерфейса из json-файла и его стилизации при помощи файла стилей. Для автоматической стилизации компонентов используются методы встроенной библиотеки RTTI

Менеджер ECS: набор классов, реализующий взаимодействие между Сущностями, Компонентами и Системами и удобную работу с ними посредством Менеджера событий

Игра пока находится на самой ранней стадии; разработка продвигается медленно, но верно. Ранее мы не сталкивались ни с сетевыми играми, ни с ECS-шаблоном, и таких крупных проектов до сего момента на себя не брали. В финале первого этапа разработки должна получиться тестовая сетевая игра без графики и геймплея будущей игры, которая даст нам уверенность в правильности выбранных нами инструментов и знания относительно программирования сетевых игр. Уже в рамках разработки этой простой игры мы столкнулись с необходимостью написания ECS и UI-менеджеров, а также, других незначительных модулей. В дальнейшем, за счет правильно построенной структуры, планируется довести этот сырой продукт до полноценной, запланированной нами изначально, игры

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

- 1 -