Jagger PapersTechnical notes & papers
Jagger Papers

Engine Architecture / Systems / Research Notes

Rengine: A Compact TypeScript Rendering and Game Engine Experiment

A scene/entity runtime for exploring transform hierarchies, update components, canvas rendering, React rendering, and live debug visualization.

TypeScriptReactViteCanvasGame LoopScene GraphEntity SystemsTransform HierarchiesRenderer AbstractionDebug VisualizationRendering

Abstract

Rengine is a compact rendering and game-engine experiment built in TypeScript. It focuses on core engine boundaries: scenes, entities, folder-style transform hierarchies, components, update loops, renderer abstraction, and debug visualization. Entities hold transform properties, component lists, render functions, and optional shaders. Folder entities compose child transforms, scenes group entities, the loop advances component state, and renderer helpers can target canvas or React output. The browser surface exposes those mechanics with live scene selection, zoom controls, wireframe toggles, and an inspectable component tree.

Rengine demo preview. The interactive route renders a live scene and exposes the corresponding runtime tree and transform values.

I

Motivation

Rengine isolates the parts of a game or rendering engine that sit below visible content: how entities are represented, how transforms compose through parents, how rendering can be swapped, and how update logic stays separate from drawing.

The visual vocabulary is deliberately small. Boxes, folders, paths, rotations, and debug markers are enough to stress the runtime boundaries without burying the engine model under art assets.

Rengine is best read as a transform debugger that happens to render animated scenes. The question is not whether it can ship a game. The question is whether the runtime makes a child box's final position explainable after its own position, its anchor, a parent folder, a component update, and the renderer's coordinate conventions all interact.

  • Primary goal: make scene graph and transform behavior visible.
  • Implementation target: a small TypeScript engine with canvas and React render paths.
  • Demo target: live inspection of runtime scenes, transforms, and component labels.

II

Entity Model

An entity is a bundle of transform properties, components, a renderer, and shaders. A folder entity is an entity-like grouping node that owns children and applies parent transforms to them. That gives the engine a compact scene graph without needing a separate hierarchy system.

Te=(pe,ae,re,se)T_e = (p_e, a_e, r_e, s_e)
Entity transform. Each entity carries position, anchor, rotation, and scale.

The folder model is the key idea. It lets a parent pivot, move, scale, or rotate a child group, while individual children still keep their own local transforms and update components.

In the implementation, a folder is deliberately practical: it owns child entities, applies a parent transformation before rendering them, and can optionally recenter its origin around the geometric bounds of its children. It is less general than a full scene-graph math library, but the inspector can explain the hierarchy and grouping stays close to ordinary entity behavior.

rworld=(rparent+rlocal)mod2πr_{\mathrm{world}} = (r_{\mathrm{parent}} + r_{\mathrm{local}}) \bmod 2\pi
Composed rotation. Folder entities compose child rotation with parent rotation.
  • Entity construction flows through Rengine.Entity.MakeEntity.
  • Folder construction flows through Rengine.Entity.MakeFolderEntity.
  • Folder bounds can optionally place origin and anchor at the geometric center.
  • Debug visualization shows anchors, positions, and parent-child relationships.

III

Engine Type Model

The engine can be reconstructed from a small set of TypeScript types. Entity is the core drawable/updateable object. FolderEntity extends Entity with children. Scene groups entities and gives them a name. EngineState stores scenes, the active scene, global configuration, camera data, and renderer selection.

type Vector2 = { x: number; y: number };
type Component = {
  name: string;
  update: (delta: number, e: Entity, s: Scene, prev: EngineState) => [Entity, EngineState];
};
type Entity = {
  id: string;
  position: Vector2;
  anchor: Vector2;
  rotation: number;
  scale: Vector2;
  components: Component[];
  render: (state: EngineState) => (entity: Entity) => unknown;
};
type FolderEntity = Entity & { entities: Entity[] };
type Scene = { name: string; entities: Entity[] };
Core engine types. The model is deliberately small: transforms, components, render function, and optional children.
FolderEntity=Entity{children:Entity}FolderEntity = Entity \cap \{children: Entity^{*}\}
Folder subtype. A folder is an entity with child ownership, not a separate scene-graph primitive.

This choice keeps traversal uniform. Rendering and updating can visit an Entity, inspect whether it owns children, and recursively traverse without switching to a distinct transform-node API.

IV

Scenes and Runtime Loop

Scenes are named collections of entities, and the engine state tracks both the list of scenes and the active scene. Scene helpers create scenes, activate them, add entities, and remove entities.

