Search code examples
pythonpygame

Make parabolic movement frame independent


My code has a box that gets pushed with an initial velocity. I'm trying to make it travel the same distance with the same time taken regardless of the fps.

When I run the code with an FPS of 60, this is my debug information

Mid time: 1.8163 s
Time for vel=0: 2.5681 s
End position: (651.94, 262.0)

When I run it with an FPS of 120, this is my debug information

Mid time: 1.3987 s
Time for vel=0: 5.0331 s
End position: (1224.91, 400.35)

I'm expecting both the debug information to be the same. I did some math and concluded that I should multiply velocity by dt and multiply friction by dt square, but clearly that doesn't work. So what's wrong with the code then?

import pygame
import sys
from pygame.locals import *
from time import time

class Entity:
    
    def __init__(self, pos, vel, friction, rgb=(0, 255, 255), size=(50, 80)):
        self.pos = pos
        self.vel = vel
        self.friction = friction
        self.rgb = rgb
        self.size = size

    def update(self, dt):
        friction = self.friction * dt**2
        for i in range(2):
            self.pos[i] += self.vel[i] * dt
            
            # Adding/subtracting friction to velocity so that it approaches 0 
            if self.vel[i] > 0:
                self.vel[i] -= friction
                if self.vel[i] < 0:                    
                    self.vel[i] = 0
            elif self.vel[i] < 0:
                self.vel[i] += friction
                if self.vel[i] > 0:
                    self.vel[i] = 0

    def render(self, surf):
        pygame.draw.rect(surf, self.rgb, (self.pos[0], self.pos[1], self.size[0], self.size[1]))

pygame.init()
clock = pygame.time.Clock()
FPS = 120
screen_size = (1600, 900)
screen = pygame.display.set_mode(screen_size)
pygame.display.set_caption('Window')

start_1 = time()
printed_first_debug = False
printed_second_debug = False

#               position, velocity, friction
player = Entity([20, 100], [8, 4], 0.05)

run = True
while run:
    t1 = time()
    try:
        dt = 60*(t1-t0)
    except NameError:
        dt = 60/FPS
    t0 = time()
    
    for event in pygame.event.get():
        
        if event.type == QUIT:
            run = False

    screen.fill((30, 30, 30))
    player.update(dt)
    player.render(screen)

    if player.pos[0] >= 600 and not printed_first_debug:
        end_time = time()
        print(f'Mid time: {round(end_time - start_1, 4)} s')
        printed_first_debug = True
    elif player.vel == [0, 0] and not printed_second_debug:
        end_time = time()
        print(f'Time for vel=0: {round(end_time - start_1, 4)} s')
        print(f'End position: ({round(player.pos[0], 2)}, {round(player.pos[1], 2)})')
        printed_second_debug = True

    pygame.display.update()
    clock.tick(FPS)

pygame.quit()
sys.exit()

Solution

  • Don't square dt in your velocity update.

    You are using Euler integration to estimate the fractional changes to position and velocity for small dt, so in the case where acceleration is constant (like from force of friction) or you are using Euler to estimate for a very small delta time, you can just multiply by delta t

    velocity ~ acceleration * dt
    

    just as

    position ~ velocity * dt