diff options
| author | Freya Murphy <freya@freyacat.org> | 2025-10-25 15:20:19 -0400 |
|---|---|---|
| committer | Freya Murphy <freya@freyacat.org> | 2025-10-25 15:20:19 -0400 |
| commit | 54eac2384f2d449289dc0b91e9ec8538fe9d3847 (patch) | |
| tree | 97afb285016b35e6d4367b24782c1a941b76b05a /graphics | |
| parent | graphics: have tilemap a consistent size and scale (diff) | |
| download | DungeonCrawl-54eac2384f2d449289dc0b91e9ec8538fe9d3847.tar.gz DungeonCrawl-54eac2384f2d449289dc0b91e9ec8538fe9d3847.tar.bz2 DungeonCrawl-54eac2384f2d449289dc0b91e9ec8538fe9d3847.zip | |
graphics: use atlas texture when rendering (and lots of refactoring)
Diffstat (limited to 'graphics')
| -rw-r--r-- | graphics/src/assets.rs | 61 | ||||
| -rw-r--r-- | graphics/src/render.rs | 265 |
2 files changed, 249 insertions, 77 deletions
diff --git a/graphics/src/assets.rs b/graphics/src/assets.rs index edac69b..f7ce68a 100644 --- a/graphics/src/assets.rs +++ b/graphics/src/assets.rs @@ -1,7 +1,7 @@ //! The `assets` crate stores all audio and image assets that need to be //! loaded during runtime -use raylib::{RaylibHandle, RaylibThread, audio::RaylibAudio}; +use raylib::{RaylibHandle, RaylibThread, audio::RaylibAudio, texture::Texture2D}; #[expect(dead_code)] type Sound = raylib::audio::Sound<'static>; @@ -32,18 +32,63 @@ impl AudioData { } } +/// The baseline size of all ingame sprites and tile textures +pub(crate) const BASE_TILE_SIZE: i32 = 32; + +/// The height of the wall (offset between tile layers) +pub(crate) const WALL_HEIGHT: i32 = 13; + +/// Texture indexes into the atlas +#[derive(Clone, Copy, Debug)] +pub(crate) enum AtlasTexture { + Wall, + FloorFull, + FloorEmpty, + WallBase, + WallEdgeNorth, + WallEdgeEast, + WallEdgeSouth, + WallEdgeWest, + Player, + Error, +} +impl AtlasTexture { + pub(crate) fn xy(&self) -> (i32, i32) { + match self { + Self::Wall => (0, 0), + Self::FloorFull => (1, 0), + Self::FloorEmpty => (2, 0), + Self::WallBase => (3, 0), + Self::WallEdgeNorth => (0, 1), + Self::WallEdgeEast => (1, 1), + Self::WallEdgeSouth => (2, 1), + Self::WallEdgeWest => (3, 1), + Self::Player => (0, 2), + Self::Error => (3, 3), + } + } + + pub(crate) fn x(&self) -> i32 { + self.xy().0 + } + + pub(crate) fn y(&self) -> i32 { + self.xy().1 + } +} + /// The `ImageData` container loads all game sprites, and other images into memory. #[derive(Debug)] -pub(crate) struct ImageData {} +pub(crate) struct ImageData { + pub(crate) atlas: Texture2D, +} impl ImageData { pub(crate) fn load( - _handle: &mut RaylibHandle, - _thread: &RaylibThread, + handle: &mut RaylibHandle, + thread: &RaylibThread, ) -> crate::Result<Self> { - // TODO: load image data - - //let example = handle.load_texture(&thread, "example.png"); + let atlas = handle.load_texture(thread, "assets/atlas.bmp")?; - Ok(Self {}) + Ok(Self { atlas }) } } diff --git a/graphics/src/render.rs b/graphics/src/render.rs index 2dc77a7..b6af7c7 100644 --- a/graphics/src/render.rs +++ b/graphics/src/render.rs @@ -5,28 +5,24 @@ /// The (prefered) view distance of the game const VIEW_DISTANCE: i32 = 5; -/// The baseline size of all ingame sprites and tile textures -const BASE_TILE_SIZE: i32 = 16; +use std::{cell::RefCell, ops::Div, rc::Rc}; -use std::ops::Div; - -use dungeon::{Dungeon, Entity, FPos, Floor, MAP_SIZE, Pos, Tile}; +use dungeon::{Direction, Dungeon, Entity, EntityKind, FPos, Floor, MAP_SIZE, Pos, Tile}; use raylib::{ RaylibThread, camera::Camera2D, color::Color, - math::Vector2, + math::{Rectangle, Vector2}, prelude::{ RaylibDraw, RaylibDrawHandle, RaylibHandle, RaylibMode2D, RaylibMode2DExt, RaylibTextureModeExt, }, - texture::RenderTexture2D, + texture::{RaylibTexture2D, RenderTexture2D}, }; -use crate::assets::ImageData; +use crate::assets::{AtlasTexture, BASE_TILE_SIZE, ImageData, WALL_HEIGHT}; -/// The `FrameInfo` struct stores persistant information used thought a frame not -/// accessable from `RaylibDraw` +/// The `FrameInfo` struct stores information used during a single frame #[derive(Clone, Copy, Debug)] struct FrameInfo { /// The last calculated fps @@ -59,36 +55,54 @@ impl FrameInfo { } } -/// The `Renderer` struct is the persistant renderer -/// for the duration for the application. +/// The `State` struct stores persistant renderer data used across multiple frames #[derive(Debug)] -pub struct Renderer { +struct State { /// Set of sprites to be drawn - #[expect(dead_code)] image: ImageData, - /// Pre-rendered map texture that updates if the map changes - /// Stores the hash of the map tiles to check this - tiles_tex: RenderTexture2D, - /// Hash of tiles used to draw on `tiles_tex` - tiles_hash: u64, + /// Top layer of the tile map (used for back and top sides of walls) + tiles_tex_fg: RefCell<RenderTexture2D>, + /// Bottom layer of the tile map (used for most things) + tiles_tex_bg: RefCell<RenderTexture2D>, + /// Hash of tiles used to draw the tile textures + tiles_hash: RefCell<u64>, } -impl Renderer { - pub(crate) fn new( +impl State { + fn new( handle: &mut RaylibHandle, thread: &RaylibThread, image: ImageData, ) -> crate::Result<Self> { - let tiles_tex = { - let size = MAP_SIZE as u32 * BASE_TILE_SIZE as u32; - handle.load_render_texture(thread, size, size)? - }; + let tex_size = MAP_SIZE as u32 * BASE_TILE_SIZE as u32; + let tiles_tex_fg = handle.load_render_texture(thread, tex_size, tex_size)?; + let tiles_tex_bg = handle.load_render_texture(thread, tex_size, tex_size)?; Ok(Self { image, - tiles_tex, - tiles_hash: 0, + tiles_tex_fg: RefCell::new(tiles_tex_fg), + tiles_tex_bg: RefCell::new(tiles_tex_bg), + tiles_hash: RefCell::new(0), }) } +} + +/// The `Renderer` struct is the persistant renderer +/// for the duration for the application. +#[derive(Debug)] +pub struct Renderer { + /// Persistant render state + state: Rc<State>, +} +impl Renderer { + pub(crate) fn new( + handle: &mut RaylibHandle, + thread: &RaylibThread, + image: ImageData, + ) -> crate::Result<Self> { + let state = Rc::new(State::new(handle, thread, image)?); + + Ok(Self { state }) + } /// Invokes the renderer for the current frame pub(crate) fn invoke<'a>( @@ -97,17 +111,29 @@ impl Renderer { thread: &'a RaylibThread, ) -> FrameRendererImpl<'a> { let info = FrameInfo::new(handle); + let state = Rc::clone(&self.state); FrameRenderer { handle: handle.begin_drawing(thread), thread, info, - renderer: self, + state, } } } pub type FrameRendererImpl<'a> = FrameRenderer<'a, RaylibDrawHandle<'a>>; +macro_rules! tex_renderer { + ($fr:expr, $tex:expr) => { + FrameRenderer { + handle: $fr.handle.begin_texture_mode($fr.thread, $tex), + thread: &$fr.thread, + info: $fr.info, + state: Rc::clone(&$fr.state), + } + }; +} + pub struct FrameRenderer<'a, T> where T: RaylibDraw, @@ -118,8 +144,8 @@ where thread: &'a RaylibThread, /// Non drawing information for this current frame info: FrameInfo, - /// Mutable reference to the main renderer (stores persistant data) - renderer: &'a mut Renderer, + /// Persistant render state + state: Rc<State>, } impl<'a, T> FrameRenderer<'a, T> where @@ -127,18 +153,19 @@ where { /// Draws an entire frame pub fn draw_frame(&mut self, dungeon: &Dungeon) { - self.clear(); + self.clear(Color::BLACK); self.draw_dungeon(dungeon); self.draw_ui(dungeon); } /// Draws the dungeon, (tiles and entities) pub fn draw_dungeon(&mut self, dungeon: &Dungeon) { - self.update_tilemap(&dungeon.floor); + self.update_tilemaps(&dungeon.floor); let camera = dungeon.camera(); let mut renderer = self.camera_renderer(camera); - renderer.draw_tiles(); + renderer.draw_tiles_bg(); renderer.draw_entities(dungeon); + renderer.draw_tiles_fg(); } } impl<'a, T> FrameRenderer<'a, T> @@ -170,7 +197,7 @@ where handle, thread: self.thread, info: self.info, - renderer: self.renderer, + state: Rc::clone(&self.state), } } } @@ -178,22 +205,73 @@ impl<'a, T> FrameRenderer<'a, T> where T: RaylibDraw + RaylibTextureModeExt, { - /// Draw tiles on a provided texture - fn update_tilemap(&mut self, floor: &Floor) { + /// Updates all tilemaps + fn update_tilemaps(&mut self, floor: &Floor) { let hash = floor.hash(); - if self.renderer.tiles_hash == hash { - // Texture is up to date + let old_hash = std::mem::replace(&mut *self.state.tiles_hash.borrow_mut(), hash); + if old_hash == hash { + // Textures are up to date return; } - self.renderer.tiles_hash = hash; - let size = BASE_TILE_SIZE; - let tex = &mut self.renderer.tiles_tex; - let mut handle = self.handle.begin_texture_mode(self.thread, tex); + self.update_fg_tilemap(floor); + self.update_bg_tilemap(floor); + } + + /// Draws the foregound tile map + fn update_fg_tilemap(&mut self, floor: &Floor) { + let size = BASE_TILE_SIZE as f32; + let tex = &mut self.state.tiles_tex_fg.borrow_mut(); + let mut renderer = tex_renderer!(self, tex); + renderer.clear(Color::BLANK); + + for pos in Pos::values() { + let (fx, fy) = FPos::from(pos).xy(); + let (xs, ys) = (fx * size, fy * size); + + // fg layer only draws a top walls + if floor.get(pos) != Tile::Wall { + continue; + }; + + // draw base wall top texture + renderer.draw_atlas(AtlasTexture::WallBase, xs, ys, size, 0.0); + + // draw top wall borders + let is_wall = + |dir| pos.step(dir).map_or(Tile::Wall, |p| floor.get(p)).is_wall(); + if !is_wall(Direction::North) { + renderer.draw_atlas(AtlasTexture::WallEdgeNorth, xs, ys, size, 0.0); + } + if !is_wall(Direction::East) { + renderer.draw_atlas(AtlasTexture::WallEdgeEast, xs, ys, size, 0.0); + } + if !is_wall(Direction::South) { + renderer.draw_atlas(AtlasTexture::WallEdgeSouth, xs, ys, size, 0.0); + } + if !is_wall(Direction::West) { + renderer.draw_atlas(AtlasTexture::WallEdgeWest, xs, ys, size, 0.0); + } + } + } + + /// Draws the foregound tile map + fn update_bg_tilemap(&mut self, floor: &Floor) { + let size = BASE_TILE_SIZE as f32; + let tex = &mut self.state.tiles_tex_bg.borrow_mut(); + let mut renderer = tex_renderer!(self, tex); + renderer.clear(Color::BLACK); + for pos in Pos::values() { let (x, y) = pos.xy(); - let color = tile_color(floor.get(pos)); - handle.draw_rectangle(x as i32 * size, y as i32 * size, size, size, color); + let tile = floor.get(pos); + let tex = match tile { + Tile::Wall => AtlasTexture::Wall, + Tile::Air if (x + y) % 2 == 0 => AtlasTexture::FloorFull, + Tile::Air if (x + y) % 2 == 1 => AtlasTexture::FloorEmpty, + _ => AtlasTexture::Error, + }; + renderer.draw_atlas(tex, x as f32 * size, y as f32 * size, size, 0.0); } } } @@ -202,8 +280,8 @@ where T: RaylibDraw, { /// Clear the screen - pub fn clear(&mut self) { - self.handle.clear_background(Color::BLACK); + pub fn clear(&mut self, color: Color) { + self.handle.clear_background(color); } /// Draws player HP, inventory, and floor number @@ -219,23 +297,57 @@ where } /// Draws an entity - #[expect(clippy::cast_possible_truncation)] fn draw_entity(&mut self, entity: &Entity) { - let size = self.info.tile_size; - let x = (entity.fpos.x() * size as f32) as i32; - let y = (entity.fpos.y() * size as f32) as i32; - // TODO: per entity color - self.handle.draw_rectangle(x, y, size, size, Color::GREEN); + let size = self.info.tile_size as f32; + let x = entity.fpos.x(); + let y = entity.fpos.y(); + let tex = match entity.kind { + EntityKind::Player => AtlasTexture::Player, + _ => AtlasTexture::Error, + }; + self.draw_atlas(tex, x * size, y * size, size, 0.0); } - /// Draw dungeon tiles - fn draw_tiles(&mut self) { - let tex = &self.renderer.tiles_tex; - self.handle.draw_texture_ex( - tex, - Vector2::zero(), - 0.0, - (self.info.tile_size / BASE_TILE_SIZE) as f32, + /// Draw dungeon tiles (background layer) + fn draw_tiles_bg(&mut self) { + let tex = &*self.state.tiles_tex_bg.borrow(); + let size = self.info.tile_size as f32; + let offset = 0.0; + self.handle.draw_tilemap(tex, size, offset); + } + + /// Draw dungeon tiles (foreground layer) + fn draw_tiles_fg(&mut self) { + let tex = &*self.state.tiles_tex_fg.borrow(); + let size = self.info.tile_size as f32; + let offset = WALL_HEIGHT as f32; + self.handle.draw_tilemap(tex, size, offset); + } + + /// Draw an atlas texture index + fn draw_atlas(&mut self, tex: AtlasTexture, x: f32, y: f32, size: f32, rotation: f32) { + let source_rec = Rectangle { + x: (tex.x() * BASE_TILE_SIZE) as f32, + y: (tex.y() * BASE_TILE_SIZE) as f32, + width: BASE_TILE_SIZE as f32, + height: BASE_TILE_SIZE as f32, + }; + let dest_rec = Rectangle { + x, + y, + width: size, + height: size, + }; + let origin = Vector2 { + x: dest_rec.width / 2.0, + y: dest_rec.height / 2.0, + }; + self.handle.draw_texture_pro( + &self.state.image.atlas, + source_rec, + dest_rec, + origin, + rotation, Color::WHITE, ); } @@ -247,15 +359,6 @@ where } } -fn tile_color(tile: Tile) -> Color { - // TODO: use textures instead of colors :) - match tile { - Tile::Wall => Color::BLUE, - Tile::Air => Color::RED, - Tile::Stairs => Color::GRAY, - } -} - trait Vector2Ext { fn min(self, other: Self) -> Self; fn max(self, other: Self) -> Self; @@ -269,3 +372,27 @@ impl Vector2Ext for Vector2 { Self::new(self.x.max(other.x), self.y.max(other.y)) } } + +trait RaylibDrawExt: RaylibDraw { + /// Draw dungeon tiles helper function + fn draw_tilemap(&mut self, tex: &RenderTexture2D, size: f32, offset: f32) { + let scale = size / BASE_TILE_SIZE as f32; + let width = tex.width() as f32; + let height = tex.height() as f32; + let source_rec = Rectangle { + x: 0.0, + y: 0.0, + width, + height: -height, + }; + let dest_rec = Rectangle { + x: 0.0, + y: -(offset * scale), + width: width * scale, + height: height * scale, + }; + let origin = Vector2::zero(); + self.draw_texture_pro(tex, source_rec, dest_rec, origin, 0.0, Color::WHITE); + } +} +impl<T: RaylibDraw> RaylibDrawExt for T {} |