visibility_service.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. #include "visibility_service.h"
  2. #include "../core/component.h"
  3. #include "../core/ownership_constants.h"
  4. #include "../core/world.h"
  5. #include "../systems/owner_registry.h"
  6. #include <algorithm>
  7. #include <atomic>
  8. #include <chrono>
  9. #include <cmath>
  10. #include <cstdint>
  11. #include <mutex>
  12. #include <shared_mutex>
  13. #include <utility>
  14. #include <vector>
  15. namespace Game::Map {
  16. namespace {
  17. constexpr float k_default_vision_range = 12.0F;
  18. constexpr float k_half_cell_offset = 0.5F;
  19. constexpr float k_min_tile_size = 0.0001F;
  20. constexpr std::chrono::milliseconds k_min_job_interval{50};
  21. auto inBoundsStatic(int grid_x, int grid_z, int width, int height) -> bool {
  22. return grid_x >= 0 && grid_x < width && grid_z >= 0 && grid_z < height;
  23. }
  24. auto indexStatic(int grid_x, int grid_z, int width) -> int {
  25. return grid_z * width + grid_x;
  26. }
  27. auto worldToGridStatic(float world_coord, float half, float tile_size) -> int {
  28. const float grid_coord = world_coord / tile_size + half;
  29. return static_cast<int>(std::floor(grid_coord + k_half_cell_offset));
  30. }
  31. } // namespace
  32. auto VisibilityService::instance() -> VisibilityService & {
  33. static VisibilityService s_instance;
  34. return s_instance;
  35. }
  36. VisibilityService::~VisibilityService() {
  37. m_shutdownRequested.store(true, std::memory_order_release);
  38. m_queueCv.notify_all();
  39. if (m_workerThread.joinable()) {
  40. m_workerThread.join();
  41. }
  42. }
  43. void VisibilityService::initialize(int width, int height, float tile_size) {
  44. std::unique_lock<std::shared_mutex> const lock(m_cellsMutex);
  45. m_width = std::max(1, width);
  46. m_height = std::max(1, height);
  47. m_tile_size = std::max(k_min_tile_size, tile_size);
  48. m_half_width =
  49. static_cast<float>(m_width) * k_half_cell_offset - k_half_cell_offset;
  50. m_half_height =
  51. static_cast<float>(m_height) * k_half_cell_offset - k_half_cell_offset;
  52. const int count = m_width * m_height;
  53. m_cells.assign(count, static_cast<std::uint8_t>(VisibilityState::Unseen));
  54. m_version.store(1, std::memory_order_release);
  55. m_generation.store(0, std::memory_order_release);
  56. resetThrottle();
  57. m_initialized = true;
  58. }
  59. void VisibilityService::reset() {
  60. if (!m_initialized) {
  61. return;
  62. }
  63. std::unique_lock<std::shared_mutex> const lock(m_cellsMutex);
  64. std::fill(m_cells.begin(), m_cells.end(),
  65. static_cast<std::uint8_t>(VisibilityState::Unseen));
  66. m_version.fetch_add(1, std::memory_order_release);
  67. m_lastPositions.clear();
  68. m_forceFullUpdate = true;
  69. resetThrottle();
  70. }
  71. auto VisibilityService::update(Engine::Core::World &world,
  72. int player_id) -> bool {
  73. if (!m_initialized) {
  74. return false;
  75. }
  76. bool integrated = false;
  77. {
  78. std::lock_guard<std::mutex> const lock(m_queueMutex);
  79. if (m_completedResult.has_value()) {
  80. integrateResult(std::move(m_completedResult.value()));
  81. m_completedResult.reset();
  82. integrated = true;
  83. }
  84. }
  85. if (shouldStartNewJob()) {
  86. auto sources = gatherVisionSources(world, player_id);
  87. if (!sources.empty()) {
  88. auto payload = composeJobPayload(sources);
  89. enqueueJob(std::move(payload));
  90. }
  91. }
  92. return integrated;
  93. }
  94. void VisibilityService::computeImmediate(Engine::Core::World &world,
  95. int player_id) {
  96. if (!m_initialized) {
  97. return;
  98. }
  99. const auto sources = gatherVisionSources(world, player_id);
  100. auto payload = composeJobPayload(sources);
  101. auto result = executeJob(std::move(payload));
  102. if (result.changed) {
  103. std::unique_lock<std::shared_mutex> const lock(m_cellsMutex);
  104. m_cells = std::move(result.cells);
  105. m_version.fetch_add(1, std::memory_order_release);
  106. }
  107. resetThrottle();
  108. }
  109. auto VisibilityService::gatherVisionSources(Engine::Core::World &world,
  110. int player_id)
  111. -> std::vector<VisibilityService::VisionSource> {
  112. std::vector<VisionSource> sources;
  113. const auto entities =
  114. world.get_entities_with<Engine::Core::TransformComponent>();
  115. const float range_padding = m_tile_size * k_half_cell_offset;
  116. auto &owner_registry = Game::Systems::OwnerRegistry::instance();
  117. std::unordered_map<std::uint32_t, CachedPosition> current_positions;
  118. bool any_moved = m_forceFullUpdate;
  119. for (auto *entity : entities) {
  120. auto *transform = entity->get_component<Engine::Core::TransformComponent>();
  121. auto *unit = entity->get_component<Engine::Core::UnitComponent>();
  122. if (transform == nullptr || unit == nullptr) {
  123. continue;
  124. }
  125. if (Game::Core::isNeutralOwner(unit->owner_id)) {
  126. continue;
  127. }
  128. if (unit->owner_id != player_id &&
  129. !owner_registry.are_allies(player_id, unit->owner_id)) {
  130. continue;
  131. }
  132. if (unit->health <= 0) {
  133. continue;
  134. }
  135. const float vision_range =
  136. std::max(unit->vision_range, k_default_vision_range);
  137. const int center_x = worldToGrid(transform->position.x, m_half_width);
  138. const int center_z = worldToGrid(transform->position.z, m_half_height);
  139. if (!inBounds(center_x, center_z)) {
  140. continue;
  141. }
  142. std::uint32_t const entity_id = entity->get_id();
  143. current_positions[entity_id] = {center_x, center_z};
  144. if (!any_moved) {
  145. auto it = m_lastPositions.find(entity_id);
  146. if (it == m_lastPositions.end() || it->second.grid_x != center_x ||
  147. it->second.grid_z != center_z) {
  148. any_moved = true;
  149. }
  150. }
  151. const int cell_radius =
  152. std::max(1, static_cast<int>(std::ceil(vision_range / m_tile_size)));
  153. const float expanded_range_sq =
  154. (vision_range + range_padding) * (vision_range + range_padding);
  155. sources.push_back({center_x, center_z, cell_radius, expanded_range_sq});
  156. }
  157. if (!any_moved) {
  158. for (const auto &[entity_id, pos] : m_lastPositions) {
  159. if (current_positions.find(entity_id) == current_positions.end()) {
  160. any_moved = true;
  161. break;
  162. }
  163. }
  164. }
  165. m_lastPositions = std::move(current_positions);
  166. m_forceFullUpdate = false;
  167. if (!any_moved && !sources.empty()) {
  168. return {};
  169. }
  170. return sources;
  171. }
  172. auto VisibilityService::composeJobPayload(
  173. const std::vector<VisionSource> &sources) const
  174. -> VisibilityService::JobPayload {
  175. std::shared_lock<std::shared_mutex> const lock(m_cellsMutex);
  176. const auto generation_value =
  177. m_generation.fetch_add(1ULL, std::memory_order_relaxed);
  178. return JobPayload{m_width, m_height, m_tile_size,
  179. m_cells, sources, generation_value};
  180. }
  181. void VisibilityService::enqueueJob(JobPayload &&payload) {
  182. {
  183. std::lock_guard<std::mutex> const lock(m_queueMutex);
  184. m_pendingPayload = std::move(payload);
  185. m_lastJobStartTime = std::chrono::steady_clock::now();
  186. }
  187. ensureWorkerRunning();
  188. m_queueCv.notify_one();
  189. }
  190. void VisibilityService::integrateResult(JobResult &&result) {
  191. if (result.changed) {
  192. std::unique_lock<std::shared_mutex> const lock(m_cellsMutex);
  193. m_cells = std::move(result.cells);
  194. m_version.fetch_add(1, std::memory_order_release);
  195. }
  196. }
  197. void VisibilityService::ensureWorkerRunning() {
  198. bool expected = false;
  199. if (m_workerRunning.compare_exchange_strong(expected, true,
  200. std::memory_order_acq_rel)) {
  201. if (m_workerThread.joinable()) {
  202. m_workerThread.join();
  203. }
  204. m_workerThread = std::thread(&VisibilityService::workerLoop, this);
  205. }
  206. }
  207. void VisibilityService::workerLoop() {
  208. while (!m_shutdownRequested.load(std::memory_order_acquire)) {
  209. std::optional<JobPayload> payload_to_process;
  210. {
  211. std::unique_lock<std::mutex> lock(m_queueMutex);
  212. m_queueCv.wait_for(lock, std::chrono::milliseconds(100), [this] {
  213. return m_pendingPayload.has_value() ||
  214. m_shutdownRequested.load(std::memory_order_acquire);
  215. });
  216. if (m_shutdownRequested.load(std::memory_order_acquire)) {
  217. break;
  218. }
  219. if (m_pendingPayload.has_value()) {
  220. payload_to_process = std::move(m_pendingPayload);
  221. m_pendingPayload.reset();
  222. } else {
  223. m_workerRunning.store(false, std::memory_order_release);
  224. break;
  225. }
  226. }
  227. if (payload_to_process.has_value()) {
  228. auto result = executeJob(std::move(payload_to_process.value()));
  229. std::lock_guard<std::mutex> const lock(m_queueMutex);
  230. if (!m_completedResult.has_value() || result.changed) {
  231. m_completedResult = std::move(result);
  232. }
  233. }
  234. }
  235. }
  236. auto VisibilityService::executeJob(JobPayload payload)
  237. -> VisibilityService::JobResult {
  238. const int cell_count = payload.width * payload.height;
  239. std::vector<std::uint8_t> current_visible(cell_count, 0U);
  240. for (const auto &source : payload.sources) {
  241. for (int dz = -source.cell_radius; dz <= source.cell_radius; ++dz) {
  242. const int grid_z = source.center_z + dz;
  243. if (!inBoundsStatic(source.center_x, grid_z, payload.width,
  244. payload.height)) {
  245. continue;
  246. }
  247. const float world_dz = static_cast<float>(dz) * payload.tile_size;
  248. for (int dx = -source.cell_radius; dx <= source.cell_radius; ++dx) {
  249. const int grid_x = source.center_x + dx;
  250. if (!inBoundsStatic(grid_x, grid_z, payload.width, payload.height)) {
  251. continue;
  252. }
  253. const float world_dx = static_cast<float>(dx) * payload.tile_size;
  254. const float dist_sq = world_dx * world_dx + world_dz * world_dz;
  255. if (dist_sq <= source.expanded_range_sq) {
  256. const int idx = indexStatic(grid_x, grid_z, payload.width);
  257. current_visible[idx] = 1U;
  258. }
  259. }
  260. }
  261. }
  262. bool changed = false;
  263. for (int idx = 0; idx < cell_count; ++idx) {
  264. const std::uint8_t now_visible = current_visible[idx];
  265. const auto visible_val =
  266. static_cast<std::uint8_t>(VisibilityState::Visible);
  267. const auto explored_val =
  268. static_cast<std::uint8_t>(VisibilityState::Explored);
  269. if (now_visible != 0U) {
  270. if (payload.cells[idx] != visible_val) {
  271. payload.cells[idx] = visible_val;
  272. changed = true;
  273. }
  274. } else if (payload.cells[idx] == visible_val) {
  275. payload.cells[idx] = explored_val;
  276. changed = true;
  277. }
  278. }
  279. return JobResult{std::move(payload.cells), payload.generation, changed};
  280. }
  281. auto VisibilityService::stateAt(int grid_x,
  282. int grid_z) const -> VisibilityState {
  283. if (!m_initialized || !inBounds(grid_x, grid_z)) {
  284. return VisibilityState::Visible;
  285. }
  286. std::shared_lock<std::shared_mutex> const lock(m_cellsMutex);
  287. return static_cast<VisibilityState>(m_cells[index(grid_x, grid_z)]);
  288. }
  289. auto VisibilityService::isVisibleWorld(float world_x,
  290. float world_z) const -> bool {
  291. if (!m_initialized) {
  292. return true;
  293. }
  294. const int grid_x = worldToGrid(world_x, m_half_width);
  295. const int grid_z = worldToGrid(world_z, m_half_height);
  296. if (!inBounds(grid_x, grid_z)) {
  297. return false;
  298. }
  299. std::shared_lock<std::shared_mutex> const lock(m_cellsMutex);
  300. return m_cells[index(grid_x, grid_z)] ==
  301. static_cast<std::uint8_t>(VisibilityState::Visible);
  302. }
  303. auto VisibilityService::isExploredWorld(float world_x,
  304. float world_z) const -> bool {
  305. if (!m_initialized) {
  306. return true;
  307. }
  308. const int grid_x = worldToGrid(world_x, m_half_width);
  309. const int grid_z = worldToGrid(world_z, m_half_height);
  310. if (!inBounds(grid_x, grid_z)) {
  311. return false;
  312. }
  313. std::shared_lock<std::shared_mutex> const lock(m_cellsMutex);
  314. const auto state = m_cells[index(grid_x, grid_z)];
  315. return state == static_cast<std::uint8_t>(VisibilityState::Visible) ||
  316. state == static_cast<std::uint8_t>(VisibilityState::Explored);
  317. }
  318. auto VisibilityService::snapshotCells() const -> std::vector<std::uint8_t> {
  319. std::shared_lock<std::shared_mutex> const lock(m_cellsMutex);
  320. return m_cells;
  321. }
  322. void VisibilityService::reveal_all() {
  323. if (!m_initialized) {
  324. return;
  325. }
  326. std::unique_lock<std::shared_mutex> const lock(m_cellsMutex);
  327. std::fill(m_cells.begin(), m_cells.end(),
  328. static_cast<std::uint8_t>(VisibilityState::Visible));
  329. m_version.fetch_add(1, std::memory_order_release);
  330. resetThrottle();
  331. }
  332. auto VisibilityService::inBounds(int grid_x, int grid_z) const -> bool {
  333. return grid_x >= 0 && grid_x < m_width && grid_z >= 0 && grid_z < m_height;
  334. }
  335. auto VisibilityService::index(int grid_x, int grid_z) const -> int {
  336. return grid_z * m_width + grid_x;
  337. }
  338. auto VisibilityService::worldToGrid(float world_coord,
  339. float half) const -> int {
  340. const float grid_coord = world_coord / m_tile_size + half;
  341. return static_cast<int>(std::floor(grid_coord + k_half_cell_offset));
  342. }
  343. auto VisibilityService::shouldStartNewJob() const -> bool {
  344. const auto now = std::chrono::steady_clock::now();
  345. return (now - m_lastJobStartTime) >= k_min_job_interval;
  346. }
  347. void VisibilityService::resetThrottle() { m_lastJobStartTime = {}; }
  348. } // namespace Game::Map