1
0
forked from 0ad/0ad

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.
This commit is contained in:
wraitii 2021-03-22 13:27:33 +00:00
parent 72f0fdb41b
commit c87229aa48
4 changed files with 140 additions and 74 deletions

View File

@ -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;
}
}

View File

@ -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 };

View File

@ -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();

View File

@ -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();