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:
parent
59b468a0df
commit
f737831167
@ -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;
|
||||
|
@ -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) *
|
||||
|
@ -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;
|
||||
|
@ -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");
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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.
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user