pythonchipmunkpymunkarcade

Why do objects start spinning after colliding with a floor in Python Arcade/Pymunk?


Why do rectangular object without rotation start spinning after colliding with a flat wall in Python Arcade/Pymunk? How to solve this problem?

Let the rectangular object has angular velocity 0, angle 0, center of gravity (0,0) and moves straight. After which the rectangular object collides with a flat wall. I noticed that rectangular objects begin to spin and shift after a collision with walls (static objects) in Arcade. Depending on the ratio of height and width, the turning speed is different. But it always present.

enter image description here

Since I use Python Arcade and Pymunk, I see the following possible solutions to the problem:

  1. Set moment of object to INF. But then object never rotate so this is strange solution.
  2. Get all calculations (related to the collision) from Chipmunk in hit_handler via _arbiter._arbiter: pymunk._chipmunk.ffi.CData. Somehow process the data from CData in Python and figure out how to fix the data that causes the error described above. It would be great if someone could tell me how to do this.
  3. Perform calculations for velocity and angular_velocity from scratch in Python. But this method essentially doubles the number of calculations.

It would be great if someone tell another way.

Perhaps this is a bug in the Chipmunk2d library and there is no simple way. I've been struggling with this problem for a very long time. Thank you in advance for any help.

Arcade 2.6.17 Pymunk 6.4.0 Python 3.10.14

This is minimal reproducible example (without Arcade for minimum code):

import pyglet

import pymunk
import pymunk.pyglet_util
from pymunk import Vec2d


