Python Forum

Full Version: Bouncing Rectangles
You're currently viewing a stripped down version of our content. View the full version with proper formatting.
import pygame
from pygame import QUIT
from sys import exit
from time import time
from random import randint, choice


class Entity:
    """A structure that holds the properties of an entity"""
    def __init__(self, rect, x, y, x_sign, y_sign):
        """Initialize entity properties"""
        # Collision rectangle with integers
        self.rect = rect
        # x coordinate in floating point
        self.x = x
        # y coordinate in floating point
        self.y = y
        # x and y direction signs
        self.x_sign = x_sign
        self.y_sign = y_sign


class Model:
    """Rectangles move about on the screen bouncing off the screen
    edges and other rectangles"""
    def __init__(self, screen, number_of_entities=25, speed=150.0):
        """Initialize state, populate model with entities"""
        self.screen = screen
        self.screen_width = screen.get_rect().width
        self.screen_height = screen.get_rect().height
        self.image = pygame.image.load('images/entity.png').convert_alpha()
        self.speed = speed
        self.entities = []
        for _ in range(number_of_entities):
            self.entities.append(self.add_entity())

    def add_entity(self):
        """Return a new entity object with randomized properties"""
        # Repeat loop until there is no collision with an existing entity
        done = False
        while not done:
            rect = self.image.get_rect()
            # rect object x and y coordinates must be integers
            # so floating point values must be stored separately
            # for the purpose of variable frame rate
            rect.x = randint(0, self.screen_width - rect.width)
            rect.y = randint(0, self.screen_height - rect.height)
            collided = False
            for entity in self.entities:
                if entity.rect.colliderect(rect):
                    collided = True
                    break
            if not collided:
                done = True
        # x and y coordinates must be floating point for variable
        # frame rates.  Otherwise the delta will truncate to 0 each cycle
        x = float(rect.x)
        y = float(rect.y)
        # Choose a random direction, in x and y axis
        x_sign = choice([-1, 1])
        y_sign = choice([-1, 1])
        return Entity(rect, x, y, x_sign, y_sign)

    def update(self, delta):
        """Apply the model's logic to go from one state to the next"""
        # delta is the time elapsed and multiplying it by speed
        # gives the amount of pixels travelled
        delta = self.speed * delta
        for entity in self.entities:
            # Check to see if entity contacts a screen edge and
            # if so reverse direction in the respective axis
            if (entity.rect.left - delta < 0) or \
               (entity.rect.right + delta > self.screen_width) or \
               (entity.rect.top - delta < 0) or \
               (entity.rect.bottom + delta > self.screen_height):
                    if (entity.rect.left - delta < 0):
                        entity.rect.left = 1
                        entity.x_sign = 1
                    elif (entity.rect.right + delta > self.screen_width):
                        entity.rect.right = self.screen_width - 1
                        entity.x_sign = -1
                    if (entity.rect.top - delta < 0):
                        entity.rect.top = 1
                        entity.y_sign = 1
                    elif (entity.rect.bottom + delta > self.screen_height):
                        entity.rect.bottom = self.screen_height - 1
                        entity.y_sign = -1
            else:
                # Entity did not hit edge so test a potential move
                potential_move = pygame.Rect.copy(entity.rect)
                potential_x = (entity.x + (delta * entity.x_sign))
                potential_y = (entity.y + (delta * entity.y_sign))
                # rect x and y must be integers
                potential_move.x = int(potential_x)
                potential_move.y = int(potential_y)
                commit_move = True
                # Test for collision between potential move and all
                # existing entities
                for other_entity in self.entities:
                    if not (entity is other_entity):
                        if potential_move.colliderect(other_entity.rect):
                            # There is a collision
                            commit_move = False
                            # Reverse directions of both entities
                            entity.x_sign *= -1
                            entity.y_sign *= -1
                            other_entity.x_sign *= -1
                            other_entity.y_sign *= -1
                if commit_move:
                    # No collisions detected so commit potential move
                    entity.rect = potential_move
                    entity.x = potential_x
                    entity.y = potential_y

    def render(self):
        """Render the entity image to all entity rectangles"""
        for entity in self.entities:
            self.screen.blit(self.image, entity.rect)


class Controller:
    """Controller for a model"""
    def __init__(self):
        """Initialize pygame, load resources, and create the model"""
        pygame.init()
        pygame.display.set_caption('Entities')
        pygame.mouse.set_visible(0)
        self.screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)
        self.background = pygame.transform.smoothscale(
            pygame.image.load('images/background.jpg').convert(), (
                self.screen.get_rect().width,
                self.screen.get_rect().height))
        self.model = Model(self.screen)

    def run_controller(self):
        """Main loop.  Uses variable frame rates.  The elapsed time
        between each iteration of the loop is passed to rendering
        methods"""
        delta_now = time()
        while True:
            delta_last, delta_now = delta_now, time()
            elapsed_time = delta_now - delta_last
            self.check_events()
            self.model.update(elapsed_time)
            self.update_screen()

    def check_events(self):
        """Handle the window close event and keyboard commands"""
        for event in pygame.event.get():
            if event.type == QUIT:
                # Window close icon clicked
                exit()
            elif event.type == pygame.KEYDOWN:
                # A key was pressed
                if event.key == pygame.K_ESCAPE:
                    # Quit
                    exit()
                self.check_keydown_events(event)

    def check_keydown_events(self, event):
        """Control the model through keyboard commands"""
        if event.key == pygame.K_UP:
            # Increase speed
            self.model.speed += 50.0
            if self.model.speed > 500.0:
                self.model.speed = 500.0
        elif event.key == pygame.K_DOWN:
            # Decrease speed
            self.model.speed -= 50.0
            if self.model.speed < 0.0:
                self.model.speed = 0.0
        elif event.key == pygame.K_LEFT:
            # Delete an entity
            if len(self.model.entities) > 0:
                del self.model.entities[0]
        elif event.key == pygame.K_RIGHT:
            # Add a new entity
            self.model.entities.append(self.model.add_entity())

    def update_screen(self):
        """Composite the background image with the model rendering"""
        self.screen.blit(self.background, self.screen.get_rect())
        self.model.render()
        # Flip the new image to the graphical display
        pygame.display.flip()


if __name__ == '__main__':
    # Create an instance of a Controller and call its entry point
    controller = Controller()
    controller.run_controller()
This code bounces rectangles off of each other and the screen edges. Substitute your own entity and background images as you see fit.