Dealing with Sprites and Collisions Using Pygame - python

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()

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!

I can see two questions in your text
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).
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 :)

Related

How to make bullets aim at player in pygame [duplicate]

This question already has answers here:
calculating direction of the player to shoot pygame
(1 answer)
Moving forward after angle change. Pygame
(1 answer)
Shooting a bullet in pygame in the direction of mouse
(2 answers)
Closed 2 years ago.
So far, the enemies in my game only fire straight down. I want to be able to aim at the player. This is my Enemy class's shoot method.
class Enemy(sprite.Sprite):
def shoot(self):
# Origin position, direction, speed
# Direction of (0,1) means straight down. 0 pixels along the x axis, and +1 pixel along the y axis
# Speed of (10,10) means that every frame, the object will move 10 px along the x and y axis.
self.bullets.shoot(self.rect.center, (0,1), (10,10))
self.bullets is an instance of my BulletPool class.
class BulletPool(sprite.Group):
def shoot(self, pos, direction, speed):
# Selects a bullet from the pool
default = self.add_bullet() if self.unlimited else None
bullet = next(self.get_inactive_bullets().__iter__(), default)
if bullet is None:
return False
# Sets up bullet movement
bullet.rect.center = pos
bullet.start_moving(direction)
bullet.set_speed(speed)
# Adds bullet to a Group of active bullets
self.active_bullets.add(bullet)
And here is my Bullet class.
class Bullet(sprite.Sprite):
def update(self):
self.move()
def set_speed(self, speed):
self.speed = Vector2(speed)
def start_moving(self, direction):
self.direction = Vector2(direction)
self.is_moving = True
def stop_moving(self):
self.is_moving = False
def move(self):
if self.is_moving:
x_pos = int(self.direction.x*self.speed.x)
y_pos = int(self.direction.y*self.speed.y)
self.rect = self.rect.move(x_pos,y_pos)
Using this, I can only make sprites go straight up (0,-1), down (0,1), left (-1,0) or right (1,0), as well as combining combining x and axes to make a 45 degree angle, (i.e. (1,1) is going down and right). I don't know how to angle something to make it go towards a particular direction other than these. Should I change the way I move my objects? I use the same methods to move my player, and it works perfectly when it's just taking controls from the arrow keys.

Handling sprite collisions against an angled rectangular obstacle that is static