(et+1,St+1)=c(Δt,et,scenet,St)(e_{t+1}, S_{t+1}) = c(\Delta t, e_t, scene_t, S_t)
Component update. A component advances an entity and may also return a changed engine state.

The update model is component-oriented. Components receive delta time, the current entity, the scene, and previous engine state, then return an updated entity and state. Movement and animation logic stay attached to entities instead of leaking into the renderer.

Rengine does not implement a full entity-component-system with indexed component stores and query planning. Components are ordered functions on an entity. The smaller model is easier to read and sufficient for the project's goal: show how motion changes entity transforms before those transforms are projected into canvas or React output.

  • Demo components include rotations, velocities, square paths, and star paths.
  • The active scene is recreated when the selected sample changes.
  • The runtime tree is periodically synchronized back into the React inspector.

V

Loop Scheduling

The loop has two modes: interval-based stepping and animation-frame stepping. In both modes the frame operation is the same: compute delta time, tick the active scene, apply the global update hook, and render the new engine state. Delta time is passed into every component so animation speed is independent of frame count.

timestamp
  |
  v
delta = timestamp - previousTimestamp
  |
  v
TickActiveScene(delta, state)
  |
  v
Lifecycle.update(delta, state)
  |
  v
render(state)
  |
  v
store state for next frame
Frame operation. The frame path is stable regardless of whether scheduling comes from requestAnimationFrame or an interval.
xt+Δt=xt+vΔtx_{t+\Delta t}=x_t+v\Delta t
Delta-time update. Velocity-style components update position from elapsed time rather than from an assumed frame rate.

The portfolio route caps large deltas before rendering so a suspended tab or stutter does not produce a single enormous movement step in the visible inspection surface.

VI

Transform Composition

Rengine's central mathematical object is the local-to-world transform. Entity position, anchor, rotation, and scale are stored locally. Folder entities compose those local values into world values for their children, which allows nested motion without copying absolute coordinates into every descendant.

Me=T(pe)R(re)S(se)T(ae)M_e = T(p_e)\,R(r_e)\,S(s_e)\,T(-a_e)
Local transform matrix. An entity transform translates to position, rotates, scales, and offsets by anchor.
We=WparentMeW_e = W_{parent}\,M_e
World transform. A child world transform is the parent world transform multiplied by the child's local transform.
Root folder W0
  |
  +-- Orbit folder M1 -> W1 = W0 M1
        |
        +-- Box M2 -> W2 = W1 M2
        |
        +-- Child folder M3 -> W3 = W1 M3
Folder composition. Folders are ordinary transform nodes that contribute a parent transform to every child.

The runtime inspector exposes both local and world-space values because most transform bugs are mismatches between the two. A child that appears wrong on screen may have a correct local transform but an unexpected parent transform, or the inverse.

VII

Renderer Abstraction

Rengine separates render functions from entity state. A box renderer can target canvas by drawing into a 2D context, or target React by returning absolutely positioned elements with computed transform styles.

The canvas path also supports transformation-point rendering. Blue, red, and relationship markers expose the distinction between entity position, anchor, and anchor-to-position offset, which is essential when debugging nested transforms.

  • Canvas rendering uses translate, rotate, and fillRect against the current entity transform.
  • React rendering converts the same transform data into CSS position, scale, and rotation.
  • Texture rendering is implemented for the React path as an image renderer.
  • Wireframe/debug overlays expose the renderer's transform basis.

VIII

Renderer Contract

Renderers consume engine state and entities; they do not own simulation state. The same scene graph can be stepped once and projected into different output targets. The canvas renderer uses imperative drawing commands, while the React renderer converts transforms into DOM/CSS output.

render:EngineStateEntityOutput\operatorname{render}: EngineState \rightarrow Entity \rightarrow Output
Render signature. A renderer is a curried function from engine state and entity to a target-specific output.
save context
translate(entity.position)
rotate(entity.rotation)
scale(entity.scale)
translate(-entity.anchor)
draw entity body
restore context
Canvas transform order. The canvas path maps entity transform fields directly onto 2D context operations.

The debug overlay is part of the renderer contract. Anchor markers, position markers, and relationship lines expose the transform basis used by the renderer, so visual output can be compared against the tree inspector without consulting source code.

IX

Component Behaviors

Components are small deterministic update functions attached to entities. A component may mutate transform fields such as position or rotation, but it returns the updated entity and engine state explicitly. Multiple components on one entity are applied in sequence.

e=cn(Δt,c2(Δt,c1(Δt,e,S),S),S)e' = c_n(\Delta t, \ldots c_2(\Delta t, c_1(\Delta t,e,S),S)\ldots,S)
Component chain. An entity's component list acts as an ordered transform/update pipeline.
rotation component:
  rotation += angularVelocity * delta

