Убираю бойлерплейт в движке
Очередная крупная задача, которая долго висела в бэклоге – сократить количество бойлерплейта для создания новых компонентов, систем и других сущностей, которые добавляются и в коде и в редакторе.
К примеру, для того чтобы добавить очки здоровья для игрового персонажа, нужно создать компонент Health с числовым полем для храненя текущего уровня здоровья:
class Health {
points: number;
constructor(config) {
this.points = config.point;
}
}
Все компоненты добавляются в акторы через редактор, поэтому нужно чтобы редактор как-то узнал про новый компонент. Для этого нужно в отдельном скрипте описать схему компонента для генерации виджета с необходимыми полями ввода:
const health = {
title: 'Health,
fields: [
{
name: 'points',
title: 'Points',
type: 'number',
},
],
getInitialState: () => ({
points: 100,
}),
};
Далее, чтобы игра узнала о новом компоненте, его нужно пробросить в конструктор класса Engine, который представляет собой инстанс игры, а схему нужно экспортировать через отдельную точку входа, которая используется для сбора информации о проекте при старте редактора.
Наконец, после описанных действий, компонент можно добавить в актор через интерфейс редактора, а в коде систем и скриптов обращаться к нему для реализации боевой системы или любой другой логики.
Все это ужасно утомляет, особенно во время гейм джемов когда код нужно писать быстро.
Вдохновляясь существующими движками и редакторами, я начал фантазировать, что было бы здорово для добавления нового компонента, прямо в редакторе нажать на кнопку “Create New”, ввести имя и получить готовый шаблон скрипта в коде проекта, который будет автоматически подключен и в игре и в редакторе, чтобы немедленно начать его использовать.
Такого поведения вполне можно добиться, но нужно решить несколько задач:
- Объединить описание класса и его схему. Напрямую проблему не решает, но сильно ускорит внесение дальнейших изменений
- Научить редактор и движок самостоятельно сканировать проект в поисках систем и компонентов
- По нажатию на кнопку в редакторе, генерировать скрипт из шаблона и складывать в нужную папку
Для решения первой проблемы я использовал TypeScript декораторы. Декоратор класса позволяет зарегистрировать класс в хранилище к которому обращается редактор для рисования виджетов, а декораторы полей помогают упростить описание схемы виджета, так как имя и тип данных (если он примитивный) можно взять из самого поля и не дублировать эту информацию. В самом простом случае это выглядит следующим образом:
@DefineComponent({
name: 'Health',
})
class Health {
@DefineField({ initialValue: 100 })
points: number;
constructor(config) {
this.points = config.points;
}
}
Переходим к автоматическому подключению. Для сборки информации о проекте под капотом редактора я использую webpack dev сервер, но теперь вместо статической точки входа, я сканирую определенные каталоги проекта по glob паттерну и выгребаю все что есть. Webpack позволяет указать несколько точек входа и собрать их в один бандл, который затем я подключаю в редакторе.
const widgetEntries = fastGlob.globSync(
[
`${config.systemsDir}/**/*.ts`,
`${config.componentsDir}/**/*.ts`,
`${config.behaviorsDir}/**/*.ts`
],
{ absolute: true }
);
// далее передаем widgetEntries в entry поле webpack конфига
Благодаря декораторам даже импортировать ничего из этого бандла не нужно, достаточно того, что при подключении выполнится код объявления классов, а с этим и декорирующих функций, которые автоматически зарегистрируют классы.
На текущий момент я нахожусь на втором шаге релизации задачи. Схему и классы объединил, редактор автоматически искать описание виджетов научил, но теперь нужно поработать над тем как все это использовать в коде игры. Вместе с декораторами при сборки игры в бандл полезло много всего лишнего и игра даже перестала запускаться. Так что работы еще много, но уже есть определенные успехи, которыми можно поделиться.