2
0
Эх сурвалжийг харах

collide: Add custom owner field to CollisionNode

This stores a weakref to a custom Python object, useful for being able to look up the game object corresponding to a particular collision node in an event without the downsides of Python tags

Fixes #1703
rdb 1 жил өмнө
parent
commit
5e05049725

+ 2 - 0
panda/src/collide/CMakeLists.txt

@@ -74,6 +74,8 @@ set(P3COLLIDE_IGATEEXT
   collisionHandlerPhysical_ext.h
   collisionHandlerQueue_ext.cxx
   collisionHandlerQueue_ext.h
+  collisionNode_ext.cxx
+  collisionNode_ext.h
   collisionPolygon_ext.cxx
   collisionPolygon_ext.h
   collisionTraverser_ext.cxx

+ 21 - 0
panda/src/collide/collisionNode.I

@@ -166,3 +166,24 @@ INLINE CollideMask CollisionNode::
 get_default_collide_mask() {
   return default_collision_node_collide_mask;
 }
+
+/**
+ * Returns the custom pointer set via set_owner().
+ */
+INLINE void *CollisionNode::
+get_owner() const {
+  return _owner;
+}
+
+/**
+ * Sets a custom pointer, together with an optional callback that will be
+ * called when the node is deleted.
+ *
+ * The owner or the callback will not be copied along with the CollisionNode.
+ */
+INLINE void CollisionNode::
+set_owner(void *owner, OwnerCallback *callback) {
+  clear_owner();
+  _owner = owner;
+  _owner_callback = callback;
+}

+ 22 - 2
panda/src/collide/collisionNode.cxx

@@ -40,7 +40,9 @@ CollisionNode::
 CollisionNode(const std::string &name) :
   PandaNode(name),
   _from_collide_mask(get_default_collide_mask()),
-  _collider_sort(0)
+  _collider_sort(0),
+  _owner(nullptr),
+  _owner_callback(nullptr)
 {
   set_cull_callback();
   set_renderable();
@@ -60,7 +62,9 @@ CollisionNode(const CollisionNode &copy) :
   PandaNode(copy),
   _from_collide_mask(copy._from_collide_mask),
   _collider_sort(copy._collider_sort),
-  _solids(copy._solids)
+  _solids(copy._solids),
+  _owner(nullptr),
+  _owner_callback(nullptr)
 {
 }
 
@@ -69,6 +73,10 @@ CollisionNode(const CollisionNode &copy) :
  */
 CollisionNode::
 ~CollisionNode() {
+  if (_owner_callback != nullptr) {
+    _owner_callback(_owner);
+    _owner_callback = nullptr;
+  }
 }
 
 /**
@@ -251,6 +259,18 @@ set_from_collide_mask(CollideMask mask) {
   _from_collide_mask = mask;
 }
 
+/**
+ * Removes the owner that was previously set using set_owner().
+ */
+void CollisionNode::
+clear_owner() {
+  if (_owner_callback != nullptr) {
+    _owner_callback(_owner);
+  }
+  _owner = nullptr;
+  _owner_callback = nullptr;
+}
+
 /**
  * Called when needed to recompute the node's _internal_bound object.  Nodes
  * that contain anything of substance should redefine this to do the right

+ 19 - 0
panda/src/collide/collisionNode.h

@@ -77,6 +77,22 @@ PUBLISHED:
   INLINE static CollideMask get_default_collide_mask();
   MAKE_PROPERTY(default_collide_mask, get_default_collide_mask);
 
+public:
+  typedef void (OwnerCallback)(void *);
+
+  INLINE void *get_owner() const;
+
+#ifndef CPPPARSER
+  INLINE void set_owner(void *owner, OwnerCallback *callback = nullptr);
+  void clear_owner();
+#endif
+
+  EXTENSION(PyObject *get_owner() const);
+  EXTENSION(void set_owner(PyObject *owner));
+
+PUBLISHED:
+  MAKE_PROPERTY(owner, get_owner, set_owner);
+
 protected:
   virtual void compute_internal_bounds(CPT(BoundingVolume) &internal_bounds,
                                        int &internal_vertices,
@@ -94,6 +110,9 @@ private:
   typedef pvector< COWPT(CollisionSolid) > Solids;
   Solids _solids;
 
+  void *_owner = nullptr;
+  OwnerCallback *_owner_callback = nullptr;
+
   friend class CollisionTraverser;
 
 public:

+ 62 - 0
panda/src/collide/collisionNode_ext.cxx

@@ -0,0 +1,62 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file collisionNode_ext.cxx
+ * @author rdb
+ * @date 2024-12-12
+ */
+
+#include "collisionNode_ext.h"
+
+#ifdef HAVE_PYTHON
+
+#include "collisionNode.h"
+
+/**
+ * Returns the object previously set via set_owner().  If the object has been
+ * destroyed, returns None.
+ */
+PyObject *Extension<CollisionNode>::
+get_owner() const {
+  PyObject *owner = (PyObject *)_this->get_owner();
+
+#if PY_VERSION_HEX >= 0x030D0000 // 3.13
+  PyObject *strong_ref;
+  int result = 0;
+  if (owner != nullptr) {
+    PyWeakref_GetRef(owner, &strong_ref);
+  }
+  if (result > 0) {
+    return strong_ref;
+  }
+  else if (result == 0) {
+    return Py_NewRef(Py_None);
+  }
+  else {
+    return nullptr;
+  }
+#else
+  return Py_NewRef(owner != nullptr ? PyWeakref_GetObject(owner) : Py_None);
+#endif
+}
+
+/**
+ * Stores a weak reference to the given object on the CollisionNode, for later
+ * use in collision events and handlers.
+ */
+void Extension<CollisionNode>::
+set_owner(PyObject *owner) {
+  if (owner != Py_None) {
+    PyObject *ref = PyWeakref_NewRef(owner, nullptr);
+    _this->set_owner(ref, [](void *obj) { Py_DECREF((PyObject *)obj); });
+  } else {
+    _this->clear_owner();
+  }
+}
+
+#endif

+ 40 - 0
panda/src/collide/collisionNode_ext.h

@@ -0,0 +1,40 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file collisionNode_ext.h
+ * @author rdb
+ * @date 2024-12-12
+ */
+
+#ifndef COLLISIONNODE_EXT_H
+#define COLLISIONNODE_EXT_H
+
+#include "pandabase.h"
+
+#ifdef HAVE_PYTHON
+
+#include "extension.h"
+#include "collisionNode.h"
+#include "py_panda.h"
+
+/**
+ * This class defines the extension methods for CollisionNode, which are called
+ * instead of any C++ methods with the same prototype.
+ *
+ * @since 1.11.0
+ */
+template<>
+class Extension<CollisionNode> : public ExtensionBase<CollisionNode> {
+public:
+  PyObject *get_owner() const;
+  void set_owner(PyObject *owner);
+};
+
+#endif  // HAVE_PYTHON
+
+#endif

+ 1 - 0
panda/src/collide/p3collide_ext_composite.cxx

@@ -1,5 +1,6 @@
 #include "collisionHandlerEvent_ext.cxx"
 #include "collisionHandlerPhysical_ext.cxx"
 #include "collisionHandlerQueue_ext.cxx"
+#include "collisionNode_ext.cxx"
 #include "collisionPolygon_ext.cxx"
 #include "collisionTraverser_ext.cxx"

+ 2 - 0
panda/src/pgraph/nodePath.h

@@ -884,6 +884,8 @@ PUBLISHED:
   INLINE void set_collide_mask(CollideMask new_mask, CollideMask bits_to_change = CollideMask::all_on(),
                                TypeHandle node_type = TypeHandle::none());
 
+  EXTENSION(void set_collide_owner(PyObject *owner));
+
   // Comparison methods
   INLINE bool operator == (const NodePath &other) const;
   INLINE bool operator != (const NodePath &other) const;

+ 59 - 0
panda/src/pgraph/nodePath_ext.cxx

@@ -15,6 +15,7 @@
 #include "typedWritable_ext.h"
 #include "shaderInput_ext.h"
 #include "shaderAttrib.h"
+#include "collisionNode.h"
 
 #ifdef HAVE_PYTHON
 
@@ -327,4 +328,62 @@ get_tight_bounds(const NodePath &other) const {
   }
 }
 
+/**
+ * Recursively assigns a weak reference to the given owner object to all
+ * collision nodes at this level and below.
+ *
+ * You may pass in None to clear all owners below this level.
+ *
+ * Note that there is no corresponding get_collide_owner(), since there may be
+ * multiple nodes below this level with different owners.
+ */
+void Extension<NodePath>::
+set_collide_owner(PyObject *owner) {
+  if (owner != Py_None) {
+    PyObject *ref = PyWeakref_NewRef(owner, nullptr);
+    if (ref != nullptr) {
+      r_set_collide_owner(_this->node(), ref);
+      Py_DECREF(ref);
+    }
+  } else {
+    r_clear_collide_owner(_this->node());
+  }
+}
+
+/**
+ * Recursive implementation of set_collide_owner.  weakref must be a weak ref
+ * object.
+ */
+void Extension<NodePath>::
+r_set_collide_owner(PandaNode *node, PyObject *weakref) {
+  if (node->is_collision_node()) {
+    CollisionNode *cnode = (CollisionNode *)node;
+    cnode->set_owner(Py_NewRef(weakref),
+                     [](void *obj) { Py_DECREF((PyObject *)obj); });
+  }
+
+  PandaNode::Children cr = node->get_children();
+  int num_children = cr.get_num_children();
+  for (int i = 0; i < num_children; i++) {
+    r_set_collide_owner(cr.get_child(i), weakref);
+  }
+}
+
+/**
+ * Recursive implementation of set_collide_owner(None).
+ */
+void Extension<NodePath>::
+r_clear_collide_owner(PandaNode *node) {
+  if (node->is_collision_node()) {
+    CollisionNode *cnode = (CollisionNode *)node;
+    cnode->clear_owner();
+  }
+
+  PandaNode::Children cr = node->get_children();
+  int num_children = cr.get_num_children();
+  for (int i = 0; i < num_children; i++) {
+    r_clear_collide_owner(cr.get_child(i));
+  }
+}
+
 #endif  // HAVE_PYTHON

+ 6 - 0
panda/src/pgraph/nodePath_ext.h

@@ -54,6 +54,12 @@ public:
   void set_shader_inputs(PyObject *args, PyObject *kwargs);
 
   PyObject *get_tight_bounds(const NodePath &other = NodePath()) const;
+
+  void set_collide_owner(PyObject *owner);
+
+private:
+  static void r_set_collide_owner(PandaNode *node, PyObject *weakref);
+  static void r_clear_collide_owner(PandaNode *node);
 };
 
 BEGIN_PUBLISH

