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
This commit is contained in:
@@ -182,7 +182,17 @@ Jedes Material bekommt deterministische Pixel-Variation basierend auf Grid-Koord
|
|||||||
|
|
||||||
### Player-System (Phase 4 — in Arbeit)
|
### 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.
|
**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
|
## Build & Entwicklung
|
||||||
|
|
||||||
@@ -215,6 +225,7 @@ cd web && npm run build
|
|||||||
| Cell + Chunk + Grid DS | `engine/src/grid.rs` |
|
| Cell + Chunk + Grid DS | `engine/src/grid.rs` |
|
||||||
| Material-Definitionen | `engine/src/material.rs` |
|
| Material-Definitionen | `engine/src/material.rs` |
|
||||||
| Physik: Kräfte, Sand, Fluide | `engine/src/physics.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` |
|
| RGBA-Buffer Export | `engine/src/render_buffer.rs` |
|
||||||
| Rust-Abhängigkeiten | `engine/Cargo.toml` |
|
| Rust-Abhängigkeiten | `engine/Cargo.toml` |
|
||||||
| WebGL2 Renderer | `web/src/renderer.ts` |
|
| WebGL2 Renderer | `web/src/renderer.ts` |
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
use crate::grid::Grid;
|
||||||
|
|
||||||
|
pub struct Actor {
|
||||||
|
pub x: f32,
|
||||||
|
pub y: f32,
|
||||||
|
pub w: u32,
|
||||||
|
pub h: u32,
|
||||||
|
pub vx: f32,
|
||||||
|
pub vy: f32,
|
||||||
|
pub color: [u8; 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Actor {
|
||||||
|
pub fn new(x: f32, y: f32, w: u32, h: u32, color: [u8; 3]) -> Self {
|
||||||
|
Self { x, y, w, h, vx: 0.0, vy: 0.0, color }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, grid: &Grid, dt: f32) {
|
||||||
|
const GRAVITY: f32 = 500.0;
|
||||||
|
self.vy += GRAVITY * dt;
|
||||||
|
self.vy = self.vy.clamp(-600.0, 600.0);
|
||||||
|
|
||||||
|
let new_x = (self.x + self.vx * dt).clamp(0.0, grid.width as f32 - self.w as f32);
|
||||||
|
let new_y = (self.y + self.vy * dt).clamp(0.0, grid.height as f32 - self.h as f32);
|
||||||
|
|
||||||
|
if self.vx != 0.0 && self.collides_at(grid, new_x, self.y) {
|
||||||
|
self.vx = 0.0;
|
||||||
|
} else {
|
||||||
|
self.x = new_x;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.collides_at(grid, self.x, new_y) {
|
||||||
|
if self.vy > 0.0 {
|
||||||
|
let mut sy = new_y;
|
||||||
|
while sy <= new_y + self.h as f32 && self.collides_at(grid, self.x, sy) {
|
||||||
|
sy -= 1.0;
|
||||||
|
}
|
||||||
|
self.y = sy;
|
||||||
|
} else {
|
||||||
|
let mut sy = new_y;
|
||||||
|
while sy >= new_y - self.h as f32 && self.collides_at(grid, self.x, sy) {
|
||||||
|
sy += 1.0;
|
||||||
|
}
|
||||||
|
self.y = sy;
|
||||||
|
}
|
||||||
|
self.vy = 0.0;
|
||||||
|
} else {
|
||||||
|
self.y = new_y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collides_at(&self, grid: &Grid, ax: f32, ay: f32) -> bool {
|
||||||
|
let x0 = ax.floor() as u32;
|
||||||
|
let y0 = ay.floor() as u32;
|
||||||
|
let x1 = ((ax + self.w as f32 - 0.01).ceil() as u32).min(grid.width);
|
||||||
|
let y1 = ((ay + self.h as f32 - 0.01).ceil() as u32).min(grid.height);
|
||||||
|
|
||||||
|
for gy in y0..y1 {
|
||||||
|
for gx in x0..x1 {
|
||||||
|
let mat = grid.get(gx, gy);
|
||||||
|
let props = &crate::material::PROPS[mat as usize];
|
||||||
|
if props.is_solid && !props.is_powder {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn displace_powders(&self, grid: &mut Grid) {
|
||||||
|
let x0 = self.x.floor() as u32;
|
||||||
|
let y0 = self.y.floor() as u32;
|
||||||
|
let x1 = ((self.x + self.w as f32).ceil() as u32).min(grid.width);
|
||||||
|
let y1 = ((self.y + self.h as f32).ceil() as u32).min(grid.height);
|
||||||
|
|
||||||
|
for gy in y0..y1 {
|
||||||
|
for gx in x0..x1 {
|
||||||
|
let mat = grid.get(gx, gy);
|
||||||
|
let props = &crate::material::PROPS[mat as usize];
|
||||||
|
if !props.is_powder { continue; }
|
||||||
|
let mut placed = false;
|
||||||
|
for dy in 0i32..=4i32 {
|
||||||
|
for dx in -4i32..=4i32 {
|
||||||
|
let sx = gx as i32 + dx;
|
||||||
|
let sy = gy as i32 + dy;
|
||||||
|
if !grid.in_bounds(sx, sy) { continue; }
|
||||||
|
let su = (sx as u32, sy as u32);
|
||||||
|
if su.0 >= x0 && su.0 < x1 && su.1 >= y0 && su.1 < y1 { continue; }
|
||||||
|
if grid.get(su.0, su.1) == 0 {
|
||||||
|
grid.set(su.0, su.1, mat);
|
||||||
|
let idx = grid.index(gx, gy);
|
||||||
|
grid.materials[idx] = 0;
|
||||||
|
placed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if placed { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+66
-2
@@ -1,12 +1,14 @@
|
|||||||
|
pub mod actors;
|
||||||
pub mod grid;
|
pub mod grid;
|
||||||
pub mod material;
|
pub mod material;
|
||||||
pub mod physics;
|
pub mod physics;
|
||||||
pub mod render_buffer;
|
pub mod render_buffer;
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
|
use actors::Actor;
|
||||||
use grid::Grid;
|
use grid::Grid;
|
||||||
use physics::Physics;
|
use physics::Physics;
|
||||||
use render_buffer::RenderBuffer;
|
use render_buffer::{ActorRect, RenderBuffer};
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
@@ -17,6 +19,7 @@ struct Engine {
|
|||||||
grid: Grid,
|
grid: Grid,
|
||||||
physics: Physics,
|
physics: Physics,
|
||||||
render_buffer: RenderBuffer,
|
render_buffer: RenderBuffer,
|
||||||
|
actor: Option<Actor>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(start)]
|
#[wasm_bindgen(start)]
|
||||||
@@ -41,6 +44,7 @@ pub fn init(width: u32, height: u32) {
|
|||||||
grid: Grid::new(width, height),
|
grid: Grid::new(width, height),
|
||||||
physics: Physics::new(),
|
physics: Physics::new(),
|
||||||
render_buffer: RenderBuffer::new(320, 180),
|
render_buffer: RenderBuffer::new(320, 180),
|
||||||
|
actor: None,
|
||||||
};
|
};
|
||||||
ENGINE.with(|cell| {
|
ENGINE.with(|cell| {
|
||||||
*cell.borrow_mut() = Some(engine);
|
*cell.borrow_mut() = Some(engine);
|
||||||
@@ -106,7 +110,19 @@ pub fn render_height() -> u32 {
|
|||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn update_render(cam_x: i32, cam_y: i32, zoom: f32) {
|
pub fn update_render(cam_x: i32, cam_y: i32, zoom: f32) {
|
||||||
with_engine(|e| e.render_buffer.render(&e.grid, cam_x, cam_y, zoom));
|
with_engine(|e| {
|
||||||
|
let mut actors = Vec::new();
|
||||||
|
if let Some(ref a) = e.actor {
|
||||||
|
actors.push(ActorRect {
|
||||||
|
x: a.x,
|
||||||
|
y: a.y,
|
||||||
|
w: a.w,
|
||||||
|
h: a.h,
|
||||||
|
color: (a.color[0], a.color[1], a.color[2]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
e.render_buffer.render(&e.grid, cam_x, cam_y, zoom, &actors);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
@@ -115,3 +131,51 @@ pub fn reset() {
|
|||||||
e.grid = Grid::new(e.grid.width, e.grid.height);
|
e.grid = Grid::new(e.grid.width, e.grid.height);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn spawn_actor(x: f32, y: f32) {
|
||||||
|
with_engine(|e| {
|
||||||
|
e.actor = Some(Actor::new(x, y, 10, 16, [255, 80, 80]));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn actor_x() -> f32 {
|
||||||
|
ENGINE.with(|cell| {
|
||||||
|
let opt = cell.borrow();
|
||||||
|
let engine = opt.as_ref().expect("Engine not initialized.");
|
||||||
|
engine.actor.as_ref().map(|a| a.x).unwrap_or(0.0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn actor_y() -> f32 {
|
||||||
|
ENGINE.with(|cell| {
|
||||||
|
let opt = cell.borrow();
|
||||||
|
let engine = opt.as_ref().expect("Engine not initialized.");
|
||||||
|
engine.actor.as_ref().map(|a| a.y).unwrap_or(0.0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn move_actor(dx: f32, dy: f32) {
|
||||||
|
with_engine(|e| {
|
||||||
|
if let Some(ref mut a) = e.actor {
|
||||||
|
a.vx = dx * 150.0;
|
||||||
|
if dy < 0.0 && a.vy == 0.0 {
|
||||||
|
a.vy = -400.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn update_actor(dt: f32) {
|
||||||
|
with_engine(|e| {
|
||||||
|
if let Some(ref mut a) = e.actor {
|
||||||
|
a.update(&e.grid, dt);
|
||||||
|
a.displace_powders(&mut e.grid);
|
||||||
|
a.vx = a.vx * 0.9;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -105,7 +105,7 @@ fn main() {
|
|||||||
|
|
||||||
loop {
|
loop {
|
||||||
physics.update(&mut grid, cam_x, cam_y, rw, rh, zoom);
|
physics.update(&mut grid, cam_x, cam_y, rw, rh, zoom);
|
||||||
rb.render(&grid, cam_x, cam_y, zoom);
|
rb.render(&grid, cam_x, cam_y, zoom, &[]);
|
||||||
frame += 1;
|
frame += 1;
|
||||||
|
|
||||||
if frame % 60 == 0 {
|
if frame % 60 == 0 {
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
use crate::grid::Grid;
|
use crate::grid::Grid;
|
||||||
use crate::material::PROPS;
|
use crate::material::PROPS;
|
||||||
|
|
||||||
|
pub struct ActorRect {
|
||||||
|
pub x: f32,
|
||||||
|
pub y: f32,
|
||||||
|
pub w: u32,
|
||||||
|
pub h: u32,
|
||||||
|
pub color: (u8, u8, u8),
|
||||||
|
}
|
||||||
|
|
||||||
pub struct RenderBuffer {
|
pub struct RenderBuffer {
|
||||||
pub pixels: Vec<u8>,
|
pub pixels: Vec<u8>,
|
||||||
pub width: u32,
|
pub width: u32,
|
||||||
@@ -24,7 +32,7 @@ impl RenderBuffer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(&mut self, grid: &Grid, cam_x: i32, cam_y: i32, zoom: f32) {
|
pub fn render(&mut self, grid: &Grid, cam_x: i32, cam_y: i32, zoom: f32, actors: &[ActorRect]) {
|
||||||
let rw = self.width as i32;
|
let rw = self.width as i32;
|
||||||
let rh = self.height as i32;
|
let rh = self.height as i32;
|
||||||
let total = (rw * rh) as usize;
|
let total = (rw * rh) as usize;
|
||||||
@@ -162,6 +170,27 @@ impl RenderBuffer {
|
|||||||
self.pixels[pi + 3] = color[3];
|
self.pixels[pi + 3] = color[3];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for actor in actors {
|
||||||
|
let ax = ((actor.x - cam_x as f32) * zoom + rw as f32 / 2.0) as i32;
|
||||||
|
let ay = ((actor.y - cam_y as f32) * zoom + rh as f32 / 2.0) as i32;
|
||||||
|
let aw = (actor.w as f32 * zoom).max(1.0) as u32;
|
||||||
|
let ah = (actor.h as f32 * zoom).max(1.0) as u32;
|
||||||
|
let (cr, cg, cb) = actor.color;
|
||||||
|
for dy in 0..ah.min(self.height) {
|
||||||
|
for dx in 0..aw.min(self.width) {
|
||||||
|
let spx = ax + dx as i32;
|
||||||
|
let spy = ay + dy as i32;
|
||||||
|
if spx >= 0 && spx < rw && spy >= 0 && spy < rh {
|
||||||
|
let pi = ((spy * rw + spx) * 4) as usize;
|
||||||
|
self.pixels[pi] = cr;
|
||||||
|
self.pixels[pi + 1] = cg;
|
||||||
|
self.pixels[pi + 2] = cb;
|
||||||
|
self.pixels[pi + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spread_light(&mut self, py: i32, px: i32, rw: i32, rh: i32) -> bool {
|
fn spread_light(&mut self, py: i32, px: i32, rw: i32, rh: i32) -> bool {
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ import __wbg_init, {
|
|||||||
render_buffer_len,
|
render_buffer_len,
|
||||||
render_width,
|
render_width,
|
||||||
render_height,
|
render_height,
|
||||||
|
spawn_actor,
|
||||||
|
actor_x,
|
||||||
|
actor_y,
|
||||||
|
move_actor,
|
||||||
|
update_actor,
|
||||||
} from "../pkg/atomic_engine.js";
|
} from "../pkg/atomic_engine.js";
|
||||||
|
|
||||||
export const M = {
|
export const M = {
|
||||||
@@ -79,6 +84,11 @@ export interface Engine {
|
|||||||
renderBufferLen: () => number;
|
renderBufferLen: () => number;
|
||||||
renderWidth: () => number;
|
renderWidth: () => number;
|
||||||
renderHeight: () => number;
|
renderHeight: () => number;
|
||||||
|
spawnActor: (x: number, y: number) => void;
|
||||||
|
actorX: () => number;
|
||||||
|
actorY: () => number;
|
||||||
|
moveActor: (dx: number, dy: number) => void;
|
||||||
|
updateActor: (dt: number) => void;
|
||||||
memory: WebAssembly.Memory;
|
memory: WebAssembly.Memory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,5 +106,10 @@ export async function createEngine(gridWidth: number, gridHeight: number): Promi
|
|||||||
renderWidth: () => render_width(),
|
renderWidth: () => render_width(),
|
||||||
renderHeight: () => render_height(),
|
renderHeight: () => render_height(),
|
||||||
memory: wasm.memory,
|
memory: wasm.memory,
|
||||||
|
spawnActor: (x, y) => spawn_actor(x, y),
|
||||||
|
actorX: () => actor_x(),
|
||||||
|
actorY: () => actor_y(),
|
||||||
|
moveActor: (dx, dy) => move_actor(dx, dy),
|
||||||
|
updateActor: (dt) => update_actor(dt),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-7
@@ -99,9 +99,9 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Spawn point: center on the castle ===
|
engine.spawnActor(400, 550);
|
||||||
camera.x = 400;
|
camera.x = 400;
|
||||||
camera.y = 630;
|
camera.y = 550;
|
||||||
|
|
||||||
const rw = engine.renderWidth();
|
const rw = engine.renderWidth();
|
||||||
const rh = engine.renderHeight();
|
const rh = engine.renderHeight();
|
||||||
@@ -184,11 +184,21 @@ async function main() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const speed = 200;
|
let mx = 0;
|
||||||
if (input.isDown("KeyA") || input.isDown("ArrowLeft")) camera.x -= speed * dt;
|
let my = 0;
|
||||||
if (input.isDown("KeyD") || input.isDown("ArrowRight")) camera.x += speed * dt;
|
if (input.isDown("KeyA") || input.isDown("ArrowLeft")) mx = -1;
|
||||||
if (input.isDown("KeyW") || input.isDown("ArrowUp")) camera.y -= speed * dt;
|
if (input.isDown("KeyD") || input.isDown("ArrowRight")) mx = 1;
|
||||||
if (input.isDown("KeyS") || input.isDown("ArrowDown")) camera.y += speed * dt;
|
if (input.isDown("KeyW") || input.isDown("ArrowUp")) my = -1;
|
||||||
|
engine.moveActor(mx, my);
|
||||||
|
engine.updateActor(dt);
|
||||||
|
|
||||||
|
const ax = engine.actorX();
|
||||||
|
const ay = engine.actorY();
|
||||||
|
if (ax > 0 || ay > 0) {
|
||||||
|
camera.x += (ax - camera.x) * 10 * dt;
|
||||||
|
camera.y += (ay - 60 - camera.y) * 10 * dt;
|
||||||
|
}
|
||||||
|
|
||||||
if (input.isDown("Equal")) camera.setZoom(camera.zoom + 2 * dt);
|
if (input.isDown("Equal")) camera.setZoom(camera.zoom + 2 * dt);
|
||||||
if (input.isDown("Minus")) camera.setZoom(camera.zoom - 2 * dt);
|
if (input.isDown("Minus")) camera.setZoom(camera.zoom - 2 * dt);
|
||||||
if (input.wheel !== 0) {
|
if (input.wheel !== 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user