diff options
| author | Freya Murphy <freya@freyacat.org> | 2025-10-23 13:09:39 -0400 |
|---|---|---|
| committer | Freya Murphy <freya@freyacat.org> | 2025-10-23 13:12:18 -0400 |
| commit | 996805bf576343e9620a875454d2e35e8e54170c (patch) | |
| tree | 9c15d3d0e4d59b3f5bf46978bce2e8677d9cd589 | |
| parent | dungeon: add hash to to check if it has been changed (diff) | |
| download | DungeonCrawl-996805bf576343e9620a875454d2e35e8e54170c.tar.gz DungeonCrawl-996805bf576343e9620a875454d2e35e8e54170c.tar.bz2 DungeonCrawl-996805bf576343e9620a875454d2e35e8e54170c.zip | |
graphics: refactor Assets, and add tile drawing!
| -rw-r--r-- | game/src/main.rs | 6 | ||||
| -rw-r--r-- | graphics/src/assets.rs | 25 | ||||
| -rw-r--r-- | graphics/src/lib.rs | 37 | ||||
| -rw-r--r-- | graphics/src/render.rs | 168 |
4 files changed, 150 insertions, 86 deletions
diff --git a/game/src/main.rs b/game/src/main.rs index 72cc9b3..854f57e 100644 --- a/game/src/main.rs +++ b/game/src/main.rs @@ -1,9 +1,7 @@ -use std::error::Error; - use dungeon::*; use graphics::*; -fn main() -> Result<(), Box<dyn Error>> { +fn main() -> Result<()> { // Load the window let mut window = Window::new(720, 480, "game")?; // Initial game state @@ -13,7 +11,7 @@ fn main() -> Result<(), Box<dyn Error>> { while window.is_open() { // TODO update game state // Draw a single frame - window.renderer().draw_frame(&dungeon); + window.renderer().draw_frame(&dungeon)?; } Ok(()) diff --git a/graphics/src/assets.rs b/graphics/src/assets.rs index 3f2354a..edac69b 100644 --- a/graphics/src/assets.rs +++ b/graphics/src/assets.rs @@ -1,8 +1,6 @@ //! The `assets` crate stores all audio and image assets that need to be //! loaded during runtime -use std::error::Error; - use raylib::{RaylibHandle, RaylibThread, audio::RaylibAudio}; #[expect(dead_code)] @@ -14,7 +12,7 @@ type Sound = raylib::audio::Sound<'static>; #[derive(Debug)] pub struct AudioData {} impl AudioData { - pub(crate) fn load() -> Result<Self, Box<dyn Error>> { + pub(crate) fn load() -> crate::Result<Self> { // Phantom handle to the raylib audio subsystem // Raylib doesnt use a handle, but the rust bindings // have one to ensure memory safety. @@ -41,7 +39,7 @@ impl ImageData { pub(crate) fn load( _handle: &mut RaylibHandle, _thread: &RaylibThread, - ) -> Result<Self, Box<dyn Error>> { + ) -> crate::Result<Self> { // TODO: load image data //let example = handle.load_texture(&thread, "example.png"); @@ -49,22 +47,3 @@ impl ImageData { Ok(Self {}) } } - -#[derive(Debug)] -pub(crate) struct Assets { - /// Audio needs to be accessible outside of the renderer - pub(crate) audio: AudioData, - /// Images are only needed by the renderer - pub(crate) image: ImageData, -} -impl Assets { - pub(crate) fn load( - handle: &mut RaylibHandle, - thread: &RaylibThread, - ) -> Result<Self, Box<dyn Error>> { - let audio = AudioData::load()?; - let image = ImageData::load(handle, thread)?; - - Ok(Self { audio, image }) - } -} diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index 1ca03b3..08faae3 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -2,12 +2,11 @@ //! rendering using the `raylib` library. use std::cell::RefCell; -use std::error::Error; use raylib::prelude::*; -use crate::assets::{Assets, AudioData}; -use crate::render::{FrameInfo, Renderer}; +use crate::assets::{AudioData, ImageData}; +use crate::render::{FrameRenderer, Renderer}; mod assets; mod render; @@ -15,14 +14,22 @@ mod render; /// The `KeyCode` type represents different keys being pressed on the users keyboard pub use raylib::consts::KeyboardKey as KeyCode; +/// The `Error` type used within this crate +pub type Error = Box<dyn std::error::Error>; + +/// The `Result` type used witin this crate +pub type Result<T> = std::result::Result<T, crate::Error>; + /// The `Window` type represents the game window #[derive(Debug)] pub struct Window { // core raylib handles handle: RefCell<RaylibHandle>, thread: RaylibThread, - // static assets - assets: Assets, + // persistant renderer + renderer: Renderer, + // audio data/subsystem + audio: AudioData, } impl Window { @@ -35,7 +42,7 @@ impl Window { /// /// let window = Window::new(800, 600, "Dungeon Crawl").unwrap(); /// ``` - pub fn new(width: i32, height: i32, title: &str) -> Result<Self, Box<dyn Error>> { + pub fn new(width: i32, height: i32, title: &str) -> crate::Result<Self> { let (mut handle, thread) = raylib::init() .size(width, height) .title(title) @@ -44,13 +51,15 @@ impl Window { .vsync() .build(); - // we will now initalize all assets - let assets = Assets::load(&mut handle, &thread)?; + // load assets + let audio = AudioData::load()?; + let image = ImageData::load(&mut handle, &thread)?; Ok(Self { handle: RefCell::new(handle), thread, - assets, + renderer: Renderer::new(image), + audio, }) } @@ -60,7 +69,7 @@ impl Window { !self.handle.borrow().window_should_close() } - /// Returns the renderer for the game + /// Returns the per frame renderer for the game /// /// # Examples /// ```no_run @@ -69,10 +78,8 @@ impl Window { /// let mut window = Window::new(800, 600, "Dungeon Crawl").unwrap(); /// let mut renderer = window.renderer(); /// ``` - pub fn renderer(&mut self) -> Renderer<'_> { - let info = FrameInfo::new(self.handle.get_mut(), &self.assets.image); - let handle = self.handle.get_mut().begin_drawing(&self.thread); - Renderer::new(handle, info) + pub fn renderer(&mut self) -> FrameRenderer<'_> { + self.renderer.invoke(self.handle.get_mut(), &self.thread) } /// Returns the per frame delta time @@ -102,6 +109,6 @@ impl Window { /// Get audio data for the window pub fn audio(&self) -> &AudioData { - &self.assets.audio + &self.audio } } diff --git a/graphics/src/render.rs b/graphics/src/render.rs index d9b0473..8c88210 100644 --- a/graphics/src/render.rs +++ b/graphics/src/render.rs @@ -2,70 +2,95 @@ //! the game, with each frame represented by a `Renderer` and //! frame specific information in `FrameInfo`. -use dungeon::{Dungeon, Entity, Player}; +/// The (prefered) view distance of the game +const VIEW_DISTANCE: i32 = 10; + +use std::ops::{Div, Sub}; + +use dungeon::{Dungeon, Entity, Floor, MAP_SIZE, Player, Pos, Tile}; use raylib::{ + RaylibThread, color::Color, - prelude::{RaylibDraw, RaylibDrawHandle, RaylibHandle}, + math::{Rectangle, Vector2}, + prelude::{RaylibDraw, RaylibDrawHandle, RaylibHandle, RaylibTextureModeExt}, + texture::RenderTexture2D, }; use crate::assets::ImageData; -/// The `FrameInfo` struct contains information about -/// the current frame being rendered. -pub struct FrameInfo<'a> { - /// Time in seconds since last frame drawn - pub delta: f32, - /// FPS for last frame drawn - pub fps: u32, - /// Loaded texture data - /// NOTE: remove `expect` once we start using image data!! - #[expect(dead_code)] - pub(crate) image: &'a ImageData, +/// The `Renderer` struct is the persistant renderer +/// for the duration for the application. +#[derive(Debug)] +pub struct Renderer { + /// Set of sprites to be drawn + image: ImageData, + /// Pre-rendered map texture that updates if the map changes + /// Stores the hash of the map tiles to check this + tiles: Option<(u64, RenderTexture2D)>, } -impl<'a> FrameInfo<'a> { - /// Creates a new `FrameInfo` from the provided - /// `RaylibHandle`. - pub(crate) fn new(handle: &RaylibHandle, image: &'a ImageData) -> Self { - Self { - delta: handle.get_frame_time(), - fps: handle.get_fps(), - image, +impl Renderer { + pub(crate) fn new(image: ImageData) -> Self { + Self { image, tiles: None } + } + + /// Invokes the renderer for the current frame + pub(crate) fn invoke<'a>( + &'a mut self, + handle: &'a mut RaylibHandle, + thread: &'a RaylibThread, + ) -> FrameRenderer<'a> { + let draw_handle = handle.begin_drawing(thread); + FrameRenderer { + handle: draw_handle, + thread, + renderer: self, } } } -/// A `Renderer` is a renderer for a single +/// A `FrameRenderer` is a renderer for a single /// frame of the game. It is created per frame. -pub struct Renderer<'a> { +pub struct FrameRenderer<'a> { + /// The current draw handle for raylib handle: RaylibDrawHandle<'a>, - info: FrameInfo<'a>, + /// The raylib thread + thread: &'a RaylibThread, + /// Mutable reference to the main renderer (stores persistant data) + renderer: &'a mut Renderer, } -impl<'a> Renderer<'a> { - /// Creates the renderer for the current frame - pub(crate) fn new(handle: RaylibDrawHandle<'a>, info: FrameInfo<'a>) -> Self { - Self { handle, info } - } - - /// Returns the info struct for the current frame - pub fn info(&self) -> &FrameInfo<'a> { - &self.info +impl<'a> FrameRenderer<'a> { + /// Returns last computed fps + fn fps(&self) -> u32 { + self.handle.get_fps() } - /// Returns the per frame delta time - pub fn delta_time(&self) -> f32 { - self.info.delta + /// Returns image data + #[expect(dead_code)] + fn image(&self) -> &ImageData { + &self.renderer.image } /// Returns the current render width - pub fn render_width(&mut self) -> i32 { + fn render_width(&self) -> i32 { self.handle.get_render_width() } /// Returns the current render height - pub fn render_height(&mut self) -> i32 { + fn render_height(&self) -> i32 { self.handle.get_render_height() } + /// Returns the wanted map tile size + fn tile_size(&self) -> i32 { + // the pixl size we will base everything on + let size = self.render_width().max(self.render_height()); + // min visible tiles + let dist = VIEW_DISTANCE * 2 + 1; + // get crude number of pixles + // TODO: force scaling levels + size.div(dist).max(16) + } + /// Clear the screen pub fn clear(&mut self) { self.handle.clear_background(Color::BLACK); @@ -82,16 +107,18 @@ impl<'a> Renderer<'a> { /// let dungeon = Dungeon::new(); /// renderer.draw_frame(&dungeon); /// ``` - pub fn draw_frame(&mut self, dungeon: &Dungeon) { + pub fn draw_frame(&mut self, dungeon: &Dungeon) -> crate::Result<()> { // Clear the background to black self.clear(); // Draw the dungeon - self.draw_tiles(dungeon); + self.draw_tiles(dungeon)?; self.draw_player(&dungeon.player); // Draw the ui self.draw_ui(dungeon); + + Ok(()) } /// Draws player HP, inventory, and floor number @@ -113,14 +140,67 @@ impl<'a> Renderer<'a> { } /// Draw dungeon tiles - #[expect(clippy::unused_self)] - pub fn draw_tiles(&mut self, _dungeon: &Dungeon) { - // TODO: + pub fn draw_tiles(&mut self, dungeon: &Dungeon) -> crate::Result<()> { + let hash = dungeon.floor.hash(); + let tex = match self.renderer.tiles.take() { + Some((h, tex)) if hash == h => tex, + _ => self.draw_tiles_to_tex(&dungeon.floor)?, + }; + + // caculate the starting postion on the texture to draw from + let size = self.tile_size() as f32; + let dist = VIEW_DISTANCE as f32; + let camera = Vector2::from(dungeon.camera().xy()); + let source_rec = Rectangle::new( + camera.x.sub(dist).max(0.0) * size, + camera.y.sub(dist).max(0.0) * size, + size * (dist * 2.0 + 1.0), + size * (dist * 2.0 + 1.0), + ); + + self.handle + .draw_texture_rec(&tex, source_rec, Vector2::zero(), Color::WHITE); + + // save the texture + self.renderer.tiles.replace((hash, tex)); + + Ok(()) + } + + /// Draw tiles on a provided texture + fn draw_tiles_to_tex(&mut self, floor: &Floor) -> crate::Result<RenderTexture2D> { + let size = self.tile_size(); + let pixels = (MAP_SIZE as i32) * size; + let mut tex = + self.handle + .load_render_texture(self.thread, pixels as u32, pixels as u32)?; + + // draw the tiles to the texture + { + let mut handle = self.handle.begin_texture_mode(self.thread, &mut tex); + handle.clear_background(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); + } + } + + Ok(tex) } /// Draw FPS counter pub fn draw_fps(&mut self) { - let fps_str = format!("{}", self.info.fps); + let fps_str = format!("{}", self.fps()); self.handle.draw_text(&fps_str, 10, 10, 30, Color::YELLOW); } } + +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, + } +} |