+ 46 - 0
tests/collide/test_collision_node.py

@@ -0,0 +1,46 @@
+from panda3d.core import *
+import sys
+
+
+class CustomObject:
+    pass
+
+
+def test_collision_node_owner():
+    owner = CustomObject()
+    initial_rc = sys.getrefcount(owner)
+
+    node = CollisionNode("node")
+    assert node.owner is None
+
+    node.owner = owner
+    assert sys.getrefcount(owner) == initial_rc
+    assert node.owner is owner
+
+    node.owner = owner
+    assert sys.getrefcount(owner) == initial_rc
+    assert node.owner is owner
+
+    node.owner = None
+    assert sys.getrefcount(owner) == initial_rc
+    assert node.owner is None
+
+    del node
+    assert sys.getrefcount(owner) == initial_rc
+
+    # Assign owner and then delete node
+    node = CollisionNode("node")
+    assert sys.getrefcount(owner) == initial_rc
+    node.owner = owner
+    assert sys.getrefcount(owner) == initial_rc
+    del node
+    assert sys.getrefcount(owner) == initial_rc
+
+    # Delete owner and see what happens to the node
+    node = CollisionNode("node")
+    assert sys.getrefcount(owner) == initial_rc
+    node.owner = owner
+    assert sys.getrefcount(owner) == initial_rc
+    del owner
+    assert node.owner is None
+

