From c87229aa48406aedd9447833332963e8464f68e6 Mon Sep 17 00:00:00 2001 From: wraitii Date: Mon, 22 Mar 2021 13:27:33 +0000 Subject: [PATCH] Optimise FindWalkAndFightTargets. FindWalkAndFightTargets is used during attack-walk (and a few other situations) to find new entities to attack. This function can be a bit slow, taking large chunks of time during battles. This optimises it by assuming that one of the surrounding unit will match preferred criteria (which, for most soldiers, are 'Human'), thus returning the first attackable entity. In the worst case, it should still be slightly faster than the current code. Differential Revision: https://code.wildfiregames.com/D3446 This was SVN commit r25102. --- .../public/simulation/components/Attack.js | 6 +- .../public/simulation/components/UnitAI.js | 130 ++++++++---------- .../components/tests/test_Attack.js | 4 +- .../components/tests/test_UnitAI.js | 74 ++++++++++ 4 files changed, 140 insertions(+), 74 deletions(-) diff --git a/binaries/data/mods/public/simulation/components/Attack.js b/binaries/data/mods/public/simulation/components/Attack.js index 5de5820fbc..a5fd11a777 100644 --- a/binaries/data/mods/public/simulation/components/Attack.js +++ b/binaries/data/mods/public/simulation/components/Attack.js @@ -334,7 +334,7 @@ Attack.prototype.CanAttack = function(target, wantedTypes) }; /** - * Returns null if we have no preference or the lowest index of a preferred class. + * Returns undefined if we have no preference or the lowest index of a preferred class. */ Attack.prototype.GetPreference = function(target) { @@ -344,7 +344,7 @@ Attack.prototype.GetPreference = function(target) let targetClasses = cmpIdentity.GetClassesList(); - let minPref = null; + let minPref; for (let type of this.GetAttackTypes()) { let preferredClasses = this.GetPreferredClasses(type); @@ -354,7 +354,7 @@ Attack.prototype.GetPreference = function(target) { if (pref === 0) return pref; - if ((minPref === null || minPref > pref)) + if ((minPref === undefined || minPref > pref)) minPref = pref; } } diff --git a/binaries/data/mods/public/simulation/components/UnitAI.js b/binaries/data/mods/public/simulation/components/UnitAI.js index 80bfbd2e69..4aff58d285 100644 --- a/binaries/data/mods/public/simulation/components/UnitAI.js +++ b/binaries/data/mods/public/simulation/components/UnitAI.js @@ -996,7 +996,9 @@ UnitAI.prototype.UnitFsmSpec = { }, "Timer": function(msg) { + Engine.ProfileStart("FindWalkAndFightTargets"); this.FindWalkAndFightTargets(); + Engine.ProfileStop(); }, "MovementUpdate": function(msg) { @@ -2221,7 +2223,11 @@ UnitAI.prototype.UnitFsmSpec = { if (this.FinishOrder()) { if (this.IsWalkingAndFighting()) + { + Engine.ProfileStart("FindWalkAndFightTargets"); this.FindWalkAndFightTargets(); + Engine.ProfileStop(); + } return true; } @@ -6056,58 +6062,77 @@ UnitAI.prototype.FindWalkAndFightTargets = function() { if (this.IsFormationController()) { - var cmpUnitAI; - var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); - for (var ent of cmpFormation.members) + let cmpUnitAI; + let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); + for (let ent of cmpFormation.members) { if (!(cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI))) continue; - var targets = cmpUnitAI.GetTargetsFromUnit(); - for (var targ of targets) - { - if (!cmpUnitAI.CanAttack(targ)) - continue; - if (this.order.data.targetClasses) - { - var cmpIdentity = Engine.QueryInterface(targ, IID_Identity); - var targetClasses = this.order.data.targetClasses; - if (targetClasses.attack && cmpIdentity && - !MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.attack)) - continue; - if (targetClasses.avoid && cmpIdentity && - MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.avoid)) - continue; - // Only used by the AIs to prevent some choices of targets - if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ]) - continue; - } - this.PushOrderFront("Attack", { "target": targ, "force": false, "allowCapture": this.order.data.allowCapture }); + if (cmpUnitAI.FindWalkAndFightTargets()) return true; - } } return false; } - var targets = this.GetTargetsFromUnit(); - for (var targ of targets) + let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); + + let entities; + if (!this.losAttackRangeQuery || !this.GetStance().targetVisibleEnemies || !cmpAttack) + entities = []; + else { - if (!this.CanAttack(targ)) - continue; - if (this.order.data.targetClasses) + let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + entities = cmpRangeManager.ResetActiveQuery(this.losAttackRangeQuery); + } + + let attackfilter = e => { + if (this?.order?.data?.targetClasses) { - var cmpIdentity = Engine.QueryInterface(targ, IID_Identity); - var targetClasses = this.order.data.targetClasses; + let cmpIdentity = Engine.QueryInterface(e, IID_Identity); + let targetClasses = this.order.data.targetClasses; if (cmpIdentity && targetClasses.attack && !MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.attack)) - continue; + return false; if (cmpIdentity && targetClasses.avoid && MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.avoid)) - continue; + return false; // Only used by the AIs to prevent some choices of targets - if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ]) - continue; + if (targetClasses.vetoEntities && targetClasses.vetoEntities[e]) + return false; } - this.PushOrderFront("Attack", { "target": targ, "force": false, "allowCapture": this.order.data.allowCapture }); + let cmpOwnership = Engine.QueryInterface(e, IID_Ownership); + if (cmpOwnership && cmpOwnership.GetOwner() > 0) + return true; + let cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI); + return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal()); + }; + + let prefs = {}; + let bestPref; + let targets = []; + let pref; + for (let v of entities) + { + if (this.CanAttack(v) && attackfilter(v)) + { + pref = cmpAttack.GetPreference(v); + if (pref === 0) + { + this.PushOrderFront("Attack", { "target": v, "force": false, "allowCapture": this?.order?.data?.allowCapture }); + return true; + } + targets.push(v); + } + prefs[v] = pref; + if (pref !== undefined && (bestPref === undefined || pref < bestPref)) + bestPref = pref; + } + + for (let targ of targets) + { + if (prefs[targ] !== bestPref) + continue; + this.PushOrderFront("Attack", { "target": targ, "force": false, "allowCapture": this?.order?.data?.allowCapture }); return true; } @@ -6118,39 +6143,6 @@ UnitAI.prototype.FindWalkAndFightTargets = function() return false; }; -UnitAI.prototype.GetTargetsFromUnit = function() -{ - if (!this.losAttackRangeQuery) - return []; - - if (!this.GetStance().targetVisibleEnemies) - return []; - - let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); - if (!cmpAttack) - return []; - - let attackfilter = function(e) { - if (!cmpAttack.CanAttack(e)) - return false; - - let cmpOwnership = Engine.QueryInterface(e, IID_Ownership); - if (cmpOwnership && cmpOwnership.GetOwner() > 0) - return true; - - let cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI); - return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal()); - }; - - let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); - let entities = cmpRangeManager.ResetActiveQuery(this.losAttackRangeQuery); - let targets = entities.filter(attackfilter).sort(function(a, b) { - return cmpAttack.CompareEntitiesByPreference(a, b); - }); - - return targets; -}; - UnitAI.prototype.GetQueryRange = function(iid) { let ret = { "min": 0, "max": 0 }; diff --git a/binaries/data/mods/public/simulation/components/tests/test_Attack.js b/binaries/data/mods/public/simulation/components/tests/test_Attack.js index ef86bd5e46..a2c30878e4 100644 --- a/binaries/data/mods/public/simulation/components/tests/test_Attack.js +++ b/binaries/data/mods/public/simulation/components/tests/test_Attack.js @@ -397,7 +397,7 @@ function testAttackPreference() TS_ASSERT_EQUALS(cmpAttack.GetPreference(attacker+1), 0); TS_ASSERT_EQUALS(cmpAttack.GetPreference(attacker+2), 1); - TS_ASSERT_EQUALS(cmpAttack.GetPreference(attacker+3), null); - TS_ASSERT_EQUALS(cmpAttack.GetPreference(attacker+4), null); + TS_ASSERT_EQUALS(cmpAttack.GetPreference(attacker+3), undefined); + TS_ASSERT_EQUALS(cmpAttack.GetPreference(attacker+4), undefined); } testAttackPreference(); diff --git a/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js b/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js index fb026b4b6e..dffb0e4723 100644 --- a/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js +++ b/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js @@ -449,3 +449,77 @@ TestFormationExiting(1); TestFormationExiting(2); TestMoveIntoFormationWhileAttacking(); + + +function TestWalkAndFightTargets() +{ + const ent = 10; + let unitAI = ConstructComponent(ent, "UnitAI", { + "FormationController": "false", + "DefaultStance": "aggressive", + "FleeDistance": 10 + }); + unitAI.OnCreate(); + unitAI.losAttackRangeQuery = true; + + // The result is stored here + let result; + unitAI.PushOrderFront = function(type, order) + { + if (type === "Attack" && order?.target) + result = order.target; + }; + + // Create some targets. + AddMock(ent+1, IID_UnitAI, { "IsAnimal": () => true, "IsDangerousAnimal": () => false }); + AddMock(ent+2, IID_Ownership, { "GetOwner": () => 2 }); + AddMock(ent+3, IID_Ownership, { "GetOwner": () => 2 }); + AddMock(ent+4, IID_Ownership, { "GetOwner": () => 2 }); + AddMock(ent+5, IID_Ownership, { "GetOwner": () => 2 }); + AddMock(ent+6, IID_Ownership, { "GetOwner": () => 2 }); + AddMock(ent+7, IID_Ownership, { "GetOwner": () => 2 }); + + unitAI.CanAttack = function(target) + { + return target !== ent+2 && target !== ent+7; + }; + + AddMock(ent, IID_Attack, { + "GetPreference": (target) => ({ + [ent+4]: 0, + [ent+5]: 1, + [ent+6]: 2, + [ent+7]: 0 + }?.[target]) + }); + + let runTest = function(ents, res) + { + result = undefined; + AddMock(SYSTEM_ENTITY, IID_RangeManager, { + "ResetActiveQuery": () => ents + }); + TS_ASSERT_EQUALS(unitAI.FindWalkAndFightTargets(), !!res); + TS_ASSERT_EQUALS(result, res); + }; + + // No entities. + runTest([]); + + // Entities that cannot be attacked. + runTest([ent+1, ent+2, ent+7]); + + // No preference, one attackable entity. + runTest([ent+1, ent+2, ent+3], ent+3); + + // Check preferences. + runTest([ent+1, ent+2, ent+3, ent+4], ent+4); + runTest([ent+1, ent+2, ent+3, ent+4, ent+5], ent+4); + runTest([ent+1, ent+2, ent+6, ent+3, ent+4, ent+5], ent+4); + runTest([ent+1, ent+2, ent+7, ent+6, ent+3, ent+4, ent+5], ent+4); + runTest([ent+1, ent+2, ent+7, ent+6, ent+3, ent+5], ent+5); + runTest([ent+1, ent+2, ent+7, ent+6, ent+3], ent+6); + runTest([ent+1, ent+2, ent+7, ent+3], ent+3); +} + +TestWalkAndFightTargets();