Index: ps/trunk/binaries/data/mods/public/maps/scenarios/unit_dancing_test_triggers.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/scenarios/unit_dancing_test_triggers.js (revision 24707) +++ ps/trunk/binaries/data/mods/public/maps/scenarios/unit_dancing_test_triggers.js (revision 24708) @@ -1,363 +1,389 @@ const ARCHER_TEMPLATE = "units/maur/infantry_archer_b"; const JAV_TEMPLATE = "units/mace/infantry_javelineer_b"; const REG_UNIT_TEMPLATE = "units/athen/infantry_spearman_b"; const FAST_UNIT_TEMPLATE = "units/athen/cavalry_swordsman_b"; +const FAST_UNIT_TEMPLATE_2 = "units/athen/cavalry_javelineer_b"; const ATTACKER = 2; var QuickSpawn = function(x, z, template, owner = 1) { let ent = Engine.AddEntity(template); let cmpEntOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpEntOwnership) cmpEntOwnership.SetOwner(owner); let cmpEntPosition = Engine.QueryInterface(ent, IID_Position); cmpEntPosition.JumpTo(x, z); return ent; }; var Rotate = function(angle, ent) { let cmpEntPosition = Engine.QueryInterface(ent, IID_Position); cmpEntPosition.SetYRotation(angle); return ent; }; var WalkTo = function(x, z, queued, ent, owner=1) { ProcessCommand(owner, { "type": "walk", "entities": Array.isArray(ent) ? ent : [ent], "x": x, "z": z, "queued": queued, "force": true, }); return ent; }; var FormationWalkTo = function(x, z, queued, ent, owner=1) { ProcessCommand(owner, { "type": "walk", "entities": Array.isArray(ent) ? ent : [ent], "x": x, "z": z, "queued": queued, "force": true, "formation": "special/formations/box" }); return ent; }; var Patrol = function(x, z, queued, ent, owner=1) { ProcessCommand(owner, { "type": "patrol", "entities": Array.isArray(ent) ? ent : [ent], "x": x, "z": z, "queued": queued, "force": true, }); return ent; }; var Attack = function(target, ent) { let comm = { "type": "attack", "entities": Array.isArray(ent) ? ent : [ent], "target": target, "queued": true, "force": true, }; ProcessCommand(ATTACKER, comm); return ent; }; var gx; var gy; var experiments = {}; var manual_dance = function(attacker, target, dance_distance, att_distance = 50, n_attackers = 1) { return () => { let dancer = QuickSpawn(gx, gy, target); for (let i = 0; i < 100; ++i) WalkTo(gx, gy + dance_distance * (i % 2), true, dancer); let attackers = []; for (let i = 0; i < n_attackers; ++i) attackers.push(Attack(dancer, WalkTo(gx + att_distance, gy + i * 2, true, QuickSpawn(gx + att_distance + i, gy, attacker, ATTACKER), ATTACKER))); return [[dancer], attackers]; }; }; var manual_square_dance = function(attacker, target, dance_distance, att_distance = 50, n_attackers = 1) { return () => { let dancer = QuickSpawn(gx, gy, target); for (let i = 0; i < 25; ++i) { WalkTo(gx + dance_distance / 2, gy + dance_distance / 2, true, dancer); WalkTo(gx + dance_distance / 2, gy - dance_distance / 2, true, dancer); WalkTo(gx - dance_distance / 2, gy - dance_distance / 2, true, dancer); WalkTo(gx - dance_distance / 2, gy + dance_distance / 2, true, dancer); } let attackers = []; for (let i = 0; i < n_attackers; ++i) attackers.push(Attack(dancer, WalkTo(gx + att_distance, gy + i * 2, true, QuickSpawn(gx + att_distance + i, gy, attacker, ATTACKER), ATTACKER))); return [[dancer], attackers]; }; }; var manual_zigzag_dance = function(attacker, target, dance_distance, att_distance = 50, n_attackers = 1) { return () => { let dancer = QuickSpawn(gx, gy, target); for (let i = 0; i < 12; ++i) { WalkTo(gx + dance_distance, gy + dance_distance, true, dancer); WalkTo(gx + dance_distance * 2, gy - dance_distance, true, dancer); WalkTo(gx + dance_distance * 3, gy + dance_distance, true, dancer); WalkTo(gx + dance_distance * 4, gy - dance_distance, true, dancer); WalkTo(gx + dance_distance * 5, gy + dance_distance, true, dancer); WalkTo(gx + dance_distance * 6, gy - dance_distance, true, dancer); WalkTo(gx + dance_distance * 7, gy + dance_distance, true, dancer); WalkTo(gx + dance_distance * 6, gy - dance_distance, true, dancer); WalkTo(gx + dance_distance * 5, gy + dance_distance, true, dancer); WalkTo(gx + dance_distance * 4, gy - dance_distance, true, dancer); WalkTo(gx + dance_distance * 3, gy + dance_distance, true, dancer); WalkTo(gx + dance_distance * 2, gy - dance_distance, true, dancer); } let attackers = []; for (let i = 0; i < n_attackers; ++i) attackers.push(Attack(dancer, WalkTo(gx + att_distance, gy + i * 2, true, QuickSpawn(gx + att_distance + i, gy, attacker, ATTACKER), ATTACKER))); return [[dancer], attackers]; }; }; var patrol_dance = function(attacker, target, dance_distance, att_distance = 50, n_attackers = 1) { return () => { let dancer = QuickSpawn(gx, gy, target); Patrol(gx, gy + dance_distance, true, dancer); let attackers = []; for (let i = 0; i < n_attackers; ++i) attackers.push(Attack(dancer, WalkTo(gx + att_distance, gy + i * 2, true, QuickSpawn(gx + att_distance + i, gy, attacker, ATTACKER), ATTACKER))); return [[dancer], attackers]; }; }; var manual_formation_dance = function(attacker, target, dance_distance, att_distance = 50, n_attackers = 1) { return () => { let dancers = []; for (let x = 0; x < 4; x++) for (let z = 0; z < 4; z++) dancers.push(QuickSpawn(gx+x, gy+z, target)); for (let i = 0; i < 100; ++i) FormationWalkTo(gx, gy + dance_distance * (i % 2), i != 0, dancers); let attackers = []; for (let i = 0; i < n_attackers; ++i) attackers.push(Attack(dancers[0], WalkTo(gx + att_distance, gy + i * 2, true, QuickSpawn(gx + att_distance + i, gy, attacker, ATTACKER), ATTACKER))); return [dancers, attackers.concat(dancers)]; }; }; +/** + * This isn't really dancing, but it can still fail. + */ +var avoidance = function(attacker, target, att_distance = 10) +{ + return () => { + let dancer = QuickSpawn(200, 300, target); + for (let i = 0; i < 5; ++i) + { + WalkTo(300, 400, true, dancer); + WalkTo(400, 300, true, dancer); + WalkTo(300, 200, true, dancer); + WalkTo(200, 300, true, dancer); + } + + let attackers = []; + attackers.push(Attack(dancer, QuickSpawn(200, 290, attacker, ATTACKER), ATTACKER)); + return [[dancer], attackers]; + }; +}; + experiments.unit_manual_dance_archer = { "spawn": manual_dance(ARCHER_TEMPLATE, REG_UNIT_TEMPLATE, 5) }; experiments.unit_manual_bad_dance_archer = { "spawn": manual_dance(ARCHER_TEMPLATE, REG_UNIT_TEMPLATE, 20) }; experiments.unit_manual_dance_multi_archer = { "spawn": manual_dance(ARCHER_TEMPLATE, REG_UNIT_TEMPLATE, 5, 50, 4) }; experiments.fast_unit_manual_dance_archer = { "spawn": manual_dance(ARCHER_TEMPLATE, FAST_UNIT_TEMPLATE, 5) }; experiments.fast_unit_manual_bad_dance_archer = { "spawn": manual_dance(ARCHER_TEMPLATE, FAST_UNIT_TEMPLATE, 20) }; experiments.unit_manual_dance_jav = { "spawn": manual_dance(JAV_TEMPLATE, REG_UNIT_TEMPLATE, 5, 20) }; experiments.unit_manual_bad_dance_jav = { "spawn": manual_dance(JAV_TEMPLATE, REG_UNIT_TEMPLATE, 20, 20) }; experiments.unit_manual_dance_multi_jav = { "spawn": manual_dance(JAV_TEMPLATE, REG_UNIT_TEMPLATE, 5, 20, 4) }; experiments.fast_unit_manual_dance_jav = { "spawn": manual_dance(JAV_TEMPLATE, FAST_UNIT_TEMPLATE, 5, 20) }; experiments.fast_unit_manual_bad_dance_jav = { "spawn": manual_dance(JAV_TEMPLATE, FAST_UNIT_TEMPLATE, 20, 20) }; experiments.fast_patrol_archer = { "spawn": patrol_dance(ARCHER_TEMPLATE, FAST_UNIT_TEMPLATE, 5, 50) }; experiments.fast_patrol_jav = { "spawn": patrol_dance(JAV_TEMPLATE, FAST_UNIT_TEMPLATE, 5, 20) }; experiments.reg_square_archer = { "spawn": manual_square_dance(ARCHER_TEMPLATE, REG_UNIT_TEMPLATE, 5, 50) }; experiments.reg_square_archer_multi = { "spawn": manual_square_dance(ARCHER_TEMPLATE, REG_UNIT_TEMPLATE, 5, 50, 5) }; experiments.fast_square_archer = { "spawn": manual_square_dance(ARCHER_TEMPLATE, FAST_UNIT_TEMPLATE, 5, 50) }; experiments.fast_square_archer_multi = { "spawn": manual_square_dance(ARCHER_TEMPLATE, FAST_UNIT_TEMPLATE, 5, 50, 5) }; experiments.reg_zigzag_archer = { "spawn": manual_zigzag_dance(ARCHER_TEMPLATE, REG_UNIT_TEMPLATE, 3, 50) }; experiments.reg_zigzag_archer_multi = { "spawn": manual_zigzag_dance(ARCHER_TEMPLATE, REG_UNIT_TEMPLATE, 3, 50, 5) }; experiments.reg_zigzag_jav = { "spawn": manual_zigzag_dance(JAV_TEMPLATE, REG_UNIT_TEMPLATE, 3, 20, 5) }; experiments.fast_zizag_archer = { "spawn": manual_zigzag_dance(ARCHER_TEMPLATE, FAST_UNIT_TEMPLATE, 3, 50) }; experiments.fast_zizag_archer_multi = { "spawn": manual_zigzag_dance(ARCHER_TEMPLATE, FAST_UNIT_TEMPLATE, 3, 50, 5) }; experiments.formation_dance_slow_archer = { "spawn": manual_formation_dance(ARCHER_TEMPLATE, REG_UNIT_TEMPLATE, 25, 50, 5) }; experiments.formation_dance_fast_archer = { "spawn": manual_formation_dance(ARCHER_TEMPLATE, FAST_UNIT_TEMPLATE, 25, 50, 5) }; experiments.formation_bad_dance_slow_archer = { "spawn": manual_formation_dance(ARCHER_TEMPLATE, REG_UNIT_TEMPLATE, 50, 50, 5) }; experiments.formation_bad_dance_fast_archer = { "spawn": manual_formation_dance(ARCHER_TEMPLATE, FAST_UNIT_TEMPLATE, 50, 50, 5) }; +experiments.fast_on_fast = { + "spawn": avoidance(FAST_UNIT_TEMPLATE, FAST_UNIT_TEMPLATE_2) +}; + var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); Trigger.prototype.SetupUnits = function() { warn("Experiment start"); let start = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime(); gx = 100; gy = 100; for (let key in experiments) { let [dancers, attackers] = experiments[key].spawn(); let ReportResults = (killed) => { warn(`Exp ${key} finished in ${Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() - start}, ` + `target was ${killed ? "killed" : "not killed (failure)"}`); ProcessCommand(1, { "type": "delete-entities", "entities": dancers, "controlAllUnits": true }); ProcessCommand(2, { "type": "delete-entities", "entities": attackers, "controlAllUnits": true }); }; // xxtreme hack: hook into UnitAI let uai = Engine.QueryInterface(dancers[0], IID_UnitAI); let odes = uai.OnDestroy; uai.OnDestroy = () => ReportResults(true) && odes(); uai.FindNewTargets = () => { ReportResults(false); uai.OnDestroy = odes; }; gx += 70; if (gx >= 520) { gx = 100; gy += 70; } } }; /** * Remove unit spread for the second wave. */ Trigger.prototype.RemoveSpread = function() { let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager); cmpModifiersManager.AddModifiers("no_promotion", { "Attack/Ranged/Spread": [{ "affects": ["Unit"], "replace": 0 }], }, 4); // player 2 is ent 4 }; Trigger.prototype.EndGame = function() { Engine.QueryInterface(3, IID_Player).SetState("defeated", "trigger"); Engine.QueryInterface(4, IID_Player).SetState("won", "trigger"); }; var cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager); // Reduce player 1 vision range (or patrolling units reacct) cmpModifiersManager.AddModifiers("no_promotion", { "Vision/Range": [{ "affects": ["Unit"], "replace": 5 }], }, 3); // player 1 is ent 3 // Prevent promotions, messes up things. cmpModifiersManager.AddModifiers("no_promotion_A", { "Promotion/RequiredXp": [{ "affects": ["Unit"], "replace": 50000 }], }, 3); cmpModifiersManager.AddModifiers("no_promotion_B", { "Promotion/RequiredXp": [{ "affects": ["Unit"], "replace": 50000 }], }, 4); // player 2 is ent 4 cmpTrigger.DoAfterDelay(3000, "SetupUnits", {}); // Second run cmpTrigger.DoAfterDelay(150000, "RemoveSpread", {}); cmpTrigger.DoAfterDelay(151000, "SetupUnits", {}); cmpTrigger.DoAfterDelay(300000, "EndGame", {}); Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry_melee_axeman.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry_melee_axeman.xml (revision 24707) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry_melee_axeman.xml (revision 24708) @@ -1,47 +1,47 @@ Axe 6.9 0 2.3 - 3.5 + 4 500 1000 Unit+!Ship 40 10 Cavalry Axeman Axeman 4 1 3 2 attack/weapon/sword_attack.xml 1.1 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry_melee_spearman.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry_melee_spearman.xml (revision 24707) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry_melee_spearman.xml (revision 24708) @@ -1,49 +1,49 @@ Spear 4 3 0 Cavalry 1.75 - 4.5 + 4 625 1250 Human 50 Cavalry Spearman Counters: 1.75× vs Cavalry. Spearman 4 3 attack/weapon/spear_attack.xml 1.1 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry_melee_swordsman.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry_melee_swordsman.xml (revision 24707) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry_melee_swordsman.xml (revision 24708) @@ -1,47 +1,47 @@ Sword 6.5 0 0 - 3.5 + 4 375 750 Unit+!Ship 40 10 Cavalry Swordsman Swordsman 4 1 3 4 attack/weapon/sword_attack.xml 1.1 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged_archer.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged_archer.xml (revision 24707) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged_archer.xml (revision 24708) @@ -1,41 +1,44 @@ Bow 0 7 0 60 0 500 1000 0 100.0 39.81 3.0 false Human 50 Archer Cavalry Archer attack/impact/arrow_impact.xml attack/weapon/bow_attack.xml + + 0.9 + Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged_javelineer.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged_javelineer.xml (revision 24707) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged_javelineer.xml (revision 24708) @@ -1,41 +1,44 @@ Javelin 0 18 0 30 0 750 1250 0 70 39.81 4.0 false Human 50 Cavalry Javelineer Javelineer attack/weapon/javelin_attack.xml attack/impact/javelin_impact.xml + + 0.9 + Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_archer.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_archer.xml (revision 24707) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_archer.xml (revision 24708) @@ -1,47 +1,50 @@ Bow 0 14 0 60 0 500 1000 0 100.0 1.0 39.81 false Human Ranged Archer Champion Cavalry Archer special/formations/skirmish -2 -2 attack/impact/arrow_impact.xml attack/weapon/bow_attack.xml + + 0.9 + Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_axeman.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_axeman.xml (revision 24707) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_axeman.xml (revision 24708) @@ -1,39 +1,39 @@ Axe 13.8 0 4.6 - 3.5 + 4 500 1000 Unit+!Ship 300 Melee Axeman Champion Cavalry Axeman 2 attack/weapon/sword_attack.xml 1.1 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_javelineer.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_javelineer.xml (revision 24707) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_javelineer.xml (revision 24708) @@ -1,47 +1,50 @@ Javelin 0 36 0 30 0 750 1250 0 70 1.0 39.81 false Human Champion Cavalry Javelineer Ranged Javelineer special/formations/skirmish -2 -2 attack/weapon/javelin_attack.xml attack/impact/javelin_impact.xml + + 0.9 + Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_spearman.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_spearman.xml (revision 24707) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_spearman.xml (revision 24708) @@ -1,47 +1,47 @@ Spear 8 6 0 - 4.5 + 4 Cavalry 1.75 500 1250 Human 300 Champion Cavalry Spearman Counters: 1.75× vs Cavalry. Melee Spearman 1 2 attack/weapon/spear_attack.xml 1.1 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_swordsman.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_swordsman.xml (revision 24707) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_swordsman.xml (revision 24708) @@ -1,39 +1,39 @@ Sword 13.0 0 0.0 - 3.5 + 4 375 750 Unit+!Ship 300 Melee Swordsman Champion Cavalry Swordsman 4 attack/weapon/sword_attack.xml 1.1 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_archer.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_archer.xml (revision 24707) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_archer.xml (revision 24708) @@ -1,47 +1,50 @@ Bow 0 28 0 60 0 500 1000 0 100.0 0.5 39.81 false Human Ranged Archer Hero Cavalry Archer special/formations/skirmish -2 -2 attack/impact/arrow_impact.xml attack/weapon/bow_attack.xml + + 0.9 + Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_axeman.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_axeman.xml (revision 24707) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_axeman.xml (revision 24708) @@ -1,33 +1,36 @@ Axe 27.6 0 9.2 - 3.5 + 4 500 1000 Unit+!Ship Melee Axeman Hero Cavalry Axeman 1 attack/weapon/sword_attack.xml + + 1.1 + Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_javelineer.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_javelineer.xml (revision 24707) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_javelineer.xml (revision 24708) @@ -1,47 +1,50 @@ Javelin 0 60 0 30 0 750 1250 0 70 0.5 39.81 false Human Hero Cavalry Javelineer Ranged Javelineer special/formations/skirmish -2 -2 attack/weapon/javelin_attack.xml attack/impact/javelin_impact.xml + + 0.9 + Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_maceman.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_maceman.xml (revision 24707) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_maceman.xml (revision 24708) @@ -1,25 +1,28 @@ Mace 0 0 32 4 500 1000 Hero Cavalry Maceman Melee Maceman attack/weapon/sword_attack.xml + + 1.1 + Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_spearman.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_spearman.xml (revision 24707) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_spearman.xml (revision 24708) @@ -1,41 +1,44 @@ Spear 16 12 0 - 4.5 + 4 Cavalry 1.5 500 1250 Human Hero Cavalry Spearman Counters: 1.5× vs Cavalry. Melee Spearman 1 2 attack/weapon/spear_attack.xml + + 1.1 + Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_swordsman.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_swordsman.xml (revision 24707) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_swordsman.xml (revision 24708) @@ -1,40 +1,43 @@ Sword 26.0 0.0 0.0 - 3.5 + 4 375 750 Unit+!Ship -50 50 Melee Swordsman Hero Cavalry Swordsman 1 1 attack/weapon/sword_attack.xml + + 1.1 + Index: ps/trunk/source/simulation2/components/CCmpUnitMotion.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpUnitMotion.cpp (revision 24707) +++ ps/trunk/source/simulation2/components/CCmpUnitMotion.cpp (revision 24708) @@ -1,1652 +1,1661 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "simulation2/system/Component.h" #include "ICmpUnitMotion.h" #include "simulation2/components/ICmpObstruction.h" #include "simulation2/components/ICmpObstructionManager.h" #include "simulation2/components/ICmpOwnership.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpPathfinder.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/components/ICmpValueModificationManager.h" #include "simulation2/components/ICmpVisual.h" #include "simulation2/helpers/Geometry.h" #include "simulation2/helpers/Render.h" #include "simulation2/MessageTypes.h" #include "simulation2/serialization/SerializedPathfinder.h" #include "simulation2/serialization/SerializedTypes.h" #include "graphics/Overlay.h" #include "graphics/Terrain.h" #include "maths/FixedVector2D.h" #include "ps/CLogger.h" #include "ps/Profile.h" #include "renderer/Scene.h" // For debugging; units will start going straight to the target // instead of calling the pathfinder #define DISABLE_PATHFINDER 0 /** * Min/Max range to restrict short path queries to. (Larger ranges are (much) slower, * smaller ranges might miss some legitimate routes around large obstacles.) * NB: keep the max-range in sync with the vertex pathfinder "move the search space" heuristic. */ static const entity_pos_t SHORT_PATH_MIN_SEARCH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*3)/2; static const entity_pos_t SHORT_PATH_MAX_SEARCH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*14); static const entity_pos_t SHORT_PATH_SEARCH_RANGE_INCREMENT = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*1); static const u8 SHORT_PATH_SEARCH_RANGE_INCREASE_DELAY = 2; /** * When using the short-pathfinder to rejoin a long-path waypoint, aim for a circle of this radius around the waypoint. */ static const entity_pos_t SHORT_PATH_LONG_WAYPOINT_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*1); /** * Minimum distance to goal for a long path request */ static const entity_pos_t LONG_PATH_MIN_DIST = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*4); /** * If we are this close to our target entity/point, then think about heading * for it in a straight line instead of pathfinding. */ -static const entity_pos_t DIRECT_PATH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*4); +static const entity_pos_t DIRECT_PATH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*6); /** * To avoid recomputing paths too often, have some leeway for target range checks * based on our distance to the target. Increase that incertainty by one navcell * for every this many tiles of distance. */ static const entity_pos_t TARGET_UNCERTAINTY_MULTIPLIER = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*2); /** * When following a known imperfect path (i.e. a path that won't take us in range of our goal * we still recompute a new path every N turn to adapt to moving targets (for example, ships that must pickup * units may easily end up in this state, they still need to adjust to moving units). * This is rather arbitrary and mostly for simplicity & optimisation (a better recomputing algorithm * would not need this). * Keep in mind that MP turns are currently 500ms. */ static const u8 KNOWN_IMPERFECT_PATH_RESET_COUNTDOWN = 12; /** * When we fail to move this many turns in a row, inform other components that the move will fail. * Experimentally, this number needs to be somewhat high or moving groups of units will lead to stuck units. * However, too high means units will look idle for a long time when they are failing to move. * TODO: if UnitMotion could send differentiated "unreachable" and "currently stuck" failing messages, * this could probably be lowered. * TODO: when unit pushing is implemented, this number can probably be lowered. */ static const u8 MAX_FAILED_MOVEMENTS = 40; /** * When computing paths but failing to move, we want to occasionally alternate pathfinder systems * to avoid getting stuck (the short pathfinder can unstuck the long-range one and vice-versa, depending). */ static const u8 ALTERNATE_PATH_TYPE_DELAY = 3; static const u8 ALTERNATE_PATH_TYPE_EVERY = 6; /** * After this many failed computations, start sending "VERY_OBSTRUCTED" messages instead. * Should probably be larger than ALTERNATE_PATH_TYPE_DELAY. */ static const u8 VERY_OBSTRUCTED_THRESHOLD = 10; static const CColor OVERLAY_COLOR_LONG_PATH(1, 1, 1, 1); static const CColor OVERLAY_COLOR_SHORT_PATH(1, 0, 0, 1); class CCmpUnitMotion : public ICmpUnitMotion { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeToMessageType(MT_Update_MotionFormation); componentManager.SubscribeToMessageType(MT_Update_MotionUnit); componentManager.SubscribeToMessageType(MT_PathResult); componentManager.SubscribeToMessageType(MT_OwnershipChanged); componentManager.SubscribeToMessageType(MT_ValueModification); componentManager.SubscribeToMessageType(MT_Deserialized); } DEFAULT_COMPONENT_ALLOCATOR(UnitMotion) bool m_DebugOverlayEnabled; std::vector m_DebugOverlayLongPathLines; std::vector m_DebugOverlayShortPathLines; // Template state: bool m_FormationController; fixed m_TemplateWalkSpeed, m_TemplateRunMultiplier; pass_class_t m_PassClass; std::string m_PassClassName; // Dynamic state: entity_pos_t m_Clearance; // cached for efficiency fixed m_WalkSpeed, m_RunMultiplier; bool m_FacePointAfterMove; // Number of turns since we last managed to move successfully. // See HandleObstructedMove() for more details. u8 m_FailedMovements = 0; // If > 0, PathingUpdateNeeded returns false always. // This exists because the goal may be unreachable to the short/long pathfinder. // In such cases, we would compute inacceptable paths and PathingUpdateNeeded would trigger every turn, // which would be quite bad for performance. // To avoid that, when we know the new path is imperfect, treat it as OK and follow it anyways. // When reaching the end, we'll go through HandleObstructedMove and reset regardless. // To still recompute now and then (the target may be moving), this is a countdown decremented on each frame. u8 m_FollowKnownImperfectPathCountdown = 0; struct Ticket { u32 m_Ticket = 0; // asynchronous request ID we're waiting for, or 0 if none enum Type { SHORT_PATH, LONG_PATH } m_Type = SHORT_PATH; // Pick some default value to avoid UB. void clear() { m_Ticket = 0; } } m_ExpectedPathTicket; struct MoveRequest { enum Type { NONE, POINT, ENTITY, OFFSET } m_Type = NONE; entity_id_t m_Entity = INVALID_ENTITY; CFixedVector2D m_Position; entity_pos_t m_MinRange, m_MaxRange; // For readability CFixedVector2D GetOffset() const { return m_Position; }; MoveRequest() = default; MoveRequest(CFixedVector2D pos, entity_pos_t minRange, entity_pos_t maxRange) : m_Type(POINT), m_Position(pos), m_MinRange(minRange), m_MaxRange(maxRange) {}; MoveRequest(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) : m_Type(ENTITY), m_Entity(target), m_MinRange(minRange), m_MaxRange(maxRange) {}; MoveRequest(entity_id_t target, CFixedVector2D offset) : m_Type(OFFSET), m_Entity(target), m_Position(offset) {}; } m_MoveRequest; // If the entity moves, it will do so at m_WalkSpeed * m_SpeedMultiplier. fixed m_SpeedMultiplier; // This caches the resulting speed from m_WalkSpeed * m_SpeedMultiplier for convenience. fixed m_Speed; // Current mean speed (over the last turn). fixed m_CurSpeed; // Currently active paths (storing waypoints in reverse order). // The last item in each path is the point we're currently heading towards. WaypointPath m_LongPath; WaypointPath m_ShortPath; static std::string GetSchema() { return "Provides the unit with the ability to move around the world by itself." "" "7.0" "default" "" "" "" "" "" "" "" "" "" "" "" "" "" "" ""; } virtual void Init(const CParamNode& paramNode) { m_FormationController = paramNode.GetChild("FormationController").ToBool(); m_FacePointAfterMove = true; m_WalkSpeed = m_TemplateWalkSpeed = m_Speed = paramNode.GetChild("WalkSpeed").ToFixed(); m_SpeedMultiplier = fixed::FromInt(1); m_CurSpeed = fixed::Zero(); m_RunMultiplier = m_TemplateRunMultiplier = fixed::FromInt(1); if (paramNode.GetChild("RunMultiplier").IsOk()) m_RunMultiplier = m_TemplateRunMultiplier = paramNode.GetChild("RunMultiplier").ToFixed(); CmpPtr cmpPathfinder(GetSystemEntity()); if (cmpPathfinder) { m_PassClassName = paramNode.GetChild("PassabilityClass").ToUTF8(); m_PassClass = cmpPathfinder->GetPassabilityClass(m_PassClassName); m_Clearance = cmpPathfinder->GetClearance(m_PassClass); CmpPtr cmpObstruction(GetEntityHandle()); if (cmpObstruction) cmpObstruction->SetUnitClearance(m_Clearance); } m_DebugOverlayEnabled = false; } virtual void Deinit() { } template void SerializeCommon(S& serialize) { serialize.StringASCII("pass class", m_PassClassName, 0, 64); serialize.NumberU32_Unbounded("ticket", m_ExpectedPathTicket.m_Ticket); Serializer(serialize, "ticket type", m_ExpectedPathTicket.m_Type, Ticket::Type::LONG_PATH); serialize.NumberU8_Unbounded("failed movements", m_FailedMovements); serialize.NumberU8_Unbounded("followknownimperfectpath", m_FollowKnownImperfectPathCountdown); Serializer(serialize, "target type", m_MoveRequest.m_Type, MoveRequest::Type::OFFSET); serialize.NumberU32_Unbounded("target entity", m_MoveRequest.m_Entity); serialize.NumberFixed_Unbounded("target pos x", m_MoveRequest.m_Position.X); serialize.NumberFixed_Unbounded("target pos y", m_MoveRequest.m_Position.Y); serialize.NumberFixed_Unbounded("target min range", m_MoveRequest.m_MinRange); serialize.NumberFixed_Unbounded("target max range", m_MoveRequest.m_MaxRange); serialize.NumberFixed_Unbounded("speed multiplier", m_SpeedMultiplier); serialize.NumberFixed_Unbounded("current speed", m_CurSpeed); serialize.Bool("facePointAfterMove", m_FacePointAfterMove); Serializer(serialize, "long path", m_LongPath.m_Waypoints); Serializer(serialize, "short path", m_ShortPath.m_Waypoints); } virtual void Serialize(ISerializer& serialize) { SerializeCommon(serialize); } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) { Init(paramNode); SerializeCommon(deserialize); CmpPtr cmpPathfinder(GetSystemEntity()); if (cmpPathfinder) m_PassClass = cmpPathfinder->GetPassabilityClass(m_PassClassName); } virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_Update_MotionFormation: { if (m_FormationController) { fixed dt = static_cast (msg).turnLength; Move(dt); } break; } case MT_Update_MotionUnit: { if (!m_FormationController) { fixed dt = static_cast (msg).turnLength; Move(dt); } break; } case MT_RenderSubmit: { PROFILE("UnitMotion::RenderSubmit"); const CMessageRenderSubmit& msgData = static_cast (msg); RenderSubmit(msgData.collector); break; } case MT_PathResult: { const CMessagePathResult& msgData = static_cast (msg); PathResult(msgData.ticket, msgData.path); break; } case MT_ValueModification: { const CMessageValueModification& msgData = static_cast (msg); if (msgData.component != L"UnitMotion") break; FALLTHROUGH; } case MT_OwnershipChanged: case MT_Deserialized: { CmpPtr cmpValueModificationManager(GetSystemEntity()); if (!cmpValueModificationManager) break; m_WalkSpeed = cmpValueModificationManager->ApplyModifications(L"UnitMotion/WalkSpeed", m_TemplateWalkSpeed, GetEntityId()); m_RunMultiplier = cmpValueModificationManager->ApplyModifications(L"UnitMotion/RunMultiplier", m_TemplateRunMultiplier, GetEntityId()); // For MT_Deserialize compute m_Speed from the serialized m_SpeedMultiplier. // For MT_ValueModification and MT_OwnershipChanged, adjust m_SpeedMultiplier if needed // (in case then new m_RunMultiplier value is lower than the old). SetSpeedMultiplier(m_SpeedMultiplier); break; } } } void UpdateMessageSubscriptions() { bool needRender = m_DebugOverlayEnabled; GetSimContext().GetComponentManager().DynamicSubscriptionNonsync(MT_RenderSubmit, this, needRender); } virtual bool IsMoveRequested() const { return m_MoveRequest.m_Type != MoveRequest::NONE; } virtual fixed GetSpeedMultiplier() const { return m_SpeedMultiplier; } virtual void SetSpeedMultiplier(fixed multiplier) { m_SpeedMultiplier = std::min(multiplier, m_RunMultiplier); m_Speed = m_SpeedMultiplier.Multiply(GetWalkSpeed()); } virtual fixed GetSpeed() const { return m_Speed; } virtual fixed GetWalkSpeed() const { return m_WalkSpeed; } virtual fixed GetRunMultiplier() const { return m_RunMultiplier; } virtual CFixedVector2D EstimateFuturePosition(const fixed dt) const { CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return CFixedVector2D(); // TODO: formation members should perhaps try to use the controller's position. CFixedVector2D pos = cmpPosition->GetPosition2D(); entity_angle_t angle = cmpPosition->GetRotation().Y; // Copy the path so we don't change it. WaypointPath shortPath = m_ShortPath; WaypointPath longPath = m_LongPath; PerformMove(dt, cmpPosition->GetTurnRate(), shortPath, longPath, pos, angle); return pos; } virtual pass_class_t GetPassabilityClass() const { return m_PassClass; } virtual std::string GetPassabilityClassName() const { return m_PassClassName; } virtual void SetPassabilityClassName(const std::string& passClassName) { m_PassClassName = passClassName; CmpPtr cmpPathfinder(GetSystemEntity()); if (cmpPathfinder) m_PassClass = cmpPathfinder->GetPassabilityClass(passClassName); } virtual fixed GetCurrentSpeed() const { return m_CurSpeed; } virtual void SetFacePointAfterMove(bool facePointAfterMove) { m_FacePointAfterMove = facePointAfterMove; } virtual bool GetFacePointAfterMove() const { return m_FacePointAfterMove; } virtual void SetDebugOverlay(bool enabled) { m_DebugOverlayEnabled = enabled; UpdateMessageSubscriptions(); } virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) { return MoveTo(MoveRequest(CFixedVector2D(x, z), minRange, maxRange)); } virtual bool MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) { return MoveTo(MoveRequest(target, minRange, maxRange)); } virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) { MoveTo(MoveRequest(target, CFixedVector2D(x, z))); } virtual bool IsTargetRangeReachable(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange); virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z); /** * Clears the current MoveRequest - the unit will stop and no longer try and move. * This should never be called from UnitMotion, since MoveToX orders are given * by other components - these components should also decide when to stop. */ virtual void StopMoving() { if (m_FacePointAfterMove) { CmpPtr cmpPosition(GetEntityHandle()); if (cmpPosition && cmpPosition->IsInWorld()) { CFixedVector2D targetPos; if (ComputeTargetPosition(targetPos)) FaceTowardsPointFromPos(cmpPosition->GetPosition2D(), targetPos.X, targetPos.Y); } } m_MoveRequest = MoveRequest(); m_ExpectedPathTicket.clear(); m_LongPath.m_Waypoints.clear(); m_ShortPath.m_Waypoints.clear(); } virtual entity_pos_t GetUnitClearance() const { return m_Clearance; } private: bool ShouldAvoidMovingUnits() const { return !m_FormationController; } bool IsFormationMember() const { // TODO: this really shouldn't be what we are checking for. return m_MoveRequest.m_Type == MoveRequest::OFFSET; } bool IsFormationControllerMoving() const { CmpPtr cmpControllerMotion(GetSimContext(), m_MoveRequest.m_Entity); return cmpControllerMotion && cmpControllerMotion->IsMoveRequested(); } entity_id_t GetGroup() const { return IsFormationMember() ? m_MoveRequest.m_Entity : GetEntityId(); } /** * Warns other components that our current movement will likely fail (e.g. we won't be able to reach our target) * This should only be called before the actual movement in a given turn, or units might both move and try to do things * on the same turn, leading to gliding units. */ void MoveFailed() { // Don't notify if we are a formation member in a moving formation - we can occasionally be stuck for a long time // if our current offset is unreachable, but we don't want to end up stuck. // (If the formation controller has stopped moving however, we can safely message). if (IsFormationMember() && IsFormationControllerMoving()) return; CMessageMotionUpdate msg(CMessageMotionUpdate::LIKELY_FAILURE); GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } /** * Warns other components that our current movement is likely over (i.e. we probably reached our destination) * This should only be called before the actual movement in a given turn, or units might both move and try to do things * on the same turn, leading to gliding units. */ void MoveSucceeded() { // Don't notify if we are a formation member in a moving formation - we can occasionally be stuck for a long time // if our current offset is unreachable, but we don't want to end up stuck. // (If the formation controller has stopped moving however, we can safely message). if (IsFormationMember() && IsFormationControllerMoving()) return; CMessageMotionUpdate msg(CMessageMotionUpdate::LIKELY_SUCCESS); GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } /** * Warns other components that our current movement was obstructed (i.e. we failed to move this turn). * This should only be called before the actual movement in a given turn, or units might both move and try to do things * on the same turn, leading to gliding units. */ void MoveObstructed() { // Don't notify if we are a formation member in a moving formation - we can occasionally be stuck for a long time // if our current offset is unreachable, but we don't want to end up stuck. // (If the formation controller has stopped moving however, we can safely message). if (IsFormationMember() && IsFormationControllerMoving()) return; CMessageMotionUpdate msg(m_FailedMovements >= VERY_OBSTRUCTED_THRESHOLD ? CMessageMotionUpdate::VERY_OBSTRUCTED : CMessageMotionUpdate::OBSTRUCTED); GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } /** * Increment the number of failed movements and notify other components if required. * @returns true if the failure was notified, false otherwise. */ bool IncrementFailedMovementsAndMaybeNotify() { m_FailedMovements++; if (m_FailedMovements >= MAX_FAILED_MOVEMENTS) { MoveFailed(); m_FailedMovements = 0; return true; } return false; } /** * If path would take us farther away from the goal than pos currently is, return false, else return true. */ bool RejectFartherPaths(const PathGoal& goal, const WaypointPath& path, const CFixedVector2D& pos) const; bool ShouldAlternatePathfinder() const { return (m_FailedMovements == ALTERNATE_PATH_TYPE_DELAY) || ((MAX_FAILED_MOVEMENTS - ALTERNATE_PATH_TYPE_DELAY) % ALTERNATE_PATH_TYPE_EVERY == 0); } bool InShortPathRange(const PathGoal& goal, const CFixedVector2D& pos) const { return goal.DistanceToPoint(pos) < LONG_PATH_MIN_DIST; } entity_pos_t ShortPathSearchRange() const { u8 multiple = m_FailedMovements < SHORT_PATH_SEARCH_RANGE_INCREASE_DELAY ? 0 : m_FailedMovements - SHORT_PATH_SEARCH_RANGE_INCREASE_DELAY; fixed searchRange = SHORT_PATH_MIN_SEARCH_RANGE + SHORT_PATH_SEARCH_RANGE_INCREMENT * multiple; if (searchRange > SHORT_PATH_MAX_SEARCH_RANGE) searchRange = SHORT_PATH_MAX_SEARCH_RANGE; return searchRange; } /** * Handle the result of an asynchronous path query. */ void PathResult(u32 ticket, const WaypointPath& path); /** * Do the per-turn movement and other updates. */ void Move(fixed dt); /** * Returns true if we are possibly at our destination. * Since the concept of being at destination is dependent on why the move was requested, * UnitMotion can only ever hint about this, hence the conditional tone. */ bool PossiblyAtDestination() const; /** * Process the move the unit will do this turn. * This does not send actually change the position. * @returns true if the move was obstructed. */ bool PerformMove(fixed dt, const fixed& turnRate, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos, entity_angle_t& angle) const; /** * Update other components on our speed. * (For performance, this should try to avoid sending messages). */ void UpdateMovementState(entity_pos_t speed); /** * React if our move was obstructed. * @param moved - true if the unit still managed to move. * @returns true if the obstruction required handling, false otherwise. */ bool HandleObstructedMove(bool moved); /** * Returns true if the target position is valid. False otherwise. * (this may indicate that the target is e.g. out of the world/dead). * NB: for code-writing convenience, if we have no target, this returns true. */ bool TargetHasValidPosition(const MoveRequest& moveRequest) const; bool TargetHasValidPosition() const { return TargetHasValidPosition(m_MoveRequest); } /** * Computes the current location of our target entity (plus offset). * Returns false if no target entity or no valid position. */ bool ComputeTargetPosition(CFixedVector2D& out, const MoveRequest& moveRequest) const; bool ComputeTargetPosition(CFixedVector2D& out) const { return ComputeTargetPosition(out, m_MoveRequest); } /** * Attempts to replace the current path with a straight line to the target, * if it's close enough and the route is not obstructed. */ bool TryGoingStraightToTarget(const CFixedVector2D& from); /** * Returns whether our we need to recompute a path to reach our target. */ bool PathingUpdateNeeded(const CFixedVector2D& from) const; /** * Rotate to face towards the target point, given the current pos */ void FaceTowardsPointFromPos(const CFixedVector2D& pos, entity_pos_t x, entity_pos_t z); /** * Returns an appropriate obstruction filter for use with path requests. */ ControlGroupMovementObstructionFilter GetObstructionFilter() const; /** * Decide whether to approximate the given range from a square target as a circle, * rather than as a square. */ bool ShouldTreatTargetAsCircle(entity_pos_t range, entity_pos_t circleRadius) const; /** * Create a PathGoal from a move request. * @returns true if the goal was successfully created. */ bool ComputeGoal(PathGoal& out, const MoveRequest& moveRequest) const; /** * Compute a path to the given goal from the given position. * Might go in a straight line immediately, or might start an asynchronous path request. */ void ComputePathToGoal(const CFixedVector2D& from, const PathGoal& goal); /** * Start an asynchronous long path query. */ void RequestLongPath(const CFixedVector2D& from, const PathGoal& goal); /** * Start an asynchronous short path query. + * @param extendRange - if true, extend the search range to at least the distance to the goal. */ - void RequestShortPath(const CFixedVector2D& from, const PathGoal& goal, bool avoidMovingUnits); + void RequestShortPath(const CFixedVector2D& from, const PathGoal& goal, bool extendRange); /** * General handler for MoveTo interface functions. */ bool MoveTo(MoveRequest request); /** * Convert a path into a renderable list of lines */ void RenderPath(const WaypointPath& path, std::vector& lines, CColor color); void RenderSubmit(SceneCollector& collector); }; REGISTER_COMPONENT_TYPE(UnitMotion) bool CCmpUnitMotion::RejectFartherPaths(const PathGoal& goal, const WaypointPath& path, const CFixedVector2D& pos) const { if (path.m_Waypoints.empty()) return false; // Reject the new path if it does not lead us closer to the target's position. if (goal.DistanceToPoint(pos) <= goal.DistanceToPoint(CFixedVector2D(path.m_Waypoints.front().x, path.m_Waypoints.front().z))) return true; return false; } void CCmpUnitMotion::PathResult(u32 ticket, const WaypointPath& path) { // Ignore obsolete path requests if (ticket != m_ExpectedPathTicket.m_Ticket || m_MoveRequest.m_Type == MoveRequest::NONE) return; Ticket::Type ticketType = m_ExpectedPathTicket.m_Type; m_ExpectedPathTicket.clear(); // If we not longer have a position, we won't be able to do much. // Fail in the next Move() call. CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return; CFixedVector2D pos = cmpPosition->GetPosition2D(); // Assume all long paths were towards the goal, and assume short paths were if there are no long waypoints. bool pathedTowardsGoal = ticketType == Ticket::LONG_PATH || m_LongPath.m_Waypoints.empty(); // Check if we need to run the short-path hack (warning: tricky control flow). bool shortPathHack = false; if (path.m_Waypoints.empty()) { // No waypoints means pathing failed. If this was a long-path, try the short-path hack. if (!pathedTowardsGoal) return; shortPathHack = ticketType == Ticket::LONG_PATH; } else if (PathGoal goal; pathedTowardsGoal && ComputeGoal(goal, m_MoveRequest) && RejectFartherPaths(goal, path, pos)) { // Reject paths that would take the unit further away from the goal. // This assumes that we prefer being closer 'as the crow flies' to unreachable goals. // This is a hack of sorts around units 'dancing' between two positions (see e.g. #3144), // but never actually failing to move, ergo never actually informing unitAI that it succeeds/fails. // (for short paths, only do so if aiming directly for the goal // as sub-goals may be farther than we are). // If this was a long-path and we no longer have waypoints, try the short-path hack. if (!m_LongPath.m_Waypoints.empty()) return; shortPathHack = ticketType == Ticket::LONG_PATH; } // Short-path hack: if the long-range pathfinder doesn't find an acceptable path, push a fake waypoint at the goal. // This means HandleObstructedMove will use the short-pathfinder to try and reach it, // and that may find a path as the vertex pathfinder is more precise. if (shortPathHack) { // If we're resorting to the short-path hack, the situation is dire. Most likely, the goal is unreachable. // We want to find a path or fail fast. Bump failed movements so the short pathfinder will run at max-range // right away. This is safe from a performance PoV because it can only happen if the target is unreachable to // the long-range pathfinder, which is rare, and since the entity will fail to move if the goal is actually unreachable, // the failed movements will be increased to MAX anyways, so just shortcut. m_FailedMovements = MAX_FAILED_MOVEMENTS - 2; CFixedVector2D targetPos; if (ComputeTargetPosition(targetPos)) m_LongPath.m_Waypoints.emplace_back(Waypoint{ targetPos.X, targetPos.Y }); return; } if (ticketType == Ticket::LONG_PATH) m_LongPath = path; else m_ShortPath = path; m_FollowKnownImperfectPathCountdown = 0; if (!pathedTowardsGoal) return; // Performance hack: If we were pathing towards the goal and this new path won't put us in range, // it's highly likely that we are going somewhere unreachable. // However, Move() will try to recompute the path every turn, which can be quite slow. // To avoid this, act as if our current path leads us to the correct destination. // NB: for short-paths, the problem might be that the search space is too small // but we'll still follow this path until the en and try again then. // Because we reject farther paths, it works out. if (PathingUpdateNeeded(pos)) { // Inform other components early, as they might have better behaviour than waiting for the path to carry out. // Send OBSTRUCTED at first - moveFailed is likely to trigger path recomputation and we might end up // recomputing too often for nothing. if (!IncrementFailedMovementsAndMaybeNotify()) MoveObstructed(); // We'll automatically recompute a path when this reaches 0, as a way to improve behaviour. // (See D665 - this is needed because the target may be moving, and we should adjust to that). m_FollowKnownImperfectPathCountdown = KNOWN_IMPERFECT_PATH_RESET_COUNTDOWN; } } void CCmpUnitMotion::Move(fixed dt) { PROFILE("Move"); // If we were idle and will still be, we can return. // TODO: this will need to be removed if pushing is implemented. if (m_CurSpeed == fixed::Zero() && m_MoveRequest.m_Type == MoveRequest::NONE) return; if (PossiblyAtDestination()) MoveSucceeded(); else if (!TargetHasValidPosition()) { // Scrap waypoints - we don't know where to go. // If the move request remains unchanged and the target again has a valid position later on, // moving will be resumed. // Units may want to move to move to the target's last known position, // but that should be decided by UnitAI (handling MoveFailed), not UnitMotion. m_LongPath.m_Waypoints.clear(); m_ShortPath.m_Waypoints.clear(); MoveFailed(); } CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return; CFixedVector2D initialPos = cmpPosition->GetPosition2D(); entity_angle_t initialAngle = cmpPosition->GetRotation().Y; // Keep track of the current unit's position and rotation during the update. CFixedVector2D pos = initialPos; entity_angle_t angle = initialAngle; // If we're chasing a potentially-moving unit and are currently close // enough to its current position, and we can head in a straight line // to it, then throw away our current path and go straight to it bool wentStraight = TryGoingStraightToTarget(initialPos); bool wasObstructed = PerformMove(dt, cmpPosition->GetTurnRate(), m_ShortPath, m_LongPath, pos, angle); // Update our speed over this turn so that the visual actor shows the correct animation. if (pos == initialPos) { if (angle != initialAngle) cmpPosition->TurnTo(angle); UpdateMovementState(fixed::Zero()); } else { // Update the Position component after our movement (if we actually moved anywhere) // When moving always set the angle in the direction of the movement. CFixedVector2D offset = pos - initialPos; angle = atan2_approx(offset.X, offset.Y); cmpPosition->MoveAndTurnTo(pos.X, pos.Y, angle); // Calculate the mean speed over this past turn. UpdateMovementState(offset.Length() / dt); } if (wasObstructed && HandleObstructedMove(pos != initialPos)) return; else if (!wasObstructed && pos != initialPos) m_FailedMovements = 0; // We may need to recompute our path sometimes (e.g. if our target moves). // Since we request paths asynchronously anyways, this does not need to be done before moving. if (!wentStraight && PathingUpdateNeeded(pos)) { PathGoal goal; if (ComputeGoal(goal, m_MoveRequest)) ComputePathToGoal(pos, goal); } else if (m_FollowKnownImperfectPathCountdown > 0) --m_FollowKnownImperfectPathCountdown; } bool CCmpUnitMotion::PossiblyAtDestination() const { if (m_MoveRequest.m_Type == MoveRequest::NONE) return false; CmpPtr cmpObstructionManager(GetSystemEntity()); ENSURE(cmpObstructionManager); if (m_MoveRequest.m_Type == MoveRequest::POINT) return cmpObstructionManager->IsInPointRange(GetEntityId(), m_MoveRequest.m_Position.X, m_MoveRequest.m_Position.Y, m_MoveRequest.m_MinRange, m_MoveRequest.m_MaxRange, false); if (m_MoveRequest.m_Type == MoveRequest::ENTITY) return cmpObstructionManager->IsInTargetRange(GetEntityId(), m_MoveRequest.m_Entity, m_MoveRequest.m_MinRange, m_MoveRequest.m_MaxRange, false); if (m_MoveRequest.m_Type == MoveRequest::OFFSET) { CmpPtr cmpControllerMotion(GetSimContext(), m_MoveRequest.m_Entity); if (cmpControllerMotion && cmpControllerMotion->IsMoveRequested()) return false; CFixedVector2D targetPos; ComputeTargetPosition(targetPos); CmpPtr cmpPosition(GetEntityHandle()); return cmpObstructionManager->IsInPointRange(GetEntityId(), targetPos.X, targetPos.Y, m_MoveRequest.m_MinRange, m_MoveRequest.m_MaxRange, false); } return false; } bool CCmpUnitMotion::PerformMove(fixed dt, const fixed& turnRate, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos, entity_angle_t& angle) const { // If there are no waypoint, behave as though we were obstructed and let HandleObstructedMove handle it. if (shortPath.m_Waypoints.empty() && longPath.m_Waypoints.empty()) return true; // Wrap the angle to (-Pi, Pi]. while (angle > entity_angle_t::Pi()) angle -= entity_angle_t::Pi() * 2; while (angle < -entity_angle_t::Pi()) angle += entity_angle_t::Pi() * 2; // TODO: there's some asymmetry here when units look at other // units' positions - the result will depend on the order of execution. // Maybe we should split the updates into multiple phases to minimise // that problem. CmpPtr cmpPathfinder(GetSystemEntity()); ENSURE(cmpPathfinder); fixed basicSpeed = m_Speed; // If in formation, run to keep up; otherwise just walk. if (IsFormationMember()) basicSpeed = m_Speed.Multiply(m_RunMultiplier); // Find the speed factor of the underlying terrain. // (We only care about the tile we start on - it doesn't matter if we're moving // partially onto a much slower/faster tile). // TODO: Terrain-dependent speeds are not currently supported. fixed terrainSpeed = fixed::FromInt(1); fixed maxSpeed = basicSpeed.Multiply(terrainSpeed); fixed timeLeft = dt; fixed zero = fixed::Zero(); while (timeLeft > zero) { // If we ran out of path, we have to stop. if (shortPath.m_Waypoints.empty() && longPath.m_Waypoints.empty()) break; CFixedVector2D target; if (shortPath.m_Waypoints.empty()) target = CFixedVector2D(longPath.m_Waypoints.back().x, longPath.m_Waypoints.back().z); else target = CFixedVector2D(shortPath.m_Waypoints.back().x, shortPath.m_Waypoints.back().z); CFixedVector2D offset = target - pos; if (turnRate > zero) { fixed maxRotation = turnRate.Multiply(timeLeft); fixed angleDiff = angle - atan2_approx(offset.X, offset.Y); if (angleDiff != zero) { fixed absoluteAngleDiff = angleDiff.Absolute(); if (absoluteAngleDiff > entity_angle_t::Pi()) absoluteAngleDiff = entity_angle_t::Pi() * 2 - absoluteAngleDiff; // Figure out whether rotating will increase or decrease the angle, and how far we need to rotate in that direction. int direction = (entity_angle_t::Zero() < angleDiff && angleDiff <= entity_angle_t::Pi()) || angleDiff < -entity_angle_t::Pi() ? -1 : 1; // Can't rotate far enough, just rotate in the correct direction. if (absoluteAngleDiff > maxRotation) { angle += maxRotation * direction; if (angle * direction > entity_angle_t::Pi()) angle -= entity_angle_t::Pi() * 2 * direction; break; } // Rotate towards the next waypoint and continue moving. angle = atan2_approx(offset.X, offset.Y); - timeLeft = (maxRotation - absoluteAngleDiff) / turnRate; + // Give some 'free' rotation for angles below 0.5 radians. + timeLeft = (std::min(maxRotation, maxRotation - absoluteAngleDiff + fixed::FromInt(1)/2)) / turnRate; } } // Work out how far we can travel in timeLeft. fixed maxdist = maxSpeed.Multiply(timeLeft); // If the target is close, we can move there directly. fixed offsetLength = offset.Length(); if (offsetLength <= maxdist) { if (cmpPathfinder->CheckMovement(GetObstructionFilter(), pos.X, pos.Y, target.X, target.Y, m_Clearance, m_PassClass)) { pos = target; // Spend the rest of the time heading towards the next waypoint. timeLeft = (maxdist - offsetLength) / maxSpeed; if (shortPath.m_Waypoints.empty()) longPath.m_Waypoints.pop_back(); else shortPath.m_Waypoints.pop_back(); continue; } else { // Error - path was obstructed. return true; } } else { // Not close enough, so just move in the right direction. offset.Normalize(maxdist); target = pos + offset; if (cmpPathfinder->CheckMovement(GetObstructionFilter(), pos.X, pos.Y, target.X, target.Y, m_Clearance, m_PassClass)) pos = target; else return true; break; } } return false; } void CCmpUnitMotion::UpdateMovementState(entity_pos_t speed) { CmpPtr cmpObstruction(GetEntityHandle()); CmpPtr cmpVisual(GetEntityHandle()); // Moved last turn, didn't this turn. if (speed == fixed::Zero() && m_CurSpeed > fixed::Zero()) { if (cmpObstruction) cmpObstruction->SetMovingFlag(false); if (cmpVisual) cmpVisual->SelectMovementAnimation("idle", fixed::FromInt(1)); } // Moved this turn, didn't last turn else if (speed > fixed::Zero() && m_CurSpeed == fixed::Zero()) { if (cmpObstruction) cmpObstruction->SetMovingFlag(true); if (cmpVisual) cmpVisual->SelectMovementAnimation(speed > (m_WalkSpeed / 2).Multiply(m_RunMultiplier + fixed::FromInt(1)) ? "run" : "walk", speed); } // Speed change, update the visual actor if necessary. else if (speed != m_CurSpeed && cmpVisual) cmpVisual->SelectMovementAnimation(speed > (m_WalkSpeed / 2).Multiply(m_RunMultiplier + fixed::FromInt(1)) ? "run" : "walk", speed); m_CurSpeed = speed; } bool CCmpUnitMotion::HandleObstructedMove(bool moved) { CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return false; // We failed to move, inform other components as they might handle it. // (don't send messages on the first failure, as that would be too noisy). // Also don't increment above the initial MoveObstructed message if we actually manage to move a little. if (!moved || m_FailedMovements < 2) { if (!IncrementFailedMovementsAndMaybeNotify() && m_FailedMovements >= 2) MoveObstructed(); } PathGoal goal; if (!ComputeGoal(goal, m_MoveRequest)) return false; // At this point we have a position in the world since ComputeGoal checked for that. CFixedVector2D pos = cmpPosition->GetPosition2D(); // Assume that we are merely obstructed and the long path is salvageable, so try going around the obstruction. // This could be a separate function, but it doesn't really make sense to call it outside of here, and I can't find a name. // I use an IIFE to have nice 'return' semantics still. if ([&]() -> bool { // If the goal is close enough, we should ignore any remaining long waypoint and just // short path there directly, as that improves behaviour in general - see D2095). if (InShortPathRange(goal, pos)) return false; // Delete the next waypoint if it's reasonably close, // because it might be blocked by units and thus unreachable. // NB: this number is tricky. Make it too high, and units start going down dead ends, which looks odd (#5795) // Make it too low, and they might get stuck behind other obstructed entities. // It also has performance implications because it calls the short-pathfinder. fixed skipbeyond = std::max(ShortPathSearchRange() / 3, fixed::FromInt(TERRAIN_TILE_SIZE*2)); if (m_LongPath.m_Waypoints.size() > 1 && (pos - CFixedVector2D(m_LongPath.m_Waypoints.back().x, m_LongPath.m_Waypoints.back().z)).CompareLength(skipbeyond) < 0) { m_LongPath.m_Waypoints.pop_back(); } else if (ShouldAlternatePathfinder()) { // Recompute the whole thing occasionally, in case we got stuck in a dead end from removing long waypoints. RequestLongPath(pos, goal); return true; } if (m_LongPath.m_Waypoints.empty()) return false; // Compute a short path in the general vicinity of the next waypoint, to help pathfinding in crowds. // The goal here is to manage to move in the general direction of our target, not to be super accurate. fixed radius = Clamp(skipbeyond/3, fixed::FromInt(TERRAIN_TILE_SIZE), fixed::FromInt(TERRAIN_TILE_SIZE*3)); PathGoal subgoal = { PathGoal::CIRCLE, m_LongPath.m_Waypoints.back().x, m_LongPath.m_Waypoints.back().z, radius }; - RequestShortPath(pos, subgoal, true); + RequestShortPath(pos, subgoal, false); return true; }()) return true; // If we couldn't use a workaround, try recomputing the entire path. ComputePathToGoal(pos, goal); return true; } bool CCmpUnitMotion::TargetHasValidPosition(const MoveRequest& moveRequest) const { if (moveRequest.m_Type != MoveRequest::ENTITY) return true; CmpPtr cmpPosition(GetSimContext(), moveRequest.m_Entity); return cmpPosition && cmpPosition->IsInWorld(); } bool CCmpUnitMotion::ComputeTargetPosition(CFixedVector2D& out, const MoveRequest& moveRequest) const { if (moveRequest.m_Type == MoveRequest::POINT) { out = moveRequest.m_Position; return true; } CmpPtr cmpTargetPosition(GetSimContext(), moveRequest.m_Entity); if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld()) return false; if (moveRequest.m_Type == MoveRequest::OFFSET) { // There is an offset, so compute it relative to orientation entity_angle_t angle = cmpTargetPosition->GetRotation().Y; CFixedVector2D offset = moveRequest.GetOffset().Rotate(angle); out = cmpTargetPosition->GetPosition2D() + offset; } else { out = cmpTargetPosition->GetPosition2D(); // If the target is moving, we might never get in range if we just try to reach its current position, // so we have to try and move to a position where we will be in-range, including their movement. // Since we request paths asynchronously a the end of our turn and the order in which two units move is uncertain, // we need to account for twice the movement speed to be sure that we're targeting the correct point. // TODO: be cleverer about this. It fixes fleeing nicely currently, but orthogonal movement should be considered, // and the overall logic could be improved upon. CmpPtr cmpUnitMotion(GetSimContext(), moveRequest.m_Entity); if (cmpUnitMotion && cmpUnitMotion->IsMoveRequested()) { CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return true; // Still return true since we don't need a position for the target to have one. CFixedVector2D tempPos = out + (out - cmpTargetPosition->GetPreviousPosition2D()) * 2; // Check if we anticipate the target to go through us, in which case we shouldn't anticipate // (or e.g. units fleeing might suddenly turn around towards their attacker). if ((out - cmpPosition->GetPosition2D()).RelativeOrientation(tempPos - cmpPosition->GetPosition2D()) >= 0) out = tempPos; } } return true; } bool CCmpUnitMotion::TryGoingStraightToTarget(const CFixedVector2D& from) { CFixedVector2D targetPos; if (!ComputeTargetPosition(targetPos)) return false; // Fail if the target is too far away if ((targetPos - from).CompareLength(DIRECT_PATH_RANGE) > 0) return false; CmpPtr cmpPathfinder(GetSystemEntity()); if (!cmpPathfinder) return false; // Move the goal to match the target entity's new position PathGoal goal; if (!ComputeGoal(goal, m_MoveRequest)) return false; goal.x = targetPos.X; goal.z = targetPos.Y; // (we ignore changes to the target's rotation, since only buildings are // square and buildings don't move) // Find the point on the goal shape that we should head towards CFixedVector2D goalPos = goal.NearestPointOnGoal(from); // Check if there's any collisions on that route. // For entity goals, skip only the specific obstruction tag or with e.g. walls we might ignore too many entities. ICmpObstructionManager::tag_t specificIgnore; if (m_MoveRequest.m_Type == MoveRequest::ENTITY) { CmpPtr cmpTargetObstruction(GetSimContext(), m_MoveRequest.m_Entity); if (cmpTargetObstruction) specificIgnore = cmpTargetObstruction->GetObstruction(); } if (specificIgnore.valid()) { if (!cmpPathfinder->CheckMovement(SkipTagObstructionFilter(specificIgnore), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass)) return false; } else if (!cmpPathfinder->CheckMovement(GetObstructionFilter(), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass)) return false; // That route is okay, so update our path m_LongPath.m_Waypoints.clear(); m_ShortPath.m_Waypoints.clear(); m_ShortPath.m_Waypoints.emplace_back(Waypoint{ goalPos.X, goalPos.Y }); return true; } bool CCmpUnitMotion::PathingUpdateNeeded(const CFixedVector2D& from) const { if (m_MoveRequest.m_Type == MoveRequest::NONE) return false; CFixedVector2D targetPos; if (!ComputeTargetPosition(targetPos)) return false; if (m_FollowKnownImperfectPathCountdown > 0) return false; if (PossiblyAtDestination()) return false; // Get the obstruction shape and translate it where we estimate the target to be. ICmpObstructionManager::ObstructionSquare estimatedTargetShape; if (m_MoveRequest.m_Type == MoveRequest::ENTITY) { CmpPtr cmpTargetObstruction(GetSimContext(), m_MoveRequest.m_Entity); if (cmpTargetObstruction) cmpTargetObstruction->GetObstructionSquare(estimatedTargetShape); } estimatedTargetShape.x = targetPos.X; estimatedTargetShape.z = targetPos.Y; CmpPtr cmpObstruction(GetEntityHandle()); ICmpObstructionManager::ObstructionSquare shape; if (cmpObstruction) cmpObstruction->GetObstructionSquare(shape); // Translate our own obstruction shape to our last waypoint or our current position, lacking that. if (m_LongPath.m_Waypoints.empty() && m_ShortPath.m_Waypoints.empty()) { shape.x = from.X; shape.z = from.Y; } else { const Waypoint& lastWaypoint = m_LongPath.m_Waypoints.empty() ? m_ShortPath.m_Waypoints.front() : m_LongPath.m_Waypoints.front(); shape.x = lastWaypoint.x; shape.z = lastWaypoint.z; } CmpPtr cmpObstructionManager(GetSystemEntity()); ENSURE(cmpObstructionManager); // Increase the ranges with distance, to avoid recomputing every turn against units that are moving and far-away for example. entity_pos_t distance = (from - CFixedVector2D(estimatedTargetShape.x, estimatedTargetShape.z)).Length(); // When in straight-path distance, we want perfect detection. distance = std::max(distance - DIRECT_PATH_RANGE, entity_pos_t::Zero()); // TODO: it could be worth computing this based on time to collision instead of linear distance. entity_pos_t minRange = std::max(m_MoveRequest.m_MinRange - distance / TARGET_UNCERTAINTY_MULTIPLIER, entity_pos_t::Zero()); entity_pos_t maxRange = m_MoveRequest.m_MaxRange < entity_pos_t::Zero() ? m_MoveRequest.m_MaxRange : m_MoveRequest.m_MaxRange + distance / TARGET_UNCERTAINTY_MULTIPLIER; if (cmpObstructionManager->AreShapesInRange(shape, estimatedTargetShape, minRange, maxRange, false)) return false; return true; } void CCmpUnitMotion::FaceTowardsPoint(entity_pos_t x, entity_pos_t z) { CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return; CFixedVector2D pos = cmpPosition->GetPosition2D(); FaceTowardsPointFromPos(pos, x, z); } void CCmpUnitMotion::FaceTowardsPointFromPos(const CFixedVector2D& pos, entity_pos_t x, entity_pos_t z) { CFixedVector2D target(x, z); CFixedVector2D offset = target - pos; if (!offset.IsZero()) { entity_angle_t angle = atan2_approx(offset.X, offset.Y); CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition) return; cmpPosition->TurnTo(angle); } } ControlGroupMovementObstructionFilter CCmpUnitMotion::GetObstructionFilter() const { return ControlGroupMovementObstructionFilter(ShouldAvoidMovingUnits(), GetGroup()); } // The pathfinder cannot go to "rounded rectangles" goals, which are what happens with square targets and a non-null range. // Depending on what the best approximation is, we either pretend the target is a circle or a square. // One needs to be careful that the approximated geometry will be in the range. bool CCmpUnitMotion::ShouldTreatTargetAsCircle(entity_pos_t range, entity_pos_t circleRadius) const { // Given a square, plus a target range we should reach, the shape at that distance // is a round-cornered square which we can approximate as either a circle or as a square. // Previously, we used the shape that minimized the worst-case error. // However that is unsage in some situations. So let's be less clever and // just check if our range is at least three times bigger than the circleradius return (range > circleRadius*3); } bool CCmpUnitMotion::ComputeGoal(PathGoal& out, const MoveRequest& moveRequest) const { if (moveRequest.m_Type == MoveRequest::NONE) return false; CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return false; CFixedVector2D pos = cmpPosition->GetPosition2D(); CFixedVector2D targetPosition; if (!ComputeTargetPosition(targetPosition, moveRequest)) return false; ICmpObstructionManager::ObstructionSquare targetObstruction; if (moveRequest.m_Type == MoveRequest::ENTITY) { CmpPtr cmpTargetObstruction(GetSimContext(), moveRequest.m_Entity); if (cmpTargetObstruction) cmpTargetObstruction->GetObstructionSquare(targetObstruction); } targetObstruction.x = targetPosition.X; targetObstruction.z = targetPosition.Y; ICmpObstructionManager::ObstructionSquare obstruction; CmpPtr cmpObstruction(GetEntityHandle()); if (cmpObstruction) cmpObstruction->GetObstructionSquare(obstruction); else { obstruction.x = pos.X; obstruction.z = pos.Y; } CmpPtr cmpObstructionManager(GetSystemEntity()); ENSURE(cmpObstructionManager); entity_pos_t distance = cmpObstructionManager->DistanceBetweenShapes(obstruction, targetObstruction); out.x = targetObstruction.x; out.z = targetObstruction.z; out.hw = targetObstruction.hw; out.hh = targetObstruction.hh; out.u = targetObstruction.u; out.v = targetObstruction.v; if (moveRequest.m_MinRange > fixed::Zero() || moveRequest.m_MaxRange > fixed::Zero() || targetObstruction.hw > fixed::Zero()) out.type = PathGoal::SQUARE; else { out.type = PathGoal::POINT; return true; } entity_pos_t circleRadius = CFixedVector2D(targetObstruction.hw, targetObstruction.hh).Length(); // TODO: because we cannot move to rounded rectangles, we have to make conservative approximations. // This means we might end up in a situation where cons(max-range) < min range < max range < cons(min-range) // When going outside of the min-range or inside the max-range, the unit will still go through the correct range // but if it moves fast enough, this might not be picked up by PossiblyAtDestination(). // Fixing this involves moving to rounded rectangles, or checking more often in PerformMove(). // In the meantime, one should avoid that 'Speed over a turn' > MaxRange - MinRange, in case where // min-range is not 0 and max-range is not infinity. if (distance < moveRequest.m_MinRange) { // Distance checks are nearest edge to nearest edge, so we need to account for our clearance // and we must make sure diagonals also fit so multiply by slightly more than sqrt(2) entity_pos_t goalDistance = moveRequest.m_MinRange + m_Clearance * 3 / 2; if (ShouldTreatTargetAsCircle(moveRequest.m_MinRange, circleRadius)) { // We are safely away from the obstruction itself if we are away from the circumscribing circle out.type = PathGoal::INVERTED_CIRCLE; out.hw = circleRadius + goalDistance; } else { out.type = PathGoal::INVERTED_SQUARE; out.hw = targetObstruction.hw + goalDistance; out.hh = targetObstruction.hh + goalDistance; } } else if (moveRequest.m_MaxRange >= fixed::Zero() && distance > moveRequest.m_MaxRange) { if (ShouldTreatTargetAsCircle(moveRequest.m_MaxRange, circleRadius)) { entity_pos_t goalDistance = moveRequest.m_MaxRange; // We must go in-range of the inscribed circle, not the circumscribing circle. circleRadius = std::min(targetObstruction.hw, targetObstruction.hh); out.type = PathGoal::CIRCLE; out.hw = circleRadius + goalDistance; } else { // The target is large relative to our range, so treat it as a square and // get close enough that the diagonals come within range entity_pos_t goalDistance = moveRequest.m_MaxRange * 2 / 3; // multiply by slightly less than 1/sqrt(2) out.type = PathGoal::SQUARE; entity_pos_t delta = std::max(goalDistance, m_Clearance + entity_pos_t::FromInt(TERRAIN_TILE_SIZE)/16); // ensure it's far enough to not intersect the building itself out.hw = targetObstruction.hw + delta; out.hh = targetObstruction.hh + delta; } } // Do nothing in particular in case we are already in range. return true; } void CCmpUnitMotion::ComputePathToGoal(const CFixedVector2D& from, const PathGoal& goal) { #if DISABLE_PATHFINDER { CmpPtr cmpPathfinder (GetSimContext(), SYSTEM_ENTITY); CFixedVector2D goalPos = m_FinalGoal.NearestPointOnGoal(from); m_LongPath.m_Waypoints.clear(); m_ShortPath.m_Waypoints.clear(); m_ShortPath.m_Waypoints.emplace_back(Waypoint{ goalPos.X, goalPos.Y }); return; } #endif // If the target is close and we can reach it in a straight line, // then we'll just go along the straight line instead of computing a path. if (!ShouldAlternatePathfinder() && TryGoingStraightToTarget(from)) return; // Otherwise we need to compute a path. // If it's close then just do a short path, not a long path // TODO: If it's close on the opposite side of a river then we really // need a long path, so we shouldn't simply check linear distance // the check is arbitrary but should be a reasonably small distance. // We want to occasionally compute a long path if we're computing short-paths, because the short path domain // is bounded and thus it can't around very large static obstacles. // Likewise, we want to compile a short-path occasionally when the target is far because we might be stuck // on a navcell surrounded by impassable navcells, but the short-pathfinder could move us out of there. bool shortPath = InShortPathRange(goal, from); if (ShouldAlternatePathfinder()) shortPath = !shortPath; if (shortPath) { m_LongPath.m_Waypoints.clear(); + // Extend the range so that our first path is probably valid. RequestShortPath(from, goal, true); } else { m_ShortPath.m_Waypoints.clear(); RequestLongPath(from, goal); } } void CCmpUnitMotion::RequestLongPath(const CFixedVector2D& from, const PathGoal& goal) { CmpPtr cmpPathfinder(GetSystemEntity()); if (!cmpPathfinder) return; // this is by how much our waypoints will be apart at most. // this value here seems sensible enough. PathGoal improvedGoal = goal; improvedGoal.maxdist = SHORT_PATH_MIN_SEARCH_RANGE - entity_pos_t::FromInt(1); cmpPathfinder->SetDebugPath(from.X, from.Y, improvedGoal, m_PassClass); m_ExpectedPathTicket.m_Type = Ticket::LONG_PATH; m_ExpectedPathTicket.m_Ticket = cmpPathfinder->ComputePathAsync(from.X, from.Y, improvedGoal, m_PassClass, GetEntityId()); } -void CCmpUnitMotion::RequestShortPath(const CFixedVector2D &from, const PathGoal& goal, bool avoidMovingUnits) +void CCmpUnitMotion::RequestShortPath(const CFixedVector2D &from, const PathGoal& goal, bool extendRange) { CmpPtr cmpPathfinder(GetSystemEntity()); if (!cmpPathfinder) return; entity_pos_t searchRange = ShortPathSearchRange(); + if (extendRange) + { + CFixedVector2D dist(from.X - goal.x, from.Y - goal.z); + if (dist.CompareLength(searchRange - entity_pos_t::FromInt(1)) >= 0) + searchRange = dist.Length() + fixed::FromInt(1); + } m_ExpectedPathTicket.m_Type = Ticket::SHORT_PATH; - m_ExpectedPathTicket.m_Ticket = cmpPathfinder->ComputeShortPathAsync(from.X, from.Y, m_Clearance, searchRange, goal, m_PassClass, avoidMovingUnits, GetGroup(), GetEntityId()); + m_ExpectedPathTicket.m_Ticket = cmpPathfinder->ComputeShortPathAsync(from.X, from.Y, m_Clearance, searchRange, goal, m_PassClass, true, GetGroup(), GetEntityId()); } bool CCmpUnitMotion::MoveTo(MoveRequest request) { PROFILE("MoveTo"); if (request.m_MinRange == request.m_MaxRange && !request.m_MinRange.IsZero()) LOGWARNING("MaxRange must be larger than MinRange; See CCmpUnitMotion.cpp for more information"); CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return false; PathGoal goal; if (!ComputeGoal(goal, request)) return false; m_MoveRequest = request; m_FailedMovements = 0; m_FollowKnownImperfectPathCountdown = 0; ComputePathToGoal(cmpPosition->GetPosition2D(), goal); return true; } bool CCmpUnitMotion::IsTargetRangeReachable(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) { CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return false; MoveRequest request(target, minRange, maxRange); PathGoal goal; if (!ComputeGoal(goal, request)) return false; CmpPtr cmpPathfinder(GetSimContext(), SYSTEM_ENTITY); CFixedVector2D pos = cmpPosition->GetPosition2D(); return cmpPathfinder->IsGoalReachable(pos.X, pos.Y, goal, m_PassClass); } void CCmpUnitMotion::RenderPath(const WaypointPath& path, std::vector& lines, CColor color) { bool floating = false; CmpPtr cmpPosition(GetEntityHandle()); if (cmpPosition) floating = cmpPosition->CanFloat(); lines.clear(); std::vector waypointCoords; for (size_t i = 0; i < path.m_Waypoints.size(); ++i) { float x = path.m_Waypoints[i].x.ToFloat(); float z = path.m_Waypoints[i].z.ToFloat(); waypointCoords.push_back(x); waypointCoords.push_back(z); lines.push_back(SOverlayLine()); lines.back().m_Color = color; SimRender::ConstructSquareOnGround(GetSimContext(), x, z, 1.0f, 1.0f, 0.0f, lines.back(), floating); } float x = cmpPosition->GetPosition2D().X.ToFloat(); float z = cmpPosition->GetPosition2D().Y.ToFloat(); waypointCoords.push_back(x); waypointCoords.push_back(z); lines.push_back(SOverlayLine()); lines.back().m_Color = color; SimRender::ConstructLineOnGround(GetSimContext(), waypointCoords, lines.back(), floating); } void CCmpUnitMotion::RenderSubmit(SceneCollector& collector) { if (!m_DebugOverlayEnabled) return; RenderPath(m_LongPath, m_DebugOverlayLongPathLines, OVERLAY_COLOR_LONG_PATH); RenderPath(m_ShortPath, m_DebugOverlayShortPathLines, OVERLAY_COLOR_SHORT_PATH); for (size_t i = 0; i < m_DebugOverlayLongPathLines.size(); ++i) collector.Submit(&m_DebugOverlayLongPathLines[i]); for (size_t i = 0; i < m_DebugOverlayShortPathLines.size(); ++i) collector.Submit(&m_DebugOverlayShortPathLines[i]); }