├── .gitattributes ├── .gitignore ├── src ├── prelude.rs ├── lib.rs ├── plugin.rs ├── render │ ├── tilemap.wgsl │ ├── mod.rs │ ├── draw.rs │ ├── pipeline.rs │ ├── extract.rs │ └── queue.rs └── tilemap.rs ├── assets └── textures │ └── tilesheet.png ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── vut.toml ├── .editorconfig ├── rustfmt.toml ├── Cargo.toml ├── LICENSE-MIT ├── README.md ├── examples ├── simple.rs ├── benchmark.rs ├── multilayer.rs ├── flipped.rs └── two_tilemaps.rs └── LICENSE-APACHE /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /src/prelude.rs: -------------------------------------------------------------------------------- 1 | pub use crate::plugin::SimpleTileMapPlugin; 2 | pub use crate::tilemap::{Tile, TileMap}; 3 | -------------------------------------------------------------------------------- /assets/textures/tilesheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forbjok/bevy_simple_tilemap/HEAD/assets/textures/tilesheet.png -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod plugin; 2 | pub mod prelude; 3 | mod render; 4 | mod tilemap; 5 | 6 | pub use self::tilemap::{Tile, TileFlags, TileMap}; 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /vut.toml: -------------------------------------------------------------------------------- 1 | [general] 2 | ignore = "**/.git" 3 | 4 | [authoritative-version-source] 5 | type = "cargo" 6 | 7 | [[update-version-sources]] 8 | globs = "**" 9 | 10 | [[templates]] 11 | globs = "**/*.vutemplate" 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.rs] 14 | indent_size = 4 15 | 16 | [*.wgsl] 17 | indent_size = 4 18 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 120 2 | hard_tabs = false 3 | tab_spaces = 4 4 | newline_style = "Auto" 5 | use_small_heuristics = "Default" 6 | reorder_imports = true 7 | reorder_modules = true 8 | remove_nested_parens = true 9 | fn_params_layout = "Tall" 10 | edition = "2021" 11 | merge_derives = true 12 | use_try_shorthand = false 13 | use_field_init_shorthand = false 14 | force_explicit_abi = true 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_simple_tilemap" 3 | version = "0.18.0" 4 | authors = ["Forb.Jok "] 5 | edition = "2024" 6 | description = "Refreshingly simple tilemap implementation for Bevy Engine." 7 | license = "MIT/Apache-2.0" 8 | repository = "https://github.com/forbjok/bevy_simple_tilemap.git" 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | bitflags = "2.9.4" 14 | bytemuck = "1.23.2" 15 | 16 | [dependencies.bevy] 17 | version = "0.17.0" 18 | default-features = false 19 | features = [ 20 | "bevy_asset", 21 | "bevy_core_pipeline", 22 | "bevy_pbr", 23 | "bevy_render", 24 | "bevy_sprite", 25 | "bevy_window", 26 | ] 27 | 28 | [dev-dependencies.bevy] 29 | version = "0.17.0" 30 | default-features = false 31 | features = ["x11", "png", "wayland", "webgl2"] 32 | 33 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 34 | rayon = "1.11.0" 35 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Kjartan F. Kvamme 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | test: 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | build: 17 | - stable 18 | - nightly 19 | - windows-msvc 20 | 21 | include: 22 | - build: stable 23 | os: ubuntu-latest 24 | toolchain: stable 25 | 26 | - build: nightly 27 | os: ubuntu-latest 28 | toolchain: nightly 29 | 30 | - build: windows-msvc 31 | os: windows-latest 32 | toolchain: stable 33 | 34 | steps: 35 | - uses: actions/checkout@v4 36 | 37 | - name: Install required packages 38 | if: matrix.os == 'ubuntu-latest' 39 | run: | 40 | sudo apt-get install -y libasound2-dev libudev-dev 41 | 42 | - name: Install Rust 43 | uses: dtolnay/rust-toolchain@stable 44 | with: 45 | toolchain: ${{ matrix.toolchain }} 46 | targets: ${{ matrix.target }} 47 | 48 | - name: Run tests 49 | run: cargo test ${{ matrix.options }} --verbose 50 | -------------------------------------------------------------------------------- /src/plugin.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | asset::load_internal_asset, 3 | core_pipeline::core_2d::Transparent2d, 4 | prelude::*, 5 | render::{ 6 | Render, RenderApp, RenderSystems, render_phase::AddRenderCommand, render_resource::SpecializedRenderPipelines, 7 | }, 8 | }; 9 | 10 | use crate::render::{ 11 | self, ExtractedTilemaps, ImageBindGroups, TILEMAP_SHADER_HANDLE, TilemapAssetEvents, TilemapMeta, 12 | draw::DrawTilemap, pipeline::TilemapPipeline, 13 | }; 14 | 15 | #[derive(Default)] 16 | pub struct SimpleTileMapPlugin; 17 | 18 | #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] 19 | pub enum TileMapSystem { 20 | ExtractTilemaps, 21 | } 22 | 23 | impl Plugin for SimpleTileMapPlugin { 24 | fn build(&self, app: &mut App) { 25 | app.add_systems(Update, crate::tilemap::update_chunks_system); 26 | 27 | load_internal_asset!(app, TILEMAP_SHADER_HANDLE, "render/tilemap.wgsl", Shader::from_wgsl); 28 | 29 | if let Some(render_app) = app.get_sub_app_mut(RenderApp) { 30 | render_app 31 | .init_resource::() 32 | .init_resource::>() 33 | .init_resource::() 34 | .init_resource::() 35 | .init_resource::() 36 | .add_render_command::() 37 | .add_systems( 38 | ExtractSchedule, 39 | ( 40 | render::extract::extract_tilemaps.in_set(TileMapSystem::ExtractTilemaps), 41 | render::extract::extract_tilemap_events, 42 | ), 43 | ) 44 | .add_systems(Render, render::queue::queue_tilemaps.in_set(RenderSystems::Queue)); 45 | }; 46 | } 47 | 48 | fn finish(&self, app: &mut App) { 49 | if let Some(render_app) = app.get_sub_app_mut(RenderApp) { 50 | render_app.init_resource::(); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bevy_simple_tilemap 2 | 3 | [![CI](https://github.com/forbjok/bevy_simple_tilemap/actions/workflows/ci.yml/badge.svg)](https://github.com/forbjok/bevy_simple_tilemap/actions/workflows/ci.yml) 4 | ![GitHub release (latest by date)](https://img.shields.io/github/v/release/forbjok/bevy_simple_tilemap) 5 | ![Crates.io](https://img.shields.io/crates/v/bevy_simple_tilemap) 6 | 7 | Refreshingly simple tilemap implementation for Bevy Engine. 8 | 9 | ## Why another tilemap? 10 | 11 | The main reason I started this was because I felt the existing tilemap implementations for Bevy were needlessly complicated to use when all you want to do is to as quickly and simply as possible render a grid of tiles to the screen, often exposing internal implementation details such as chunks to the user. 12 | 13 | ## Goals: 14 | * Allow the user to render a grid of rectangular tiles to the screen 15 | * Make this as simple and intuitive as possible 16 | 17 | ## Non-goals: 18 | * Supporting every imaginable shape of tile 19 | * 3D tilemaps 20 | * Assisting with non-rendering-related game-logic 21 | 22 | ## How to use: 23 | 24 | ### Spawning: 25 | ```rust 26 | fn setup( 27 | asset_server: Res, 28 | mut commands: Commands, 29 | mut texture_atlases: ResMut>, 30 | ) { 31 | // Load tilesheet texture and make a texture atlas from it 32 | let image = asset_server.load("textures/tilesheet.png"); 33 | let atlas = TextureAtlasLayout::from_grid(uvec2(16, 16), 4, 1, Some(uvec2(1, 1)), None); 34 | let atlas_handle = texture_atlases.add(atlas); 35 | 36 | // Spawn tilemap 37 | commands.spawn(TileMap::new(image, atlas_handle)); 38 | } 39 | ``` 40 | 41 | ### Updating (or inserting) single tile: 42 | ```rust 43 | tilemap.set_tile(ivec3(0, 0, 0), Some(Tile { sprite_index: 0, color: Color::WHITE })); 44 | ``` 45 | 46 | ### Updating (or inserting) multiple tiles: 47 | ```rust 48 | // List to store set tile operations 49 | let mut tiles: Vec<(IVec3, Option)> = Vec::new(); 50 | tiles.push((ivec3(0, 0, 0), Some(Tile { sprite_index: 0, color: Color::WHITE }))); 51 | tiles.push((ivec3(1, 0, 0), Some(Tile { sprite_index: 1, color: Color::WHITE }))); 52 | 53 | // Perform tile update 54 | tilemap.set_tiles(tiles); 55 | ``` 56 | -------------------------------------------------------------------------------- /src/render/tilemap.wgsl: -------------------------------------------------------------------------------- 1 | struct View { 2 | view_proj: mat4x4, 3 | world_position: vec3, 4 | }; 5 | 6 | @group(0) @binding(0) 7 | var view: View; 8 | 9 | struct VertexOutput { 10 | @location(0) uv: vec2, 11 | @location(1) color: vec4, 12 | @location(2) tile_uv: vec2, 13 | @builtin(position) position: vec4, 14 | }; 15 | 16 | struct TilemapGpuData { 17 | transform: mat4x4, 18 | tile_size: vec2, 19 | texture_size: vec2, 20 | }; 21 | 22 | @group(2) @binding(0) 23 | var tilemap: TilemapGpuData; 24 | 25 | @vertex 26 | fn vertex( 27 | @builtin(vertex_index) vertex_index: u32, 28 | @location(0) vertex_position: vec3, 29 | @location(1) vertex_uv: vec2, 30 | @location(2) vertex_tile_uv: vec2, 31 | @location(3) vertex_color: vec4, 32 | ) -> VertexOutput { 33 | var out: VertexOutput; 34 | 35 | out.uv = vertex_uv; 36 | out.tile_uv = vertex_tile_uv; 37 | out.position = view.view_proj * tilemap.transform * vec4(vertex_position, 1.0); 38 | out.color = vertex_color; 39 | 40 | return out; 41 | } 42 | 43 | @group(1) @binding(0) 44 | var sprite_texture: texture_2d; 45 | @group(1) @binding(1) 46 | var sprite_sampler: sampler; 47 | 48 | @fragment 49 | fn fragment(in: VertexOutput) -> @location(0) vec4 { 50 | let half_texture_pixel_size_u = 0.5 / tilemap.texture_size.x; 51 | let half_texture_pixel_size_v = 0.5 / tilemap.texture_size.y; 52 | let half_tile_pixel_size_u = 0.5 / tilemap.tile_size.x; 53 | let half_tile_pixel_size_v = 0.5 / tilemap.tile_size.y; 54 | 55 | // Offset the UV 1/2 pixel from the sides of the tile, so that the sampler doesn't bleed onto 56 | // adjacent tiles at the edges. 57 | var uv_offset = vec2(0.0, 0.0); 58 | 59 | if (in.tile_uv.x < half_tile_pixel_size_u) { 60 | uv_offset.x = half_texture_pixel_size_u; 61 | } else if (in.tile_uv.x > (1.0 - half_tile_pixel_size_u)) { 62 | uv_offset.x = -half_texture_pixel_size_u; 63 | } 64 | 65 | if (in.tile_uv.y < half_tile_pixel_size_v) { 66 | uv_offset.y = half_texture_pixel_size_v; 67 | } else if (in.tile_uv.y > (1.0 - half_tile_pixel_size_v)) { 68 | uv_offset.y = -half_texture_pixel_size_v; 69 | } 70 | 71 | var color = in.color * textureSample(sprite_texture, sprite_sampler, in.uv + uv_offset); 72 | 73 | return color; 74 | } 75 | -------------------------------------------------------------------------------- /src/render/mod.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Range; 2 | 3 | use bevy::{ 4 | asset::uuid_handle, 5 | color::LinearRgba, 6 | math::{IVec2, IVec3, Mat4, URect, UVec2, Vec2}, 7 | platform::collections::HashMap, 8 | prelude::{AssetEvent, AssetId, Component, Entity, GlobalTransform, Handle, Image, Resource, Shader}, 9 | render::{ 10 | render_resource::{BindGroup, BufferUsages, DynamicUniformBuffer, RawBufferVec, ShaderType}, 11 | sync_world::MainEntity, 12 | }, 13 | }; 14 | use bytemuck::{Pod, Zeroable}; 15 | 16 | use crate::TileFlags; 17 | 18 | pub mod draw; 19 | pub mod extract; 20 | pub mod pipeline; 21 | pub mod queue; 22 | 23 | pub const TILEMAP_SHADER_HANDLE: Handle = uuid_handle!("3f7c8913-f14a-40cb-b044-4916400481e2"); 24 | 25 | pub struct ExtractedTile { 26 | pub pos: IVec2, 27 | pub rect: URect, 28 | pub color: LinearRgba, 29 | pub flags: TileFlags, 30 | } 31 | 32 | pub struct ExtractedChunk { 33 | pub origin: IVec3, 34 | pub tiles: Vec, 35 | } 36 | 37 | pub struct ExtractedTilemap { 38 | pub transform: GlobalTransform, 39 | pub image_handle_id: AssetId, 40 | pub tile_size: UVec2, 41 | pub chunks: Vec, 42 | pub visible_chunks: Vec, 43 | } 44 | 45 | #[derive(Default, Resource)] 46 | pub struct ExtractedTilemaps { 47 | pub tilemaps: HashMap<(Entity, MainEntity), ExtractedTilemap>, 48 | } 49 | 50 | #[derive(Default, Resource)] 51 | pub struct TilemapAssetEvents { 52 | pub images: Vec>, 53 | } 54 | 55 | #[repr(C)] 56 | #[derive(Copy, Clone, Pod, Zeroable)] 57 | struct TilemapVertex { 58 | pub position: [f32; 3], 59 | pub uv: [f32; 2], 60 | pub tile_uv: [f32; 2], 61 | pub color: [f32; 4], 62 | } 63 | 64 | #[repr(C)] 65 | #[derive(Copy, Clone, Default, Pod, Zeroable, ShaderType)] 66 | pub struct TilemapGpuData { 67 | pub transform: Mat4, 68 | pub tile_size: Vec2, 69 | pub texture_size: Vec2, 70 | } 71 | 72 | pub struct ChunkMeta { 73 | vertices: RawBufferVec, 74 | tilemap_gpu_data: DynamicUniformBuffer, 75 | tilemap_gpu_data_bind_group: Option, 76 | texture_size: UVec2, 77 | tile_size: UVec2, 78 | } 79 | 80 | impl Default for ChunkMeta { 81 | fn default() -> Self { 82 | Self { 83 | vertices: RawBufferVec::new(BufferUsages::VERTEX), 84 | tilemap_gpu_data: DynamicUniformBuffer::default(), 85 | tilemap_gpu_data_bind_group: None, 86 | texture_size: UVec2::ZERO, 87 | tile_size: UVec2::ZERO, 88 | } 89 | } 90 | } 91 | 92 | pub type ChunkKey = (Entity, IVec3); 93 | 94 | #[derive(Default, Resource)] 95 | pub struct TilemapMeta { 96 | chunks: HashMap, 97 | view_bind_group: Option, 98 | } 99 | 100 | #[derive(Component, PartialEq, Clone, Eq)] 101 | pub struct TilemapBatch { 102 | image_handle_id: AssetId, 103 | range: Range, 104 | chunk_key: (Entity, IVec3), 105 | } 106 | 107 | #[derive(Default, Resource)] 108 | pub struct ImageBindGroups { 109 | values: HashMap, BindGroup>, 110 | } 111 | -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | math::{ivec3, uvec2}, 3 | prelude::*, 4 | window::WindowResolution, 5 | }; 6 | 7 | use bevy_simple_tilemap::prelude::*; 8 | 9 | fn main() { 10 | App::new() 11 | .add_plugins( 12 | DefaultPlugins 13 | .set(WindowPlugin { 14 | primary_window: Some(Window { 15 | resolution: WindowResolution::new(1280, 720).with_scale_factor_override(1.0), 16 | ..Default::default() 17 | }), 18 | ..default() 19 | }) 20 | .set(ImagePlugin::default_nearest()), 21 | ) 22 | .add_plugins(SimpleTileMapPlugin) 23 | .add_systems(Startup, setup) 24 | .add_systems(Update, input_system) 25 | .run(); 26 | } 27 | 28 | fn input_system( 29 | mut camera_transform_query: Query<&mut Transform, With>, 30 | mut tilemap_visible_query: Query<&mut Visibility, With>, 31 | keyboard_input: Res>, 32 | time: Res