Рефакторинг жизненного цикла сцен
Последний месяц потратил на переписывание жизненного цикла сцен и систем в движке. Давно хотел это сделать, убрать лишние сущности и сделать интерфейс более понятным.
Какие были проблемы
Системы были привязаны к сценам и уничтожались при их смене. Это не позволяло пошарить состояние между разными экранами игры и за неимением других возможностей я складывал данные в 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 – это и есть продукт, а заказчики – это те, кто будут им пользоваться. Или не будут если оно будет сложным и неудобным.