Hey guys,
looking for some comments/opinions/criticism on the following:
I’ve got a character controller which is based on Bullet’s btKinematicCharacterController.
Internally, the Bullet class uses a btPairCachingGhostObject to provide the character’s outer physics capsule. Unfortunately for me, Urho3D has no wrapper for GhostObjects, and Urho’s PhysicsWorld class does not provide collision events where ghost objects are concerned.
It is worth noting that Ghost Objects are NOT the same thing as trigger volumes (which Urho does support). The distinction between them is subtle, but we can definitely say that btPairCachingGhostObject is quite a different animal to btRigidBody.
In order to remedy this, I began by defining some new Urho events:
URHO3D_EVENT(GHOST_COLLISION_STARTED, OnGhostCollisionBegin){
URHO3D_PARAM(P_BODY, Body); /// RigidBody which collided with btGhostObject
URHO3D_PARAM(P_GHOST, Ghost); /// btGhostObject which collided with RigidBody
URHO3D_PARAM(P_GHOSTNODE, GhostNode); /// Scene node which acts as Parent to btGhostNode
}
URHO3D_EVENT(GHOST_COLLISION_STAY, OnGhostCollisionStay){
URHO3D_PARAM(P_BODY, Body);
URHO3D_PARAM(P_GHOST, Ghost);
URHO3D_PARAM(P_GHOSTNODE, GhostNode);
}
URHO3D_EVENT(GHOST_COLLISION_ENDED, OnGhostCollisionEnded){
URHO3D_PARAM(P_BODY, Body);
URHO3D_PARAM(P_GHOST, Ghost);
URHO3D_PARAM(P_GHOSTNODE, GhostNode);
}
You can see I chose not to provide contact information for ghost object collisions, and also I am only handling collisions between ghosts and rigidbodies (not ghosts and other ghosts).
I chose to track my collisions across frames in a simplified fashion:
HashSet<WeakPtr<RigidBody>> prevCollisions;
HashSet<WeakPtr<RigidBody>> currentCollisions;
Finally, here’s the code to deal with collision events for a single ghost object.
I chose not to iterate the world collision manifolds, because the ghost object is already keeping track of a list of objects that it (potentially) collides with (since it represents a broadphase list of objects whose AABB are intersecting that of the ghost)…
/// After physics update, move character root node to suit the physics
/// We also deal with collision detection / event sourcing
void KinematicCharacterController::HandlePostPhysicsUpdate(StringHash eventType, VariantMap& eventData){
if(ghostObject_){
currentCollisions.Clear();
/// Query the current world transform of the ghost object
btTransform t=ghostObject_->getWorldTransform();
/// Teleport this character's parent node to suit the physics object
Vector3 worldPos = ToVector3(t.getOrigin()) + Vector3::DOWN * height_*0.5f;
node_->SetWorldPosition( worldPos );
btManifoldArray manifoldArray;
/// Process all current collision events
/// (Ask ghost shape for a list of objects that it is potentially "colliding" with)
///
int numObjects = ghostObject_->getNumOverlappingObjects();
for(int i=0;i<numObjects;i++){
manifoldArray.clear();
/// Access the next collision object whose AABB overlaps with that of our ghost shape
btCollisionObject* obj = ghostObject_->getOverlappingObject(i);
/// Try to cast the current collision object to bullet rigidbody
/// If this fails, its not a rigidbody - could be another ghost etc.
btRigidBody* rb = dynamic_cast<btRigidBody*>(obj);
if(rb){
/// Query the physics broadphase for deeper information about the colliding pair
auto* paircache = node_->GetScene()->GetComponent<PhysicsWorld>()->GetWorld()->getPairCache();
btBroadphasePair* collisionPair = paircache->findPair(ghostObject_->getBroadphaseHandle(), obj->getBroadphaseHandle());
if (collisionPair == nullptr)
continue;
/// Query the colliding pair for deeper information about the contact manifold(s)
if (collisionPair->m_algorithm != nullptr)
collisionPair->m_algorithm->getAllContactManifolds(manifoldArray);
if(manifoldArray.size()==0)
continue;
/// Confirm that the two objects are in contact
int numContacts=0;
for(int i=0;i<manifoldArray.size();i++){
btPersistentManifold* manifold = manifoldArray[i];
numContacts += manifold->getNumContacts();
}
if(numContacts==0)
continue;
/// Cast the bullet rigidbody userpointer to Urho RigidBody
/// Dangerous assumption that this can never fail - hope springs eternal!
RigidBody* RB = (RigidBody*)rb->getUserPointer();
/// Wrap the object pointer
WeakPtr<RigidBody> weakRB(RB);
VariantMap& newData = GetEventDataMap();
/// Determine if this collision is "new", or "persistant"
if(!prevCollisions.Contains(weakRB))
{
/// Send "collision started" event
newData[OnGhostCollisionBegin::P_BODY] = RB;
newData[OnGhostCollisionBegin::P_GHOST] = ghostObject_;
newData[OnGhostCollisionBegin::P_GHOSTNODE] = node_;
RB->GetNode()->SendEvent(GHOST_COLLISION_STARTED, newData);
URHO3D_LOGINFO( RB->GetNode()->GetName()+" BEGIN!");
/// Collect the new collision
currentCollisions.Insert(weakRB);
}else{
/// Send "collision ongoing" event
newData[OnGhostCollisionStay::P_BODY] = RB;
newData[OnGhostCollisionStay::P_GHOST] = ghostObject_;
newData[OnGhostCollisionStay::P_GHOSTNODE] = node_;
RB->GetNode()->SendEvent(GHOST_COLLISION_STAY, newData);
URHO3D_LOGINFO( RB->GetNode()->GetName()+" STAY!");
}
}
}
/// Process any collisions which have ended
for(auto it=prevCollisions.Begin();it!=prevCollisions.End();it++){
/// Check that the object has not been destroyed, and that the collision has ceased
if( (*it)!=nullptr && !currentCollisions.Contains(*it))
{
VariantMap& newData = GetEventDataMap();
newData[OnGhostCollisionEnded::P_BODY] = *it;
newData[OnGhostCollisionEnded::P_GHOST] = ghostObject_;
newData[OnGhostCollisionEnded::P_GHOSTNODE] = node_;
(*it)->GetNode()->SendEvent(GHOST_COLLISION_ENDED, newData);
URHO3D_LOGINFO( (*it)->GetNode()->GetName()+" ENDED!");
}
}
/// Keep track of collisions across frames
prevCollisions = currentCollisions;
}
}