- Acid now uses acid_resist + health-based damage (not instant delete) - Acid dissolve speeds: Wood ~2s, Dirt ~2.8s, Stone ~4.3s, Iron immune - New material: Iron (ID 16) - silver, static, acid-proof, heat-conductive - Iron texture: bright highlights for metallic shine - Iron reflects light (light_block: 150) - Iron heat_conduct: 200 - conducts heat quickly through entire block - Improved heat transfer formula (divisor 255→200) - Updated AGENTS.md status - No player code (rolled back for fresh start)
11 KiB
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).
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_resistlight_emit(0–255, wie viel Licht das Material abstrahlt)light_block(0–255, 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
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 + Holz/Sand → zerstört Holz/Sand
- Säure + Stein/Erde → zerstört Stein/Erde (langsamer)
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/flamFrames 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 (4–12 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 (4–12 Frames Lebensdauer)
Ephemere Partikel (Funken + Rauch):
update_ephemeral: dekrementiert Health, bei 0 → Luft- Flammen-Funken: 4–12 Frames, 1/5 Emission, kurzlebig für Flammenspitzen
- Funken: 30–80 Frames Lebensdauer, orange, 1/16 Emission/Frame
- Rauch: 140–220 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).
- update_fire — Feuer-Update: Brennstoff-Verbrauch, Ausbreitung, Funken/Rauch-Emission, Flammen-Züngeln
- 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!)
- update_gases (top→bottom, nur jedes 2. Frame, nur aktive Region): Gas + Flammen-Funken steigen max 1px/Frame
- 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
- WASM schreibt sichtbaren Bereich als RGBA-Buffer
- Buffer als Uint8Array aus WASM-Speicher gelesen
- WebGL2 lädt als Textur → Fullscreen-Quad
- 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:
- Emissions-Pass: Jedes Pixel mit
light_emit > 0strahlt Licht - Flood-Fill (max
16/zoomPasses, early-termination): 8-Richtungs-Propagation- Falloff:
(2|3)/zoom(kardinal/diagonal) +light_block(gecapped bei 200) - Opaque Pixel blocken Weitergabe, werden aber selbst beleuchtet
- Falloff:
- Abwechselnde Scan-Richtung für gleichmäßige Verteilung
- 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 1–179) 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.
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)
Player-Material (7) existiert als grüner Pixel-Cluster. move_player(dx, dy)-API in physics.rs via WASM exportiert (engine.movePlayer). Aktueller Ansatz: Clear+Place (alle Player-Zellen löschen, an Zielposition neu setzen), all-or-nothing bei Kollision mit Solids. Gravitation: dy=1 jede Frame. Horizontal-Input: Pfeiltasten separat. Kamera: folgt Player-Zentrum (Scan 200×200 Region um letzte Position, cached). Offen: Cluster-Form stabil halten (kreisrund), Sprung-Mechanik, Schadenssystem.
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 |
| 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)