Manage game states/screens with transitions and overlay support:
import type { ScreenDefinition } from 'ecspresso';
type Screens = {
menu: ScreenDefinition<
Record<string, never>, // Config (passed when entering)
{ selectedOption: number } // State (mutable during screen)
>;
gameplay: ScreenDefinition<
{ difficulty: string; level: number },
{ score: number; isPaused: boolean }
>;
pause: ScreenDefinition<Record<string, never>, Record<string, never>>;
};
const game = ECSpresso.create()
.withComponentTypes<Components>()
.withEventTypes<Events>()
.withResourceTypes<Resources>()
.withScreens(screens => screens
.add('menu', {
initialState: () => ({ selectedOption: 0 }),
onEnter: () => console.log('Entered menu'),
onExit: () => console.log('Left menu'),
})
.add('gameplay', {
initialState: () => ({ score: 0, isPaused: false }),
onEnter: (config) => console.log(`Starting level ${config.level}`),
onExit: () => console.log('Gameplay ended'),
requiredAssetGroups: ['level1'],
})
.add('pause', {
initialState: () => ({}),
})
)
.build();
await game.initialize();
await game.setScreen('menu', {}); // Set initial screen
await game.setScreen('gameplay', { difficulty: 'hard', level: 1 }); // Transition
await game.pushScreen('pause', {}); // Push overlay
await game.popScreen(); // Pop overlay
const current = game.getCurrentScreen(); // 'gameplay'
const config = game.getScreenConfig(); // { difficulty: 'hard', level: 1 }
const state = game.getScreenState(); // { score: 0, isPaused: false }
game.updateScreenState({ score: 100 });
Use ScreenConfiguratorFn when a .withScreens(...) callback is extracted into a named helper:
import type { ScreenConfiguratorFn, ScreenDefinition } from 'ecspresso';
type AppScreens = {
playing: ScreenDefinition<{ level: number }>;
pause: ScreenDefinition;
};
export const configureScreens: ScreenConfiguratorFn<AppScreens> = function configureScreens(screens) {
return screens
.add('playing', { initialState: config => config })
.add('pause', { initialState: () => ({}) });
};
const game = ECSpresso.create()
.withScreens(configureScreens)
.build();
Subscribe to a specific screen entering or exiting without writing inline screenEnter / screenExit event guards. Multiple handlers can be registered for the same screen and fire in registration order. Each returns a disposer.
const offEnter = game.onScreenEnter('gameplay', ({ config, ecs }) => {
// Fires on setScreen('gameplay', ...) and pushScreen('gameplay', ...)
console.log(`Starting level ${config.level}`);
ecs.spawn({ player: true });
});
const offExit = game.onScreenExit('gameplay', ({ ecs }) => {
// Fires when leaving 'gameplay' via setScreen away or popScreen
console.log('Gameplay ended');
});
// Later, if needed:
offEnter();
offExit();
Pass { scope: screenName } to spawn or spawnChild to have the entity automatically removed when that screen exits. This replaces hand-maintained per-component teardown lists.
await game.setScreen('gameplay', { level: 1 });
// Removed automatically when 'gameplay' exits
game.spawn({ enemy: { hp: 10 } }, { scope: 'gameplay' });
game.spawnChild(parentId, { projectile: { speed: 5 } }, { scope: 'gameplay' });
The screen name is type-checked against your declared screens.
game.addSystem('menuUI')
.inScreens(['menu']) // Only runs in 'menu'
.setProcess(({ ecs }) => {
renderMenu(ecs.getScreenState().selectedOption);
});
game.addSystem('animations')
.excludeScreens(['pause']) // Runs in all screens except 'pause'
.setProcess(() => { /* ... */ });
Access screen state through the $screen resource:
game.addSystem('ui')
.setProcess(({ ecs }) => {
const screen = ecs.getResource('$screen');
screen.current; // Current screen name
screen.config; // Current screen config
screen.state; // Current screen state (mutable)
screen.isOverlay; // true if screen was pushed
screen.stackDepth; // Number of screens in stack
screen.isCurrent('gameplay'); // Check current screen
screen.isActive('menu'); // true if in current or stack
});