# New dynamic territories design

This was SVN commit r9906.
This commit is contained in:
Ykkrosh 2011-07-24 11:42:35 +00:00
parent 78fd3a2ba2
commit 955f94976e
8 changed files with 210 additions and 110 deletions

View File

@ -28,6 +28,8 @@
<Actor>structures/hellenes/wall_med.xml</Actor>
</VisualActor>
<TerritoryInfluence>
<OverrideCost>100</OverrideCost>
<OverrideCost>64</OverrideCost>
<Weight>0</Weight>
<Radius>0</Radius>
</TerritoryInfluence>
</Entity>

View File

@ -29,5 +29,7 @@
</VisualActor>
<TerritoryInfluence>
<OverrideCost>0</OverrideCost>
<Weight>0</Weight>
<Radius>0</Radius>
</TerritoryInfluence>
</Entity>

View File

@ -67,4 +67,8 @@
units/{civ}_support_female_citizen
</Entities>
</TrainingQueue>
<TerritoryInfluence>
<Radius>256</Radius>
<Weight>65536</Weight>
</TerritoryInfluence>
</Entity>

View File

@ -54,4 +54,8 @@
units/{civ}_support_female_citizen
</Entities>
</TrainingQueue>
<TerritoryInfluence>
<Radius>64</Radius>
<Weight>65536</Weight>
</TerritoryInfluence>
</Entity>

View File

@ -29,19 +29,37 @@ public:
DEFAULT_COMPONENT_ALLOCATOR(TerritoryInfluence)
u8 m_Cost;
int m_Cost;
u32 m_Weight;
u32 m_Radius;
static std::string GetSchema()
{
return
"<element name='OverrideCost'>"
"<optional>"
"<element name='OverrideCost'>"
"<data type='nonNegativeInteger'>"
"<param name='maxInclusive'>255</param>"
"</data>"
"</element>"
"</optional>"
"<element name='Weight'>"
"<data type='nonNegativeInteger'/>"
"</element>"
"<element name='Radius'>"
"<data type='nonNegativeInteger'/>"
"</element>";
}
virtual void Init(const CParamNode& paramNode)
{
m_Cost = paramNode.GetChild("OverrideCost").ToInt();
if (paramNode.GetChild("OverrideCost").IsOk())
m_Cost = paramNode.GetChild("OverrideCost").ToInt();
else
m_Cost = -1;
m_Weight = paramNode.GetChild("Weight").ToInt();
m_Radius = paramNode.GetChild("Radius").ToInt();
}
virtual void Deinit()
@ -57,11 +75,20 @@ public:
Init(paramNode);
}
virtual u8 GetCost()
virtual int GetCost()
{
return m_Cost;
}
virtual u32 GetWeight()
{
return m_Weight;
}
virtual u32 GetRadius()
{
return m_Radius;
}
};
REGISTER_COMPONENT_TYPE(TerritoryInfluence)

View File

