Choppy animation in Pygame

1.3k views Asked by At

I'm attempting to create an animation that shows a box bouncing off the edges of the screen. And, I'm attempting to accomplish this using time-based animation and dirty rectangles.

I was able to animate the box; however, the animation is pretty choppy. Here are two videos that illustrate what I'm talking about:

30 FPS: https://www.youtube.com/watch?v=0de8ENxn7GQ

60 FPS: https://www.youtube.com/watch?v=b5sXgeOlgHU

And here is my code:

import sys
import random

import pygame


class Box:

    def __init__(self, x, y):
        self.width = 38
        self.height = 38
        self.color = (255, 0, 0)
        self.x = x
        self.y = y
        self.old_x = x
        self.old_y = y
        self.d_x = 1
        self.d_y = -1
        self.px_per_second = 200

    def move(self):
        self.old_x = self.x
        self.old_y = self.y
        if self.x <= 0:
            self.d_x *= -1
        if self.x + self.width >= task.width:
            self.d_x *= -1
        if self.y <= 0:
            self.d_y *= -1
        if self.y + self.height >= task.height:
            self.d_y *= -1
        self.x += ((self.px_per_second*self.d_x)*
                  (task.ms_from_last_frame/1000.0))
        self.y += ((self.px_per_second*self.d_y)*
                  (task.ms_from_last_frame/1000.0))

    def draw(self):
        self.x_i = int(self.x)
        self.y_i = int(self.y)
        self.old_x_i = int(self.old_x)
        self.old_y_i = int(self.old_y)
        _old_rect = (pygame.Rect(self.old_x_i, self.old_y_i, 
                                 self.width, self.height))
        _new_rect = (pygame.Rect(self.x_i, self.y_i, self.width, self.height))
        if _old_rect.colliderect(_new_rect):
            task.dirty_rects.append(_old_rect.union(_new_rect))
        else:
            task.dirty_rects.append(_old_rect)
            task.dirty_rects.append(_new_rect)
        pygame.draw.rect(task.screen, task.bg_color, _old_rect)
        pygame.draw.rect(task.screen, (self.color), _new_rect)


class ObjectTask:

    def __init__(self, width, height):
        pygame.init()
        self.max_fps = 60
        self.clock = pygame.time.Clock()
        self.width = width
        self.height = height
        self.bg_color = (255, 255, 255)
        self.dirty_rects = []
        self.screen = pygame.display.set_mode((self.width, self.height),
                                               pygame.FULLSCREEN)
        self.screen.fill(self.bg_color)
        pygame.display.update()

    def animation_loop(self):
        self.box1 = Box(self.width/2, self.height/2)
        while 1:
            self.ms_from_last_frame = self.clock.tick(self.max_fps)
            self.box1.move()
            self.box1.draw()
            pygame.display.update(self.dirty_rects)
            self.dirty_rects = []
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()


if __name__ == "__main__":
    task = ObjectTask(726, 546)
    task.animation_loop()

Is there anything I can do to reduce the choppiness? Also, I'm new to Pygame, so if you see anything that I'm doing incorrectly/inefficiently, please let me know.

I'm running the animation on a 64-bit Windows 7, i5-6300u machine with 12 GB of RAM. I'm using Python 2.7.12 and Pygame 1.9.2.

Thanks in advance!

1

There are 1 answers

0
Ted Klein Bergman On

I had some time over and thought I might show how I would write this program. I've tried to explain what everything do in comments. I removed your ObjectTask class because everything could fit in a single function and functions are neat (they're quicker and easier to read/debug in my opinion).

I also changed it so you pass variables into methods instead of reading it from a global variable (the task object) because it makes the program more modular and easier to change/refactor.

Lastly I put some attributes into one. x and y are connected, so I put them into one common attribute position.

import pygame
pygame.init()