+ 44 - 0
tests/pgraph/test_nodepath.py

@@ -312,3 +312,47 @@ def test_nodepath_replace_texture_none():
     assert path2.get_texture() == tex1
     path1.replace_texture(tex1, None)
     assert path2.get_texture() is None
+
+
+def test_nodepath_set_collide_owner():
+    from panda3d.core import NodePath, CollisionNode
+
+    class CustomOwner:
+        pass
+
+    owner1 = CustomOwner()
+    owner2 = CustomOwner()
+    owner3 = CustomOwner()
+
+    root = NodePath("root")
+    model1 = root.attach_new_node("model1")
+    collider1 = model1.attach_new_node(CollisionNode("collider1"))
+    collider2 = model1.attach_new_node(CollisionNode("collider2"))
+    model2 = root.attach_new_node("model2")
+    collider3 = model2.attach_new_node(CollisionNode("collider3"))
+
+    root.set_collide_owner(owner1)
+    assert collider1.node().owner is owner1
+    assert collider2.node().owner is owner1
+    assert collider3.node().owner is owner1
+
+    model1.set_collide_owner(None)
+    assert collider1.node().owner is None
+    assert collider2.node().owner is None
+    assert collider3.node().owner is owner1
+
+    collider2.set_collide_owner(owner2)
+    assert collider1.node().owner is None
+    assert collider2.node().owner is owner2
+    assert collider3.node().owner is owner1
+
+    del owner1
+    assert collider1.node().owner is None
+    assert collider2.node().owner is owner2
+    assert collider3.node().owner is None
+
+    root.set_collide_owner(owner3)
+    model2.set_collide_owner(owner2)
+    assert collider1.node().owner is owner3
+    assert collider2.node().owner is owner3
+    assert collider3.node().owner is owner2