Partition
Effectively locate objects by storing them in a data structure organized by their positions.
You’ll want to separate out objects on a notion of time and space. A dead object shouldn’t be talking, and you shouldn’t hear an object far away talking either. If you have to iterate through every object to check its location, it can become a performance bottleneck.
Let’s say we’re making an RTS. opposing armies with hundreds of units will clash on the field of battle. The naive way to handle this is by looking at every pair of units and seeing how close they are to each other:
void handleMelee(Unit* units[], int numUnits) {
for (int a = 0; a < numUnits - 1; a++) {
for (int b = a + 1; b < numUnits; b++) {
if (units[a]->position() == units[b]->position()) {
(units[a], units[b]);
handleAttack}
}
}
}
This is an O(n^2) algorithm, which quickly becomes too expensive with a large amount of units.
The problem that we’re running into is that there’s no underlying order to the array of units. Instead of having a 2D battlefield, imagine that it is a 1D battle line.
Let’s try to make a 2D battlefield sortable in a 1D field.
For a set of objects, each has a position in space. Store them in a spatial data structure that organizes the objects by their positions. This data structure lets you efficiently query for objects at or near a location. When an object’s position changes, update the spatial data structure so that it can continue to find the object.
This is commonly used for storing live, moving game objects. Use it when you are querying for objects by location and your performance is suffering.
Spatial partitions knock an O(n) or O(n^2) operation down to something more manageable. The more objects you have, the more valuable that becomes.
Since this pattern involves organizing objects by their positions, objects that change position are harder to deal with.
A spatial partition uses additional memory for its bookkeeping data structures. It trades memory for speed. If you can’t spare the memory, this pattern won’t help you.
This pattern’s implementation varies: let’s start off with a fixed grid.
Imagine the entire field of battle as a matrix. Units that are close enough to fight are in the same grid. We put all units that are adjacent to each other in the same cell in the matrix.
Let’s implement it.
Let’s get coding.
class Unit {
friend class Grid;
public:
(Grid* grid, double x, double y)
Unit: grid_(grid),
x_(x),
y_(y)
{}
void move(double x, double y);
private:
double x_, y_;
* grid_;
Grid};
Each unit has a position in 2D and a pointer to the grid that it lives on.
here’s the grid:
class Grid {
public:
() {
Grid// Clear the grid.
for (int x = 0; x < NUM_CELLS; x++) {
for (int y = 0; y < NUM_CELLS; y++) {
cells_[x][y] = NULL;
}
}
}
static const int NUM_CELLS = 10;
static const int CELL_SIZE = 20;
private:
* cells_[NUM_CELLS][NUM_CELLS];
Unit};
Now, since every unit should be a linked list, we’ll extend unit with
next
and prev
pointers.
class Unit {
// Previous code...
private:
* prev_;
Unit* next_;
Unit};
Next, we’ll want to make sure that new units are placed in the grid on construction:
::Unit(Grid* grid, double x, double y)
Unit: grid_(grid),
x_(x),
y_(y),
prev_(NULL),
next_(NULL) {
grid_->add(this);
}
With the add()
method defined like so:
void Grid::add(Unit* unit) {
// Determine which grid cell it's in.
int cellX = (int)(unit->x_ / Grid::CELL_SIZE);
int cellY = (int)(unit->y_ / Grid::CELL_SIZE);
// Add to the front of list for the cell it's in.
->prev_ = NULL;
unit->next_ = cells_[cellX][cellY];
unitcells_[cellX][cellY] = unit;
if (unit->next_ != NULL) {
->next_->prev_ = unit;
unit}
}
Once all of the units are nestled in their cells, let’s have them fight:
void Grid::handleMelee() {
for (int x = 0; x < NUM_CELLS; x++) {
for (int y = 0; y < NUM_CELLS; y++) {
(cells_[x][y]);
handleCell}
}
}
void Grid::handleCell(Unit* unit) {
while (unit != NULL) {
* other = unit->next_;
Unitwhile (other != NULL) {
if (unit->x_ == other->x_ &&
->y_ == other->y_) {
unit(unit, other);
handleAttack}
= other->next_;
other }
= unit->next_;
unit }
}
Aside from having to traverse a linked list, we’ve partitioned the battlefield into little isolated skirmishes.
We have no code for moving a unit between cells. Let’s fix that:
Let’s define a method for that:
void Unit::move(double x, double y) {
grid_->move(this, x, y);
}
And then add it to the Grid.
void Grid::move(Unit* unit, double x, double y) {
// See which cell it was in.
int oldCellX = (int)(unit->x_ / Grid::CELL_SIZE);
int oldCellY = (int)(unit->y_ / Grid::CELL_SIZE);
// See which cell it's moving to.
int cellX = (int)(x / Grid::CELL_SIZE);
int cellY = (int)(y / Grid::CELL_SIZE);
->x_ = x;
unit->y_ = y;
unit
// If it didn't change cells, we're done.
if (oldCellX == cellX && oldCellY == cellY) return;
// Unlink it from the list of its old cell.
if (unit->prev_ != NULL) {
->prev_->next_ = unit->next_;
unit}
if (unit->next_ != NULL) {
->next_->prev_ = unit->prev_;
unit}
// If it's the head of a list, remove it.
if (cells_[oldCellX][oldCellY] == unit) {
cells_[oldCellX][oldCellY] = unit->next_;
}
// Add it back to the grid at its new cell.
(unit);
add}
If we move but don’t cross a cell boundary, we return there. Otherwise, we unlink the node from its current location and then add it back to the grid at its new cell.
This works, but only for units with a melee range. What happens for ranged units?
Let’s handle ranged units like so:
if (distance(unit, other) < ATTACK_DISTANCE) {
(unit, other);
handleAttack}
void Grid::handleUnit(Unit* unit, Unit* other) {
while (other != NULL) {
if (distance(unit, other) < ATTACK_DISTANCE) {
(unit, other);
handleAttack}
= other->next_;
other }
}
Then have the grid handle units that are in the cell:
void Grid::handleCell(int x, int y) {
* unit = cells_[x][y];
Unitwhile (unit != NULL) {
// Handle other units in this cell.
(unit, unit->next_);
handleUnit
= unit->next_;
unit }
}
And neighboring cells:
void Grid::handleCell(int x, int y) {
* unit = cells_[x][y];
Unitwhile (unit != NULL) {
// Handle other units in this cell.
(unit, unit->next_);
handleUnit
// Also try the neighboring cells.
if (x > 0 && y > 0) handleUnit(unit, cells_[x - 1][y - 1]);
if (x > 0) handleUnit(unit, cells_[x - 1][y]);
if (y > 0) handleUnit(unit, cells_[x][y - 1]);
if (x > 0 && y < NUM_CELLS - 1) {
(unit, cells_[x - 1][y + 1]);
handleUnit}
= unit->next_;
unit }
}
We have to make sure to only handle half of the cells around each unit. If we handled all adjacent nodes, we would get a situation where a unit could attack one unit when scanning for nearby enemies, and when the nearby enemy scans, that unit would be attacked again.
We went through an example using a matrix, but quadtrees and Binary space partitions (BSPs) are commonly used in place of them.
If it’s flat
If it’s hierarchical
If the partitioning is object-independent
If the partitioning adapts to the set of objects
If the partitioning is object-independent, but hierarchy is object-dependent
You can store the object in the partition, or a secondary cache to make look-up faster.
If it is the only place objects are stored:
If there is another collection for the objects
Here are some of the common structures for Spatial partitions.
Prev: object-pool