1
0
forked from mirrors/0ad

Improve group movement by distributing units around the target.

This improves pathfinding feel as units no longer try to clump on a particular point. However, can lead to oddities if there's a lot of impassable terrain around.

Closes #7791
This commit is contained in:
Lancelot de Ferrière
2025-04-26 17:48:41 +02:00
parent efdc1fab0a
commit a7330cf469
6 changed files with 133 additions and 14 deletions
@@ -104,9 +104,18 @@ var g_Commands = {
"walk": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Walk(cmd.x, cmd.z, cmd.queued, cmd.pushFront);
});
const ents = data.entities.length;
const uais = GetFormationUnitAIs(data.entities, player, cmd, data.formation);
if (uais.length === 1 || uais.length !== ents)
uais.forEach(cmpUnitAI => {
cmpUnitAI.Walk(cmd.x, cmd.z, cmd.queued, cmd.pushFront);
});
else {
const positions = Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).DistributeAround(data.entities, cmd.x, cmd.z);
uais.forEach((cmpUnitAI, index) => {
cmpUnitAI.Walk(positions[index].x, positions[index].y, cmd.queued, cmd.pushFront);
});
}
},
"walk-custom": function(player, cmd, data)
@@ -130,9 +139,18 @@ var g_Commands = {
"attack-walk": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, cmd.allowCapture, cmd.queued, cmd.pushFront);
});
const ents = data.entities.length;
const uais = GetFormationUnitAIs(data.entities, player, cmd, data.formation);
if (uais.length === 1 || uais.length !== ents)
uais.forEach(cmpUnitAI => {
cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, cmd.allowCapture, cmd.queued, cmd.pushFront);
});
else {
const positions = Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).DistributeAround(data.entities, cmd.x, cmd.z);
uais.forEach((cmpUnitAI, index) => {
cmpUnitAI.WalkAndFight(positions[index].x, positions[index].y, cmd.targetClasses, cmd.allowCapture, cmd.queued, cmd.pushFront);
});
}
},
"attack-walk-custom": function(player, cmd, data)
@@ -152,9 +170,18 @@ var g_Commands = {
"patrol": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI =>
cmpUnitAI.Patrol(cmd.x, cmd.z, cmd.targetClasses, cmd.allowCapture, cmd.queued)
);
const ents = data.entities.length;
const uais = GetFormationUnitAIs(data.entities, player, cmd, data.formation);
if (uais.length === 1 || uais.length !== ents)
uais.forEach(cmpUnitAI => {
cmpUnitAI.Patrol(cmd.x, cmd.z, cmd.targetClasses, cmd.allowCapture, cmd.queued)
});
else {
const positions = Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).DistributeAround(data.entities, cmd.x, cmd.z);
uais.forEach((cmpUnitAI, index) => {
cmpUnitAI.Patrol(positions[index].x, positions[index].y, cmd.targetClasses, cmd.allowCapture, cmd.queued)
});
}
},
"heal": function(player, cmd, data)
@@ -1,4 +1,4 @@
/* Copyright (C) 2024 Wildfire Games.
/* Copyright (C) 2025 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -27,6 +27,7 @@
#include "simulation2/MessageTypes.h"
#include "simulation2/components/ICmpObstruction.h"
#include "simulation2/components/ICmpObstructionManager.h"
#include "simulation2/components/ICmpPosition.h"
#include "simulation2/components/ICmpTerrain.h"
#include "simulation2/components/ICmpWaterManager.h"
#include "simulation2/helpers/HierarchicalPathfinder.h"
@@ -857,6 +858,88 @@ bool CCmpPathfinder::IsGoalReachable(entity_pos_t x0, entity_pos_t z0, const Pat
return m_PathfinderHier->IsGoalReachable(i, j, goal, passClass);
}
std::vector<CFixedVector2D> CCmpPathfinder::DistributeAround(std::vector<entity_id_t> units, entity_pos_t x, entity_pos_t z) const
{
PROFILE2("DistributeAround");
std::vector<CFixedVector2D> positions;
if (units.empty())
return positions;
positions.reserve(units.size());
// Initialize spiral parameters
fixed angle = fixed::Zero();
fixed radius = fixed::FromInt(1);
const fixed increment = fixed::FromInt(7) / 4;
// Calculate the angle step so that at this radius we don't crowd the units
fixed angleStep = fixed::FromInt(9).MulDiv(fixed::FromInt(1), fixed::Pi().Multiply(radius));
for (size_t i = 0; i < units.size(); ++i)
{
CFixedVector2D offset(radius, fixed::Zero());
offset = offset.Rotate(angle);
positions.emplace_back(x + offset.X, z + offset.Y);
angle += angleStep;
// If we complete a circle, increase radius and recalculate angle step
if (angle >= fixed::Pi().Multiply(fixed::FromInt(2)))
{
angle = fixed::Zero();
radius += increment;
angleStep = fixed::FromInt(9).MulDiv(fixed::FromInt(1), fixed::Pi().Multiply(radius));
}
}
// Get current unit positions to calculate travel distances
std::vector<CFixedVector2D> unitPositions;
unitPositions.reserve(units.size());
CmpPtr<ICmpPosition> cmpPosition(GetSystemEntity());
for (entity_id_t unit : units)
{
CmpPtr<ICmpPosition> unitPos(GetSimContext(), unit);
if (!unitPos || !unitPos->IsInWorld())
return positions;
CFixedVector2D pos(unitPos->GetPosition2D());
unitPositions.push_back(pos);
}
// Optimize positions by swapping them if it reduces total travel distance.
bool improved;
do
{
improved = false;
for (size_t i = 0; i < positions.size(); ++i)
{
// Helper to compute squared distance between two points as integers
auto distSq = [](const CFixedVector2D& p1, const CFixedVector2D& p2) -> u32 {
i32 dx = (p1.X - p2.X).ToInt_RoundToInfinity();
i32 dy = (p1.Y - p2.Y).ToInt_RoundToInfinity();
return dx*dx + dy*dy;
};
for (size_t j = i + 1; j < positions.size(); ++j)
{
u32 currentDistSq = distSq(positions[i], unitPositions[i]) + distSq(positions[j], unitPositions[j]);
u32 swappedDistSq = distSq(positions[j], unitPositions[i]) + distSq(positions[i], unitPositions[j]);
// Swap if it reduces total squared distance
if (swappedDistSq < currentDistSq)
{
std::swap(positions[i], positions[j]);
improved = true;
}
}
}
} while (improved);
return positions;
}
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
@@ -1,4 +1,4 @@
/* Copyright (C) 2022 Wildfire Games.
/* Copyright (C) 2025 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -225,6 +225,8 @@ public:
void SetAtlasOverlay(bool enable, pass_class_t passClass = 0) override;
std::vector<CFixedVector2D> DistributeAround(std::vector<entity_id_t> units, entity_pos_t x, entity_pos_t z) const override;
bool 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 override;
ICmpObstruction::EFoundationCheck CheckUnitPlacement(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r, pass_class_t passClass, bool onlyCenterPoint) const override;
@@ -1,4 +1,4 @@
/* Copyright (C) 2022 Wildfire Games.
/* Copyright (C) 2025 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -27,4 +27,5 @@ DEFINE_INTERFACE_METHOD("SetHierDebugOverlay", ICmpPathfinder, SetHierDebugOverl
DEFINE_INTERFACE_METHOD("GetClearance", ICmpPathfinder, GetClearance)
DEFINE_INTERFACE_METHOD("GetPassabilityClass", ICmpPathfinder, GetPassabilityClass)
DEFINE_INTERFACE_METHOD("UpdateGrid", ICmpPathfinder, UpdateGrid)
DEFINE_INTERFACE_METHOD("DistributeAround", ICmpPathfinder, DistributeAround)
END_INTERFACE_WRAPPER(Pathfinder)
@@ -1,4 +1,4 @@
/* Copyright (C) 2021 Wildfire Games.
/* Copyright (C) 2025 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -130,6 +130,11 @@ public:
*/
virtual void SetDebugPath(entity_pos_t x0, entity_pos_t z0, const PathGoal& goal, pass_class_t passClass) = 0;
/**
* Distribute units around a point, returning a list of positions.
*/
virtual std::vector<CFixedVector2D> DistributeAround(std::vector<entity_id_t> units, entity_pos_t x, entity_pos_t z) const = 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.
@@ -1,4 +1,4 @@
/* Copyright (C) 2024 Wildfire Games.
/* Copyright (C) 2025 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -54,6 +54,7 @@ public:
virtual WaypointPath ComputeShortPathImmediate(const ShortPathRequest&) const override { return WaypointPath(); }
virtual void SetDebugPath(entity_pos_t, entity_pos_t, const PathGoal&, pass_class_t) override {}
virtual bool IsGoalReachable(entity_pos_t, entity_pos_t, const PathGoal&, pass_class_t) override { return false; }
virtual std::vector<CFixedVector2D> DistributeAround(std::vector<entity_id_t>, entity_pos_t, entity_pos_t) const override { return {}; }
virtual bool CheckMovement(const IObstructionTestFilter&, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t, pass_class_t) const override { return false; }
virtual ICmpObstruction::EFoundationCheck CheckUnitPlacement(const IObstructionTestFilter&, entity_pos_t, entity_pos_t, entity_pos_t, pass_class_t, bool = false) const override { return ICmpObstruction::FOUNDATION_CHECK_SUCCESS; }
virtual ICmpObstruction::EFoundationCheck CheckBuildingPlacement(const IObstructionTestFilter&, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t, entity_id_t, pass_class_t) const override { return ICmpObstruction::FOUNDATION_CHECK_SUCCESS; }