2D Game Development
This guide covers everything specific to building 2D games with Web Engine Dev: tilemaps and tile-based worlds, sprite rendering and batching, 2D lighting, pixel art mode, and common 2D game patterns.
Sprite Rendering
typescript
import { Sprite, SpriteRenderer } from '@web-engine-dev/sprites';
// Basic sprite
world.insert(playerEntity, Sprite, {
texture: 'sprites/hero.png',
width: 48,
height: 48,
pivot: { x: 0.5, y: 0.5 }, // center pivot
flipX: false,
flipY: false,
color: [1, 1, 1, 1], // tint (white = no tint)
sortingLayer: 'characters',
sortingOrder: 0,
});
// Flip sprite based on movement direction
function FlipSpriteSystem(world: World): void {
const q = world.query().with(Velocity, Sprite, Player).build();
for (const result of world.run(q)) {
const [vel] = result.components;
if (vel.x !== 0) {
const sprite = world.get(result.entity, Sprite);
world.insert(result.entity, Sprite, { ...sprite, flipX: vel.x < 0 });
}
}
}Sprite Sheets
typescript
import { SpriteSheet, SpriteFrame } from '@web-engine-dev/sprites';
// Define a sprite sheet with named frames
const heroSheet = new SpriteSheet('sprites/hero-sheet.png', {
cellWidth: 48,
cellHeight: 48,
});
world.insert(playerEntity, Sprite, {
sheet: heroSheet,
frame: 'idle_0',
width: 48,
height: 48,
});Sprite Sorting
Control draw order with sorting layers:
typescript
// Define sorting layers in your engine config (order = back to front)
const SORTING_LAYERS = ['background', 'terrain', 'pickups', 'characters', 'fx', 'ui-world'];
world.insert(backgroundEntity, Sprite, { sortingLayer: 'background', sortingOrder: 0 });
world.insert(treeEntity, Sprite, { sortingLayer: 'terrain', sortingOrder: 5 });
world.insert(playerEntity, Sprite, { sortingLayer: 'characters', sortingOrder: 0 });
world.insert(effectEntity, Sprite, { sortingLayer: 'fx', sortingOrder: 0 });Tilemaps
Loading from Tiled
The engine imports .tmj (Tiled JSON) files natively:
typescript
import { TiledImporter } from '@web-engine-dev/tilemap';
// Parse a Tiled map JSON (fetched or bundled asset)
const tiledJson = await fetch('maps/level-01.tmj').then((r) => r.json());
const importer = new TiledImporter();
const result = importer.import(tiledJson, {
generateColliders: true, // auto-generate physics colliders from collision layer
collisionLayers: ['Collision'], // layer names in Tiled to use as colliders
});
// result.tilemap, result.layers, result.collisionShapes are ready to useCreating Tilemaps Procedurally
typescript
import { Tilemap, TileLayer, TileSet } from '@web-engine-dev/tilemap';
const tileset = new TileSet('tilesets/dungeon.tsj', { tileWidth: 16, tileHeight: 16 });
const tilemapEntity = world.spawn();
world.insert(tilemapEntity, Tilemap, {
tileWidth: 16,
tileHeight: 16,
mapWidth: 64,
mapHeight: 32,
});
// Attach a TileLayer component for each layer
world.insert(tilemapEntity, TileLayer, {
name: 'Ground',
tiles: generateGroundTiles(64, 32), // Int32Array of tile IDs
visible: true,
opacity: 1.0,
});Procedural Generation
typescript
import { NoiseGenerator } from '@web-engine-dev/procgen';
function generateLevel(): Int32Array {
const noise = new NoiseGenerator({ seed: 42, scale: 0.05 });
const tiles = new Int32Array(MAP_WIDTH * MAP_HEIGHT);
for (let y = 0; y < MAP_HEIGHT; y++) {
for (let x = 0; x < MAP_WIDTH; x++) {
const value = noise.sample2D(x, y);
tiles[y * MAP_WIDTH + x] =
value < -0.3
? TILE_WATER
: value < 0.0
? TILE_SAND
: value < 0.4
? TILE_GRASS
: value < 0.6
? TILE_FOREST
: TILE_MOUNTAIN;
}
}
return tiles;
}Tile Queries
typescript
import { Tilemap, TileLayer } from '@web-engine-dev/tilemap';
// Read the Tilemap and TileLayer components (returns copies - must re-insert to persist changes)
const tilemap = world.get(tilemapEntity, Tilemap);
const layer = world.get(tilemapEntity, TileLayer);
// Convert world position to tile coordinates
const tileX = Math.floor(worldX / tilemap.tileWidth);
const tileY = Math.floor(worldY / tilemap.tileHeight);
// Get tile index at tile coordinate
const tileIndex = layer.tiles[tileY * tilemap.mapWidth + tileX];
// Set a tile (mutate the copy, then re-insert to persist)
const updatedLayer = { ...layer };
updatedLayer.tiles = new Int32Array(layer.tiles);
updatedLayer.tiles[tileY * tilemap.mapWidth + tileX] = TILE_TORCH;
world.insert(tilemapEntity, TileLayer, updatedLayer);2D Lighting
typescript
import {
createGlobalLightComponent,
createPointLightComponent,
createSpotLightComponent,
Light2DComponentData,
GlobalLight2DData,
} from '@web-engine-dev/sprites';
// Global ambient light on a dedicated entity
const ambientEntity = world.spawn();
world.insert(
ambientEntity,
Light2DComponentData,
createGlobalLightComponent({
colorR: 0.1,
colorG: 0.1,
colorB: 0.2,
intensity: 0.3,
})
);
// Torch/point light attached to an entity
world.insert(
torchEntity,
Light2DComponentData,
createPointLightComponent({
colorR: 1.0,
colorG: 0.7,
colorB: 0.3,
intensity: 2.5,
radius: 200,
})
);
// Flashlight / spot light
world.insert(
playerEntity,
Light2DComponentData,
createSpotLightComponent({
colorR: 1,
colorG: 1,
colorB: 0.9,
intensity: 3,
radius: 300,
falloff: 60, // inner cone angle (degrees)
outerAngle: 80, // outer cone angle (degrees)
})
);Pixel Art Mode
typescript
import { PixelPerfectSettings } from '@web-engine-dev/renderer';
// Enable pixel-perfect rendering (no sub-pixel filtering)
world.insertResource(PixelPerfectSettings, {
enabled: true,
pixelScale: 3, // 3x native pixel size (e.g. 320×180 → 960×540)
filterMode: 'nearest', // no bilinear filtering
snapCameraToPixel: true, // prevent sub-pixel camera movement
});Parallax Backgrounds
typescript
import { Sprite } from '@web-engine-dev/sprites';
import { Position } from '@web-engine-dev/ecs';
// Create parallax layers (further = slower scroll)
// PixelScale/layerDepth convention: lower z = more parallax
const skyEntity = world.spawn();
world.insert(skyEntity, Sprite, { texture: 'bg/sky.png', width: 1280, height: 720 });
world.insert(skyEntity, Position, { x: 0, y: 0, z: -3 }); // static sky
const mountainEntity = world.spawn();
world.insert(mountainEntity, Sprite, { texture: 'bg/mountains.png', width: 1280, height: 360 });
world.insert(mountainEntity, Position, { x: 0, y: 360, z: -2 }); // slow mountains
const forestEntity = world.spawn();
world.insert(forestEntity, Sprite, { texture: 'bg/forest.png', width: 1280, height: 200 });
world.insert(forestEntity, Position, { x: 0, y: 520, z: -1 }); // medium treesCommon 2D Patterns
Platformer Ground Detection
typescript
function GroundDetectionSystem(world: World): void {
const physics = world.getResource(PhysicsWorld2DResource);
const groundQ = world.query().with(Position, RigidBody2D, Player).build();
for (const result of world.run(groundQ)) {
const [pos] = result.components;
const entity = result.entity;
// Cast two rays from the feet for robust detection
const leftHit = physics.raycast(
{ x: pos.x - 12, y: pos.y + 22 },
{ x: 0, y: 1 },
{ maxDistance: 3 }
);
const rightHit = physics.raycast(
{ x: pos.x + 12, y: pos.y + 22 },
{ x: 0, y: 1 },
{ maxDistance: 3 }
);
const grounded = !!(leftHit || rightHit);
if (grounded && !world.has(entity, Grounded)) {
world.insert(entity, Grounded, {});
} else if (!grounded && world.has(entity, Grounded)) {
world.remove(entity, Grounded);
}
}
}Top-Down RPG Movement
typescript
function TopDownMovementSystem(world: World): void {
const input = world.getResource(InputState);
const { delta } = world.getResource(Time);
const moveQ = world.query().with(Position, PlayerStats, Player).build();
for (const result of world.run(moveQ)) {
const [pos, stats] = result.components;
const entity = result.entity;
const speed = stats.moveSpeed;
let dx = 0,
dy = 0;
if (input.pressedKeys.has('ArrowLeft')) dx -= 1;
if (input.pressedKeys.has('ArrowRight')) dx += 1;
if (input.pressedKeys.has('ArrowUp')) dy -= 1;
if (input.pressedKeys.has('ArrowDown')) dy += 1;
// Normalize diagonal movement
if (dx !== 0 && dy !== 0) {
dx *= 0.707;
dy *= 0.707;
}
world.insert(entity, Position, {
x: pos.x + dx * speed * delta,
y: pos.y + dy * speed * delta,
z: 0,
});
// Update facing direction
if (dx !== 0 || dy !== 0) {
world.insert(entity, Facing, { x: dx, y: dy });
}
}
}Next Steps
- Scenes & Prefabs, organize tilemap levels into scene files
- Camera Systems, 2D follow camera with deadzone
- Physics, tilemap colliders and character controllers