use crate::painter::Painter;
use crate::sprite::{BlendMode, SpriteInstance};
use crate::texture::TextureId;
pub struct Tilemap {
pub atlas: TextureId,
pub atlas_size: [u32; 2],
pub tile_size: [u32; 2],
pub width: u32,
pub height: u32,
pub origin: [f32; 2],
pub tiles: Vec<u16>,
pub layer: i16,
chunk_size: u32,
chunk_cols: u32,
chunks: Vec<Chunk>,
last_origin: [f32; 2],
}
struct Chunk {
instances: Vec<SpriteInstance>,
dirty: bool,
}
impl Tilemap {
pub fn new(
atlas: TextureId,
atlas_size: [u32; 2],
tile_size: [u32; 2],
width: u32,
height: u32,
) -> Self {
Self::with_chunk_size(atlas, atlas_size, tile_size, width, height, 32)
}
pub fn with_chunk_size(
atlas: TextureId,
atlas_size: [u32; 2],
tile_size: [u32; 2],
width: u32,
height: u32,
chunk_size: u32,
) -> Self {
let chunk_size = chunk_size.max(1);
let chunk_cols = width.div_ceil(chunk_size);
let chunk_rows = height.div_ceil(chunk_size);
let chunks = (0..chunk_cols * chunk_rows)
.map(|_| Chunk {
instances: Vec::new(),
dirty: true,
})
.collect();
Self {
atlas,
atlas_size,
tile_size,
width,
height,
origin: [0.0, 0.0],
tiles: vec![0; (width * height) as usize],
layer: 0,
chunk_size,
chunk_cols,
chunks,
last_origin: [0.0, 0.0],
}
}
pub fn set(&mut self, x: u32, y: u32, tile: u16) {
if x < self.width && y < self.height {
self.tiles[(y * self.width + x) as usize] = tile;
let ci = (y / self.chunk_size) * self.chunk_cols + (x / self.chunk_size);
self.chunks[ci as usize].dirty = true;
}
}
pub fn get(&self, x: u32, y: u32) -> u16 {
if x < self.width && y < self.height {
self.tiles[(y * self.width + x) as usize]
} else {
0
}
}
pub fn mark_all_dirty(&mut self) {
for c in &mut self.chunks {
c.dirty = true;
}
}
fn tiles_per_row(&self) -> u32 {
(self.atlas_size[0] / self.tile_size[0]).max(1)
}
fn uv_for(&self, tile: u16) -> ([f32; 2], [f32; 2]) {
let idx = (tile - 1) as u32;
let cols = self.tiles_per_row();
let cx = idx % cols;
let cy = idx / cols;
let u0 = (cx * self.tile_size[0]) as f32 / self.atlas_size[0] as f32;
let v0 = (cy * self.tile_size[1]) as f32 / self.atlas_size[1] as f32;
let u1 = ((cx + 1) * self.tile_size[0]) as f32 / self.atlas_size[0] as f32;
let v1 = ((cy + 1) * self.tile_size[1]) as f32 / self.atlas_size[1] as f32;
([u0, v0], [u1, v1])
}
fn chunk_cells(&self, ci: usize) -> (u32, u32, u32, u32) {
let ccx = ci as u32 % self.chunk_cols;
let ccy = ci as u32 / self.chunk_cols;
let x0 = ccx * self.chunk_size;
let y0 = ccy * self.chunk_size;
(
x0,
y0,
(x0 + self.chunk_size).min(self.width),
(y0 + self.chunk_size).min(self.height),
)
}
fn chunk_aabb(&self, ci: usize) -> ([f32; 2], [f32; 2]) {
let (x0, y0, x1, y1) = self.chunk_cells(ci);
let tw = self.tile_size[0] as f32;
let th = self.tile_size[1] as f32;
(
[
self.origin[0] + x0 as f32 * tw,
self.origin[1] + y0 as f32 * th,
],
[
self.origin[0] + x1 as f32 * tw,
self.origin[1] + y1 as f32 * th,
],
)
}
fn build_chunk(&self, ci: usize) -> Vec<SpriteInstance> {
let (x0, y0, x1, y1) = self.chunk_cells(ci);
let tw = self.tile_size[0] as f32;
let th = self.tile_size[1] as f32;
let mut instances = Vec::new();
for y in y0..y1 {
for x in x0..x1 {
let t = self.tiles[(y * self.width + x) as usize];
if t == 0 {
continue;
}
let (uv_min, uv_max) = self.uv_for(t);
let pos = [
self.origin[0] + x as f32 * tw,
self.origin[1] + y as f32 * th,
];
instances.push(SpriteInstance::at(pos, [tw, th]).with_uv(uv_min, uv_max));
}
}
instances
}
pub fn draw(&mut self, painter: &mut Painter) {
if self.origin != self.last_origin {
self.mark_all_dirty();
self.last_origin = self.origin;
}
for ci in 0..self.chunks.len() {
if self.chunks[ci].dirty {
self.chunks[ci].instances = self.build_chunk(ci);
self.chunks[ci].dirty = false;
}
}
let (vmin, vmax) = painter.visible_rect();
for ci in 0..self.chunks.len() {
let (cmin, cmax) = self.chunk_aabb(ci);
if !rects_intersect(cmin, cmax, vmin, vmax) {
continue;
}
for inst in &self.chunks[ci].instances {
painter.sprite_ex(self.atlas, *inst, self.layer, BlendMode::Alpha);
}
}
}
}
fn rects_intersect(amin: [f32; 2], amax: [f32; 2], bmin: [f32; 2], bmax: [f32; 2]) -> bool {
amin[0] < bmax[0] && amax[0] > bmin[0] && amin[1] < bmax[1] && amax[1] > bmin[1]
}
#[cfg(test)]
mod tests {
use super::*;
fn map() -> Tilemap {
Tilemap::new(TextureId(0), [64, 64], [16, 16], 256, 256)
}
#[test]
fn chunk_grid_dimensions() {
let m = map();
assert_eq!(m.chunk_cols, 8);
assert_eq!(m.chunks.len(), 64); }
#[test]
fn set_marks_only_the_owning_chunk() {
let mut m = map();
m.mark_all_dirty();
for c in &mut m.chunks {
c.dirty = false;
}
m.set(40, 70, 1); assert!(m.chunks[17].dirty);
assert_eq!(m.chunks.iter().filter(|c| c.dirty).count(), 1);
}
#[test]
fn culling_skips_offscreen_chunks() {
let m = map();
let (vmin, vmax) = ([0.0, 0.0], [32.0, 32.0]);
let visible = (0..m.chunks.len())
.filter(|&ci| {
let (cmin, cmax) = m.chunk_aabb(ci);
rects_intersect(cmin, cmax, vmin, vmax)
})
.count();
assert_eq!(visible, 1);
}
#[test]
fn empty_tiles_emit_no_instances() {
let mut m = map();
m.set(3, 3, 5);
let insts = m.build_chunk(0);
assert_eq!(insts.len(), 1);
}
}