diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 3d29cbbf..9e09de54 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -15,11 +15,13 @@ jobs: steps: - uses: actions/checkout@v2 - - uses: Swatinem/rust-cache@v1 - - name: Build - run: cargo build --verbose + - name: Install dependencies + run: sudo apt-get install -y libsfml-dev libcsfml-dev + + - name: Build library + run: cargo build --lib --verbose - name: Run tests run: cargo test --verbose diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 0ad8f4cb..7aca7caf 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -1,3 +1,4 @@ * Matthew Hall * Kevin Balz -* Thorbjørn Lindeijer \ No newline at end of file +* Thorbjørn Lindeijer +* Alejandro Perea diff --git a/Cargo.toml b/Cargo.toml index a9c176dc..88107678 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,8 +22,16 @@ path = "src/lib.rs" name = "example" path = "examples/main.rs" +[[example]] +name = "sfml" +path = "examples/sfml/main.rs" + [dependencies] -base64 = "0.13.0" -xml-rs = "0.8.4" +base64 = "0.13.0" +xml-rs = "0.8.4" libflate = "1.1.2" zstd = { version = "0.10.0", optional = true } + +[dev-dependencies.sfml] +version = "0.16" +features = ["graphics"] diff --git a/examples/sfml/main.rs b/examples/sfml/main.rs new file mode 100644 index 00000000..0dab7cd5 --- /dev/null +++ b/examples/sfml/main.rs @@ -0,0 +1,184 @@ +//! ## rs-tiled demo with SFML +//! -------------------------- +//! Displays a map, use WASD keys to move the camera around. +//! Only draws its tile layers and nothing else. + +mod mesh; +mod tilesheet; + +use mesh::QuadMesh; +use sfml::{ + graphics::{BlendMode, Color, Drawable, RenderStates, RenderTarget, RenderWindow, Transform}, + system::{Vector2f, Vector2u}, + window::{ContextSettings, Key, Style}, +}; +use std::{env, path::PathBuf, time::Duration}; +use tiled::{FilesystemResourceCache, Map, TileLayer}; +use tilesheet::Tilesheet; + +/// A path to the map to display. +const MAP_PATH: &'static str = "assets/tiled_base64_external.tmx"; + +/// A [Map] wrapper which also contains graphical information such as the tileset texture or the layer meshes. +/// +/// Wrappers like these are generally recommended to use instead of using the crate structures (e.g. [LayerData]) as you have more freedom +/// with what you can do with them, they won't change between crate versions and they are more specific to your needs. +/// +/// [Map]: tiled::map::Map +pub struct Level { + layers: Vec, + /// Unique tilesheet related to the level, which contains the Tiled tileset + Its only texture. + tilesheet: Tilesheet, + tile_size: f32, +} + +impl Level { + /// Create a new level from a Tiled map. + pub fn from_map(map: Map) -> Self { + let tilesheet = { + let tileset = map.tilesets()[0].clone(); + Tilesheet::from_tileset(tileset) + }; + let tile_size = map.tile_width as f32; + + let layers = map + .layers() + .filter_map(|layer| match &layer.layer_type() { + tiled::LayerType::TileLayer(l) => Some(generate_mesh(l, &tilesheet)), + _ => None, + }) + .collect(); + + Self { + tilesheet, + layers, + tile_size, + } + } +} + +/// Generates a vertex mesh from a tile layer for rendering. +fn generate_mesh(layer: &TileLayer, tilesheet: &Tilesheet) -> QuadMesh { + let finite = match layer.data() { + tiled::TileLayerData::Finite(f) => f, + tiled::TileLayerData::Infinite(_) => panic!("Infinite maps not supported"), + }; + let (width, height) = (finite.width() as usize, finite.height() as usize); + let mut mesh = QuadMesh::with_capacity(width * height); + for x in 0..width { + for y in 0..height { + // TODO: `FiniteTileLayer` for getting tiles directly from finite tile layers? + if let Some(tile) = layer.get_tile(x, y) { + let uv = tilesheet.tile_rect(tile.id); + mesh.add_quad(Vector2f::new(x as f32, y as f32), 1., uv); + } + } + } + + mesh +} + +impl Drawable for Level { + fn draw<'a: 'shader, 'texture, 'shader, 'shader_texture>( + &'a self, + target: &mut dyn RenderTarget, + states: &sfml::graphics::RenderStates<'texture, 'shader, 'shader_texture>, + ) { + let mut states = states.clone(); + states.set_texture(Some(&self.tilesheet.texture())); + for mesh in self.layers.iter() { + target.draw_with_renderstates(mesh, &states); + } + } +} + +fn main() { + let mut cache = FilesystemResourceCache::new(); + + let map = Map::parse_file( + PathBuf::from( + env::var("CARGO_MANIFEST_DIR") + .expect("To run the example, use `cargo run --example sfml`"), + ) + .join(MAP_PATH), + &mut cache, + ) + .unwrap(); + let level = Level::from_map(map); + + let mut window = create_window(); + let mut camera_position = Vector2f::default(); + let mut last_frame_time = std::time::Instant::now(); + + loop { + while let Some(event) = window.poll_event() { + use sfml::window::Event; + match event { + Event::Closed => return, + _ => (), + } + } + + let this_frame_time = std::time::Instant::now(); + let delta_time = this_frame_time - last_frame_time; + + handle_input(&mut camera_position, delta_time); + + let camera_transform = camera_transform(window.size(), camera_position, level.tile_size); + let render_states = RenderStates::new(BlendMode::ALPHA, camera_transform, None, None); + + window.clear(Color::BLACK); + window.draw_with_renderstates(&level, &render_states); + window.display(); + + last_frame_time = this_frame_time; + } +} + +/// Creates the window of the application +fn create_window() -> RenderWindow { + let mut context_settings = ContextSettings::default(); + context_settings.set_antialiasing_level(2); + let mut window = RenderWindow::new( + (1080, 720), + "rs-tiled demo", + Style::CLOSE, + &context_settings, + ); + window.set_vertical_sync_enabled(true); + + window +} + +fn handle_input(camera_position: &mut Vector2f, delta_time: Duration) { + let mut movement = Vector2f::default(); + + const SPEED: f32 = 5.; + if Key::W.is_pressed() { + movement.y -= 1.; + } + if Key::A.is_pressed() { + movement.x -= 1.; + } + if Key::S.is_pressed() { + movement.y += 1.; + } + if Key::D.is_pressed() { + movement.x += 1.; + } + + *camera_position += movement * delta_time.as_secs_f32() * SPEED; +} + +fn camera_transform(window_size: Vector2u, camera_position: Vector2f, tile_size: f32) -> Transform { + let window_size = Vector2f::new(window_size.x as f32, window_size.y as f32); + + let mut x = Transform::IDENTITY; + x.translate(window_size.x / 2., window_size.y / 2.); + x.translate( + -camera_position.x * tile_size, + -camera_position.y * tile_size, + ); + x.scale_with_center(tile_size, tile_size, 0f32, 0f32); + x +} diff --git a/examples/sfml/mesh.rs b/examples/sfml/mesh.rs new file mode 100644 index 00000000..f6fbc200 --- /dev/null +++ b/examples/sfml/mesh.rs @@ -0,0 +1,43 @@ +use sfml::{ + graphics::{Drawable, FloatRect, PrimitiveType, Vertex}, + system::Vector2f, +}; + +pub struct QuadMesh(Vec); + +impl QuadMesh { + /// Create a new mesh with capacity for the given amount of quads. + pub fn with_capacity(quads: usize) -> Self { + Self(Vec::with_capacity(quads * 4)) + } + + /// Add a quad made up of vertices to the mesh. + pub fn add_quad(&mut self, position: Vector2f, size: f32, uv: FloatRect) { + self.0.push(Vertex::with_pos_coords( + position, + Vector2f::new(uv.left, uv.top), + )); + self.0.push(Vertex::with_pos_coords( + position + Vector2f::new(size, 0f32), + Vector2f::new(uv.left + uv.width, uv.top), + )); + self.0.push(Vertex::with_pos_coords( + position + Vector2f::new(size, size), + Vector2f::new(uv.left + uv.width, uv.top + uv.height), + )); + self.0.push(Vertex::with_pos_coords( + position + Vector2f::new(0f32, size), + Vector2f::new(uv.left, uv.top + uv.height), + )); + } +} + +impl Drawable for QuadMesh { + fn draw<'a: 'shader, 'texture, 'shader, 'shader_texture>( + &'a self, + target: &mut dyn sfml::graphics::RenderTarget, + states: &sfml::graphics::RenderStates<'texture, 'shader, 'shader_texture>, + ) { + target.draw_primitives(&self.0, PrimitiveType::QUADS, states); + } +} diff --git a/examples/sfml/tilesheet.rs b/examples/sfml/tilesheet.rs new file mode 100644 index 00000000..b366a64f --- /dev/null +++ b/examples/sfml/tilesheet.rs @@ -0,0 +1,51 @@ +use std::rc::Rc; + +use sfml::{ + graphics::{FloatRect, Texture}, + SfBox, +}; +use tiled::Tileset; + +/// A container for a tileset and the texture it references. +pub struct Tilesheet { + texture: SfBox, + tileset: Rc, +} + +impl Tilesheet { + /// Create a tilesheet from a Tiled tileset, loading its texture along the way. + pub fn from_tileset<'p>(tileset: Rc) -> Self { + let tileset_image = tileset.image.as_ref().unwrap(); + + let texture = { + let texture_path = &tileset_image + .source + .to_str() + .expect("obtaining valid UTF-8 path"); + Texture::from_file(texture_path).unwrap() + }; + + Tilesheet { texture, tileset } + } + + pub fn texture(&self) -> &Texture { + &self.texture + } + + pub fn tile_rect(&self, id: u32) -> FloatRect { + let tile_width = self.tileset.tile_width; + let tile_height = self.tileset.tile_height; + let spacing = self.tileset.spacing; + let margin = self.tileset.margin; + let tiles_per_row = (self.texture.size().x - margin + spacing) / (tile_width + spacing); + let x = id % tiles_per_row * tile_width; + let y = id / tiles_per_row * tile_height; + + FloatRect { + left: x as f32, + top: y as f32, + width: tile_width as f32, + height: tile_height as f32, + } + } +}