Collision System: Changing Radius Prevents Events?

I’ve stumbled upon an odd behaviour, and I’m not sure of whether it’s a bug, or whether I’m doing something with an undefined result:

In broad strokes, I have two CollisionNodes, intended to collide with each other. However, instead of moving one into the other, I’m changing the radius of one of the solids in a task, until that solid’s CollisionNode thus engulfs the other CollisionNode.

(Note for clarity: I’m not scaling the node; I’m changing the radius of the solid.)

The problem is that… it doesn’t work.

Or rather, it works perfectly well as long as the expanding CollisionNode is the “from”-object. When the expanding CollisionNode is instead the “into”-object, I get neither “in” nor “again” events.

Here below is a short test-program that demonstrates the issue on my machine. It creates two CollisionNodes, and offers buttons to start a task that either changes the radius of or moves one of those nodes, sufficiently that it should collide with the other node. Two labels indicate whether an “in” or “again” event has fired.

You should find that when the “Position” button is pressed and one node moves to encounter the other, both labels “light up”. And that conversely, when the “Scale” button is pressed and one node changes radius to encounter the other, neither label “lights up”.

You should also find that if you change the call to “addCollider” to use the node that is altered instead of the other node, the labels should “light up” for both approaches.

from direct.showbase.ShowBase import ShowBase

from panda3d.core import CollisionNode, CollisionSphere, CollisionTraverser, CollisionHandlerEvent
from direct.gui.DirectGui import *

from panda3d import __version__ as pandaVersion
print (pandaVersion)

import sys
print (sys.version)

