Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ add_executable(${PROJECT_NAME}
src/game/components/health.cpp
src/game/components/player.cpp
src/game/game-registration.cpp
src/game/systems/collision-system.cpp

src/common/systems/forward-renderer.cpp
)
Expand Down
11 changes: 10 additions & 1 deletion src/game/components/collider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,24 @@ static gameplay::ColliderShape parseColliderShape(const std::string& value, game
return fallback;
}

static inline gameplay::CollisionLayer layerStringToGroup(const std::string& layer) {
if (layer == "player") return gameplay::CollisionLayer::LAYER_PLAYER;
if (layer == "enemy") return gameplay::CollisionLayer::LAYER_ENEMY;
if (layer == "projectile") return gameplay::CollisionLayer::LAYER_PROJECTILE;
if (layer == "trigger") return gameplay::CollisionLayer::LAYER_TRIGGER;
return gameplay::CollisionLayer::LAYER_ENVIRONMENT; // environment is the default layer
}

namespace gameplay {

void ColliderComponent::deserialize(const nlohmann::json& data) {
if (!data.is_object()) return;
shape = parseColliderShape(data.value("shape", std::string("Sphere")), shape);
layer = data.value("layer", layer);
radius = data.value("radius", radius);
height = data.value("height", height);
isTrigger = data.value("isTrigger", isTrigger);
std::string stringLayer = data.value("layer", "");
layer = layerStringToGroup(stringLayer);
}

} // namespace gameplay
12 changes: 10 additions & 2 deletions src/game/components/collider.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,20 @@ namespace gameplay {

enum class ColliderShape { Sphere, Capsule };

enum CollisionLayer : short {
LAYER_PLAYER = 1 << 0, // bit 0: 0000 0001
LAYER_ENEMY = 1 << 1, // bit 1: 0000 0010
LAYER_ENVIRONMENT = 1 << 2, // bit 2: 0000 0100
LAYER_PROJECTILE = 1 << 3, // bit 3: 0000 1000
LAYER_TRIGGER = 1 << 4, // bit 4: 0001 0000
};

class ColliderComponent : public our::Component {
public:
ColliderShape shape = ColliderShape::Sphere;
std::string layer = "default";
CollisionLayer layer = CollisionLayer::LAYER_ENVIRONMENT;
float radius = 0.5f;
float height = 1.0f;
float height = 1.0f; // must be the total height
bool isTrigger = false;

static std::string getID() {
Expand Down
295 changes: 295 additions & 0 deletions src/game/systems/collision-system.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
#include "collision-system.hpp"

#include <btBulletCollisionCommon.h>

#include <components/collider.hpp>
#include <ecs/entity.hpp>
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>

// Helper functions for GLM-Bullet conversions
inline btVector3 glmToBtVec3(const glm::vec3& v) {
return btVector3(v.x, v.y, v.z);
}
inline glm::vec3 btToGlmVec3(const btVector3& v) {
return glm::vec3(v.getX(), v.getY(), v.getZ());
}
static btTransform entityToBtTransform(our::Entity* entity) {
glm::mat4 m = entity->getLocalToWorldMatrix();

// Extract position from column 3
glm::vec3 pos = glm::vec3(m[3]);

// Extract rotation by stripping scale from the 3x3 submatrix.
// Each column of the 3x3 has length = scale along that axis.
// Normalizing each column removes the scale, leaving pure rotation.
glm::mat3 rotMat;
Comment thread
LoayAhmed304 marked this conversation as resolved.
rotMat[0] = glm::normalize(glm::vec3(m[0]));
rotMat[1] = glm::normalize(glm::vec3(m[1]));
rotMat[2] = glm::normalize(glm::vec3(m[2]));
glm::quat rot = glm::quat_cast(rotMat);

btTransform t;
t.setOrigin(btVector3(pos.x, pos.y, pos.z));
t.setRotation(btQuaternion(rot.x, rot.y, rot.z, rot.w));
return t;
}

namespace gameplay {

inline short getMaskForLayer(short group) {
switch (group) {
case LAYER_PLAYER:
return LAYER_ENEMY | LAYER_ENVIRONMENT | LAYER_TRIGGER;
case LAYER_ENEMY:
return LAYER_PLAYER | LAYER_PROJECTILE | LAYER_ENVIRONMENT;
case LAYER_ENVIRONMENT:
return LAYER_PLAYER | LAYER_ENEMY | LAYER_PROJECTILE;
case LAYER_PROJECTILE:
return LAYER_ENEMY | LAYER_ENVIRONMENT;
case LAYER_TRIGGER:
return LAYER_PLAYER;
default:
return 0;
}
}

void CollisionSystem::initialize() {
// collision configuration contains default setup for memory, collision setup.
collisionConfiguration = new btDefaultCollisionConfiguration();

// use the default collision dispatcher
dispatcher = new btCollisionDispatcher(collisionConfiguration);

broadphase = new btDbvtBroadphase();

// the default constraint solver
collisionWorld = new btCollisionWorld(dispatcher, broadphase, collisionConfiguration);
}

void CollisionSystem::destroy() {
// remove collision objects
for (auto& [entity, obj] : entityToBullet) {
collisionWorld->removeCollisionObject(obj);
delete obj;
}
entityToBullet.clear();

// remove shapes
for (auto shape : ownedShapes) {
delete shape;
}
ownedShapes.clear();

// delete bullet internals
delete collisionWorld;
delete broadphase;
delete dispatcher;
delete collisionConfiguration;
collisionWorld = nullptr;
broadphase = nullptr;
dispatcher = nullptr;
collisionConfiguration = nullptr;

frameCollisions.clear();
}

void CollisionSystem::update(our::World* world) {
// clear previous frame's collisions
frameCollisions.clear();

// Sync entities with colliders to Bullet
for (auto entity : world->getEntities()) {
ColliderComponent* collider = entity->getComponent<ColliderComponent>();

if (!collider) continue;

if (entityToBullet.find(entity) == entityToBullet.end()) {
addEntity(entity);
} else {
syncTransform(entity);
Comment thread
LoayAhmed304 marked this conversation as resolved.
}
}

// Remove bullet objects for entities that no longer exists
std::vector<our::Entity*> toRemove;
Comment thread
LoayAhmed304 marked this conversation as resolved.
for (const auto& [entity, obj] : entityToBullet) {
if (world->getEntities().find(entity) == world->getEntities().end()) {
removeEntity(entity);
Comment thread
LoayAhmed304 marked this conversation as resolved.
Outdated
}
}

// Run collision detection
collisionWorld->performDiscreteCollisionDetection();

// Read collision events
int numManifolds = dispatcher->getNumManifolds();
for (int i = 0; i < numManifolds; i++) {
// any potential collision will have a manifold, even if it has no contact points
btPersistentManifold* manifold = dispatcher->getManifoldByIndexInternal(i);
const btCollisionObject* objA = manifold->getBody0();
const btCollisionObject* objB = manifold->getBody1();
our::Entity* entityA = static_cast<our::Entity*>(objA->getUserPointer());
our::Entity* entityB = static_cast<our::Entity*>(objB->getUserPointer());

// check if they collide (if they have contact points)
// find deepest contact point (the one with the largest penetration depth)
int numContacts = manifold->getNumContacts();
if (numContacts == 0) continue;
float deepest = 0.0f;
int deepestIndex = -1;
for (int j = 0; j < numContacts; j++) {
// negative distance means penetration, and positive distance means separation. So we want the most
// negative distance.
float d = manifold->getContactPoint(j).getDistance();
if (d < deepest) {
deepest = d;
deepestIndex = j;
}
}

if (deepestIndex != -1) {
// we have a collision!
btManifoldPoint& contactPoint = manifold->getContactPoint(deepestIndex);
CollisionEvent event;
event.entityA = entityA;
event.entityB = entityB;
event.point = btToGlmVec3(contactPoint.getPositionWorldOnB());
event.normal = btToGlmVec3(contactPoint.m_normalWorldOnB);
event.penetrationDepth = -contactPoint.getDistance(); // convert back to positive penetration depth
frameCollisions.push_back(event);
}
}
// apply pushback for non-trigger collisions
Comment thread
LoayAhmed304 marked this conversation as resolved.
for (const CollisionEvent& event : frameCollisions) {
// discard the trigger collisions since they don't need pushback
auto* colliderA = event.entityA->getComponent<ColliderComponent>();
auto* colliderB = event.entityB->getComponent<ColliderComponent>();

if (!colliderA || !colliderB) continue;
if (colliderA->isTrigger || colliderB->isTrigger) continue;

// push back logic (don't push environments)
if (colliderA->layer == CollisionLayer::LAYER_ENVIRONMENT) {
event.entityB->localTransform.position -= event.normal * event.penetrationDepth;
Comment thread
LoayAhmed304 marked this conversation as resolved.
Outdated
} else if (colliderB->layer == CollisionLayer::LAYER_ENVIRONMENT) {
event.entityA->localTransform.position += event.normal * event.penetrationDepth;
} else { // this may be edited or removed later
Comment thread
LoayAhmed304 marked this conversation as resolved.
event.entityA->localTransform.position -= event.normal * (event.penetrationDepth / 2.0f);
event.entityB->localTransform.position += event.normal * (event.penetrationDepth / 2.0f);
}
}
}

// On-demand function
HitInfo CollisionSystem::raycast(const Ray& ray, float maxDistance, const short targetLayer) const {
HitInfo hitInfo;

if (!collisionWorld) return hitInfo;
btVector3 from = glmToBtVec3(ray.origin);
btVector3 to = glmToBtVec3(ray.origin + ray.direction * maxDistance);

btCollisionWorld::ClosestRayResultCallback callback(from, to);

callback.m_collisionFilterGroup = btBroadphaseProxy::AllFilter; // check against all layers
callback.m_collisionFilterMask = targetLayer; // only collide with the target layer

collisionWorld->rayTest(from, to, callback);

if (callback.hasHit()) {
hitInfo.hit = true;
hitInfo.entity = static_cast<our::Entity*>(callback.m_collisionObject->getUserPointer());
hitInfo.point = btToGlmVec3(callback.m_hitPointWorld);
hitInfo.normal = btToGlmVec3(callback.m_hitNormalWorld);
hitInfo.distance = callback.m_closestHitFraction * maxDistance;
}

return hitInfo;
}

// On-demand function
std::vector<our::Entity*> CollisionSystem::overlapSphere(const glm::vec3& center, float radius, short targetLayer) {
std::vector<our::Entity*> results;
if (!collisionWorld) return results;

for (const auto& [entity, obj] : entityToBullet) {
short objGroup = obj->getBroadphaseHandle()->m_collisionFilterGroup;
if ((objGroup & targetLayer) == 0) continue;

glm::vec3 entityPos = btToGlmVec3(obj->getWorldTransform().getOrigin());

ColliderComponent* collider = entity->getComponent<ColliderComponent>();
float entityRadius = collider ? collider->radius : 0.0f;

// sphere vs sphere hit check
float dist = glm::length(entityPos - center);
if (dist <= radius + entityRadius) {
results.push_back(entity);
}
}

return results;
}

void CollisionSystem::addEntity(our::Entity* entity) {
auto* collider = entity->getComponent<ColliderComponent>();
if (!collider) return;

// Create the collision shape based on component data
btCollisionShape* shape = nullptr;
switch (collider->shape) {
case ColliderShape::Sphere:
shape = new btSphereShape(collider->radius);
break;
case ColliderShape::Capsule: {
// convert from total height to spine
float spine = collider->height - 2.0f * collider->radius;
if (spine < 0.0f) spine = 0.0f;
shape = new btCapsuleShape(collider->radius, spine);
break;
}
Comment thread
LoayAhmed304 marked this conversation as resolved.
}
ownedShapes.push_back(shape);

// Create the collision object
btCollisionObject* obj = new btCollisionObject();
obj->setCollisionShape(shape);
obj->setWorldTransform(entityToBtTransform(entity));
obj->setUserPointer(entity); // so we can go back to the entity

// Mark non-environment objects as KINEMATIC.
if (collider->layer != CollisionLayer::LAYER_ENVIRONMENT) {
obj->setCollisionFlags(obj->getCollisionFlags() | btCollisionObject::CF_KINEMATIC_OBJECT);
}

// Add to Bullet world with layer filtering
short group = collider->layer;
short mask = getMaskForLayer(group);
collisionWorld->addCollisionObject(obj, group, mask);
entityToBullet[entity] = obj;
}

void CollisionSystem::removeEntity(our::Entity* entity) {
if (entityToBullet.find(entity) == entityToBullet.end()) return;

Comment thread
LoayAhmed304 marked this conversation as resolved.
btCollisionObject* obj = entityToBullet[entity];
collisionWorld->removeCollisionObject(obj);
delete obj;
entityToBullet.erase(entity);
}

void CollisionSystem::syncTransform(our::Entity* entity) {
btCollisionObject* obj = entityToBullet[entity];

if (!obj) return;

obj->setWorldTransform(entityToBtTransform(entity));

// Update the broadphase AABB so Bullet uses the new position for collision detection
collisionWorld->updateSingleAabb(obj);
}
const std::vector<CollisionEvent>& CollisionSystem::getCollisions() const {
return frameCollisions;
}

} // namespace gameplay
Loading