BGE physics: When colliding, report first contact point to Python
authorSybren A. Stüvel <sybren@stuvel.eu>
Thu, 5 Feb 2015 08:39:53 +0000 (09:39 +0100)
committerSybren A. Stüvel <sybren@stuvel.eu>
Sun, 8 Feb 2015 14:52:13 +0000 (15:52 +0100)
This patch adds two parameters to the functions in the
collisionCallbacks list. The callback function should thus be like
this:

```
def on_colliding(other, point, normal):
    print("Colliding with %s at %s with normal %s" % (other, point, normal))

game_ob.collisionCallbacks.append(on_colliding)
```

The `point` parameter will contain the collision point in world
coordinates on the current object, and the `normal` contains the
surface normal at the collision point.

The callback functions are checked for the number of arguments
`co_argcount`. The new `point` and `normal` arguments are only passed
when `co_argcount > 1` or when `co_argcount` cannot be determined.

Reviewers: brita_, campbellbarton

Subscribers: sergey, sybren, agoose77

Projects: #game_physics

Differential Revision: https://developer.blender.org/D926

doc/python_api/rst/bge_types/bge.types.KX_GameObject.rst
source/gameengine/Ketsji/KX_GameObject.cpp
source/gameengine/Ketsji/KX_GameObject.h
source/gameengine/Ketsji/KX_TouchEventManager.cpp
source/gameengine/Ketsji/KX_TouchEventManager.h
source/gameengine/Physics/Bullet/CcdPhysicsEnvironment.cpp

index 8b2edf183e44a28536cac3699d3405455e944d38..4a9e5dabe1bc42c8c6181276ce1e1d5541763316 100644 (file)
@@ -157,9 +157,46 @@ base class --- :class:`SCA_IObject`
 
    .. attribute:: collisionCallbacks
 
-      A list of callables to be run when a collision occurs.
+      A list of functions to be called when a collision occurs.
 
-      :type: list
+      :type: list of functions and/or methods
+
+      Callbacks should either accept one argument `(object)`, or three
+      arguments `(object, point, normal)`. For simplicity, per
+      colliding object only the first collision point is reported.
+
+      .. code-block:: python
+
+        # Function form
+        def callback_three(object, point, normal):
+            print('Hit by %r at %s with normal %s' % (object.name, point, normal))
+
+        def callback_one(object):
+            print('Hit by %r' % object.name)
+
+        def register_callback(controller):
+            controller.owner.collisionCallbacks.append(callback_three)
+            controller.owner.collisionCallbacks.append(callback_one)
+
+
+        # Method form
+        class YourGameEntity(bge.types.KX_GameObject):
+            def __init__(self, old_owner):
+                self.collisionCallbacks.append(self.on_collision_three)
+                self.collisionCallbacks.append(self.on_collision_one)
+
+            def on_collision_three(self, object, point, normal):
+                print('Hit by %r at %s with normal %s' % (object.name, point, normal))
+
+            def on_collision_one(self, object):
+                print('Hit by %r' % object.name)
+
+      .. note::
+        For backward compatibility, a callback with variable number of
+        arguments (using `*args`) will be passed only the `object`
+        argument. Only when there is more than one fixed argument (not
+        counting `self` for methods) will the three-argument form be
+        used.
 
    .. attribute:: scene
 
index e8b68d20e84209aa174990f2cf89487399f4fbb8..6d4b5564e193d56d23eee11433981364477d9b21 100644 (file)
@@ -77,6 +77,8 @@ typedef unsigned long uint_ptr;
 #include "BL_Action.h"
 
 #include "PyObjectPlus.h" /* python stuff */
+#include "BLI_utildefines.h"
+#include "python_utildefines.h"
 
 // This file defines relationships between parents and children
 // in the game engine.
@@ -1498,7 +1500,7 @@ void KX_GameObject::RegisterCollisionCallbacks()
                        pe->AddSensor(spc);
        }
 }