class Game(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

        self.scale = False
        self.timer = 0
        self.speed = 0.5
        self.resetting = False

        node = CollisionNode("mew")
        self.solid = CollisionSphere(-2, 10, 0, 1)
        node.addSolid(self.solid)
        self.np1 = render.attachNewNode(node)
        self.np1.show()

        node = CollisionNode("mew 2")
        self.solid2 = CollisionSphere(2, 10, 0, 1)
        node.addSolid(self.solid2)
        self.np2 = render.attachNewNode(node)
        self.np2.show()

        self.cTrav = CollisionTraverser()
        self.handler = CollisionHandlerEvent()
        self.handler.addAgainPattern("again")
        self.handler.addInPattern("into")
        self.cTrav.addCollider(self.np2, self.handler)

        self.accept("again", self.again)
        self.accept("into", self.into)

        self.label1 = DirectLabel(text = "INTO", scale = 0.25, frameColor = (0.1, 0.1, 0.1, 1), pos = (0, 0, -0.65))
        self.label2 = DirectLabel(text = "AGAIN", scale = 0.25, frameColor = (0.1, 0.1, 0.1, 1), pos = (0, 0, -0.9))

        self.btn1 = DirectButton(text = "Scale", scale = 0.2, command = self.startScaling, pos = (0, 0, 0.8))
        self.btn2 = DirectButton(text = "Position", scale = 0.2, command = self.startPositioning, pos = (0, 0, 0.55))
        
    def startScaling(self):
        self.scale = True
        self.taskMgr.add(self.update, "update")
        self.btn1.hide()
        self.btn2.hide()

    def startPositioning(self):
        self.scale = False
        self.timer = 1
        self.taskMgr.add(self.update, "update")
        self.btn1.hide()
        self.btn2.hide()

    def again(self, entry):
        self.label2["frameColor"] = (0.3, 0.7, 0.5, 1)

    def into(self, entry):
        self.label1["frameColor"] = (0.3, 0.7, 0.5, 1)

    def update(self, task):
        # Reset the demo
        if self.resetting:
            self.timer -= self.clock.getDt()
            if self.timer <= 0:
                self.resetting = False
                self.btn1.show()
                self.btn2.show()
                self.solid.setRadius(1)
                self.np1.setX(0)
                self.label1["frameColor"] = (0.1, 0.1, 0.1, 1)
                self.label2["frameColor"] = (0.1, 0.1, 0.1, 1)
                return task.done
            return task.cont
        # The actual logic: either scale the solid,
        #  or move its node-path
        if self.scale:
            if self.solid.getRadius() < 5:
                self.solid.setRadius(self.solid.getRadius() + self.speed * self.clock.getDt())
            else:
                self.resetting = True
                self.timer = 1
        else:
            if self.np1.getX() < 3:
                self.np1.setX(self.np1, self.speed * self.clock.getDt())
            else:
                self.resetting = True
                self.timer = 1
        return task.cont


app = Game()
app.run()

Thoughts…?

Hi Thaumaturge,

Interesting problem you’ve run into here! :smiley:
Just had a little play around and what I suspect is happening, is the self.solid you’re modifying in your update function isn’t the same object that’s added to CollisionNode("mew").

Looking at the online documentation for the CollisionNode class, I came across a function modifySolid which seems to be what you’re after. (You can confirm that the objects aren’t the same by is checking the result of this with self.solid.)
Further investigation shows that this technique is how the ProjectileInterval is working, so I assume it’s the right way to be doing it! :stuck_out_tongue:

In your update function, rather than using self.solid, use the result from modifySolid(0) caveat being that’s your first added solid on the node on your CollisionNode("mew") (either store that, or pull it from self.np1.node()) to check and modify the radius of the CollisionSphere.

            solid = self.np1.node().modifySolid(0)
            print(solid is self.solid) # This will be false
            if solid.getRadius() < 5:
                solid.setRadius(solid.getRadius() + self.speed * self.clock.getDt())

This should get you both the into and again collision events that you’re after!

Curious to see what you’re using this scaling sphere for! (If you’re willing to share :slight_smile: No pressure though!)

Cheers,
H3LLB0Y.

P.S. Guess you’ll need to update your reset to use the modifySolid too.
Exercise for the reader that one :wink:

1 Like

Thank you for your answer! :slight_smile:

Hmm… I was aware of “modifySolid”–but this approach of simply keeping a reference to the relevant solid has (seemed to) work well in other places.

[edit] Not to mention that, with the collision-node made visible, as in this test-program, the solid can be seen to expand–suggesting that it is the correct solid… o_0 [/edit]

And indeed, it can be seen to work here–when the solid in question belongs to a “from”-object!

And yet–trying “modifySolid” in my test-program, it does indeed solve the problem!

Very odd!

I do wonder then why the behaviour is different for “from”-objects…

One way or another, I’ll have to keep an eye open for that, I think!

Thank you again! :slight_smile:

1 Like

Hi again Thaumaturge,

I was wondering more about your problem, and currently thinking that we’re haven’t reached the bottom of the issue as yet.

I too, am perplexed as to why the visual is updated but isn’t triggering the collision when using self.solid.

Looking into the C++ code for the CollisionSphere class, we see the following:

INLINE void CollisionSphere::
set_radius(PN_stdfloat radius) {
  nassertv(radius >= 0.0f);
  _radius = radius;
  mark_internal_bounds_stale();
  mark_viz_stale();
}

So it seems to be calling this mark_internal_bounds_stale() but perhaps for some reason it’s not getting applied at the CollisionNode that it belongs to.

Looking at the C++ code for the CollisionNode class, we see the following:

INLINE PT(CollisionSolid) CollisionNode::
modify_solid(size_t n) {
  nassertr(n < get_num_solids(), nullptr);
  mark_internal_bounds_stale();
  return _solids[n].get_write_pointer();
}

So it is calling mark_internal_bounds_stale() itself, which appears to work as expected.

If we try calling this ourselves following the setRadius call on self.solid seems to be enough to make things work as expected. (No modifySolid() call or anything else required.)
self.np1.node().mark_internal_bounds_stale()
Adding this single explicit call to your original code seems to make everything work.
This is an indication that the self.solid rather than using modifySolid result was not the issue at all apologies for that suggestion on my part. :sweat_smile:

This is actually a call to the parent class PandaNode. (CollisionSolid C++ class actually has the same function too, which is what the CollisionSphere is calling that doesn’t seem to work.)
https://docs.panda3d.org/1.10/python/reference/panda3d.core.PandaNode#panda3d.core.PandaNode.markInternalBoundsStale
The API documentation states the following, which makes me wonder if something perhaps isn’t working as expected.

It is normally not necessary to call this method directly; each node should be responsible for calling it when its internals have changed.

Perhaps there is an issue with how the flag propogates from the CollisionSolid that means it’s not applying correctly at the CollisionNode/PandaNode level. :thinking:

If I have time I’ll look into getting the engine built locally so I can investigate further!

If there’s anyone with more understanding of the internals who could perhaps shed some light on this it would be much appreciated. :slight_smile:

Cheers,
H3LLB0Y.

(Before I start, let me note that I’ve subsequently found an alternative–and much simpler–approach to achieving what I was trying to achieve with this. As a result, there’s no longer any urgency on my part in the addressing of this.

Still, it does remain unexpected behaviour, I feel.)

~

Ah, interesting! It turns out to be a slightly more-complex issue than at first thought!

Hmm… I don’t know: I would argue that this indicates that it’s not a method that developers should expect to use, and that rather the internals of the node would be expected to detect the change and call the method in question.

I think that you might be onto something with regards to propagation of the call!

No, not at all! You weren’t entirely wrong there–you just weren’t right for the reason that you thought you were! And your suggestion did work, so it was a potentially-helpful answer! :slight_smile:

1 Like