1
0
forked from 0ad/0ad

Improve ship pickup.

Improve unitAI: don't move if the requester can reach us and we are
close enough. This avoids an issue where ships moved more than necessary
when picking up many units.
Also improve requester UnitAI -> retry pickup if the target entity is
Idle.
Improve unitMotion: periodically recompute paths in "known bad path"
mode to adapt to moving targets.
Expose UnitMotion reachability to scripts and other code.

This adds a test map for some common and some tricky pickup cases, using
triggers.

Based on a patch by: causative
Reviewed By: Freagarach
Fixes #3472

Differential Revision: https://code.wildfiregames.com/D665
This was SVN commit r23925.
This commit is contained in:
wraitii 2020-08-03 12:02:24 +00:00
parent 88dc6d8e1e
commit 375c319639
12 changed files with 372 additions and 44 deletions

BIN
binaries/data/mods/public/maps/scenarios/pickup_test_map.pmp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
binaries/data/mods/public/maps/scenarios/pickup_test_map.xml (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,218 @@
const UNIT_TEMPLATE = "units/athen_infantry_marine_archer_b";
const SHIP_TEMPLATE = "units/athen_ship_trireme";
const RAM_TEMPLATE = "units/brit_siege_ram";
const point_plaza_nw = 14; // Center plaza #1 (NW)
const point_plaza_ne = 15; // Center plaza #2 (NE)
const point_plaza_se = 16; // Center plaza #3 (SE)
const point_plaza_sw = 17; // Center plaza #4 (SW)
const point_road_1 = 18; // 'Road' from plaza to land end in the NW, #1
const point_road_2 = 19; // 'Road' from plaza to land end in the NW, #2
const point_road_3 = 20; // 'Road' from plaza to land end in the NW, #3
const point_road_4 = 21; // 'Road' from plaza to land end in the NW, #4
const point_road_5 = 22; // 'Road' from plaza to land end in the NW, #5 and last
const point_island_1 = 23; // Sand Island #1 (north)
const point_island_2 = 24; // Sand Island #2 (south)
const point_sea_1 = 25; // Sea #1 (west)
const point_sea_2 = 26; // Sea #2 (east)
const point_coast_1 = 27; // Coast point #1 (E)
const point_coast_2 = 28; // Coast point #2 (W)
var QuickSpawn = function(x, z, template, owner = 1)
{
let ent = Engine.AddEntity(template);
let cmpEntOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpEntOwnership)
cmpEntOwnership.SetOwner(owner);
let cmpEntPosition = Engine.QueryInterface(ent, IID_Position);
cmpEntPosition.JumpTo(x, z);
return ent;
};
var Rotate = function(angle, ent)
{
let cmpEntPosition = Engine.QueryInterface(ent, IID_Position);
cmpEntPosition.SetYRotation(angle);
return ent;
};
var WalkTo = function(x, z, ent, owner = 1)
{
ProcessCommand(owner, {
"type": "walk",
"entities": Array.isArray(ent) ? ent : [ent],
"x": x,
"z": z,
"queued": false
});
return ent;
};
var Do = function(name, data, ent, owner=1)
{
let comm = {
"type": name,
"entities": Array.isArray(ent) ? ent : [ent],
"queued": false
};
for (let k in data)
comm[k] = data[k];
ProcessCommand(owner, comm);
};
var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
Trigger.prototype.UnitsIntoRamClose = function()
{
let pos = TriggerHelper.GetEntityPosition2D(point_plaza_nw);
let ram = QuickSpawn(pos.x, pos.y, RAM_TEMPLATE, 1);
for (let i = 0; i < 5; ++i)
{
pos = TriggerHelper.GetEntityPosition2D(point_plaza_ne);
Do("garrison", { "target": ram }, QuickSpawn(pos.x+i, pos.y, UNIT_TEMPLATE));
}
};
Trigger.prototype.UnitsIntoRamFar = function()
{
let pos = TriggerHelper.GetEntityPosition2D(point_road_5);
let ram = QuickSpawn(pos.x, pos.y, RAM_TEMPLATE, 1);
for (let i = 0; i < 5; ++i)
{
pos = TriggerHelper.GetEntityPosition2D(point_road_1);
Do("garrison", { "target": ram }, QuickSpawn(pos.x+i, pos.y, UNIT_TEMPLATE));
}
};
Trigger.prototype.UnitsIntoRamOppositeDir = function()
{
let pos = TriggerHelper.GetEntityPosition2D(point_plaza_sw);
let ram = QuickSpawn(pos.x, pos.y, RAM_TEMPLATE, 1);
let spawnpoints = [
TriggerHelper.GetEntityPosition2D(point_plaza_se),
TriggerHelper.GetEntityPosition2D(point_road_1)
];
for (let i = 0; i < 6; ++i)
{
pos = spawnpoints[i%2];
Do("garrison", { "target": ram }, QuickSpawn(pos.x+i, pos.y, UNIT_TEMPLATE));
}
};
Trigger.prototype.UnitsIntoRamImpossible = function()
{
let pos = TriggerHelper.GetEntityPosition2D(point_island_1);
let ram = QuickSpawn(pos.x, pos.y, RAM_TEMPLATE, 1);
for (let i = 0; i < 3; ++i)
{
pos = TriggerHelper.GetEntityPosition2D(point_plaza_se);
Do("garrison", { "target": ram }, QuickSpawn(pos.x+i, pos.y, UNIT_TEMPLATE));
}
};
Trigger.prototype.UnitsIntoShipClose = function()
{
let pos = TriggerHelper.GetEntityPosition2D(point_sea_1);
let ship = QuickSpawn(pos.x, pos.y, SHIP_TEMPLATE, 1);
for (let i = 0; i < 6; ++i)
{
pos = TriggerHelper.GetEntityPosition2D(point_plaza_se);
Do("garrison", { "target": ship }, QuickSpawn(pos.x+i, pos.y, UNIT_TEMPLATE));
}
};
Trigger.prototype.UnitsIntoShipFar = function()
{
let pos = TriggerHelper.GetEntityPosition2D(point_sea_1);
let ship = QuickSpawn(pos.x, pos.y, SHIP_TEMPLATE, 1);
for (let i = 0; i < 50; ++i)
{
pos = TriggerHelper.GetEntityPosition2D(point_road_4);
Do("garrison", { "target": ship }, QuickSpawn(pos.x+i, pos.y, UNIT_TEMPLATE));
}
};
Trigger.prototype.UnitsIntoShipTwoIslands = function()
{
let pos = TriggerHelper.GetEntityPosition2D(point_sea_2);
let ship = QuickSpawn(pos.x, pos.y, SHIP_TEMPLATE, 1);
for (let i = 0; i < 3; ++i)
{
pos = TriggerHelper.GetEntityPosition2D(point_road_2);
Do("garrison", { "target": ship }, QuickSpawn(pos.x+i, pos.y, UNIT_TEMPLATE));
}
for (let i = 0; i < 3; ++i)
{
pos = TriggerHelper.GetEntityPosition2D(point_island_2);
Do("garrison", { "target": ship }, QuickSpawn(pos.x+i, pos.y, UNIT_TEMPLATE));
}
};
Trigger.prototype.UnitsIntoShipThenAnotherCloseBy = function()
{
// This orders a unit to garrison in the ship, then orders another close by (by land)
// but not actually close by (by sea) so that the ship behaves erratically.
let pos = TriggerHelper.GetEntityPosition2D(point_sea_1);
let ship = QuickSpawn(pos.x, pos.y, SHIP_TEMPLATE, 1);
pos = TriggerHelper.GetEntityPosition2D(point_coast_1);
Do("garrison", { "target": ship }, QuickSpawn(pos.x, pos.y, UNIT_TEMPLATE));
// Don't do this at home
let holder = Engine.QueryInterface(ship, IID_GarrisonHolder);
let og = Object.getPrototypeOf(holder).PerformGarrison;
holder.PerformGarrison = (...args) => {
let res = og.apply(holder, args);
delete holder.PerformGarrison;
pos = TriggerHelper.GetEntityPosition2D(point_coast_2);
Do("garrison", { "target": ship }, QuickSpawn(pos.x, pos.y, UNIT_TEMPLATE));
return res;
};
};
Trigger.prototype.UnitsIntoShipAlreadyInRange = function()
{
// Yay ship on land.
let pos = TriggerHelper.GetEntityPosition2D(point_road_3);
let ship = QuickSpawn(pos.x, pos.y, SHIP_TEMPLATE, 1);
for (let i = 0; i < 10; ++i)
{
pos = TriggerHelper.GetEntityPosition2D(point_road_3);
Do("garrison", { "target": ship }, QuickSpawn(pos.x, pos.y, UNIT_TEMPLATE));
}
};
Trigger.prototype.IslandUnitsPlayerGivesBadOrder = function()
{
let pos = TriggerHelper.GetEntityPosition2D(point_sea_2);
let ship = QuickSpawn(pos.x, pos.y, SHIP_TEMPLATE, 1);
for (let i = 0; i < 3; ++i)
{
pos = TriggerHelper.GetEntityPosition2D(point_island_1);
Do("garrison", { "target": ship }, QuickSpawn(pos.x+i, pos.y, UNIT_TEMPLATE));
}
// Order the ship to go somewhere it can't pick up the units from.
WalkTo(567, 53, ship);
};
// Call everything here to easily disable/enable only some
cmpTrigger.DoAfterDelay(200, "UnitsIntoRamClose", {});
cmpTrigger.DoAfterDelay(200, "UnitsIntoRamFar", {});
cmpTrigger.DoAfterDelay(200, "UnitsIntoRamOppositeDir", {});
cmpTrigger.DoAfterDelay(200, "UnitsIntoRamImpossible", {});
cmpTrigger.DoAfterDelay(200, "UnitsIntoShipClose", {});
cmpTrigger.DoAfterDelay(200, "UnitsIntoShipFar", {});
cmpTrigger.DoAfterDelay(200, "UnitsIntoShipTwoIslands", {});
cmpTrigger.DoAfterDelay(200, "UnitsIntoShipThenAnotherCloseBy", {});
cmpTrigger.DoAfterDelay(200, "UnitsIntoShipAlreadyInRange", {});
cmpTrigger.DoAfterDelay(200, "IslandUnitsPlayerGivesBadOrder", {});

