//! The `render` module contains the structures for displaying //! the game, with each frame represented by a `Renderer` and //! frame specific information in `FrameInfo`. /// The (prefered) view distance of the game const VIEW_DISTANCE: i32 = 5; use std::{cell::RefCell, ops::Div, rc::Rc}; use dungeon::{Direction, Dungeon, Entity, EntityKind, FPos, Floor, MAP_SIZE, Pos, Tile}; use raylib::{ RaylibThread, camera::Camera2D, color::Color, math::{Rectangle, Vector2}, prelude::{ RaylibDraw, RaylibDrawHandle, RaylibHandle, RaylibMode2D, RaylibMode2DExt, RaylibTextureModeExt, }, texture::{RaylibTexture2D, RenderTexture2D}, }; use crate::assets::{AtlasTexture, BASE_TILE_SIZE, ImageData, WALL_HEIGHT}; /// The `FrameInfo` struct stores information used during a single frame #[derive(Clone, Copy, Debug)] struct FrameInfo { /// The last calculated fps fps: u32, /// The render width of the framebuffer in pixels width: i32, /// The render height of the framebuffer in pixels height: i32, /// The tile size in pixels tile_size: i32, } impl FrameInfo { fn new(handle: &RaylibHandle) -> Self { let fps = handle.get_fps(); let width = handle.get_render_width(); let height = handle.get_render_height(); let tile_size = { let size = width.min(height); let dist = VIEW_DISTANCE * 2 + 1; let pixels = size.div(dist).max(BASE_TILE_SIZE); 1 << (i32::BITS - pixels.leading_zeros()) }; Self { fps, width, height, tile_size, } } } /// The `State` struct stores persistant renderer data used across multiple frames #[derive(Debug)] struct State { /// Set of sprites to be drawn image: ImageData, /// Top layer of the tile map (used for back and top sides of walls) tiles_tex_fg: RefCell, /// Bottom layer of the tile map (used for most things) tiles_tex_bg: RefCell, /// Hash of tiles used to draw the tile textures tiles_hash: RefCell, } impl State { fn new( handle: &mut RaylibHandle, thread: &RaylibThread, image: ImageData, ) -> crate::Result { 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_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, } impl Renderer { pub(crate) fn new( handle: &mut RaylibHandle, thread: &RaylibThread, image: ImageData, ) -> crate::Result { let state = Rc::new(State::new(handle, thread, image)?); Ok(Self { state }) } /// Invokes the renderer for the current frame pub(crate) fn invoke<'a>( &'a mut self, handle: &'a mut RaylibHandle, thread: &'a RaylibThread, ) -> FrameRendererImpl<'a> { let info = FrameInfo::new(handle); let state = Rc::clone(&self.state); FrameRenderer { handle: handle.begin_drawing(thread), thread, info, 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, { /// The current draw handle for raylib handle: T, /// The raylib thread thread: &'a RaylibThread, /// Non drawing information for this current frame info: FrameInfo, /// Persistant render state state: Rc, } impl<'a, T> FrameRenderer<'a, T> where T: RaylibDraw + RaylibMode2DExt + RaylibTextureModeExt, { /// Draws an entire frame pub fn draw_frame(&mut self, dungeon: &Dungeon) { 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_tilemaps(&dungeon.floor); let camera = dungeon.camera(); let mut renderer = self.camera_renderer(camera); renderer.draw_tiles_bg(); renderer.draw_entities(dungeon); renderer.draw_tiles_fg(); } } impl<'a, T> FrameRenderer<'a, T> where T: RaylibDraw + RaylibMode2DExt, { /// Returns a raylib camera for the given position #[must_use] fn camera_renderer<'b>( &'b mut self, cpos: FPos, ) -> FrameRenderer<'b, RaylibMode2D<'b, T>> { let width = self.info.width; let height = self.info.height; let camera = Camera2D { target: Vector2::from(cpos.xy()) .scale_by(self.info.tile_size as f32) .max(Vector2::new(width as f32 / 2.0, height as f32 / 2.0)) .min(Vector2::new( (MAP_SIZE as i32 * self.info.tile_size) as f32 - (width as f32 / 2.0), (MAP_SIZE as i32 * self.info.tile_size) as f32 - (height as f32 / 2.0), )), offset: Vector2::new(width as f32 / 2.0, height as f32 / 2.0), rotation: 0.0, zoom: 1.0, }; let handle = self.handle.begin_mode2D(camera); FrameRenderer { handle, thread: self.thread, info: self.info, state: Rc::clone(&self.state), } } } impl<'a, T> FrameRenderer<'a, T> where T: RaylibDraw + RaylibTextureModeExt, { /// Updates all tilemaps fn update_tilemaps(&mut self, floor: &Floor) { let hash = floor.hash(); 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.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 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); } } } impl<'a, T> FrameRenderer<'a, T> where T: RaylibDraw, { /// Clear the screen pub fn clear(&mut self, color: Color) { self.handle.clear_background(color); } /// Draws player HP, inventory, and floor number pub fn draw_ui(&mut self, _dungeon: &Dungeon) { #[cfg(feature = "debug")] // Draw fps (debug only) self.draw_fps(); } /// Draws the entities on the map fn draw_entities(&mut self, dungeon: &Dungeon) { self.draw_entity(&dungeon.player.entity); } /// Draws an entity fn draw_entity(&mut self, entity: &Entity) { 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 (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, ); } /// Draw FPS counter fn draw_fps(&mut self) { let fps_str = format!("{}", self.info.fps); self.handle.draw_text(&fps_str, 10, 10, 30, Color::YELLOW); } } trait Vector2Ext { fn min(self, other: Self) -> Self; fn max(self, other: Self) -> Self; } impl Vector2Ext for Vector2 { fn min(self, other: Self) -> Self { Self::new(self.x.min(other.x), self.y.min(other.y)) } fn max(self, other: Self) -> Self { 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 RaylibDrawExt for T {}