diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index 3d29cbbf1e0d603832a4e2468f6543c4ca248461..9e09de54cb067e837b5d1f05478b290036193441 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 0ad8f4cb323fb28dd0fc094b29e69c8caeacbbf0..7aca7cafe92a9f84c82dae5cdbd49a20236a0096 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 a9c176dce5c3818ca41fcdf746feb10d36fddfa9..8810767891eeeee7a9bb8fca5d0a14ffb3f8d5b0 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 0000000000000000000000000000000000000000..0dab7cd5f3ed25cb6839bdcb5e58e703ee29c191
--- /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<QuadMesh>,
+    /// 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 0000000000000000000000000000000000000000..f6fbc2005e9515831f02d6eb7318e17dfd5575a2
--- /dev/null
+++ b/examples/sfml/mesh.rs
@@ -0,0 +1,43 @@
+use sfml::{
+    graphics::{Drawable, FloatRect, PrimitiveType, Vertex},
+    system::Vector2f,
+};
+
+pub struct QuadMesh(Vec<Vertex>);
+
+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 0000000000000000000000000000000000000000..b366a64fa13ae570d7c977de57576e4e8f5ea782
--- /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<Texture>,
+    tileset: Rc<Tileset>,
+}
+
+impl Tilesheet {
+    /// Create a tilesheet from a Tiled tileset, loading its texture along the way.
+    pub fn from_tileset<'p>(tileset: Rc<Tileset>) -> 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,
+        }
+    }
+}