Files
atomic-engine/AGENTS.md
T
nico f5d16ffe64 v0.6: Actor system (rigidbody) replaces pixel player
- New actors.rs: Rigidbody Actor with gravity, grid collision, powder displacement
- Actor rendered as colored rectangle on pixel buffer (zoom-scaled)
- WASM API: spawn_actor, move_actor, update_actor, actor_x/y
- WASD/Arrow keys control actor, camera follows with soft lerp
- displace_powders pushes sand/dirt out of actor area
- Collision: stops at solids, walks through powders
- Updated AGENTS.md with actor documentation
2026-07-10 16:09:58 +02:00

12 KiB
Raw Blame History

AtomicEngine

Eine 2D-Platformer-Gameengine bei der jeder Pixel wie ein Atom mit eigenen Eigenschaften und Anziehungskräften zu seinen Nachbarn agiert. Browser-basiert mit Rust/WASM für die Physik und WebGL2 fürs Rendering.

Architektur-Übersicht

engine/  (Rust → WASM)          web/  (TypeScript + Vite)
┌─────────────────────┐    ┌──────────────────────┐
│ grid.rs              │    │ main.ts              │
│ material.rs          │    │ renderer.ts (WebGL2) │
│ physics.rs           │    │ input.ts             │
│ render_buffer.rs     │    │ camera.ts            │
│ lib.rs (wasm API)    │    │                      │
└─────────────────────┘    └──────────────────────┘
         │                           │
         └── SharedArrayBuffer ──────┘

Tech-Stack

Schicht Technologie Warum
Physik Rust → WASM Performance, Sicherheit
WASM Bridge wasm-bindgen, wasm-pack Auto JS-Bindings
Renderer WebGL2 (Canvas) GPU-beschleunigt, 60fps
Web Shell TypeScript, Vite Build, Dev-Server, HMR
Speicher SharedArrayBuffer Zero-copy WASM ↔ JS

Welt-Modell

  • Große Welt: 4096×3072 Pixel (16×12 Chunks à 256×256, tiled 4×4 Szenen)
  • Nur Zellen im Sichtfeld + 200px Margin werden simuliert
  • Rest eingefroren — keine CPU-Kosten für unsichtbare Bereiche
  • Grid wächst auf 4096×3072 = 12.5M Zellen (~48 MB Speicher)

Datenstruktur Cell (pro Pixel)

struct Cell {
    material: u8,  // Material-ID (0 = Luft)
    health: u8,    // Lebenspunkte/Dichte
    temp: u8,      // Temperatur
    flags: u8,     // Bitflags (aktiv, Player, Flüssig, ...)
}

Grid ist SoA (Structure of Arrays):

  • Vec<u8> für materials, healths, temps, flags
  • Breite × Höhe = Chunk-Größe

Material-System

16 Materialien (0=Luft, 1=Stein, 2=Erde, 3=Sand, 4=Wasser, 5=Holz, 6=Lava, 7=Player, 8=Dampf, 9=Feuer, 10=Glas, 11=Eis, 12=Öl, 13=Säure, 14=Funken, 15=Rauch, 16=Eisen).

Properties pro Material: MaterialProps in engine/src/material.rs:

  • color, density, Bewegung-Typ (is_powder/is_liquid/is_gas/is_solid/is_static)
  • flammability, melt_temp, boil_temp, heat_conduct, acid_resist
  • light_emit (0255, wie viel Licht das Material abstrahlt)
  • light_block (0255, wie stark das Material Licht blockiert)

Interaktions-System

Heat-Transfer: Temperatur-Diffusion über Moore-Nachbarn (heat_conduct-abhängig).

Phase-Transformationen (material::transform):

  • Eis (11) → Wasser (4) bei temp ≥ 5°C
  • Wasser (4) → Dampf (8) bei temp ≥ 100°C
  • Dampf (8) → Wasser (4) bei temp ≤ 80°C
  • Lava (6) → Stein (1) bei temp ≤ 5°C
  • Holz (5) → Feuer (9) bei temp ≥ 240°C
  • Öl (12) → Feuer (9) bei temp ≥ 120°C
  • Sand (3) → Glas (10) bei temp ≥ 200°C
  • Glas (10) → Lava (6) bei temp ≥ 160°C
  • Eisen (16) → Lava (6) bei temp ≥ 150°C
  • Stein schmilzt nicht (hält Lava auf)

Reaktionen (physics::react):

  • Wasser + Lava → Dampf + Stein
  • Wasser + Feuer → Dampf + Luft (löscht Feuer)
  • Lava + Holz/Öl → entzündet Holz/Öl sofort zu Feuer
  • Säure greift ALLE Materialien via acid_resist an (Health-basiert, nicht Instant-Delete):
    • resist < 100 (Holz 50, Erde 70): ~2s/Zelle
    • resist 100200 (Sand 150, Stein 180): ~4s/Zelle
    • resist ≥ 200 (Eis, Öl): ~13s/Zelle
    • resist = 255 (Eisen, Glas, Wasser, Lava): immun