class Main(pyglet.window.Window):
    def __init__(self):

        pyglet.window.Window.__init__(self, vsync=False)

        pyglet.clock.schedule_interval(self.update, 1 / 60.0)
        self.fps_display = pyglet.window.FPSDisplay(self)

        self.create_world()

        self.draw_options = pymunk.pyglet_util.DrawOptions()
        self.draw_options.flags = self.draw_options.DRAW_SHAPES

    def create_world(self):
        self.space = pymunk.Space()
        self.space.gravity = Vec2d(0.0, -9.0)
        self.space.sleep_time_threshold = 0.3

        static_lines = [
            pymunk.Segment(self.space.static_body, Vec2d(20, 55), Vec2d(600, 55), 1),
            pymunk.Segment(self.space.static_body, Vec2d(550, 55), Vec2d(550, 400), 1),
        ]
        for l in static_lines:
            l.friction = 0.3
            l.elasticity = 1
        self.space.add(*static_lines)

        size = 20
        mass = 1.0
        moment = pymunk.moment_for_box(mass, (size//2, size))
        body = pymunk.Body(mass, moment)
        body.position = Vec2d(300 + 0 * 50, 105 + 5 * (size + 0.1))
        shape = pymunk.Poly.create_box(body, (size//2, size))
        shape.elasticity = 1
        shape.friction = 0.3
        self.space.add(body, shape)

    def update(self, dt):
        self.space.step(dt)

    def on_draw(self):
        self.clear()
        self.fps_display.draw()
        self.space.debug_draw(self.draw_options)


def main():
    main = Main()
    pyglet.app.run()


if __name__ == "__main__":
    main()

Solution

  • My solution - change velocity by bounce from arbiter:CData and change angular velocity in special case. So we have next steps:

    1. Setting up FFI (use this file )
    self.ffibuilder = FFI()
    with open(r"Path\chipmunk_cdef.h", "r") as f:
        self.ffibuilder.cdef(f.read())
    
    1. Add pre_handler to remember angular velocity
    def pre_handler(_arbiter, _space, _data):
        _data["a_w"] = _arbiter.shapes[0].body.angular_velocity
        return True
    
    1. Add post_handler to change velocity in case (prev. angular velocity small and contact points have similar distances)
    def post_handler(ffibuilder, _arbiter, _space, _data):
        arb = ffibuilder.cast("struct cpArbiter *",_arbiter._arbiter)
        n = (arb.n.x, arb.n.y)
        distances = [i.distance for i in _arbiter.contact_point_set.points]
        if distances.count(distances[0]) == len(distances) and abs(_data["a_w"])<=0.1**15:
            _arbiter.shapes[0].body.velocity = cpvmult(n,abs(arb.contacts[0].bounce))
            _arbiter.shapes[0].body.angular_velocity = 0.0
    

    Collision slop and bias also affect. We can set self.space.collision_slop = N (N max number what you can use, N=2 for me) or set some custom calculations in arb (post_handler).

    For Python Arcade answer is the same.

    Full code:

    import pyglet
    
    import pymunk
    import pymunk.pyglet_util
    from pymunk import Vec2d
    from cffi import FFI
    
    def pre_handler(_arbiter, _space, _data):
        _data["a_w"] = _arbiter.shapes[0].body.angular_velocity
        return True
    
    def post_handler(ffibuilder, _arbiter, _space, _data):
        arb = ffibuilder.cast("struct cpArbiter *",_arbiter._arbiter)
        n = (arb.n.x, arb.n.y)
        distances = [i.distance for i in _arbiter.contact_point_set.points]
        if distances.count(distances[0]) == len(distances) and abs(_data["a_w"])<=0.1**15:
            _arbiter.shapes[0].body.velocity = cpvmult(n,abs(arb.contacts[0].bounce))
            _arbiter.shapes[0].body.angular_velocity = 0.0
                
    def cpvmult(t1,number):
        return t1[0]*number,t1[1]*number
    
    class Main(pyglet.window.Window):
        def __init__(self):
    
            pyglet.window.Window.__init__(self, vsync=False)
    
            pyglet.clock.schedule_interval(self.update, 1 / 60.0)
            self.fps_display = pyglet.window.FPSDisplay(self)
    
            self.create_world()
    
            self.draw_options = pymunk.pyglet_util.DrawOptions()
            self.draw_options.flags = self.draw_options.DRAW_SHAPES
            
            self.ffibuilder = FFI()
            with open(r"C:\Users\HYPERPC\Downloads\chipmunk_cdef.h", "r") as f:
                self.ffibuilder.cdef(f.read())
    
        def add_handler(self, shape, pre_handler=None,post_handler=None):
    
            def _f1(arbiter, space, data):
                post_handler(self.ffibuilder, arbiter, space, data)
    
            def _f2(arbiter, space, data):
                return pre_handler(arbiter, space, data)
    
            h = self.space.add_wildcard_collision_handler(shape.collision_type)
            if post_handler:
                h.post_solve = _f1
            if pre_handler:
                h.pre_solve = _f2
        
        def create_world(self):
            self.space = pymunk.Space()
            self.space.gravity = Vec2d(0.0, -9.0)
            self.space.sleep_time_threshold = 0.3
            self.space.collision_slop = 2
            static_lines = [
                pymunk.Segment(self.space.static_body, Vec2d(20, 55), Vec2d(600, 55), 1),
                pymunk.Segment(self.space.static_body, Vec2d(550, 55), Vec2d(550, 400), 1),
            ]
            for l in static_lines:
                l.friction = 0.3
                l.elasticity = 1
            self.space.add(*static_lines)
    
            size = 20
            mass = 1.0
            moment = pymunk.moment_for_box(mass, (size//2, size))
            body = pymunk.Body(mass, moment)
            body.position = Vec2d(300 + 0 * 50, 105 + 5 * (size + 0.1))
            shape = pymunk.Poly.create_box(body, (size//2, size))
            shape.elasticity = 1
            shape.friction = 0.3
            self.space.add(body, shape)
            
            self.add_handler(shape, pre_handler = pre_handler)
            self.add_handler(shape, post_handler = post_handler)
    
        def update(self, dt):
            self.space.step(dt)
    
        def on_draw(self):
            self.clear()
            self.fps_display.draw()
            self.space.debug_draw(self.draw_options)
    
    
    def main():
        main = Main()
        pyglet.app.run()
    
    
    if __name__ == "__main__":
        main()