Files
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

254 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` (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.
```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)