Feuer-System:

  • Braucht Brennstoff (Holz, Öl) um zu überleben — prüft direkt unter sich + Moore-Nachbarn
  • Feuer-Säule: Hat eine Zelle Feuer unter sich, gilt sie als versorgt (Ketten-Propagation)
  • Mit Brennstoff: keine Health-Decay
  • Ohne Brennstoff: 6hp/Frame → erlischt in ~7 Frames (~0.1s)
  • Brenn-Rate flammability-abhängig: 300/flam Frames zwischen 1hp-Konsum
    • Holz (30): ~10 Frames/Konsum → ~42s pro Block
    • Öl (80): ~3 Frames/Konsum → ~13s pro Block
  • Ausbreitung: 1/250 Chance auf brennbare Nachbarn
  • Wasser/Eis in Nachbarschaft → Feuer erlischt sofort
  • Erzeugt Funken (14) und Rauch (15) beim Brennen
  • Flammen-Züngeln: 1/5 Chance pro Zelle, Funken-Partikel nach oben zu feuern (412 Frames Lebensdauer)
  • Liquid-Momentum: FLAG_MOMENTUM auf fallendem/sloshendem Wasser, waves klettern an Schalenwänden hoch
  • Flammen-Züngeln: 1/5 Chance pro Zelle, Funken-Partikel nach oben zu feuern (412 Frames Lebensdauer)

Ephemere Partikel (Funken + Rauch):

  • update_ephemeral: dekrementiert Health, bei 0 → Luft
  • Flammen-Funken: 412 Frames, 1/5 Emission, kurzlebig für Flammenspitzen
  • Funken: 3080 Frames Lebensdauer, orange, 1/16 Emission/Frame
  • Rauch: 140220 Frames, hellgrau transparent, 1/6 Emission/Frame
  • Gas-Pass separat (alle 2 Frames, top→bottom = max 1px/Frame Aufstieg)

Physik-Loop (pro Frame)

Reihenfolge in physics::update(cam_x, cam_y, rw, rh, zoom):

Nur Zellen innerhalb Kamera-Sichtfeld + 200px Margin werden simuliert. Rest der Welt ist eingefroren (keine CPU-Kosten).

  1. update_fire — Feuer-Update: Brennstoff-Verbrauch, Ausbreitung, Funken/Rauch-Emission, Flammen-Züngeln
  2. Haupt-Loop (bottom→top, alternierende Spalten, nur aktive Region):
    • Heat-Transfer über Moore-Nachbarn
    • Material-Reaktionen (Water+Lava, Säure+Holz, ...)
    • Phasen-Transformation (Temp-basiert)
    • Bewegung: Powders ↓, Liquids ↓↔, Solids ↓ (kein Gas!)
  3. update_gases (top→bottom, nur jedes 2. Frame, nur aktive Region): Gas + Flammen-Funken steigen max 1px/Frame
  4. update_ephemeral (nur aktive Region): Spark/Smoke Health dekrementieren, bei 0 → Luft

Level-Format

RGBA-PNG, jeder Pixel = 1 Atom:

  • R = Material-ID (0=Luft)
  • G = Dichte/Gesundheit
  • B = Temperatur
  • A = Flags

Levels in Aseprite/Photoshop malbar. Große Welten = Raster von PNG-Dateien.

Rendering

  1. WASM schreibt sichtbaren Bereich als RGBA-Buffer
  2. Buffer als Uint8Array aus WASM-Speicher gelesen
  3. WebGL2 lädt als Textur → Fullscreen-Quad
  4. Kamera-Matrix für Scroll/Zoom

Textur-System

Jedes Material bekommt deterministische Pixel-Variation basierend auf Grid-Koordinaten:

  • Holz: Vertikale Maserung (dünne Fasern alle ~5px, ±1px Welle)
  • Erde: Grobkörnige Flecken (niederfrequentes Rauschen, ±20 RGB)
  • Stein/Glas: Subtiles Rauschen (±10 RGB)
  • Sand: Feines Granulat (±6 RGB)
  • Eis: Minimales Rauschen (±5 RGB)
  • Flüssigkeiten/Gase: Keine Textur
  • Feuer/Lava: Kanal-getrennte Varianz (R ±6, G ±12, B ±8) — natürliche Gelb-/Orange-Mischung

Beleuchtungs-System

Light-Propagation in render_buffer.rs:

  1. Emissions-Pass: Jedes Pixel mit light_emit > 0 strahlt Licht
  2. Flood-Fill (max 16/zoom Passes, early-termination): 8-Richtungs-Propagation
    • Falloff: (2|3)/zoom (kardinal/diagonal) + light_block (gecapped bei 200)
    • Opaque Pixel blocken Weitergabe, werden aber selbst beleuchtet
  3. Abwechselnde Scan-Richtung für gleichmäßige Verteilung
  4. Zoom-skaliert: Passes + Falloff passen sich an Kamera-Zoom an

