508 lines
18 KiB
C
508 lines
18 KiB
C
|
|
/**
|
||
|
|
* @file partition.c
|
||
|
|
* @brief The broad phase, checked against brute force.
|
||
|
|
*
|
||
|
|
* A broad phase has one contract and it is asymmetric: it may report a pair that
|
||
|
|
* does not overlap, and it may never miss one that does. Over-reporting costs a
|
||
|
|
* narrowphase call; under-reporting is a wall an actor walks through, and it
|
||
|
|
* fails silently and intermittently, only for the positions where the structure
|
||
|
|
* happens to be wrong.
|
||
|
|
*
|
||
|
|
* So every query here is compared against a linear scan over the same proxies,
|
||
|
|
* and the assertion is containment in one direction and not equality. That is
|
||
|
|
* the only check that has any teeth: a broad phase can be tested against itself
|
||
|
|
* all day and agree with itself all day.
|
||
|
|
*
|
||
|
|
* The suite is written as a table over partitioners so the same assertions run
|
||
|
|
* against every implementation. A second implementation that is not held to the
|
||
|
|
* first one's contract is not pluggable, it is just present.
|
||
|
|
*/
|
||
|
|
|
||
|
|
#include <math.h>
|
||
|
|
#include <string.h>
|
||
|
|
|
||
|
|
#include <akerror.h>
|
||
|
|
|
||
|
|
#include <akgl/collision.h>
|
||
|
|
#include <akgl/error.h>
|
||
|
|
#include <akgl/heap.h>
|
||
|
|
|
||
|
|
#include "testutil.h"
|
||
|
|
|
||
|
|
/** @brief Every partitioner the factory knows, so the same tests run over each. */
|
||
|
|
static char *partitioners[] = { "grid" };
|
||
|
|
|
||
|
|
/** @brief What a visit callback accumulates. */
|
||
|
|
typedef struct {
|
||
|
|
int16_t seen[AKGL_MAX_HEAP_COLLISION_PROXY];
|
||
|
|
int count;
|
||
|
|
} visit_record;
|
||
|
|
|
||
|
|
static akerr_ErrorContext *record_visit(akgl_CollisionProxy *proxy, void *data)
|
||
|
|
{
|
||
|
|
visit_record *rec = (visit_record *)data;
|
||
|
|
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
FAIL_ZERO_RETURN(errctx, proxy, AKERR_NULLPOINTER, "NULL proxy in a visit");
|
||
|
|
FAIL_ZERO_RETURN(errctx, rec, AKERR_NULLPOINTER, "NULL record in a visit");
|
||
|
|
|
||
|
|
if ( rec->count < AKGL_MAX_HEAP_COLLISION_PROXY ) {
|
||
|
|
rec->seen[rec->count] = (int16_t)(proxy - &akgl_heap_collision_proxies[0]);
|
||
|
|
rec->count += 1;
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief How many cell entries are currently claimed from the pool. */
|
||
|
|
static int live_cells(void)
|
||
|
|
{
|
||
|
|
int live = 0;
|
||
|
|
int i = 0;
|
||
|
|
|
||
|
|
for ( i = 0; i < AKGL_MAX_HEAP_COLLISION_CELL; i++ ) {
|
||
|
|
if ( akgl_heap_collision_cells[i].refcount != 0 ) {
|
||
|
|
live += 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return live;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief Was this proxy index reported? */
|
||
|
|
static bool sawit(visit_record *rec, int16_t idx)
|
||
|
|
{
|
||
|
|
int i = 0;
|
||
|
|
|
||
|
|
for ( i = 0; i < rec->count; i++ ) {
|
||
|
|
if ( rec->seen[i] == idx ) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief Claim a proxy at a position and put it in the partitioner. */
|
||
|
|
static akerr_ErrorContext *spawn(akgl_CollisionWorld *world, akgl_CollisionShape *shape,
|
||
|
|
float32_t x, float32_t y, akgl_CollisionProxy **dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
PASS(errctx, akgl_heap_next_collision_proxy(dest));
|
||
|
|
PASS(errctx, akgl_collision_proxy_initialize(*dest, NULL, shape, x, y, 0.0f));
|
||
|
|
PASS(errctx, world->partitioner.insert(&world->partitioner, *dest));
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief A query must report every proxy a linear scan would, and may report more.
|
||
|
|
*
|
||
|
|
* Positions are chosen to land on cell boundaries, span several cells, and sit
|
||
|
|
* outside the grid entirely, because those are the three places an index is
|
||
|
|
* wrong and a linear scan is not.
|
||
|
|
*/
|
||
|
|
akerr_ErrorContext *test_query_never_misses(char *name)
|
||
|
|
{
|
||
|
|
akgl_CollisionWorld world;
|
||
|
|
akgl_CollisionShape small;
|
||
|
|
akgl_CollisionShape big;
|
||
|
|
akgl_CollisionProxy *proxies[16];
|
||
|
|
visit_record rec;
|
||
|
|
SDL_FRect area;
|
||
|
|
SDL_FRect small_body = { .x = 0.0f, .y = 0.0f, .w = 16.0f, .h = 16.0f };
|
||
|
|
SDL_FRect big_body = { .x = 0.0f, .y = 0.0f, .w = 96.0f, .h = 96.0f };
|
||
|
|
float32_t spots[][2] = {
|
||
|
|
{ 0.0f, 0.0f }, { 16.0f, 0.0f }, { 32.0f, 32.0f }, { 8.0f, 8.0f },
|
||
|
|
{ 100.0f, 100.0f }, { 1000.0f, 40.0f }, { -40.0f, -40.0f }, { 15.9f, 15.9f }
|
||
|
|
};
|
||
|
|
int spotcount = (int)(sizeof(spots) / sizeof(spots[0]));
|
||
|
|
bool missed = false;
|
||
|
|
float32_t missx = 0.0f;
|
||
|
|
float32_t missy = 0.0f;
|
||
|
|
int i = 0;
|
||
|
|
int q = 0;
|
||
|
|
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
ATTEMPT {
|
||
|
|
CATCH(errctx, akgl_heap_init());
|
||
|
|
CATCH(errctx, akgl_collision_world_init(&world, name, 16.0f, 16.0f));
|
||
|
|
CATCH(errctx, akgl_collision_shape_box(&small, &small_body, 0.0f));
|
||
|
|
CATCH(errctx, akgl_collision_shape_box(&big, &big_body, 0.0f));
|
||
|
|
|
||
|
|
for ( i = 0; i < spotcount; i++ ) {
|
||
|
|
CATCH(errctx, spawn(&world, &small, spots[i][0], spots[i][1], &proxies[i]));
|
||
|
|
}
|
||
|
|
// One deliberately large enough to spill onto the oversized chain, which
|
||
|
|
// is a separate code path a query must not forget to walk.
|
||
|
|
CATCH(errctx, spawn(&world, &big, 200.0f, 200.0f, &proxies[spotcount]));
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Sweep a query window across the world and compare against a linear
|
||
|
|
* scan every time. Recorded into a flag and asserted after the loops: a
|
||
|
|
* TEST_ASSERT in here reports by breaking, which would leave the loop
|
||
|
|
* rather than the ATTEMPT block and could not fail the test.
|
||
|
|
*/
|
||
|
|
for ( q = 0; q < 40; q++ ) {
|
||
|
|
area.x = (float32_t)((q * 13) % 300) - 50.0f;
|
||
|
|
area.y = (float32_t)((q * 7) % 300) - 50.0f;
|
||
|
|
area.w = 24.0f;
|
||
|
|
area.h = 24.0f;
|
||
|
|
|
||
|
|
memset(&rec, 0x00, sizeof(rec));
|
||
|
|
if ( world.partitioner.query(&world.partitioner, &area, AKGL_COLLISION_LAYER_ALL,
|
||
|
|
&record_visit, &rec) != NULL ) {
|
||
|
|
missed = true;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
for ( i = 0; i <= spotcount; i++ ) {
|
||
|
|
if ( !SDL_HasRectIntersectionFloat(&proxies[i]->bounds, &area) ) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if ( !sawit(&rec, (int16_t)(proxies[i] - &akgl_heap_collision_proxies[0])) ) {
|
||
|
|
missed = true;
|
||
|
|
missx = area.x;
|
||
|
|
missy = area.y;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
TEST_ASSERT(errctx, (missed == false),
|
||
|
|
"%s missed a proxy that overlaps the query at (%f, %f); a broad phase may "
|
||
|
|
"over-report and may never under-report", name, missx, missy);
|
||
|
|
|
||
|
|
// A query is allowed to report a proxy more than the once, but must not:
|
||
|
|
// a proxy spanning four cells appears on four chains.
|
||
|
|
area.x = 0.0f;
|
||
|
|
area.y = 0.0f;
|
||
|
|
area.w = 300.0f;
|
||
|
|
area.h = 300.0f;
|
||
|
|
memset(&rec, 0x00, sizeof(rec));
|
||
|
|
CATCH(errctx, world.partitioner.query(&world.partitioner, &area, AKGL_COLLISION_LAYER_ALL,
|
||
|
|
&record_visit, &rec));
|
||
|
|
TEST_ASSERT(errctx, (rec.count <= (spotcount + 1)),
|
||
|
|
"%s reported %d proxies where at most %d exist; a proxy spanning several "
|
||
|
|
"cells is being reported once per cell", name, rec.count, spotcount + 1);
|
||
|
|
} CLEANUP {
|
||
|
|
} PROCESS(errctx) {
|
||
|
|
} FINISH(errctx, true);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief The layer mask filters, and filters in the right direction.
|
||
|
|
*/
|
||
|
|
akerr_ErrorContext *test_query_respects_mask(char *name)
|
||
|
|
{
|
||
|
|
akgl_CollisionWorld world;
|
||
|
|
akgl_CollisionShape actor;
|
||
|
|
akgl_CollisionShape scenery;
|
||
|
|
akgl_CollisionProxy *a = NULL;
|
||
|
|
akgl_CollisionProxy *b = NULL;
|
||
|
|
visit_record rec;
|
||
|
|
SDL_FRect body = { .x = 0.0f, .y = 0.0f, .w = 16.0f, .h = 16.0f };
|
||
|
|
SDL_FRect area = { .x = -50.0f, .y = -50.0f, .w = 200.0f, .h = 200.0f };
|
||
|
|
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
ATTEMPT {
|
||
|
|
CATCH(errctx, akgl_heap_init());
|
||
|
|
CATCH(errctx, akgl_collision_world_init(&world, name, 16.0f, 16.0f));
|
||
|
|
CATCH(errctx, akgl_collision_shape_box(&actor, &body, 0.0f));
|
||
|
|
CATCH(errctx, akgl_collision_shape_box(&scenery, &body, 0.0f));
|
||
|
|
scenery.layermask = AKGL_COLLISION_LAYER_STATIC;
|
||
|
|
|
||
|
|
CATCH(errctx, spawn(&world, &actor, 0.0f, 0.0f, &a));
|
||
|
|
CATCH(errctx, spawn(&world, &scenery, 32.0f, 0.0f, &b));
|
||
|
|
|
||
|
|
memset(&rec, 0x00, sizeof(rec));
|
||
|
|
CATCH(errctx, world.partitioner.query(&world.partitioner, &area, AKGL_COLLISION_LAYER_STATIC,
|
||
|
|
&record_visit, &rec));
|
||
|
|
TEST_ASSERT(errctx, (rec.count == 1), "%s reported %d proxies on the static layer, expected 1",
|
||
|
|
name, rec.count);
|
||
|
|
TEST_ASSERT(errctx, sawit(&rec, (int16_t)(b - &akgl_heap_collision_proxies[0])),
|
||
|
|
"%s filtered out the static proxy and kept the actor", name);
|
||
|
|
|
||
|
|
memset(&rec, 0x00, sizeof(rec));
|
||
|
|
CATCH(errctx, world.partitioner.query(&world.partitioner, &area, AKGL_COLLISION_LAYER_NONE,
|
||
|
|
&record_visit, &rec));
|
||
|
|
TEST_ASSERT(errctx, (rec.count == 0), "%s reported %d proxies for an empty mask", name, rec.count);
|
||
|
|
} CLEANUP {
|
||
|
|
} PROCESS(errctx) {
|
||
|
|
} FINISH(errctx, true);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Moving a proxy has to update the index, and moving it nowhere has not to.
|
||
|
|
*
|
||
|
|
* The first half is the correctness claim: a stale entry in the old cell is the
|
||
|
|
* classic incremental-structure bug, and it shows up as a collision with where
|
||
|
|
* something used to be. The second half is the *performance* claim the whole
|
||
|
|
* structure was chosen for, and it is asserted rather than assumed.
|
||
|
|
*/
|
||
|
|
akerr_ErrorContext *test_move_is_incremental(char *name)
|
||
|
|
{
|
||
|
|
akgl_CollisionWorld world;
|
||
|
|
akgl_CollisionShape shape;
|
||
|
|
akgl_CollisionProxy *proxy = NULL;
|
||
|
|
visit_record rec;
|
||
|
|
SDL_FRect body = { .x = 0.0f, .y = 0.0f, .w = 8.0f, .h = 8.0f };
|
||
|
|
SDL_FRect here = { .x = 0.0f, .y = 0.0f, .w = 8.0f, .h = 8.0f };
|
||
|
|
SDL_FRect there = { .x = 200.0f, .y = 200.0f, .w = 8.0f, .h = 8.0f };
|
||
|
|
int16_t firstbefore = 0;
|
||
|
|
int before = 0;
|
||
|
|
int i = 0;
|
||
|
|
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
ATTEMPT {
|
||
|
|
CATCH(errctx, akgl_heap_init());
|
||
|
|
CATCH(errctx, akgl_collision_world_init(&world, name, 16.0f, 16.0f));
|
||
|
|
CATCH(errctx, akgl_collision_shape_box(&shape, &body, 0.0f));
|
||
|
|
CATCH(errctx, spawn(&world, &shape, 0.0f, 0.0f, &proxy));
|
||
|
|
|
||
|
|
// Move it a long way and re-query both ends.
|
||
|
|
CATCH(errctx, akgl_collision_proxy_sync(proxy, &shape, 200.0f, 200.0f, 0.0f));
|
||
|
|
CATCH(errctx, world.partitioner.move(&world.partitioner, proxy));
|
||
|
|
|
||
|
|
memset(&rec, 0x00, sizeof(rec));
|
||
|
|
CATCH(errctx, world.partitioner.query(&world.partitioner, &here, AKGL_COLLISION_LAYER_ALL,
|
||
|
|
&record_visit, &rec));
|
||
|
|
TEST_ASSERT(errctx, (rec.count == 0),
|
||
|
|
"%s still reports a proxy where it used to be; the old cell entry was not "
|
||
|
|
"removed, and a game would collide with a ghost", name);
|
||
|
|
|
||
|
|
memset(&rec, 0x00, sizeof(rec));
|
||
|
|
CATCH(errctx, world.partitioner.query(&world.partitioner, &there, AKGL_COLLISION_LAYER_ALL,
|
||
|
|
&record_visit, &rec));
|
||
|
|
TEST_ASSERT(errctx, (rec.count == 1), "%s does not report the proxy where it now is", name);
|
||
|
|
|
||
|
|
/*
|
||
|
|
* A move within the cells already occupied must touch nothing. That is
|
||
|
|
* the property the uniform grid was chosen for over a tree, so it is
|
||
|
|
* checked rather than trusted: the proxy's chain head is the cheapest
|
||
|
|
* observable proof that no entry was released and re-claimed.
|
||
|
|
*/
|
||
|
|
firstbefore = proxy->first;
|
||
|
|
CATCH(errctx, akgl_collision_proxy_sync(proxy, &shape, 201.0f, 201.0f, 0.0f));
|
||
|
|
CATCH(errctx, world.partitioner.move(&world.partitioner, proxy));
|
||
|
|
TEST_ASSERT(errctx, (proxy->first == firstbefore),
|
||
|
|
"%s re-celled a proxy that did not leave its cells; the structure is not "
|
||
|
|
"incremental and the argument for choosing it does not hold", name);
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Moving repeatedly must not accumulate index entries. A move that links
|
||
|
|
* into the new cells without unlinking the old ones answers *queries*
|
||
|
|
* correctly -- the bounds re-check filters the stale entries out -- so
|
||
|
|
* nothing above catches it. What it does instead is leak the cell pool
|
||
|
|
* until the grid stops working, on a timescale no unit test reaches by
|
||
|
|
* accident. Counting the pool is the only thing that sees it.
|
||
|
|
*/
|
||
|
|
before = live_cells();
|
||
|
|
for ( i = 0; i < 200; i++ ) {
|
||
|
|
CATCH(errctx, akgl_collision_proxy_sync(proxy, &shape,
|
||
|
|
(float32_t)((i * 37) % 400),
|
||
|
|
(float32_t)((i * 53) % 400), 0.0f));
|
||
|
|
CATCH(errctx, world.partitioner.move(&world.partitioner, proxy));
|
||
|
|
}
|
||
|
|
TEST_ASSERT(errctx, (live_cells() <= before),
|
||
|
|
"%s holds %d cell entries for one proxy after 200 moves, up from %d; the "
|
||
|
|
"old entries are not being unlinked and the pool is leaking",
|
||
|
|
name, live_cells(), before);
|
||
|
|
|
||
|
|
// Removing is idempotent, because a caller tearing down should not have
|
||
|
|
// to remember what it already dropped.
|
||
|
|
CATCH(errctx, world.partitioner.remove(&world.partitioner, proxy));
|
||
|
|
CATCH(errctx, world.partitioner.remove(&world.partitioner, proxy));
|
||
|
|
memset(&rec, 0x00, sizeof(rec));
|
||
|
|
CATCH(errctx, world.partitioner.query(&world.partitioner, &there, AKGL_COLLISION_LAYER_ALL,
|
||
|
|
&record_visit, &rec));
|
||
|
|
TEST_ASSERT(errctx, (rec.count == 0), "%s still reports a removed proxy", name);
|
||
|
|
} CLEANUP {
|
||
|
|
} PROCESS(errctx) {
|
||
|
|
} FINISH(errctx, true);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief What each_pair accumulates. */
|
||
|
|
typedef struct {
|
||
|
|
int count;
|
||
|
|
int selfpairs;
|
||
|
|
} pair_record;
|
||
|
|
|
||
|
|
static akerr_ErrorContext *record_pair(akgl_CollisionProxy *a, akgl_CollisionProxy *b, void *data)
|
||
|
|
{
|
||
|
|
pair_record *rec = (pair_record *)data;
|
||
|
|
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
FAIL_ZERO_RETURN(errctx, a, AKERR_NULLPOINTER, "NULL first proxy in a pair");
|
||
|
|
FAIL_ZERO_RETURN(errctx, b, AKERR_NULLPOINTER, "NULL second proxy in a pair");
|
||
|
|
FAIL_ZERO_RETURN(errctx, rec, AKERR_NULLPOINTER, "NULL record in a pair");
|
||
|
|
|
||
|
|
if ( a == b ) {
|
||
|
|
rec->selfpairs += 1;
|
||
|
|
}
|
||
|
|
rec->count += 1;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Every overlapping pair is reported, and none is reported twice.
|
||
|
|
*
|
||
|
|
* Two proxies sharing four cells is the case that produces four reports from a
|
||
|
|
* naive implementation, and four reports means four impulses for one contact.
|
||
|
|
*/
|
||
|
|
akerr_ErrorContext *test_each_pair_is_once(char *name)
|
||
|
|
{
|
||
|
|
akgl_CollisionWorld world;
|
||
|
|
akgl_CollisionShape shape;
|
||
|
|
akgl_CollisionProxy *proxies[4];
|
||
|
|
pair_record rec;
|
||
|
|
SDL_FRect body = { .x = 0.0f, .y = 0.0f, .w = 40.0f, .h = 40.0f };
|
||
|
|
int i = 0;
|
||
|
|
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
ATTEMPT {
|
||
|
|
CATCH(errctx, akgl_heap_init());
|
||
|
|
CATCH(errctx, akgl_collision_world_init(&world, name, 16.0f, 16.0f));
|
||
|
|
// 40 pixels on a 16 pixel grid: each of these covers nine cells, so every
|
||
|
|
// pair among them shares several.
|
||
|
|
CATCH(errctx, akgl_collision_shape_box(&shape, &body, 0.0f));
|
||
|
|
|
||
|
|
for ( i = 0; i < 4; i++ ) {
|
||
|
|
CATCH(errctx, spawn(&world, &shape, (float32_t)(i * 4), 0.0f, &proxies[i]));
|
||
|
|
}
|
||
|
|
|
||
|
|
memset(&rec, 0x00, sizeof(rec));
|
||
|
|
CATCH(errctx, world.partitioner.each_pair(&world.partitioner, &record_pair, &rec));
|
||
|
|
|
||
|
|
// Four mutually overlapping proxies are six distinct pairs.
|
||
|
|
TEST_ASSERT(errctx, (rec.selfpairs == 0), "%s paired a proxy with itself %d times",
|
||
|
|
name, rec.selfpairs);
|
||
|
|
TEST_ASSERT(errctx, (rec.count == 6),
|
||
|
|
"%s reported %d pairs among four mutually overlapping proxies, expected 6; "
|
||
|
|
"duplicates mean one contact resolved several times", name, rec.count);
|
||
|
|
} CLEANUP {
|
||
|
|
} PROCESS(errctx) {
|
||
|
|
} FINISH(errctx, true);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief An empty world answers nothing, and reset really empties it. */
|
||
|
|
akerr_ErrorContext *test_reset_and_empty(char *name)
|
||
|
|
{
|
||
|
|
akgl_CollisionWorld world;
|
||
|
|
akgl_CollisionShape shape;
|
||
|
|
akgl_CollisionProxy *proxy = NULL;
|
||
|
|
visit_record rec;
|
||
|
|
SDL_FRect body = { .x = 0.0f, .y = 0.0f, .w = 16.0f, .h = 16.0f };
|
||
|
|
SDL_FRect area = { .x = -100.0f, .y = -100.0f, .w = 400.0f, .h = 400.0f };
|
||
|
|
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
ATTEMPT {
|
||
|
|
CATCH(errctx, akgl_heap_init());
|
||
|
|
CATCH(errctx, akgl_collision_world_init(&world, name, 16.0f, 16.0f));
|
||
|
|
|
||
|
|
// The null hypothesis. A broken structure that reports nothing passes
|
||
|
|
// every other test in this file, so this one is not a formality.
|
||
|
|
memset(&rec, 0x00, sizeof(rec));
|
||
|
|
CATCH(errctx, world.partitioner.query(&world.partitioner, &area, AKGL_COLLISION_LAYER_ALL,
|
||
|
|
&record_visit, &rec));
|
||
|
|
TEST_ASSERT(errctx, (rec.count == 0), "%s reported %d proxies in an empty world",
|
||
|
|
name, rec.count);
|
||
|
|
|
||
|
|
CATCH(errctx, spawn(&world, &shape, 0.0f, 0.0f, &proxy));
|
||
|
|
CATCH(errctx, akgl_collision_shape_box(&shape, &body, 0.0f));
|
||
|
|
CATCH(errctx, akgl_collision_proxy_sync(proxy, &shape, 0.0f, 0.0f, 0.0f));
|
||
|
|
CATCH(errctx, world.partitioner.move(&world.partitioner, proxy));
|
||
|
|
|
||
|
|
memset(&rec, 0x00, sizeof(rec));
|
||
|
|
CATCH(errctx, world.partitioner.query(&world.partitioner, &area, AKGL_COLLISION_LAYER_ALL,
|
||
|
|
&record_visit, &rec));
|
||
|
|
TEST_ASSERT(errctx, (rec.count == 1), "%s lost the only proxy in the world", name);
|
||
|
|
|
||
|
|
CATCH(errctx, world.partitioner.reset(&world.partitioner, &world));
|
||
|
|
memset(&rec, 0x00, sizeof(rec));
|
||
|
|
CATCH(errctx, world.partitioner.query(&world.partitioner, &area, AKGL_COLLISION_LAYER_ALL,
|
||
|
|
&record_visit, &rec));
|
||
|
|
TEST_ASSERT(errctx, (rec.count == 0), "%s still reports %d proxies after a reset",
|
||
|
|
name, rec.count);
|
||
|
|
} CLEANUP {
|
||
|
|
} PROCESS(errctx) {
|
||
|
|
} FINISH(errctx, true);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *test_world_and_factory(void)
|
||
|
|
{
|
||
|
|
akgl_CollisionWorld world;
|
||
|
|
akgl_Partitioner part;
|
||
|
|
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
ATTEMPT {
|
||
|
|
CATCH(errctx, akgl_collision_world_init(&world, NULL, 16.0f, 16.0f));
|
||
|
|
TEST_ASSERT(errctx, (strcmp(world.partitioner.name, "grid") == 0),
|
||
|
|
"a NULL partitioner name installed \"%s\" rather than the default",
|
||
|
|
world.partitioner.name);
|
||
|
|
TEST_ASSERT(errctx, ((world.flags & AKGL_COLLISION_TEST_PLANAR) != 0),
|
||
|
|
"a new world does not force planar normals; every game today is 2D");
|
||
|
|
|
||
|
|
CATCH(errctx, akgl_partitioner_factory(&part, "grid"));
|
||
|
|
TEST_ASSERT(errctx, (part.query != NULL), "the grid installed no query");
|
||
|
|
TEST_ASSERT(errctx, (part.move != NULL), "the grid installed no move");
|
||
|
|
|
||
|
|
TEST_EXPECT_STATUS(errctx, AKERR_KEY, akgl_partitioner_factory(&part, "quadtree"),
|
||
|
|
"a partitioner nobody wrote");
|
||
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_partitioner_factory(NULL, "grid"),
|
||
|
|
"the factory with no destination");
|
||
|
|
TEST_EXPECT_STATUS(errctx, AKERR_VALUE, akgl_collision_world_init(&world, NULL, 0.0f, 16.0f),
|
||
|
|
"a world with no cell width");
|
||
|
|
TEST_EXPECT_STATUS(errctx, AKERR_VALUE, akgl_collision_world_init(&world, NULL, 16.0f, -1.0f),
|
||
|
|
"a world with negative cell height");
|
||
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_collision_world_init(NULL, NULL, 16.0f, 16.0f),
|
||
|
|
"a world with no destination");
|
||
|
|
} CLEANUP {
|
||
|
|
} PROCESS(errctx) {
|
||
|
|
} FINISH(errctx, true);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
int main(void)
|
||
|
|
{
|
||
|
|
akerr_ErrorContext *inner = NULL;
|
||
|
|
int i = 0;
|
||
|
|
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
ATTEMPT {
|
||
|
|
CATCH(errctx, akgl_error_init());
|
||
|
|
CATCH(errctx, test_world_and_factory());
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Every partitioner is held to the same assertions. Collected rather
|
||
|
|
* than CATCHed inside the loop, because CATCH reports by breaking and a
|
||
|
|
* break here would leave the loop rather than the ATTEMPT block, so a
|
||
|
|
* failing partitioner would be silently skipped.
|
||
|
|
*/
|
||
|
|
for ( i = 0; i < (int)(sizeof(partitioners) / sizeof(partitioners[0])); i++ ) {
|
||
|
|
inner = test_query_never_misses(partitioners[i]);
|
||
|
|
if ( inner != NULL ) { break; }
|
||
|
|
inner = test_query_respects_mask(partitioners[i]);
|
||
|
|
if ( inner != NULL ) { break; }
|
||
|
|
inner = test_move_is_incremental(partitioners[i]);
|
||
|
|
if ( inner != NULL ) { break; }
|
||
|
|
inner = test_each_pair_is_once(partitioners[i]);
|
||
|
|
if ( inner != NULL ) { break; }
|
||
|
|
inner = test_reset_and_empty(partitioners[i]);
|
||
|
|
if ( inner != NULL ) { break; }
|
||
|
|
}
|
||
|
|
CATCH(errctx, inner);
|
||
|
|
} CLEANUP {
|
||
|
|
} PROCESS(errctx) {
|
||
|
|
} FINISH_NORETURN(errctx);
|
||
|
|
|
||
|
|
return 0;
|
||
|
|
}
|