velocity component:
  position += velocity * delta

square path component:
  select edge by phase
  move along edge at configured speed

star path component:
  interpolate along star vertices by phase
Component catalog. The sample components cover continuous rotation, linear motion, and scripted path following.

Because component labels are exposed in the runtime tree, they also function as instrumentation. The inspector can explain why a node moves, not only where it currently is.

X

Browser Inspection Surface

The browser integration turns the engine sandbox into an inspection surface. It keeps the live canvas, adds scene selection, zoom controls, wireframe toggling, scene-focus copy, and a collapsible runtime tree.

That tree is the important bridge between visual output and engine state. The moving scene can be inspected through the corresponding hierarchy, component labels, local/world transforms, anchors, rotations, scales, and node IDs.

  • Demo scenes include stacked orbits, counter drift, layered motion, diagonal sweep arrays, in-place arrays, square paths, and star paths.
  • The stage zooms from 0.1x to 2x.
  • The tree supports local/world-space toggling per node.
  • The browser route makes the runtime inspectable without opening the source.

XI

Inspector Snapshot Semantics

The runtime tree is a snapshot of the current engine graph, not the graph itself. Each node stores identity, kind, component labels, local transform, world transform, color, and child snapshots. React inspection state stays separate from the mutable engine state used by the canvas loop.

type RuntimeTreeNode = {
  id: string;
  label: string;
  kind: "entity" | "folder";
  componentLabels: string[];
  local: TransformSnapshot;
  world: TransformSnapshot;
  color?: string;
  children: RuntimeTreeNode[];
};
Runtime tree node. Snapshots contain the information needed by the inspector without giving React ownership of engine mutation.
snapshot:EngineStateTree\operatorname{snapshot}: EngineState \rightarrow Tree
Snapshot function. The inspector is a pure projection of current engine state.

Collapsed-node state and local/world display preferences live outside the snapshot. When the scene changes, those UI sets are filtered against the new node IDs so stale inspector state cannot refer to removed nodes.

XII

Sample Scene Suite

The sample scenes are not arbitrary; each stresses a different engine property. Stacked orbits stress parent rotation. Counter drift stresses competing component updates. Layered motion stresses folder nesting. Sweep and in-place arrays stress repeated entity construction. Square and star paths stress time-dependent component motion.

stacked orbits      -> nested rotation composition
counter drift       -> opposing component fields
layered motion      -> multi-level folder transforms
diagonal sweep      -> repeated entity construction
in-place array      -> shared origin with varied transforms
square/star paths   -> scripted component trajectories
Scene coverage. Each scene maps to a specific engine behavior rather than only a visual variation.
St+Δt=R(Uglobal(Δt,Uscene(Δt,St)))S_{t+\Delta t} = R\left(U_{global}(\Delta t, U_{scene}(\Delta t, S_t))\right)
Frame update. A frame advances the active scene, applies any global update, then renders the resulting state.

The site integration adds a second verification surface. The canvas shows whether motion is visually correct; the runtime tree shows whether the underlying hierarchy and transforms explain that motion.

XIII

Tradeoffs and Scope

Rengine chooses a small, inspectable engine model over a feature-complete game framework. There is no asset pipeline, physics system, input abstraction, collision model, audio layer, or retained editor format. The reward for that narrowness is that every moving part in the demo can be traced to one of four ideas: entity state, folder composition, component updates, or renderer projection.

The canvas and React renderers are also intentionally asymmetric. Canvas is the better fit for wireframe markers and imperative drawing; React is useful for proving that the same entity state can become DOM. Keeping both paths exposes the renderer boundary, even though a production engine would likely commit harder to one primary target.

  • Folder entities make hierarchy simple, but they are not a full transform-node system.
  • Ordered component functions are easy to inspect, but they do not scale like a data-oriented ECS.
  • Debug overlays add visual noise, but they make anchor and position mistakes obvious.
  • The demo scenes act as regression examples for transform behavior rather than as finished game content.

XIV

Results and Design Properties

Rengine establishes a compact engine model with explicit scenes, entities, folder entities, component update functions, renderer abstraction, and live inspection. The result is a small but complete environment for validating hierarchical transforms and render-target boundaries in TypeScript.

The strongest result is observability. The same running scene can be inspected as pixels, as a runtime tree, as local transforms, as world transforms, and as component labels. The engine demonstrates transform and update semantics without relying on a large game layer.

  • Folder entities provide hierarchy without a separate transform-node type.
  • Components update entities through an explicit delta-time contract.
  • Canvas and React render paths share entity state while differing in projection target.
  • The runtime inspector makes transform composition auditable from the browser surface.