movement_system.cpp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. #include "movement_system.h"
  2. #include "../map/terrain_service.h"
  3. #include "../units/troop_config.h"
  4. #include "command_service.h"
  5. #include "core/component.h"
  6. #include "core/event_manager.h"
  7. #include "map/terrain.h"
  8. #include "pathfinding.h"
  9. #include <QVector3D>
  10. #include <algorithm>
  11. #include <cmath>
  12. #include <numbers>
  13. #include <qvectornd.h>
  14. #include <random>
  15. #include <vector>
  16. namespace Game::Systems {
  17. static constexpr int max_waypoint_skip_count = 4;
  18. static constexpr float repath_cooldown_seconds = 0.4F;
  19. namespace {
  20. auto is_point_allowed(const QVector3D &pos,
  21. Engine::Core::EntityID ignore_entity,
  22. float unit_radius = 0.5F) -> bool {
  23. auto &terrain_service = Game::Map::TerrainService::instance();
  24. Pathfinding *pathfinder = CommandService::get_pathfinder();
  25. (void)ignore_entity;
  26. (void)unit_radius;
  27. if (pathfinder != nullptr) {
  28. Point const grid = CommandService::world_to_grid(pos.x(), pos.z());
  29. return pathfinder->is_walkable(grid.x, grid.y);
  30. } else if (terrain_service.is_initialized()) {
  31. int const grid_x = static_cast<int>(std::round(pos.x()));
  32. int const grid_z = static_cast<int>(std::round(pos.z()));
  33. return terrain_service.is_walkable(grid_x, grid_z);
  34. }
  35. return true;
  36. }
  37. auto is_segment_walkable(const QVector3D &from, const QVector3D &to,
  38. Engine::Core::EntityID ignore_entity,
  39. float unit_radius = 0.5F) -> bool {
  40. (void)ignore_entity;
  41. (void)unit_radius;
  42. Pathfinding *pathfinder = CommandService::get_pathfinder();
  43. if (pathfinder == nullptr) {
  44. return true;
  45. }
  46. Point const end_grid = CommandService::world_to_grid(to.x(), to.z());
  47. if (!pathfinder->is_walkable(end_grid.x, end_grid.y)) {
  48. return false;
  49. }
  50. QVector3D const direction = to - from;
  51. float const length = direction.length();
  52. if (length < 0.5F) {
  53. return true;
  54. }
  55. constexpr float sample_interval = 0.5F;
  56. int const num_samples = static_cast<int>(length / sample_interval);
  57. for (int i = 1; i <= num_samples; ++i) {
  58. float const t = static_cast<float>(i) / static_cast<float>(num_samples + 1);
  59. QVector3D const sample_pos = from + direction * t;
  60. Point const sample_grid =
  61. CommandService::world_to_grid(sample_pos.x(), sample_pos.z());
  62. if (!pathfinder->is_walkable(sample_grid.x, sample_grid.y)) {
  63. return false;
  64. }
  65. }
  66. return true;
  67. }
  68. } // namespace
  69. void MovementSystem::update(Engine::Core::World *world, float delta_time) {
  70. CommandService::process_path_results(*world);
  71. auto entities = world->get_entities_with<Engine::Core::MovementComponent>();
  72. for (auto *entity : entities) {
  73. move_unit(entity, world, delta_time);
  74. }
  75. }
  76. void MovementSystem::move_unit(Engine::Core::Entity *entity,
  77. Engine::Core::World *world, float delta_time) {
  78. auto *transform = entity->get_component<Engine::Core::TransformComponent>();
  79. auto *movement = entity->get_component<Engine::Core::MovementComponent>();
  80. auto *unit = entity->get_component<Engine::Core::UnitComponent>();
  81. if ((transform == nullptr) || (movement == nullptr) || (unit == nullptr)) {
  82. return;
  83. }
  84. if (unit->health <= 0 ||
  85. entity->has_component<Engine::Core::PendingRemovalComponent>()) {
  86. return;
  87. }
  88. auto *hold_mode = entity->get_component<Engine::Core::HoldModeComponent>();
  89. bool in_hold_mode = false;
  90. if (hold_mode != nullptr) {
  91. if (hold_mode->exit_cooldown > 0.0F) {
  92. hold_mode->exit_cooldown =
  93. std::max(0.0F, hold_mode->exit_cooldown - delta_time);
  94. }
  95. if (hold_mode->active) {
  96. movement->has_target = false;
  97. movement->vx = 0.0F;
  98. movement->vz = 0.0F;
  99. movement->clear_path();
  100. movement->path_pending = false;
  101. in_hold_mode = true;
  102. }
  103. if (hold_mode->exit_cooldown > 0.0F && !in_hold_mode) {
  104. movement->vx = 0.0F;
  105. movement->vz = 0.0F;
  106. return;
  107. }
  108. }
  109. if (in_hold_mode) {
  110. if (!entity->has_component<Engine::Core::BuildingComponent>()) {
  111. if (transform->has_desired_yaw) {
  112. float const current = transform->rotation.y;
  113. float const target_yaw = transform->desired_yaw;
  114. float const diff =
  115. std::fmod((target_yaw - current + 540.0F), 360.0F) - 180.0F;
  116. float const turn_speed = 180.0F;
  117. float const step =
  118. std::clamp(diff, -turn_speed * delta_time, turn_speed * delta_time);
  119. transform->rotation.y = current + step;
  120. if (std::fabs(diff) < 0.5F) {
  121. transform->has_desired_yaw = false;
  122. }
  123. }
  124. }
  125. return;
  126. }
  127. auto *atk = entity->get_component<Engine::Core::AttackComponent>();
  128. if ((atk != nullptr) && atk->in_melee_lock) {
  129. movement->has_target = false;
  130. movement->vx = 0.0F;
  131. movement->vz = 0.0F;
  132. movement->clear_path();
  133. movement->path_pending = false;
  134. return;
  135. }
  136. auto *builder_prod =
  137. entity->get_component<Engine::Core::BuilderProductionComponent>();
  138. bool const bypass_mode =
  139. (builder_prod != nullptr) && builder_prod->bypass_movement_active;
  140. if (bypass_mode) {
  141. float const dx = builder_prod->bypass_target_x - transform->position.x;
  142. float const dz = builder_prod->bypass_target_z - transform->position.z;
  143. float const dist_sq = dx * dx + dz * dz;
  144. constexpr float k_bypass_arrival_dist_sq = 0.25F;
  145. if (dist_sq < k_bypass_arrival_dist_sq) {
  146. transform->position.x = builder_prod->bypass_target_x;
  147. transform->position.z = builder_prod->bypass_target_z;
  148. builder_prod->bypass_movement_active = false;
  149. movement->vx = 0.0F;
  150. movement->vz = 0.0F;
  151. movement->has_target = false;
  152. movement->clear_path();
  153. } else {
  154. float const dist = std::sqrt(std::max(dist_sq, 0.0001F));
  155. float const nx = dx / dist;
  156. float const nz = dz / dist;
  157. float base_speed = std::max(0.1F, unit->speed);
  158. movement->vx = nx * base_speed;
  159. movement->vz = nz * base_speed;
  160. transform->position.x += movement->vx * delta_time;
  161. transform->position.z += movement->vz * delta_time;
  162. float const target_yaw = std::atan2(movement->vx, movement->vz) * 180.0F /
  163. std::numbers::pi_v<float>;
  164. float const current = transform->rotation.y;
  165. float const diff =
  166. std::fmod((target_yaw - current + 540.0F), 360.0F) - 180.0F;
  167. float const turn_speed = 720.0F;
  168. float const step =
  169. std::clamp(diff, -turn_speed * delta_time, turn_speed * delta_time);
  170. transform->rotation.y = current + step;
  171. }
  172. return;
  173. }
  174. QVector3D const final_goal(movement->goal_x, 0.0F, movement->goal_y);
  175. float const unit_radius =
  176. CommandService::get_unit_radius(*world, entity->get_id());
  177. QVector3D const current_pos_3d(transform->position.x, 0.0F,
  178. transform->position.z);
  179. bool const destination_allowed =
  180. is_point_allowed(final_goal, entity->get_id(), unit_radius);
  181. if (movement->has_target && !destination_allowed) {
  182. movement->clear_path();
  183. movement->has_target = false;
  184. movement->path_pending = false;
  185. movement->pending_request_id = 0;
  186. movement->vx = 0.0F;
  187. movement->vz = 0.0F;
  188. return;
  189. }
  190. if (movement->repath_cooldown > 0.0F) {
  191. movement->repath_cooldown =
  192. std::max(0.0F, movement->repath_cooldown - delta_time);
  193. }
  194. if (movement->time_since_last_path_request < 10.0F) {
  195. movement->time_since_last_path_request += delta_time;
  196. }
  197. float base_speed = std::max(0.1F, unit->speed);
  198. auto *stamina = entity->get_component<Engine::Core::StaminaComponent>();
  199. if (stamina != nullptr && stamina->is_running) {
  200. base_speed *= Engine::Core::StaminaComponent::kRunSpeedMultiplier;
  201. }
  202. const float max_speed = base_speed;
  203. const float accel = max_speed * 4.0F;
  204. const float damping = 6.0F;
  205. if (!movement->has_target) {
  206. QVector3D const current_pos(transform->position.x, 0.0F,
  207. transform->position.z);
  208. float const goal_dist_sq = (final_goal - current_pos).lengthSquared();
  209. constexpr float k_stuck_distance_sq = 0.6F * 0.6F;
  210. bool requested_recovery_move = false;
  211. if (!movement->path_pending && movement->repath_cooldown <= 0.0F &&
  212. goal_dist_sq > k_stuck_distance_sq && std::isfinite(goal_dist_sq) &&
  213. destination_allowed) {
  214. CommandService::MoveOptions opts;
  215. opts.clear_attack_intent = false;
  216. opts.allow_direct_fallback = true;
  217. std::vector<Engine::Core::EntityID> const ids = {entity->get_id()};
  218. std::vector<QVector3D> const targets = {final_goal};
  219. CommandService::move_units(*world, ids, targets, opts);
  220. movement->repath_cooldown = repath_cooldown_seconds;
  221. requested_recovery_move = true;
  222. }
  223. if (!requested_recovery_move) {
  224. movement->vx *= std::max(0.0F, 1.0F - damping * delta_time);
  225. movement->vz *= std::max(0.0F, 1.0F - damping * delta_time);
  226. }
  227. } else {
  228. QVector3D current_pos(transform->position.x, 0.0F, transform->position.z);
  229. QVector3D segment_target(movement->target_x, 0.0F, movement->target_y);
  230. if (movement->has_waypoints()) {
  231. const auto &wp = movement->current_waypoint();
  232. segment_target = QVector3D(wp.first, 0.0F, wp.second);
  233. }
  234. auto refresh_segment_target = [&]() {
  235. if (movement->has_waypoints()) {
  236. const auto &wp = movement->current_waypoint();
  237. movement->target_x = wp.first;
  238. movement->target_y = wp.second;
  239. segment_target =
  240. QVector3D(movement->target_x, 0.0F, movement->target_y);
  241. } else {
  242. segment_target =
  243. QVector3D(movement->target_x, 0.0F, movement->target_y);
  244. }
  245. };
  246. auto try_advance_past_blocked_segment = [&]() {
  247. bool recovered = false;
  248. int skips_remaining = max_waypoint_skip_count;
  249. while (movement->has_waypoints() && skips_remaining-- > 0) {
  250. movement->advance_waypoint();
  251. refresh_segment_target();
  252. if (is_segment_walkable(current_pos, segment_target, entity->get_id(),
  253. unit_radius)) {
  254. recovered = true;
  255. break;
  256. }
  257. }
  258. if (!recovered && !movement->has_waypoints()) {
  259. refresh_segment_target();
  260. if (is_segment_walkable(current_pos, segment_target, entity->get_id(),
  261. unit_radius)) {
  262. recovered = true;
  263. }
  264. }
  265. return recovered;
  266. };
  267. if (!is_segment_walkable(current_pos, segment_target, entity->get_id(),
  268. unit_radius)) {
  269. if (try_advance_past_blocked_segment()) {
  270. } else {
  271. bool issued_path_request = false;
  272. if (!movement->path_pending && movement->repath_cooldown <= 0.0F) {
  273. float const goal_dist_sq = (final_goal - current_pos).lengthSquared();
  274. if (goal_dist_sq > 0.01F && destination_allowed) {
  275. CommandService::MoveOptions opts;
  276. opts.clear_attack_intent = false;
  277. opts.allow_direct_fallback = false;
  278. std::vector<Engine::Core::EntityID> const ids = {entity->get_id()};
  279. std::vector<QVector3D> const targets = {
  280. QVector3D(movement->goal_x, 0.0F, movement->goal_y)};
  281. CommandService::move_units(*world, ids, targets, opts);
  282. movement->repath_cooldown = repath_cooldown_seconds;
  283. issued_path_request = true;
  284. }
  285. }
  286. if (!issued_path_request) {
  287. movement->path_pending = false;
  288. movement->pending_request_id = 0;
  289. }
  290. movement->clear_path();
  291. movement->has_target = false;
  292. movement->vx = 0.0F;
  293. movement->vz = 0.0F;
  294. return;
  295. }
  296. }
  297. float const arrive_radius =
  298. std::clamp(max_speed * delta_time * 2.0F, 0.05F, 0.25F);
  299. float const arrive_radius_sq = arrive_radius * arrive_radius;
  300. float dx = movement->target_x - transform->position.x;
  301. float dz = movement->target_y - transform->position.z;
  302. float dist2 = dx * dx + dz * dz;
  303. int safety_counter = max_waypoint_skip_count;
  304. while (movement->has_target && dist2 < arrive_radius_sq &&
  305. safety_counter-- > 0) {
  306. if (movement->has_waypoints()) {
  307. movement->advance_waypoint();
  308. if (movement->has_waypoints()) {
  309. const auto &wp = movement->current_waypoint();
  310. movement->target_x = wp.first;
  311. movement->target_y = wp.second;
  312. dx = movement->target_x - transform->position.x;
  313. dz = movement->target_y - transform->position.z;
  314. dist2 = dx * dx + dz * dz;
  315. continue;
  316. }
  317. }
  318. transform->position.x = movement->target_x;
  319. transform->position.z = movement->target_y;
  320. movement->has_target = false;
  321. movement->vx = movement->vz = 0.0F;
  322. auto *guard_mode =
  323. entity->get_component<Engine::Core::GuardModeComponent>();
  324. if ((guard_mode != nullptr) && guard_mode->active &&
  325. guard_mode->returning_to_guard_position) {
  326. guard_mode->returning_to_guard_position = false;
  327. }
  328. break;
  329. }
  330. if (!movement->has_target) {
  331. movement->vx *= std::max(0.0F, 1.0F - damping * delta_time);
  332. movement->vz *= std::max(0.0F, 1.0F - damping * delta_time);
  333. } else {
  334. float const distance = std::sqrt(std::max(dist2, 0.0F));
  335. float const nx = dx / std::max(0.0001F, distance);
  336. float const nz = dz / std::max(0.0001F, distance);
  337. float desired_speed = max_speed;
  338. float const slow_radius = arrive_radius * 4.0F;
  339. if (distance < slow_radius) {
  340. desired_speed = max_speed * (distance / slow_radius);
  341. }
  342. float const desired_vx = nx * desired_speed;
  343. float const desired_vz = nz * desired_speed;
  344. float const ax = (desired_vx - movement->vx) * accel;
  345. float const az = (desired_vz - movement->vz) * accel;
  346. movement->vx += ax * delta_time;
  347. movement->vz += az * delta_time;
  348. movement->vx *= std::max(0.0F, 1.0F - 0.5F * damping * delta_time);
  349. movement->vz *= std::max(0.0F, 1.0F - 0.5F * damping * delta_time);
  350. }
  351. }
  352. transform->position.x += movement->vx * delta_time;
  353. transform->position.z += movement->vz * delta_time;
  354. Pathfinding *pathfinder_check = CommandService::get_pathfinder();
  355. if (pathfinder_check != nullptr) {
  356. Point const new_grid = CommandService::world_to_grid(transform->position.x,
  357. transform->position.z);
  358. if (!pathfinder_check->is_walkable(new_grid.x, new_grid.y)) {
  359. transform->position.x -= movement->vx * delta_time;
  360. transform->position.z -= movement->vz * delta_time;
  361. movement->vx = 0.0F;
  362. movement->vz = 0.0F;
  363. movement->has_target = false;
  364. movement->clear_path();
  365. movement->path_pending = false;
  366. if (movement->goal_x != 0.0F || movement->goal_y != 0.0F) {
  367. Point const goal_grid =
  368. CommandService::world_to_grid(movement->goal_x, movement->goal_y);
  369. if (pathfinder_check->is_walkable(goal_grid.x, goal_grid.y)) {
  370. CommandService::MoveOptions opts;
  371. opts.clear_attack_intent = false;
  372. opts.allow_direct_fallback = false;
  373. std::vector<Engine::Core::EntityID> const ids = {entity->get_id()};
  374. std::vector<QVector3D> const targets = {
  375. QVector3D(movement->goal_x, 0.0F, movement->goal_y)};
  376. CommandService::move_units(*world, ids, targets, opts);
  377. }
  378. }
  379. }
  380. }
  381. auto &terrain = Game::Map::TerrainService::instance();
  382. if (terrain.is_initialized()) {
  383. const Game::Map::TerrainHeightMap *hm = terrain.get_height_map();
  384. if (hm != nullptr) {
  385. const float tile = hm->getTileSize();
  386. const int w = hm->getWidth();
  387. const int h = hm->getHeight();
  388. if (w > 0 && h > 0) {
  389. const float half_w = w * 0.5F - 0.5F;
  390. const float half_h = h * 0.5F - 0.5F;
  391. const float min_x = -half_w * tile;
  392. const float max_x = half_w * tile;
  393. const float min_z = -half_h * tile;
  394. const float max_z = half_h * tile;
  395. transform->position.x = std::clamp(transform->position.x, min_x, max_x);
  396. transform->position.z = std::clamp(transform->position.z, min_z, max_z);
  397. }
  398. }
  399. }
  400. float const speed2 =
  401. movement->vx * movement->vx + movement->vz * movement->vz;
  402. bool const is_moving = speed2 > 1e-5F;
  403. if (terrain.is_initialized() && is_moving) {
  404. auto *terrain_ctx =
  405. entity->get_component<Engine::Core::TerrainContextComponent>();
  406. if (terrain_ctx != nullptr) {
  407. terrain_ctx->is_on_bridge = false;
  408. terrain_ctx->is_at_hill_entrance = false;
  409. }
  410. }
  411. auto *terrain_ctx =
  412. entity->get_component<Engine::Core::TerrainContextComponent>();
  413. if (terrain_ctx != nullptr && terrain_ctx->audio_cooldown > 0.0F) {
  414. terrain_ctx->audio_cooldown =
  415. std::max(0.0F, terrain_ctx->audio_cooldown - delta_time);
  416. }
  417. if (!entity->has_component<Engine::Core::BuildingComponent>()) {
  418. if (is_moving) {
  419. float const target_yaw = std::atan2(movement->vx, movement->vz) * 180.0F /
  420. std::numbers::pi_v<float>;
  421. float const current = transform->rotation.y;
  422. float const diff =
  423. std::fmod((target_yaw - current + 540.0F), 360.0F) - 180.0F;
  424. float const turn_speed = 720.0F;
  425. float const step =
  426. std::clamp(diff, -turn_speed * delta_time, turn_speed * delta_time);
  427. transform->rotation.y = current + step;
  428. } else if (transform->has_desired_yaw) {
  429. float const current = transform->rotation.y;
  430. float const target_yaw = transform->desired_yaw;
  431. float const diff =
  432. std::fmod((target_yaw - current + 540.0F), 360.0F) - 180.0F;
  433. float const turn_speed = 180.0F;
  434. float const step =
  435. std::clamp(diff, -turn_speed * delta_time, turn_speed * delta_time);
  436. transform->rotation.y = current + step;
  437. if (std::fabs(diff) < 0.5F) {
  438. transform->has_desired_yaw = false;
  439. }
  440. }
  441. }
  442. }
  443. auto MovementSystem::has_reached_target(
  444. const Engine::Core::TransformComponent *transform,
  445. const Engine::Core::MovementComponent *movement) -> bool {
  446. float const dx = movement->target_x - transform->position.x;
  447. float const dz = movement->target_y - transform->position.z;
  448. float const distance_squared = dx * dx + dz * dz;
  449. const float threshold = 0.1F;
  450. return distance_squared < threshold * threshold;
  451. }
  452. } // namespace Game::Systems