Python Forum
Bouncing Rectangles
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Bouncing Rectangles
#1
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.
Reply


Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020