Scene Cleanup Design Patterns

Hey,
I’m a bit overwhelmed when it comes to cleaning up objects with complex dependencies and would like to ask you all what your design choices are when it comes to creating and removing objects or scenes cleanly.

I’ve found various issues regarding removing nodes and cleaning up things, but I’d like to know if there are any recommended design choices for how to handle object/scene removing, level changing etc.
There are many good tips here in the manual, but it doesn’t fully answer my questions.

For example, I would like to cleanly remove an NPC which has many complicated dependencies (pathfinding, collision, IK joints…).
My approach would be to let every class throughout the project remove what it created itself (NodePaths, tasks, custom class instances…), however this isn’t always straight forward:

  1. I have a factory class to create the NPC (to split the NPC setup process from the later NPC updating). Then the factory class should probably also remove the NPC? However in this case the NPC would probably have to call the factory’s “removeNPC()” function, which creates an (ugly) cyclic dependency. Alternatively, the NPC could not know at all that it is being removed, which is also strange.
  2. To add and remove objects to/from the bullet collision, I need to have access to the BulletWorld. Passing instances of the BulletWorld to each object’s “remove()” member function (where it is used to remove the object from the BulletWorld) seems ugly. Having the scene or factory class remove the object from the BulletWorld seems even uglier, since then the Scene/Factory needs to know too much about the object’s internal structures.
  3. How do you cleanly handle references between objects, i.e. if a missile object is following an NPC while the NPC is being deleted? I’d hate for the NPC to have to hold a reference to the missile object in order to tell it that it’s being deleted…

This would generally be my core idea too, I daresay.

How I’d handle this might depend on how I was otherwise handling the NPC.

For example, if I had another object handling the “lifetime” of the NPC–something calling its “update” method(s), managing it in other ways, etc.–then that might take responsibility too for its destruction.

Here I think that I would likely keep a broadly-imported “common” script which provides easy access to things like the BulletWorld (or some other class that handles the BulletWorld). This would then allow me to have NPCs clean up their physics elements themselves, and without passing the BulletWorld into every applicable method.

Something like this:

class Common
    framework = None

    # Other variables and perhaps some common functions follow...

Which might then be used something like this:

# The usual Panda importations here...

from Common import Common

class Game(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)
        Common.framework = self

        self.bulletWorld = BulletWorld( ... )

And then, in some other file:

from Common import Common

class NPC:
    # Constructor and other methods here...

    def destroy(self):
        Common.framework.bulletWorld.remove(self.collider)

And so on.

(Of course, you needn’t implement this in exactly the above manner. For example, you might prefer to use a bare script instead of a class to store your “common” stuff, you might organise things differently, and so on. The above is just an example.)

You might be able to have the missile check each update whether the NPC is still valid.

Otherwise, you might indeed have the NPC keep a list of things that might be interested in its destruction, and notify them on that event.

2 Likes

I have come to the conclusion that creating a NodePath to communicate with BulletWorld is unjustified. I use simple position copying in a loop to synchronize their position. This contributes to a cleaner object tree, and simplifies the logic. So the object adds itself to the list of objects that require synchronization and also removes itself from it if the destructor was called. But there is another python problem here, the destructor will not be executed if there is a link to the class somewhere. I circumvent this problem banally using builtins:

builtins.fps = fps()

class FreeCamera():
    def __init__(self):

        self.camera = NodePath(self.camera_class)
        self.camera.set_sx(fps.win.get_x_size()/fps.win.get_y_size())
        self.camera.set_pos(0, 0, 0)
        self.camera.reparent_to(fps.world)

        self.mouse_sens = 0.05

        self.mouse3_stage = False

    def move(self):

        dt = fps.clock.dt

        if not(fps.key['w'] and fps.key['s'] and fps.key['d'] and fps.key['a']):

            if fps.key['shift']:
                speed = 180
            else:
                speed = 5
...

I just don’t use it like that.

def __init__(self, fps):
    self.fps = fps
class FreeCamera():
    def __init__(self, fps):
    
        self.fps = fps

        self.camera = NodePath(self.camera_class)
        self.camera.set_sx(self.fps.win.get_x_size()/self.fps.win.get_y_size())
        self.camera.set_pos(0, 0, 0)
        self.camera.reparent_to(fps.world)

        self.mouse_sens = 0.05

        self.mouse3_stage = False

    def move(self):

        dt = self.fps.clock.dt

        if not(self.fps.key['w'] and self.fps.key['s'] and self.fps.key['d'] and self.fps.key['a']):

            if self.fps.key['shift']:
                speed = 180
            else:
                speed = 5
...

In other words, don’t use assignment for objects and avoid it in every possible way. Try to work directly with the *pointer as C++

  • python does not have this concept, however, if you always refer to a single link, it will be like -> to analog С++.
1 Like

Hey @Thaumaturge and @serega-kkz thanks a lot for the hints!

I’ll have to sort through these and some further recommendations I got on Discord (like using an ECS instead - but after a bit of back-and-forth I think I don’t want to go down that route).
I do like the idea of using a “Common” class to hold globals. Even though I usually dislike globals, it seems like a clean solution here.
Separating collision objects from the scene also sounds very interesting. However I do like though that in my current setup, debugging is quite straight forward (simply done by enabling visualization on the collision nodes, which are part of the normal scene graph) and I can be sure that I never loose synchronization anywhere. Despite this, I could still outsource collision handling more into separate classes…

@serega-kkz Sorry, I don’t undestand your second point. What are you trying to (potentially) delete in your examples? What’s “fps”?

1 Like

This is a regular class with important variables, which is equivalent to the example with the common module