Murmur: ECS-Based Emergent Simulation

Murmur: ECS-Based Emergent Simulation

Overview

Murmur explores how modern systems achieve performance and scale through data-oriented design. Rather than building agents as individual objects, Murmur organizes thousands of entities and their behaviors into a data-first architecture inspired by Entity-Component-System (ECS) patterns.

The project demonstrates how runtime performance emerges from architectural choices, how to measure and reason about bottlenecks in real-time systems, and how the same design patterns apply across domains, from game engines to web simulations to distributed platforms.

Why This Matters

Most simulation systems begin with object-oriented thinking: "I'll create an Agent class, give each agent behavior, and instantiate 5000 of them."

This approach works until performance becomes the constraint. At 5000 agents, the overhead of object-oriented design becomes visible: cache misses, pointer chasing, method dispatch overhead, all multiplied across thousands of instances.

ECS inverts this approach: organize data first, then write systems that operate on that data efficiently.

The target is a system capable of simulating 5000 agents at 60 FPS in a browser, while remaining deterministic, debuggable, and observable.

Murmur explores these principles by building a flocking simulation from scratch. No physics engine, no agent library. Instead: raw ECS, custom spatial hashing, and direct measurement of where CPU cycles go.

What I'll Learn

By building Murmur, I'll better understand:

Data-Oriented Design:

  • Why data layout matters for performance (cache efficiency, memory locality)
  • How to reorganize code around data rather than objects
  • When and why ECS patterns apply

Real-Time System Performance:

  • How to identify bottlenecks (physics vs render)
  • How architectural choices directly impact frame rate
  • How to measure and reason about performance constraints

Spatial Algorithms:

  • How spatial hashing works and why it's essential
  • When to use spatial partitioning vs brute force
  • How to tune data structures for specific problems

Web Graphics and Rendering:

  • How instanced rendering works (one draw call for many entities)
  • How to efficiently update geometry data each frame
  • How GPU and CPU bottlenecks differ

ECS Across Contexts:

  • How ECS patterns exist in game engines, web systems, and beyond
  • How the same design principles scale across domains
  • How to implement ECS without relying on a specific framework

Core Idea

Rather than agents being objects, they are pure data:

Entity 0: position=[100, 200], velocity=[2, 1], maxSpeed=5 Entity 1: position=[105, 205], velocity=[-1, 3], maxSpeed=5 Entity 2: position=[110, 210], velocity=[0, -2], maxSpeed=5 ... Entity 4999: position=[...], velocity=[...], maxSpeed=[...]

Then systems operate on these data arrays:

SpatialHashSystem: Build a grid of agent positions SteeringSystem: For each agent, find neighbors, calculate steering force MovementSystem: Apply velocity to position RenderSystem: Draw all agents

Each system processes all entities of that type in a tight loop. This is cache-friendly and scales linearly with agent count.

Architecture Walkthrough

The System Schedule

Every frame follows a deterministic order. This ensures correct behavior and reproducibility:

Frame N: 1. Spatial Hash System - Clear grid - Assign each agent to its grid cell - Build neighbor lookup structure 2. Steering System - For each agent: - Query spatial hash: "Who are my neighbors?" - Calculate separation (avoid others) - Calculate alignment (match velocity) - Calculate cohesion (move toward group center) - Apply mouse attractor force - Clamp velocity to maxSpeed 3. Movement System - For each agent: - Apply velocity to position 4. Render System - Send agent positions to GPU - Render all agents in one instanced draw call - Display performance metrics Frame N+1: (Repeat in same order)

Why this order? Movement must happen before the next frame's spatial hash rebuild. The hash grid describes positions after movement, so steering the next frame is based on current positions.

How Data Flows

Step 1: Spatial Hashing

The world is divided into a grid. Each cell is a bucket:

Grid (10x10): ┌─────┬─────┬─────┐ │ [0] │ [1] │ [2] │ Cell [0] contains agents {5, 12, 18} ├─────┼─────┼─────┤ Cell [1] contains agents {3, 7} │ [3] │ [4] │ [5] │ Cell [4] contains agents {1, 2, 9, 14, 22} ├─────┼─────┼─────┤ │ [6] │ [7] │ [8] │ └─────┴─────┴─────┘

For each agent, you calculate which cell it occupies:

cellX = Math.floor(position.x / cellSize); cellY = Math.floor(position.y / cellSize); cellIndex = cellY * gridWidth + cellX; grid[cellIndex].push(agentId);

This is O(n). You visit each agent once and place it in a cell.

Step 2: Steering Queries

When Agent 42 needs to find neighbors, instead of checking all 5000 agents:

// Naive: O(n²) for (let i = 0; i < 5000; i++) { distance = distance(agent42, allAgents[i]); if (distance < radius) { /* neighbor */ } } // Spatial hash: O(1) cell = getCellForAgent(agent42); neighbors = grid[cell] + grid[adjacent cells]; for (neighbor of neighbors) { /* ~10-20 checks */ }

The spatial hash reduces neighbor checks from a complexity of O(n²) to O(n). The exact number of comparisons per agent depends on grid resolution and cell occupancy.

