1
0
forked from 0ad/0ad

Update range queries to account for entity size.

CCmpRangeManager queries do not take obstruction size into account,
meaning they return fewer entities than they should. This particularly
affects buildings with ranged attacks, gates, and a few other templates.

This is, unfortunately, a slight performance decrease.

Discovered following Angen's comment [[
https://code.wildfiregames.com/D2738#116269 | here ]].

Comments by: Angen
Reviewed By: bb
Refs #3381 (not marking it down as 'fixes' and I'm not entirely sure it
was the only moving part here).

Differential Revision: https://code.wildfiregames.com/D2759
This was SVN commit r24217.
This commit is contained in:
wraitii 2020-11-19 14:07:24 +00:00
parent b57abe806c
commit d0fc8ff67d
16 changed files with 183 additions and 40 deletions

View File

@ -339,12 +339,34 @@ BuildingAI.prototype.FireArrows = function()
for (let target of this.targetUnits)
addTarget(target);
// The obstruction manager performs approximate range checks
// so we need to verify them here.
// TODO: perhaps an optional 'precise' mode to range queries would be more performant.
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
let range = cmpAttack.GetRange(attackType);
let thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!thisCmpPosition.IsInWorld())
return;
let s = thisCmpPosition.GetPosition();
for (let i = 0; i < arrowsToFire; ++i)
{
let selectedIndex = targets.randomIndex();
let selectedTarget = targets.itemAt(selectedIndex);
if (selectedTarget && this.CheckTargetVisible(selectedTarget))
// Copied from UnitAI's MoveToTargetAttackRange.
let targetCmpPosition = Engine.QueryInterface(selectedTarget, IID_Position);
if (!targetCmpPosition.IsInWorld())
continue;
let t = targetCmpPosition.GetPosition();
// h is positive when I'm higher than the target
let h = s.y - t.y + range.elevationBonus;
let parabolicMaxRange = Math.sqrt(Math.square(range.max) + 2 * range.max * h);
if (selectedTarget && this.CheckTargetVisible(selectedTarget) &&
h > -range.max / 2 && cmpObstructionManager.IsInTargetRange(
this.entity, selectedTarget, range.min, parabolicMaxRange, false))
{
cmpAttack.PerformAttack(attackType, selectedTarget);
PlaySound("attack_" + attackType.toLowerCase(), this.entity);

View File

@ -7,7 +7,7 @@
<Category>Monument</Category>
<Distance>
<FromClass>Monument</FromClass>
<MinDistance>150</MinDistance>
<MinDistance>135</MinDistance>
</Distance>
</BuildRestrictions>
<Capturable disable=""/>

View File

@ -7,7 +7,7 @@
<Category>Pillar</Category>
<Distance>
<FromClass>Pillar</FromClass>
<MinDistance>75</MinDistance>
<MinDistance>70</MinDistance>
</Distance>
</BuildRestrictions>
<Capturable disable=""/>

View File

@ -4,7 +4,7 @@
<Territory>own neutral</Territory>
<Distance>
<FromClass>MercenaryCamp</FromClass>
<MinDistance>100</MinDistance>
<MinDistance>70</MinDistance>
</Distance>
</BuildRestrictions>
<Cost>

View File

@ -11,7 +11,7 @@
<Height>7.0</Height>
</Footprint>
<Gate>
<PassRange>20</PassRange>
<PassRange>2</PassRange>
</Gate>
<Health>
<Max op="mul">0.5</Max>

View File

@ -4,7 +4,7 @@
<Territory>own neutral</Territory>
<Distance>
<FromClass>MercenaryCamp</FromClass>
<MinDistance>100</MinDistance>
<MinDistance>70</MinDistance>
</Distance>
</BuildRestrictions>
<Cost>

View File

@ -38,7 +38,7 @@
<Category>ArmyCamp</Category>
<Distance>
<FromClass>ArmyCamp</FromClass>
<MinDistance>80</MinDistance>
<MinDistance>45</MinDistance>
</Distance>
</BuildRestrictions>
<Capturable>

View File

@ -44,7 +44,7 @@
<Category>CivilCentre</Category>
<Distance>
<FromClass>CivilCentre</FromClass>
<MinDistance>200</MinDistance>
<MinDistance>160</MinDistance>
</Distance>
</BuildRestrictions>
<Capturable>

View File

@ -8,7 +8,7 @@
<Category>Colony</Category>
<Distance>
<FromClass>CivilCentre</FromClass>
<MinDistance>120</MinDistance>
<MinDistance>80</MinDistance>
</Distance>
</BuildRestrictions>
<Cost>

View File

@ -7,7 +7,7 @@
<Territory>own neutral</Territory>
<Distance>
<FromClass>Outpost</FromClass>
<MinDistance>50</MinDistance>
<MinDistance>45</MinDistance>
</Distance>
</BuildRestrictions>
<Cost>

View File

@ -35,7 +35,7 @@
<Category>Tower</Category>
<Distance>
<FromClass>Tower</FromClass>
<MinDistance>60</MinDistance>
<MinDistance>55</MinDistance>
</Distance>
</BuildRestrictions>
<GarrisonHolder>

View File

@ -12,7 +12,7 @@
</Resources>
</Cost>
<Gate>
<PassRange>20</PassRange>
<PassRange>2</PassRange>
</Gate>
<Health>
<Max>2500</Max>

View File

@ -37,7 +37,7 @@
<Category>Fortress</Category>
<Distance>
<FromClass>Fortress</FromClass>
<MinDistance>80</MinDistance>
<MinDistance>55</MinDistance>
</Distance>
</BuildRestrictions>
<Capturable>

View File

@ -142,22 +142,24 @@ static inline u16 CalcVisionSharingMask(player_id_t player)
*/
struct Query
{
bool enabled;
bool parabolic;
std::vector<entity_id_t> lastMatch;
CEntityHandle source; // TODO: this could crash if an entity is destroyed while a Query is still referencing it
entity_pos_t minRange;
entity_pos_t maxRange;
entity_pos_t elevationBonus;
entity_pos_t elevationBonus; // Used for parabolas only.
u32 ownersMask;
i32 interface;
std::vector<entity_id_t> lastMatch;
u8 flagsMask;
bool enabled;
bool parabolic;
};
/**
* Checks whether v is in a parabolic range of (0,0,0)
* The highest point of the paraboloid is (0,range/2,0)
* and the circle of distance 'range' around (0,0,0) on height y=0 is part of the paraboloid
* This equates to computing f(x, z) = y = -(xx + zz)/(2*range) + range/2 > 0,
* or alternatively (xx+zz) <= (range^2 - 2range*y).
*
* Avoids sqrting and overflowing.
*/
@ -1206,19 +1208,20 @@ public:
continue;
CFixedVector3D secondPosition = cmpSecondPosition->GetPosition();
// Restrict based on precise distance
if (!InParabolicRange(
CFixedVector3D(it->second.x, secondPosition.Y, it->second.z)
- pos3d,
q.maxRange))
// Doing an exact check for parabolas with obstruction sizes is not really possible.
// However, we can prove that InParabolicRange(d, range + size) > InParabolicRange(d, range)
// in the sense that it always returns true when the latter would, which is enough.
// To do so, compute the derivative with respect to distance, and notice that
// they have an intersection after which the former grows slower, and then use that to prove the above.
// Note that this is only true because we do not account for vertical size here,
// if we did, we would also need to artificially 'raise' the source over the target.
if (!InParabolicRange(CFixedVector3D(it->second.x, secondPosition.Y, it->second.z) - pos3d,
q.maxRange + fixed::FromInt(it->second.size)))
continue;
if (!q.minRange.IsZero())
{
int distVsMin = (CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.minRange);
if (distVsMin < 0)
if ((CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.minRange) < 0)
continue;
}
r.push_back(it->first);
}
@ -1239,17 +1242,13 @@ public:
if (!TestEntityQuery(q, it->first, it->second))
continue;
// Restrict based on precise distance
int distVsMax = (CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.maxRange);
if (distVsMax > 0)
// Restrict based on approximate circle-circle distance.
if ((CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.maxRange + fixed::FromInt(it->second.size)) > 0)
continue;
if (!q.minRange.IsZero())
{
int distVsMin = (CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.minRange);
if (distVsMin < 0)
if ((CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.minRange) < 0)
continue;
}
r.push_back(it->first);
}
@ -1377,6 +1376,16 @@ public:
q.maxRange = maxRange;
q.elevationBonus = entity_pos_t::Zero();
if (q.source.GetId() != INVALID_ENTITY && q.maxRange != entity_pos_t::FromInt(-1))
{
EntityMap<EntityData>::const_iterator it = m_EntityData.find(q.source.GetId());
ENSURE(it != m_EntityData.end());
// Adjust the range query based on the querier's obstruction radius.
// The smallest side of the obstruction isn't known here, so we can't safely adjust the min-range, only the max.
// 'size' is the diagonal size rounded up so this will cover all possible rotations of the querier.
q.maxRange += fixed::FromInt(it->second.size);
}
q.ownersMask = 0;
for (size_t i = 0; i < owners.size(); ++i)
q.ownersMask |= CalcOwnerMask(owners[i]);

