movement_system.cpp 18 KB

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