pxl8/pxl8d/src/sim.rs

278 lines
7.8 KiB
Rust
Raw Normal View History

2026-01-25 09:26:30 -06:00
extern crate alloc;
use alloc::vec::Vec;
use libm::{cosf, sinf, sqrtf};
use crate::math::Vec3;
use crate::protocol::*;
use crate::voxel::VoxelWorld;
use crate::world::World;
const ALIVE: u32 = 1 << 0;
const PLAYER: u32 = 1 << 1;
const GROUNDED: u32 = 1 << 2;
const MAX_ENTITIES: usize = 1024;
#[derive(Clone, Copy)]
pub struct Entity {
pub flags: u32,
pub kind: u16,
pub pos: Vec3,
pub vel: Vec3,
pub yaw: f32,
pub pitch: f32,
}
impl Default for Entity {
fn default() -> Self {
Self {
flags: 0,
kind: 0,
pos: Vec3::ZERO,
vel: Vec3::ZERO,
yaw: 0.0,
pitch: 0.0,
}
}
}
pub struct Simulation {
pub entities: Vec<Entity>,
pub free_list: Vec<u32>,
pub player: Option<u32>,
pub tick: u64,
pub time: f32,
pub voxels: VoxelWorld,
pub world: World,
}
impl Simulation {
pub fn new() -> Self {
let mut entities = Vec::with_capacity(MAX_ENTITIES);
entities.resize(MAX_ENTITIES, Entity::default());
let mut free_list = Vec::with_capacity(MAX_ENTITIES);
for i in (0..MAX_ENTITIES).rev() {
free_list.push(i as u32);
}
Self {
entities,
free_list,
player: None,
tick: 0,
time: 0.0,
voxels: VoxelWorld::new(12345),
world: World::new(12345),
}
}
pub fn spawn(&mut self) -> Option<u32> {
let idx = self.free_list.pop()?;
self.entities[idx as usize] = Entity {
flags: ALIVE,
..Default::default()
};
Some(idx)
}
pub fn despawn(&mut self, id: u32) {
if (id as usize) < self.entities.len() {
self.entities[id as usize].flags = 0;
self.free_list.push(id);
}
}
pub fn spawn_player(&mut self, x: f32, y: f32, z: f32) -> u32 {
let id = self.spawn().unwrap_or(0);
let ent = &mut self.entities[id as usize];
ent.flags = ALIVE | PLAYER;
ent.pos = Vec3::new(x, y, z);
self.player = Some(id);
self.voxels.load_chunks_around(ent.pos, 2);
id
}
pub fn step(&mut self, inputs: &[pxl8_input_msg], dt: f32) {
self.tick += 1;
self.time += dt;
for input in inputs {
self.move_player(input, dt);
}
self.integrate(dt);
}
fn trace(&self, from: Vec3, to: Vec3, radius: f32) -> Vec3 {
if self.world.active().is_some() {
return self.world.trace(from, to, radius);
}
self.voxels.trace(from, to, radius)
}
fn integrate(&mut self, dt: f32) {
const GRAVITY: f32 = 800.0;
const FRICTION: f32 = 6.0;
const RADIUS: f32 = 16.0;
for i in 0..self.entities.len() {
let ent = &self.entities[i];
if ent.flags & ALIVE == 0 || ent.flags & PLAYER != 0 {
continue;
}
let mut vel = ent.vel;
let pos = ent.pos;
let flags = ent.flags;
vel.y = vel.y - GRAVITY * dt;
if flags & GROUNDED != 0 {
let speed = sqrtf(vel.x * vel.x + vel.z * vel.z);
if speed > 0.0 {
let drop = speed * FRICTION * dt;
let scale = (speed - drop).max(0.0) / speed;
vel.x = vel.x * scale;
vel.z = vel.z * scale;
}
}
let target = pos + vel * dt;
let new_pos = self.trace(pos, target, RADIUS);
let ent = &mut self.entities[i];
ent.vel = vel;
ent.pos = new_pos;
}
}
fn move_player(&mut self, input: &pxl8_input_msg, dt: f32) {
let Some(id) = self.player else { return };
let ent = &mut self.entities[id as usize];
if ent.flags & ALIVE == 0 {
return;
}
const MOVE_SPEED: f32 = 320.0;
const GROUND_ACCEL: f32 = 10.0;
const AIR_ACCEL: f32 = 1.0;
const STOP_SPEED: f32 = 100.0;
const FRICTION: f32 = 6.0;
const RADIUS: f32 = 16.0;
let sin_yaw = sinf(input.yaw);
let cos_yaw = cosf(input.yaw);
let input_len = sqrtf(input.move_x * input.move_x + input.move_y * input.move_y);
let (move_dir, target_speed) = if input_len > 0.0 {
let nx = input.move_x / input_len;
let ny = input.move_y / input_len;
let dir = Vec3::new(
cos_yaw * nx - sin_yaw * ny,
0.0,
-sin_yaw * nx - cos_yaw * ny,
);
(dir, MOVE_SPEED)
} else {
(Vec3::ZERO, 0.0)
};
let grounded = ent.flags & GROUNDED != 0;
if grounded {
let speed = sqrtf(ent.vel.x * ent.vel.x + ent.vel.z * ent.vel.z);
if speed > 0.0 {
let control = speed.max(STOP_SPEED);
let drop = control * FRICTION * dt;
let scale = (speed - drop).max(0.0) / speed;
ent.vel.x *= scale;
ent.vel.z *= scale;
}
}
if target_speed > 0.0 {
let accel = if grounded { GROUND_ACCEL } else { AIR_ACCEL };
let current = ent.vel.x * move_dir.x + ent.vel.z * move_dir.z;
let add = target_speed - current;
if add > 0.0 {
let amount = (accel * target_speed * dt).min(add);
ent.vel.x += move_dir.x * amount;
ent.vel.z += move_dir.z * amount;
}
}
ent.yaw = input.yaw;
ent.pitch = (ent.pitch - input.look_dy * 0.008).clamp(-1.5, 1.5);
let pos = ent.pos;
let vel = ent.vel;
let target = pos + vel * dt;
let new_pos = self.trace(pos, target, RADIUS);
let ground_check = self.trace(new_pos, new_pos - Vec3::Y * 2.0, RADIUS);
let ent = &mut self.entities[id as usize];
ent.pos = new_pos;
if ground_check.y < new_pos.y - 1.0 {
ent.flags &= !GROUNDED;
} else {
ent.flags |= GROUNDED;
}
}
pub fn generate_snapshot<F>(&self, _player_id: u64, mut writer: F)
where
F: FnMut(&pxl8_entity_state),
{
for (i, ent) in self.entities.iter().enumerate() {
if ent.flags & ALIVE == 0 {
continue;
}
let mut state = pxl8_entity_state {
entity_id: i as u64,
userdata: [0u8; 56],
};
let bytes = &mut state.userdata;
bytes[0..4].copy_from_slice(&ent.pos.x.to_be_bytes());
bytes[4..8].copy_from_slice(&ent.pos.y.to_be_bytes());
bytes[8..12].copy_from_slice(&ent.pos.z.to_be_bytes());
bytes[12..16].copy_from_slice(&ent.yaw.to_be_bytes());
bytes[16..20].copy_from_slice(&ent.pitch.to_be_bytes());
bytes[20..24].copy_from_slice(&ent.vel.x.to_be_bytes());
bytes[24..28].copy_from_slice(&ent.vel.y.to_be_bytes());
bytes[28..32].copy_from_slice(&ent.vel.z.to_be_bytes());
bytes[32..36].copy_from_slice(&ent.flags.to_be_bytes());
bytes[36..38].copy_from_slice(&ent.kind.to_be_bytes());
writer(&state);
}
}
pub fn entity_count(&self) -> usize {
self.entities.iter().filter(|e| e.flags & ALIVE != 0).count()
}
pub fn get_player_position(&self, player_id: u64) -> Option<(f32, f32, f32)> {
let idx = player_id as usize;
if idx < self.entities.len() {
let ent = &self.entities[idx];
if ent.flags & ALIVE != 0 && ent.flags & PLAYER != 0 {
return Some((ent.pos.x, ent.pos.y, ent.pos.z));
}
}
None
}
}
impl Default for Simulation {
fn default() -> Self {
Self::new()
}
}