NodePath inheritance - could we solve with aggregation?

Hey all,

It seems like the workaround for NodePath inheritance issues is well known, that being to create a circular reference with your custom class and attaching it to your node path with setPythonTag.

I was think about this problem again and wondered, can we fix it with aggregation instead of inheritance?

Eg:

import pandac.PandaModules as pm
from direct.showbase.ShowBase import ShowBase


PSUEDO_TAG = 'psuedoTag'


class PsuedoNodePath():
    
    def __init__( self, np ):

        # Obviously we would copy the entire interface
        # from NodePath
        self.setPos = np.setPos
        self.removeNode = np.removeNode
        self.reparentTo = np.reparentTo
        np.setPythonTag( PSUEDO_TAG, self )
        
    def __del__( self ):
        print 'deleted!'
    

base = ShowBase()
base.cam.setPos( pm.Vec3( 0, -10, 0 ) )

smiley = PsuedoNodePath( loader.loadModel( 'smiley' ) )
smiley.reparentTo( render )


base.run()

You still have to call getPythonTag( PSUEDO_TAG ) to get your custom class, but calling removeNode() on the node path shouldn’t leave you with a memory leak as there is no circular reference you need to break beforehand.

Is this something anyone has tried before?

Hmm… Interesting. I’m perhaps a little tired now to properly assess your code, but one issue that does occur to me is that it seems to call for function assignments for each NodePath method that you want access to - and that there are rather a lot of methods in NodePath.

What I’ve done, I believe, if I’m correct in thinking that it solves the same problem, is to aggregate the NodePath as a member of my class; access to methods of NodePath is then done via this instance member. However, I tend to not access my classes through the node hierarchy, instead controlling the hierarchy through my classes. When I do want access through the node hierarchy - generally in order to handle collisions, I think - I confessedly do tend to use the “circular reference” method.

[edit]
… Come to think of it, I’m not sure that your solution does actually avoid circular references. I am, confessedly, uncertain, but I think that there may still be a reference to the instance in those method references, meaning that you may still have a circular reference.

Absolutely - which is why some automation would be useful…

import pandac.PandaModules as pm 
from direct.showbase.ShowBase import ShowBase 


PSUEDO_TAG = 'psuedoTag' 


class PsuedoNodePath(): 
    
    def __init__( self, np ): 

        # Copy the entire interface from NodePath.
        for name, value in pm.NodePath.__dict__.items():
            setattr( self, name, getattr( np, name ) )
        
        np.setPythonTag( PSUEDO_TAG, self ) 

    def __del__( self ): 
        print 'deleted!' 
    

np = pm.NodePath( 'test' )
pnp = PsuedoNodePath( np )
pnp.setPos( 1,1,1 )

print np.getPos()
print pnp.getPos()

You might well be right - I haven’t done enough testing to be certain. I have done a reference count however, and it seems like keeping a reference to a.b doesn’t keep a reference to a. I’ve also tried removing the node and falling out of scope, and the del statement is printed, leaving me to believe the nodepath was destroyed properly.

Know of a way to test this at all?

This code:

class PsuedoNodePath(): 
    def __init__( self, np ): 
        self.setPos = np.setPos 
        self.removeNode = np.removeNode 
        self.reparentTo = np.reparentTo 
        np.setPythonTag( PSUEDO_TAG, self ) 

That’s still uses circular references. Look at this Python session:

>>> np = NodePath('some_node')
>>> sys.getrefcount(np)
2
>>> mySetPos = np.setPos
>>> sys.getrefcount(np)
3

You can verify that mySetPos holds a reference to np by calling gc.get_referrers(np), you’ll see that mySetPos is in there.
So, your circular reference is PseudoNodePath -> setPos ->np -> PseudoNodePath.

You could try experimenting with Python’s weakref module to set the tag to be a weak reference, and seeing if that would solve the problem.

The real solution is not to inherit from NodePath, of course, but inherit from PandaNode, not use a tag at all, but that would also require us to switch from interrogate to a Python wrapper generator that properly supports bidirectional polymorphism between Python and C++. They exist, and there has been talk of upgrading to such a system, but we’re talking long term here.

Dang, I thought I was on to something there. Thanks for the clear explanation, guys.