Created
July 4, 2013 23:59
-
-
Save vittorioromeo/5930873 to your computer and use it in GitHub Desktop.
Philip Diffenderfer's grid spatial partitioning implementation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <cstdlib> | |
#include <cmath> | |
#include <iostream> | |
#ifdef _WIN32 | |
#include "sys\time.h" | |
#include <windows.h> | |
#else | |
#include "sys/time.h" | |
#endif | |
using namespace std; | |
namespace steerio | |
{ | |
/** | |
* An axis-aligned bounding box. | |
*/ | |
struct AABB { | |
float l, t, r, b; | |
AABB() { } | |
AABB(float l, float t, float r, float b) { | |
set(l, t, r, b); | |
} | |
void set(float left, float top, float right, float bottom) { | |
l = left; t = top; r = right; b = bottom; | |
} | |
bool intersects(const AABB &y) const { | |
return !(l >= y.r || r < y.l || t >= y.b || b < y.t); | |
} | |
bool contains(const AABB &y) const { | |
return !(y.l < l || y.r >= r || y.t < t || y.b >= b); | |
} | |
}; | |
/** | |
* A body in 2d space that has an axis-aligned bounding box, a set of groups | |
* it's in, can collide with, can resolve collisions with, and can be inert. | |
*/ | |
struct Body { | |
AABB aabb; | |
long groups = -1; | |
long collisionGroups = -1; | |
long resolveGroups = -1; | |
bool inert = false; | |
bool freed = false; | |
bool canCollideWith(const Body &b) const { | |
return (collisionGroups & b.groups); | |
} | |
bool canResolveWith(const Body &b) const { | |
return (resolveGroups & b.groups); | |
} | |
}; | |
/** | |
* A callback interface that listens for collisions from a BroadphaseHandler. | |
*/ | |
class BroadphaseCallback { | |
public: | |
/** | |
* A method called before the BroadphaseHandler starts looking for potential collisions. | |
*/ | |
virtual void onBroadphaseStart() = 0; | |
/** | |
* A method called for every collision detected by a BroadphaseHandler. | |
* | |
* @param a The current body. | |
* @param b The body that has collided with a. | |
* @param index The number of collisions found already. | |
* @param mutual True if b has already been notified of a collision with a. | |
*/ | |
virtual void onBroadphaseCollision(const Body *a, const Body *b, int index, bool mutual) = 0; | |
/** | |
* A method called when the BroadphaseHandler is finished looking for potential collisions. | |
* | |
* @param collisionCount The number of collisions detected. | |
*/ | |
virtual void onBroadphaseEnd(int collisionCount) = 0; | |
}; | |
/** | |
* An interface that you can add bodies to and check for collisions between bodies. | |
* Bodies added to a BroadphaseHandler will not be deleted from memory after the | |
* body becomes inert or the BroadphaseHandler is deallocated. Bodies can not be | |
* explicitly removed from the Handler, their inert flag must be set to true and | |
* after the next refresh they will be removed and are available for deletion. Never | |
* delete a body that has not been implicitly removed from a BroadphaseHandler. | |
*/ | |
class BroadphaseHandler { | |
public: | |
/** | |
* Adds a body to the BroadphaseHandler so that it can now collide with other bodies. | |
* Bodies can not be explicitly removed | |
* | |
* @param body The body to add to the BroadphaseHandler. | |
*/ | |
virtual void add(Body *body) = 0; | |
/** | |
* Refreshes the handler by removing any inert bodies. This should be called | |
* after every time a body is updated (and has had their AABB updated) in the | |
* event that the BroadphaseHandler implementation uses the body's AABB to | |
* acceleration collision detection. | |
* | |
* @return The number of live (not inert) bodies that exist in this BroadphaseHandler. | |
*/ | |
virtual int refresh() = 0; | |
/** | |
* Finds all potential collisions between all bodies in this BroadphaseHandler. The | |
* implementation is expected to never report a collision between two entities more | |
* than once. The AABBs of the bodies in this BroadphaseHandler are used to determine | |
* collision, the callback should perform a more detailed collision check if one is | |
* necessary. | |
* | |
* @param callback The callback interface to notify when the collision checking starts, | |
* has found a collision pair, and when it ends. | |
* @return The number of collisions handled. | |
*/ | |
virtual int handle(BroadphaseCallback *callback) = 0; | |
}; | |
/** | |
* A node for a singly-linked list. | |
*/ | |
template<class V> class LinkedNode { | |
public: | |
V *value; | |
LinkedNode *next; | |
LinkedNode() | |
{ | |
} | |
LinkedNode(V *value, LinkedNode *next) | |
{ | |
this->value = value; | |
this->next = next; | |
} | |
}; | |
/** | |
* A singly-linked list implementation that can have nodes added to it and can | |
* have nodes removed from it through a cleaning process. | |
*/ | |
template<class V> class LinkedList { | |
public: | |
LinkedNode<V> *head; | |
LinkedList() | |
{ | |
head = new LinkedNode<V>(); | |
} | |
~LinkedList() | |
{ | |
LinkedNode<V> *next = NULL; | |
while (head != NULL) { | |
next = head->next; | |
delete head; | |
head = next; | |
} | |
} | |
/** | |
* Adds a node to this LinkedList. The node must not already exist in another LinkedList. | |
* | |
* @param node The node to add to the LinkedList. | |
*/ | |
void add(LinkedNode<V> *node) | |
{ | |
node->next = head->next; | |
head->next = node; | |
} | |
/** | |
* Cleans this linked list removing any nodes determined dirty by the callback function | |
* and possibly deleting the node altogether if the callback function sets autoDelete | |
* to true. | |
* | |
* @param metadata A pointer to pass along to the isDirty callback. | |
* @param isDirty A callback function to invoke to determine if a given node is dirty | |
* and should be removed from this list, even possibly deleted forever. | |
* @return The number of clean nodes in this list, essentially the new size of the list. | |
*/ | |
int clean(void *metadata, bool (*isDirty)(LinkedNode<V> *node, LinkedList<V> *list, void *metadata, bool &autoDelete)) | |
{ | |
LinkedNode<V> *prev = head, *curr = head->next, *next = NULL; | |
int cleanCount = 0; | |
bool autoDelete; | |
while (curr != NULL) { | |
next = curr->next; | |
autoDelete = false; | |
if (isDirty(curr, this, metadata, autoDelete)) { | |
prev->next = next; | |
if (autoDelete) { | |
delete curr; | |
} | |
} else { | |
cleanCount++; | |
prev = curr; | |
} | |
curr = next; | |
} | |
return cleanCount; | |
} | |
}; | |
// GRID IMPLEMENTATION | |
/** | |
* A BroadphaseHandler implementation that is a grid of cells where every cell | |
* is a list of bodies that exist in that cell. A body exists in a cell if the | |
* top-left corner of the body's AABB lies within the cell's boundaries. This | |
* implementation maintains a separate list for all bodies outside the grid. | |
*/ | |
class BroadphaseGrid : public BroadphaseHandler { | |
public: | |
/** | |
* Allocates a BroadphaseHandler implementation that is a grid of cells where | |
* every cell is a list of bodies that exist in that cell. | |
* | |
* @param x The left side of the grid. | |
* @param y The top side of the grid. | |
* @param columns The number of cells there are across the grid on the x-axis. | |
* @param rows The number of cells there are down the grid on the y-axis. | |
* @param cellWidth The width of a cell. | |
* @param cellHeight The height of a cell. | |
*/ | |
BroadphaseGrid(float x, float y, int columns, int rows, float cellWidth, float cellHeight) | |
{ | |
this->boundary.set( x, y, x + ((columns + 1) * cellWidth), y + ((rows + 1) * cellHeight) ); | |
this->columns = columns; | |
this->rows = rows; | |
this->cellWidth = cellWidth; | |
this->cellHeight = cellHeight; | |
this->cells = new LinkedList<Body>*[rows]; | |
for (int cy = 0; cy < rows; cy++) { | |
this->cells[cy] = new LinkedList<Body>[columns]; | |
} | |
} | |
~BroadphaseGrid() | |
{ | |
delete [] *cells; | |
delete [] cells; | |
} | |
void add(Body *body) | |
{ | |
LinkedNode<Body> *node = new LinkedNode<Body>(body, NULL); | |
int cx, cy; | |
if (getCell(body->aabb, cx, cy)) { | |
cells[cy][cx].add(node); | |
} else { | |
outsiders.add(node); | |
} | |
} | |
int refresh() | |
{ | |
int alive = 0; | |
// Remove inert bodies. | |
for (int y = 0; y < rows; y++) { | |
for (int x = 0; x < columns; x++) { | |
alive += cells[y][x].clean(this, &BroadphaseGrid::cleanRefresh); | |
} | |
} | |
alive += outsiders.clean(this, &BroadphaseGrid::cleanRefresh); | |
// Relocate bodies to new cells | |
for (int y = 0; y < rows; y++) { | |
for (int x = 0; x < columns; x++) { | |
cells[y][x].clean(this, &BroadphaseGrid::cleanUpdate); | |
} | |
} | |
outsiders.clean(this, &BroadphaseGrid::cleanUpdate); | |
return alive; | |
} | |
int handle(BroadphaseCallback *callback) | |
{ | |
int collisionCount = 0; | |
callback->onBroadphaseStart(); | |
for (int y = 0; y < rows; y++) { | |
for (int x = 0; x < columns; x++) { | |
handleCollisionList( cells[y][x], x, y, collisionCount, callback ); | |
} | |
} | |
handleCollisionList( outsiders, -1, -1, collisionCount, callback ); | |
callback->onBroadphaseEnd(collisionCount); | |
return collisionCount; | |
} | |
private: | |
AABB boundary; | |
int rows, columns; | |
float cellWidth, cellHeight; | |
LinkedList<Body> **cells; | |
LinkedList<Body> outsiders; | |
/** | |
* A callback method to the LinkedList.clean method which will notify the list to | |
* remove the node and delete it if the body is inert. | |
* | |
* @param node The current node being cleaned. | |
* @param list The list being cleaned. | |
* @param autoDelete Set to true when the list should delete the current node from memory. | |
* @return True if the list should remove and delete the node, otherwise false. | |
*/ | |
static bool cleanRefresh(LinkedNode<Body> *node, LinkedList<Body> *list, void *metadata, bool &autoDelete) | |
{ | |
// Remove and delete node if the body is inert, mark it as freed. | |
return (node->value->freed = autoDelete = node->value->inert); | |
} | |
/** | |
* A callback method to the LinkedList.clean method which will notify the list to | |
* remove the node if the body no longer belongs in the given list. | |
* | |
* @param node The current node being cleaned. | |
* @param list The list being cleaned. | |
* @param autoDelete Set to true when the list should delete the current node from memory. | |
* @return True if the list should remove and delete the node, otherwise false. | |
*/ | |
static bool cleanUpdate(LinkedNode<Body> *node, LinkedList<Body> *list, void *metadata, bool &autoDelete) | |
{ | |
BroadphaseGrid *grid = (BroadphaseGrid*)metadata; | |
LinkedList<Body> *destination = list; | |
bool changed = false; | |
int cx = 0, cy = 0; | |
// If the body is inside the grid... | |
if (grid->getCell(node->value->aabb, cx, cy)) { | |
destination = &grid->cells[cy][cx]; | |
// If the body is outside the grid... | |
} else { | |
destination = &grid->outsiders; | |
} | |
if ((changed = (list != destination))) { | |
destination->add(node); | |
} | |
return changed; | |
} | |
/** | |
* Handles collisions for all bodies in the given list. This will check for collisions | |
* for every body in the list against all other bodies in the list as well as any bodies | |
* that exist in the cells the current body overlaps. | |
* | |
* @param list The list of Bodies to handle collisions for. | |
* @param skipX The column of a cell to skip. | |
* @param skipY The row of a cell to skip. | |
* @param collisionCount The counter used to keep track of number of collisions. | |
* @param callback The callback to notifiy when a collision is detected. | |
*/ | |
void handleCollisionList(const LinkedList<Body> &list, const int skipX, const int skipY, int &collisionCount, BroadphaseCallback *callback) const | |
{ | |
int sx, sy, ex, ey; | |
LinkedNode<Body> *curr = list.head->next; | |
while (curr != NULL) { | |
Body *body = curr->value; | |
// Handle collisions with this body and all other bodies in this list | |
handleCollision(body, curr, collisionCount, callback); | |
// Get the span of cells this body covers... | |
getCellSpan(body->aabb, sx, sy, ex, ey); | |
// For each cell this body covers, handle collisions between this body and all bodies in the cell. | |
if ((ex - sx) > 1 || (ey - sy) > 1) { | |
for (int cy = sy; cy < ey; cy++) { | |
for (int cx = sx; cx < ex; cx++) { | |
if (cx != skipX || cy != skipY) { | |
handleCollision(body, cells[cy][cx].head, collisionCount, callback); | |
} | |
} | |
} | |
} | |
curr = curr->next; | |
} | |
} | |
/** | |
* Handles collision checks between a and all bodies on a list starting with node. | |
* | |
* @param a The body to check for collisions. | |
* @param node The head of a linked chain of bodies to check for collisions against a. | |
* @param collisionCount The counter used to keep track of number of collisions. | |
* @param callback The callback to notifiy when a collision is detected. | |
*/ | |
void handleCollision(Body *a, LinkedNode<Body> *node, int &collisionCount, BroadphaseCallback *callback) const | |
{ | |
node = node->next; | |
while (node != NULL) { | |
// Avoid checking for collisions for inert bodies. | |
if (a->inert) { | |
return; | |
} | |
Body *b = node->value; | |
// Avoid checking for collisions for inert bodies. | |
if (b->inert) { | |
node = node->next; | |
continue; | |
} | |
// Based on their groups, determine if they are applicable for collision | |
bool applicableA = a->canCollideWith(*b); | |
bool applicableB = b->canCollideWith(*a); | |
// At least one needs to be... | |
if (applicableA | applicableB) { | |
// If they are intersecting... | |
if ( a->aabb.intersects( b->aabb ) ) { | |
// If they both can intersect with each other, make sure to | |
// let the callback know that it's a duplicate collision | |
// notification, it's just going the other way. | |
bool mutual = false; | |
// If A can collide with B, notify A of a collision. | |
if (applicableA) { | |
callback->onBroadphaseCollision(a, b, collisionCount, mutual); | |
mutual = true; | |
} | |
// If B can collide with A, notify B of a collision if it is not inert | |
if (applicableB && !a->inert && !b->inert) { | |
callback->onBroadphaseCollision(b, a, collisionCount, mutual); | |
} | |
collisionCount++; | |
} | |
} | |
node = node->next; | |
} | |
} | |
/** | |
* Given an AABB, set {cx,cy} to the cell that the AABB belongs in based on upper | |
* left corner. Returns false if the AABB is not in a cell. | |
* | |
* @param aabb The AABB to find a cell for. | |
* @param cx The column of the cell aabb exists in. | |
* @param cy The row of the cell aabb exists in. | |
* @return True if the AABB's top-left corner lies in a cell, otherwise false. | |
*/ | |
inline bool getCell(const AABB &aabb, int &cx, int &cy) const | |
{ | |
cx = floor((aabb.l - boundary.l) / cellWidth); | |
cy = floor((aabb.t - boundary.t) / cellHeight); | |
return isColumn(cx) && isRow(cy); | |
} | |
/** | |
* Given an AABB, set {sx,sy}->{ex,ey} to the span of cells that the AABB | |
* intersects with based on all four sides of the AABB, where the left and | |
* top are inclusive and the right and bottom are exclusive. | |
* | |
* @param aabb The AABB to find a span of cells for. | |
* @param sx The left-most cell the aabb intersects with (inclusive). | |
* @param sy The top-most cell the aabb intersects with (inclusive). | |
* @param ex The right-most cell the aabb intersects with (exclusive). | |
* @param ey The bottom-most cell the aabb intersects with (exclusive). | |
*/ | |
inline void getCellSpan(const AABB &aabb, int &sx, int &sy, int &ex, int &ey) const | |
{ | |
sx = clamp( (int)floor((aabb.l - boundary.l) / cellWidth), 0, columns); | |
sy = clamp( (int)floor((aabb.t - boundary.t) / cellHeight), 0, rows); | |
ex = clamp( (int)ceil((aabb.r - boundary.l) / cellWidth), 0, columns); | |
ey = clamp( (int)ceil((aabb.b - boundary.t) / cellHeight), 0, rows); | |
} | |
/** | |
* @return True if x is a valid column, otherwise false. | |
*/ | |
inline bool isColumn(const int x) const | |
{ | |
return (x >= 0 && x < columns); | |
} | |
/** | |
* @return True if y is a valid row, otherwise false. | |
*/ | |
inline bool isRow(const int y) const | |
{ | |
return (y >= 0 && y < rows); | |
} | |
/** | |
* Calculates the value for a given inclusive minimum and maximum bounds. | |
* | |
* @param p The value to claim between the min and the max. | |
* @param min The minimum value to return. | |
* @param max The maximum value to return. | |
* @return The clamped value. | |
*/ | |
inline int clamp(const int a, const int min, const int max) const | |
{ | |
return (a < min ? min : (a > max ? max : a)); | |
} | |
}; | |
} | |
class Timer { | |
private: | |
ulong frequency, time; | |
void getFrequency(ulong &frequency) { | |
#if defined _WIN32 || defined _WIN64 | |
LARGE_INTEGER windowsFrequency; | |
if (QueryPerformanceFrequency(&windowsFrequency)) { | |
frequency = windowsFrequency.QuadPart; | |
} else { | |
frequency = 1000; | |
} | |
#else | |
frequency = 1000000; | |
#endif | |
} | |
void getTime(ulong &time) { | |
#if defined _WIN32 || defined _WIN64 | |
LARGE_INTEGER windowsTime; | |
if (QueryPerformanceCounter(&windowsTime)) { | |
time = windowsTime.QuadPart; | |
} else { | |
time = GetTickCount(); | |
} | |
#else | |
struct timeval timex; | |
gettimeofday(&timex, NULL); | |
time = timex.tv_usec + (timex.tv_sec * 1000000); | |
#endif | |
} | |
public: | |
Timer() { | |
getFrequency(frequency); | |
} | |
void start() { | |
getTime(time); | |
} | |
float elapsed() { | |
ulong start = time; | |
getTime(time); | |
return (time - start) / (float)frequency; | |
} | |
ulong getTime() { | |
getTime(time); | |
return time; | |
} | |
ulong getFrequency() { | |
return frequency; | |
} | |
}; | |
using namespace steerio; | |
class Ball { | |
public: | |
Body body; | |
float x, y, r, vx, vy; | |
void update(float dt, const AABB &bounds) { | |
x += vx * dt; | |
y += vy * dt; | |
if (x + r > bounds.r) { | |
x = bounds.r - r; | |
vx = -vx; | |
} | |
if (x - r < bounds.l) { | |
x = bounds.l + r; | |
vx = -vx; | |
} | |
if (y + r > bounds.b) { | |
y = bounds.b - r; | |
vy = -vy; | |
} | |
if (y - r < bounds.t) { | |
y = bounds.t + r; | |
vy = -vy; | |
} | |
body.aabb.set(x - r, y - r, x + r, y + r); | |
} | |
}; | |
class BallCallback : public BroadphaseCallback { | |
public: | |
void onBroadphaseStart() {}; | |
void onBroadphaseCollision(const Body *a, const Body *b, int index, bool mutual) { | |
if (!mutual) { // we only care about the first collision between a and b (opposed to b and a). | |
// TODO notify of collision | |
if ((a->groups & b->resolveGroups) || (a->resolveGroups & b->groups)) { | |
// TODO resolve collision | |
} | |
} | |
}; | |
void onBroadphaseEnd(int collisionCount) {}; | |
}; | |
float randomFloat() { | |
return (float)rand() / (float)RAND_MAX; | |
} | |
float randomFloat(float min, float max) { | |
return (max - min) * randomFloat() + min; | |
} | |
long randomLong(long min, long max) { | |
return (long)((max - min + 1) * randomFloat() + min); | |
} | |
int randomInt(int min, int max) { | |
return (int)((max - min + 1) * randomFloat() + min); | |
} | |
int main(int argc, char **argv) | |
{ | |
const int BALL_COUNT = 10000; | |
const AABB WORLD_BOUNDS ( 0.0f, 0.0f, 12800.0f, 12800.0f ); | |
const float VELOCITY_MAX = 300.0f; | |
const float RADIUS_MIN = 0.5f; | |
const float RADIUS_MAX = 3.0f; | |
const float DELTATIME = 0.01f; | |
const int ITERATIONS = 1000; | |
const int GRID_ROWS = 64; | |
const int GRID_COLUMNS = 64; | |
const long COLLISION_MIN = 0; | |
const long COLLISION_MAX = 15; | |
const int STATIC_PERCENT = 5; | |
// Initialize balls | |
Ball balls[BALL_COUNT]; | |
for (int i = 0; i < BALL_COUNT; i++) { | |
Ball *b = &balls[i]; | |
b->r = randomFloat( RADIUS_MIN, RADIUS_MAX ); | |
b->x = randomFloat( WORLD_BOUNDS.l + b->r, WORLD_BOUNDS.r - b->r ); | |
b->y = randomFloat( WORLD_BOUNDS.t + b->r, WORLD_BOUNDS.b - b->r ); | |
if (randomInt(0, 100) < STATIC_PERCENT) { | |
b->vx = b->vy = 0.0f; | |
} else { | |
b->vx = randomFloat( -VELOCITY_MAX, VELOCITY_MAX ); | |
b->vy = randomFloat( -VELOCITY_MAX, VELOCITY_MAX ); | |
} | |
b->body.groups = randomLong( COLLISION_MIN, COLLISION_MAX ); | |
b->body.collisionGroups = randomLong( COLLISION_MIN, COLLISION_MAX ); | |
b->update( 0.0f, WORLD_BOUNDS ); | |
} | |
// Broadphase callback | |
BallCallback *callback = new BallCallback(); | |
// Broadphase handler | |
BroadphaseHandler *handler = new BroadphaseGrid(WORLD_BOUNDS.l, WORLD_BOUNDS.t, GRID_COLUMNS, GRID_ROWS, (WORLD_BOUNDS.r - WORLD_BOUNDS.l) / GRID_COLUMNS, (WORLD_BOUNDS.b - WORLD_BOUNDS.t) / GRID_ROWS); | |
for (int i = 0; i < BALL_COUNT; i++) { | |
handler->add( &(balls[i].body) ); | |
} | |
// Simulation | |
Timer refreshTimer, collisionTimer; | |
float refreshTotal = 0.0f; | |
float collisionTotal = 0.0f; | |
for (int i = 0; i < ITERATIONS; i++) { | |
for (int k = 0; k < BALL_COUNT; k++) { | |
balls[k].update( DELTATIME, WORLD_BOUNDS ); | |
} | |
refreshTimer.start(); | |
handler->refresh(); | |
refreshTotal += refreshTimer.elapsed(); | |
collisionTimer.start(); | |
handler->handle(callback); | |
collisionTotal += collisionTimer.elapsed(); | |
} | |
// Statistics | |
cout << "Refresh Average: " << (refreshTotal / ITERATIONS) << endl; | |
cout << "Collision Detection Average: " << (collisionTotal / ITERATIONS) << endl; | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment