Implement TileSet and TileMap classes

This commit is contained in:
Lexi / Zoe 2026-04-28 19:45:41 +02:00
parent bb9d108102
commit bb43d308c0
Signed by: binaryDiv
GPG key ID: F8D4956E224DA232
6 changed files with 243 additions and 8 deletions

BIN
assets/tilesets/terrain.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

View file

@ -3,7 +3,6 @@ module;
#include <cassert>
#include <optional>
#include <string>
#include <SDL3/SDL.h>
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<sdl::FRect> texture_boundaries,

View file

@ -0,0 +1,109 @@
module;
#include <cassert>
#include <cstddef>
#include <memory>
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<TileID[]> 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<TileID[]>(
static_cast<std::size_t>(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<float>(tile_size_),
static_cast<float>(tile_size_),
};
render_server.render_texture(
tile_set_texture,
&tile_atlas_src_rect,
&tile_dest_rect
);
}
}
}
};
}

View file

@ -0,0 +1,94 @@
module;
#include <cassert>
#include <string>
#include <utility>
#include <vector>
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<TileDefinition> tile_definitions_;
public:
TileSet() = delete;
explicit TileSet(
const TextureID texture_id,
std::vector<TileDefinition> 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<sdl::Point>& 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<TileDefinition> 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<float>(x * tile_size),
static_cast<float>(y * tile_size),
static_cast<float>(tile_size),
static_cast<float>(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);
}
};
}

View file

@ -2,7 +2,6 @@ module;
#include <cassert>
#include <memory>
#include <SDL3/SDL.h>
export module core.engine;

View file

@ -4,11 +4,12 @@ module;
#include <memory>
#include <optional>
#include <vector>
#include <SDL3/SDL.h>
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<core::Sprite> 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);
}