├── .gitignore ├── src ├── CostMap.hpp ├── Util.hpp ├── VersionConf.hpp.in ├── TileType.hpp ├── DesirePaths.hpp ├── Version.hpp ├── Bitmap.hpp ├── WorldMap.hpp ├── Options.hpp ├── DesirePaths.cpp ├── Queue.hpp ├── UpdateRect.hpp ├── WorldGen.hpp ├── DesirePathSim.hpp ├── Villager.hpp ├── main.cpp ├── Map.hpp ├── Voronoi.hpp ├── Bitmap.cpp ├── Villager.cpp ├── Pathfinding.hpp ├── DesirePathSim.cpp └── WorldGen.cpp ├── CMakeLists.txt ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /.vs 2 | /out/build 3 | /CMakeSettings.json 4 | -------------------------------------------------------------------------------- /src/CostMap.hpp: -------------------------------------------------------------------------------- 1 | #ifndef COSTMAP_HPP 2 | #define COSTMAP_HPP 3 | 4 | #include "Map.hpp" 5 | 6 | using CostMap = Map; 7 | 8 | #endif // COSTMAP_HPP 9 | -------------------------------------------------------------------------------- /src/Util.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UTIL_HPP 2 | #define UTIL_HPP 3 | 4 | template 5 | inline T absDiff(const T a, const T b) 6 | { 7 | return (a >= b) ? (a - b) : (b - a); 8 | } 9 | 10 | #endif // UTIL_HPP 11 | -------------------------------------------------------------------------------- /src/VersionConf.hpp.in: -------------------------------------------------------------------------------- 1 | #ifndef VERSIONCONF_HPP 2 | #define VERSIONCONF_HPP 3 | 4 | #define DPS_APP_NAME "@PROJECT_NAME@" 5 | 6 | #define DPS_VERSION_MAJOR @PROJECT_VERSION_MAJOR@ 7 | #define DPS_VERSION_MINOR @PROJECT_VERSION_MINOR@ 8 | #define DPS_VERSION_PATCH @PROJECT_VERSION_PATCH@ 9 | 10 | #endif // VERSIONCONF_HPP 11 | -------------------------------------------------------------------------------- /src/TileType.hpp: -------------------------------------------------------------------------------- 1 | #ifndef TILETYPE_HPP 2 | #define TILETYPE_HPP 3 | 4 | #include 5 | 6 | enum class TileType : std::uint8_t 7 | { 8 | Grass = 0, 9 | Street = 1, 10 | Building = 2, 11 | BuildingEntrance = 3, 12 | Tree = 4, 13 | Water = 5, 14 | Temporary = 253, 15 | 16 | PatternAny = 254, 17 | 18 | PatchKeep = 255 19 | }; 20 | 21 | #endif // TILETYPE_HPP 22 | -------------------------------------------------------------------------------- /src/DesirePaths.hpp: -------------------------------------------------------------------------------- 1 | #ifndef DESIREPATHS_HPP 2 | #define DESIREPATHS_HPP 3 | 4 | #include 5 | 6 | #include "Map.hpp" 7 | #include "CostMap.hpp" 8 | 9 | using DesirePathsMap = Map; 10 | 11 | std::uint8_t adjustDesirePathStress(const std::size_t tileX, const std::size_t tileY, DesirePathsMap& desirePathsMap, CostMap& baseCostMap, const int adjustment); 12 | 13 | void decayDesirePaths(DesirePathsMap& desirePathsMap, CostMap& baseCostMap); 14 | 15 | #endif // DESIREPATHS_HPP 16 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.24) 2 | 3 | project(DesirePathSim LANGUAGES CXX VERSION 0.1.0) 4 | 5 | find_package(raylib 4.2 REQUIRED) 6 | 7 | configure_file("src/VersionConf.hpp.in" "VersionConf.hpp" @ONLY) 8 | 9 | add_executable(${PROJECT_NAME} "src/main.cpp" "src/Bitmap.cpp" "src/Villager.cpp" "src/DesirePaths.cpp" "src/DesirePathSim.cpp" "src/WorldGen.cpp") 10 | 11 | target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17) 12 | 13 | target_include_directories(${PROJECT_NAME} PRIVATE ${PROJECT_BINARY_DIR}) 14 | 15 | target_link_libraries(${PROJECT_NAME} PRIVATE raylib) 16 | 17 | set_target_properties(${PROJECT_NAME} PROPERTIES MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") 18 | -------------------------------------------------------------------------------- /src/Version.hpp: -------------------------------------------------------------------------------- 1 | #ifndef VERSION_HPP 2 | #define VERSION_HPP 3 | 4 | #include "VersionConf.hpp" 5 | 6 | #define DPS_STRINGIFY_IMPL(N) #N 7 | #define DPS_STRINGIFY(N) DPS_STRINGIFY_IMPL(N) 8 | 9 | constexpr const char* getAppName() 10 | { 11 | return DPS_APP_NAME; 12 | } 13 | 14 | constexpr const char* getVersionStr() 15 | { 16 | return DPS_STRINGIFY(DPS_VERSION_MAJOR) "." DPS_STRINGIFY(DPS_VERSION_MINOR) "." DPS_STRINGIFY(DPS_VERSION_PATCH); 17 | } 18 | 19 | constexpr const char* getAppNameWithVersion() 20 | { 21 | return DPS_APP_NAME " v" DPS_STRINGIFY(DPS_VERSION_MAJOR) "." DPS_STRINGIFY(DPS_VERSION_MINOR) "." DPS_STRINGIFY(DPS_VERSION_PATCH); 22 | } 23 | 24 | #undef DPS_STRINGIFY 25 | #undef DPS_STRINGIFY_IMPL 26 | 27 | #endif // VERSION_HPP 28 | -------------------------------------------------------------------------------- /src/Bitmap.hpp: -------------------------------------------------------------------------------- 1 | #ifndef BITMAP_HPP 2 | #define BITMAP_HPP 3 | 4 | #include "Map.hpp" 5 | #include 6 | using Bitmap = Map; 7 | 8 | namespace BitmapTransform 9 | { 10 | Bitmap erode(const Bitmap& bitmap, const bool includeDiagonals); 11 | 12 | Bitmap dilate(const Bitmap& bitmap, const bool includeDiagonals); 13 | 14 | Bitmap border(const Bitmap& bitmap, const bool includeDiagonals); 15 | 16 | Bitmap shift(const Bitmap& bitmap, const int deltaX, const int deltaY, const uint8_t fill); 17 | 18 | Bitmap invert(const Bitmap& bitmap); 19 | 20 | // Sets bitmap to 0 where mask is 1 21 | Bitmap mask(const Bitmap& bitmap, const Bitmap& mask, const int bitmapBOffsetX, const int bitmapBOffsetY); 22 | } // namespace BitmapTransform 23 | 24 | #endif // BITMAP_HPP 25 | -------------------------------------------------------------------------------- /src/WorldMap.hpp: -------------------------------------------------------------------------------- 1 | #ifndef WORLDMAP_HPP 2 | #define WORLDMAP_HPP 3 | 4 | #include 5 | 6 | #include "Map.hpp" 7 | #include "TileType.hpp" 8 | 9 | using WorldMap = Map; 10 | 11 | inline std::uint8_t getBaseCostForValue(const TileType tileType, std::mt19937_64& rng) 12 | { 13 | switch (tileType) 14 | { 15 | case TileType::Grass : return std::uniform_int_distribution<>{14, 16}(rng); break; 16 | case TileType::Street : return 10; break; 17 | case TileType::Building : return 0; break; 18 | case TileType::BuildingEntrance: return 12; break; 19 | case TileType::Tree : return 0; break; 20 | case TileType::Water : return 100; break; 21 | default : return 0; 22 | } 23 | } 24 | 25 | #endif // WORLDMAP_HPP 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Dienes 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 | -------------------------------------------------------------------------------- /src/Options.hpp: -------------------------------------------------------------------------------- 1 | #ifndef OPTIONS_HPP 2 | #define OPTIONS_HPP 3 | 4 | #include 5 | 6 | struct Options final 7 | { 8 | int screenWidthPixels = 1920; 9 | int screenHeightPixels = 1080; 10 | 11 | int targetFPS = 60; // 0 to disable 12 | 13 | bool removeStreetsAfterGeneration = false; 14 | 15 | std::size_t villagerCount = 1000; 16 | 17 | std::size_t voronoiCentroidCountPerLevel = 6; 18 | int voronoiSubdivideProbabilityLevel1 = 90; 19 | int voronoiSubdivideProbabilityLevel2 = 80; 20 | float voronoiLevel0MinkowskiP = 3.0f; // 3.0f for more curved long streets on top level 21 | float voronoiLevel1MinkowskiP = 3.0f; // 3.0f for more curved streets on level 1 22 | float voronoiLevel2MinkowskiP = 1.0f; // 1.0f for more straight streets on level 2 23 | 24 | bool placeRoundabouts = false; 25 | bool placePonds = true; 26 | bool placeFullPavedAreas = true; 27 | bool placeLargeTrees = true; 28 | bool placeSmallTrees = true; 29 | 30 | std::size_t pathfindingThreadCount = 4; 31 | 32 | bool paveDesirePaths = true; 33 | bool decayDesirePaths = true; 34 | }; 35 | 36 | #endif // OPTIONS_HPP 37 | -------------------------------------------------------------------------------- /src/DesirePaths.cpp: -------------------------------------------------------------------------------- 1 | #include "DesirePaths.hpp" 2 | 3 | #include 4 | 5 | std::uint8_t adjustDesirePathStress(const std::size_t tileX, const std::size_t tileY, DesirePathsMap& desirePathsMap, CostMap& baseCostMap, const int adjustment) 6 | { 7 | const auto valueBefore = desirePathsMap.at(tileX, tileY); 8 | const auto valueAfter = static_cast(std::clamp(valueBefore + adjustment, 0, 255)); 9 | 10 | if (valueBefore != valueAfter) 11 | { 12 | desirePathsMap.at(tileX, tileY) = valueAfter; 13 | 14 | const auto baseCostAdjustmentBefore = -(valueBefore / 64); 15 | const auto baseCostAdjustmentAfter = -(valueAfter / 64); 16 | 17 | baseCostMap.at(tileX, tileY) = baseCostMap.at(tileX, tileY) - baseCostAdjustmentBefore + baseCostAdjustmentAfter; 18 | } 19 | 20 | return valueAfter; 21 | } 22 | 23 | void decayDesirePaths(DesirePathsMap& desirePathsMap, CostMap& baseCostMap) 24 | { 25 | for (std::size_t worldTileY = 0; worldTileY < desirePathsMap.height(); ++worldTileY) 26 | { 27 | for (std::size_t worldTileX = 0; worldTileX < desirePathsMap.width(); ++worldTileX) 28 | { 29 | adjustDesirePathStress(worldTileX, worldTileY, desirePathsMap, baseCostMap, -1); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Queue.hpp: -------------------------------------------------------------------------------- 1 | #ifndef QUEUE_HPP 2 | #define QUEUE_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | template 11 | class Queue final 12 | { 13 | public: 14 | Queue() = default; 15 | 16 | Queue(const Queue&) = default; 17 | Queue(Queue&&) noexcept = default; 18 | 19 | ~Queue() = default; 20 | 21 | Queue& operator=(const Queue&) = default; 22 | Queue& operator=(Queue&&) noexcept = default; 23 | 24 | void push(T value); 25 | T pop(); 26 | std::optional tryPopFor(std::chrono::milliseconds duration); 27 | 28 | private: 29 | std::queue m_queue; 30 | 31 | std::mutex m_mutex; 32 | std::condition_variable m_cond; 33 | }; 34 | 35 | template 36 | void Queue::push(T value) 37 | { 38 | { 39 | std::lock_guard lock{m_mutex}; 40 | 41 | m_queue.push(value); 42 | } 43 | 44 | m_cond.notify_one(); 45 | } 46 | 47 | template 48 | T Queue::pop() 49 | { 50 | std::unique_lock lock{m_mutex}; 51 | 52 | m_cond.wait(lock, [&] { return m_queue.empty() == false; }); 53 | 54 | T value = m_queue.front(); 55 | 56 | m_queue.pop(); 57 | 58 | return value; 59 | } 60 | 61 | template 62 | std::optional Queue::tryPopFor(std::chrono::milliseconds duration) 63 | { 64 | std::unique_lock lock{m_mutex}; 65 | 66 | if (m_cond.wait_for(lock, duration, [&] { return m_queue.empty() == false; }) == false) 67 | return std::nullopt; 68 | 69 | T value = m_queue.front(); 70 | 71 | m_queue.pop(); 72 | 73 | return value; 74 | } 75 | 76 | #endif // QUEUE_HPP 77 | -------------------------------------------------------------------------------- /src/UpdateRect.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UPDATERECT_HPP 2 | #define UPDATERECT_HPP 3 | 4 | #include 5 | 6 | struct UpdateRect final 7 | { 8 | std::size_t left = 0; 9 | std::size_t top = 0; 10 | std::size_t right = 0; 11 | std::size_t bottom = 0; 12 | 13 | UpdateRect() = default; 14 | 15 | UpdateRect(const std::size_t left, const std::size_t top, const std::size_t right, const std::size_t bottom); 16 | 17 | void reset(); 18 | 19 | bool empty() const; 20 | 21 | void add(const std::size_t x, const std::size_t y); 22 | 23 | std::size_t width() const; 24 | std::size_t height() const; 25 | }; 26 | 27 | inline UpdateRect::UpdateRect(const std::size_t left, const std::size_t top, const std::size_t right, const std::size_t bottom): 28 | left {left }, 29 | top {top }, 30 | right {right }, 31 | bottom{bottom} 32 | { 33 | // NOP 34 | } 35 | 36 | inline void UpdateRect::reset() 37 | { 38 | left = 0; 39 | top = 0; 40 | right = 0; 41 | bottom = 0; 42 | } 43 | 44 | inline bool UpdateRect::empty() const 45 | { 46 | return ((left + top + right + bottom) == 0); 47 | } 48 | 49 | inline void UpdateRect::add(const std::size_t x, const std::size_t y) 50 | { 51 | if (empty() == false) 52 | { 53 | left = std::min(left , x); 54 | top = std::min(top , y); 55 | right = std::max(right , x); 56 | bottom = std::max(bottom, y); 57 | } 58 | else 59 | { 60 | left = x; 61 | top = y; 62 | right = x; 63 | bottom = y; 64 | } 65 | } 66 | 67 | inline std::size_t UpdateRect::width() const 68 | { 69 | return (right - left + 1); 70 | } 71 | 72 | inline std::size_t UpdateRect::height() const 73 | { 74 | return (bottom - top + 1); 75 | } 76 | 77 | #endif // UPDATERECT_HPP 78 | -------------------------------------------------------------------------------- /src/WorldGen.hpp: -------------------------------------------------------------------------------- 1 | #ifndef WORLDGEN_HPP 2 | #define WORLDGEN_HPP 3 | 4 | #include 5 | 6 | #include "WorldMap.hpp" 7 | #include "Voronoi.hpp" 8 | 9 | class WorldGen final 10 | { 11 | public: 12 | using Pattern = Map; 13 | using Patch = Map; 14 | 15 | public: 16 | WorldGen(WorldMap& worldMap, std::mt19937_64& rng); 17 | 18 | WorldGen(const WorldGen&) = default; 19 | WorldGen(WorldGen&&) noexcept = default; 20 | 21 | ~WorldGen() = default; 22 | 23 | WorldGen& operator=(const WorldGen&) = default; 24 | WorldGen& operator=(WorldGen&&) noexcept = default; 25 | 26 | void placeStreetsFromVoronoiMap(const VoronoiMap& voronoiMap); 27 | 28 | void placePonds(const float fillLimitInPercent, const float areaSizeLimitInPercent); 29 | 30 | void placeFullPavedAreas(const float fillLimitInPercent, const float areaSizeLimitInPercent); 31 | 32 | void replaceAll(const TileType replaceWhat, const TileType replaceWith); 33 | 34 | void placeBuildings(const int fillRate = 100); 35 | 36 | void placeLargeTrees(const int fillRate = 100); 37 | void placeSmallTrees(const int fillRate = 100); 38 | 39 | void placeRoundaboutsA(); 40 | void placeRoundaboutsB(); 41 | 42 | private: 43 | static constexpr int m_rngScaledPercentFactor = 1'000; 44 | 45 | WorldMap& m_worldMap; 46 | 47 | std::mt19937_64& m_rng; 48 | std::uniform_int_distribution<> m_rngScaledPercent; // Scaled by m_rngScaledPercentFactor 49 | std::uniform_int_distribution m_rngWorldMapWidth; 50 | std::uniform_int_distribution m_rngWorldMapHeight; 51 | 52 | bool findPattern(const Pattern& pattern, std::size_t& x, std::size_t& y, const std::size_t startX = 0, const std::size_t startY = 0) const; 53 | 54 | void applyPatch(const Patch& patch, const std::size_t x, const std::size_t y); 55 | 56 | std::size_t floodFill(const std::size_t x, const std::size_t y, const TileType fillTileType); 57 | }; 58 | 59 | #endif // WORLDGEN_HPP 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DesirePathSim 2 | Simulates desire paths by sending people through a randomly generated village 3 | 4 | A random set of streets and houses is generated using nested Voronoi patterns. People spawn at houses and move to other houses using A* pathfinding. Along the way, they might leave streets, walk through the woods or cut corners to get to their destination faster. Over time, this creates "desire paths" which start out as dirt roads, but eventually get paved into actual streets, if enough people use it. 5 | 6 | Project uses C++17 and [Raylib](https://www.raylib.com/) for rendering. 7 | 8 | # Command line arguments 9 | |Argument|Description| 10 | |--------|-----------| 11 | |`-width=`|Screen width in pixels (default 1920)| 12 | |`-height=`|Screen height in pixels (default 1080)| 13 | |`-target_fps=`|Limit FPS (default 60)| 14 | |`-remove_streets=<1/0>`|Remove streets after world generation and let all paths be desire paths (default 0)| 15 | |`-villager_count=`|How many villagers to spawn (default 1000)| 16 | |`-centroid_count=`|How many centroids to use per Voronoi pattern (default 6)| 17 | |`-subdiv_prob_1=<1-100>`|Probability that a Voronoi shape on level 0 receives a subdivision (default 90)| 18 | |`-subdiv_prob_2=<1-100>`|Probability that a Voronoi shape on level 1 receives a subdivision (default 60)| 19 | |`-minkowski_0=`|Minkowski P value for Voronoi generation on level 0 (default 3)| 20 | |`-minkowski_1=`|Minkowski P value for Voronoi generation on level 1 (default 3)| 21 | |`-minkowski_2=`|Minkowski P value for Voronoi generation on level 2 (default 1)| 22 | |`-place_roundabouts=<1/0>`|Place roundabouts during generation (default 0)| 23 | |`-place_ponds=<1/0>`|Place ponds during generation (default 1)| 24 | |`-place_full_paved_areas=<1/0>`|Place fully paved areas during generation (default 1)| 25 | |`-place_large_trees=<1/0>`|Place large trees during generation (default 1)| 26 | |`-place_small_trees=<1/0>`|Place small trees during generation (default 1)| 27 | |`-pathfinding_threads=`|Number of threads to use for pathfinding (default 4)| 28 | |`-pave_desire_paths=<1/0>`|Pave heavily used desires paths (default 1)| 29 | |`-decay_desire_paths=<1/0>`|Decay underused desire paths (default 1)| 30 | -------------------------------------------------------------------------------- /src/DesirePathSim.hpp: -------------------------------------------------------------------------------- 1 | #ifndef DESIREPATHSIM_HPP 2 | #define DESIREPATHSIM_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | #include "Options.hpp" 11 | #include "Voronoi.hpp" 12 | #include "WorldMap.hpp" 13 | #include "DesirePaths.hpp" 14 | #include "Bitmap.hpp" 15 | #include "Villager.hpp" 16 | #include "UpdateRect.hpp" 17 | #include "Queue.hpp" 18 | 19 | class DesirePathSim final 20 | { 21 | public: 22 | DesirePathSim(const Options& options); 23 | 24 | DesirePathSim(const DesirePathSim&) = default; 25 | DesirePathSim(DesirePathSim&&) = default; 26 | 27 | ~DesirePathSim(); 28 | 29 | DesirePathSim& operator=(const DesirePathSim&) = default; 30 | DesirePathSim& operator=(DesirePathSim&&) = default; 31 | 32 | void run(); 33 | 34 | private: 35 | const Options& m_options; 36 | 37 | // Assigns a color to each Voronoi Centroid 38 | using VoronoiColorTable = std::vector; 39 | 40 | static constexpr auto m_tileWidthPixels = 6; 41 | static constexpr auto m_tileHeightPixels = 6; 42 | 43 | int m_worldWidthTiles; 44 | int m_worldHeightTiles; 45 | 46 | int m_worldWidthPixels; 47 | int m_worldHeightPixels; 48 | 49 | std::mt19937_64 m_baseRNG; 50 | 51 | // For each world tile, stores the index of the closest Centroid 52 | VoronoiMap m_voronoiMap; 53 | 54 | VoronoiColorTable m_voronoiColorTable = {}; 55 | 56 | WorldMap m_worldMap; 57 | 58 | CostMap m_baseCostMap; 59 | 60 | DesirePathsMap m_desirePathsMap; 61 | 62 | Bitmap m_shadowBitmap; 63 | 64 | std::vector m_villagers; 65 | 66 | Camera2D m_camera = {0}; 67 | 68 | RenderTexture2D m_worldMapTexture = {}; 69 | RenderTexture2D m_shadowMapTexture = {}; 70 | RenderTexture2D m_desirePathsMapTexture = {}; 71 | 72 | // Villagers waiting for new path are enqueued here 73 | Queue m_pathfindingQueue; 74 | std::atomic m_stopPathfindingThread; 75 | 76 | private: 77 | void tickVillagers(UpdateRect& mapUpdateRect, const float delta); 78 | 79 | void updateCamera(const float delta); 80 | 81 | void drawVoronoiMap(); 82 | 83 | void updateWorldMapTexture(RenderTexture2D& texture, UpdateRect& updateRect); 84 | void updateWorldMapTexture(RenderTexture2D& texture); 85 | 86 | void updateShadowMapTexture(RenderTexture2D& texture, const Bitmap& shadowBitmap); 87 | 88 | void updateDesirePathsMapTexture(RenderTexture2D& texture, const UpdateRect& updateRect); 89 | void updateDesirePathsMapTexture(RenderTexture2D& texture); 90 | 91 | void updateBaseCostMap(); 92 | void updateShadowBitmap(); 93 | }; 94 | 95 | #endif // DESIREPATHSIM_HPP 96 | -------------------------------------------------------------------------------- /src/Villager.hpp: -------------------------------------------------------------------------------- 1 | #ifndef VILLAGER_HPP 2 | #define VILLAGER_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | #include "WorldMap.hpp" 12 | #include "UpdateRect.hpp" 13 | #include "CostMap.hpp" 14 | #include "DesirePaths.hpp" 15 | #include "Pathfinding.hpp" 16 | 17 | class Villager final 18 | { 19 | public: 20 | enum class State 21 | { 22 | AwaitingPath, // Villager has no path and is not yet enqueued for pathfinding 23 | EnqueuedForPath, // Villager has been put into pathfinding queue 24 | PathProvided, // Villager has been assigned a path but is not yet moving 25 | Moving // Villager is moving along its current path 26 | }; 27 | 28 | public: 29 | Villager() = default; 30 | 31 | Villager(const Villager&) = default; 32 | Villager(Villager&&) noexcept = default; 33 | 34 | ~Villager() = default; 35 | 36 | Villager& operator=(const Villager&) = default; 37 | Villager& operator=(Villager&&) noexcept = default; 38 | 39 | Vector2 m_position = {0.0f, 0.0f}; 40 | 41 | float m_movementPixelPerSec = 50.0f; 42 | 43 | Color m_color = {0, 0, 0, 255}; 44 | 45 | State getState() const 46 | { 47 | return m_state.load(); 48 | } 49 | 50 | void setState(const State state) 51 | { 52 | m_state.store(state); 53 | } 54 | 55 | void tick( 56 | WorldMap& worldMap, 57 | UpdateRect& mapUpdateRect, 58 | CostMap& baseCostMap, 59 | DesirePathsMap& desirePathsMap, 60 | const bool pavePaths, 61 | const int tileWidthPixels, 62 | const int tileHeightPixels, 63 | std::mt19937_64& rng, 64 | const float delta 65 | ); 66 | 67 | void reset( 68 | const WorldMap& worldMap, 69 | Pathfinder& pathfinder, 70 | const CostMap& baseCostMap, 71 | const int tileWidthPixels, 72 | const int tileHeightPixels, 73 | std::mt19937_64& rng 74 | ); 75 | 76 | private: 77 | std::vector> m_path; 78 | 79 | Vector2 m_tileToTileMovementStart = {0.0f, 0.0f}; 80 | Vector2 m_tileToTileMovementTarget = {0.0f, 0.0f}; 81 | Vector2 m_tileToTileMovementDirection = {0.0f, 0.0f}; 82 | float m_tileToTileMovementDistance = 0.0f; 83 | 84 | std::size_t m_currentPathIndex = 0; 85 | 86 | std::atomic m_state = State::AwaitingPath; 87 | 88 | void moveOntoTile( 89 | const std::size_t tileX, 90 | const std::size_t tileY, 91 | WorldMap& worldMap, 92 | UpdateRect& mapUpdateRect, 93 | CostMap& baseCostMap, 94 | DesirePathsMap& desirePathsMap, 95 | const bool pavePaths, 96 | const int tileWidthPixels, 97 | const int tileHeightPixels, 98 | std::mt19937_64& rng 99 | ); 100 | 101 | void paveTile( 102 | const std::size_t tileX, 103 | const std::size_t tileY, 104 | WorldMap& worldMap, 105 | UpdateRect& mapUpdateRect, 106 | CostMap& baseCostMap, 107 | DesirePathsMap& desirePathsMap, 108 | const int tileWidthPixels, 109 | const int tileHeightPixels, 110 | std::mt19937_64& rng 111 | ); 112 | }; 113 | 114 | #endif // VILLAGER_HPP 115 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "Options.hpp" 5 | #include "DesirePathSim.hpp" 6 | 7 | std::optional tryReadArgInt(const std::string& arg, const std::string& name) try 8 | { 9 | std::string argStart = '-' + name + '='; 10 | 11 | if (arg.find(argStart) == 0) 12 | return std::stoi(arg.substr(argStart.length())); 13 | 14 | return std::nullopt; 15 | } 16 | catch (...) 17 | { 18 | throw std::runtime_error{"Error parsing argument \"" + arg + "\""}; 19 | } 20 | 21 | std::optional tryReadArgBool(const std::string& arg, const std::string& name) 22 | { 23 | const auto v = tryReadArgInt(arg, name); 24 | 25 | if (v.has_value() == false) 26 | return std::nullopt; 27 | 28 | return (v.value() != 0); 29 | } 30 | 31 | int main(int argc, char** argv) 32 | { 33 | Options options; 34 | 35 | try 36 | { 37 | for (int argIndex = 1; argIndex < argc; ++argIndex) 38 | { 39 | std::string arg = argv[argIndex]; 40 | 41 | if (auto v = tryReadArgInt (arg, "width" ); v.has_value()) options.screenWidthPixels = v.value(); 42 | else if (auto v = tryReadArgInt (arg, "height" ); v.has_value()) options.screenHeightPixels = v.value(); 43 | else if (auto v = tryReadArgInt (arg, "target_fps" ); v.has_value()) options.targetFPS = v.value(); 44 | else if (auto v = tryReadArgBool(arg, "remove_streets" ); v.has_value()) options.removeStreetsAfterGeneration = v.value(); 45 | else if (auto v = tryReadArgInt (arg, "villager_count" ); v.has_value()) options.villagerCount = v.value(); 46 | else if (auto v = tryReadArgInt (arg, "centroid_count" ); v.has_value()) options.voronoiCentroidCountPerLevel = v.value(); 47 | else if (auto v = tryReadArgInt (arg, "subdiv_prob_1" ); v.has_value()) options.voronoiSubdivideProbabilityLevel1 = v.value(); 48 | else if (auto v = tryReadArgInt (arg, "subdiv_prob_2" ); v.has_value()) options.voronoiSubdivideProbabilityLevel2 = v.value(); 49 | else if (auto v = tryReadArgInt (arg, "minkowski_0" ); v.has_value()) options.voronoiLevel0MinkowskiP = static_cast(v.value()); 50 | else if (auto v = tryReadArgInt (arg, "minkowski_1" ); v.has_value()) options.voronoiLevel1MinkowskiP = static_cast(v.value()); 51 | else if (auto v = tryReadArgInt (arg, "minkowski_2" ); v.has_value()) options.voronoiLevel2MinkowskiP = static_cast(v.value()); 52 | else if (auto v = tryReadArgBool(arg, "place_roundabouts" ); v.has_value()) options.placeRoundabouts = v.value(); 53 | else if (auto v = tryReadArgBool(arg, "place_ponds" ); v.has_value()) options.placePonds = v.value(); 54 | else if (auto v = tryReadArgBool(arg, "place_full_paved_areas"); v.has_value()) options.placeFullPavedAreas = v.value(); 55 | else if (auto v = tryReadArgBool(arg, "place_large_trees" ); v.has_value()) options.placeLargeTrees = v.value(); 56 | else if (auto v = tryReadArgBool(arg, "place_small_trees" ); v.has_value()) options.placeSmallTrees = v.value(); 57 | else if (auto v = tryReadArgInt (arg, "pathfinding_threads" ); v.has_value()) options.pathfindingThreadCount = v.value(); 58 | else if (auto v = tryReadArgBool(arg, "pave_desire_paths" ); v.has_value()) options.paveDesirePaths = v.value(); 59 | else if (auto v = tryReadArgBool(arg, "decay_desire_paths" ); v.has_value()) options.decayDesirePaths = v.value(); 60 | } 61 | } 62 | catch (const std::exception& ex) 63 | { 64 | std::cerr << ex.what(); 65 | return EXIT_FAILURE; 66 | } 67 | 68 | DesirePathSim desirePathSim{options}; 69 | 70 | desirePathSim.run(); 71 | 72 | return EXIT_SUCCESS; 73 | } 74 | -------------------------------------------------------------------------------- /src/Map.hpp: -------------------------------------------------------------------------------- 1 | #ifndef MAP_HPP 2 | #define MAP_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | template 10 | class Map 11 | { 12 | public: 13 | Map(const std::size_t width, const std::size_t height, const T& fill = T{}); 14 | Map(std::initializer_list> values); 15 | 16 | Map(const Map&) = default; 17 | Map(Map&&) noexcept = default; 18 | 19 | virtual ~Map() = default; 20 | 21 | Map& operator=(const Map&) = default; 22 | Map& operator=(Map&&) noexcept = default; 23 | 24 | const T& at(const std::size_t x, const std::size_t y) const; 25 | T& at(const std::size_t x, const std::size_t y); 26 | 27 | std::size_t width() const; 28 | std::size_t height() const; 29 | 30 | Map rotated90CW() const; 31 | Map rotated90CCW() const; 32 | Map rotated180() const; 33 | 34 | std::optional> 35 | find(const T value, const std::size_t startX = 0, const std::size_t startY = 0, const bool wrap = false) const; 36 | 37 | protected: 38 | std::size_t m_width; 39 | std::size_t m_height; 40 | 41 | std::vector m_values; 42 | }; 43 | 44 | template 45 | Map::Map(const std::size_t width, const std::size_t height, const T& fill) : 46 | m_width{width}, 47 | m_height{height}, 48 | m_values(m_width * m_height, fill) 49 | { 50 | // NOP 51 | } 52 | 53 | template 54 | Map::Map(std::initializer_list> values) : 55 | m_width{0}, 56 | m_height{values.size()} 57 | { 58 | auto minWidth = std::numeric_limits::max(); 59 | 60 | for (const auto& row : values) 61 | { 62 | if (row.size() > m_width) 63 | { 64 | m_width = row.size(); 65 | } 66 | 67 | if (row.size() < minWidth) 68 | { 69 | minWidth = row.size(); 70 | } 71 | } 72 | 73 | assert(minWidth == m_width); 74 | 75 | m_values.reserve(m_width * m_height); 76 | 77 | for (const auto& row : values) 78 | { 79 | for (const auto& column : row) 80 | { 81 | m_values.emplace_back(column); 82 | } 83 | } 84 | } 85 | 86 | template 87 | const T& Map::at(const std::size_t x, const std::size_t y) const 88 | { 89 | return m_values[y * m_width + x]; 90 | } 91 | 92 | template 93 | T& Map::at(const std::size_t x, const std::size_t y) 94 | { 95 | return m_values[y * m_width + x]; 96 | } 97 | 98 | template 99 | std::size_t Map::width() const 100 | { 101 | return m_width; 102 | } 103 | 104 | template 105 | std::size_t Map::height() const 106 | { 107 | return m_height; 108 | } 109 | 110 | template 111 | Map Map::rotated90CW() const 112 | { 113 | Map newMap{m_height, m_width}; 114 | 115 | for (std::size_t y = 0; y < m_height; ++y) 116 | { 117 | for (std::size_t x = 0; x < m_width; ++x) 118 | { 119 | newMap.at(newMap.width() - y - 1, x) = at(x, y); 120 | } 121 | } 122 | 123 | return newMap; 124 | } 125 | 126 | template 127 | Map Map::rotated90CCW() const 128 | { 129 | Map newMap{m_height, m_width}; 130 | 131 | for (std::size_t y = 0; y < m_height; ++y) 132 | { 133 | for (std::size_t x = 0; x < m_width; ++x) 134 | { 135 | newMap.at(y, newMap.height() - x - 1) = at(x, y); 136 | } 137 | } 138 | 139 | return newMap; 140 | } 141 | 142 | template 143 | Map Map::rotated180() const 144 | { 145 | Map newMap{m_height, m_width}; 146 | 147 | for (std::size_t y = 0; y < m_height; ++y) 148 | { 149 | for (std::size_t x = 0; x < m_width; ++x) 150 | { 151 | newMap.at(newMap.width() - x - 1, newMap.height() - y - 1) = at(x, y); 152 | } 153 | } 154 | 155 | return newMap; 156 | } 157 | 158 | template 159 | std::optional> 160 | Map::find(const T value, const std::size_t startX, const std::size_t startY, const bool wrap) const 161 | { 162 | // First row 163 | if (startY >= m_height || startX >= m_width) 164 | return std::nullopt; 165 | 166 | for (std::size_t x = startX; x < m_width; ++x) 167 | { 168 | if (at(x, startY) == value) 169 | return std::make_pair(x, startY); 170 | } 171 | 172 | for (std::size_t y = startY + 1; y < m_height; ++y) 173 | { 174 | for (std::size_t x = 0; x < m_width; ++x) 175 | { 176 | if (at(x, y) == value) 177 | return std::make_pair(x, y); 178 | } 179 | } 180 | 181 | if (wrap) 182 | { 183 | for (std::size_t y = 0; y <= startY; ++y) 184 | { 185 | for (std::size_t x = 0; x < (y == startY ? startX : m_width); ++x) 186 | { 187 | if (at(x, y) == value) 188 | return std::make_pair(x, y); 189 | } 190 | } 191 | } 192 | 193 | return std::nullopt; 194 | } 195 | 196 | #endif // MAP_HPP 197 | -------------------------------------------------------------------------------- /src/Voronoi.hpp: -------------------------------------------------------------------------------- 1 | #ifndef VORONOI_HPP 2 | #define VORONOI_HPP 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "Map.hpp" 10 | 11 | using VoronoiMap = Map; 12 | 13 | struct VoronoiCentroid final 14 | { 15 | std::size_t x; 16 | std::size_t y; 17 | }; 18 | 19 | using VoronoiCentroidList = std::vector; 20 | 21 | namespace Voronoi 22 | { 23 | inline float distanceMinkowski(const std::size_t aX, const std::size_t aY, const std::size_t bX, const std::size_t bY, const float p) 24 | { 25 | const auto faX = static_cast(aX); 26 | const auto faY = static_cast(aY); 27 | const auto fbX = static_cast(bX); 28 | const auto fbY = static_cast(bY); 29 | 30 | return static_cast(std::pow(std::pow(std::abs(faX - fbX), p) + std::pow(std::abs(faY - fbY), p), 1.0f / p)); 31 | } 32 | 33 | inline float distanceManhatten(const std::size_t aX, const std::size_t aY, const std::size_t bX, const std::size_t bY) 34 | { 35 | return distanceMinkowski(aX, aY, bX, bY, 1.0f); 36 | } 37 | 38 | inline float distanceEuclidean(const std::size_t aX, const std::size_t aY, const std::size_t bX, const std::size_t bY) 39 | { 40 | return distanceMinkowski(aX, aY, bX, bY, 2.0f); 41 | } 42 | 43 | inline float distanceChebyshev(const std::size_t aX, const std::size_t aY, const std::size_t bX, const std::size_t bY) 44 | { 45 | return static_cast(std::max((aX > bX ? aX - bX : bX - aX), (aY > bY ? aY - bY : bY - aY))); 46 | } 47 | 48 | inline bool alwaysTruePredicate(const std::size_t, const std::size_t) 49 | { 50 | return true; 51 | } 52 | 53 | template 54 | VoronoiCentroidList generateCentroids(const std::size_t areaLeft, const std::size_t areaTop, const std::size_t areaWidth, const std::size_t areaHeight, const std::size_t count, std::mt19937_64& rng, PredT predicate = alwaysTruePredicate) 55 | { 56 | VoronoiCentroidList centroidList; 57 | 58 | centroidList.reserve(count); 59 | 60 | const float minDistance = static_cast(areaWidth + areaHeight) / (static_cast(count) * 1.5f); 61 | 62 | for (std::size_t counter = 1; counter <= count; ++counter) 63 | { 64 | std::size_t x = 0; 65 | std::size_t y = 0; 66 | 67 | float minDistanceToOtherCentroids = 0.0f; 68 | 69 | do 70 | { 71 | x = std::uniform_int_distribution{areaLeft, areaLeft + areaWidth - 1}(rng); 72 | y = std::uniform_int_distribution{areaTop , areaTop + areaHeight - 1}(rng); 73 | 74 | minDistanceToOtherCentroids = std::numeric_limits::max(); 75 | 76 | for (const auto& otherCentroid : centroidList) 77 | { 78 | const auto distance = distanceEuclidean(x, y, otherCentroid.x, otherCentroid.y); 79 | 80 | if (distance < minDistanceToOtherCentroids) 81 | { 82 | minDistanceToOtherCentroids = distance; 83 | } 84 | } 85 | } 86 | while (predicate(x, y) == false || minDistanceToOtherCentroids < minDistance); 87 | 88 | centroidList.emplace_back(VoronoiCentroid{x, y}); 89 | } 90 | 91 | return centroidList; 92 | } 93 | 94 | template 95 | std::size_t getShortestDistanceCentroidIndex(const std::size_t voronoiX, const std::size_t voronoiY, const VoronoiCentroidList& centroidList, DistanceF getDistance) 96 | { 97 | float shortestDistance = std::numeric_limits::max(); 98 | std::size_t shortestDistanceIndex = 0; 99 | 100 | for (std::size_t index = 0; index < centroidList.size(); ++index) 101 | { 102 | const auto& centroid = centroidList[index]; 103 | 104 | const auto distance = getDistance(voronoiX, voronoiY, centroid.x, centroid.y); 105 | 106 | if (distance < shortestDistance) 107 | { 108 | shortestDistance = distance; 109 | shortestDistanceIndex = index; 110 | } 111 | } 112 | 113 | return shortestDistanceIndex; 114 | } 115 | 116 | template 117 | void subdivide( 118 | VoronoiCentroidList& voronoiCentroidList, 119 | const std::size_t subdivideCentroidIndex, 120 | VoronoiMap& voronoiMap, 121 | const std::size_t centroidCountToAdd, 122 | std::mt19937_64& rng, 123 | DistanceF getDistance 124 | ) 125 | { 126 | const auto newCentroidStartIndex = voronoiCentroidList.size(); 127 | 128 | // Determine bounding box of Voronoi area 129 | 130 | std::size_t topLeftX = std::numeric_limits::max(); 131 | std::size_t topLeftY = std::numeric_limits::max(); 132 | std::size_t bottomRightX = 0; 133 | std::size_t bottomRightY = 0; 134 | 135 | for (std::size_t voronoiY = 0; voronoiY < voronoiMap.height(); ++voronoiY) 136 | { 137 | for (std::size_t voronoiX = 0; voronoiX < voronoiMap.width(); ++voronoiX) 138 | { 139 | if (voronoiMap.at(voronoiX, voronoiY) == subdivideCentroidIndex) 140 | { 141 | if (voronoiX < topLeftX) 142 | { 143 | topLeftX = voronoiX; 144 | } 145 | 146 | if (voronoiX > bottomRightX) 147 | { 148 | bottomRightX = voronoiX; 149 | } 150 | 151 | if (voronoiY < topLeftY) 152 | { 153 | topLeftY = voronoiY; 154 | } 155 | 156 | if (voronoiY > bottomRightY) 157 | { 158 | bottomRightY = voronoiY; 159 | } 160 | } 161 | } 162 | } 163 | 164 | const auto innerVoronoiMapWidth = bottomRightX - topLeftX + 1; 165 | const auto innerVoronoiMapHeight = bottomRightY - topLeftY + 1; 166 | 167 | VoronoiMap newVoronoiMap{innerVoronoiMapWidth, innerVoronoiMapHeight}; 168 | 169 | // Create new set of Centroids within the chosen area 170 | const auto newCentroids = Voronoi::generateCentroids(topLeftX, topLeftY, newVoronoiMap.width(), newVoronoiMap.height(), centroidCountToAdd, rng, [&] (const std::size_t kiX, const std::size_t kiY) 171 | { 172 | return (voronoiMap.at(kiX, kiY) == subdivideCentroidIndex); 173 | }); 174 | 175 | for (std::size_t innerVoronoiY = 0; innerVoronoiY < innerVoronoiMapHeight; ++innerVoronoiY) 176 | { 177 | for (std::size_t innerVoronoiX = 0; innerVoronoiX < innerVoronoiMapWidth; ++innerVoronoiX) 178 | { 179 | newVoronoiMap.at(innerVoronoiX, innerVoronoiY) = getShortestDistanceCentroidIndex(topLeftX + innerVoronoiX, topLeftY + innerVoronoiY, newCentroids, getDistance); 180 | } 181 | } 182 | // Combine inner with outer 183 | 184 | for (std::size_t innerVoronoiY = 0; innerVoronoiY < innerVoronoiMapHeight; ++innerVoronoiY) 185 | { 186 | for (std::size_t innerVoronoiX = 0; innerVoronoiX < innerVoronoiMapWidth; ++innerVoronoiX) 187 | { 188 | const auto outerVoronoiX = topLeftX + innerVoronoiX; 189 | const auto outerVoronoiY = topLeftY + innerVoronoiY; 190 | 191 | if (voronoiMap.at(outerVoronoiX, outerVoronoiY) == subdivideCentroidIndex) 192 | { 193 | voronoiMap.at(outerVoronoiX, outerVoronoiY) = newVoronoiMap.at(innerVoronoiX, innerVoronoiY) + newCentroidStartIndex; 194 | } 195 | } 196 | } 197 | 198 | // Extend Centroid list 199 | 200 | voronoiCentroidList.reserve(voronoiCentroidList.size() + centroidCountToAdd); 201 | voronoiCentroidList.insert(voronoiCentroidList.end(), newCentroids.begin(), newCentroids.end()); 202 | } 203 | } // namespace Voronoi 204 | 205 | #endif // VORONOI_HPP 206 | -------------------------------------------------------------------------------- /src/Bitmap.cpp: -------------------------------------------------------------------------------- 1 | #include "Bitmap.hpp" 2 | 3 | namespace BitmapTransform 4 | { 5 | Bitmap erode(const Bitmap& bitmap, const bool includeDiagonals) 6 | { 7 | Bitmap outMap{bitmap.width(), bitmap.height()}; 8 | 9 | for (std::size_t centerY = 0; centerY < bitmap.height(); ++centerY) 10 | { 11 | for (std::size_t centerX = 0; centerX < bitmap.width(); ++centerX) 12 | { 13 | if (bitmap.at(centerX, centerY) == 1) 14 | { 15 | std::uint8_t sum = 0; 16 | 17 | if (centerX > 0) 18 | { 19 | if (includeDiagonals) 20 | { 21 | if (centerY > 0) 22 | { 23 | sum += bitmap.at(centerX - 1, centerY - 1); 24 | } 25 | 26 | if (centerY < bitmap.height() - 1) 27 | { 28 | sum += bitmap.at(centerX - 1, centerY + 1); 29 | } 30 | } 31 | 32 | sum += bitmap.at(centerX - 1, centerY); 33 | } 34 | 35 | if (centerX < bitmap.width() - 1) 36 | { 37 | if (includeDiagonals) 38 | { 39 | if (centerY > 0) 40 | { 41 | sum += bitmap.at(centerX + 1, centerY - 1); 42 | } 43 | 44 | if (centerY < bitmap.height() - 1) 45 | { 46 | sum += bitmap.at(centerX + 1, centerY + 1); 47 | } 48 | } 49 | 50 | sum += bitmap.at(centerX + 1, centerY); 51 | } 52 | 53 | if (centerY > 0) 54 | { 55 | sum += bitmap.at(centerX, centerY - 1); 56 | } 57 | 58 | if (centerY < bitmap.height() - 1) 59 | { 60 | sum += bitmap.at(centerX, centerY + 1); 61 | } 62 | 63 | if (sum == (includeDiagonals ? 8 : 4)) 64 | { 65 | // All neighbors were 1 66 | outMap.at(centerX, centerY) = 1; 67 | } 68 | else 69 | { 70 | // Some neighbor was 0 71 | outMap.at(centerX, centerY) = 0; 72 | } 73 | } 74 | else 75 | { 76 | outMap.at(centerX, centerY) = 0; 77 | } 78 | } 79 | } 80 | 81 | return outMap; 82 | } 83 | 84 | Bitmap dilate(const Bitmap& bitmap, const bool includeDiagonals) 85 | { 86 | Bitmap outMap{bitmap.width(), bitmap.height()}; 87 | 88 | for (std::size_t centerY = 0; centerY < bitmap.height(); ++centerY) 89 | { 90 | for (std::size_t centerX = 0; centerX < bitmap.width(); ++centerX) 91 | { 92 | if (bitmap.at(centerX, centerY) == 0) 93 | { 94 | std::uint8_t sum = 0; 95 | 96 | if (centerX > 0) 97 | { 98 | if (includeDiagonals) 99 | { 100 | if (centerY > 0) 101 | { 102 | sum += bitmap.at(centerX - 1, centerY - 1); 103 | } 104 | 105 | if (centerY < bitmap.height() - 1) 106 | { 107 | sum += bitmap.at(centerX - 1, centerY + 1); 108 | } 109 | } 110 | 111 | sum += bitmap.at(centerX - 1, centerY); 112 | } 113 | 114 | if (centerX < bitmap.width() - 1) 115 | { 116 | if (includeDiagonals) 117 | { 118 | if (centerY > 0) 119 | { 120 | sum += bitmap.at(centerX + 1, centerY - 1); 121 | } 122 | 123 | if (centerY < bitmap.height() - 1) 124 | { 125 | sum += bitmap.at(centerX + 1, centerY + 1); 126 | } 127 | } 128 | 129 | sum += bitmap.at(centerX + 1, centerY); 130 | } 131 | 132 | if (centerY > 0) 133 | { 134 | sum += bitmap.at(centerX, centerY - 1); 135 | } 136 | 137 | if (centerY < bitmap.height() - 1) 138 | { 139 | sum += bitmap.at(centerX, centerY + 1); 140 | } 141 | 142 | if (sum == 0) 143 | { 144 | // All neighbors were 0 145 | outMap.at(centerX, centerY) = 0; 146 | } 147 | else 148 | { 149 | // Some neighbor was 1 150 | outMap.at(centerX, centerY) = 1; 151 | } 152 | } 153 | else 154 | { 155 | outMap.at(centerX, centerY) = 1; 156 | } 157 | } 158 | } 159 | 160 | return outMap; 161 | } 162 | 163 | Bitmap border(const Bitmap& bitmap, const bool includeDiagonals) 164 | { 165 | Bitmap outMap{bitmap.width(), bitmap.height()}; 166 | 167 | for (std::size_t centerY = 0; centerY < bitmap.height(); ++centerY) 168 | { 169 | for (std::size_t centerX = 0; centerX < bitmap.width(); ++centerX) 170 | { 171 | if (bitmap.at(centerX, centerY) == 0) 172 | { 173 | std::uint8_t sum = 0; 174 | 175 | if (centerX > 0) 176 | { 177 | if (includeDiagonals) 178 | { 179 | if (centerY > 0) 180 | { 181 | sum += bitmap.at(centerX - 1, centerY - 1); 182 | } 183 | 184 | if (centerY < bitmap.height() - 1) 185 | { 186 | sum += bitmap.at(centerX - 1, centerY + 1); 187 | } 188 | } 189 | 190 | sum += bitmap.at(centerX - 1, centerY); 191 | } 192 | 193 | if (centerX < bitmap.width() - 1) 194 | { 195 | if (includeDiagonals) 196 | { 197 | if (centerY > 0) 198 | { 199 | sum += bitmap.at(centerX + 1, centerY - 1); 200 | } 201 | 202 | if (centerY < bitmap.height() - 1) 203 | { 204 | sum += bitmap.at(centerX + 1, centerY + 1); 205 | } 206 | } 207 | 208 | sum += bitmap.at(centerX + 1, centerY); 209 | } 210 | 211 | if (centerY > 0) 212 | { 213 | sum += bitmap.at(centerX, centerY - 1); 214 | } 215 | 216 | if (centerY < bitmap.height() - 1) 217 | { 218 | sum += bitmap.at(centerX, centerY + 1); 219 | } 220 | 221 | if (sum == 0) 222 | { 223 | // All neighbors were 0 224 | outMap.at(centerX, centerY) = 0; 225 | } 226 | else 227 | { 228 | // Some neighbor was 1 229 | outMap.at(centerX, centerY) = 1; 230 | } 231 | } 232 | else 233 | { 234 | outMap.at(centerX, centerY) = 0; 235 | } 236 | } 237 | } 238 | 239 | return outMap; 240 | } 241 | 242 | Bitmap shift(const Bitmap& bitmap, const int deltaX, const int deltaY, const uint8_t fill) 243 | { 244 | Bitmap outMap{bitmap.width(), bitmap.height(), fill}; 245 | 246 | for (std::size_t y = 0; y < bitmap.height(); ++y) 247 | { 248 | for (std::size_t x = 0; x < bitmap.width(); ++x) 249 | { 250 | const auto outX = x + deltaX; 251 | const auto outY = y + deltaY; 252 | 253 | if (outX < 0 || outX >= outMap.width()) 254 | continue; 255 | 256 | if (outY < 0 || outY >= outMap.height()) 257 | continue; 258 | 259 | outMap.at(outX, outY) = bitmap.at(x, y); 260 | } 261 | } 262 | 263 | return outMap; 264 | } 265 | 266 | Bitmap invert(const Bitmap& bitmap) 267 | { 268 | Bitmap outMap{bitmap.width(), bitmap.height()}; 269 | 270 | for (std::size_t y = 0; y < bitmap.height(); ++y) 271 | { 272 | for (std::size_t x = 0; x < bitmap.width(); ++x) 273 | { 274 | outMap.at(x, y) = (bitmap.at(x, y) == 0 ? 1 : 0); 275 | } 276 | } 277 | 278 | return outMap; 279 | } 280 | 281 | Bitmap mask(const Bitmap& bitmap, const Bitmap& mask, const int bitmapBOffsetX, const int bitmapBOffsetY) 282 | { 283 | const auto overlapRegionX1 = static_cast(std::max(0, bitmapBOffsetX)); 284 | const auto overlapRegionY1 = static_cast(std::max(0, bitmapBOffsetY)); 285 | const auto overlapRegionX2 = std::min(bitmap.width(), bitmapBOffsetX + mask.width()); 286 | const auto overlapRegionY2 = std::min(bitmap.height(), bitmapBOffsetY + mask.height()); 287 | 288 | Bitmap outMap{bitmap.width(), bitmap.height()}; 289 | 290 | for (std::size_t y = 0; y < bitmap.height(); ++y) 291 | { 292 | for (std::size_t x = 0; x < bitmap.width(); ++x) 293 | { 294 | if (x >= overlapRegionX1 && x <= overlapRegionX2 && y >= overlapRegionY1 && y <= overlapRegionY2) 295 | { 296 | if (mask.at(x - bitmapBOffsetX, y - bitmapBOffsetY) == 1) 297 | { 298 | outMap.at(x, y) = 0; 299 | } 300 | else 301 | { 302 | outMap.at(x, y) = bitmap.at(x, y); 303 | } 304 | } 305 | else 306 | { 307 | outMap.at(x, y) = bitmap.at(x, y); 308 | } 309 | } 310 | } 311 | 312 | return outMap; 313 | } 314 | } // namespace BitmapTransform 315 | -------------------------------------------------------------------------------- /src/Villager.cpp: -------------------------------------------------------------------------------- 1 | #include "Villager.hpp" 2 | 3 | #include 4 | 5 | #include "Voronoi.hpp" 6 | #include "Util.hpp" 7 | 8 | void Villager::tick( 9 | WorldMap& worldMap, 10 | UpdateRect& mapUpdateRect, 11 | CostMap& baseCostMap, 12 | DesirePathsMap& desirePathsMap, 13 | const bool pavePaths, 14 | const int tileWidthPixels, 15 | const int tileHeightPixels, 16 | std::mt19937_64& rng, 17 | const float delta 18 | ) 19 | { 20 | const auto state = m_state.load(); 21 | 22 | if (state == State::AwaitingPath) 23 | { 24 | // NOP 25 | } 26 | else if (state == State::EnqueuedForPath) 27 | { 28 | // NOP 29 | } 30 | else if (state == State::PathProvided) 31 | { 32 | assert(m_path.size() >= 2); 33 | 34 | // "Spawn" at first point in path 35 | m_currentPathIndex = 0; 36 | 37 | const auto startTileX = m_path[m_currentPathIndex].first; 38 | const auto startTileY = m_path[m_currentPathIndex].second; 39 | 40 | moveOntoTile(startTileX, startTileY, worldMap, mapUpdateRect, baseCostMap, desirePathsMap, pavePaths, tileWidthPixels, tileHeightPixels, rng); 41 | 42 | m_position.x = static_cast(static_cast(startTileX) * tileWidthPixels); 43 | m_position.y = static_cast(static_cast(startTileY) * tileHeightPixels); 44 | 45 | m_tileToTileMovementStart = m_position; 46 | 47 | const auto nextTileX = m_path[m_currentPathIndex + 1].first; 48 | const auto nextTileY = m_path[m_currentPathIndex + 1].second; 49 | 50 | m_tileToTileMovementTarget.x = static_cast(static_cast(nextTileX) * tileWidthPixels); 51 | m_tileToTileMovementTarget.y = static_cast(static_cast(nextTileY) * tileHeightPixels); 52 | 53 | m_tileToTileMovementDirection = Vector2Normalize(Vector2Subtract(m_tileToTileMovementTarget, m_position)); 54 | 55 | m_tileToTileMovementDistance = Vector2Distance(m_tileToTileMovementTarget, m_position); 56 | 57 | m_state.store(State::Moving); 58 | } 59 | else if (state == State::Moving) 60 | { 61 | m_position = Vector2Add(m_position, Vector2Scale(m_tileToTileMovementDirection, m_movementPixelPerSec * delta)); 62 | 63 | if (Vector2Distance(m_position, m_tileToTileMovementStart) >= m_tileToTileMovementDistance) // Next tile reached 64 | { 65 | m_position = m_tileToTileMovementTarget; 66 | 67 | m_currentPathIndex += 1; 68 | 69 | const auto reachedTileX = m_path[m_currentPathIndex].first; 70 | const auto reachedTileY = m_path[m_currentPathIndex].second; 71 | 72 | moveOntoTile(reachedTileX, reachedTileY, worldMap, mapUpdateRect, baseCostMap, desirePathsMap, pavePaths, tileWidthPixels, tileHeightPixels, rng); 73 | 74 | if (m_path.size() > m_currentPathIndex + 1) 75 | { 76 | m_tileToTileMovementStart = m_position; 77 | 78 | const auto nextTileX = m_path[m_currentPathIndex + 1].first; 79 | const auto nextTileY = m_path[m_currentPathIndex + 1].second; 80 | 81 | m_tileToTileMovementTarget.x = static_cast(static_cast(nextTileX) * tileWidthPixels); 82 | m_tileToTileMovementTarget.y = static_cast(static_cast(nextTileY) * tileHeightPixels); 83 | 84 | m_tileToTileMovementDirection = Vector2Normalize(Vector2Subtract(m_tileToTileMovementTarget, m_position)); 85 | 86 | m_tileToTileMovementDistance = Vector2Distance(m_tileToTileMovementTarget, m_position); 87 | } 88 | else 89 | { 90 | m_path.clear(); 91 | m_currentPathIndex = 0; 92 | 93 | m_state.store(State::AwaitingPath); 94 | } 95 | } 96 | } 97 | } 98 | 99 | void Villager::moveOntoTile( 100 | const std::size_t tileX, 101 | const std::size_t tileY, 102 | WorldMap& worldMap, 103 | UpdateRect& mapUpdateRect, 104 | CostMap& baseCostMap, 105 | DesirePathsMap& desirePathsMap, 106 | const bool pavePaths, 107 | const int tileWidthPixels, 108 | const int tileHeightPixels, 109 | std::mt19937_64& rng 110 | ) 111 | { 112 | if (worldMap.at(tileX, tileY) == TileType::Grass) 113 | { 114 | const auto currentDesirePathTileStress = adjustDesirePathStress(tileX, tileY, desirePathsMap, baseCostMap, std::uniform_int_distribution<>{2, 6}(rng)); 115 | 116 | const auto shouldBePaved = pavePaths && (currentDesirePathTileStress == 255); 117 | 118 | std::vector> neighbors; 119 | 120 | neighbors.reserve(4); 121 | 122 | if (tileY > 0) 123 | { 124 | neighbors.emplace_back(tileX, tileY - 1); 125 | 126 | if (tileX > 0) 127 | { 128 | neighbors.emplace_back(tileX - 1, tileY - 1); 129 | } 130 | 131 | if (tileX < worldMap.width() - 1) 132 | { 133 | neighbors.emplace_back(tileX + 1, tileY - 1); 134 | } 135 | } 136 | 137 | if (tileX > 0) 138 | { 139 | neighbors.emplace_back(tileX - 1, tileY); 140 | } 141 | 142 | if (tileX < worldMap.width() - 1) 143 | { 144 | neighbors.emplace_back(tileX + 1, tileY); 145 | } 146 | 147 | if (tileY < worldMap.height() - 1) 148 | { 149 | neighbors.emplace_back(tileX, tileY + 1); 150 | 151 | if (tileX > 0) 152 | { 153 | neighbors.emplace_back(tileX - 1, tileY + 1); 154 | } 155 | 156 | if (tileX < worldMap.width() - 1) 157 | { 158 | neighbors.emplace_back(tileX + 1, tileY + 1); 159 | } 160 | } 161 | 162 | bool paved = false; 163 | 164 | if (shouldBePaved) 165 | { 166 | // Only pave if an adjacent tile is paved 167 | for (const auto& neighbor : neighbors) 168 | { 169 | const auto neighborValue = worldMap.at(neighbor.first, neighbor.second); 170 | 171 | if (neighborValue == TileType::Street || neighborValue == TileType::BuildingEntrance) 172 | { 173 | paveTile(tileX, tileY, worldMap, mapUpdateRect, baseCostMap, desirePathsMap, tileWidthPixels, tileHeightPixels, rng); 174 | paved = true; 175 | break; 176 | } 177 | } 178 | } 179 | 180 | // Pave neighboring grass 181 | if (paved) 182 | { 183 | for (const auto& neighbor : neighbors) 184 | { 185 | if (worldMap.at(neighbor.first, neighbor.second) == TileType::Grass) 186 | { 187 | paveTile(neighbor.first, neighbor.second, worldMap, mapUpdateRect, baseCostMap, desirePathsMap, tileWidthPixels, tileHeightPixels, rng); 188 | } 189 | } 190 | } 191 | } 192 | } 193 | 194 | void Villager::reset( 195 | const WorldMap& worldMap, 196 | Pathfinder& pathfinder, 197 | const CostMap& baseCostMap, 198 | const int tileWidthPixels, 199 | const int tileHeightPixels, 200 | std::mt19937_64& rng 201 | ) 202 | { 203 | std::uniform_int_distribution rngWidth {0, worldMap.width () - 1}; 204 | std::uniform_int_distribution rngHeight{0, worldMap.height() - 1}; 205 | 206 | auto findRandomBuildingEntrance = [&] () -> std::pair 207 | { 208 | return worldMap.find(TileType::BuildingEntrance, rngWidth(rng), rngHeight(rng), true).value_or(std::make_pair(0, 0)); // Fallback, entrances should always exist 209 | }; 210 | 211 | const auto& [spawnX, spawnY] = findRandomBuildingEntrance(); 212 | 213 | std::size_t destinationX = 0; 214 | std::size_t destinationY = 0; 215 | 216 | do 217 | { 218 | std::tie(destinationX, destinationY) = findRandomBuildingEntrance(); 219 | } 220 | while (spawnX == destinationX && spawnY == destinationY); // Spawn point and destination must differ 221 | 222 | m_position.x = static_cast(spawnX * tileWidthPixels ); 223 | m_position.y = static_cast(spawnY * tileHeightPixels); 224 | 225 | m_currentPathIndex = 0; 226 | 227 | auto isBlocked = [] (const TileType tileType) 228 | { 229 | switch (tileType) 230 | { 231 | case TileType::Street: 232 | case TileType::BuildingEntrance: 233 | case TileType::Grass: 234 | //case TileType::Water: 235 | return false; 236 | default: 237 | return true; 238 | } 239 | }; 240 | 241 | auto canTraverse = [&] (const std::size_t fromX, const std::size_t fromY, const std::size_t toX, const std::size_t toY) 242 | { 243 | if (isBlocked(worldMap.at(toX, toY))) 244 | return false; 245 | 246 | if (fromX == toX || fromY == toY) 247 | return true; 248 | 249 | // Diagonal movement is allowed if both "corners" are free 250 | return ((isBlocked(worldMap.at(fromX, toY)) == false) && (isBlocked(worldMap.at(toX, fromY)) == false)); 251 | }; 252 | 253 | auto getTraversalCost = [&] (const std::size_t fromX, const std::size_t fromY, const std::size_t toX, const std::size_t toY) 254 | { 255 | const bool isDiagonal = (fromX != toX && fromY != toY); 256 | 257 | const std::uint8_t factor = isDiagonal ? 14 : 10; // (sqrt(2) * 10) or (1 * 10) 258 | 259 | return static_cast(factor * baseCostMap.at(toX, toY)); 260 | }; 261 | 262 | // See http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html 263 | auto heuristic = [&] (const std::size_t fromX, const std::size_t fromY, const std::size_t toX, const std::size_t toY) 264 | { 265 | // Diagonal distance (times 10) 266 | const auto diffX = absDiff(fromX, toX); 267 | const auto diffY = absDiff(fromY, toY); 268 | 269 | static constexpr std::size_t d = 10; // 1 * 10 270 | static constexpr std::size_t d2 = 14; // sqrt(2) * 10 271 | 272 | return static_cast(d * (diffX + diffY) + (d2 - 2 * d) * std::min(diffX, diffY)); 273 | }; 274 | 275 | m_path = pathfinder.getPath(spawnX, spawnY, destinationX, destinationY, canTraverse, getTraversalCost, heuristic); 276 | 277 | m_state.store(State::PathProvided); 278 | } 279 | 280 | void Villager::paveTile( 281 | const std::size_t tileX, 282 | const std::size_t tileY, 283 | WorldMap& worldMap, 284 | UpdateRect& mapUpdateRect, 285 | CostMap& baseCostMap, 286 | DesirePathsMap& desirePathsMap, 287 | const int tileWidthPixels, 288 | const int tileHeightPixels, 289 | std::mt19937_64& rng 290 | ) 291 | { 292 | if (worldMap.at(tileX, tileY) != TileType::Street) 293 | { 294 | worldMap.at(tileX, tileY) = TileType::Street; 295 | 296 | baseCostMap.at(tileX, tileY) = getBaseCostForValue(TileType::Street, rng); 297 | 298 | desirePathsMap.at(tileX, tileY) = 0; 299 | 300 | mapUpdateRect.add(tileX, tileY); 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /src/Pathfinding.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PATHFINDING_HPP 2 | #define PATHFINDING_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "Util.hpp" 10 | 11 | class Pathfinder final 12 | { 13 | private: 14 | struct PathNode final 15 | { 16 | PathNode* m_parent = nullptr; 17 | 18 | std::size_t m_x = 0; 19 | std::size_t m_y = 0; 20 | 21 | bool m_inOpenList = false; 22 | bool m_closed = false; 23 | 24 | std::size_t m_openListIndex = 0; 25 | 26 | std::int32_t m_f = 0; 27 | std::int32_t m_g = 0; 28 | std::int32_t m_h = 0; 29 | 30 | PathNode() = default; 31 | 32 | PathNode(const std::size_t x, const std::size_t y): 33 | m_x{x}, 34 | m_y{y} 35 | { 36 | // NOP 37 | } 38 | }; 39 | 40 | public: 41 | static inline bool alwaysTraversable(const std::size_t /*fromX*/, const std::size_t /*fromY*/, const std::size_t /*toX*/, const std::size_t /*toY*/) 42 | { 43 | return true; 44 | } 45 | 46 | static inline std::int32_t zeroCost(const std::size_t /*fromX*/, const std::size_t /*fromY*/, const std::size_t /*toX*/, const std::size_t /*toY*/) 47 | { 48 | return 0; 49 | } 50 | 51 | static inline std::int32_t defaultHeuristic(const std::size_t fromX, const std::size_t fromY, const std::size_t toX, const std::size_t toY) 52 | { 53 | // Chebyshev distance (diagonals are counted as same distance as orthogonals) 54 | 55 | const auto diffX = absDiff(fromX, toX); 56 | const auto diffY = absDiff(fromY, toY); 57 | 58 | return static_cast(std::max(diffX, diffY)); 59 | } 60 | 61 | private: 62 | std::size_t m_width; 63 | std::size_t m_height; 64 | 65 | std::vector m_pathNodeMap; 66 | 67 | std::vector m_openList; 68 | 69 | public: 70 | Pathfinder(const std::size_t width, const std::size_t height): 71 | m_width{width}, 72 | m_height{height} 73 | { 74 | m_pathNodeMap.resize(m_width * m_height); 75 | 76 | for (std::size_t y = 0; y < m_height; ++y) 77 | { 78 | for (std::size_t x = 0; x < m_width; ++x) 79 | { 80 | m_pathNodeMap[y * m_width + x].m_x = x; 81 | m_pathNodeMap[y * m_width + x].m_y = y; 82 | } 83 | } 84 | } 85 | 86 | template 87 | std::vector> getPath( 88 | const std::size_t startX, const std::size_t startY, 89 | const std::size_t endX, const std::size_t endY, 90 | CanTraverseCallable canTraverse = alwaysTraversable, 91 | TraversalCostCallable getTraversalCost = zeroCost, 92 | HeuristicCallable heuristic = defaultHeuristic 93 | ) 94 | { 95 | bool success = false; 96 | 97 | auto& startPathNode = getPathNodeAt(startX, startY); 98 | auto& endPathNode = getPathNodeAt(endX , endY ); 99 | 100 | // Make some room for the nodes using heuristic distance 101 | m_openList.reserve(std::max(absDiff(startPathNode.m_x, endPathNode.m_x), absDiff(startPathNode.m_y, endPathNode.m_y))); 102 | 103 | m_openList.emplace_back(&startPathNode); 104 | 105 | startPathNode.m_inOpenList = true; 106 | startPathNode.m_openListIndex = 0; 107 | 108 | // As we work through the open, we do not pop anything from the front, instead we increase the starting position. 109 | // This ensures consistent indices and no overhead caused by memory shifts when popping. It comes with the cost of 110 | // having to store more data in the vector. 111 | 112 | std::size_t openListStartingPosition = 0; 113 | 114 | while (openListStartingPosition < m_openList.size()) 115 | { 116 | PathNode& smallestFPathNode = *m_openList[openListStartingPosition]; 117 | 118 | openListStartingPosition += 1; 119 | 120 | smallestFPathNode.m_inOpenList = false; 121 | smallestFPathNode.m_closed = true; 122 | 123 | if (smallestFPathNode.m_x == endPathNode.m_x && smallestFPathNode.m_y == endPathNode.m_y) 124 | { 125 | success = true; 126 | break; 127 | } 128 | 129 | auto handleAdjacentPathNode = [&] (PathNode& adjacentPathNode) 130 | { 131 | if (adjacentPathNode.m_closed || canTraverse(smallestFPathNode.m_x, smallestFPathNode.m_y, adjacentPathNode.m_x, adjacentPathNode.m_y) == false) 132 | return; 133 | 134 | // G is the distance from the start node to this node plus any costs for traversing this node 135 | const std::int32_t tempG = smallestFPathNode.m_g + getTraversalCost(smallestFPathNode.m_x, smallestFPathNode.m_y, adjacentPathNode.m_x, adjacentPathNode.m_y); 136 | 137 | if (adjacentPathNode.m_inOpenList == false) 138 | { 139 | adjacentPathNode.m_parent = &smallestFPathNode; 140 | 141 | // Set G 142 | adjacentPathNode.m_g = tempG; 143 | 144 | // H is the heuristic distance to the end node, it has to be less than or equal to the actual cost neccessary 145 | adjacentPathNode.m_h = heuristic(adjacentPathNode.m_x, adjacentPathNode.m_y, endX, endY); 146 | 147 | // F is the sum of G and H 148 | adjacentPathNode.m_f = adjacentPathNode.m_g + adjacentPathNode.m_h; 149 | 150 | adjacentPathNode.m_inOpenList = true; 151 | 152 | // Find out where to insert 153 | const auto insertionPoint = 154 | std::upper_bound( 155 | std::begin(m_openList) + openListStartingPosition, 156 | std::end(m_openList), 157 | &adjacentPathNode, 158 | [] (const auto pathNodeA, const auto pathNodeB) { return (pathNodeA->m_f < pathNodeB->m_f); } 159 | ); 160 | 161 | if (insertionPoint == std::end(m_openList)) 162 | { 163 | // We can just push it back at the end, its value is the largest so far 164 | 165 | // Store the index in the PathNode 166 | adjacentPathNode.m_openListIndex = m_openList.size(); 167 | 168 | m_openList.emplace_back(&adjacentPathNode); 169 | } 170 | else 171 | { 172 | const std::size_t insertionIndex = std::distance(std::begin(m_openList) + openListStartingPosition, insertionPoint) + openListStartingPosition; 173 | 174 | // Store the new index in the PathNode 175 | adjacentPathNode.m_openListIndex = insertionIndex; 176 | 177 | // Inserting into the middle 178 | m_openList.insert(insertionPoint, &adjacentPathNode); 179 | } 180 | } 181 | else 182 | { 183 | // Better G? 184 | if (tempG < adjacentPathNode.m_g) 185 | { 186 | adjacentPathNode.m_parent = &smallestFPathNode; 187 | 188 | adjacentPathNode.m_g = tempG; 189 | adjacentPathNode.m_f = adjacentPathNode.m_g + adjacentPathNode.m_h; 190 | 191 | // The entry might have to move up in the list to keep the list sorted 192 | 193 | auto insertionIndex = adjacentPathNode.m_openListIndex; 194 | 195 | // Counting downwards until we wrap around 196 | while ((--insertionIndex < (adjacentPathNode.m_openListIndex + 1)) && (insertionIndex >= openListStartingPosition)) 197 | { 198 | if (m_openList[insertionIndex]->m_f <= adjacentPathNode.m_f) 199 | break; // We reached the insertion point 200 | } 201 | 202 | if (insertionIndex < adjacentPathNode.m_openListIndex) 203 | { 204 | // Store the new index in the PathNode 205 | adjacentPathNode.m_openListIndex = insertionIndex; 206 | 207 | m_openList.insert(std::begin(m_openList) + insertionIndex, &adjacentPathNode); 208 | } 209 | } 210 | } 211 | }; 212 | 213 | if (smallestFPathNode.m_y > 0) 214 | { 215 | handleAdjacentPathNode(getPathNodeAt(smallestFPathNode.m_x, smallestFPathNode.m_y - 1)); 216 | 217 | if (smallestFPathNode.m_x > 0) 218 | { 219 | handleAdjacentPathNode(getPathNodeAt(smallestFPathNode.m_x - 1, smallestFPathNode.m_y - 1)); 220 | } 221 | 222 | if (smallestFPathNode.m_x < m_width - 1) 223 | { 224 | handleAdjacentPathNode(getPathNodeAt(smallestFPathNode.m_x + 1, smallestFPathNode.m_y - 1)); 225 | } 226 | } 227 | 228 | if (smallestFPathNode.m_y < m_height - 1) 229 | { 230 | handleAdjacentPathNode(getPathNodeAt(smallestFPathNode.m_x, smallestFPathNode.m_y + 1)); 231 | 232 | if (smallestFPathNode.m_x > 0) 233 | { 234 | handleAdjacentPathNode(getPathNodeAt(smallestFPathNode.m_x - 1, smallestFPathNode.m_y + 1)); 235 | } 236 | 237 | if (smallestFPathNode.m_x < m_width - 1) 238 | { 239 | handleAdjacentPathNode(getPathNodeAt(smallestFPathNode.m_x + 1, smallestFPathNode.m_y + 1)); 240 | } 241 | } 242 | 243 | if (smallestFPathNode.m_x > 0) 244 | { 245 | handleAdjacentPathNode(getPathNodeAt(smallestFPathNode.m_x - 1, smallestFPathNode.m_y)); 246 | } 247 | 248 | if (smallestFPathNode.m_x < m_width - 1) 249 | { 250 | handleAdjacentPathNode(getPathNodeAt(smallestFPathNode.m_x + 1, smallestFPathNode.m_y)); 251 | } 252 | } 253 | 254 | if (success) 255 | { 256 | std::vector> mapNodes; 257 | 258 | mapNodes.reserve(m_width + m_height); 259 | 260 | PathNode* node = &endPathNode; 261 | 262 | mapNodes.emplace_back(node->m_x, node->m_y); 263 | 264 | while (node->m_parent != nullptr) 265 | { 266 | mapNodes.emplace_back(node->m_parent->m_x, node->m_parent->m_y); 267 | node = node->m_parent; 268 | } 269 | 270 | std::reverse(std::begin(mapNodes), std::end(mapNodes)); 271 | 272 | clearOpenList(); 273 | 274 | return mapNodes; 275 | } 276 | 277 | clearOpenList(); 278 | 279 | return {}; 280 | } 281 | 282 | private: 283 | PathNode& getPathNodeAt(const std::size_t x, const std::size_t y) 284 | { 285 | return m_pathNodeMap[y * m_width + x]; 286 | } 287 | 288 | void clearOpenList() 289 | { 290 | for (auto* const pathNode : m_openList) 291 | { 292 | pathNode->m_parent = nullptr; 293 | 294 | pathNode->m_inOpenList = false; 295 | pathNode->m_closed = false; 296 | } 297 | 298 | m_openList.clear(); 299 | } 300 | }; 301 | 302 | #endif // PATHFINDING_HPP 303 | -------------------------------------------------------------------------------- /src/DesirePathSim.cpp: -------------------------------------------------------------------------------- 1 | #include "DesirePathSim.hpp" 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | #include "WorldGen.hpp" 9 | #include "Pathfinding.hpp" 10 | #include "Version.hpp" 11 | 12 | DesirePathSim::DesirePathSim(const Options& options): 13 | m_options{options}, 14 | m_worldWidthTiles{m_options.screenWidthPixels * 2 / m_tileWidthPixels}, 15 | m_worldHeightTiles{m_options.screenHeightPixels * 2 / m_tileHeightPixels}, 16 | m_worldWidthPixels{m_worldWidthTiles * m_tileWidthPixels}, 17 | m_worldHeightPixels{m_worldHeightTiles * m_tileHeightPixels}, 18 | m_baseRNG{std::random_device{}()}, 19 | m_voronoiMap{m_worldWidthTiles, m_worldHeightTiles}, 20 | m_worldMap{m_worldWidthTiles, m_worldHeightTiles, TileType::Grass}, 21 | m_baseCostMap{m_worldWidthTiles, m_worldHeightTiles, 0}, 22 | m_desirePathsMap{m_worldWidthTiles, m_worldHeightTiles, 0}, 23 | m_shadowBitmap{m_worldWidthTiles, m_worldHeightTiles, 0}, 24 | m_villagers{m_options.villagerCount} 25 | { 26 | std::uniform_int_distribution<> rngPercent{1, 100}; 27 | 28 | auto voronoiCentroidList = Voronoi::generateCentroids(0, 0, m_voronoiMap.width(), m_voronoiMap.height(), m_options.voronoiCentroidCountPerLevel, m_baseRNG); 29 | 30 | for (std::size_t centroidY = 0; centroidY < m_voronoiMap.height(); ++centroidY) 31 | { 32 | for (std::size_t centroidX = 0; centroidX < m_voronoiMap.width(); ++centroidX) 33 | { 34 | m_voronoiMap.at(centroidX, centroidY) = Voronoi::getShortestDistanceCentroidIndex(centroidX, centroidY, voronoiCentroidList, [this] (const std::size_t aX, const std::size_t aY, const std::size_t bX, const std::size_t bY) 35 | { 36 | return Voronoi::distanceMinkowski(aX, aY, bX, bY, m_options.voronoiLevel0MinkowskiP); 37 | }); 38 | } 39 | } 40 | 41 | // Subdivide level 1 42 | 43 | for (std::size_t subdivideCentroidIndex = 0; subdivideCentroidIndex < m_options.voronoiCentroidCountPerLevel; ++subdivideCentroidIndex) 44 | { 45 | if (rngPercent(m_baseRNG) <= m_options.voronoiSubdivideProbabilityLevel1) 46 | { 47 | Voronoi::subdivide(voronoiCentroidList, subdivideCentroidIndex, m_voronoiMap, m_options.voronoiCentroidCountPerLevel, m_baseRNG, [this] (const std::size_t aX, const std::size_t aY, const std::size_t bX, const std::size_t bY) 48 | { 49 | return Voronoi::distanceMinkowski(aX, aY, bX, bY, m_options.voronoiLevel1MinkowskiP); 50 | }); 51 | } 52 | } 53 | 54 | // Subdivide level 2 55 | 56 | for (std::size_t subdivideCentroidIndex = m_options.voronoiCentroidCountPerLevel; subdivideCentroidIndex < m_options.voronoiCentroidCountPerLevel + m_options.voronoiCentroidCountPerLevel * m_options.voronoiCentroidCountPerLevel; ++subdivideCentroidIndex) 57 | { 58 | if (rngPercent(m_baseRNG) <= m_options.voronoiSubdivideProbabilityLevel2) 59 | { 60 | Voronoi::subdivide(voronoiCentroidList, subdivideCentroidIndex, m_voronoiMap, m_options.voronoiCentroidCountPerLevel, m_baseRNG, [this] (const std::size_t aX, const std::size_t aY, const std::size_t bX, const std::size_t bY) 61 | { 62 | return Voronoi::distanceMinkowski(aX, aY, bX, bY, m_options.voronoiLevel2MinkowskiP); 63 | }); 64 | } 65 | } 66 | 67 | m_voronoiColorTable.reserve(voronoiCentroidList.size()); 68 | 69 | for (std::size_t cellColorIndex = 0; cellColorIndex < voronoiCentroidList.size(); ++cellColorIndex) 70 | { 71 | std::uniform_int_distribution<> rngCellColorChannel{50, 255}; 72 | 73 | Color color 74 | { 75 | static_cast(rngCellColorChannel(m_baseRNG)), 76 | static_cast(rngCellColorChannel(m_baseRNG)), 77 | static_cast(rngCellColorChannel(m_baseRNG)), 78 | 255 79 | }; 80 | 81 | m_voronoiColorTable.emplace_back(color); 82 | } 83 | 84 | WorldGen worldGen{m_worldMap, m_baseRNG}; 85 | 86 | worldGen.placeStreetsFromVoronoiMap(m_voronoiMap); 87 | 88 | if (m_options.placeRoundabouts) 89 | { 90 | worldGen.placeRoundaboutsA(); 91 | worldGen.placeRoundaboutsB(); 92 | } 93 | 94 | if (m_options.placePonds) 95 | { 96 | worldGen.placePonds(4.0f, 5.0f); 97 | } 98 | 99 | worldGen.placeBuildings(100); 100 | 101 | if (m_options.placeFullPavedAreas) 102 | { 103 | worldGen.placeFullPavedAreas(2.0f, 0.5f); 104 | } 105 | 106 | if (m_options.placeLargeTrees) 107 | { 108 | worldGen.placeLargeTrees(5); 109 | } 110 | 111 | if (m_options.placeSmallTrees) 112 | { 113 | worldGen.placeSmallTrees(10); 114 | } 115 | 116 | if (m_options.removeStreetsAfterGeneration) 117 | { 118 | worldGen.replaceAll(TileType::Street, TileType::Grass); 119 | } 120 | 121 | updateBaseCostMap(); 122 | updateShadowBitmap(); 123 | 124 | for (auto& villager : m_villagers) 125 | { 126 | std::uniform_int_distribution<> rngColorChannel{8, 128}; 127 | 128 | villager.m_color.r = static_cast(rngColorChannel(m_baseRNG)); 129 | villager.m_color.g = static_cast(rngColorChannel(m_baseRNG)); 130 | villager.m_color.b = static_cast(rngColorChannel(m_baseRNG)); 131 | 132 | villager.m_movementPixelPerSec = static_cast(std::uniform_int_distribution<>{15, 20}(m_baseRNG)) * 4.0f; 133 | 134 | villager.setState(Villager::State::EnqueuedForPath); 135 | m_pathfindingQueue.push(&villager); 136 | } 137 | 138 | InitWindow(m_options.screenWidthPixels, m_options.screenHeightPixels, getAppNameWithVersion()); 139 | 140 | if (m_options.targetFPS > 0) 141 | { 142 | SetTargetFPS(m_options.targetFPS); 143 | } 144 | 145 | m_worldMapTexture = LoadRenderTexture(m_worldWidthPixels, m_worldHeightPixels); 146 | updateWorldMapTexture(m_worldMapTexture); 147 | 148 | m_shadowMapTexture = LoadRenderTexture(m_worldWidthPixels, m_worldHeightPixels); 149 | updateShadowMapTexture(m_shadowMapTexture, m_shadowBitmap); 150 | 151 | m_desirePathsMapTexture = LoadRenderTexture(m_worldWidthPixels, m_worldHeightPixels); 152 | 153 | m_camera.target = {static_cast(m_options.screenWidthPixels ), static_cast(m_options.screenHeightPixels )}; 154 | m_camera.offset = {static_cast(m_options.screenWidthPixels / 2), static_cast(m_options.screenHeightPixels / 2)}; 155 | m_camera.zoom = 0.5f; 156 | } 157 | 158 | DesirePathSim::~DesirePathSim() 159 | { 160 | UnloadRenderTexture(m_desirePathsMapTexture); 161 | UnloadRenderTexture(m_shadowMapTexture); 162 | UnloadRenderTexture(m_worldMapTexture); 163 | 164 | CloseWindow(); 165 | } 166 | 167 | void DesirePathSim::run() 168 | { 169 | bool drawVoronoi = false; 170 | bool drawShadowMap = true; 171 | bool drawDesirePath = true; 172 | bool drawDesirePathUpdateRect = false; 173 | bool drawKeysInfo = false; 174 | 175 | std::size_t currentDesirePathsMapUpdateIndex = 0; 176 | 177 | float desirePathDecayRatePerSec = 0.25f; 178 | float desirePathDecayAccu = 0.0f; 179 | 180 | float fpsUpdateRatePerSec = 2.0f; 181 | float fpsUpdateAccu = 0.0f; 182 | int fps = 0; 183 | 184 | std::vector pathfinders; 185 | pathfinders.reserve(m_options.pathfindingThreadCount); 186 | 187 | std::vector pathfindingRNGs; 188 | pathfindingRNGs.reserve(m_options.pathfindingThreadCount); 189 | 190 | std::vector> pathfindingFutures; 191 | pathfindingFutures.reserve(m_options.pathfindingThreadCount); 192 | 193 | for (std::size_t pathfindingThreadIndex = 0; pathfindingThreadIndex < m_options.pathfindingThreadCount; ++pathfindingThreadIndex) 194 | { 195 | pathfinders.emplace_back(m_worldMap.width(), m_worldMap.height()); 196 | 197 | pathfindingRNGs.emplace_back(m_baseRNG()); 198 | 199 | pathfindingFutures.emplace_back(std::async(std::launch::async, [&] (const std::size_t threadIndex) 200 | { 201 | using namespace std::chrono_literals; 202 | 203 | while (m_stopPathfindingThread.load() == false) 204 | { 205 | if (auto villager = m_pathfindingQueue.tryPopFor(100ms).value_or(nullptr); villager != nullptr) 206 | { 207 | villager->reset(m_worldMap, pathfinders[threadIndex], m_baseCostMap, m_tileWidthPixels, m_tileHeightPixels, pathfindingRNGs[threadIndex]); 208 | } 209 | } 210 | }, pathfindingThreadIndex)); 211 | } 212 | 213 | UpdateRect mapUpdateRect; // If this becomes non-0, the map itself has changed there and needs a visual update 214 | 215 | std::vector desirePathsUpdateRects; // Every frame a different subsection of the desire paths will be redrawn 216 | 217 | { 218 | constexpr std::size_t desirePathUpdateRectEdgeSize = 64; 219 | 220 | UpdateRect currentDesirePathUpdateRect{0, 0, desirePathUpdateRectEdgeSize, desirePathUpdateRectEdgeSize}; 221 | 222 | currentDesirePathUpdateRect.right = std::min(currentDesirePathUpdateRect.right , m_worldMap.width () - 1); 223 | currentDesirePathUpdateRect.bottom = std::min(currentDesirePathUpdateRect.bottom, m_worldMap.height() - 1); 224 | 225 | desirePathsUpdateRects.emplace_back(currentDesirePathUpdateRect); 226 | 227 | for (;;) 228 | { 229 | currentDesirePathUpdateRect.left += desirePathUpdateRectEdgeSize; 230 | currentDesirePathUpdateRect.right = std::min(currentDesirePathUpdateRect.left + desirePathUpdateRectEdgeSize, m_worldMap.width() - 1); 231 | 232 | if (currentDesirePathUpdateRect.left >= m_worldMap.width()) 233 | { 234 | currentDesirePathUpdateRect.left = 0; 235 | currentDesirePathUpdateRect.right = std::min(currentDesirePathUpdateRect.left + desirePathUpdateRectEdgeSize, m_worldMap.width() - 1); 236 | 237 | currentDesirePathUpdateRect.top += desirePathUpdateRectEdgeSize; 238 | currentDesirePathUpdateRect.bottom = std::min(currentDesirePathUpdateRect.top + desirePathUpdateRectEdgeSize, m_worldMap.height() - 1); 239 | 240 | if (currentDesirePathUpdateRect.top >= m_worldMap.height()) 241 | break; // Reached end of map 242 | } 243 | 244 | desirePathsUpdateRects.emplace_back(currentDesirePathUpdateRect); 245 | } 246 | } 247 | 248 | while (WindowShouldClose() == false) 249 | { 250 | const auto delta = GetFrameTime(); 251 | 252 | fpsUpdateAccu += delta; 253 | 254 | if (fpsUpdateAccu >= 1.0f / fpsUpdateRatePerSec) 255 | { 256 | fps = GetFPS(); 257 | 258 | fpsUpdateAccu = 0.0f; 259 | } 260 | 261 | tickVillagers(mapUpdateRect, delta); 262 | 263 | updateDesirePathsMapTexture(m_desirePathsMapTexture, desirePathsUpdateRects[currentDesirePathsMapUpdateIndex]); 264 | currentDesirePathsMapUpdateIndex = currentDesirePathsMapUpdateIndex == desirePathsUpdateRects.size() - 1 ? 0 : currentDesirePathsMapUpdateIndex + 1; 265 | 266 | desirePathDecayAccu += delta; 267 | 268 | if (desirePathDecayAccu >= 1.0f / desirePathDecayRatePerSec) 269 | { 270 | if (m_options.decayDesirePaths) 271 | { 272 | decayDesirePaths(m_desirePathsMap, m_baseCostMap); 273 | } 274 | 275 | desirePathDecayAccu = 0.0f; 276 | } 277 | 278 | if (mapUpdateRect.empty() == false) 279 | { 280 | updateWorldMapTexture(m_worldMapTexture, mapUpdateRect); 281 | } 282 | 283 | if (IsKeyPressed(KEY_ESCAPE)) 284 | break; 285 | 286 | if (IsKeyPressed(KEY_V)) 287 | { 288 | drawVoronoi = !drawVoronoi; 289 | } 290 | 291 | if (IsKeyPressed(KEY_S)) 292 | { 293 | drawShadowMap = !drawShadowMap; 294 | } 295 | 296 | if (IsKeyPressed(KEY_D)) 297 | { 298 | drawDesirePath = !drawDesirePath; 299 | } 300 | 301 | if (IsKeyPressed(KEY_U)) 302 | { 303 | drawDesirePathUpdateRect = !drawDesirePathUpdateRect; 304 | } 305 | 306 | if (IsKeyPressed(KEY_F1)) 307 | { 308 | drawKeysInfo = !drawKeysInfo; 309 | } 310 | 311 | const auto mouseWheelMove = GetMouseWheelMove(); 312 | 313 | if (mouseWheelMove != 0) 314 | { 315 | m_camera.zoom += static_cast(mouseWheelMove) * 0.2f; 316 | } 317 | 318 | updateCamera(delta); 319 | 320 | BeginDrawing(); 321 | 322 | ClearBackground({0, 0, 0, 255}); 323 | 324 | BeginMode2D(m_camera); 325 | 326 | if (drawVoronoi) 327 | { 328 | drawVoronoiMap(); 329 | } 330 | else 331 | { 332 | // NOTE: Render texture must be y-flipped due to default OpenGL coordinates (left-bottom) 333 | DrawTextureRec( 334 | m_worldMapTexture.texture, 335 | {0.0f, 0.0f, static_cast(m_worldMapTexture.texture.width), -static_cast(m_worldMapTexture.texture.height)}, 336 | Vector2{0.0f, 0.0f}, 337 | WHITE 338 | ); 339 | 340 | if (drawDesirePath) 341 | { 342 | // NOTE: Render texture must be y-flipped due to default OpenGL coordinates (left-bottom) 343 | DrawTextureRec( 344 | m_desirePathsMapTexture.texture, 345 | {0.0f, 0.0f, static_cast(m_desirePathsMapTexture.texture.width), -static_cast(m_desirePathsMapTexture.texture.height)}, 346 | Vector2{0.0f, 0.0f}, 347 | WHITE 348 | ); 349 | } 350 | 351 | // Draw villagers 352 | 353 | for (const auto& villager : m_villagers) 354 | { 355 | DrawRectangleV(villager.m_position, {static_cast(m_tileWidthPixels), static_cast(m_tileHeightPixels)}, villager.m_color); 356 | } 357 | 358 | if (drawShadowMap) 359 | { 360 | // NOTE: Render texture must be y-flipped due to default OpenGL coordinates (left-bottom) 361 | DrawTextureRec( 362 | m_shadowMapTexture.texture, 363 | {0.0f, 0.0f, static_cast(m_shadowMapTexture.texture.width), -static_cast(m_shadowMapTexture.texture.height)}, 364 | Vector2{0.0f, 0.0f}, 365 | WHITE 366 | ); 367 | } 368 | 369 | if (drawDesirePathUpdateRect) 370 | { 371 | const auto& desirePathsMapUpdateRect = desirePathsUpdateRects[currentDesirePathsMapUpdateIndex]; 372 | 373 | Vector2 position{static_cast(desirePathsMapUpdateRect.left * m_tileWidthPixels), static_cast(desirePathsMapUpdateRect.top * m_tileHeightPixels)}; 374 | 375 | Vector2 size{static_cast(desirePathsMapUpdateRect.width() * m_tileWidthPixels), static_cast(desirePathsMapUpdateRect.height() * m_tileHeightPixels)}; 376 | 377 | DrawRectangleV(position, size, {255, 0, 0, 64}); 378 | } 379 | } 380 | 381 | EndMode2D(); 382 | 383 | DrawText(TextFormat("FPS: %i", fps), 10, 10, 20, BLACK); 384 | 385 | static constexpr int s_kkiKeyInfoFontSize = 20; 386 | 387 | static constexpr char s_kksz8KeysInfoShort[] = "[F1] Toggle keys info"; 388 | static constexpr char s_kksz8KeysInfoFull[] = 389 | "[F1] Toggle keys info\n" 390 | "[ESC] Exit\n" 391 | "[V] Toggle Voronoi\n" 392 | "[S] Toggle shadow map\n" 393 | "[D] Toggle desire path\n" 394 | "[U] Toggle desire path update rect\n" 395 | "[ARROW KEYS] Move map\n" 396 | "[MOUSE WHEEL] Zoom\n"; 397 | 398 | const int keysInfoShortTextWidth = MeasureText(s_kksz8KeysInfoShort, s_kkiKeyInfoFontSize); 399 | 400 | if (drawKeysInfo) 401 | { 402 | const int keysInfoFullTextWidth = MeasureText(s_kksz8KeysInfoFull, s_kkiKeyInfoFontSize); 403 | DrawText(s_kksz8KeysInfoFull, m_options.screenWidthPixels - 10 - keysInfoFullTextWidth, 10, s_kkiKeyInfoFontSize, BLACK); 404 | } 405 | else 406 | { 407 | const int keysInfoShortTextWidth = MeasureText(s_kksz8KeysInfoShort, s_kkiKeyInfoFontSize); 408 | DrawText(s_kksz8KeysInfoShort, m_options.screenWidthPixels - 10 - keysInfoShortTextWidth, 10, s_kkiKeyInfoFontSize, BLACK); 409 | } 410 | 411 | EndDrawing(); 412 | } 413 | 414 | m_stopPathfindingThread.store(true); 415 | 416 | for (auto& pathfindingFuture : pathfindingFutures) 417 | { 418 | pathfindingFuture.wait(); 419 | } 420 | } 421 | 422 | void DesirePathSim::tickVillagers( 423 | UpdateRect& mapUpdateRect, 424 | const float delta 425 | ) 426 | { 427 | for (auto& villager : m_villagers) 428 | { 429 | villager.tick(m_worldMap, mapUpdateRect, m_baseCostMap, m_desirePathsMap, m_options.paveDesirePaths, m_tileWidthPixels, m_tileHeightPixels, m_baseRNG, delta); 430 | 431 | if (villager.getState() == Villager::State::AwaitingPath) 432 | { 433 | villager.setState(Villager::State::EnqueuedForPath); 434 | m_pathfindingQueue.push(&villager); 435 | } 436 | } 437 | } 438 | 439 | void DesirePathSim::updateCamera(const float delta) 440 | { 441 | auto cameraSpeed = 400.0f; 442 | 443 | if (IsKeyDown(KEY_RIGHT_SHIFT)) 444 | cameraSpeed *= 2.0; 445 | 446 | if (IsKeyDown(KEY_LEFT)) 447 | m_camera.target.x -= cameraSpeed * delta; 448 | else if (IsKeyDown(KEY_RIGHT)) 449 | m_camera.target.x += cameraSpeed * delta; 450 | 451 | if (IsKeyDown(KEY_UP)) 452 | m_camera.target.y -= cameraSpeed * delta; 453 | else if (IsKeyDown(KEY_DOWN)) 454 | m_camera.target.y += cameraSpeed * delta; 455 | } 456 | 457 | void DesirePathSim::drawVoronoiMap() 458 | { 459 | for (std::size_t voronoiY = 0; voronoiY < m_voronoiMap.height(); ++voronoiY) 460 | { 461 | for (std::size_t voronoiX = 0; voronoiX < m_voronoiMap.width(); ++voronoiX) 462 | { 463 | Vector2 tilePos{static_cast(voronoiX * m_tileWidthPixels), static_cast(voronoiY * m_tileHeightPixels)}; 464 | 465 | DrawRectangleV(tilePos, {static_cast(m_tileWidthPixels), static_cast(m_tileHeightPixels)}, m_voronoiColorTable.at(m_voronoiMap.at(voronoiX, voronoiY))); 466 | } 467 | } 468 | } 469 | 470 | void DesirePathSim::updateWorldMapTexture(RenderTexture2D& texture, UpdateRect& updateRect) 471 | { 472 | std::uniform_int_distribution<> rngPercent{1, 100}; 473 | 474 | BeginTextureMode(texture); 475 | 476 | for (std::size_t worldTileY = updateRect.top; worldTileY <= updateRect.bottom; ++worldTileY) 477 | { 478 | for (std::size_t worldTileX = updateRect.left; worldTileX <= updateRect.right; ++worldTileX) 479 | { 480 | Vector2 tilePos{static_cast(worldTileX * m_tileWidthPixels), static_cast(worldTileY * m_tileHeightPixels)}; 481 | 482 | Color color{0, 0, 0, 255}; 483 | 484 | if (m_worldMap.at(worldTileX, worldTileY) == TileType::Water) 485 | { 486 | if (worldTileY % 3 == 1) 487 | { 488 | color = (worldTileX % 3 == 0) ? Color{60, 170, 255, 255} : Color{20, 130, 230, 255}; 489 | } 490 | else if (worldTileY % 3 == 0) 491 | { 492 | color = (worldTileX % 3 > 0) ? Color{60, 170, 255, 255} : Color{20, 130, 230, 255}; 493 | } 494 | else 495 | { 496 | color = Color{20, 130, 230, 255}; 497 | } 498 | 499 | // Bevel 500 | if (worldTileX > 0 && m_worldMap.at(worldTileX - 1, worldTileY) != TileType::Water) 501 | { 502 | color.r -= 20; 503 | color.g -= 20; 504 | color.b -= 20; 505 | } 506 | else if (worldTileY > 0 && m_worldMap.at(worldTileX, worldTileY - 1) != TileType::Water) 507 | { 508 | color.r -= 20; 509 | color.g -= 20; 510 | color.b -= 20; 511 | } 512 | } 513 | else if (m_worldMap.at(worldTileX, worldTileY) == TileType::Tree) 514 | { 515 | color = (worldTileX + worldTileY) % 2 ? Color{90, 180, 40, 255} : Color{70, 160, 20, 255}; 516 | 517 | // Bevel 518 | if (worldTileX < m_worldMap.width() - 1 && m_worldMap.at(worldTileX + 1, worldTileY) != TileType::Tree) 519 | { 520 | color.r -= 20; 521 | color.g -= 20; 522 | color.b -= 20; 523 | } 524 | else if (worldTileY < m_worldMap.height() - 1 && m_worldMap.at(worldTileX, worldTileY + 1) != TileType::Tree) 525 | { 526 | color.r -= 20; 527 | color.g -= 20; 528 | color.b -= 20; 529 | } 530 | } 531 | else if (m_worldMap.at(worldTileX, worldTileY) == TileType::BuildingEntrance) 532 | { 533 | if (rngPercent(m_baseRNG) <= 90) 534 | { 535 | color = Color{150, 150, 150, 255}; 536 | } 537 | else 538 | { 539 | color = Color{130, 130, 130, 255}; 540 | } 541 | } 542 | else if (m_worldMap.at(worldTileX, worldTileY) == TileType::Building) 543 | { 544 | color = worldTileX % 2 ? Color{230, 130, 80, 255} : Color{210, 110, 60, 255}; 545 | 546 | // Bevel 547 | if (worldTileX > 0 && m_worldMap.at(worldTileX - 1, worldTileY) != TileType::Building) 548 | { 549 | color.r += 20; 550 | color.g += 20; 551 | color.b += 20; 552 | } 553 | else if (worldTileY > 0 && m_worldMap.at(worldTileX, worldTileY - 1) != TileType::Building) 554 | { 555 | color.r += 20; 556 | color.g += 20; 557 | color.b += 20; 558 | } 559 | else if (worldTileX < m_worldMap.width() - 1 && m_worldMap.at(worldTileX + 1, worldTileY) != TileType::Building) 560 | { 561 | color.r -= 20; 562 | color.g -= 20; 563 | color.b -= 20; 564 | } 565 | else if (worldTileY < m_worldMap.height() - 1 && m_worldMap.at(worldTileX, worldTileY + 1) != TileType::Building) 566 | { 567 | color.r -= 20; 568 | color.g -= 20; 569 | color.b -= 20; 570 | } 571 | } 572 | else if (m_worldMap.at(worldTileX, worldTileY) == TileType::Street) 573 | { 574 | if (rngPercent(m_baseRNG) <= 90) 575 | { 576 | color = Color{150, 150, 150, 255}; 577 | } 578 | else 579 | { 580 | color = Color{130, 130, 130, 255}; 581 | } 582 | } 583 | else if(m_worldMap.at(worldTileX, worldTileY) == TileType::Grass) 584 | { 585 | if (rngPercent(m_baseRNG) <= 20) 586 | { 587 | color = Color{110, 200, 60, 255}; 588 | } 589 | else 590 | { 591 | color = Color{130, 220, 80, 255}; 592 | } 593 | } 594 | 595 | DrawRectangleV(tilePos, {static_cast(m_tileWidthPixels), static_cast(m_tileHeightPixels)}, color); 596 | } 597 | } 598 | 599 | EndTextureMode(); 600 | 601 | updateRect.reset(); 602 | } 603 | 604 | void DesirePathSim::updateWorldMapTexture(RenderTexture2D& texture) 605 | { 606 | UpdateRect updateRect{0, 0, m_worldMap.width() - 1, m_worldMap.height() - 1}; 607 | 608 | updateWorldMapTexture(texture, updateRect); 609 | } 610 | 611 | void DesirePathSim::updateShadowMapTexture(RenderTexture2D& texture, const Bitmap& shadowBitmap) 612 | { 613 | BeginTextureMode(texture); 614 | 615 | for (std::size_t worldTileY = 0; worldTileY < shadowBitmap.height(); ++worldTileY) 616 | { 617 | for (std::size_t worldTileX = 0; worldTileX < shadowBitmap.width(); ++worldTileX) 618 | { 619 | Vector2 tilePos{static_cast(worldTileX * m_tileWidthPixels), static_cast(worldTileY * m_tileHeightPixels)}; 620 | 621 | Color color{0, 0, 0, 0}; 622 | 623 | if (shadowBitmap.at(worldTileX, worldTileY) == 1) // Shadow 624 | { 625 | color = Color{0, 0, 0, 150}; 626 | } 627 | 628 | DrawRectangleV(tilePos, {static_cast(m_tileWidthPixels), static_cast(m_tileHeightPixels)}, color); 629 | } 630 | } 631 | 632 | EndTextureMode(); 633 | } 634 | 635 | void DesirePathSim::updateDesirePathsMapTexture(RenderTexture2D& texture, const UpdateRect& updateRect) 636 | { 637 | BeginTextureMode(texture); 638 | 639 | const Vector2 updateRectPos = {static_cast(updateRect.left * m_tileWidthPixels), static_cast(updateRect.top * m_tileHeightPixels)}; 640 | const Vector2 updateRectSize = {static_cast(updateRect.width() * m_tileWidthPixels), static_cast(updateRect.height() * m_tileHeightPixels)}; 641 | 642 | // SRC_ALPHA, SRC_ALPHA, MIN 643 | rlSetBlendFactors( 0x0302, 0x0302, 0x8007); // Clear 644 | rlSetBlendMode(BLEND_CUSTOM); 645 | DrawRectangleV(updateRectPos, updateRectSize, {0, 0, 0, 0}); 646 | rlSetBlendMode(BLEND_ALPHA); 647 | 648 | for (std::size_t worldTileY = updateRect.top; worldTileY <= updateRect.bottom; ++worldTileY) 649 | { 650 | for (std::size_t worldTileX = updateRect.left; worldTileX <= updateRect.right; ++worldTileX) 651 | { 652 | const Vector2 tilePos{static_cast(worldTileX * m_tileWidthPixels), static_cast(worldTileY * m_tileHeightPixels)}; 653 | 654 | const int selfAlpha = m_desirePathsMap.at(worldTileX, worldTileY); 655 | 656 | int neighborAlphaSum = 0; 657 | 658 | if (worldTileX > 0 ) neighborAlphaSum += m_desirePathsMap.at(worldTileX - 1, worldTileY ); 659 | if (worldTileX < m_worldMap.width () - 1) neighborAlphaSum += m_desirePathsMap.at(worldTileX + 1, worldTileY ); 660 | if (worldTileY > 0 ) neighborAlphaSum += m_desirePathsMap.at(worldTileX , worldTileY - 1); 661 | if (worldTileY < m_worldMap.height() - 1) neighborAlphaSum += m_desirePathsMap.at(worldTileX , worldTileY + 1); 662 | 663 | const auto finalAlpha = static_cast(std::max(selfAlpha, neighborAlphaSum / 4)); // Could be less than 4 neighbors but let's not care too much about map edges 664 | 665 | const auto color = Color{90, 80, 50, finalAlpha}; 666 | 667 | DrawRectangleV(tilePos, {static_cast(m_tileWidthPixels), static_cast(m_tileHeightPixels)}, color); 668 | } 669 | } 670 | 671 | EndTextureMode(); 672 | } 673 | 674 | void DesirePathSim::updateDesirePathsMapTexture(RenderTexture2D& texture) 675 | { 676 | UpdateRect updateRect{0, 0, m_desirePathsMap.width() - 1, m_desirePathsMap.height() - 1}; 677 | 678 | updateDesirePathsMapTexture(texture, updateRect); 679 | } 680 | 681 | void DesirePathSim::updateBaseCostMap() 682 | { 683 | for (std::size_t y = 0; y < m_worldMap.height(); ++y) 684 | { 685 | for (std::size_t x = 0; x < m_worldMap.width(); ++x) 686 | { 687 | m_baseCostMap.at(x, y) = getBaseCostForValue(m_worldMap.at(x, y), m_baseRNG); 688 | } 689 | } 690 | } 691 | 692 | void DesirePathSim::updateShadowBitmap() 693 | { 694 | for (std::size_t y = 0; y < m_worldMap.height(); ++y) 695 | { 696 | for (std::size_t x = 0; x < m_worldMap.width(); ++x) 697 | { 698 | if (m_worldMap.at(x, y) == TileType::Building || m_worldMap.at(x, y) == TileType::Tree) 699 | { 700 | m_shadowBitmap.at(x, y) = 1; 701 | } 702 | else 703 | { 704 | m_shadowBitmap.at(x, y) = 0; 705 | } 706 | } 707 | } 708 | 709 | auto shadowBitmapDilatedEroded = BitmapTransform::dilate(m_shadowBitmap, false); 710 | shadowBitmapDilatedEroded = BitmapTransform::erode(shadowBitmapDilatedEroded, false); 711 | auto shadowBitmapDilatedErodedShifted = BitmapTransform::shift(shadowBitmapDilatedEroded, 1, 1, 0); 712 | m_shadowBitmap = BitmapTransform::mask(shadowBitmapDilatedErodedShifted, m_shadowBitmap, 0, 0); 713 | } 714 | -------------------------------------------------------------------------------- /src/WorldGen.cpp: -------------------------------------------------------------------------------- 1 | #include "WorldGen.hpp" 2 | 3 | #include 4 | 5 | WorldGen::WorldGen(WorldMap& worldMap, std::mt19937_64& rng): 6 | m_worldMap{worldMap}, 7 | m_rng{rng}, 8 | m_rngScaledPercent{1, 100 * m_rngScaledPercentFactor}, 9 | m_rngWorldMapWidth {0, m_worldMap.width () - 1}, 10 | m_rngWorldMapHeight{0, m_worldMap.height() - 1} 11 | { 12 | // NOP 13 | } 14 | 15 | bool WorldGen::findPattern(const Pattern& pattern, std::size_t& x, std::size_t& y, const std::size_t startX, const std::size_t startY) const 16 | { 17 | for (std::size_t mapY = startY; mapY < m_worldMap.height() - pattern.height() + 1; ++mapY) 18 | { 19 | for (std::size_t mapX = (mapY == startY ? startX : 0); mapX < m_worldMap.width() - pattern.width() + 1; ++mapX) 20 | { 21 | bool breakPattern = false; 22 | 23 | for (std::size_t patternY = 0; patternY < pattern.height(); ++patternY) 24 | { 25 | for (std::size_t patternX = 0; patternX < pattern.width(); ++patternX) 26 | { 27 | if (pattern.at(patternX, patternY) == TileType::PatternAny) 28 | continue; 29 | 30 | if (m_worldMap.at(mapX + patternX, mapY + patternY) != pattern.at(patternX, patternY)) 31 | { 32 | breakPattern = true; 33 | break; 34 | } 35 | } 36 | 37 | if (breakPattern) 38 | break; 39 | } 40 | 41 | if (breakPattern == false) 42 | { 43 | // Pattern found 44 | x = mapX; 45 | y = mapY; 46 | 47 | return true; 48 | } 49 | } 50 | } 51 | 52 | return false; 53 | } 54 | 55 | void WorldGen::applyPatch(const Patch& patch, const std::size_t x, const std::size_t y) 56 | { 57 | for (std::size_t patchY = 0; patchY < patch.height(); ++patchY) 58 | { 59 | for (std::size_t patchX = 0; patchX < patch.width(); ++patchX) 60 | { 61 | if (patch.at(patchX, patchY) == TileType::PatchKeep) 62 | continue; 63 | 64 | m_worldMap.at(x + patchX, y + patchY) = patch.at(patchX, patchY); 65 | } 66 | } 67 | } 68 | 69 | // Returns number of replacements performed 70 | std::size_t WorldGen::floodFill(const std::size_t x, const std::size_t y, const TileType fillTileType) 71 | { 72 | const auto tileTypeToReplace = m_worldMap.at(x, y); 73 | 74 | std::size_t count = 0; 75 | 76 | std::stack> coordStack; 77 | 78 | coordStack.emplace(x, y); 79 | 80 | while (coordStack.empty() == false) 81 | { 82 | const auto [currentX, currentY] = coordStack.top(); 83 | coordStack.pop(); 84 | 85 | if (m_worldMap.at(currentX, currentY) == tileTypeToReplace) 86 | { 87 | m_worldMap.at(currentX, currentY) = fillTileType; 88 | 89 | if (currentY < m_worldMap.height() - 1) coordStack.emplace(currentX , currentY + 1); 90 | if (currentY > 0 ) coordStack.emplace(currentX , currentY - 1); 91 | if (currentX < m_worldMap.width () - 1) coordStack.emplace(currentX + 1, currentY ); 92 | if (currentX > 0 ) coordStack.emplace(currentX - 1, currentY ); 93 | 94 | count += 1; 95 | } 96 | } 97 | 98 | return count; 99 | } 100 | 101 | void WorldGen::placeStreetsFromVoronoiMap(const VoronoiMap& voronoiMap) 102 | { 103 | for (std::size_t centerY = 0; centerY < voronoiMap.height(); ++centerY) 104 | { 105 | for (std::size_t centerX = 0; centerX < voronoiMap.width(); ++centerX) 106 | { 107 | const auto self = voronoiMap.at(centerX, centerY); 108 | 109 | if (centerX > 0) 110 | { 111 | if (centerY > 0) 112 | { 113 | if (voronoiMap.at(centerX - 1, centerY - 1) != self) 114 | { 115 | m_worldMap.at(centerX, centerY) = TileType::Street; 116 | continue; 117 | } 118 | } 119 | 120 | if (centerY < voronoiMap.height() - 1) 121 | { 122 | if (voronoiMap.at(centerX - 1, centerY + 1) != self) 123 | { 124 | m_worldMap.at(centerX, centerY) = TileType::Street; 125 | continue; 126 | } 127 | } 128 | 129 | if (voronoiMap.at(centerX - 1, centerY) != self) 130 | { 131 | m_worldMap.at(centerX, centerY) = TileType::Street; 132 | continue; 133 | } 134 | } 135 | 136 | if (centerX < voronoiMap.width() - 1) 137 | { 138 | if (centerY > 0) 139 | { 140 | if (voronoiMap.at(centerX + 1, centerY - 1) != self) 141 | { 142 | m_worldMap.at(centerX, centerY) = TileType::Street; 143 | continue; 144 | } 145 | } 146 | 147 | if (centerY < voronoiMap.height() - 1) 148 | { 149 | if (voronoiMap.at(centerX + 1, centerY + 1) != self) 150 | { 151 | m_worldMap.at(centerX, centerY) = TileType::Street; 152 | continue; 153 | } 154 | } 155 | 156 | if (voronoiMap.at(centerX + 1, centerY) != self) 157 | { 158 | m_worldMap.at(centerX, centerY) = TileType::Street; 159 | continue; 160 | } 161 | } 162 | 163 | if (centerY > 0) 164 | { 165 | if (voronoiMap.at(centerX, centerY - 1) != self) 166 | { 167 | m_worldMap.at(centerX, centerY) = TileType::Street; 168 | continue; 169 | } 170 | } 171 | 172 | if (centerY < voronoiMap.height() - 1) 173 | { 174 | if (voronoiMap.at(centerX, centerY + 1) != self) 175 | { 176 | m_worldMap.at(centerX, centerY) = TileType::Street; 177 | continue; 178 | } 179 | } 180 | } 181 | } 182 | } 183 | 184 | void WorldGen::placePonds(const float fillLimitInPercent, const float areaSizeLimitInPercent) 185 | { 186 | const std::size_t totalTileCount = m_worldMap.width() * m_worldMap.height(); 187 | 188 | std::size_t waterTileCount = 0; 189 | 190 | for (std::size_t count = 1; count <= 100; ++count) 191 | { 192 | const auto startX = m_rngWorldMapWidth (m_rng); 193 | const auto startY = m_rngWorldMapHeight(m_rng); 194 | 195 | const auto randomGrass = m_worldMap.find(TileType::Grass, startX, startY, true); 196 | 197 | if (randomGrass.has_value() == false) 198 | break; 199 | 200 | const auto& [x, y] = randomGrass.value(); 201 | 202 | const auto filledCount = floodFill(x, y, TileType::Water); 203 | 204 | if ((filledCount * 100.0f / static_cast(totalTileCount)) <= areaSizeLimitInPercent) 205 | { 206 | if (((waterTileCount + filledCount) * 100.0f / static_cast(totalTileCount)) <= fillLimitInPercent) 207 | { 208 | waterTileCount += filledCount; 209 | continue; 210 | } 211 | } 212 | 213 | // Revert 214 | floodFill(x, y, TileType::Grass); 215 | } 216 | } 217 | 218 | void WorldGen::placeFullPavedAreas(const float fillLimitInPercent, const float areaSizeLimitInPercent) 219 | { 220 | const std::size_t totalTileCount = m_worldMap.width() * m_worldMap.height(); 221 | 222 | std::size_t filledTileCount = 0; 223 | 224 | for (std::size_t count = 1; count <= 100; ++count) 225 | { 226 | const auto startX = m_rngWorldMapWidth (m_rng); 227 | const auto startY = m_rngWorldMapHeight(m_rng); 228 | 229 | const auto randomGrass = m_worldMap.find(TileType::Grass, startX, startY, true); 230 | 231 | if (randomGrass.has_value() == false) 232 | break; 233 | 234 | const auto& [x, y] = randomGrass.value(); 235 | 236 | const auto filledCount = floodFill(x, y, TileType::Temporary); 237 | 238 | if ((filledCount * 100.0f / static_cast(totalTileCount)) <= areaSizeLimitInPercent) 239 | { 240 | if (((filledTileCount + filledCount) * 100.0f / static_cast(totalTileCount)) <= fillLimitInPercent) 241 | { 242 | floodFill(x, y, TileType::Street); 243 | filledTileCount += filledCount; 244 | 245 | continue; 246 | } 247 | } 248 | 249 | // Revert 250 | floodFill(x, y, TileType::Grass); 251 | } 252 | } 253 | 254 | void WorldGen::replaceAll(const TileType replaceWhat, const TileType replaceWith) 255 | { 256 | for (std::size_t y = 0; y < m_worldMap.height(); ++y) 257 | { 258 | for (std::size_t x = 0; x < m_worldMap.width(); ++x) 259 | { 260 | if (m_worldMap.at(x, y) == replaceWhat) 261 | { 262 | m_worldMap.at(x, y) = replaceWith; 263 | } 264 | } 265 | } 266 | } 267 | 268 | void WorldGen::placeBuildings(const int fillRate) 269 | { 270 | /* 271 | 000000 -> ...... -> 000000 272 | 000000 -> .2222. -> 022220 273 | 000000 -> .2222. -> 022220 274 | 000000 -> .2222. -> 022220 275 | 000000 -> .2222. -> 022220 276 | ..00.. -> ..3... -> ..30.. 277 | ..11.. -> ...... -> ..11.. 278 | ..11.. -> ...... -> ..11.. 279 | ..00.. -> ...... -> ..00.. 280 | */ 281 | 282 | // Variations in size, corner blocks, L shapes and width of entrance paths 283 | 284 | const std::size_t minBuildingWidth = 6; 285 | const std::size_t maxBuildingWidth = 10; 286 | const std::size_t minBuildingHeight = 4; 287 | const std::size_t maxBuildingHeight = 12; 288 | 289 | const std::size_t minEstateWidth = minBuildingWidth + 2; 290 | const std::size_t maxEstateWidth = maxBuildingWidth + 2; 291 | const std::size_t minEstateHeight = minBuildingHeight + 5; 292 | const std::size_t maxEstateHeight = maxBuildingHeight + 5; 293 | 294 | std::vector patterns; 295 | std::vector> patches; // Inner array is for variations 296 | 297 | patterns.reserve((maxBuildingWidth - minBuildingWidth) * (maxBuildingHeight - minBuildingHeight)); 298 | patches.reserve(patterns.capacity()); 299 | 300 | for (std::size_t estateHeight = minEstateHeight; estateHeight <= maxEstateHeight; ++estateHeight) 301 | { 302 | for (std::size_t estateWidth = minEstateWidth; estateWidth <= maxEstateWidth; ++estateWidth) 303 | { 304 | Patch pattern{estateWidth, estateHeight, TileType::Grass}; 305 | 306 | // Street short 307 | for (std::size_t x = 0; x < pattern.width(); ++x) 308 | { 309 | if (x < 2 || x > pattern.width() - 3) 310 | { 311 | pattern.at(x, estateHeight - 4) = TileType::PatternAny; 312 | pattern.at(x, estateHeight - 3) = TileType::PatternAny; 313 | pattern.at(x, estateHeight - 2) = TileType::PatternAny; 314 | pattern.at(x, estateHeight - 1) = TileType::PatternAny; 315 | } 316 | else 317 | { 318 | pattern.at(x, estateHeight - 3) = TileType::Street; 319 | pattern.at(x, estateHeight - 2) = TileType::Street; 320 | } 321 | } 322 | 323 | patterns.emplace_back(std::move(pattern)); 324 | 325 | std::vector patchVariations; 326 | 327 | for (std::size_t buildingHeight = minBuildingHeight; buildingHeight <= estateHeight - 5; ++buildingHeight) 328 | { 329 | const auto buildingWidth = estateWidth - 2; 330 | 331 | Patch patch{estateWidth, estateHeight, TileType::PatchKeep}; 332 | 333 | // Building 334 | for (std::size_t y = 1; y <= buildingHeight; ++y) 335 | { 336 | for (std::size_t x = 1; x <= buildingWidth; ++x) 337 | { 338 | patch.at(x, y) = TileType::Building; 339 | } 340 | } 341 | 342 | // Entrance 343 | #if 1 344 | const bool wideEntrance = (m_rngScaledPercent(m_rng) <= 50 * m_rngScaledPercentFactor); 345 | const bool fullWidthEntrance = (wideEntrance == true) && (m_rngScaledPercent(m_rng) <= 50 * m_rngScaledPercentFactor); 346 | const bool fullWidthEntranceSpansEstate = false; 347 | #else 348 | const bool wideEntrance = false; 349 | const bool fullWidthEntrance = true; 350 | const bool fullWidthEntranceSpansEstate = true; 351 | #endif 352 | 353 | for (std::size_t y = 1 + buildingHeight; y < estateHeight - 3; ++y) 354 | { 355 | for (std::size_t x = 0; x <= buildingWidth + 1; ++x) 356 | { 357 | if (x == 0 || x == buildingWidth + 1) 358 | { 359 | if (fullWidthEntrance && fullWidthEntranceSpansEstate) 360 | { 361 | patch.at(x, y) = TileType::BuildingEntrance; 362 | } 363 | } 364 | else if (x == buildingWidth / 2) 365 | { 366 | patch.at(x, y) = TileType::BuildingEntrance; 367 | } 368 | else if (x == buildingWidth / 2 + 1) 369 | { 370 | if (wideEntrance) 371 | { 372 | patch.at(x, y) = TileType::BuildingEntrance; 373 | } 374 | } 375 | else 376 | { 377 | if (fullWidthEntrance) 378 | { 379 | patch.at(x, y) = TileType::BuildingEntrance; 380 | } 381 | } 382 | } 383 | } 384 | 385 | patchVariations.emplace_back(patch); 386 | 387 | if (buildingWidth >= 5 && buildingHeight >= 5) 388 | { 389 | // Create variation without corner blocks 390 | { 391 | auto patchVariation{patch}; 392 | 393 | patchVariation.at(1, 1) = TileType::PatchKeep; 394 | patchVariation.at(1, buildingHeight) = TileType::PatchKeep; 395 | patchVariation.at(buildingWidth, 1) = TileType::PatchKeep; 396 | patchVariation.at(buildingWidth, buildingHeight) = TileType::PatchKeep; 397 | 398 | patchVariations.emplace_back(std::move(patchVariation)); 399 | } 400 | 401 | // Create L shape variation (top left cut out) 402 | { 403 | auto patchVariation{patch}; 404 | 405 | for (std::size_t y = 1; y <= buildingHeight / 2; ++y) 406 | { 407 | for (std::size_t x = 1; x <= buildingWidth / 2; ++x) 408 | { 409 | patchVariation.at(x, y) = TileType::PatchKeep; 410 | } 411 | } 412 | 413 | patchVariations.emplace_back(std::move(patchVariation)); 414 | } 415 | 416 | // Create L shape variation (top right cut out) 417 | { 418 | auto patchVariation{patch}; 419 | 420 | for (std::size_t y = 1; y <= buildingHeight / 2; ++y) 421 | { 422 | for (std::size_t x = buildingWidth / 2; x <= buildingWidth; ++x) 423 | { 424 | patchVariation.at(x, y) = TileType::PatchKeep; 425 | } 426 | } 427 | 428 | patchVariations.emplace_back(std::move(patchVariation)); 429 | } 430 | 431 | // Create L shape variation (bottom left cut out) 432 | { 433 | auto patchVariation{patch}; 434 | 435 | for (std::size_t y = buildingHeight; y > buildingHeight / 2; --y) 436 | { 437 | for (std::size_t x = 1; x <= buildingWidth / 2; ++x) 438 | { 439 | if (patchVariation.at(x, y + 1) == TileType::BuildingEntrance) 440 | { 441 | patchVariation.at(x, y) = TileType::BuildingEntrance; 442 | } 443 | else 444 | { 445 | patchVariation.at(x, y) = TileType::PatchKeep; 446 | } 447 | } 448 | } 449 | 450 | patchVariations.emplace_back(std::move(patchVariation)); 451 | } 452 | 453 | // Create L shape variation (bottom right cut out) 454 | { 455 | auto patchVariation{patch}; 456 | 457 | for (std::size_t y = buildingHeight; y > buildingHeight / 2; --y) 458 | { 459 | for (std::size_t x = buildingWidth / 2; x <= buildingWidth; ++x) 460 | { 461 | if (patchVariation.at(x, y + 1) == TileType::BuildingEntrance) 462 | { 463 | patchVariation.at(x, y) = TileType::BuildingEntrance; 464 | } 465 | else 466 | { 467 | patchVariation.at(x, y) = TileType::PatchKeep; 468 | } 469 | } 470 | } 471 | 472 | patchVariations.emplace_back(std::move(patchVariation)); 473 | } 474 | } 475 | } 476 | 477 | patches.emplace_back(std::move(patchVariations)); 478 | } 479 | } 480 | 481 | auto applyPatches = [this] (const Pattern& pattern, const std::vector& patchVariations, const int scaledFillRate) 482 | { 483 | std::size_t patternPosX = 0; 484 | std::size_t patternPosY = 0; 485 | 486 | std::size_t findStartPosX = 0; 487 | std::size_t findStartPosY = 0; 488 | 489 | for (;;) 490 | { 491 | if (findPattern(pattern, patternPosX, patternPosY, findStartPosX, findStartPosY) == false) 492 | break; 493 | 494 | if (m_rngScaledPercent(m_rng) <= scaledFillRate) 495 | { 496 | const auto& patch = patchVariations[std::uniform_int_distribution{0, patchVariations.size() - 1}(m_rng)]; 497 | 498 | applyPatch(patch, patternPosX, patternPosY); 499 | } 500 | 501 | findStartPosX = patternPosX + pattern.width(); 502 | findStartPosY = patternPosY; 503 | } 504 | }; 505 | 506 | for (std::size_t index = patterns.size(); index --> 0;) 507 | { 508 | const auto scaledFillRate = fillRate * m_rngScaledPercentFactor / static_cast(patterns.size()) * m_rngScaledPercentFactor; 509 | 510 | applyPatches(patterns[index], patches[index], scaledFillRate); 511 | } 512 | 513 | // Rotate 514 | 515 | for (auto& pattern : patterns) 516 | { 517 | pattern = pattern.rotated90CW(); 518 | } 519 | 520 | for (auto& patchVariations : patches) 521 | { 522 | for (auto& patchVariation : patchVariations) 523 | { 524 | patchVariation = patchVariation.rotated90CW(); 525 | } 526 | } 527 | 528 | for (std::size_t index = patterns.size(); index --> 0;) 529 | { 530 | const auto scaledFillRate = fillRate * m_rngScaledPercentFactor / static_cast(patterns.size()) * m_rngScaledPercentFactor; 531 | 532 | applyPatches(patterns[index], patches[index], scaledFillRate); 533 | } 534 | 535 | // Rotate 536 | 537 | for (auto& pattern : patterns) 538 | { 539 | pattern = pattern.rotated90CW(); 540 | } 541 | 542 | for (auto& patchVariations : patches) 543 | { 544 | for (auto& patchVariation : patchVariations) 545 | { 546 | patchVariation = patchVariation.rotated90CW(); 547 | } 548 | } 549 | 550 | for (std::size_t index = patterns.size(); index --> 0;) 551 | { 552 | const auto scaledFillRate = fillRate * m_rngScaledPercentFactor / static_cast(patterns.size()) * m_rngScaledPercentFactor; 553 | 554 | applyPatches(patterns[index], patches[index], scaledFillRate); 555 | } 556 | 557 | // Rotate 558 | 559 | for (auto& pattern : patterns) 560 | { 561 | pattern = pattern.rotated90CW(); 562 | } 563 | 564 | for (auto& patchVariations : patches) 565 | { 566 | for (auto& patchVariation : patchVariations) 567 | { 568 | patchVariation = patchVariation.rotated90CW(); 569 | } 570 | } 571 | 572 | for (std::size_t index = patterns.size(); index --> 0;) 573 | { 574 | const auto scaledFillRate = fillRate * m_rngScaledPercentFactor / static_cast(patterns.size()) * m_rngScaledPercentFactor; 575 | 576 | applyPatches(patterns[index], patches[index], scaledFillRate); 577 | } 578 | } 579 | 580 | void WorldGen::placeLargeTrees(const int fillRate) 581 | { 582 | /* 583 | 000000000 -> ......... -> 000000000 584 | 000000000 -> ...444... -> 000444000 585 | 000000000 -> ..44444.. -> 004444400 586 | 000000000 -> .4444444. -> 044444440 587 | 000000000 -> .4444444. -> 044444440 588 | 000000000 -> .4444444. -> 044444440 589 | 000000000 -> ..44444.. -> 004444400 590 | 000000000 -> ...444... -> 000444000 591 | 000000000 -> ......... -> 000000000 592 | */ 593 | 594 | static constexpr auto GRASS = TileType::Grass; 595 | static constexpr auto TREE = TileType::Tree; 596 | static constexpr auto KEEP = TileType::PatchKeep; 597 | 598 | Map oPattern{ 599 | {GRASS,GRASS,GRASS,GRASS,GRASS,GRASS,GRASS,GRASS,GRASS}, 600 | {GRASS,GRASS,GRASS,GRASS,GRASS,GRASS,GRASS,GRASS,GRASS}, 601 | {GRASS,GRASS,GRASS,GRASS,GRASS,GRASS,GRASS,GRASS,GRASS}, 602 | {GRASS,GRASS,GRASS,GRASS,GRASS,GRASS,GRASS,GRASS,GRASS}, 603 | {GRASS,GRASS,GRASS,GRASS,GRASS,GRASS,GRASS,GRASS,GRASS}, 604 | {GRASS,GRASS,GRASS,GRASS,GRASS,GRASS,GRASS,GRASS,GRASS}, 605 | {GRASS,GRASS,GRASS,GRASS,GRASS,GRASS,GRASS,GRASS,GRASS}, 606 | {GRASS,GRASS,GRASS,GRASS,GRASS,GRASS,GRASS,GRASS,GRASS}, 607 | {GRASS,GRASS,GRASS,GRASS,GRASS,GRASS,GRASS,GRASS,GRASS} 608 | }; 609 | 610 | Map oPatch{ 611 | {KEEP,KEEP,KEEP,KEEP,KEEP,KEEP,KEEP,KEEP,KEEP}, 612 | {KEEP,KEEP,KEEP,TREE,TREE,TREE,KEEP,KEEP,KEEP}, 613 | {KEEP,KEEP,TREE,TREE,TREE,TREE,TREE,KEEP,KEEP}, 614 | {KEEP,TREE,TREE,TREE,TREE,TREE,TREE,TREE,KEEP}, 615 | {KEEP,TREE,TREE,TREE,TREE,TREE,TREE,TREE,KEEP}, 616 | {KEEP,TREE,TREE,TREE,TREE,TREE,TREE,TREE,KEEP}, 617 | {KEEP,KEEP,TREE,TREE,TREE,TREE,TREE,KEEP,KEEP}, 618 | {KEEP,KEEP,KEEP,TREE,TREE,TREE,KEEP,KEEP,KEEP}, 619 | {KEEP,KEEP,KEEP,KEEP,KEEP,KEEP,KEEP,KEEP,KEEP} 620 | }; 621 | 622 | std::size_t patternPosX = 0; 623 | std::size_t patternPosY = 0; 624 | 625 | std::size_t findStartPosX = 0; 626 | std::size_t findStartPosY = 0; 627 | 628 | for (;;) 629 | { 630 | if (findPattern(oPattern, patternPosX, patternPosY, findStartPosX, findStartPosY) == false) 631 | break; 632 | 633 | if (m_rngScaledPercent(m_rng) <= fillRate * m_rngScaledPercentFactor) 634 | { 635 | applyPatch(oPatch, patternPosX, patternPosY); 636 | } 637 | 638 | findStartPosX = patternPosX + oPattern.width(); 639 | findStartPosY = patternPosY; 640 | } 641 | } 642 | 643 | void WorldGen::placeSmallTrees(const int fillRate) 644 | { 645 | /* 646 | 000000 -> ...... -> 000000 647 | 000000 -> ..44.. -> 004400 648 | 000000 -> .4444. -> 044440 649 | 000000 -> .4444. -> 044440 650 | 000000 -> ..44.. -> 004400 651 | 000000 -> ...... -> 000000 652 | */ 653 | 654 | static constexpr auto GRASS = TileType::Grass; 655 | static constexpr auto TREE = TileType::Tree; 656 | static constexpr auto KEEP = TileType::PatchKeep; 657 | 658 | Map oPattern{ 659 | {GRASS,GRASS,GRASS,GRASS,GRASS,GRASS}, 660 | {GRASS,GRASS,GRASS,GRASS,GRASS,GRASS}, 661 | {GRASS,GRASS,GRASS,GRASS,GRASS,GRASS}, 662 | {GRASS,GRASS,GRASS,GRASS,GRASS,GRASS}, 663 | {GRASS,GRASS,GRASS,GRASS,GRASS,GRASS}, 664 | {GRASS,GRASS,GRASS,GRASS,GRASS,GRASS} 665 | }; 666 | 667 | Map oPatch{ 668 | {KEEP,KEEP,KEEP,KEEP,KEEP,KEEP}, 669 | {KEEP,KEEP,TREE,TREE,KEEP,KEEP}, 670 | {KEEP,TREE,TREE,TREE,TREE,KEEP}, 671 | {KEEP,TREE,TREE,TREE,TREE,KEEP}, 672 | {KEEP,KEEP,TREE,TREE,KEEP,KEEP}, 673 | {KEEP,KEEP,KEEP,KEEP,KEEP,KEEP} 674 | }; 675 | 676 | std::size_t patternPosX = 0; 677 | std::size_t patternPosY = 0; 678 | 679 | std::size_t findStartPosX = 0; 680 | std::size_t findStartPosY = 0; 681 | 682 | for (;;) 683 | { 684 | if (findPattern(oPattern, patternPosX, patternPosY, findStartPosX, findStartPosY) == false) 685 | break; 686 | 687 | if (m_rngScaledPercent(m_rng) <= fillRate * m_rngScaledPercentFactor) 688 | { 689 | applyPatch(oPatch, patternPosX, patternPosY); 690 | } 691 | 692 | findStartPosX = patternPosX + oPattern.width(); 693 | findStartPosY = patternPosY; 694 | } 695 | } 696 | 697 | void WorldGen::placeRoundaboutsA() 698 | { 699 | /* 700 | 00011000 -> 00011000 701 | 00011000 -> 00111100 702 | 00011000 -> 01111110 703 | 11111111 -> 11100111 704 | 11111111 -> 11100111 705 | 00011000 -> 01111110 706 | 00011000 -> 00111100 707 | 00011000 -> 00011000 708 | */ 709 | 710 | static constexpr auto GRASS = TileType::Grass; 711 | static constexpr auto STREET = TileType::Street; 712 | 713 | Map oCrossingStreetPattern{ 714 | { GRASS, GRASS, GRASS,STREET,STREET, GRASS, GRASS, GRASS}, 715 | { GRASS, GRASS, GRASS,STREET,STREET, GRASS, GRASS, GRASS}, 716 | { GRASS, GRASS, GRASS,STREET,STREET, GRASS, GRASS, GRASS}, 717 | {STREET,STREET,STREET,STREET,STREET,STREET,STREET,STREET}, 718 | {STREET,STREET,STREET,STREET,STREET,STREET,STREET,STREET}, 719 | { GRASS, GRASS, GRASS,STREET,STREET, GRASS, GRASS, GRASS}, 720 | { GRASS, GRASS, GRASS,STREET,STREET, GRASS, GRASS, GRASS}, 721 | { GRASS, GRASS, GRASS,STREET,STREET, GRASS, GRASS, GRASS} 722 | }; 723 | 724 | auto placeRoundabout = [&] (const std::size_t x, const std::size_t y) 725 | { 726 | // Top left corner 727 | m_worldMap.at(x + 2, y + 1) = TileType::Street; 728 | m_worldMap.at(x + 1, y + 2) = TileType::Street; 729 | m_worldMap.at(x + 2, y + 2) = TileType::Street; 730 | 731 | // Top right corner 732 | m_worldMap.at(x + 5, y + 1) = TileType::Street; 733 | m_worldMap.at(x + 6, y + 2) = TileType::Street; 734 | m_worldMap.at(x + 5, y + 2) = TileType::Street; 735 | 736 | // Bottom left corner 737 | m_worldMap.at(x + 1, y + 5) = TileType::Street; 738 | m_worldMap.at(x + 2, y + 6) = TileType::Street; 739 | m_worldMap.at(x + 2, y + 5) = TileType::Street; 740 | 741 | // Bottom right corner 742 | m_worldMap.at(x + 5, y + 5) = TileType::Street; 743 | m_worldMap.at(x + 6, y + 5) = TileType::Street; 744 | m_worldMap.at(x + 5, y + 6) = TileType::Street; 745 | 746 | // Center 747 | m_worldMap.at(x + 3, y + 3) = TileType::Grass; 748 | m_worldMap.at(x + 4, y + 3) = TileType::Grass; 749 | m_worldMap.at(x + 3, y + 4) = TileType::Grass; 750 | m_worldMap.at(x + 4, y + 4) = TileType::Grass; 751 | }; 752 | 753 | std::size_t patternPosX = 0; 754 | std::size_t patternPosY = 0; 755 | 756 | std::size_t findStartPosX = 0; 757 | std::size_t findStartPosY = 0; 758 | 759 | while (findPattern(oCrossingStreetPattern, patternPosX, patternPosY, findStartPosX, findStartPosY)) 760 | { 761 | placeRoundabout(patternPosX, patternPosY); 762 | 763 | findStartPosX = patternPosX + oCrossingStreetPattern.width(); 764 | findStartPosY = patternPosY; 765 | } 766 | } 767 | 768 | void WorldGen::placeRoundaboutsB() 769 | { 770 | /* 771 | -> .1111. 772 | 1111 -> 111111 773 | 1111 -> 110011 774 | 1111 -> 110011 775 | 1111 -> 111111 776 | -> .1111. 777 | */ 778 | 779 | Pattern crossingStreetPatternA{4, 5, TileType::Street}; 780 | Pattern crossingStreetPatternB{5, 4, TileType::Street}; 781 | 782 | auto placeRoundabout = [&] (const std::size_t x, const std::size_t y) 783 | { 784 | // Top edge 785 | m_worldMap.at(x , y - 1) = TileType::Street; 786 | m_worldMap.at(x + 1, y - 1) = TileType::Street; 787 | m_worldMap.at(x + 2, y - 1) = TileType::Street; 788 | m_worldMap.at(x + 3, y - 1) = TileType::Street; 789 | 790 | // Bottom edge 791 | m_worldMap.at(x , y + 4) = TileType::Street; 792 | m_worldMap.at(x + 1, y + 4) = TileType::Street; 793 | m_worldMap.at(x + 2, y + 4) = TileType::Street; 794 | m_worldMap.at(x + 3, y + 4) = TileType::Street; 795 | 796 | // Left edge 797 | m_worldMap.at(x - 1, y ) = TileType::Street; 798 | m_worldMap.at(x - 1, y + 1) = TileType::Street; 799 | m_worldMap.at(x - 1, y + 2) = TileType::Street; 800 | m_worldMap.at(x - 1, y + 3) = TileType::Street; 801 | 802 | // Right edge 803 | m_worldMap.at(x + 4, y ) = TileType::Street; 804 | m_worldMap.at(x + 4, y + 1) = TileType::Street; 805 | m_worldMap.at(x + 4, y + 2) = TileType::Street; 806 | m_worldMap.at(x + 4, y + 3) = TileType::Street; 807 | 808 | // Center 809 | m_worldMap.at(x + 1, y + 1) = TileType::Grass; 810 | m_worldMap.at(x + 1, y + 2) = TileType::Grass; 811 | m_worldMap.at(x + 2, y + 1) = TileType::Grass; 812 | m_worldMap.at(x + 2, y + 2) = TileType::Grass; 813 | }; 814 | 815 | std::size_t patternPosX = 0; 816 | std::size_t patternPosY = 0; 817 | 818 | std::size_t findStartPosX = 0; 819 | std::size_t findStartPosY = 0; 820 | 821 | while (findPattern(crossingStreetPatternA, patternPosX, patternPosY, findStartPosX, findStartPosY)) 822 | { 823 | placeRoundabout(patternPosX, patternPosY); 824 | 825 | findStartPosX = patternPosX + 8; 826 | findStartPosY = patternPosY; 827 | } 828 | 829 | findStartPosX = 0; 830 | findStartPosY = 0; 831 | 832 | while (findPattern(crossingStreetPatternB, patternPosX, patternPosY, findStartPosX, findStartPosY)) 833 | { 834 | placeRoundabout(patternPosX, patternPosY); 835 | 836 | findStartPosX = patternPosX + 8; 837 | findStartPosY = patternPosY; 838 | } 839 | } 840 | --------------------------------------------------------------------------------