@ -27,6 +27,7 @@
#include "simulation2/MessageTypes.h"
#include "simulation2/components/ICmpObstruction.h"
#include "simulation2/components/ICmpObstructionManager.h"
#include "simulation2/components/ICmpOwnership.h"
#include "simulation2/components/ICmpPathfinder.h"
#include "simulation2/components/ICmpPosition.h"
#include "simulation2/components/ICmpSettlement.h"
@ -167,48 +168,72 @@ public:
* a TerritoryInfluence component. Grid cells are 0 if no influence,
* or 1+c if the influence have cost c (assumed between 0 and 254).
*/
void RasteriseInfluences(Grid<u8>& grid);
void RasteriseInfluences(CComponentManager::InterfaceList& infls, Grid<u8>& grid);
};
REGISTER_COMPONENT_TYPE(TerritoryManager)
/*
We compute territory influences with a kind of best-first search:
1) Initialise an 'open' list with tiles that contain settlements (annotated with
territory ID) with initial cost 0
2) Pick the lowest cost tile from 'item'
3) For each neighbour which has not already been assigned to a territory,
assign it to this territory and compute its new cost (effectively the
distance from the associated settlement) and add to 'open'
4) Go to 2 until 'open' is empty
We compute the territory influence of an entity with a kind of best-first search,
storing an 'open' list of tiles that have not yet been processed,
then taking the highest-weight tile (closest to origin) and updating the weight
of extending to each neighbour (based on radius-determining 'falloff' value,
adjusted by terrain movement cost), and repeating until all tiles are processed.
*/
typedef PriorityQueueHeap<std::pair<u16, u16>, u32> OpenQueue;
typedef PriorityQueueHeap<std::pair<u16, u16>, u32, std::greater<u32> > OpenQueue;
static void ProcessNeighbour(u8 pid, u16 i, u16 j, u32 pg, bool diagonal,
Grid<u8>& grid, OpenQueue& queue, const Grid<u8>& influenceGrid)
static void ProcessNeighbour(u32 falloff, u16 i, u16 j, u32 pg, bool diagonal,
Grid<u32>& grid, OpenQueue& queue, const Grid<u8>& costGrid)
{
// Ignore tiles that are already claimed
u8 id = grid.get(i, j);
if (id)
u32 dg = falloff * costGrid.get(i, j);
if (diagonal)
dg = (dg * 362) / 256;
// Stop if new cost g=pg-dg is not better than previous value for that tile
// (arranged to avoid underflow if pg < dg)
if (pg <= grid.get(i, j) + dg)
return;
// Base cost for moving onto this tile
u32 dg = diagonal ? 362 : 256;
u32 g = pg - dg; // cost to this tile = cost to predecessor - falloff from predecessor
// Adjust cost based on this tile's influences
dg *= influenceGrid.get(i, j);
u32 g = pg + dg; // cost to this tile = cost to predecessor + delta from predecessor
grid.set(i, j, pid);
grid.set(i, j, g);
OpenQueue::Item tile = { std::make_pair(i, j), g };
queue.push(tile);
}
static void FloodFill(Grid<u32>& grid, Grid<u8>& costGrid, OpenQueue& openTiles, u32 falloff)
{
u32 tilesW = grid.m_W;
u32 tilesH = grid.m_H;
while (!openTiles.empty())
{
OpenQueue::Item tile = openTiles.pop();
// Process neighbours (if they're not off the edge of the map)
u16 x = tile.id.first;
u16 z = tile.id.second;
if (x > 0)
ProcessNeighbour(falloff, x-1, z, tile.rank, false, grid, openTiles, costGrid);
if (x < tilesW-1)
ProcessNeighbour(falloff, x+1, z, tile.rank, false, grid, openTiles, costGrid);
if (z > 0)
ProcessNeighbour(falloff, x, z-1, tile.rank, false, grid, openTiles, costGrid);
if (z < tilesH-1)
ProcessNeighbour(falloff, x, z+1, tile.rank, false, grid, openTiles, costGrid);
if (x > 0 && z > 0)
ProcessNeighbour(falloff, x-1, z-1, tile.rank, true, grid, openTiles, costGrid);
if (x > 0 && z < tilesH-1)
ProcessNeighbour(falloff, x-1, z+1, tile.rank, true, grid, openTiles, costGrid);
if (x < tilesW-1 && z > 0)
ProcessNeighbour(falloff, x+1, z-1, tile.rank, true, grid, openTiles, costGrid);
if (x < tilesW-1 && z < tilesH-1)
ProcessNeighbour(falloff, x+1, z+1, tile.rank, true, grid, openTiles, costGrid);
}
}
void CCmpTerritoryManager::CalculateTerritories()
{
PROFILE("CalculateTerritories");
@ -220,98 +245,124 @@ void CCmpTerritoryManager::CalculateTerritories()
uint32_t tilesW = cmpTerrain->GetVerticesPerSide() - 1;
uint32_t tilesH = cmpTerrain->GetVerticesPerSide() - 1;
Grid<u8> influenceGrid(tilesW, tilesH);
RasteriseInfluences(influenceGrid);
SAFE_DELETE(m_Territories);
m_Territories = new Grid<u8>(tilesW, tilesH);
// Compute terrain-passability-dependent costs per tile
Grid<u8> influenceGrid(tilesW, tilesH);
CmpPtr<ICmpPathfinder> cmpPathfinder(GetSimContext(), SYSTEM_ENTITY);
ICmpPathfinder::pass_class_t passClass = cmpPathfinder->GetPassabilityClass("default");
const Grid<u16>& passGrid = cmpPathfinder->GetPassabilityGrid();
// Adjust influenceGrid so it contains terrain-passability-dependent costs,
// unless overridden by existing values in influenceGrid
for (u32 j = 0; j < tilesH; ++j)
{
for (u32 i = 0; i < tilesW; ++i)
{
u8 cost;
u8 inflCost = influenceGrid.get(i, j);
if (inflCost)
{
cost = inflCost-1; // undo RasteriseInfluences's offset
}
if (passGrid.get(i, j) & passClass)
cost = 4; // TODO: should come from some XML file
else
{
if (passGrid.get(i, j) & passClass)
cost = 100;
else
cost = 1;
}
cost = 1;
influenceGrid.set(i, j, cost);
}
}
OpenQueue openTiles;
// Find all territory influence entities
CComponentManager::InterfaceList influences = GetSimContext().GetComponentManager().GetEntitiesWithInterface(IID_TerritoryInfluence);
// Initialise open list with all settlements
// Allow influence entities to override the terrain costs
RasteriseInfluences(influences, influenceGrid);
CComponentManager::InterfaceList settlements = GetSimContext().GetComponentManager().GetEntitiesWithInterface(IID_Settlement);
u8 id = 1;
for (CComponentManager::InterfaceList::iterator it = settlements.begin(); it != settlements.end(); ++it)
// Split influence entities into per-player lists, ignoring any with invalid properties
std::map<player_id_t, std::vector<entity_id_t> > influenceEntities;
for (CComponentManager::InterfaceList::iterator it = influences.begin(); it != influences.end(); ++it)
{
entity_id_t settlement = it->first;
CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), settlement);
// Ignore any with no weight or radius (to avoid divide-by-zero later)
ICmpTerritoryInfluence* cmpTerritoryInfluence = static_cast<ICmpTerritoryInfluence*>(it->second);
if (cmpTerritoryInfluence->GetWeight() == 0 || cmpTerritoryInfluence->GetRadius() == 0)
continue;
CmpPtr<ICmpOwnership> cmpOwnership(GetSimContext(), it->first);
if (cmpOwnership.null())
continue;
// Ignore Gaia and unassigned
player_id_t owner = cmpOwnership->GetOwner();
if (owner <= 0)
continue;
// Ignore if invalid position
CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), it->first);
if (cmpPosition.null() || !cmpPosition->IsInWorld())
continue;
// TODO: maybe we need to ignore settlements with owner -1,
// since they're probably destroyed
CFixedVector2D pos = cmpPosition->GetPosition2D();
int i = clamp((pos.X / (int)CELL_SIZE).ToInt_RoundToNegInfinity(), 0, (int)tilesW-1);
int j = clamp((pos.Y / (int)CELL_SIZE).ToInt_RoundToNegInfinity(), 0, (int)tilesH-1);
// Must avoid duplicates in the priority queue; ignore the settlement
// if there's already one on that tile
if (!m_Territories->get(i, j))
{
m_Territories->set(i, j, id);
OpenQueue::Item tile = { std::make_pair((u16)i, (i16)j), 0 };
openTiles.push(tile);
}
id += 1;
influenceEntities[owner].push_back(it->first);
}
while (!openTiles.empty())
// For each player, store the sum of influences on each tile
std::vector<std::pair<player_id_t, Grid<u32> > > playerGrids;
// TODO: this is a large waste of memory; we don't really need to store
// all the intermediate grids
for (std::map<player_id_t, std::vector<entity_id_t> >::iterator it = influenceEntities.begin(); it != influenceEntities.end(); ++it)
{
OpenQueue::Item tile = openTiles.pop();
Grid<u32> playerGrid(tilesW, tilesH);
// Get current tile's territory ID
u8 tid = m_Territories->get(tile.id.first, tile.id.second);
std::vector<entity_id_t>& ents = it->second;
for (std::vector<entity_id_t>::iterator eit = ents.begin(); eit != ents.end(); ++eit)
{
// Compute the influence map of the current entity, then add it to the player grid
// Process neighbours (if they're not off the edge of the map)
u16 x = tile.id.first;
u16 z = tile.id.second;
if (x > 0)
ProcessNeighbour(tid, x-1, z, tile.rank, false, *m_Territories, openTiles, influenceGrid);
if (x < tilesW-1)
ProcessNeighbour(tid, x+1, z, tile.rank, false, *m_Territories, openTiles, influenceGrid);
if (z > 0)
ProcessNeighbour(tid, x, z-1, tile.rank, false, *m_Territories, openTiles, influenceGrid);
if (z < tilesH-1)
ProcessNeighbour(tid, x, z+1, tile.rank, false, *m_Territories, openTiles, influenceGrid);
if (x > 0 && z > 0)
ProcessNeighbour(tid, x-1, z-1, tile.rank, true, *m_Territories, openTiles, influenceGrid);
if (x > 0 && z < tilesH-1)
ProcessNeighbour(tid, x-1, z+1, tile.rank, true, *m_Territories, openTiles, influenceGrid);
if (x < tilesW-1 && z > 0)
ProcessNeighbour(tid, x+1, z-1, tile.rank, true, *m_Territories, openTiles, influenceGrid);
if (x < tilesW-1 && z < tilesH-1)
ProcessNeighbour(tid, x+1, z+1, tile.rank, true, *m_Territories, openTiles, influenceGrid);
Grid<u32> entityGrid(tilesW, tilesH);
CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), *eit);
CFixedVector2D pos = cmpPosition->GetPosition2D();
int i = clamp((pos.X / (int)CELL_SIZE).ToInt_RoundToNegInfinity(), 0, (int)tilesW-1);
int j = clamp((pos.Y / (int)CELL_SIZE).ToInt_RoundToNegInfinity(), 0, (int)tilesH-1);
CmpPtr<ICmpTerritoryInfluence> cmpTerritoryInfluence(GetSimContext(), *eit);
u32 weight = cmpTerritoryInfluence->GetWeight();
u32 radius = cmpTerritoryInfluence->GetRadius() / CELL_SIZE;
u32 falloff = weight / radius; // earlier check for GetRadius() == 0 prevents divide-by-zero
// TODO: we should have some maximum value on weight, to avoid overflow
// when doing all the sums
// Initialise the tile under the entity
entityGrid.set(i, j, weight);
OpenQueue openTiles;
OpenQueue::Item tile = { std::make_pair((u16)i, (i16)j), weight };
openTiles.push(tile);
// Expand influences outwards
FloodFill(entityGrid, influenceGrid, openTiles, falloff);
// TODO: we should do a sparse grid and only add the non-zero regions, for performance
for (u16 j = 0; j < entityGrid.m_H; ++j)
for (u16 i = 0; i < entityGrid.m_W; ++i)
playerGrid.set(i, j, playerGrid.get(i, j) + entityGrid.get(i, j));
}
playerGrids.push_back(std::make_pair(it->first, playerGrid));
}
// Set m_Territories to the player ID with the highest influence for each tile
for (u16 j = 0; j < tilesH; ++j)
{
for (u16 i = 0; i < tilesW; ++i)
{
u32 bestWeight = 0;
for (size_t k = 0; k < playerGrids.size(); ++k)
{
u32 w = playerGrids[k].second.get(i, j);
if (w > bestWeight)
{
player_id_t id = playerGrids[k].first;
m_Territories->set(i, j, (u8)id);
bestWeight = w;
}
}
}
}
}
@ -336,13 +387,16 @@ static void TileCenter(u16 i, u16 j, entity_pos_t& x, entity_pos_t& z)
// TODO: would be nice not to duplicate those two functions from CCmpObstructionManager.cpp
void CCmpTerritoryManager::RasteriseInfluences(Grid<u8>& grid)
void CCmpTerritoryManager::RasteriseInfluences(CComponentManager::InterfaceList& infls, Grid<u8>& grid)
{
CComponentManager::InterfaceList infls = GetSimContext().GetComponentManager().GetEntitiesWithInterface(IID_TerritoryInfluence);
for (CComponentManager::InterfaceList::iterator it = infls.begin(); it != infls.end(); ++it)
{
ICmpTerritoryInfluence* cmpTerritoryInfluence = static_cast<ICmpTerritoryInfluence*>(it->second);
int cost = cmpTerritoryInfluence->GetCost();
if (cost == -1)
continue;
CmpPtr<ICmpObstruction> cmpObstruction(GetSimContext(), it->first);
if (cmpObstruction.null())
continue;
@ -351,8 +405,6 @@ void CCmpTerritoryManager::RasteriseInfluences(Grid<u8>& grid)
if (!cmpObstruction->GetObstructionSquare(square))
continue;
u8 cost = cmpTerritoryInfluence->GetCost();
CFixedVector2D halfSize(square.hw, square.hh);
CFixedVector2D halfBound = Geometry::GetHalfBoundingBox(square.u, square.v, halfSize);
@ -366,7 +418,7 @@ void CCmpTerritoryManager::RasteriseInfluences(Grid<u8>& grid)
entity_pos_t x, z;
TileCenter(i, j, x, z);
if (Geometry::PointIsInSquare(CFixedVector2D(x - square.x, z - square.z), square.u, square.v, halfSize))
grid.set(i, j, cost+1);
grid.set(i, j, cost);
}
}

