Tinyworld is a small component-based 2D engine built with Svelte and Canvas2D. It is opinionated, geometric, and intentionally lightweight: scene graphs, runtime component contracts, transform inheritance, rendering, collision, and demo gameplay all live in a compact TypeScript codebase.
This project is not trying to be a general-purpose AAA engine. It is trying to be expressive, hackable, and fun to extend. That said, it's design is heavily influenced by the Unity engine with some semantics borrowed from Unreal.
*Exclusive early acces in-game footage of the Game of Life demo - Todd Howard*
At a high level, Tinyworld is built around a few core ideas:
GameObject owns identity, child hierarchy, and a bag of Components.Components add behavior and data.Transform defines local motion and derives world motion through the parent chain.Mesh describes geometry.Renderer draws geometry.Collider reasons about geometry and emits collision events.Systems orchestrate world-scale work like rendering and collision passes.Scene flattens object graphs into a linear runtime view without losing parent-child semantics.World owns the main loop, scene switching, and system lifecycle.So we get things that are composition-first:
Transform + RectangleMesh + RectRenderer gives you a visible rectangle.Transform + RectangleMesh + BoxCollider gives you collision geometry.Transform + RectangleMesh + RectRenderer + BoxCollider gives you both.GameObjects inherit transform state from their parent object.Tinyworld has a few strong opinions:
Main engine code lives in src/lib/engine.
Important directories:
src/lib/engine/core: engine primitivessrc/lib/engine/instances: concrete component/system implementationssrc/lib/engine/game: gameplay-facing objects and demossrc/lib/engine/scenes: scene buildersWorldWorld is the runtime root:
reset() so the Svelte app can reboot the engine cleanly during hot reload / remountsFrame order is:
GameObjectsComponentsSystemsThat ordering matters. It lets gameplay mutate transforms before systems like rendering and collision consume the updated state.
SceneScene owns a flattened runtime list of game objects while still respecting hierarchical graphs.
Key responsibilities:
onStart()SQRY, a small scene query helperThe important design choice here is: the world loop iterates a flat array, but scenes still accept nested parent-child graphs.
GameObjectsrc/lib/engine/core/game-object.ts
GameObject is the composition container:
GameObjectsComponentsResponsibilities include:
getComponent, find, query, etc.)Componentsrc/lib/engine/core/component.ts
Component is the unit of reusable behavior/data.
It provides:
onStart, onTick, onDestroystart() / destroy() entry points so the engine can safely manage lifecyclesrc/lib/engine/core/decorators.ts
The engine supports declarative component dependencies via decorators.
Example:
@RequireComponents([Transform, Mesh])
export abstract class Collider extends Component {}
The decorator stores metadata. GameObject enforces it at runtime when components are attached and when starts are processed.
Components can say what they need, and wiring mistakes fail loudly instead of turning into mysterious
undefinedbehavior later.
src/lib/engine/instances/physics/transform.ts
Transform supports:
position, rotation, scaleworld transformlocalRightlocalUprightupThis means child objects can be authored relative to their parent, while systems and gameplay can still read fully composed world state.
MeshMeshes are geometry providers. Current concrete meshes include:
RenderingSystemsrc/lib/engine/instances/renderers/rendering-system.ts
Rendering is system-owned.
The key abstraction is RenderFrame, a small command buffer for renderer intent.
Renderers do not manually juggle canvas boilerplate. Instead, they describe draw intent:
frame
.useStyle(this.style)
.shape((ctx) => {
ctx.rect(x, y, width, height);
})
.fill(...)
.stroke(...)
.commit();
Why this design is useful:
Important render files:
src/lib/engine/instances/renderers/mesh-renderer.tssrc/lib/engine/instances/renderers/rect-renderer.tssrc/lib/engine/instances/renderers/circle-renderer.tsCollision currently lives in:
src/lib/engine/instances/colliders/collider.tssrc/lib/engine/instances/colliders/box-collider.tssrc/lib/engine/instances/physics/collision-system.tsWhat exists today:
passive: triggersactive: boundary / wall behaviorThis is intentionally lightweight. It is not a full physics engine, but it is adjacent.
The current collision model is best understood as:
The event system is used for:
This allows engine-level coordination without every subsystem tightly reaching into every other subsystem.
pnpm install
pnpm dev
pnpm check
pnpm format
pnpm lint
The simplest visible object usually looks like:
const go = new GameObject('MyObject', [
new Transform(new Vec2(100, 100)),
new RectangleMesh(40, 40),
new RectRenderer()
]);
If it should collide:
const go = new GameObject('MyWall', [
new Transform(new Vec2(200, 300)),
new BoxCollider(40, 40),
new RectangleMesh(40, 40),
new RectRenderer()
]);
go.getComponent(BoxCollider)!.collisionBehavior = 'active';
If it should just be a trigger, keep the collider passive.
Create a scene:
const scene = new Scene('main');
Spawn objects:
scene.spawn([player, wall, enemy]);
Register and start it with the world:
const world = World.instance();
world.registerSystems([new CollisionSystem(), new RenderingSystem(canvas)]);
world.addScene(scene);
await world.start('main');
genesissrc/lib/engine/scenes/genetic-life-scene.ts
This is the current editor demo scene.
It builds a cellular automaton board using the engine's regular object/component stack:
GameObjectTransform, RectangleMesh, and RectRendererGenes currently influence:
The scene reseeds itself when the board stagnates or the population crashes, so it stays visually active.
The repo also contains a handful of interactive/demo objects from earlier engine exploration:
Character: wandering actorFollower: parent-following actorHunter: prey-seeking actorInteractable: static collision/trigger geometryMouseLeader: cursor-driven objectMouseTracker: reusable input componentThese are useful examples of how to build gameplay on top of the engine, even when they are not the currently mounted scene.
Things the engine is already especially good at:
The genetics demo is a good proof point here: a totally different simulation sits on top of the same object model, render layer, and scene runtime.
This engine is intentionally small, and some systems are still evolving.
Current limitations:
These are not flaws so much as current boundaries of the project.
There are a few strong opinions:
A few things i'd like to add soon:
No explicit license is currently declared in this repository. Add one if you plan to distribute or open-source the engine.