-void KX_GameObject::RunCollisionCallbacks(KX_GameObject *collider)
+void KX_GameObject::RunCollisionCallbacks(KX_GameObject *collider, const MT_Vector3 &point, const MT_Vector3 &normal)
 {
        #ifdef WITH_PYTHON
        Py_ssize_t len;
@@ -1506,15 +1508,50 @@ void KX_GameObject::RunCollisionCallbacks(KX_GameObject *collider)
 
        if (collision_callbacks && (len=PyList_GET_SIZE(collision_callbacks)))
        {
-               PyObject* args = Py_BuildValue("(O)", collider->GetProxy()); // save python creating each call
+               // Argument tuples are created lazily, only when they are needed.
+               PyObject *args_3 = NULL;
+               PyObject *args_1 = NULL; // Only for compatibility with pre-2.74 callbacks that take 1 argument.
+
                PyObject *func;
                PyObject *ret;
+               int co_argcount;
 
                // Iterate the list and run the callbacks
                for (Py_ssize_t pos=0; pos < len; pos++)
                {
                        func = PyList_GET_ITEM(collision_callbacks, pos);
-                       ret = PyObject_Call(func, args, NULL);
+
+                       // Get the number of arguments, supporting functions, methods and generic callables.
+                       if (PyMethod_Check(func)) {
+                               // Take away the 'self' argument for methods.
+                               co_argcount = ((PyCodeObject *)PyFunction_GET_CODE(PyMethod_GET_FUNCTION(func)))->co_argcount - 1;
+                       } else if (PyFunction_Check(func)) {
+                               co_argcount = ((PyCodeObject *)PyFunction_GET_CODE(func))->co_argcount;
+                       } else {
+                               // We'll just assume the callable takes the correct number of arguments.
+                               co_argcount = 3;
+                       }
+
+                       // Check whether the function expects the colliding object only,
+                       // or also the point and normal.
+                       if (co_argcount <= 1) {
+                               // One argument, or *args (which gives co_argcount == 0)
+                               if (args_1 == NULL) {
+                                       args_1 = PyTuple_New(1);
+                                       PyTuple_SET_ITEMS(args_1, collider->GetProxy());
+                               }
+                               ret = PyObject_Call(func, args_1, NULL);
+                       } else {
+                               // More than one argument, assume we can give point & normal.
+                               if (args_3 == NULL) {
+                                       args_3 = PyTuple_New(3);
+                                       PyTuple_SET_ITEMS(args_3,
+                                                         collider->GetProxy(),
+                                                         PyObjectFrom(point),
+                                                         PyObjectFrom(normal));
+                               }
+                               ret = PyObject_Call(func, args_3, NULL);
+                       }
 
                        if (ret == NULL) {
                                PyErr_Print();
@@ -1525,7 +1562,8 @@ void KX_GameObject::RunCollisionCallbacks(KX_GameObject *collider)
                        }
                }
 
-               Py_DECREF(args);
+               if (args_3) Py_DECREF(args_3);
+               if (args_1) Py_DECREF(args_1);
        }
        #endif
 }
index 4538679303fc78e5a3c8de48363ff885415e1153..5800cbc683406d6ce2a4d39880055ddef008187d 100644 (file)
@@ -919,7 +919,7 @@ public:
 
        void RegisterCollisionCallbacks();
        void UnregisterCollisionCallbacks();
-       void RunCollisionCallbacks(KX_GameObject *collider);
+       void RunCollisionCallbacks(KX_GameObject *collider, const MT_Vector3 &point, const MT_Vector3 &normal);
        /**
         * Stop making progress
         */
index 7ec379eec269484e5d3e46f42837a3872a319129..eb774960d41efeb1805c23ecabbc8dc7a8d69944 100644 (file)
@@ -60,7 +60,7 @@ bool  KX_TouchEventManager::NewHandleCollision(void* object1, void* object2, cons
        PHY_IPhysicsController* obj1 = static_cast<PHY_IPhysicsController*>(object1);
        PHY_IPhysicsController* obj2 = static_cast<PHY_IPhysicsController*>(object2);
        
-       m_newCollisions.insert(std::pair<PHY_IPhysicsController*, PHY_IPhysicsController*>(obj1, obj2));
+       m_newCollisions.insert(NewCollision(obj1, obj2, coll_data));
                
        return false;
 }
@@ -209,9 +209,11 @@ void KX_TouchEventManager::NextFrame()
                                }
                        }
                        // Run python callbacks
-                       kxObj1->RunCollisionCallbacks(kxObj2);
-                       kxObj2->RunCollisionCallbacks(kxObj1);
+                       PHY_CollData *colldata = cit->colldata;
+                       kxObj1->RunCollisionCallbacks(kxObj2, colldata->m_point1, colldata->m_normal);
+                       kxObj2->RunCollisionCallbacks(kxObj1, colldata->m_point2, -colldata->m_normal);
 
+                       delete cit->colldata;
                }
                        
                m_newCollisions.clear();
