1
votes

I am learning python using pygame and I am working on something that involves sprites and collisions. I've looked at some examples but I still don't quite understand it. What I am attempting to do is to be able to add sprites(a ball) when the user presses the "=" key and also be able to remove the last sprite added when pressing "-". I am not able to remove just the last one, I have only been able to remove all of them.

So far I have been able to add the balls to the window and have them bounce off the walls and one another(sort of). When 2 balls collide, they don't completely touch yet they bounce off. Sometimes the balls get stuck and won't move and sometimes the balls bounce off the frame which they aren't suppose to.

Its my first time working with sprite groups and would appreciate any help/guidance into making this work smoothly.Thanks.

The code:

ball.py

import pygame
from pygame.locals import *


class Ball(pygame.sprite.Sprite):
    def __init__(self, x, y, vx, vy):

        super().__init__();
        self.image = pygame.image.load("ball.png").convert()


        self.image.set_colorkey(pygame.Color(0, 0, 0))

        self.rect = self.image.get_rect()

        self.rect.x = x
        self.rect.y = y
        self.vx = vx
        self.vy = vy

    def draw(self, SCREEN):
        SCREEN.blit(self.image, (self.rect.x, self.rect.y))

    def move(self, SCREEN, balls):
        l_collide = self.rect.x + self.image.get_width() + self.vx > SCREEN.get_width()
        r_collide = self.rect.x + self.vx < 0
        t_collide = self.rect.y + self.vy < 0
        b_collide = self.rect.y + self.image.get_height() + self.vy > SCREEN.get_height()

        a = pygame.sprite.spritecollide(self, balls, False, False)

        if len(a) > 1:
            self.vx *= -1
            self.vy *= -1



        if l_collide or r_collide:
            self.vx *= -1


        if t_collide or b_collide:
            self.vy *= -1

        self.rect.x += self.vx
        self.rect.y += self.vy

ball_animation.py

import pygame
import sys
import random
import math
from pygame.locals import *
from ball.ball import Ball
from random import randint

def ball_list(num):
    ball_list = pygame.sprite.Group()

    for x in range(num):
        rand_x = random.randint(0,400)
        rand_y = random.randint(0,400)
        vx = 4
        vy = 5

        ball_list.add(Ball(rand_x, rand_y, vx, vy))

    return ball_list

def main():
    pygame.init()

    FPS = 30
    FPS_CLOCK = pygame.time.Clock()

    # COLOR LIST
    BLACK = pygame.Color(0, 0, 0)

    # Code to create the initial window
    window_size = (500, 500)
    SCREEN = pygame.display.set_mode(window_size)

    # set the title of the window
    pygame.display.set_caption("Bouncing Ball Animation")

    # change the initial background color to white
    SCREEN.fill(BLACK)

    balls = ball_list(0)

    while True:  # <--- main game loop
        for event in pygame.event.get():
            if event.type == QUIT:  # QUIT event to exit the game
                pygame.quit()
                sys.exit()
            if event.type == KEYDOWN:
                if event.key == K_EQUALS:
                    balls.add(Ball(randint(0,400),randint(0,400), 4,5))
                if event.key == K_MINUS:
                    try:
                        balls.remove()
                    except IndexError:
                        print('There is no balls to take!')




        SCREEN.fill(BLACK)
        for x in balls:
            x.move(SCREEN,balls)
            x.draw(SCREEN)

        pygame.display.update()  # Update the display when all events have been processed
        FPS_CLOCK.tick(FPS)

if __name__ == "__main__":
    main()
2
@Johan is right, you have two questions here, which is why I went back and titled my answer to just the first question, since I did not answer everything. - mgallagher

2 Answers

1
votes

Removing Sprites on Press

The problem is sprite.Group.remove(sprites) wants you to specify which sprites it should remove. sprites here should be a sprite/list of sprites that you want to remove from the group. This means to remove the last ball added on key press you need to keep a list of the ball sprites and pop() the most recently added item from it, and then use the result of the pop() as the sprite to remove from the group. sprite.Group has a .sprites() method which returns a list of all sprites in the group, in the order they were added. This list is generated from the group and is not actually an interface with it, so doing things to this list won't affect the group. We can still however use it to get the last added sprite. Here is what it looks like:

elif event.key == K_0:
    try:
        sprite_list = balls.sprites()
        to_remove = sprite_list[-1] # Get last element of list
        balls.remove(to_remove)
    except IndexError:
        print('There is no balls to take!')

Collisions

So this is a bit more involved and not so simple to fix in your code. To understand what the problem is, look at what your collision velocity adjustments are actually doing for the screen border case.

l_collide = self.rect.x + self.image.get_width() + self.vx > SCREEN.get_width()
r_collide = self.rect.x + self.vx < 0
t_collide = self.rect.y + self.vy < 0
b_collide = self.rect.y + self.image.get_height() + self.vy > SCREEN.get_height()

#################

if l_collide or r_collide:
    self.vx *= -1

if t_collide or b_collide:
    self.vy *= -1

Consider a single time-step in your code. We check to see if the sprite is sitting over the edge of the boundaries by any amount. If its hanging over, we reverse the velocity. There is a case where your edge checking will get you into trouble. If your self.vx is less than the difference between your current position X and the boundary of the x dimension, you will reverse your speed, travel self.vx back towards the boundary, but not make it past. In the next time-step, you will see that you are still over the boundary, and your program will again reverse self.vx, actually sending you away from the boundary. In this case you will bound back and forth each time-step by self.vx. Normally this wouldn't happen in your code, except for when you spawn a new ball sprite over the boundary further than your self.vx or self.vy for that ball. This can be remedied by making sure you don't spawn balls off the edges, or better yet, only reversing your velocity if you need to.

if (l_collide and self.vx>0) or (r_collide and self.vx<0):
    self.vx *= -1


if (t_collide and self.vy<0) or (b_collide and self.vy>0):
    self.vy *= -1

Notice here we only reverse the velocity if we are over the edge AND the velocity is headed deeper in that direction. Now for your sprites you have two options, just like with the boundaries:

  • Only initiate a new ball in empty space where it cannot collide.
  • Implement some way to calculate the correct velocity adjustment and only apply it if the velocity is headed in the opposite direction.

From what I read in the documentation, sprite.Group looks like it is meant for checking if sprites are overlapping, and not for physics simulation. I recommend doing some research on 2d physics simulation to get a nice conceptualization of what information you should want to communicate between objects. I'm sure there are some nice tutorials out there.

Finally, to address your other question about why they are colliding when they don't appear to be touching. sprite.spritecollide is returning which sprites have rectangles that intersect. If your ball.png is color keyed for transparency, this does not affect the rect of the sprite. Pygame appears to have functionality implemented designed to handle this problem in the collided keyword of sprite.spritecollide:

pygame.sprite.spritecollide()

  • Find sprites in a group that intersect another sprite.

spritecollide(sprite, group, dokill, collided = None) -> Sprite_list

The collided argument is a callback function used to calculate if two sprites >are colliding. it should take two sprites as values, and return a bool value >indicating if they are colliding. If collided is not passed, all sprites must >have a “rect” value, which is a rectangle of the sprite area, which will be >used to calculate the collision. collided callables:

  • collide_rect
  • collide_rect_ratio
  • collide_circle
  • collide_circle_ratio
  • collide_mask

That's from the pygame documentation. The documentation for the collide_circle function states that your sprite should have a radius attribute, or else one will be calculated to fit the entire rectangle inside a circle. As such, in your Ball.__init__ function I would recommend adding:

self.radius = self.rect.width/2 

This will make collide_circle use a radius that approximates your ball image, assuming it is centered and circular and occupies the entire image. Next, you must add the collision specification to your collision check by changing:

a = pygame.sprite.spritecollide(self, balls, False, False)

to

a = pygame.sprite.spritecollide(self, balls, False, pygame.sprite.collide_circle)

If you solve the problem of not spawning new ball objects inside each other, this should all work nicely. If you can't get them to spawn inside each other, think about a different data-structure or different way of collision checking to get the results you want. Best of luck!

0
votes

I can see two questions in your text

  1. You want to only remove one sprite, rather than all the sprites in the spritegroup

If you look at the pygame documentation, you can see that spritegroup.remove has an optional argument. You can remove a single sprite by putting your desired sprite as the argument, such as myspritegroup.remove(mysprite).

  1. You have issues with the colliding

Your collision works for me as long as the balls don't spawn on top of each other on creation which you can simply check. Good luck :)