1
0
forked from 0ad/0ad

Use UnitMotion to predict target position in Attack.js to prevent 'dancing'

Attack.js can use UnitMotion to calculate the position of the unit in
the future, accounting for odd movements such
as zigzags, turnarouds and early stops (to the extent of the current
order).
This improves the resilience of units against the 'dancing' trick.

The linear interpolation is kept as a failsafe and to avoid an edge case
in the new prediction code.

Patch by: bb
Refs #5106

Differential Revision: https://code.wildfiregames.com/D3225
This was SVN commit r24701.
This commit is contained in:
wraitii 2021-01-19 15:59:03 +00:00
parent 59b468a0df
commit f737831167
7 changed files with 142 additions and 21 deletions

View File

@ -39,6 +39,19 @@ var WalkTo = function(x, z, queued, ent, owner=1)
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)
{
@ -82,7 +95,7 @@ var manual_dance = function(attacker, target, dance_distance, att_distance = 50,
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];
return [[dancer], attackers];
};
};
@ -102,7 +115,7 @@ var manual_square_dance = function(attacker, target, dance_distance, att_distanc
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];
return [[dancer], attackers];
};
};
@ -130,7 +143,7 @@ var manual_zigzag_dance = function(attacker, target, dance_distance, att_distanc
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];
return [[dancer], attackers];
};
};
@ -144,10 +157,29 @@ var patrol_dance = function(attacker, target, dance_distance, att_distance = 50,
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];
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)];
};
};
experiments.unit_manual_dance_archer = {
"spawn": manual_dance(ARCHER_TEMPLATE, REG_UNIT_TEMPLATE, 5)
};
@ -232,6 +264,22 @@ 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)
};
var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
Trigger.prototype.SetupUnits = function()
@ -242,16 +290,15 @@ Trigger.prototype.SetupUnits = function()
gy = 100;
for (let key in experiments)
{
let [dancer, attackers] = experiments[key].spawn();
let [dancers, attackers] = experiments[key].spawn();
let ReportResults = (killed) => {
warn((killed ? "Success " : "Failure ") + "(" + (Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() - start) +
"): Experiment " + key + " finished: target was " + (killed ? "killed" : "not killed"));
if (!killed)
ProcessCommand(1, {
"type": "delete-entities",
"entities": [dancer],
"controlAllUnits": true
});
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,
@ -259,7 +306,7 @@ Trigger.prototype.SetupUnits = function()
});
};
// xxtreme hack: hook into UnitAI
let uai = Engine.QueryInterface(dancer, IID_UnitAI);
let uai = Engine.QueryInterface(dancers[0], IID_UnitAI);
let odes = uai.OnDestroy;
uai.OnDestroy = () => ReportResults(true) && odes();
uai.FindNewTargets = () => {
@ -268,7 +315,7 @@ Trigger.prototype.SetupUnits = function()
};
gx += 70;
if (gx >= 450)
if (gx >= 520)
{
gx = 100;
gy += 70;

View File

@ -515,6 +515,9 @@ Attack.prototype.PerformAttack = function(type, target)
let gravity = +this.template[type].Projectile.Gravity;
// horizSpeed /= 2; gravity /= 2; // slow it down for testing
// We will try to estimate the position of the target, where we can hit it.
// We first estimate the time-till-hit by extrapolating linearly the movement
// of the last turn. We compute the time till an arrow will intersect the target.
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
@ -524,11 +527,36 @@ Attack.prototype.PerformAttack = function(type, target)
return;
let targetPosition = cmpTargetPosition.GetPosition();
let previousTargetPosition = Engine.QueryInterface(target, IID_Position).GetPreviousPosition();
let targetVelocity = Vector3D.sub(targetPosition, previousTargetPosition).div(turnLength);
let targetVelocity = Vector3D.sub(targetPosition, cmpTargetPosition.GetPreviousPosition()).div(turnLength);
let timeToTarget = PositionHelper.PredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity);
let predictedPosition = (timeToTarget !== false) ? Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition) : targetPosition;
// 'Cheat' and use UnitMotion to predict the position in the near-future.
// This avoids 'dancing' issues with units zigzagging over very short distances.
// However, this could fail if the player gives several short move orders, so
// occasionally fall back to basic interpolation.
let predictedPosition = targetPosition;
if (timeToTarget !== false)
{
// Don't predict too far in the future, but avoid threshold effects.
// After 1 second, always use the 'dumb' interpolated past-motion prediction.
let useUnitMotion = randBool(Math.max(0, 0.75 - timeToTarget / 1.333));
if (useUnitMotion)
{
let cmpTargetUnitMotion = Engine.QueryInterface(target, IID_UnitMotion);
let cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI);
if (cmpTargetUnitMotion && (!cmpTargetUnitAI || !cmpTargetUnitAI.IsFormationMember()))
{
let pos2D = cmpTargetUnitMotion.EstimateFuturePosition(timeToTarget);
predictedPosition.x = pos2D.x;
predictedPosition.z = pos2D.y;
}
else
predictedPosition = Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition);
}
else
predictedPosition = Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition);
}
// Add inaccuracy based on spread.
let distanceModifiedSpread = ApplyValueModificationsToEntity("Attack/Ranged/Spread", +this.template[type].Projectile.Spread, this.entity) *