@@ -219,3 +221,19 @@ void KX_TouchEventManager::NextFrame()
                for (it.begin();!it.end();++it)
                        (*it)->Activate(m_logicmgr);
        }
+
+
+KX_TouchEventManager::NewCollision::NewCollision(PHY_IPhysicsController *first,
+                                                 PHY_IPhysicsController *second,
+                                                 const PHY_CollData *colldata)
+    : first(first), second(second), colldata(new PHY_CollData(*colldata))
+{}
+
+KX_TouchEventManager::NewCollision::NewCollision(const NewCollision &to_copy)
+       : first(to_copy.first), second(to_copy.second), colldata(to_copy.colldata)
+{}
+
+bool KX_TouchEventManager::NewCollision::operator<(const NewCollision &other) const
+{
+       return first < other.first || second < other.second || colldata < other.colldata;
+}
index bd4903c1545536a0c0ed8c012c9c5317955d6d1b..d9c6fdad307b0f3c28d3103aed970cde4c8b00a4 100644 (file)
@@ -45,7 +45,29 @@ class PHY_IPhysicsEnvironment;
 
 class KX_TouchEventManager : public SCA_EventManager
 {
-       typedef std::pair<PHY_IPhysicsController*, PHY_IPhysicsController*> NewCollision;
+       /**
+        * Contains two colliding objects and the first contact point.
+        */
+       class NewCollision {
+       public:
+               PHY_IPhysicsController *first;
+               PHY_IPhysicsController *second;
+               PHY_CollData *colldata;
+
+               /**
+            * Creates a copy of the given PHY_CollData; freeing that copy should be done by the owner of
+            * the NewCollision object.
+            *
+            * This allows us to efficiently store NewCollision objects in a std::set without creating more
+            * copies of colldata, as the NewCollision copy constructor reuses the pointer and doesn't clone
+            * it again. */
+               NewCollision(PHY_IPhysicsController *first,
+                            PHY_IPhysicsController *second,
+                            const PHY_CollData *colldata);
+               NewCollision(const NewCollision &to_copy);
+               bool operator<(const NewCollision &other) const;
+       };
+
        PHY_IPhysicsEnvironment*        m_physEnv;
        
        std::set<NewCollision> m_newCollisions;
index bcdc6f471ec2bd4797d3f49846aa2dfb66528749..c43a3f782a73ba8d18a2c0726e7dc8057c4c4a0c 100644 (file)
@@ -2261,6 +2261,7 @@ void      CcdPhysicsEnvironment::CallbackTriggers()
        int numManifolds = dispatcher->getNumManifolds();
        for (int i=0;i<numManifolds;i++)
        {
+               bool colliding_ctrl0 = true;
                btPersistentManifold* manifold = dispatcher->getManifoldByIndexInternal(i);
                int numContacts = manifold->getNumContacts();
                if (!numContacts) continue;
@@ -2289,12 +2290,27 @@ void    CcdPhysicsEnvironment::CallbackTriggers()
                if (iter == m_triggerControllers.end())
                {
                        iter = m_triggerControllers.find(ctrl1);
+                       colliding_ctrl0 = false;
                }
 
                if (iter != m_triggerControllers.end())
                {
+                       static PHY_CollData coll_data;
+                       const btManifoldPoint &cp = manifold->getContactPoint(0);
+
+                       /* Make sure that "point1" is always on the object we report on, and
+                        * "point2" on the other object. Also ensure the normal is oriented
+                        * correctly. */
+                       btVector3 point1 = colliding_ctrl0 ? cp.m_positionWorldOnA : cp.m_positionWorldOnB;
+                       btVector3 point2 = colliding_ctrl0 ? cp.m_positionWorldOnB : cp.m_positionWorldOnA;
+                       btVector3 normal = colliding_ctrl0 ? -cp.m_normalWorldOnB : cp.m_normalWorldOnB;
+
+                       coll_data.m_point1 = MT_Vector3(point1.m_floats);
+                       coll_data.m_point2 = MT_Vector3(point2.m_floats);
+                       coll_data.m_normal = MT_Vector3(normal.m_floats);
+
                        m_triggerCallbacks[PHY_OBJECT_RESPONSE](m_triggerCallbacksUserPtrs[PHY_OBJECT_RESPONSE],
-                               ctrl0,ctrl1,0);
+                               ctrl0, ctrl1, &coll_data);
                }
                // Bullet does not refresh the manifold contact point for object without contact response
                // may need to remove this when a newer Bullet version is integrated