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:
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user