Search code examples
pythonpygamegame-physicspyglet

How to make object bounce forever at same height


I am trying to make a ball bounce forever, at the same height always, as it would if no energy was lost in the bounce.

I don't know why, depending on what I do it bounces higher as time goes by, or it bounces lower with every jump. My expectation is that in every bounce it should always reach the same height, not higher or lower.

This is my code (the code is in pyglet, but it is really easy to read even if you don't know the library). Here, a square is dropped from the middle of the screen, marked by an arrow, and I expect the cube to bounce in the floor and go exactly to the original height when coming back:

import pyglet


class Polygon(object):
    def __init__(self, vertices, color, velocity=0, acceleration=-600):
        self.vertices = vertices
        self.y_idx = 1
        self.num_vertices = int(len(self.vertices) // 2)
        self.colors = color * self.num_vertices
        self.velocity = velocity
        self.acceleration = acceleration
        self.bottom_edge = 0

    def draw(self):
        self.vertex_list = pyglet.graphics.vertex_list(self.num_vertices,
                                                       ("v2f", self.vertices),
                                                       ("c3B", self.colors))
        self.vertex_list.draw(pyglet.gl.GL_POLYGON)

    def move_by_offset(self, offset):
        for i in range(1, len(self.vertices), 2):
            self.vertices[i] += offset  # only modify y values

    def bounce(self, dt):
        if self.vertices[self.y_idx] < self.bottom_edge:
            self.velocity = abs(self.velocity)
            return True
        return False

    def update(self, dt):
        # move
        self.move_by_offset(self.velocity * dt)
        # check if bounce
        self.bounce(dt)
        # accelerate
        self.velocity += self.acceleration * dt


class GameWindow(pyglet.window.Window):
    def __init__(self, objects=None, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.objects = objects

    def update(self, dt):
        for obj in self.objects:
            obj.update(dt)

    def on_draw(self):
        self.clear()
        for obj in self.objects:
            obj.draw()


class Game(object):
    def __init__(self, w=400, h=400, title="My game", resizable=False):
        self.w = w
        self.h = h
        objects = [
            # square
            Polygon(vertices=[w/2-20, h/2, w/2-20, h/2+40, w/2+20, h/2+40, w/2+20, h/2],
                    color=[0, 128, 32],  # green
                    velocity=0,
                    acceleration=-6000),
            # arrow, marks exactly how high the square should bounce
            Polygon(vertices=[w/2, h/2, w/2+40, h/2+20, w/2+30, h/2, w/2+40, h/2-20],
                    color=[255, 255, 0], # yellow
                    velocity=0,
                    acceleration=0)
        ]
        self.window = GameWindow(objects, self.w, self.h, title, resizable)

    def update(self, dt):
        self.window.update(dt)


if __name__ == "__main__":
    game = Game(resizable=False)
    pyglet.clock.schedule_interval(game.update, 1/120)
    pyglet.app.run()

I tried different update orders, like modifying speed according to acceleration before changing velocity after bounce, or even not accelerating at all after bounce (which seems to work the best) but still the bounce is not accurate and changes heights constantly:

    def update2(self, dt):
        # move
        self.move_by_offset(self.velocity * dt)
        # accelerate
        self.velocity += self.acceleration * dt
        # check if bounce
        self.bounce(dt)

    def update3(self, dt):
        # move
        self.move_by_offset(self.velocity * dt)
        # check if bounce
        bounced = self.bounce(dt)
        if not bounced:
            # accelerate (only if no bounce happened)
            self.velocity += self.acceleration * dt

I even tried something more complex: creating 2 dt's, one before and one after bounce, and doing 2 acceleration updates, but this did not work either.

Can you guys help me? What is the way to program game physics for such a simple scenario?


Solution

  • Numerical integration is hard! Since you can easily solve the one-dimensional ballistic equation exactly, do so: compute

    y1=y0+v0*dt+g*dt*dt/2
    v1=v0+g*dt
    

    This is the velocity Verlet method in the trivial case of a constant acceleration. If y1<0, you can solve the quadratic equation to find out when it bounced and restart the integration from that point (with the velocity negated).

    If you want to incorporate more complicated physics while still being numerically accurate, consider the centering of your velocity variable. Better accuracy can be had by staggering it—defining it at points in time that are halfway between the points at which position is defined gives the similar leapfrog method.

    A very different approach for conservative forces is to define a total energy for the ball and move it according to how much of it must be kinetic based on its height. Even then, you have to include the above correction with dt*dt to avoid numerical problems near the maximum altitude.