- 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
254 lines
12 KiB
Markdown
254 lines
12 KiB
Markdown
# 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)
|
||
|
||
```rust
|
||
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` (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
|
||
- 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 100–200 (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 (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).
|
||
|
||
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 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.
|
||
|
||
**Hintergrund-Gradient**: Blauer Himmel (`[80,140,220]`) von y=0–600, fließender Übergang zu dunkel (`[20,20,30]`) bei y=600–700. 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.
|
||
|
||
```bash
|
||
# 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
|
||
|
||
- [x] Phase 0: Projekt-Struktur + WASM/WebGL End-to-End ← **fertig**
|
||
- [x] Phase 1: Sand-Physik (Pixel fällt nach unten) ← **fertig** (Sand + Flüssigkeiten)
|
||
- [x] Phase 2: Mehrere Materialien + Kräfte-Tabelle ← **fertig**
|
||
- [x] 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)
|