Left
Right
Shoot
Restart (after death)
const renderLayers = {
background: 1 << 0,
foreground: 1 << 1,
};
// A fraction of the camera's vertical world units, so the shake reads as
// the same proportional jolt regardless of resolution/aspect ratio.
const explosionShakeIntensity = DEMO_VERTICAL_WORLD_UNITS * 0.007;
const explosionShakeDurationSeconds = 0.15;
export const bloomDefaults: BloomEcsComponent = {
threshold: 0.95,
passes: 6,
intensity: 3,
};
export const blurDefaults: GaussianBlurEcsComponent = {
passes: 6,
intensity: 0.6,
};
export const createSpaceShooterGame = async (
onBloomReady?: (bloom: BloomEcsComponent) => void,
onBlurReady?: (blur: GaussianBlurEcsComponent) => void,
): Promise<Game> => {
const { game, world, renderContext, time } = createGame('demo-game');
// Background and foreground each get their own off-screen target, so the
// blur post-process pass can affect the background only: the present
// pass then layers the sharp foreground back on top of the blurred
// background when it draws both to the canvas.
const backgroundRenderTarget = createRenderTarget(
renderContext.gl,
renderContext.width,
renderContext.height,
);
// HDR so the bullet's emissive map (see _create-player.ts) can bloom
// based on true brightness rather than an 8-bit white ceiling;
// addToneMappingComponent compresses it back to displayable range before the
// present pass draws it.
const foregroundRenderTarget = createRenderTarget(
renderContext.gl,
renderContext.width,
renderContext.height,
RENDER_TARGET_FORMAT.hdr,
);
// The background sits on its own static camera so it doesn't shake along
// with the foreground when an explosion happens.
const backgroundCameraEntity = createCamera(world, {
cullingMask: renderLayers.background,
isStatic: true,
renderTarget: backgroundRenderTarget,
verticalWorldUnits: DEMO_VERTICAL_WORLD_UNITS,
});
const foregroundCameraEntity = createCamera(world, {
cullingMask: renderLayers.foreground,
renderTarget: foregroundRenderTarget,
verticalWorldUnits: DEMO_VERTICAL_WORLD_UNITS,
});
// The component is handed back to the caller (see the sliders on this
// demo's page) so it can be retuned live.
const blurComponent = addGaussianBlurComponent(
world,
backgroundCameraEntity,
blurDefaults,
);
onBlurReady?.(blurComponent);
// Bullets and explosion flashes are the brightest things in the
// foreground, so a bloom glow makes them read as glowing/energetic
// instead of flat sprites. The component is handed back to the caller
// (see the sliders on this demo's page) so it can be retuned live.
const bloomComponent = addBloomComponent(world, foregroundCameraEntity, bloomDefaults);
onBloomReady?.(bloomComponent);
addToneMappingComponent(world, foregroundCameraEntity);
world.addComponent(foregroundCameraEntity, cameraShakeId, {
intensity: 0,
durationSeconds: 0,
elapsedSeconds: 0,
currentOffset: Vector2.zero,
nextOffsetChangeSeconds: 0,
});
const triggerCameraShake = (): void => {
const shakeComponent = world.getComponent<CameraShakeEcsComponent>(
foregroundCameraEntity,
cameraShakeId,
);
if (!shakeComponent) {
return;
}
shakeComponent.intensity = explosionShakeIntensity;
shakeComponent.durationSeconds = explosionShakeDurationSeconds;
shakeComponent.elapsedSeconds = 0;
shakeComponent.nextOffsetChangeSeconds = 0;
};
const { moveInput, shootInput, restartInput } = createInputs(
world,
time,
game,
);
await createBackground(world, renderContext, renderLayers.background);
const playerSprites = await createPlayer(
renderContext,
world,
renderLayers.foreground,
);
await createAsteroidSpawner(world, renderContext, renderLayers.foreground);
const explosionSpawner = await createExplosionSpawner(
renderContext,
renderLayers.foreground,
triggerCameraShake,
);
createMusic(world);
const gameOverEntity = world.createEntity();
const gameOverMessageElement = document.createElement('div');
gameOverMessageElement.textContent = "Press 'R' to restart";
gameOverMessageElement.style.cssText = `
position: absolute;
inset: 0;
display: none;
align-items: center;
justify-content: center;
color: white;
font-family: sans-serif;
font-size: 2rem;
text-shadow: 0 0 8px black;
pointer-events: none;
z-index: 1;
`;
game.container.appendChild(gameOverMessageElement);
world.addComponent(gameOverEntity, gameOverId, {
isGameOver: false,
messageElement: gameOverMessageElement,
});
const respawnPlayer = (): void => {
spawnPlayer(renderContext, world, renderLayers.foreground, playerSprites);
};
const onPlayerDeath = (): void => {
const gameOverComponent = world.getComponent<GameOverEcsComponent>(
gameOverEntity,
gameOverId,
);
if (gameOverComponent) {
gameOverComponent.isGameOver = true;
}
};
const random = new Random();
const physicsWorld = new PhysicsWorld();
world.addSystem(createCameraEcsSystem(time));
world.addSystem(createCameraShakeEcsSystem(time, random));
world.addSystem(createRenderEcsSystem(renderContext));
world.addSystem(createBloomEcsSystem(renderContext));
world.addSystem(createGaussianBlurEcsSystem(renderContext));
world.addSystem(createToneMapEcsSystem(renderContext));
world.addSystem(createPresentEcsSystem(renderContext));
world.addSystem(createMovementEcsSystem(moveInput, time));
world.addSystem(createBackgroundEcsSystem(time));
world.addSystem(createAudioEcsSystem());
world.addSystem(createLifetimeTrackingEcsSystem(time));
world.addSystem(createRemoveFromWorldEcsSystem());
world.addSystem(createGunEcsSystem(time, world, shootInput));
world.addSystem(createBulletEcsSystem(time));
world.addSystem(createAsteroidSpawnerEcsSystem(time, random));
world.addSystem(createAsteroidEcsSystem(time));
world.addSystem(
createSpriteAnimationEcsSystem(time, explosionSpawner.animationRegistry),
);
world.addSystem(createPhysicsSyncEcsSystem(physicsWorld, time));
world.addSystem(
createAsteroidCollisionEcsSystem(
physicsWorld,
time,
explosionSpawner,
onPlayerDeath,
),
);
world.addSystem(createGameOverEcsSystem(restartInput, respawnPlayer));
return game;
};
This demo showcases a complete space shooter game built using the Forge Game Engine. It features player-controlled movement, shooting mechanics, enemy spawning, and collision detection. The game demonstrates how to leverage the engine's capabilities to create an engaging and interactive experience. Players can navigate their spaceship, avoid obstacles, and shoot down enemies while enjoying smooth rendering and responsive controls.