Step 3: Steering Calculation

For each agent, calculate three forces:

  • Separation: Move away from neighbors that are too close
  • Alignment: Match the average velocity of neighbors
  • Cohesion: Move toward the center of the neighbor group
  • Attraction: Move toward the mouse cursor (user-controlled)

Apply these forces to velocity, clamp to maxSpeed, store the result.

Step 4: Movement

Apply velocity to position

for (let i = 0; i < numAgents; i++) { positions[i] += velocities[i]; }

All contiguous. Cache-friendly. Fast.

Step 5: Rendering and Measurement

Send positions to GPU via instanced rendering and measure performance:

const physicsTime = measureTime(() => { spatialHashSystem.update(); steeringSystem.update(); movementSystem.update(); }); const renderTime = measureTime(() => { renderer.render(scene, camera); }); const fps = 1000 / (physicsTime + renderTime); displayMetrics({ physicsTime, renderTime, fps });

What Performance Teaches

By exposing physics time, render time, and FPS, users see exactly where bottlenecks appear:

  • Add 1000 agents: Physics time increases slightly, FPS drops slightly
  • Add 5000 agents: Physics time dominates, render time stays flat (GPU is efficient)
  • Change grid size: Watch spatial hash efficiency change as cell sizes vary

This is observable system behavior. Users see cause and effect.

Tech Stack

JavaScript and TypeScript (Runtime)

JavaScript is the browser's native language. You can build real-time, data-intensive systems in modern JS. Performance-critical code runs in tight loops where JavaScript's JIT compiler makes it competitive with compiled languages for this workload.

More importantly, this demonstrates you can build high-performance systems across different technology stacks, not just game engines, but web platforms.

Trade-off: JavaScript is slower than compiled languages (C++, Rust), but proper data structures (contiguous arrays, spatial hashing) matter more than language choice for this scale.

three.js (Rendering)

three.js is the industry standard WebGL wrapper. It abstracts away verbose WebGL boilerplate while exposing the features you need: geometry, instanced rendering, camera control, scene management.

It integrates seamlessly with JavaScript and handles the graphics pipeline cleanly.

Alternative considered: Raw WebGL would give you lower-level control but requires 10x more code for no benefit here. Babylon.js would be equally valid.

Trade-off: Abstraction over raw graphics APIs, but you retain control over rendering strategy (instanced draws, buffer updates).

ECS Architecture (System Design)

I have worked with Unity's ECS framework in game development. But understanding ECS fundamentally, not just as a framework, requires implementing it from scratch in a different context.

By building ECS in a web environment without an engine, you see:

  • Why data layout matters (cache efficiency)
  • Why system ordering is critical (determinism)
  • How to measure performance impact of architectural choices

This understanding transfers to any system: game engines, distributed platforms, simulations, etc.

What ECS provides:

  • Entities (pure data, indexed by ID)
  • Components (data arrays: positions, velocities, behaviors)
  • Systems (logic that operates on component data)
  • Deterministic scheduling (systems run in a fixed order)

Spatial Hashing (Data Structure)

Spatial hashing is a fundamental data structure for any system dealing with proximity queries: simulations, graphics, physics, game engines, robotics.

Understanding it deeply, not as a black box library, means you can:

  • Recognize when neighbor queries are your bottleneck
  • Tune grid resolution for your specific case
  • Adapt the pattern to different problems (2D flocking, 3D particles, large-scale simulations)

For Murmur specifically, spatial hashing reduces neighbor queries from O(n²) to O(n), making 5000 agents feasible.

Implementation: A simple grid (2D array) and hash function to map positions to cells. Roughly 100 lines of code.

performance.now() API (Profiling)

Real-time systems are only real-time if you measure them. By instrumenting the code with performance.now() calls, you make bottlenecks visible.

Users can see: "Adding 1000 agents added 2ms to physics time" or "Render time is capped by GPU bandwidth."

This is a teaching tool as much as a diagnostic tool.

What you measure:

  • Physics time (spatial hashing plus steering plus movement)
  • Render time (GPU operations)
  • Frame time (total)
  • FPS (derived from frame time)

Scalability and Interactivity

Murmur includes real-time control:

  • Agent count slider: Add or remove agents instantly, watch performance metrics update
  • Mouse as attractor: Click and drag to steer the flock in real-time
  • Performance metrics display: See physics time, render time, and FPS live

This makes the relationship between architecture and performance tangible and immediate.

Status

Designed.

Architecture, data flow, and tech stack are documented and understood. Implementation can begin with:

  1. ECS core structure (entities, components, systems)
  2. Spatial hashing grid implementation
  3. Steering behavior system (separation, alignment, cohesion)
  4. three.js rendering and instanced draw
  5. Performance measurement and metrics display
  6. Mouse attractor interaction

What This Sets Up

Murmur establishes the execution plane. How to build high-performance runtime behavior in web systems. Once complete, it becomes the simulation engine that Zephyr controls through Nimbus's governance layer, demonstrating how architecture decisions directly reshape observable behavior.