Все заметки

Убираю бойлерплейт в движке

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

К примеру, для того чтобы добавить очки здоровья для игрового персонажа, нужно создать компонент 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”, ввести имя и получить готовый шаблон скрипта в коде проекта, который будет автоматически подключен и в игре и в редакторе, чтобы немедленно начать его использовать.

Такого поведения вполне можно добиться, но нужно решить несколько задач:

  1. Объединить описание класса и его схему. Напрямую проблему не решает, но сильно ускорит внесение дальнейших изменений
  2. Научить редактор и движок самостоятельно сканировать проект в поисках систем и компонентов
  3. По нажатию на кнопку в редакторе, генерировать скрипт из шаблона и складывать в нужную папку

Для решения первой проблемы я использовал 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 конфига

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

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