View File

@ -366,26 +366,24 @@ UnitAI.prototype.UnitFsmSpec = {
return;
}
let range = cmpGarrisonHolder.GetLoadingRange();
this.order.data.min = range.min;
this.order.data.max = range.max;
if (this.CheckRange(this.order.data))
{
this.FinishOrder();
return;
}
// Check if we need to move
// TODO implement a better way to know if we are on the shoreline
let needToMove = true;
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (this.lastShorelinePosition && cmpPosition && (this.lastShorelinePosition.x == cmpPosition.GetPosition().x) &&
(this.lastShorelinePosition.z == cmpPosition.GetPosition().z))
// we were already on the shoreline, and have not moved since
if (DistanceBetweenEntities(this.entity, this.order.data.target) < 50)
needToMove = false;
if (needToMove)
this.SetNextState("INDIVIDUAL.PICKUP.APPROACHING");
else
// If the target can reach us and we are reasonably close, don't move.
// TODO: it would be slightly more optimal to check for real, not bird-flight distance.
let cmpPassengerMotion = Engine.QueryInterface(this.order.data.target, IID_UnitMotion);
if (cmpPassengerMotion &&
cmpPassengerMotion.IsTargetRangeReachable(this.entity, range.min, range.max) &&
DistanceBetweenEntities(this.entity, this.order.data.target) < 200)
this.SetNextState("INDIVIDUAL.PICKUP.LOADING");
else
this.SetNextState("INDIVIDUAL.PICKUP.APPROACHING");
},
"Order.Guard": function(msg) {
@ -3089,10 +3087,6 @@ UnitAI.prototype.UnitFsmSpec = {
// If a pickup has been requested, remove it
if (this.pickup)
{
var cmpHolderPosition = Engine.QueryInterface(target, IID_Position);
var cmpHolderUnitAI = Engine.QueryInterface(target, IID_UnitAI);
if (cmpHolderUnitAI && cmpHolderPosition)
cmpHolderUnitAI.lastShorelinePosition = cmpHolderPosition.GetPosition();
Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity });
delete this.pickup;
}
@ -3113,7 +3107,8 @@ UnitAI.prototype.UnitFsmSpec = {
if (this.pickup)
{
var cmpUnitAI = Engine.QueryInterface(this.pickup, IID_UnitAI);
if (!cmpUnitAI || !cmpUnitAI.HasPickupOrder(this.entity))
if (!cmpUnitAI || (!cmpUnitAI.HasPickupOrder(this.entity) &&
!cmpUnitAI.IsIdle()))
{
this.FinishOrder();
return true;
@ -4830,17 +4825,12 @@ UnitAI.prototype.CheckFormationTargetAttackRange = function(target)
UnitAI.prototype.CheckGarrisonRange = function(target)
{
var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
let cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
if (!cmpGarrisonHolder)
return false;
var range = cmpGarrisonHolder.GetLoadingRange();
var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
if (cmpObstruction)
range.max += cmpObstruction.GetUnitRadius()*1.5; // multiply by something larger than sqrt(2)
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false);
let range = cmpGarrisonHolder.GetLoadingRange();
return this.CheckTargetRangeExplicit(target, range.min, range.max);
};
/**

View File

@ -875,6 +875,18 @@ void CCmpPathfinder::PushRequestsToWorkers(std::vector<T>& from)
//////////////////////////////////////////////////////////
bool CCmpPathfinder::IsGoalReachable(entity_pos_t x0, entity_pos_t z0, const PathGoal& goal, pass_class_t passClass)
{
PROFILE2("IsGoalReachable");
u16 i, j;
Pathfinding::NearestNavcell(x0, z0, i, j, m_MapSize*Pathfinding::NAVCELLS_PER_TILE, m_MapSize*Pathfinding::NAVCELLS_PER_TILE);
if (!IS_PASSABLE(m_Grid->get(i, j), passClass))
m_PathfinderHier->FindNearestPassableNavcell(i, j, passClass);
return m_PathfinderHier->IsGoalReachable(i, j, goal, passClass);
}
bool CCmpPathfinder::CheckMovement(const IObstructionTestFilter& filter,
entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r,
pass_class_t passClass) const

View File

@ -204,6 +204,8 @@ public:
virtual u32 ComputeShortPathAsync(entity_pos_t x0, entity_pos_t z0, entity_pos_t clearance, entity_pos_t range, const PathGoal& goal, pass_class_t passClass, bool avoidMovingUnits, entity_id_t controller, entity_id_t notify);
virtual bool IsGoalReachable(entity_pos_t x0, entity_pos_t z0, const PathGoal& goal, pass_class_t passClass);
virtual void SetDebugPath(entity_pos_t x0, entity_pos_t z0, const PathGoal& goal, pass_class_t passClass);
virtual void SetDebugOverlay(bool enabled);

View File

@ -76,6 +76,16 @@ static const entity_pos_t DIRECT_PATH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE
*/
static const entity_pos_t TARGET_UNCERTAINTY_MULTIPLIER = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*2);
/**
* When following a known imperfect path (i.e. a path that won't take us in range of our goal
* we still recompute a new path every N turn to adapt to moving targets (for example, ships that must pickup
* units may easily end up in this state, they still need to adjust to moving units).
* This is rather arbitrary and mostly for simplicity & optimisation (a better recomputing algorithm
* would not need this).
* Keep in mind that MP turns are currently 500ms.
*/
static const u8 KNOWN_IMPERFECT_PATH_RESET_COUNTDOWN = 12;
/**
* When we fail more than this many path computations in a row, inform other components that the move will fail.
* Experimentally, this number needs to be somewhat high or moving groups of units will lead to stuck units.
@ -136,12 +146,14 @@ public:
// that the move will likely fail.
u8 m_FailedPathComputations = 0;
// If true, PathingUpdateNeeded returns false always.
// If > 0, PathingUpdateNeeded returns false always.
// This exists because the goal may be unreachable to the short/long pathfinder.
// In such cases, we would compute inacceptable paths and PathingUpdateNeeded would trigger every turn.
// To avoid that, when we know the new path is imperfect, treat it as OK and follow it until the end.
// When reaching the end, we'll run through HandleObstructedMove and this will be reset.
bool m_FollowKnownImperfectPath = false;
// In such cases, we would compute inacceptable paths and PathingUpdateNeeded would trigger every turn,
// which would be quite bad for performance.
// To avoid that, when we know the new path is imperfect, treat it as OK and follow it anyways.
// When reaching the end, we'll go through HandleObstructedMove and reset regardless.
// To still recompute now and then (the target may be moving), this is a countdown decremented on each frame.
u8 m_FollowKnownImperfectPathCountdown = 0;
struct Ticket {
u32 m_Ticket = 0; // asynchronous request ID we're waiting for, or 0 if none
@ -251,8 +263,8 @@ public:
serialize.NumberU32_Unbounded("ticket", m_ExpectedPathTicket.m_Ticket);
SerializeU8_Enum<Ticket::Type, Ticket::Type::LONG_PATH>()(serialize, "ticket type", m_ExpectedPathTicket.m_Type);
serialize.NumberU8("failed path computations", m_FailedPathComputations, 0, 255);
serialize.Bool("followknownimperfectpath", m_FollowKnownImperfectPath);
serialize.NumberU8_Unbounded("failed path computations", m_FailedPathComputations);
serialize.NumberU8_Unbounded("followknownimperfectpath", m_FollowKnownImperfectPathCountdown);
SerializeU8_Enum<MoveRequest::Type, MoveRequest::Type::OFFSET>()(serialize, "target type", m_MoveRequest.m_Type);
serialize.NumberU32_Unbounded("target entity", m_MoveRequest.m_Entity);
@ -440,6 +452,8 @@ public:
MoveTo(MoveRequest(target, CFixedVector2D(x, z)));
}
virtual bool IsTargetRangeReachable(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange);
virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z);
/**
@ -547,15 +561,18 @@ private:
/**
* Increment the number of failed path computations and notify other components if required.
* @returns true if the failure was notified, false otherwise.
*/
void IncrementFailedPathComputationAndMaybeNotify()
bool IncrementFailedPathComputationAndMaybeNotify()
{
m_FailedPathComputations++;
if (m_FailedPathComputations >= MAX_FAILED_PATH_COMPUTATIONS)
{
MoveFailed();
m_FailedPathComputations = 0;
return true;
}
return false;
}
/**
@ -745,7 +762,7 @@ void CCmpUnitMotion::PathResult(u32 ticket, const WaypointPath& path)
m_LongPath = path;
m_FollowKnownImperfectPath = false;
m_FollowKnownImperfectPathCountdown = 0;
// If there's no waypoints then we couldn't get near the target.
// Sort of hack: Just try going directly to the goal point instead
@ -765,10 +782,18 @@ void CCmpUnitMotion::PathResult(u32 ticket, const WaypointPath& path)
// (we will still fail the move when we arrive to the best possible position, and if we were blocked by
// an obstruction and it goes away we will notice when getting there as having no waypoint goes through
// HandleObstructedMove, so this is safe).
// TODO: For now, we won't warn components straight away as that could lead to units idling earlier than expected,
// but it should be done someday when the message can differentiate between different failure causes.
else if (PathingUpdateNeeded(pos))
m_FollowKnownImperfectPath = true;
{
// Inform other components early, as they might have better behaviour than waiting for the path to carry out.
// Send OBSTRUCTED at first - moveFailed is likely to trigger path recomputation and we might end up
// recomputing too often for nothing.
if (!IncrementFailedPathComputationAndMaybeNotify())
{
CMessageMotionUpdate msg(CMessageMotionUpdate::OBSTRUCTED);
GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
}
m_FollowKnownImperfectPathCountdown = KNOWN_IMPERFECT_PATH_RESET_COUNTDOWN;
}
return;
}
@ -781,11 +806,21 @@ void CCmpUnitMotion::PathResult(u32 ticket, const WaypointPath& path)
m_ShortPath = path;
m_FollowKnownImperfectPath = false;
m_FollowKnownImperfectPathCountdown = 0;
if (!m_ShortPath.m_Waypoints.empty())
{
if (PathingUpdateNeeded(pos))
m_FollowKnownImperfectPath = true;
{
// Inform other components early, as they might have better behaviour than waiting for the path to carry out.
// Send OBSTRUCTED at first - moveFailed is likely to trigger path recomputation and we might end up
// recomputing too often for nothing.
if (!IncrementFailedPathComputationAndMaybeNotify())
{
CMessageMotionUpdate msg(CMessageMotionUpdate::OBSTRUCTED);
GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
}
m_FollowKnownImperfectPathCountdown = KNOWN_IMPERFECT_PATH_RESET_COUNTDOWN;
}
return;
}
@ -883,6 +918,8 @@ void CCmpUnitMotion::Move(fixed dt)
if (ComputeGoal(goal, m_MoveRequest))
ComputePathToGoal(pos, goal);
}
else if (m_FollowKnownImperfectPathCountdown > 0)
--m_FollowKnownImperfectPathCountdown;
}
bool CCmpUnitMotion::PossiblyAtDestination() const
@ -1187,7 +1224,7 @@ bool CCmpUnitMotion::PathingUpdateNeeded(const CFixedVector2D& from) const
if (!ComputeTargetPosition(targetPos))
return false;
if (m_FollowKnownImperfectPath)
if (m_FollowKnownImperfectPathCountdown > 0)
return false;
if (PossiblyAtDestination())
@ -1480,12 +1517,29 @@ bool CCmpUnitMotion::MoveTo(MoveRequest request)
m_MoveRequest = request;
m_FailedPathComputations = 0;
m_FollowKnownImperfectPath = false;
m_FollowKnownImperfectPathCountdown = 0;
ComputePathToGoal(cmpPosition->GetPosition2D(), goal);
return true;
}
bool CCmpUnitMotion::IsTargetRangeReachable(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange)
{
CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle());
if (!cmpPosition || !cmpPosition->IsInWorld())
return false;
MoveRequest request(target, minRange, maxRange);
PathGoal goal;
if (!ComputeGoal(goal, request))
return false;
CmpPtr<ICmpPathfinder> cmpPathfinder(GetSimContext(), SYSTEM_ENTITY);
CFixedVector2D pos = cmpPosition->GetPosition2D();
return cmpPathfinder->IsGoalReachable(pos.X, pos.Y, goal, m_PassClass);
}
void CCmpUnitMotion::RenderPath(const WaypointPath& path, std::vector<SOverlayLine>& lines, CColor color)
{
bool floating = false;

View File

@ -133,6 +133,12 @@ public:
*/
virtual void SetDebugPath(entity_pos_t x0, entity_pos_t z0, const PathGoal& goal, pass_class_t passClass) = 0;
/**
* @return true if the goal is reachable from (x0, z0) for the given passClass, false otherwise.
* Warning: this is synchronous, somewhat expensive and not should not be called too liberally.
*/
virtual bool IsGoalReachable(entity_pos_t x0, entity_pos_t z0, const PathGoal& goal, pass_class_t passClass) = 0;
/**
* Check whether the given movement line is valid and doesn't hit any obstructions
* or impassable terrain.

View File

@ -26,6 +26,7 @@ BEGIN_INTERFACE_WRAPPER(UnitMotion)
DEFINE_INTERFACE_METHOD_4("MoveToPointRange", bool, ICmpUnitMotion, MoveToPointRange, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t)
DEFINE_INTERFACE_METHOD_3("MoveToTargetRange", bool, ICmpUnitMotion, MoveToTargetRange, entity_id_t, entity_pos_t, entity_pos_t)
DEFINE_INTERFACE_METHOD_3("MoveToFormationOffset", void, ICmpUnitMotion, MoveToFormationOffset, entity_id_t, entity_pos_t, entity_pos_t)
DEFINE_INTERFACE_METHOD_3("IsTargetRangeReachable", bool, ICmpUnitMotion, IsTargetRangeReachable, entity_id_t, entity_pos_t, entity_pos_t)
DEFINE_INTERFACE_METHOD_2("FaceTowardsPoint", void, ICmpUnitMotion, FaceTowardsPoint, entity_pos_t, entity_pos_t)
DEFINE_INTERFACE_METHOD_0("StopMoving", void, ICmpUnitMotion, StopMoving)
DEFINE_INTERFACE_METHOD_CONST_0("GetCurrentSpeed", fixed, ICmpUnitMotion, GetCurrentSpeed)
@ -61,6 +62,11 @@ public:
m_Script.CallVoid("MoveToFormationOffset", target, x, z);
}
virtual bool IsTargetRangeReachable(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange)
{
return m_Script.Call<bool>("IsTargetRangeReachable", target, minRange, maxRange);
}
virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z)
{
m_Script.CallVoid("FaceTowardsPoint", x, z);

View File

@ -60,6 +60,15 @@ public:
*/
virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) = 0;
/**
* Check if the target is reachable.
* Don't take this as absolute gospel since there are things that the pathfinder may not detect, such as
* entity obstructions in the way, but in general it should return satisfactory results.
* The interface is similar to MoveToTargetRange but the move is not attempted.
* @return true if the target is assumed reachable, false otherwise.
*/
virtual bool IsTargetRangeReachable(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0;
/**
* Turn to look towards the given point.
*/

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2019 Wildfire Games.
/* Copyright (C) 2020 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -704,6 +704,25 @@ bool HierarchicalPathfinder::MakeGoalReachable(u16 i0, u16 j0, PathGoal& goal, p
return false;
}
bool HierarchicalPathfinder::IsGoalReachable(u16 i0, u16 j0, const PathGoal& goal, pass_class_t passClass) const
{
PROFILE2("IsGoalReachable");
u16 iGoal, jGoal;
Pathfinding::NearestNavcell(goal.x, goal.z, iGoal, jGoal, m_W, m_H);
std::set<InterestingRegion, SortByBestToPoint> goalRegions(SortByBestToPoint(i0, j0));
// This returns goal regions ordered by distance from the best navcell in each region.
FindGoalRegionsAndBestNavcells(i0, j0, iGoal, jGoal, goal, goalRegions, passClass);
// Because of the sorting above, we can stop as soon as the first reachable goal region is found.
for (const InterestingRegion& region : goalRegions)
if (GetGlobalRegion(region.region, passClass) == GetGlobalRegion(i0, j0, passClass))
return true;
return false;
}
void HierarchicalPathfinder::FindNearestPassableNavcell(u16& i, u16& j, pass_class_t passClass) const
{
std::set<RegionID, SortByCenterToPoint> regions(SortByCenterToPoint(i, j));

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2019 Wildfire Games.
/* Copyright (C) 2020 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -128,6 +128,12 @@ public:
*/
bool MakeGoalReachable(u16 i0, u16 j0, PathGoal& goal, pass_class_t passClass) const;
/**
* @return true if the goal is reachable from navcell i0, j0.
* (similar to MakeGoalReachable but only checking for reachability).
*/
bool IsGoalReachable(u16 i0, u16 j0, const PathGoal& goal, pass_class_t passClass) const;
/**
* Updates @p i, @p j (which is assumed to be an impassable navcell)
* to the nearest passable navcell.