Physics & Collision
src/core/collision.hh, src/core/raycast.hh, and src/mesh/colbin_manager.hh — AABB/SAT collision tests, raycasting, and loading baked .COLBIN collision meshes (see the COLBIN format spec).
Collision
src/core/collision.hh
struct CollisionTest {
psyqo::Vec3 mtv; // minimum translation vector to resolve the overlap
psyqo::Vec3 normal;
psyqo::FixedPoint<> penetration;
};
class Collision {
public:
void GenerateAABBForMesh(const GameObject *object, AABBCollision *collisionBoxOut);
bool IsAABBCollision(const AABBCollision &collisionA, const AABBCollision &collisionB);
bool IsSATCollision(const OBB &collisionA, const OBB &collisionB, CollisionTest *resultOut);
};
Non-static member of MadnightEngine (g_madnightEngine.m_collisionHelper).
IsAABBCollisionis a cheap axis-aligned overlap test, provided for the game to call.IsSATCollisionruns the Separating Axis Theorem against two oriented boxes and, on overlap, fillsCollisionTestwith the minimum translation vector needed to push them apart. Provided for push-out collision response against.COLBINwallOBBs (see COLBIN).
Neither function is currently used by the engine. IsSATCollision's only reference outside its own definition is a commented-out line in GameplayScene::frame, and IsAABBCollision has no callers at all. Both are here for the game to call.
OBB and AABBCollision themselves are defined in src/core/collision_types.hh — see Core → Collision types.
Usage
Push-out collision response between two game objects, the standard pattern (also shown, commented out, in the engine's own GameplayScene):
CollisionTest result;
if (g_madnightEngine.m_collisionHelper.IsSATCollision(player->obb(), wall->obb(), &result))
player->SetPosition(player->pos() + result.mtv);
Internals
IsSATCollisionchecks 15 axes total (each box's 3 face normals, plus the 9 pairwise cross products between them) and tracks whichever axis has the smallest overlap — that's what ends up as the resolutionmtv.GenerateAABBForMeshis marked[[deprecated]]in-source ("should be done offline, DO NOT USE") — collision boxes are meant to be baked into the mesh at export time, not computed at runtime.
Raycast
src/core/raycast.hh
static constexpr psyqo::FixedPoint<> maxRayDistance = ONE_METRE * 10; // 10m
struct Ray {
psyqo::Vec3 origin;
psyqo::Vec3 direction; // normalized
psyqo::FixedPoint<> maxDistance; // metres — keep small, 128px = 1m
};
struct RayHit {
bool hit;
psyqo::FixedPoint<> distance;
psyqo::Vec3 hitPos; // world-space hit position
GameObject *object;
};
class Raycast {
public:
bool RaycastScene(const Ray &ray, GameObjectTag targetTag, RayHit *hitOut);
};
Non-static member of MadnightEngine (g_madnightEngine.m_raycast).
RaycastScene only tests against game objects carrying the given GameObjectTag (see Core → GameObjectTag) — pass a specific tag to avoid testing against everything in the world, e.g. INTERACTABLE for a "what am I looking at" prompt. Internally it tests the ray against each candidate object's AABB.
RaycastScene is not currently used by the engine. Its only reference outside its own definition is a commented-out line in GameplayScene::frame, same as IsSATCollision above. It's here for the game to call.
Usage
Ray ray = {.origin = camera->pos(), .direction = camera->forwardVector(), .maxDistance = 2.0_ws};
RayHit hit = {0};
if (g_madnightEngine.m_raycast.RaycastScene(ray, GameObjectTag::INTERACTABLE, &hit))
ShowInteractPrompt(hit.object);
Internals
- Returns the first object hit while iterating the tagged list, not the closest one — if two interactables overlap along the ray, which one you get depends on iteration order, not distance.
- The header itself notes this is inaccurate for rotated objects — it's a straight AABB test, no OBB/SAT version yet.
ColbinManager
src/mesh/colbin_manager.hh
Loads a single .COLBIN collision mesh (floor triangles, wall OBBs, and a spatial grid bucketing the walls) and makes it available via Colbin()/walls(). See the COLBIN format spec for the full binary layout.
Loading is all ColbinManager does. ColBin::floors and the grid (gridCells) are parsed and stored but nothing in the engine reads them, and walls()/Colbin() have no callers either. Ground detection against floor triangles, SAT-based wall collision, and any grid-based broad-phase culling are the game's to write against this loaded data, not something the engine currently does for you.
struct FloorTri {
psyqo::Vec3 v0, v1, v2; // scaled by 128
int16_t n[3]; // face normal, FP12 (scaled by 4096)
};
struct GridHeader {
int32_t originX, originZ;
uint32_t cellSize;
uint16_t gridWidth, gridHeight;
};
struct GridCell {
uint16_t count;
uint16_t *indices; // into ColBin::walls
};
struct ColBin {
Header header;
GridHeader gridHeader;
GridCell *gridCells;
FloorTri *floors;
OBB *walls;
};
class ColbinManager {
public:
psyqo::Coroutine<> LoadColbin(const eastl::fixed_string<char, MAX_ARCHIVE_FILE_NAME_LEN> &name, ColBin **colbinOut);
ColBin *Colbin(void);
void Dump(void);
eastl::span<OBB> walls(void);
};
Non-static member of MadnightEngine (g_madnightEngine.m_colbinManager). Unlike MeshManager/TextureManager, ColbinManager holds a single collision mesh at a time (m_colbin is a lone member, not a pool) — one .COLBIN per loaded scene/level.
Usage
ColBin *level;
co_await g_madnightEngine.m_colbinManager.LoadColbin("level01.colbin", &level);
for (auto &wall : g_madnightEngine.m_colbinManager.walls()) {
CollisionTest result;
if (g_madnightEngine.m_collisionHelper.IsSATCollision(player->obb(), wall, &result))
player->SetPosition(player->pos() + result.mtv);
}
Internals
- Loading a new
.COLBINfully overwritesm_colbin— there's no unload/reload-alongside step, so swap it out only when you're actually changing levels/rooms. - The spatial grid (
gridCells) buckets wall indices per cell, but the engine has no grid lookup: nothing outsideColbinManagerreadsgridCells,gridWidth,gridHeight,cellSize,originX, ororiginZ. See the COLBIN format spec for the cell-index mapping the exporter uses, if you want to implement the lookup yourself.