--- id: game-loop-ecs title: Game Loop / ECS β€” λ§€ frame / Entity-Component-System category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [game, ecs, architecture, vibe-coding] tech_stack: { language: "TS / Various", applicable_to: ["Game", "Frontend"] } applied_in: [] aliases: [game loop, fixed timestep, ECS, entity-component-system, bevy, bitECS] --- # Game Loop / ECS > Game = λ§€ frame update + render. **Fixed timestep + interpolation**. 큰 game = ECS (Entity-Component-System). data-oriented + cache-friendly. ## πŸ“– 핡심 κ°œλ… - Frame: 16ms (60fps). - Fixed timestep: physics κ²°μ •μ„±. - ECS: Entity (id) + Component (data) + System (logic). - Data-oriented: cache locality. ## πŸ’» μ½”λ“œ νŒ¨ν„΄ ### κΈ°λ³Έ game loop ```ts let last = performance.now(); function frame(now: number) { const dt = (now - last) / 1000; last = now; update(dt); render(); requestAnimationFrame(frame); } requestAnimationFrame(frame); ``` β†’ `dt` κ°€ frame 차이. 30fps λ“  120fps λ“  같은 속도. ### Fixed timestep (physics) ```ts const FIXED = 1 / 60; let acc = 0; let last = performance.now(); function frame(now: number) { const dt = (now - last) / 1000; last = now; acc += dt; while (acc >= FIXED) { fixedUpdate(FIXED); // physics β€” 항상 같은 step acc -= FIXED; } const alpha = acc / FIXED; // interpolation render(alpha); requestAnimationFrame(frame); } ``` β†’ "Fix Your Timestep!" νŒ¨ν„΄. multiplayer / replay 일관. ### λ‹¨μˆœ entity 직접 ```ts class Player { x = 0; y = 0; vx = 0; vy = 0; hp = 100; update(dt: number) { this.x += this.vx * dt; this.y += this.vy * dt; } render(ctx: CanvasRenderingContext2D) { ctx.fillRect(this.x, this.y, 32, 32); } } const player = new Player(); const enemies = [...]; // Enemy[] function update(dt: number) { player.update(dt); for (const e of enemies) e.update(dt); } ``` β†’ μž‘μ€ κ²Œμž„ OK. 100+ entity = ECS. ### ECS β€” bitECS (TS, fast) ```ts import { createWorld, addEntity, addComponent, defineComponent, defineQuery, defineSystem, Types, pipe } from 'bitecs'; const world = createWorld(); const Position = defineComponent({ x: Types.f32, y: Types.f32 }); const Velocity = defineComponent({ vx: Types.f32, vy: Types.f32 }); const Sprite = defineComponent({ texture: Types.ui8 }); // Entity 생성 const player = addEntity(world); addComponent(world, Position, player); addComponent(world, Velocity, player); Position.x[player] = 100; Position.y[player] = 100; Velocity.vx[player] = 50; // System β€” Position + Velocity λ‘˜ λ‹€ κ°€μ§„ entity const moveQuery = defineQuery([Position, Velocity]); const moveSystem = defineSystem((world) => { const ents = moveQuery(world); for (const eid of ents) { Position.x[eid] += Velocity.vx[eid] * world.dt; Position.y[eid] += Velocity.vy[eid] * world.dt; } }); const renderQuery = defineQuery([Position, Sprite]); const renderSystem = defineSystem((world) => { for (const eid of renderQuery(world)) { drawSprite(Sprite.texture[eid], Position.x[eid], Position.y[eid]); } }); const pipeline = pipe(moveSystem, renderSystem); function frame(dt: number) { world.dt = dt; pipeline(world); } ``` β†’ Component = SoA (Struct of Arrays). Cache-friendly. ### Why ECS? ``` OOP class: class Enemy { x, y, hp, ... } - 상속 hierarchy 의 함정 (Enemy extends Character) - 같은 class 의 instance = memory λΆ„μ‚° ECS: Entity = id, Component = data only - μ–΄λ–€ component 쑰합도 κ°€λŠ₯ - Cache-friendly iteration - System = pure function on data ``` ### Bevy (Rust ECS, web κ°€λŠ₯) ```rust use bevy::prelude::*; fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) .add_systems(Update, (movement, collision)) .run(); } #[derive(Component)] struct Velocity(Vec2); fn movement(mut q: Query<(&mut Transform, &Velocity)>, time: Res