Все заметки

Автоподключение скриптов в редакторе

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

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

К счастью, подобный механизм можно реализовать на уровне исходного кода игры, а не движка. К примеру, если игра собирается через Webpack, то можно воспользоваться функцией require.context, которая позволяет по регулярке импортировать все нужные модули и затем передать все что удалось найти в конструктор класса Engine. В Vite для тех же целей есть import.meta.glob.

function importAll(context) {
  const modules = context.keys().map(context);

  // ожидаем что система/компонент доступны через дефолтный экспорт в скрипте
  return modules.map((module) => module.default);
}

const components = importAll(require.context('./', true, /.component.ts$/));
const systems = importAll(require.context('./', true, /.system.ts$/));

const engine = new Engine({
  components,
  systems
});

engine.play();

Помимо этого, пользуясь советом из комментариев к предыдущему посту, я доработал редактор и заменил использование нескольких точек входа и поиск по glob паттерну на виртуальный модуль с require.context внутри. Для этого я использовал плагин, который позволяет создать виртуальный модуль из динамически сгенерированной строки с кодом.

// в упрощенном виде выглядит примерно так:
const virtualModules = new VirtualModulesPlugin({
  './virtual-entry.ts': () => `
  function importAll(r) {
    r.keys().forEach(r);
  }

  importAll(
    require.context('./', true, /\.system\.ts$/),
  );
  importAll(
    require.context('./', true, /\.component\.ts$/),
  );
  `
});

webpack({
  entry: {
    extension: './virtual-entry.ts'
  },

  output: {
    libraryTarget: 'umd',
    library: '[name]'
  },

  plugins: [virtualModules]
});

Код проекта все еще собирается отдельным скриптом и подключается в редактор как UMD модуль (после загрузки и исполнения скрипта его экспорт можно достать из window). Так сделано, потому что при запуске редактора, Webpack собирает только код игры, а клиентский код редактора бандлится заранее.

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

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