So I have this obstacle that I want my sprite to collide with and it is at a particular angle.In this case, we are measuring from the positive x-axis to the top of the rectangle and in this instance it is 333.02 degrees with respect to the positive x-axis or 63.02 degrees with respect to the negative y-axis. So my issue is that how do I set up my pygame sprite to properly collide with the angle rectangle obstacle? Pygame sprite rectangles have no rotation attribute (to my knowledge) and I can't just say, "Hey when the right corner of my sprite collides with top, etc" because of this lack of rotation. My collisions work great for horizontal and even vertical surfaces but I am stuck on how to collide with angled obstacles.
Here is my collision code right now. It uses vectors and checks both x and y independently to see if anything is there. And below is a picture of the object I want to collide with created in the Tile Map Editor. It is at an angle of 333.02 degrees like I mentioned before. I also included a rough sketch of the axis in case that is relevant.
def update(self):
self.animate()
self.acc = vec(0, PLAYER_MASS * GRAVITY)
self.move()
# Equations of Motion
self.acc.x += self.vel.x * PLAYER_FRICTION
self.vel += self.acc
# Collision check in all 4 directions
self.pos.x += (
self.vel.x + 0.5 * self.acc.x * self.game.dt
) # Update x component (Frame-independent motion)
if abs(self.vel.x) < PLAYER_VELX_EPSILON:
self.vel.x = 0
self.rect.x = self.pos.x
hits = pg.sprite.spritecollide(self, self.game.platforms, False)
for hit in hits: # Horizontal collision
if self.vel.x > 0: # Rightward motion
self.rect.right = hit.rect.left
elif self.vel.y < 0: # Leftward motion
self.rect.left = hit.rect.right
self.pos.x = self.rect.x # Update true postion
self.pos.y += self.vel.y + 0.5 * self.acc.y * self.game.dt # Update y component
self.rect.y = self.pos.y + 5
# This prevents double jumping
if self.vel.y > 0:
self.onGnd = False
hits = pg.sprite.spritecollide(self, self.game.platforms, False)
for hit in hits: # Vertical Collision
if self.vel.y > 0: # Downward motion
self.rect.bottom = hit.rect.top
self.vel.y = 0
self.onGnd = True
elif self.vel.y < 0: # Upward motion
self.rect.top = hit.rect.bottom
self.vel.y = 0
self.pos.y = self.rect.y # Update true postion
# Limit Player's movement
if self.rect.bottom > HEIGHT:
self.vel.y = 0
self.rect.bottom = HEIGHT
self.pos.y = self.rect.y
Any help on this problem would be greatly appreciated!
The answer is coordinate translation. Imagine that the rotated object had its own coordinate system, where x runs along the bottom of the rectangle, and y up the side on the left. Then, if you could find the position of your sprite in that coordinate system, you could check for collisions the way you normally would with an unrotated rectangle, i.e., if x >=0 and x <= width and y >=0 and y <= height then there's a collision.
But how do you get the translated coordinates? The answer is matrices. You can use 2d transformation matrices to rotate, scale and translate vectors and coordinates. Unfortunately my experience with these types of transformations is in C#, not python, but this page for instance provides examples and explanations in python using numpy.
Note that this is quite simply the way 2d (and 3d) games work - matrix transformations are everywhere, and are the way to do collision detection of rotated, scaled and translated objects. They are also how sprites are moved, rotated etc: the pygame transform module is a matrix transformation module. So if the code and explanations looks scary at first glance, it is worth investing the time to understand it, since it's hard to write games without it beyond a certain point.
I'm aware this is not a full answer to your question, since I haven't given you the code, but it's too long for a comment, and hopefully points you in the right direction. There's also this answer on SO, which provides some simple code.
EDIT: just to add some further information, a full collision detection routine would check each collidable pixel's position against the object. This may be the required approach in your game, depending on how accurate you need the collision detection to be. That's what I do in my game Magnetar (https://www.youtube.com/watch?v=mbgr2XiIR7w), which collides multiple irregularly shaped sprites at arbitrary positions, scales and rotations.
I note however that in your game there's a way you could possibly "cheat" if all you need is collision detection with some angled slopes. That is you could have a data structure which records the 'corners' of the ground (points it change angle), and then use simple geometry to determine if an x,y point is below or above ground. That is, you would take the x value of a point and check which segment of the ground it is over. If it is over the sloped ground, work out how far along the x axis of the sloped ground it is, then use the sin of the angle times this value to work out the y value of the slope at that position, and if that is greater than the y value of the point you are checking, you have a collision.
The answer from seesharper may be the right way to go. I have no experience with that but it looks interesting and I will have to go read the links and learn something about this approach. I will still provide my immediate reaction to the question before I saw his answer though.
You could use pygames mask and mask collision detection routines. Create a mask that was the shape of the angled rectangle/platform and use the methods to detect collisions with that.
If you add a self.mask attribute to your Sprite subclass, the sprites will automatically use that mask in the collision detection.

How do I make the ball bounce off the paddle?

I'm making a basic pong game (paddle is a rectangle on the bottom of the screen and the ball drops from the top of the screen). I want the ball to bounce back up ONLY when it hits the paddle. So far, I've written code that will make the ball bounce off the top and bottom screen, but I'm having trouble with getting the ball to bounce off the paddle.
I have to modify the parameters that are passed to my test_collide_ball method. If it’s current x values are within the range of the paddle, then it bounces back up.
I've been trying to think of a solution for this, and what I'm thinking is that if the ball hits the paddle's y coordinate (the height), then it bounces back up. But it also has to be within the range of x coordinates that make up the paddle (so the width of the paddle).
But when I do this, the ball just gets stuck in place. Any feedback is appreciated! Thanks in advance.
Here is my code for the ball class/methods:
import pygame
class Ball:
def __init__(self, x, y, radius, color, dx, dy):
self.x = x
self.y = y
self.radius = radius
self.color = color
self.dx = dx
self.dy = dy
def draw_ball(self, screen):
pygame.draw.ellipse(screen, self.color,
pygame.Rect(self.x, self.y, self.radius, self.radius))
def update_ball(self):
self.x += self.dx
self.y += self.dy
def test_collide_top_ball(self, top_height):
if (self.y <= top_height):
self.dy *= -1
def test_collide_bottom_ball(self, paddle):
if (self.y == paddle.y) and (self.x >= paddle.x) and (self.x <= paddle.x + paddle.width):
self.dy *= -1
What appears to be happening is your ball enters the collision zone and reverses it's direction. The ball is still in the collision zone, however, and it reverses it's direction again.
What you should look into is a debounce check. Put simply, this is code that prevents something from happening twice or more times (de-bouncing it).
From your code example, the ball's momentum is reversed when it enters the paddle zone. What you might add is a boolean flag to see if you have already detected that the ball entered the zone. When it is first detected, set the flag to true. When the ball moves outside of the zone, set the flag back to false. Only reverse the ball's momentum if the flag is false.
So, (excusing my rusty Python)
def test_collide_bottom_ball(self, paddle):
if (self.y == paddle.y) and (self.x >= paddle.x) and (self.x <= paddle.x + paddle.width) and (!self.hitPaddle):
self.dy *= -1
self.hitPaddle = true
else
self.hitPaddle = false
And in your entity:
self.hitPaddle = false
Just like #MrDoomBringer is saying, you need to prevent it from getting stuck within the pad.
One easy method to solve that is to check whether self.dy is positive - the ball is moving downwards. This way you could also add the same "within" check for the Y-pos as you did with the X-pos. Otherwise, having a collision with an exact Y-coordinate is pretty hard unless you're using the right speed etc.
Another thing - if you have a ball, you most likely want to add it's size to the equation. Then you might want to use some more fancy collision-techniques, such as this: http://www.migapro.com/circle-and-rotated-rectangle-collision-detection/

PyGame-Character Goes Off Screen

I am trying to make a game with pygame but I can't figure out how to keep my character from going off screen(set a limit). I have a .png image controlled by user input, but it's possible for the character to go off the visible screen area normally. I can't figure out how to do this. I made a rectangle around the window, (pygame.draw.rect) but I can't assign the rect to a variable so I can create a collision. I also tried this:
if not character.get_rect() in screen.get_rect():
print("error")
But it didn't work, just spammed the python console with "error" messages.
(i checked the other post with this question but nothing worked/didn't get it)
So my question is, how can I keep my character from going offscreen, and which is the best way to do that?
~thanks
EDIT: My game doesn't have a scrolling playfield/camera. (just a fixed view on the whole window)
if not character.get_rect() in screen.get_rect():
print("error")
I see what you are trying here. If you want to check if a Rect is inside another one, use contains():
contains()
test if one rectangle is inside another
contains(Rect) -> bool
Returns true when the argument is completely inside the Rect.
If you simply want to stop the movement on the edges on the screen, an easy solution is to use clamp_ip():
clamp_ip()
moves the rectangle inside another, in place
clamp_ip(Rect) -> None
Same as the Rect.clamp() [Returns a new rectangle that is moved to be completely inside the argument Rect. If the rectangle is too large to fit inside, it is centered inside the argument Rect, but its size is not changed.] method, but operates in place.
Here's a simple example where you can't move the black rect outside the screen:
import pygame
pygame.init()
screen=pygame.display.set_mode((400, 400))
screen_rect=screen.get_rect()
player=pygame.Rect(180, 180, 20, 20)
run=True
while run:
for e in pygame.event.get():
if e.type == pygame.QUIT: run = False
keys = pygame.key.get_pressed()
if keys[pygame.K_w]: player.move_ip(0, -1)
if keys[pygame.K_a]: player.move_ip(-1, 0)
if keys[pygame.K_s]: player.move_ip(0, 1)
if keys[pygame.K_d]: player.move_ip(1, 0)
player.clamp_ip(screen_rect) # ensure player is inside screen
screen.fill((255,255,255))
pygame.draw.rect(screen, (0,0,0), player)
pygame.display.flip()
When you used pygame.draw.rect, you didn't actually create a "physical" boundary- you just set the colour of the pixels on the screen in a rectangular shape.
If you know the size of the screen, and the displacement of all of the objects on the screen (only applicable if your game has a scrolling playfield or camera), then you can do something like this:
# In the lines of code where you have the player move around
# I assume you might be doing something like this
if keys[pygame.K_RIGHT]:
player.move(player.getSpeed(),0) # giving the x and y displacements
if keys[pygame.K_LEFT]:
player.move(-player.getSpeed(),0)
...
class Player:
...
def move(self, dx, dy):
newX = self.x + dx
newY = self.y + dy
self.x = max(0, min(newX, SCREEN_WIDTH)) # you handle where to store screen width
self.y = max(0, min(newY, SCREEN_HEIGHT))
Note that a useful tool for you to get the size of the Pygame window is pygame.display.get_surface().get_size() which will give you a tuple of the width and height. It is still better, however, to avoid calling this every time you need to know the boundaries of the player. That is, you should store the width and height of the window for later retrieval.
Here's a simple control code that I use in my games to keep sprites from going off the screen:
# Control so Player doesn't go off screen
if self.rect.right > WIDTH:
self.rect.right = WIDTH
if self.rect.left < 0:
self.rect.left = 0
if self.rect.bottom > HEIGHT:
self.rect.bottom = HEIGHT
if self.rect.top < 0:
self.rect.top = 0
WIDTH and HEIGHT are constants that you define to set the size of your screen. I hope this helps.

Problem with 2D collision detection from beginner

I've taken an introductory course in Computer Science, but a short while back I decided to try and make a game. I'm having a problem with collision detection. My idea was to move an object, and if there is a collision, move it back the way it came until there is no longer a collision. Here is my code:
class Player(object):
...
def move(self):
#at this point, velocity = some linear combination of (5, 0)and (0, 5)
#gPos and velocity are types Vector2
self.hitBox = Rect(self.gPos.x, self.gPos.y, 40, 40)
self.gPos += self.velocity
while CheckCollisions(self):
self.gPos -= self.velocity/n #see footnote
self.hitBox = Rect(self.gPos.x, self.gPos.y, 40, 40)
...
def CheckCollisions(obj):
#archList holds all 'architecture' objects, solid == True means you can't walk
#through it. colliderect checks to see if the rectangles are overlapping
for i in archList:
if i.solid:
if i.hitBox.colliderect(obj.hitBox):
return True
return False
*I substituted several different values for n, both integers and floats, to change the increment by which the player moves back. I thought by trying a large float, it would only move one pixel at a time
When I run the program, the sprite for the player vibrates very fast over a range of about 5 pixels whenever I run into a wall. If I let go of the arrow key, the sprite will get stuck in the wall permanently. I wondering why the sprite is inside the wall in the first place, since by the time I blit the sprite to the screen, it should have been moved just outside of the wall.
Is there something wrong with my method, or does the problem lie within my execution?
Looks like you're setting the hitbox BEFORE updating the position. The Fix seems simple.
Find:
self.hitBox = Rect(self.gPos.x, self.gPos.y, 40, 40)
self.gPos += self.velocity
Replace:
self.gPos += self.velocity
self.hitBox = Rect(self.gPos.x, self.gPos.y, 40, 40)
Other Suggestions: What you should do is check the position BEFORE you move there, and if it's occupied, don't move. This is untested so please just use this as psuedocode intended to illustrate the point:
class Player(object):
...
def move(self):
#at this point, velocity = some linear combination of (5, 0)and (5, 5)
#gPos and velocity are types Vector2
selfCopy = self
selfCopy.gPos += self.velocity
selfCopy.hitBox = Rect(selfCopy.gPos.x, selfCopy.gPos.y, 40, 40)
if not CheckCollisions(selfCopy)
self.gPos += self.velocity
...
def CheckCollisions(obj):
#archList holds all 'architecture' objects, solid == True means you can't walk
#through it. colliderect checks to see if the rectangles are overlapping
for i in archList:
if i.solid:
if i.hitBox.colliderect(obj.hitBox):
return True
return False

Categories