diff --git a/tilemap/docs/api.md b/tilemap/docs/api.md index e26318a..f069eee 100644 --- a/tilemap/docs/api.md +++ b/tilemap/docs/api.md @@ -8,7 +8,7 @@ The tilemap library provides a flexible system for generating and managing tile- - **Chunk**: 64x64 tile containers with biome information - **Tile**: Individual map tiles with base and surface types - **TerrainGenerator**: Pass-based procedural terrain generation system -- **Generation Passes**: Modular generation components (biome, base terrain) +- **Generation Passes**: Modular generation components (biome, base terrain, hole filling) - **Biome System**: Climate-based terrain variation ## Core Classes @@ -120,19 +120,22 @@ struct GenerationConfig { Seed seed; // 128-bit seed for random generation // Temperature noise parameters - double temperature_scale = /*default value*/; // Scale for temperature noise - int temperature_octaves = /*default value*/; // Number of octaves for temperature noise - double temperature_persistence = /*default value*/; // Persistence for temperature noise + double temperature_scale = 0.05; // Scale for temperature noise + int temperature_octaves = 3; // Number of octaves for temperature noise + double temperature_persistence = 0.4; // Persistence for temperature noise // Humidity noise parameters - double humidity_scale = /*default value*/; // Scale for humidity noise - int humidity_octaves = /*default value*/; // Number of octaves for humidity noise - double humidity_persistence = /*default value*/; // Persistence for humidity noise + double humidity_scale = 0.05; // Scale for humidity noise + int humidity_octaves = 3; // Number of octaves for humidity noise + double humidity_persistence = 0.4; // Persistence for humidity noise // Base terrain noise parameters - double base_scale = /*default value*/; // Scale for base terrain noise - int base_octaves = /*default value*/; // Number of octaves for base terrain noise - double base_persistence = /*default value*/; // Persistence for base terrain noise + double base_scale = 0.08; // Scale for base terrain noise + int base_octaves = 3; // Number of octaves for base terrain noise + double base_persistence = 0.5; // Persistence for base terrain noise + + // Hole filling parameters + std::uint32_t fill_threshold = 16; // Fill holes smaller than this size }; ``` @@ -148,6 +151,7 @@ struct GenerationConfig { - `base_scale`: Controls the scale/frequency of base terrain height variation - `base_octaves`: Number of noise octaves for base terrain - `base_persistence`: How much each octave contributes to base terrain noise (0.0-1.0) +- `fill_threshold`: Maximum size of connected components to fill with mountains (hole filling) ### Generation Passes @@ -208,6 +212,35 @@ private: - **Noise-based Distribution**: Uses calibrated noise for balanced terrain distribution - **Tile-level Detail**: Generates terrain at individual tile resolution +#### HoleFillPass + +Fills small holes in the terrain using breadth-first search (BFS) algorithm. + +```cpp +class HoleFillPass { +public: + explicit HoleFillPass(const GenerationConfig& config); + void operator()(TileMap& tilemap); + +private: + bool is_passable(BaseTileType type) const; + std::uint32_t bfs_component_size( + TileMap& tilemap, TilePos start_pos, + std::vector>& visited, + std::vector& positions + ); + std::vector get_neighbors(TileMap& tilemap, TilePos pos) const; + bool is_at_boundary(TileMap& tilemap, TilePos pos) const; +}; +``` + +**Key Features:** +- **BFS Algorithm**: Uses breadth-first search to identify connected components +- **Boundary Awareness**: Preserves holes that touch the map boundary +- **Size-based Filtering**: Only fills holes smaller than `fill_threshold` +- **Mountain-as-Impassable**: Treats mountains as impassable terrain for connectivity +- **Hole Filling**: Converts small isolated areas to mountains for cleaner terrain + ### TerrainGenerator Main orchestrator class that manages the generation process using multiple passes. @@ -221,12 +254,14 @@ public: private: void biome_pass(TileMap& tilemap); void base_tile_type_pass(TileMap& tilemap); + void hole_fill_pass(TileMap& tilemap); }; ``` **Generation Flow:** 1. **Biome Pass**: Generate climate data and assign biomes to sub-chunks 2. **Base Tile Type Pass**: Generate base terrain types based on biomes and noise +3. **Hole Fill Pass**: Fill small holes in the terrain using BFS algorithm ### Generation Function @@ -393,6 +428,9 @@ config.base_scale = 0.08; config.base_octaves = 3; config.base_persistence = 0.5; +// Hole filling settings +config.fill_threshold = 16; // Fill holes smaller than 16 tiles + // Generate terrain istd::map_generate(tilemap, config); diff --git a/tilemap/include/generation.h b/tilemap/include/generation.h index 47ba735..08a4621 100644 --- a/tilemap/include/generation.h +++ b/tilemap/include/generation.h @@ -7,6 +7,7 @@ #include "tilemap.h" #include #include +#include #include namespace istd { @@ -26,6 +27,9 @@ struct GenerationConfig { double base_scale = 0.08; // Scale for base terrain noise int base_octaves = 3; // Number of octaves for base terrain noise double base_persistence = 0.5; // Persistence for base terrain noise + + // Hole filling parameters + std::uint32_t fill_threshold = 16; // Fill holes smaller than this size }; class BiomeGenerationPass { @@ -120,6 +124,61 @@ public: ) const; }; +class HoleFillPass { +private: + const GenerationConfig &config_; + +public: + /** + * @brief Construct a hole fill pass + * @param config Generation configuration parameters + */ + explicit HoleFillPass(const GenerationConfig &config); + + /** + * @brief Fill small holes in the terrain using BFS + * @param tilemap The tilemap to process + */ + void operator()(TileMap &tilemap); + +private: + /** + * @brief Check if a tile type is passable for BFS + * @param type The base tile type to check + * @return True if the tile is passable (not mountain or at boundary) + */ + bool is_passable(BaseTileType type) const; + + /** + * @brief Perform BFS to find connected component size + * @param tilemap The tilemap to search + * @param start_pos Starting position for BFS + * @param visited 2D array tracking visited tiles + * @param positions Output vector of positions in this component + * @return Size of the connected component + */ + std::uint32_t bfs_component_size( + TileMap &tilemap, TilePos start_pos, + std::vector> &visited, std::vector &positions + ); + + /** + * @brief Get all valid neighbors of a position + * @param tilemap The tilemap for bounds checking + * @param pos The position to get neighbors for + * @return Vector of valid neighbor positions + */ + std::vector get_neighbors(TileMap &tilemap, TilePos pos) const; + + /** + * @brief Check if a position is at the map boundary + * @param tilemap The tilemap for bounds checking + * @param pos The position to check + * @return True if the position is at the boundary + */ + bool is_at_boundary(TileMap &tilemap, TilePos pos) const; +}; + // Terrain generator class that manages the generation process class TerrainGenerator { private: @@ -151,6 +210,12 @@ private: * @param tilemap The tilemap to generate base types into */ void base_tile_type_pass(TileMap &tilemap); + + /** + * @brief Fill small holes in the terrain + * @param tilemap The tilemap to process + */ + void hole_fill_pass(TileMap &tilemap); }; /** diff --git a/tilemap/src/generation.cpp b/tilemap/src/generation.cpp index a635b86..6de2ec8 100644 --- a/tilemap/src/generation.cpp +++ b/tilemap/src/generation.cpp @@ -164,6 +164,168 @@ BaseTileType BaseTileTypeGenerationPass::determine_base_type( std::unreachable(); } +HoleFillPass::HoleFillPass(const GenerationConfig &config): config_(config) {} + +void HoleFillPass::operator()(TileMap &tilemap) { + std::uint8_t map_size = tilemap.get_size(); + std::uint32_t total_tiles = map_size * Chunk::size; + + // Create visited array for the entire map + std::vector> visited( + total_tiles, std::vector(total_tiles, false) + ); + + // Process all tiles in the map + for (std::uint8_t chunk_x = 0; chunk_x < map_size; ++chunk_x) { + for (std::uint8_t chunk_y = 0; chunk_y < map_size; ++chunk_y) { + for (std::uint8_t local_x = 0; local_x < Chunk::size; ++local_x) { + for (std::uint8_t local_y = 0; local_y < Chunk::size; + ++local_y) { + TilePos pos{chunk_x, chunk_y, local_x, local_y}; + std::uint32_t global_x = chunk_x * Chunk::size + local_x; + std::uint32_t global_y = chunk_y * Chunk::size + local_y; + + // Skip if already visited + if (visited[global_x][global_y]) { + continue; + } + + const Tile &tile = tilemap.get_tile(pos); + + // Only process passable tiles + if (!is_passable(tile.base)) { + visited[global_x][global_y] = true; + continue; + } + + // Find connected component + std::vector component_positions; + std::uint32_t component_size = bfs_component_size( + tilemap, pos, visited, component_positions + ); + + // Check if this component touches the boundary + bool touches_boundary = false; + for (const TilePos &component_pos : component_positions) { + if (is_at_boundary(tilemap, component_pos)) { + touches_boundary = true; + break; + } + } + + // Fill small holes that don't touch the boundary + if (!touches_boundary + && component_size < config_.fill_threshold) { + for (const TilePos &fill_pos : component_positions) { + Tile fill_tile = tilemap.get_tile(fill_pos); + fill_tile.base = BaseTileType::Mountain; + tilemap.set_tile(fill_pos, fill_tile); + } + } + } + } + } + } +} + +bool HoleFillPass::is_passable(BaseTileType type) const { + return type != BaseTileType::Mountain; +} + +std::uint32_t HoleFillPass::bfs_component_size( + TileMap &tilemap, TilePos start_pos, + std::vector> &visited, std::vector &positions +) { + std::queue queue; + queue.push(start_pos); + + std::uint8_t map_size = tilemap.get_size(); + std::uint32_t start_global_x + = start_pos.chunk_x * Chunk::size + start_pos.local_x; + std::uint32_t start_global_y + = start_pos.chunk_y * Chunk::size + start_pos.local_y; + visited[start_global_x][start_global_y] = true; + + std::uint32_t size = 0; + positions.clear(); + + while (!queue.empty()) { + TilePos current = queue.front(); + queue.pop(); + positions.push_back(current); + ++size; + + // Check all neighbors + std::vector neighbors = get_neighbors(tilemap, current); + for (const TilePos &neighbor : neighbors) { + std::uint32_t neighbor_global_x + = neighbor.chunk_x * Chunk::size + neighbor.local_x; + std::uint32_t neighbor_global_y + = neighbor.chunk_y * Chunk::size + neighbor.local_y; + + if (visited[neighbor_global_x][neighbor_global_y]) { + continue; + } + + const Tile &neighbor_tile = tilemap.get_tile(neighbor); + if (is_passable(neighbor_tile.base)) { + visited[neighbor_global_x][neighbor_global_y] = true; + queue.push(neighbor); + } + } + } + + return size; +} + +std::vector HoleFillPass::get_neighbors( + TileMap &tilemap, TilePos pos +) const { + std::vector neighbors; + std::uint8_t map_size = tilemap.get_size(); + + // Calculate global coordinates + std::uint32_t global_x = pos.chunk_x * Chunk::size + pos.local_x; + std::uint32_t global_y = pos.chunk_y * Chunk::size + pos.local_y; + std::uint32_t max_global = map_size * Chunk::size; + + // Four cardinal directions + const int dx[] = {-1, 1, 0, 0}; + const int dy[] = {0, 0, -1, 1}; + + for (int i = 0; i < 4; ++i) { + int new_global_x = static_cast(global_x) + dx[i]; + int new_global_y = static_cast(global_y) + dy[i]; + + // Check bounds + if (new_global_x >= 0 && new_global_x < static_cast(max_global) + && new_global_y >= 0 + && new_global_y < static_cast(max_global)) { + // Convert back to chunk and local coordinates + std::uint8_t new_chunk_x = new_global_x / Chunk::size; + std::uint8_t new_chunk_y = new_global_y / Chunk::size; + std::uint8_t new_local_x = new_global_x % Chunk::size; + std::uint8_t new_local_y = new_global_y % Chunk::size; + + neighbors.push_back( + {new_chunk_x, new_chunk_y, new_local_x, new_local_y} + ); + } + } + + return neighbors; +} + +bool HoleFillPass::is_at_boundary(TileMap &tilemap, TilePos pos) const { + std::uint8_t map_size = tilemap.get_size(); + std::uint32_t global_x = pos.chunk_x * Chunk::size + pos.local_x; + std::uint32_t global_y = pos.chunk_y * Chunk::size + pos.local_y; + std::uint32_t max_global = map_size * Chunk::size - 1; + + return global_x == 0 || global_x == max_global || global_y == 0 + || global_y == max_global; +} + TerrainGenerator::TerrainGenerator(const GenerationConfig &config) : config_(config), master_rng_(config.seed) {} @@ -184,6 +346,9 @@ void TerrainGenerator::operator()(TileMap &tilemap) { // Then, generate base tile types based on biomes base_tile_type_pass(tilemap); + + // Finally, fill small holes in the terrain + hole_fill_pass(tilemap); } void TerrainGenerator::base_tile_type_pass(TileMap &tilemap) { @@ -192,6 +357,11 @@ void TerrainGenerator::base_tile_type_pass(TileMap &tilemap) { pass(tilemap); } +void TerrainGenerator::hole_fill_pass(TileMap &tilemap) { + HoleFillPass pass(config_); + pass(tilemap); +} + void map_generate(TileMap &tilemap, const GenerationConfig &config) { TerrainGenerator generator(config); generator(tilemap);