diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a601095..f129bb30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +#### Added + +- **utilities:** `createGame` takes an optional second `options` argument whose `renderContext` field is forwarded to `createRenderContext`, so a game set up with `createGame` can cap its render resolution, e.g. `createGame('game', { renderContext: { maxPixelRatio: 1.5 } })` + #### Fixed - **rendering:** Gaussian blur and bloom are now sized in CSS pixels instead of render target (device) pixels, so the same `passes`, `threshold` and `intensity` look the same at every display pixel ratio. Since 0.25.6, a high-DPI display made the blur and the bloom halo spread less far on screen, and made bloom much brighter close to small bright sprites. Looks at a pixel ratio of 1 are unchanged. On a high-DPI display the Gaussian blur now runs at CSS-pixel resolution, which also makes it cheaper there diff --git a/documentation-site/docs/docs/ecs/game.md b/documentation-site/docs/docs/ecs/game.md index 725ea106..5068de28 100644 --- a/documentation-site/docs/docs/ecs/game.md +++ b/documentation-site/docs/docs/ecs/game.md @@ -96,6 +96,17 @@ const { game, world, time, renderContext, resizeSync } = createGame('game'); game.run(); ``` +`createGame` takes an optional second argument. Its `renderContext` field is +forwarded to [`createRenderContext`](/Forge/docs/api/functions/createRenderContext), +so you can, for example, cap the render resolution on high-DPI displays (see +[High-DPI displays](../rendering/world-units-and-cameras.md#high-dpi-displays)): + +```ts +const { game, renderContext } = createGame('game', { + renderContext: { maxPixelRatio: 1.5 }, +}); +``` + Manual setup (when you need fine-grained control): ```ts diff --git a/documentation-site/docs/docs/rendering/world-units-and-cameras.md b/documentation-site/docs/docs/rendering/world-units-and-cameras.md index 39f24d83..63cdadb0 100644 --- a/documentation-site/docs/docs/rendering/world-units-and-cameras.md +++ b/documentation-site/docs/docs/rendering/world-units-and-cameras.md @@ -148,6 +148,15 @@ can cap it with `maxPixelRatio`: const renderContext = createRenderContext(canvas, { maxPixelRatio: 2 }); ``` +If you set up with `createGame`, pass the same options through its +`renderContext` option: + +```ts +const { renderContext } = createGame('game-container', { + renderContext: { maxPixelRatio: 1.5 }, +}); +``` + Pass `maxPixelRatio: 1` to always render at CSS resolution, the engine's behavior before it supported high-DPI displays. diff --git a/src/utilities/create-game.test.ts b/src/utilities/create-game.test.ts index 2d270ad0..cfcef01a 100644 --- a/src/utilities/create-game.test.ts +++ b/src/utilities/create-game.test.ts @@ -56,6 +56,26 @@ describe('createGame', () => { expect(createRenderContext).toHaveBeenCalled(); }); + it('creates the render context with the canvas and no options by default', () => { + const canvas = document.createElement('canvas'); + vi.mocked(createCanvas).mockReturnValueOnce(canvas); + + createGame('game-container'); + + expect(createRenderContext).toHaveBeenLastCalledWith(canvas, {}); + }); + + it('forwards renderContext options to createRenderContext', () => { + const canvas = document.createElement('canvas'); + vi.mocked(createCanvas).mockReturnValueOnce(canvas); + + createGame('game-container', { renderContext: { maxPixelRatio: 1.5 } }); + + expect(createRenderContext).toHaveBeenLastCalledWith(canvas, { + maxPixelRatio: 1.5, + }); + }); + it('returns a resize sync', () => { const { resizeSync } = createGame('game-container'); expect(typeof resizeSync.stop).toBe('function'); diff --git a/src/utilities/create-game.ts b/src/utilities/create-game.ts index c24bf9d6..7e9d91c8 100644 --- a/src/utilities/create-game.ts +++ b/src/utilities/create-game.ts @@ -4,6 +4,7 @@ import { createCanvas, createRenderContext, RenderContext, + RenderContextOptions, } from '../rendering/index.js'; import { ContainerResizeSync, @@ -11,18 +12,44 @@ import { } from './create-container-resize-sync.js'; import { Game } from './game.js'; +/** + * Options for `createGame`. + */ +export interface CreateGameOptions { + /** + * Options forwarded to `createRenderContext` for the game's render context, + * e.g. `{ maxPixelRatio: 1.5 }` to cap the render resolution on high-DPI + * displays. + */ + renderContext?: RenderContextOptions; +} + +const defaultCreateGameOptions = { + renderContext: {}, +}; + /** * Creates a new game instance with the specified container ID. * @param containerId - The ID of the container element where the game will be rendered. + * @param options - Options for the game, such as the `RenderContextOptions` to create its render context with. * @returns An object containing the game instance, ECS world, render context, time, and the resize sync keeping the render context's canvas sized to the container. + * @throws An error if no DOM element with `containerId` exists. */ -export function createGame(containerId: string): { +export function createGame( + containerId: string, + options: CreateGameOptions = {}, +): { game: Game; world: EcsWorld; renderContext: RenderContext; time: Time; resizeSync: ContainerResizeSync; } { + const { renderContext: renderContextOptions } = { + ...defaultCreateGameOptions, + ...options, + }; + const time = new Time(); const world = new EcsWorld(); const container = document.getElementById(containerId); @@ -33,7 +60,7 @@ export function createGame(containerId: string): { const canvas = createCanvas(container); - const renderContext = createRenderContext(canvas); + const renderContext = createRenderContext(canvas, renderContextOptions); const resizeSync = createContainerResizeSync(container, [renderContext]);