Skip to content

Philosophy & Design Principles

Web Engine Dev is built around a set of architectural principles that inform every design decision, from package boundaries to API surface to memory layout.

Bring Your Own Engine

The foundational philosophy is that no one engine fits all projects. Rather than a monolithic framework, Web Engine Dev provides a ecosystem of composable packages. You assemble the engine that fits your project:

  • Building a 2D puzzle game? Use math, ecs, sprites, input, and physics2d.
  • Building a 3D shooter? Add renderer, gltf, animation, physics3d, netcode, and audio.
  • Building a simulation tool? Use ecs, spatial, ai, and pathfinding with no rendering at all.

Each package declares its dependencies explicitly and can be installed independently via npm. The umbrella @web-engine-dev/engine package re-exports everything for convenience, but it is never required.

Modularity Through Layered Dependencies

Packages are organized into dependency layers (Layer 0 through Layer 9). A package at Layer N may only depend on packages at Layer N-1 or below. This rule is enforced by design and prevents circular dependencies:

Layer 0: math (zero dependencies)
Layer 1: ecs, events, time, scheduler, hierarchy, resources, change-detection
Layer 2: serialization, reflection, splines, scripting
Layer 3: input, audio, spatial
Layer 4: physics2d, physics3d, cloth, ragdoll, destruction
Layer 5: animation, character, gesture
Layer 6: render-graph, shader-compiler
Layer 7: renderer, gltf, particles, sprites, text, terrain, tilemap, ui, vfx, gizmos
Layer 8: scene, prefab, save, assets, netcode, ai, pathfinding, procgen, state
Layer 9: engine (umbrella), editor-core, editor-ui

This layering means that @web-engine-dev/math has zero dependencies, @web-engine-dev/ecs depends only on Layer 0-1 packages, and the renderer depends only on layers below it. You get exactly the transitive dependency tree you expect.

Data-Oriented Design

The engine uses data-oriented design (DOD) principles throughout, prioritizing cache-friendly memory access patterns over object-oriented hierarchies.

ECS with SoA Storage

The Entity Component System uses archetype-based Struct of Arrays (SoA) columnar storage. Entities with the same set of components share an archetype, and each component type is stored in a contiguous array within that archetype. This means iterating over all positions is a linear memory scan rather than pointer chasing through scattered objects.

Archetype [Position, Velocity]
Position column: [x0,y0,z0, x1,y1,z1, x2,y2,z2, ...]  (contiguous)
Velocity column: [x0,y0,z0, x1,y1,z1, x2,y2,z2, ...]  (contiguous)

Zero-Allocation Hot Paths

Systems that run per-frame (ECS queries, transform propagation, rendering, physics) are designed to avoid heap allocations in steady state. The math library provides three API tiers for this purpose:

  • Immutable API (default) -- returns new instances, safe and readable
  • Mutable API (addMut, scaleMut) -- modifies in place, no allocation
  • Batch API (BatchMath) -- operates on packed Float32Array buffers

Object pools (MathPool, PosePool, buffer pools) are available for temporary objects in hot paths.

WebGPU-First Rendering

The renderer targets WebGPU with WebGPU-only runtime execution. This means:

  • Runtime shaders are authored in WGSL
  • Compute-driven features (GPU culling, GPU particles, GPU-driven rendering) are first-class paths
  • The device abstraction (GpuDevice) exposes a stable API while runtime execution remains WebGPU
typescript
import { createDevice } from '@web-engine-dev/renderer';

// WebGPU runtime (preferredBackend retained for API compatibility)
const { device, backend } = await createDevice({
  canvas,
  preferredBackend: 'auto',
});

The rendering pipeline includes physically-based materials (metallic-roughness workflow aligned with glTF), cascaded shadow maps, image-based lighting with anti-firefly techniques, clustered forward lighting, and a post-processing pipeline with 22 effects.

TypeScript-Native

Web Engine Dev is written in TypeScript from the ground up, not bolted on as type definitions for a JavaScript library. This enables:

  • Strict mode with noUncheckedIndexedAccess -- indexed access returns T | undefined, catching off-by-one errors at compile time
  • Branded types for domain-specific values (entity IDs, resource descriptors) that prevent accidental misuse
  • Discriminated unions for state machines and variant types that the compiler can exhaustively check
  • Generic constraints on ECS queries, resource descriptors, and event types that preserve type safety through the entire pipeline

The build target is ES2022 with ESNext module format and verbatimModuleSyntax. Each package produces ESM and CJS output with .d.ts type declarations.

Performance as a Feature

Performance is not an afterthought in a game engine. Key design decisions reflect this:

  • Algorithm complexity is documented -- JSDoc annotations include @remarks O(n log n) where applicable
  • Data structures are chosen for access patterns -- spatial hashes for O(1) lookups, intrusive heaps for O(1) decrease-key in pathfinding, bucket queues for flow fields
  • Determinism is required for gameplay systems -- physics, netcode, and animation produce identical results given the same inputs. No Date.now() or Math.random() in gameplay code (use seeded RNG)
  • Parallel execution is supported -- the ECS scheduler detects read/write conflicts between systems and can execute non-conflicting systems in parallel via Web Workers

Interface-First Design

External systems like physics, audio, and storage are accessed through interfaces (ports), not concrete implementations:

  • @web-engine-dev/physics2d defines the physics interface (bodies, shapes, joints, collision detection)
  • @web-engine-dev/physics2d-rapier provides the Rapier adapter for that interface
  • The same pattern applies to 3D physics (physics3d / physics3d-rapier)

This means you can swap physics backends without changing game code, and the core engine never imports concrete implementations directly.

Fail Fast, Fail Loud

The engine uses invariant() assertions throughout to catch invalid states as early as possible. A corrupted entity ID discovered at creation time (with a clear error message and stack trace) is vastly preferable to silent data corruption that manifests three systems later. In development builds, these assertions are active; in production, they are stripped.

Next Steps

Proprietary software. All rights reserved.