summaryrefslogtreecommitdiff
path: root/graphics
diff options
context:
space:
mode:
authorFreya Murphy <freya@freyacat.org>2025-10-25 15:20:19 -0400
committerFreya Murphy <freya@freyacat.org>2025-10-25 15:20:19 -0400
commit54eac2384f2d449289dc0b91e9ec8538fe9d3847 (patch)
tree97afb285016b35e6d4367b24782c1a941b76b05a /graphics
parentgraphics: have tilemap a consistent size and scale (diff)
downloadDungeonCrawl-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.rs61
-rw-r--r--graphics/src/render.rs265
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 {}