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
@@ -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