Coordinate Systems
Coordinate systems in games: world vs screen vs local, the Y-axis fight, transforms, and the math that ports across engines.
Game dev — coordinate systems
EXAMPLE
// ===== Three frames you live in =====
// World space the global game origin (0,0) somewhere meaningful
// Local space relative to a parent (a child of the player rotates with the player)
// Screen space pixels on the canvas / viewport
// Converting between them is most of game math.
// ===== Y-axis: screen vs math =====
// Math: +Y up
// Screen: +Y DOWN (top-left origin)
// 3D engines vary: Unity +Y up, Unreal +Z up, Three.js +Y up, Godot +Y up (2D) / +Y up (3D)
// PICK ONE and stick to it. Translate at the rendering boundary if needed.
// ===== 2D world -> screen =====
// Camera at world position (cx, cy), screen size (sw, sh):
function worldToScreen(wx, wy, cx, cy, sw, sh) {
return { x: wx - cx + sw / 2, y: wy - cy + sh / 2 };
}
function screenToWorld(sx, sy, cx, cy, sw, sh) {
return { x: sx + cx - sw / 2, y: sy + cy - sh / 2 };
}
// ===== Vector basics =====
const v = (x, y) => ({ x, y });
const add = (a, b) => v(a.x + b.x, a.y + b.y);
const sub = (a, b) => v(a.x - b.x, a.y - b.y);
const mul = (a, s) => v(a.x * s, a.y * s);
const len = (a) => Math.hypot(a.x, a.y);
const norm = (a) => { const L = len(a) || 1; return v(a.x / L, a.y / L); };
const dot = (a, b) => a.x * b.x + a.y * b.y;
const cross = (a, b) => a.x * b.y - a.y * b.x; // 2D 'cross' returns scalar
// ===== Rotation (2D) =====
function rotate(p, angle) {
const c = Math.cos(angle), s = Math.sin(angle);
return v(p.x * c - p.y * s, p.x * s + p.y * c);
}
// ===== Transform a local point to world =====
// transform = { pos, rot, scale }
function localToWorld(local, t) {
const rotated = rotate(v(local.x * t.scale, local.y * t.scale), t.rot);
return add(rotated, t.pos);
}
// ===== Matrices (3D, conceptually) =====
// Translation * Rotation * Scale (T R S) is the common order.
// In WebGL: gl_Position = projection * view * model * vec4(localPos, 1).
// Three.js handles it via Object3D parent/child + matrix update.
// ===== Camera projection =====
// Perspective: foreshortened, near + far + fov
// Orthographic: parallel, top-down / side-scroller / UI
// Choose by gameplay needs, not by 'looks cool'.
// ===== Tile / grid coords =====
const tileSize = 32;
const tileFromWorld = (x, y) => v(Math.floor(x / tileSize), Math.floor(y / tileSize));
const worldFromTile = (tx, ty) => v(tx * tileSize, ty * tileSize);
// ===== Picking (mouse to world) =====
// 2D: invert the camera transform.
// 3D: raycast from the camera through the cursor pixel into the scene.
// Most engines provide raycasters; reach for them.
// ===== Patterns to internalise =====
// - Pick a single world convention; convert at boundaries
// - Use matrix stacks (or scene graphs) for nested transforms
// - Cache world transforms when nothing moves
// - Use named conversion helpers; do not inline the formulas
// ===== Pitfalls =====
// - Mixing radians + degrees (most APIs use radians)
// - Forgetting Y-flip when rendering math-style coords
// - Cumulative float error in long rotation chains -> renormalise periodically
// - Hard-coding screen size in conversions; pull from the viewport
Why it matters
World, local, screen — three frames, lots of converting. Pick a Y convention, lean on the engine scene graph for nested transforms, and write small named helpers (worldToScreen, localToWorld) rather than inlining matrix math. The same skeleton ports across 2D and 3D, across engines.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// 2D screen-space: x right, y down. // 3D world-space: many engines use y-up (Unity, Three.js) or z-up (Unreal, Godot 4). // Pick one and stay consistent.Try it Yourself »
Discussion
Loading…