Search code examples
python2dgame-physicschipmunkpymunk

Transfer of energy from one object to another in collision (in pymunk/chipmunk)


This question is about pymunk, but I'm aware that there are a lot more Chimpunk users out there; so if your answer involves C/Chipmunk code, it's okay. While I don't know how to write C code, I can usually figure out what's going on if I read someone else's.

The Setup

I'm simulating a top-down sliding object game (think curling or shuffleboard). I have made a minimal example of the relevant part of the code (at the end of the question), but the heart of it is:

  • Two identical physics objects are created (following curling terms, I call them 'stones')
  • their locations have the same x value but different y values (one is in a straight line above the other).
  • using apply_impulse (for now, though other methods were tried), the lower stone is 'launched' straight up at the other stone

What I'm hoping to achieve

When the stones collide, the lower stone should come to a sudden stop (or maybe bounce back a little - I'm not sweating that detail quite yet), while all or a majority of its energy is transferred to the upper stone, which would start moving upscreen.

What I'm getting instead

When the stones collide, the lower stone does not stop, and starts pushing the upper stone upscreen. It's as if the lower had more mass than the upper, but they're created with the same function so they should be the identical.

I've uploaded a .gif to imgur which illustrates this, if it helps: https://i.sstatic.net/A6B1N.jpg

It's a lower framerate than when actually running the script, but it still illustrates what's going on.

What I've tried

Reading through the pygame documentation, to try to identify all the body and shape properties that might be relevant, I've tried adjusting all of the following in various combinations:

  • body.mass
  • shape.friction
  • shape.elasticity
  • 'ground friction' (the max_force of the pivot constraint that simulates ground friction in a top-down scenario)
  • the 'power' at which the lower stone is launched (one of the arguments passed to apply_impulse)
  • using apply_force instead of apply_impulse

Tweaking any/all of these made noticeable and expected changes in the behavior of the stones, but none changed the fundamental problem of one stone pushing the other when they collide.

I have read about using pymunk.CollisionHandler(), but haven't tried using it yet. From the documentation, I get the sense that this is mainly intended for adding additional effects to collisions, not for modifying the basic physics of what happens at a collision in the first place. But I may have misunderstood and am open to any suggestion.

I have looked at several of the pymunk demos. Most notably, a demo called newtons_cradle.py exhibits the behavior I want. It's a simulation of one of those gadgets with five balls suspended in a row; when the user pulls one ball on the end back, it hits the rest of the row and the energy is transferred to the ball on the opposite side. newtons_crade.py only has two major differences from my code:

  • it's 'side-view' (so, gravity is greater than 0)
  • rather than use apply_impulse or apply_force, only the gravity is used to propel the ball toward the others (constrained by a constraint).

Regrettably, using gravity isn't an option in my top-down setup. So the problem might be my use of apply_impulse/apply_force, but I can't see any way to modify the way these are used (I've already tried various combinations of power and mass, as well adjusting the settings of the constraints).

Even pointing me in the right direction - i.e. some advice on what else I might read up on, what else I might try modifying - would be greatly appreciated. I can't be the first person to try this in pymunk/chipmunk, but I haven't been able to find an example. At least not on the pymunk side; if there's a good example in C/Chipmunk I could study, that'd be useful too.

Thank you all for your time.

Minimal Example Code

It's not necessary to study the code to understand the question, but I've posted it here just in case it's helpful. Although stripped down to show just heart of the code, it is a full script and can be run. It's in Python 3.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
import pygame
from pygame.locals import *
import pymunk
import pymunk.pygame_util

def add_and_tether_stone(space,sx,sy):
    """Creates a stone and its corresponding shape, and tethers it with constraints that simulate ground friction and govern spin."""
    #body
    mass = stone_mass
    radius = stone_radius
    moment = pymunk.moment_for_circle(mass, 0, radius)
    body = pymunk.Body(mass, moment)
    body.position = sx,sy
    #shape
    shape = pymunk.Circle(body, radius)
    shape.friction = stone_friction
    shape.elisticity = stone_elisticity
    space.add(body, shape)
    #constraints
    fpiv = pymunk.constraint.PivotJoint(space.static_body,body,(0.0,0.0),(0.0,0.0))
    fpiv.max_force = ground_friction
    fpiv.max_bias = 0.0
    fmot = pymunk.constraint.SimpleMotor(space.static_body,body,0)
    fmot.max_force = 5000000 #arbitry 'very high' value clamps down on the high rotation imparted by apply_impulse or apply_force
    space.add(fpiv)
    space.add(fmot)
    return body,shape,fpiv,fmot

def launch_stone(body,power):
    """Launches a stone in the manner of a player taking a shot."""
    body.apply_impulse_at_world_point((0,power),(0,0)) #force(x,y),offset(x,y)

def main():
    global ground_friction,stone_mass,stone_radius,stone_friction,stone_elisticity
    running = True
    #PyGame setup
    pygame.init()
    screen = pygame.display.set_mode((500,500))
    clock = pygame.time.Clock()
    sheet = pygame.Surface((500,500))
    sheetcolor = (0,0,0)
    sheet.fill(sheetcolor)
    sheet = sheet.convert()
    sheetblit = (0,0)
    screen.blit(sheet,sheetblit)

    #PyMunk setup
    space = pymunk.Space() #space.damping defaults to 1.0, and space.gravity defaults to (0.0, 0.0).
    draw_options = pymunk.pygame_util.DrawOptions(sheet) #used only for the pygame_util debug draw mode

    #Constants to Tweak
    stone_mass = 1.4
    stone_radius = 20
    power = 340 #in a full implementation, this would vary with player input
    ground_friction = 4.5
    stone_friction = 2.0
    stone_elisticity = 1.0

    #Setup for the minimal example: add two stones and launch one at the other.
    stone_a = add_and_tether_stone(space,40,260)
    stone_b = add_and_tether_stone(space,40,21)
    launch_stone(stone_b[0],power)

    while running:
        for event in pygame.event.get(): #listen for controls (all the controls except 'esc' have been removed for the minimal example)
            if event.type == KEYDOWN and event.key == K_ESCAPE:
                running = False
        #Draw, update physics, and advance
        sheet.fill(sheetcolor)
        space.debug_draw(draw_options) #from pymunk.pygame-util (handy!)
        screen.blit(sheet,sheetblit)
        space.step(1/50.0)
        pygame.display.flip()
        clock.tick(50)

if __name__ == '__main__':
    sys.exit(main())

Thank you all again for your time.


Solution

  • Maybe I missed something in your code (I only have command line python here so I cant run your script), but I cant recreate your problem.

    Here is a short code I tried which seems to work as you want:

    import pymunk
    
    s = pymunk.Space()
    
    b1 = pymunk.Body(1,10)
    b1.position = 0,0
    
    b2 = pymunk.Body(1,10)
    b2.position = 0,10
    
    c1 = pymunk.Circle(b1, 1)
    c1.elasticity = 1.0
    c2 = pymunk.Circle(b2, 1)
    c2.elasticity = 1.0
    
    j1 = pymunk.constraint.PivotJoint(s.static_body, b1, (0,0),(0,0))
    j1.max_force = 4.5
    j1.max_bias = 0
    
    j2 = pymunk.constraint.PivotJoint(s.static_body, b2, (0,0),(0,0))
    j2.max_force = 4.5
    j2.max_bias = 0
    
    j3 = pymunk.constraint.SimpleMotor(s.static_body,b1,0)
    j3.max_force = 5000000
    j4 = pymunk.constraint.SimpleMotor(s.static_body,b2,0)
    j4.max_force = 5000000
    
    s.add(b1,b2,c1,c2,j1,j2,j3,j4)
    
    b1.apply_impulse_at_world_point((0,30),(0,0))
    
    for x in range(25):
        s.step(0.02)
        print(b1.position, b2.position)
    

    This prints out this on my screen (so b1 stopped and all the movement is transferred to b2):

    Vec2d(0.0, 0.6) Vec2d(0.0, 10.0)
    Vec2d(0.0, 1.1982) Vec2d(0.0, 10.0)
    Vec2d(0.0, 1.7946) Vec2d(0.0, 10.0)
    Vec2d(0.0, 2.3891999999999998) Vec2d(0.0, 10.0)
    Vec2d(0.0, 2.9819999999999998) Vec2d(0.0, 10.0)
    Vec2d(0.0, 3.573) Vec2d(0.0, 10.0)
    Vec2d(0.0, 4.1622) Vec2d(0.0, 10.0)
    Vec2d(0.0, 4.7496) Vec2d(0.0, 10.0)
    Vec2d(0.0, 5.3352) Vec2d(0.0, 10.0)
    Vec2d(0.0, 5.9190000000000005) Vec2d(0.0, 10.0)
    Vec2d(0.0, 6.501) Vec2d(0.0, 10.0)
    Vec2d(0.0, 7.081200000000001) Vec2d(0.0, 10.0)
    Vec2d(0.0, 7.659600000000001) Vec2d(0.0, 10.0)
    Vec2d(0.0, 8.2362) Vec2d(0.0, 10.0)
    Vec2d(0.0, 8.228112001309862) Vec2d(0.0, 10.584682725252637)
    Vec2d(0.0, 8.228112001309862) Vec2d(0.0, 11.159477451815137)
    Vec2d(0.0, 8.228112001309862) Vec2d(0.0, 11.732472178377638)
    Vec2d(0.0, 8.228112001309862) Vec2d(0.0, 12.303666904940137)
    Vec2d(0.0, 8.228112001309862) Vec2d(0.0, 12.873061631502637)
    Vec2d(0.0, 8.228112001309862) Vec2d(0.0, 13.440656358065137)