View File

@ -23,7 +23,16 @@
class ICmpTerritoryInfluence : public IComponent
{
public:
virtual u8 GetCost() = 0;
/**
* Returns either -1 to indicate no special terrain cost, or a value
* in [0, 255] to indicate overriding the normal cost of the terrain
* under the entity's obstruction.
*/
virtual int GetCost() = 0;
virtual u32 GetWeight() = 0;
virtual u32 GetRadius() = 0;
DECLARE_INTERFACE_TYPE(TerritoryInfluence)
};

View File

@ -29,14 +29,14 @@
#define PRIORITYQUEUE_DEBUG 1
#endif
template <typename Item>
template <typename Item, typename CMP>
struct QueueItemPriority
{
bool operator()(const Item& a, const Item& b)
{
if (a.rank > b.rank) // higher costs are lower priority
if (CMP()(b.rank, a.rank)) // higher costs are lower priority
return true;
if (a.rank < b.rank)
if (CMP()(a.rank, b.rank))
return false;
// Need to tie-break to get a consistent ordering
// TODO: Should probably tie-break on g or h or something, but don't bother for now
@ -57,7 +57,7 @@ struct QueueItemPriority
* This is quite dreadfully slow in MSVC's debug STL implementation,
* so we shouldn't use it unless we reimplement the heap functions more efficiently.
*/
template <typename ID, typename R>
template <typename ID, typename R, typename CMP = std::less<R> >
class PriorityQueueHeap
{
public:
@ -70,7 +70,7 @@ public:
void push(const Item& item)
{
m_Heap.push_back(item);
push_heap(m_Heap.begin(), m_Heap.end(), QueueItemPriority<Item>());
push_heap(m_Heap.begin(), m_Heap.end(), QueueItemPriority<Item, CMP>());
}
Item* find(ID id)
@ -93,7 +93,7 @@ public:
ENSURE(m_Heap[n].rank > newrank);
#endif
m_Heap[n].rank = newrank;
push_heap(m_Heap.begin(), m_Heap.begin()+n+1, QueueItemPriority<Item>());
push_heap(m_Heap.begin(), m_Heap.begin()+n+1, QueueItemPriority<Item, CMP>());
return;
}
}
@ -105,7 +105,7 @@ public:
ENSURE(m_Heap.size());
#endif
Item r = m_Heap.front();
pop_heap(m_Heap.begin(), m_Heap.end(), QueueItemPriority<Item>());
pop_heap(m_Heap.begin(), m_Heap.end(), QueueItemPriority<Item, CMP>());
m_Heap.pop_back();
return r;
}
@ -130,7 +130,7 @@ public:
* It seems fractionally slower than a binary heap in optimised builds, but is
* much simpler and less susceptible to MSVC's painfully slow debug STL.
*/
template <typename ID, typename R>
template <typename ID, typename R, typename CMP = std::less<R> >
class PriorityQueueList
{
public:
@ -171,7 +171,7 @@ public:
size_t bestidx = m_List.size()-1;
for (ssize_t i = (ssize_t)bestidx-1; i >= 0; --i)
{
if (QueueItemPriority<Item>()(best, m_List[i]))
if (QueueItemPriority<Item, CMP>()(best, m_List[i]))
{
bestidx = i;
best = m_List[i];