Lichtquellen: Feuer (255), Lava (200), Funken (180)

Glow-Effekt: Luft-Pixel mit Licht blenden von dunklem Hintergrund zu warmem Orange — quadratische Kurve (t = lvl²/255) für natürlichen Abfall. Nur direkt an der Quelle stark sichtbar.

Reflexion: Semi-transparente Materialien (Glas, Wasser, Eis — block 1179) werfen 50% des empfangenen Lichts an Nachbar-Pixel zurück → indirekte Beleuchtung.

Beleuchtungs-Formel (Material-Pixel): Additiv: r = min(255, r × 150/256 + light) — Ambient-Basis 59% + Licht obendrauf.

Hintergrund-Gradient: Blauer Himmel ([80,140,220]) von y=0600, fließender Übergang zu dunkel ([20,20,30]) bei y=600700. Untergrund ab y=700 dunkel.

Schatten: Experimentell versucht (Platform-Schatten, Sonnen-Raycasting), aktuell ausgebaut. Neu-Ansatz für nächste Session.

UI

  • Material-Palette: Leiste am unteren Bildschirmrand, Farb-Swatch + Name + Taste
  • FPS-Anzeige: Oben rechts, grün, alle 500ms aktualisiert

Player-System (Phase 4 — in Arbeit)

Aktueller Ansatz: Rigidbody-Actor-System statt Pixel-Player.

  • engine/src/actors.rs: Actor-Struct (x, y, w, h, vx, vy, color)
  • Gravitation (500px/s²), Geschwindigkeits-Dämpfung (0.9×/Frame)
  • Grid-Kollision: collides_at() prüft Solids (nicht Powders), seitliches Stoppen + vertikales Snap an Oberflächen
  • displace_powders(): Verschiebt Sand/Erde aus dem Actor-Bereich in leere Nachbarzellen
  • WASM-API: spawn_actor, move_actor, update_actor, actor_x/y
  • Rendering: Actor als farbiges Rechteck über dem Pixel-Buffer (zoom-skaliert)
  • Input: WASD/Pfeiltasten, Kamera folgt Actor weich
  • Player-Material (7) existiert weiterhin, wird aktuell nicht genutzt

Offen: Mehrere Actors, Maus-Selektion, Pathfinding (A*), Task-System

Build & Entwicklung

WICHTIG: Native Debug-Binary und WASM/Web-Build müssen immer identisch laufen. Der Debug-Build nutzt dieselben Module (grid, material, physics, render_buffer) und dieselben Dimensionen (256×192 Grid, 320×180 Render-Buffer). Performance-Unterschiede sind WASM-Overhead, nicht Logik-Abweichungen. Bei Änderungen an der Engine immer beide Builds testen.

# Rust → WASM bauen
cd engine && wasm-pack build --target web --out-dir ../web/pkg

# Native Debug-Binary (x86, ohne WASM)
cd engine && cargo run --bin atomic-debug --release

# Web Dev-Server
cd web && npm run dev

# Produktion
cd web && npm run build

Datei-Index (was findet man wo)

Was Datei
WASM öffentliche API engine/src/lib.rs
Native Debug Binary engine/src/main.rs
Cell + Chunk + Grid DS engine/src/grid.rs
Material-Definitionen engine/src/material.rs
Physik: Kräfte, Sand, Fluide engine/src/physics.rs
Actor-System (Rigidbody) engine/src/actors.rs
RGBA-Buffer Export engine/src/render_buffer.rs
Rust-Abhängigkeiten engine/Cargo.toml
WebGL2 Renderer web/src/renderer.ts
WASM-Bridge + Material-Konstanten web/src/engine.ts
Game-Loop, Wiring web/src/main.ts
Input (Keyboard, Maus) web/src/input.ts
Kamera (Scroll, Zoom) web/src/camera.ts
HTML Einstieg web/index.html
Vite Konfiguration web/vite.config.ts
TypeScript Konfiguration web/tsconfig.json

Aktueller Status

  • Phase 0: Projekt-Struktur + WASM/WebGL End-to-End ← fertig
  • Phase 1: Sand-Physik (Pixel fällt nach unten) ← fertig (Sand + Flüssigkeiten)
  • Phase 2: Mehrere Materialien + Kräfte-Tabelle ← fertig
  • Phase 3: Chunk-System + große Welt + Kamera ← fertig
  • Phase 4: Spieler als Atom-Cluster + Input ← angefangen — Player-Material (7) existiert, move_player(dx,dy) API da, aber Cluster-Zusammenhalt + Steuerung noch instabil. Ansatz: Player-Zellen als Gruppe bewegen (clear+place, nicht swap), Gravitation separat von Horizontalbewegung, Kamera folgt Player-Zentrum.
  • Phase 5: Erweiterungen (Items, Player-Interaktion, Editor, ...)

Namenskonventionen

  • Rust: snake_case, English
  • TypeScript: camelCase, English
  • Keine Kommentare im Code (nur wenn explizit gewünscht)