Все заметки

Рефакторинг жизненного цикла сцен

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

Какие были проблемы

Системы были привязаны к сценам и уничтожались при их смене. Это не позволяло пошарить состояние между разными экранами игры и за неимением других возможностей я складывал данные в window или local storage. К примеру, в новогоднем мини-квесте я так хранил инвентарь с предметами.

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

Короче говоря, интерфейс был громоздким, непонятным, а возможностей недостаточно.

Что сделал

Чтобы решить все проблемы разом я:

  • Вытащил системы на общий уровень
  • Списки акторов вернул обратно в сцены
  • Уровни удалил

Системы разделил на два типа: WorldSystem и SceneSystem. Первые являются синглтонами, которые сохраняют состояние при переходах между сценами, а вторые инстанцируются под каждую сцену отдельно.

Как следить за изменениями

Для отслеживания переходов между сценами я добавил методы:

  onSceneLoad(scene: Scene): Promise<void> // загрузка ресурсов: текстуры, звуки и тд.
  onSceneEnter(scene: Scene): void // активация сцены
  onSceneExit(scene: Scene): void // выход со сцены, без уничтожения
  onSceneDestroy(scene: Scene): void // сцена полностью уничтожается

Для World систем есть еще пара методов:

  onWorldLoad(world: World): Promise<void> // загрузка глобальных ресурсов. К примеру, бандл с интерфейсом игры.
  onWorldReady(world: World): void // глобальные ресурсы загружены
  onWorldDestroy(world: World): void // полное завершение игры

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

Как управлять переходами

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

type LoadSceneEvent = WorldEvent<{
  id: string;
  autoEnter?: boolean;
  autoDestroy?: boolean;
}>;
type EnterSceneEvent = WorldEvent<{
  id: string;
  autoDestroy?: boolean;
}>;
type ExitSceneEvent = WorldEvent<{
  autoDestroy?: boolean;
}>;
type DestroySceneEvent = WorldEvent<{ id: string }>;

Флаги autoEnter и autoDestroy для автоматического перехода или уничтожения сцен. По умолчанию, оба в true. Если выключить autoEnter, то сцена загрузится, но перехода не произойдет. Полезно если потребуется сцены предзагружать заранее.

Зачем я это делал

Проектирование API библиотеки для меня – это одна из самых сложных задач. При написании бизнес логики, главное чтобы работало так как хочет заказчик, а на удобство использования кода можно хоть и не всегда, но закрыть глаза. Ну не будет никто эту функцию использовать кроме тебя, зато работает. При разработке библиотеки, ее API – это и есть продукт, а заказчики – это те, кто будут им пользоваться. Или не будут если оно будет сложным и неудобным.