From bb43d308c0b70247f3e5a313a600ce57e46d51d7 Mon Sep 17 00:00:00 2001 From: binaryDiv Date: Tue, 28 Apr 2026 19:45:41 +0200 Subject: [PATCH] Implement TileSet and TileMap classes --- assets/tilesets/terrain.png | Bin 0 -> 622 bytes src/core/drawing/sprite.cppm | 3 +- src/core/drawing/tile_map.cppm | 109 +++++++++++++++++++++++++++++++++ src/core/drawing/tile_set.cppm | 94 ++++++++++++++++++++++++++++ src/core/engine.cppm | 1 - src/game/game.cppm | 44 +++++++++++-- 6 files changed, 243 insertions(+), 8 deletions(-) create mode 100644 assets/tilesets/terrain.png create mode 100644 src/core/drawing/tile_map.cppm create mode 100644 src/core/drawing/tile_set.cppm diff --git a/assets/tilesets/terrain.png b/assets/tilesets/terrain.png new file mode 100644 index 0000000000000000000000000000000000000000..d14db6724c846be171cab80c246283553b5cf7df GIT binary patch literal 622 zcmV-!0+IcRP)Px%C`m*?RCt{2n>%j9Fc3sHL0};UQn+&w_!_!U?vg9z2+=iMrBh)bfZQY%0l^T- z{qQrC^lDNvFss>FN`4}uKdMOo`Cjw(boqL%djIdkN7eg3SG{|~oR9aa`+ZEOo~BdJ zUJp?Kx9e2pWSF$!la|Mu6P$^dNV+B-Fcn3ractm6K zEwqLd?%_h76kl6u&H;90#2f?C0W>zBx;04W^we{GsvsD1jNJh|NbNdxICP?=?)i^E zIzVn}<^`!!XnQ`FCJF#4;7J5rTAO=}l(vmcwt%I)z@70W0+u=vw6uvP3P5^Ns0oA! z$G2Lz!Y>sHQ~_wk+u^YkETIY@1vv^R_YrVu<_-WA0#Tp-TH=>@DYQBVSOd9d|2a?& zP=XXArEe4rjfj%*G5730r`ZCic{bo{387X|qm6J{$mUvrFK4VN=t)`#L;+~Vztn-T z74bRZO43((p;($O&oN!piw^ce%f0no-jYdetc*Sz^#^%p@7ZDCjoiX&bq)yMYx z_LfZl4(I|v#^Ml2d1Ehtjnh;CSi@aw;ZQ@9e+`>s5y>hr;{W})g1U8~9IB2U|qWHBMSXn`VE`X<&Zse{Q}O;yZsfp8x;=07*qo IM6N<$g6wM%b^rhX literal 0 HcmV?d00001 diff --git a/src/core/drawing/sprite.cppm b/src/core/drawing/sprite.cppm index a927b02..31224cd 100644 --- a/src/core/drawing/sprite.cppm +++ b/src/core/drawing/sprite.cppm @@ -3,7 +3,6 @@ module; #include #include #include -#include export module core.drawing.sprite; @@ -47,7 +46,7 @@ export namespace core assert(texture_boundaries_.w > 0 && texture_boundaries_.h > 0); } - static Sprite create_with_texture( + static Sprite create_from_texture( RenderServer& render_server, const std::string& filename, std::optional texture_boundaries, diff --git a/src/core/drawing/tile_map.cppm b/src/core/drawing/tile_map.cppm new file mode 100644 index 0000000..31bfbfa --- /dev/null +++ b/src/core/drawing/tile_map.cppm @@ -0,0 +1,109 @@ +module; + +#include +#include +#include + +export module core.drawing.tile_map; + +import core.drawing.tile_set; +import core.render_server; +import wrappers.sdl; + +export namespace core +{ + // Restrict map size to 16 bit per dimension, so that width*height still fits within size_t + using MapCoord = uint16_t; + + class TileMap + { + // Tile set ID + // TODO: Needed later when we centralize tile set management using a ResourceManager + [[maybe_unused]] + TileSetID tile_set_id_; + + // Tile size + int tile_size_; + + // Map boundaries + MapCoord width_; + MapCoord height_; + + // Tile map origin in world coordinates + sdl::FPoint position_{0, 0}; + + // Tile map data: Tiles in a continuous array + std::unique_ptr tiles_{nullptr}; + + public: + explicit TileMap( + const TileSetID tile_set_id, + const int tile_size, + const MapCoord width, + const MapCoord height, + const sdl::FPoint position = {0, 0} + ) + : tile_set_id_(tile_set_id), + tile_size_(tile_size), + width_(width), + height_(height), + position_{position} + { + // TODO: This maybe shouldn't be an assert... + assert(width_ > 0 && height_ > 0); + tiles_ = std::make_unique( + static_cast(width * height) + ); + } + + constexpr std::size_t map_coords_to_index(const MapCoord x, const MapCoord y) const + { + // TODO: Properly handle x<0, y<0, x>=width, y>=height + assert(x < width_ && y < height_); + return y * width_ + x; + } + + constexpr TileID get_tile(const MapCoord x, const MapCoord y) const + { + return tiles_[map_coords_to_index(x, y)]; + } + + constexpr void set_tile(const MapCoord x, const MapCoord y, const TileID tile_id) + { + tiles_[map_coords_to_index(x, y)] = tile_id; + } + + // TODO: Should we get the TileSet from the RenderServer, similar to how we get textures? + void draw(const RenderServer& render_server, const TileSet& tile_set) const + { + // Get tile set texture + const sdl::Texture& tile_set_texture = render_server.get_texture(tile_set.get_texture_id()); + + // TODO: Implement more efficiently (we could just iterate over tiles_ directly) + for (MapCoord y = 0; y < height_; y++) { + for (MapCoord x = 0; x < width_; x++) { + const int tile_index = get_tile(x, y); + + // Ignore tiles with ID 0 + if (tile_index == 0) { + continue; + } + + const auto [tile_atlas_src_rect] = tile_set.get_tile_definition(tile_index); + const sdl::FRect tile_dest_rect{ + position_.x + x * tile_size_, + position_.y + y * tile_size_, + static_cast(tile_size_), + static_cast(tile_size_), + }; + + render_server.render_texture( + tile_set_texture, + &tile_atlas_src_rect, + &tile_dest_rect + ); + } + } + } + }; +} diff --git a/src/core/drawing/tile_set.cppm b/src/core/drawing/tile_set.cppm new file mode 100644 index 0000000..39ba81d --- /dev/null +++ b/src/core/drawing/tile_set.cppm @@ -0,0 +1,94 @@ +module; + +#include +#include +#include +#include + +export module core.drawing.tile_set; + +import core.render_server; +import wrappers.sdl; + +export namespace core +{ + using TileSetID = unsigned int; + using TileID = unsigned int; + + struct TileDefinition + { + sdl::FRect atlas_src_rect; + }; + + class TileSet + { + // Texture ID of tileset atlas texture + TextureID texture_id_; + + // Size of each tile in pixels + // TODO: Do we need this? + // int tile_size_; + + // Tile definitions: Maps tile ID to a struct describing the tile, e.g. the texture source boundaries, + // collision data, etc. + std::vector tile_definitions_; + + public: + TileSet() = delete; + + explicit TileSet( + const TextureID texture_id, + std::vector tile_definitions + ) + : texture_id_{texture_id}, + tile_definitions_{std::move(tile_definitions)} + {} + + static TileSet create_from_texture( + RenderServer& render_server, + const std::string& filename, + const int tile_size, + const std::vector& tile_atlas_coords + ) + { + const TextureID texture_id = render_server.load_texture(filename); + const sdl::Texture& texture = render_server.get_texture(texture_id); + + assert(tile_size > 0); + // TODO: These probably shouldn't be asserts... + assert(texture.get_width() % tile_size == 0); + assert(texture.get_height() % tile_size == 0); + + std::vector tile_definitions; + + // TODO: Should we automatically create tile 0 (no tile) instead of listing it in tile_atlas_coords? + for (const auto& [x, y] : tile_atlas_coords) { + const TileDefinition tile{ + { + static_cast(x * tile_size), + static_cast(y * tile_size), + static_cast(tile_size), + static_cast(tile_size), + } + }; + + // TODO: These probably shouldn't be asserts... + assert(tile.atlas_src_rect.x + tile.atlas_src_rect.w <= texture.get_width()); + assert(tile.atlas_src_rect.y + tile.atlas_src_rect.h <= texture.get_height()); + tile_definitions.push_back(tile); + } + + return TileSet(texture_id, tile_definitions); + } + + constexpr TextureID get_texture_id() const + { + return texture_id_; + } + + constexpr const TileDefinition& get_tile_definition(const TileID tile_id) const + { + return tile_definitions_.at(tile_id); + } + }; +} diff --git a/src/core/engine.cppm b/src/core/engine.cppm index 0978cdf..d9bd5ad 100644 --- a/src/core/engine.cppm +++ b/src/core/engine.cppm @@ -2,7 +2,6 @@ module; #include #include -#include export module core.engine; diff --git a/src/game/game.cppm b/src/game/game.cppm index 213cb50..a18cc6f 100644 --- a/src/game/game.cppm +++ b/src/game/game.cppm @@ -4,11 +4,12 @@ module; #include #include #include -#include export module game.game; import core.drawing.sprite; +import core.drawing.tile_map; +import core.drawing.tile_set; import core.engine; import core.render_server; import wrappers.sdl; @@ -28,19 +29,48 @@ export namespace game core::Sprite player_sprite_; std::vector sprites_; + // Tile set and tile map (TODO: tile set should be moved to resource manager in engine) + core::TileSet tile_set_; + core::TileMap tile_map_; + // Private constructor explicit Game(core::Engine& engine) : engine_(engine), player_sprite_{ - core::Sprite::create_with_texture( + core::Sprite::create_from_texture( engine_.get_render_server(), "assets/sprites/neocat_64.png", std::nullopt, sdl::FPoint{0, 0}, sdl::FPoint{-32, -32} ) - } - {} + }, + tile_set_{ + core::TileSet::create_from_texture( + engine_.get_render_server(), + "assets/tilesets/terrain.png", + 32, + { + // Tile 0: No tile + {0, 0}, + // Tile 1: Actual first tile in the texture + {0, 0}, + {1, 0}, + {2, 0}, + {3, 0}, + } + ) + }, + tile_map_(0, 32, 16, 12, {50, 50}) + { + // Initialize tile map with example data + for (auto x = 0; x < 10; x++) { + for (auto y = 0; y < 10; y++) { + const auto tile_index = 2 + (x + y) % 2; + tile_map_.set_tile(x, y, tile_index); + } + } + } public: Game() = delete; @@ -75,7 +105,7 @@ export namespace game if (event->type == sdl::EventType::MouseButtonUp) { sprites_.push_back( - core::Sprite::create_with_texture( + core::Sprite::create_from_texture( engine_.get_render_server(), "assets/sprites/neofox_64.png", std::nullopt, @@ -98,6 +128,10 @@ export namespace game const auto& render_server = engine_.get_render_server(); render_server.start_frame(); + // Render tilemap + tile_map_.draw(render_server, tile_set_); + + // Render sprites for (const core::Sprite& sprite : sprites_) { sprite.draw(render_server); }