View File

@ -73,7 +73,7 @@ class CLosQuerier;
*
* In most cases the users are event-based and want notifications when something
* has entered or left the range, and the query can be set up once and rarely changed.
* These queries have to be fast. It's fine to approximate an entity as a point.
* These queries have to be fast. Entities are approximated as circles.
*
* Current design:
*

View File

@ -17,13 +17,14 @@
#include "simulation2/system/ComponentTest.h"
#include "simulation2/components/ICmpRangeManager.h"
#include "simulation2/components/ICmpObstruction.h"
#include "simulation2/components/ICmpPosition.h"
#include "simulation2/components/ICmpVision.h"
#include <boost/random/mersenne_twister.hpp>
#include <boost/random/uniform_real_distribution.hpp>
class MockVision : public ICmpVision
class MockVisionRgm : public ICmpVision
{
public:
DEFAULT_MOCK_COMPONENT()
@ -32,7 +33,7 @@ public:
virtual bool GetRevealShore() const { return false; }
};
class MockPosition : public ICmpPosition
class MockPositionRgm : public ICmpPosition
{
public:
DEFAULT_MOCK_COMPONENT()
@ -56,8 +57,8 @@ public:
virtual void SetFloating(bool UNUSED(flag)) { }
virtual void SetActorFloating(bool UNUSED(flag)) { }
virtual void SetConstructionProgress(fixed UNUSED(progress)) { }
virtual CFixedVector3D GetPosition() const { return CFixedVector3D(); }
virtual CFixedVector2D GetPosition2D() const { return CFixedVector2D(); }
virtual CFixedVector3D GetPosition() const { return m_Pos; }
virtual CFixedVector2D GetPosition2D() const { return CFixedVector2D(m_Pos.X, m_Pos.Z); }
virtual CFixedVector3D GetPreviousPosition() const { return CFixedVector3D(); }
virtual CFixedVector2D GetPreviousPosition2D() const { return CFixedVector2D(); }
virtual void TurnTo(entity_angle_t UNUSED(y)) { }
@ -67,6 +68,45 @@ public:
virtual fixed GetDistanceTravelled() const { return fixed::Zero(); }
virtual void GetInterpolatedPosition2D(float UNUSED(frameOffset), float& x, float& z, float& rotY) const { x = z = rotY = 0; }
virtual CMatrix3D GetInterpolatedTransform(float UNUSED(frameOffset)) const { return CMatrix3D(); }
CFixedVector3D m_Pos;
};
class MockObstructionRgm : public ICmpObstruction
{
public:
DEFAULT_MOCK_COMPONENT();
MockObstructionRgm(entity_pos_t s) : m_Size(s) {};
virtual ICmpObstructionManager::tag_t GetObstruction() const { return {}; };
virtual bool GetObstructionSquare(ICmpObstructionManager::ObstructionSquare&) const { return false; };
virtual bool GetPreviousObstructionSquare(ICmpObstructionManager::ObstructionSquare&) const { return false; };
virtual entity_pos_t GetSize() const { return m_Size; };
virtual CFixedVector2D GetStaticSize() const { return {}; };
virtual EObstructionType GetObstructionType() const { return {}; };
virtual void SetUnitClearance(const entity_pos_t&) {};
virtual bool IsControlPersistent() const { return {}; };
virtual bool CheckShorePlacement() const { return {}; };
virtual EFoundationCheck CheckFoundation(const std::string&) const { return {}; };
virtual EFoundationCheck CheckFoundation(const std::string& , bool) const { return {}; };
virtual std::string CheckFoundation_wrapper(const std::string&, bool) const { return {}; };
virtual bool CheckDuplicateFoundation() const { return {}; };
virtual std::vector<entity_id_t> GetEntitiesByFlags(ICmpObstructionManager::flags_t) const { return {}; };
virtual std::vector<entity_id_t> GetEntitiesBlockingMovement() const { return {}; };
virtual std::vector<entity_id_t> GetEntitiesBlockingConstruction() const { return {}; };
virtual std::vector<entity_id_t> GetEntitiesDeletedUponConstruction() const { return {}; };
virtual void ResolveFoundationCollisions() const {};
virtual void SetActive(bool) {};
virtual void SetMovingFlag(bool) {};
virtual void SetDisableBlockMovementPathfinding(bool, bool, int32_t) {};
virtual bool GetBlockMovementFlag() const { return {}; };
virtual void SetControlGroup(entity_id_t) {};
virtual entity_id_t GetControlGroup() const { return {}; };
virtual void SetControlGroup2(entity_id_t) {};
virtual entity_id_t GetControlGroup2() const { return {}; };
private:
entity_pos_t m_Size;
};
class TestCmpRangeManager : public CxxTest::TestSuite
@ -91,10 +131,10 @@ public:
ICmpRangeManager* cmp = test.Add<ICmpRangeManager>(CID_RangeManager, "", SYSTEM_ENTITY);
MockVision vision;
MockVisionRgm vision;
test.AddMock(100, IID_Vision, vision);
MockPosition position;
MockPositionRgm position;
test.AddMock(100, IID_Position, position);
// This tests that the incremental computation produces the correct result
@ -153,4 +193,76 @@ public:
}
}
}
void test_queries()
{
ComponentTestHelper test(g_ScriptContext);
ICmpRangeManager* cmp = test.Add<ICmpRangeManager>(CID_RangeManager, "", SYSTEM_ENTITY);
MockVisionRgm vision, vision2;
MockPositionRgm position, position2;
MockObstructionRgm obs(fixed::FromInt(2)), obs2(fixed::Zero());
test.AddMock(100, IID_Vision, vision);
test.AddMock(100, IID_Position, position);
test.AddMock(100, IID_Obstruction, obs);
test.AddMock(101, IID_Vision, vision2);
test.AddMock(101, IID_Position, position2);
test.AddMock(101, IID_Obstruction, obs2);
cmp->SetBounds(entity_pos_t::FromInt(0), entity_pos_t::FromInt(0), entity_pos_t::FromInt(512), entity_pos_t::FromInt(512), 512/TERRAIN_TILE_SIZE + 1);
cmp->Verify();
{ CMessageCreate msg(100); cmp->HandleMessage(msg, false); }
{ CMessageCreate msg(101); cmp->HandleMessage(msg, false); }
{ CMessageOwnershipChanged msg(100, -1, 1); cmp->HandleMessage(msg, false); }
{ CMessageOwnershipChanged msg(101, -1, 1); cmp->HandleMessage(msg, false); }
auto move = [&cmp](entity_id_t ent, MockPositionRgm& pos, fixed x, fixed z) {
pos.m_Pos = CFixedVector3D(x, fixed::Zero(), z);
{ CMessagePositionChanged msg(ent, true, x, z, entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
};
move(100, position, fixed::FromInt(10), fixed::FromInt(10));
move(101, position2, fixed::FromInt(10), fixed::FromInt(20));
std::vector<entity_id_t> nearby = cmp->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {1}, 0);
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{});
nearby = cmp->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {1}, 0);
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{101});
move(101, position2, fixed::FromInt(10), fixed::FromInt(10));
nearby = cmp->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {1}, 0);
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{101});
nearby = cmp->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {1}, 0);
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{});
move(101, position2, fixed::FromInt(10), fixed::FromInt(13));
nearby = cmp->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {1}, 0);
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{101});
nearby = cmp->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {1}, 0);
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{});
move(101, position2, fixed::FromInt(10), fixed::FromInt(15));
// In range thanks to self obstruction size.
nearby = cmp->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {1}, 0);
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{101});
// In range thanks to target obstruction size.
nearby = cmp->ExecuteQuery(101, fixed::FromInt(0), fixed::FromInt(4), {1}, 0);
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{100});
// Trickier: min-range is closest-to-closest, but rotation may change the real distance.
nearby = cmp->ExecuteQuery(100, fixed::FromInt(2), fixed::FromInt(50), {1}, 0);
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{101});
nearby = cmp->ExecuteQuery(100, fixed::FromInt(5), fixed::FromInt(50), {1}, 0);
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{101});
nearby = cmp->ExecuteQuery(100, fixed::FromInt(6), fixed::FromInt(50), {1}, 0);
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{});
nearby = cmp->ExecuteQuery(101, fixed::FromInt(5), fixed::FromInt(50), {1}, 0);
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{100});
nearby = cmp->ExecuteQuery(101, fixed::FromInt(6), fixed::FromInt(50), {1}, 0);
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{});
}
};