Skip to content

Lighting

@yagejs/lighting adds soft radial lights to a scene and gives gameplay code a continuous 0..1 light level. A stealth game can use the same light sources for its visuals and detection rules.

Terminal window
npm install @yagejs/lighting

Install LightingPlugin after RendererPlugin:

import { Engine } from "@yagejs/core";
import { LightingPlugin } from "@yagejs/lighting";
import { RendererPlugin } from "@yagejs/renderer";
const engine = new Engine();
engine.use(new RendererPlugin());
engine.use(
new LightingPlugin({
ambient: {
level: 0.2,
color: 0xb0b8cc,
},
}),
);

The ambient level controls how bright the scene and gameplay query are where no light reaches. Both level and light intensity use a range from 0 to 1.

A light follows the world position of the Transform on the same entity:

import { Transform, Vec2 } from "@yagejs/core";
import { LightSource } from "@yagejs/lighting";
const torch = this.spawn("torch");
torch.add(new Transform({ position: new Vec2(320, 180) }));
const light = torch.add(
new LightSource({
radius: 180,
intensity: 0.9,
color: 0xffb060,
}),
);

radius is measured in world pixels. The light follows parent transforms, but transform scale does not resize it. Change the light directly when its gameplay radius changes:

light.radius = 240;
light.intensity = 0.7;
light.color = 0x80aaff;
light.enabled = false;

Disabling the light or its entity removes it from rendering and queries until it becomes active again. Light sources save and restore through @yagejs/save.

Resolve the scene’s LightingWorld from a component:

import { Component } from "@yagejs/core";
import { LightingWorldKey } from "@yagejs/lighting";
class GuardVision extends Component {
private readonly lighting = this.service(LightingWorldKey);
canSee(x: number, y: number): boolean {
return this.lighting.levelAt(x, y) >= 0.45;
}
}

levelAt(x, y) starts with the ambient level, adds the contribution from each enabled light, and clamps the answer to 1. Each light fades linearly from its full intensity at the centre to zero at its radius. The query is scalar, so light colours do not affect it.

Change the scene’s ambient light at runtime:

const lighting = this.service(LightingWorldKey);
lighting.setAmbient(0.05);
lighting.setAmbient(0.2, 0x8090b8);

The default renderer draws ambient colour and radial lights into an offscreen buffer, then multiplies it over the scene. Coloured lights tint the surfaces they reach, as real coloured illumination does.

import { LightingPlugin, overlayLighting } from "@yagejs/lighting";
engine.use(
new LightingPlugin({
renderer: overlayLighting({
layer: "lighting",
order: 900,
resolutionScale: 0.5,
antialias: true,
}),
}),
);

The default layer is screen-space and uses order 900. A UI layer at the conventional order 1000 stays above the lighting. Lower resolutionScale to reduce the light buffer’s GPU cost at the expense of sharper gradient edges.

The renderer follows the highest-priority enabled camera in each scene. With no camera, world coordinates map directly to the viewport.

For tests or gameplay that needs only levelAt(), pass renderer: null. You still install RendererPlugin; the option only disables lighting’s visual backend.

LightOccluder stores geometry for a lighting renderer without tying the component to Pixi:

import { LightOccluder } from "@yagejs/lighting";
wall.add(
new LightOccluder({
shape: { type: "box", width: 96, height: 24 },
}),
);
pillar.add(
new LightOccluder({
shape: { type: "circle", radius: 20 },
}),
);
rock.add(
new LightOccluder({
shape: {
type: "polygon",
vertices: [
{ x: -20, y: 12 },
{ x: 0, y: -18 },
{ x: 24, y: 10 },
],
},
}),
);

The shape uses local pixels. Its centre and rotation follow the entity’s world transform.

Set renderer to a factory when a game needs a different visual result:

import type { LightingRendererFactory } from "@yagejs/lighting";
const customLighting: LightingRendererFactory = ({
scene,
world,
renderer,
}) => ({
render(frame) {
// Read world.sources and world.occluders.
// frame.camera is this scene's active, highest-priority camera.
// frame.width and frame.height are the virtual viewport size.
},
destroy() {
// Release resources owned by this scene.
},
});
engine.use(new LightingPlugin({ renderer: customLighting }));

YAGE calls the factory once for each entered scene and calls destroy() when that scene exits. Each source contains renderer-neutral light data, so a custom renderer can draw visibility polygons, material lighting, or another visual treatment without changing game entities.