View File

@ -311,6 +311,20 @@ UnitMotionFlying.prototype.GetRunMultiplier = function()
return 1;
};
/**
* Estimate the next position of the unit. Just linearly extrapolate.
* TODO: Reuse the movement code for a better estimate.
*/
UnitMotionFlying.prototype.EstimateNextPosition = function(dt)
{
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return Vector2D();
let position = cmpPosition.GetPosition2D();
return Vector2D.add(position, Vector2D.sub(position, cmpPosition.GetPreviousPosition2D()).mult(dt/Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetLatestTurnLength()));
};
UnitMotionFlying.prototype.IsMoveRequested = function()
{
return this.hasTarget;

View File

@ -22,6 +22,7 @@ Engine.LoadComponentScript("interfaces/Promotion.js");
Engine.LoadComponentScript("interfaces/ModifiersManager.js");
Engine.LoadComponentScript("interfaces/Resistance.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("interfaces/UnitAI.js");
Engine.LoadComponentScript("Attack.js");
Engine.LoadComponentScript("DelayedDamage.js");
Engine.LoadComponentScript("Timer.js");

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2020 Wildfire Games.
/* 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
@ -406,6 +406,25 @@ public:
return m_RunMultiplier;
}
virtual CFixedVector2D EstimateFuturePosition(const fixed dt) const
{
CmpPtr<ICmpPosition> 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;

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2020 Wildfire Games.
/* 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
@ -34,6 +34,7 @@ DEFINE_INTERFACE_METHOD_CONST_0("IsMoveRequested", bool, ICmpUnitMotion, IsMoveR
DEFINE_INTERFACE_METHOD_CONST_0("GetSpeed", fixed, ICmpUnitMotion, GetSpeed)
DEFINE_INTERFACE_METHOD_CONST_0("GetWalkSpeed", fixed, ICmpUnitMotion, GetWalkSpeed)
DEFINE_INTERFACE_METHOD_CONST_0("GetRunMultiplier", fixed, ICmpUnitMotion, GetRunMultiplier)
DEFINE_INTERFACE_METHOD_CONST_1("EstimateFuturePosition", CFixedVector2D, ICmpUnitMotion, EstimateFuturePosition, fixed)
DEFINE_INTERFACE_METHOD_1("SetSpeedMultiplier", void, ICmpUnitMotion, SetSpeedMultiplier, fixed)
DEFINE_INTERFACE_METHOD_CONST_0("GetPassabilityClassName", std::string, ICmpUnitMotion, GetPassabilityClassName)
DEFINE_INTERFACE_METHOD_CONST_0("GetUnitClearance", entity_pos_t, ICmpUnitMotion, GetUnitClearance)
@ -112,6 +113,11 @@ public:
return m_Script.Call<fixed>("GetSpeedMultiplier");
}
virtual CFixedVector2D EstimateFuturePosition(fixed dt) const
{
return m_Script.Call<CFixedVector2D>("EstimateFuturePosition", dt);
}
virtual void SetFacePointAfterMove(bool facePointAfterMove)
{
m_Script.CallVoid("SetFacePointAfterMove", facePointAfterMove);

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2020 Wildfire Games.
/* 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
@ -116,6 +116,12 @@ public:
*/
virtual fixed GetSpeed() const = 0;
/**
* @return the estimated position of the unit in @param dt seconds,
* following current paths. This is allowed to 'look into the future'.
*/
virtual CFixedVector2D EstimateFuturePosition(const fixed dt) const = 0;
/**
* Set whether the unit will turn to face the target point after finishing moving.
*/