# It's convenient if all your visible objects inherit 'pygame.sprite.Sprite'. It allows you to use sprite groups and
# some useful collision detection functions.
class Box(pygame.sprite.Sprite):
    def __init__(self, x, y, screen_width, screen_height):
        super(Box, self).__init__()  # Initialize the 'pygame.sprite.Sprite' (the super class).
        self.size = (38, 38)
        self.color = (255, 0, 0)
        self.position = pygame.math.Vector2(x, y)  # Vector is a 2-element tuple with some neat methods shown later.
        self.velocity = pygame.math.Vector2(200, -200)
        self.screen_size = (screen_width, screen_height)

        # Instead of drawing the rectangle onto the screen we're creating an image which we 'blit' (put) onto the screen
        self.image = pygame.Surface(self.size)
        self.image.fill(self.color)

        # We create a rectangle same size as our image and where the top left of the rectangle is positioned at our
        # 'position'.
        self.rect = self.image.get_rect(topleft=self.position)

    # Changing name to update. Game objects usually have two methods 'update' and 'draw', where 'update' is the function
    # that will be called every game loop and update the object, and 'draw' draws the game object every game loop.
    def update(self, dt):  # 'dt' (delta time) is what you formerly called task.ms_from_last_frame.
        # unpack the variables so it's easier to read. Not necessary, but nice.
        width, height = self.size
        x, y = self.position
        screen_width, screen_height = self.screen_size

        if x <= 0 or x + width >= screen_width:  # Only one if-statement needed.
            self.velocity[0] *= -1  # First element is the x-velocity.
        if y <= 0 or y + height >= screen_height:
            self.velocity[1] *= -1  # Second element is y-velocity.

        # Addition and subtraction works element-wise with vectors. Adding our two vectors 'position' and 'velocity':
        #    position += velocity  <=>  position = position + velocity
        # is the same as this:
        #    position_x, position_y = position_x + velocity_x, position_y + position_y
        #
        # Multiplication and division works on all elements. So 'velocity * dt / 1000' is the same as:
        #    velocity_x * dt / 1000, velocity_y * dt / 1000
        self.position += self.velocity * dt / 1000

        # Instead of creating a new rect we just move the top left corner of our rect to where we calculated our
        # position to be.
        self.rect.topleft = self.position

    # 'surface' is the Surface object you want to draw on (in this case the screen).
    def draw(self, surface):
        # This puts our image unto the screen at the given position.
        surface.blit(self.image, self.position)


def main():
    max_fps = 60
    clock = pygame.time.Clock()
    bg_color = (255, 255, 255)
    width, height = 726, 546
    screen = pygame.display.set_mode((width, height))
    screen.fill(bg_color)

    box1 = Box(width / 2, height / 2, width, height)
    sprite_group = pygame.sprite.Group(box1)
    while 1:
        # First handle time.
        dt = clock.tick(max_fps)

        # Then handle events.
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                quit()  # 'pygame.quit' does not quit the program! It only uninitialize the pygame modules.
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    quit()

        # Update your objects.
        sprite_group.update(dt)  # Calls the update method on all sprites in the group.

        # Draw your objects.
        screen.fill(bg_color)  # Clear the screen.
        sprite_group.draw(screen)  # Draws all sprites on the screen using the 'image' and 'rect' attributes.

        # Display everything you've drawn.
        pygame.display.update()


if __name__ == "__main__":
    main()

In this example I didn't bother with tracking dirty rects. I've experienced that it's what causing the jitter. This example I've provided should work decent, but there's always some jitter from time to time. If you want to try out this example with tracking dirty rects, just change:

sprite_group = pygame.sprite.Group(box1)

to

sprite_group = pygame.sprite.RenderUpdates(box1)

and

sprite_group.draw(screen)  # Draws all sprites on the screen using the 'image' and 'rect' attributes.

# Display everything you've drawn.
pygame.display.update() 

to

dirty_rects = sprite_group.draw(screen)  # Draws all sprites on the screen using the 'image' and 'rect' attributes.

# Display everything you've drawn.
pygame.display.update(dirty_rects)  # Update only the areas that have changes