summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFreya Murphy <freya@freyacat.org>2025-10-23 13:09:39 -0400
committerFreya Murphy <freya@freyacat.org>2025-10-23 13:12:18 -0400
commit996805bf576343e9620a875454d2e35e8e54170c (patch)
tree9c15d3d0e4d59b3f5bf46978bce2e8677d9cd589
parentdungeon: add hash to to check if it has been changed (diff)
downloadDungeonCrawl-996805bf576343e9620a875454d2e35e8e54170c.tar.gz
DungeonCrawl-996805bf576343e9620a875454d2e35e8e54170c.tar.bz2
DungeonCrawl-996805bf576343e9620a875454d2e35e8e54170c.zip
graphics: refactor Assets, and add tile drawing!
-rw-r--r--game/src/main.rs6
-rw-r--r--graphics/src/assets.rs25
-rw-r--r--graphics/src/lib.rs37
-rw-r--r--graphics/src/render.rs168
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,
+ }
+}