Could not draw the pillars of flappy bird using sprite in pygame - python

I am making a flappy bird clone game, using pygame. I want to draw pillars by using Sprite.draw. I made a Pillar class and initialized it with two rectangles p_upper and p_lower on the left side of the screen, coming towards the right side with the help of the update function of the sprite. But the screen is only showing the p_lower pillar. Can anyone help?
class Pillar(pygame.sprite.Sprite):
# the "h" parameter is height of upper pillar upto gap
# "w" is the width of the pillar
# pillar is coming from left to right
def __init__(self, w, h, gap):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface((w, h))
self.image.fill(green)
self.p_upper = self.rect = self.image.get_rect()
self.p_upper.topleft = (-w, 0)
self.image = pygame.Surface((w, HEIGHT - (h + gap)))
self.image.fill(green)
self.p_lower = self.rect = self.image.get_rect()
self.p_lower.topleft = (-w, h + gap)
def update(self):
self.p_upper.x += 1
self.p_lower.x += 1

Because of the following two lines:
self.p_upper = self.rect = self.image.get_rect()
and...
self.p_lower = self.rect = self.image.get_rect()
These are both grabbing the same reference to the self.rect. The first line runs and assigns the rect reference to p_upper. Then the same reference is assigned to p_lower. Because it's the same reference, when you update the location of the lower rectangle, you're actually updating both.

Using a sprite that consists of two rects and images isn't a good solution for this problem. I suggest to create two separate sprites with their own image and rect. To create two sprite instances at the same time and to add them to a sprite group, you can write a short function as you can see in this example:
import pygame as pg
from pygame.math import Vector2
green = pg.Color('green')
HEIGHT = 480
class Pillar(pg.sprite.Sprite):
def __init__(self, x, y, w, h):
pg.sprite.Sprite.__init__(self)
self.image = pg.Surface((w, h))
self.image.fill(green)
self.rect = self.image.get_rect(topleft=(x, y))
def update(self):
self.rect.x += 1
def create_pillars(w, h, gap, sprite_group):
sprite_group.add(Pillar(0, 0, w, h-gap))
sprite_group.add(Pillar(0, HEIGHT-(h+gap), w, h+gap))
def main():
screen = pg.display.set_mode((640, 480))
clock = pg.time.Clock()
all_sprites = pg.sprite.Group()
create_pillars(50, 170, 0, all_sprites)
done = False
while not done:
for event in pg.event.get():
if event.type == pg.QUIT:
done = True
elif event.type == pg.KEYDOWN:
if event.key == pg.K_a:
create_pillars(50, 170, 15, all_sprites)
elif event.key == pg.K_s:
create_pillars(50, 170, 30, all_sprites)
elif event.key == pg.K_d:
create_pillars(50, 100, -60, all_sprites)
all_sprites.update()
screen.fill((30, 30, 30))
all_sprites.draw(screen)
pg.display.flip()
clock.tick(30)
if __name__ == '__main__':
pg.init()
main()
pg.quit()

Related

Detecting the point of collision with Pygame

I am making a game with Pygame. For this game I need to be able to detect not only that two rectangles have collided, but also the point of collision between them. I looked at the documentation but couldn't seem to find any answers.
Is something like this possible?
You can use Rect.clip:
crops a rectangle inside another
clip(Rect) -> Rect
Returns a new rectangle that is cropped to be completely inside the argument Rect. If the two rectangles do not overlap to begin with, a Rect with 0 size is returned.
Here's an example:
import pygame
import random
class Stuff(pygame.sprite.Sprite):
def __init__(self, pos, color, *args):
super().__init__(*args)
self.image = pygame.Surface((30, 30))
self.image.fill(color)
self.rect = self.image.get_rect(center=pos)
self.pos = pygame.Vector2(pos)
def update(self):
self.rect.center = self.pos
def main():
pygame.init()
screen = pygame.display.set_mode((500, 500))
screen_rect = screen.get_rect()
font = pygame.font.SysFont(None, 26)
clock = pygame.time.Clock()
sprites = pygame.sprite.Group()
blocks = pygame.sprite.Group()
movement = {
pygame.K_UP: ( 0, -1),
pygame.K_DOWN: ( 0, 1),
pygame.K_LEFT: (-1, 0),
pygame.K_RIGHT: ( 1, 0)
}
for _ in range(15):
x, y = random.randint(0, 500), random.randint(0, 500)
color = random.choice(['green', 'yellow'])
Stuff((x, y), pygame.Color(color), sprites, blocks)
player = Stuff(screen_rect.center, pygame.Color('dodgerblue'))
sprites.add(player)
dt = 0
while True:
events = pygame.event.get()
for e in events:
if e.type == pygame.QUIT:
return
pressed = pygame.key.get_pressed()
move = pygame.Vector2()
for dir in (movement[key] for key in movement if pressed[key]):
move += dir
if move.length() > 0: move.normalize_ip()
player.pos += move * dt/5
sprites.update()
screen.fill(pygame.Color('black'))
sprites.draw(screen)
for block in pygame.sprite.spritecollide(player, blocks, False):
clip = player.rect.clip(block.rect)
pygame.draw.rect(screen, pygame.Color('red'), clip)
hits = [edge for edge in ['bottom', 'top', 'left', 'right'] if getattr(clip, edge) == getattr(player.rect, edge)]
text = font.render(f'Collision at {", ".join(hits)}', True, pygame.Color('white'))
screen.blit(text, (20, 20))
pygame.display.flip()
dt = clock.tick(60)
if __name__ == '__main__':
main()

Pygame Collision Bug

I'm quite new to pygame and came across a bug that i just can't fix on my own. I'm trying to program a Flappy Bird game. The Problem is that the collision detection works, but it also messes with my sprites. If i manage to get past the first obstacle while playing, then the gap resets itself randomly. But the gap should always be the same, just on another position. If i remove the collision detection, it works perfectly fine. Any ideas?
import pygame
import random
randomy = random.randint(-150, 150)
class Bird(pygame.sprite.Sprite):
def __init__(self):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface((25,25))
self.image.fill((255,255,255))
self.rect = self.image.get_rect()
self.rect.center = (100, 200)
self.velocity = 0.05
self.acceleration =0.4
def update(self):
self.rect.y += self.velocity
self.velocity += self.acceleration
if self.rect.bottom > 590:
self.velocity = 0
self.acceleration = 0
class Pipe1(pygame.sprite.Sprite):
def __init__(self):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface((85, 500))
self.image.fill((255, 255, 255))
self.rect = self.image.get_rect()
self.rect.center = (500, randomy)
self.randomyupdate = random.randint(-150, 150)
def update(self):
self.rect.x -= 2
if self.rect.x < -90:
self.randomyupdate = random.randint(-150, 150)
self.rect.center = (450, self.randomyupdate)
class Pipe2(pygame.sprite.Sprite):
def __init__(self):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface((85, 500))
self.image.fill((255, 255, 255))
self.rect = self.image.get_rect()
self.rect.center = (500, (randomy +640))
def update(self):
self.rect.x -= 2
self.randomyupdate = Pipe1.randomyupdate
if self.rect.x < -90:
self.rect.center = (450, (self.randomyupdate + 640))
pygame.init()
pygame.mouse.set_visible(1)
pygame.key.set_repeat(1, 30)
pygame.display.set_caption('Crappy Bird')
clock = pygame.time.Clock()
Bird_sprite = pygame.sprite.Group()
Pipe_sprite = pygame.sprite.Group()
Bird = Bird()
Pipe1 = Pipe1()
Pipe2 = Pipe2 ()
Bird_sprite.add(Bird)
Pipe_sprite.add(Pipe1)
Pipe_sprite.add(Pipe2)
def main():
running = True
while running:
clock.tick(60)
screen = pygame.display.set_mode((400,600))
screen.fill((0,0,0))
Bird_sprite.update()
Pipe_sprite.update()
Bird_sprite.draw(screen)
Pipe_sprite.draw(screen)
The line im talking about:
collide = pygame.sprite.spritecollideany(Bird, Pipe_sprite)
if collide:
running = False
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
pygame.event.post(pygame.event.Event(pygame.QUIT))
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
Bird.rect.y -= 85
Bird.velocity = 0.05
Bird.acceleration = 0.4
Bird.rect.y += Bird.velocity
Bird.velocity += Bird.acceleration
pygame.display.flip()
if __name__ == '__main__':
main()
This has nothing to do with the collision detection, it has to do with the order of the sprites in the sprite group which can vary because sprite groups use dictionaries internally which are unordered (in Python versions < 3.6). So if the Pipe1 sprite comes first in the group, the game will work correctly, but if the Pipe2 sprite comes first, then its update method is also called first and the previous randomyupdate of Pipe1 is used to set the new centery coordinate of the sprite.
To fix this you could either turn the sprite group into an ordered group, e.g.
Pipe_sprite = pygame.sprite.OrderedUpdates()
or update the rect of Pipe2 each frame,
def update(self):
self.rect.x -= 2
self.rect.centery = Pipe1.randomyupdate + 640
if self.rect.x < -90:
self.rect.center = (450, Pipe1.randomyupdate + 640)
Also, remove the global randomy variable and always use the randomyupdate attribute of Pipe1.

Offset between origins of sprite.rect and sprite.image coordinates

I've found in the pygame docs that the .rect attribute of a sprite is often initialised with the same width and the same height as the .image attribute.
However, you can change the .rect width or height so that the .rect becomes larger or smaller than the .image but the origins (topleft corners) of both .image and .rect are always the same point.
So here is my question:
Is there a way to create an offset between the origins (ie topleft corners) of the .rect and the .image of a sprite?
I know you can avoid this by:
creating and using a second Rect attribute (for instance a .collide_rect) that would be updated each time the .rect would
using transparency in the provided .image
But those methods are quite inefficient and I would really appreciate a "Yes/No" answer.
I've searched in the official docs for hours but I didn't find the way to create such an offset so I suppose the answer to my question is "No" (even the .inflate_ip() method doesn't work since it doesn't keep the center of the .rect in place as written in the docs).
You could give the sprite class an additional offset attribute (a Vector2) and then iterate over the sprites with a for loop and add this offset to the .rect.topleft position when you're blitting the images.
import pygame as pg
from pygame.math import Vector2
class Player(pg.sprite.Sprite):
def __init__(self, pos, *groups):
super().__init__(*groups)
self.image = pg.Surface((220, 120))
self.image.fill(pg.Color('steelblue2'))
self.rect = self.image.get_rect(center=pos)
self.rect.inflate_ip(-42, -72) # Make the rect smaller.
self.vel = Vector2(0, 0)
self.pos = Vector2(pos)
# Offset is half of the inflation size,
# so that the rect will be centered.
self.offset = Vector2(-21, -36)
def update(self):
self.pos += self.vel
self.rect.center = self.pos
def main():
screen = pg.display.set_mode((640, 480))
clock = pg.time.Clock()
all_sprites = pg.sprite.Group()
player = Player((300, 200), all_sprites)
done = False
while not done:
for event in pg.event.get():
if event.type == pg.QUIT:
done = True
elif event.type == pg.KEYDOWN:
if event.key == pg.K_d:
player.vel.x = 5
elif event.type == pg.KEYUP:
if event.key == pg.K_d:
player.vel.x = 0
all_sprites.update()
screen.fill((30, 30, 30))
# Assign `blit` to a local variable to improve the performance.
screen_blit = screen.blit
for sprite in all_sprites:
# Now blit the sprites at topleft + offset.
screen_blit(sprite.image, sprite.rect.topleft+sprite.offset)
pg.draw.rect(screen, (250, 30, 0), sprite.rect, 2)
pg.display.flip()
clock.tick(30)
if __name__ == '__main__':
pg.init()
main()
pg.quit()
For the sake of completeness, here's the solution with a second, scaled "hitbox" rect and a custom collided callback function that you can pass to one of the collision detection functions like pygame.sprite.spritecollide.
import pygame as pg
from pygame.math import Vector2
class Player(pg.sprite.Sprite):
def __init__(self, pos, *groups):
super().__init__(*groups)
self.image = pg.Surface((70, 40))
self.image.fill(pg.Color('steelblue4'))
self.rect = self.image.get_rect(center=pos)
# A inflated rect as the hitbox.
self.hitbox = self.rect.copy()
self.hitbox.inflate_ip(-42, -22)
self.vel = Vector2(0, 0)
self.pos = Vector2(pos)
def update(self):
self.pos += self.vel
self.rect.center = self.pos
self.hitbox.center = self.pos # Also update the hitbox coords.
def collided(sprite, other):
"""Check if the hitboxes of the two sprites collide."""
return sprite.hitbox.colliderect(other.hitbox)
def main():
screen = pg.display.set_mode((640, 480))
clock = pg.time.Clock()
all_sprites = pg.sprite.Group()
player = Player((300, 200), all_sprites)
enemies = pg.sprite.Group(
Player((100, 250), all_sprites),
Player((400, 300), all_sprites),
)
done = False
while not done:
for event in pg.event.get():
if event.type == pg.QUIT:
done = True
elif event.type == pg.MOUSEMOTION:
player.pos = event.pos
all_sprites.update()
# Pass the custom collided callback function to spritecollide.
collided_sprites = pg.sprite.spritecollide(
player, enemies, False, collided)
for sp in collided_sprites:
print('Collision', sp)
screen.fill((30, 30, 30))
all_sprites.draw(screen)
for sprite in all_sprites:
# Draw rects and hitboxes.
pg.draw.rect(screen, (0, 230, 0), sprite.rect, 2)
pg.draw.rect(screen, (250, 30, 0), sprite.hitbox, 2)
pg.display.flip()
clock.tick(30)
if __name__ == '__main__':
pg.init()
main()
pg.quit()

Pygame: Collision of two images

I'm working on my school project for which im designing a 2D game.
I have 3 images, one is the player and the other 2 are instances (coffee and computer). What i want to do is, when the player image collides with one of the 2 instances i want the program to print something.
I'm unsure if image collision is possible. But i know rect collision is possible. However, after several failed attempts, i can't manage to make my images rects. Somebody please help me. Here is my source code:
import pygame
import os
black=(0,0,0)
white=(255,255,255)
blue=(0,0,255)
class Player(object):
def __init__(self):
self.image = pygame.image.load("player1.png")
self.image2 = pygame.transform.flip(self.image, True, False)
self.coffee=pygame.image.load("coffee.png")
self.computer=pygame.image.load("computer.png")
self.flipped = False
self.x = 0
self.y = 0
def handle_keys(self):
""" Movement keys """
key = pygame.key.get_pressed()
dist = 5
if key[pygame.K_DOWN]:
self.y += dist
elif key[pygame.K_UP]:
self.y -= dist
if key[pygame.K_RIGHT]:
self.x += dist
self.flipped = False
elif key[pygame.K_LEFT]:
self.x -= dist
self.flipped = True
def draw(self, surface):
if self.flipped:
image = self.image2
else:
im = self.image
for x in range(0, 810, 10):
pygame.draw.rect(screen, black, [x, 0, 10, 10])
pygame.draw.rect(screen, black, [x, 610, 10, 10])
for x in range(0, 610, 10):
pygame.draw.rect(screen, black, [0, x, 10, 10])
pygame.draw.rect(screen, black, [810, x, 10, 10])
surface.blit(self.coffee, (725,500))
surface.blit(self.computer,(15,500))
surface.blit(im, (self.x, self.y))
pygame.init()
screen = pygame.display.set_mode((800, 600))#creates the screen
player = Player()
clock = pygame.time.Clock()
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit() # quit the screen
running = False
player.handle_keys() # movement keys
screen.fill((255,255,255)) # fill the screen with white
player.draw(screen) # draw the player to the screen
pygame.display.update() # update the screen
clock.tick(60) # Limits Frames Per Second to 60 or less
Use pygame.Rect() to keep image size and position.
Image (or rather pygame.Surface()) has function get_rect() which returns pygame.Rect() with image size (and position).
self.rect = self.image.get_rect()
Now you can set start position ie. (0, 0)
self.rect.x = 0
self.rect.y = 0
# or
self.rect.topleft = (0, 0)
# or
self.rect = self.image.get_rect(x=0, y=0)
(Rect use left top corner as (x,y)).
Use it to change position
self.rect.x += dist
and to draw image
surface.blit(self.image, self.rect)
and then you can test collision
if self.rect.colliderect(self.rect_coffe):
BTW: and now class Player looks almost like pygame.sprite.Sprite :)

TypeError: Argument must be rect style object in pygame

For a while now I've been trying to teach myself how to use sprites in pygame and right now I am stuck on collision detection.
The specific place I am having trouble with in my code is the commented section labeled "error here" and that's the code that keeps giving me the "TypeError: Argument must be rect style object" error and the goal of that specific code is to detect collision.
The goal of this code is to print a message in the shell whenever the player block enters the non-player block and as I said earlier I have been having trouble getting that to happen.
from pygame.locals import *
import pygame
pygame.init()
SIZE = WIDTH, HEIGHT = 500, 700
screen = pygame.display.set_mode(SIZE)
plr_g = pygame.sprite.Group()
h_box = pygame.sprite.Group()
BLUE = (0, 206, 209)
GREEN = (0, 255, 0)
class Player(pygame.sprite.Sprite):
def __init__(self, width, height):
pygame.sprite.Sprite.__init__(self, plr_g)
self.image = pygame.Surface([width, height])
self.image.fill(BLUE)
self.rect = self.image.get_rect()
self.rect.center = (WIDTH / 2, HEIGHT / 2)
#self.rect = (400, 200)
def x_pos(self):
self.rect.x
def y_pos(self):
self.rect.y
def move_l(self, pixels):
self.rect.x -= pixels
def move_r(self, pixels):
self.rect.x += pixels
def move_u(self, pixels):
self.rect.y -= pixels
def move_d(self, pixels):
self.rect.y += pixels
class Hitbox(pygame.sprite.Sprite):
def __init__(self, bx, by):
pygame.sprite.Sprite.__init__(self, h_box)
self.image = pygame.Surface([100, 100])
self.image.fill(GREEN)
self.rect = self.image.get_rect()
self.rect = (bx, by)
hitbox = Hitbox(300, 300)
hitbox = Hitbox(100, 500)
player = Player(50, 50)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
elif event.type==pygame.KEYDOWN:
if event.key==pygame.K_LEFT:
player.move_l(10)
if event.key==pygame.K_RIGHT:
player.move_r(10)
if event.key==pygame.K_UP:
player.move_u(10)
if event.key==pygame.K_DOWN:
player.move_d(10)
#error here
if plr_g.colliderect(h_box):
print("collide")
#----------------
plr_g.update()
h_box.update()
screen.fill((50, 50, 50))
h_box.draw(screen)
plr_g.draw(screen)
pygame.display.flip()
h_box is a sprite group, not a sprite and definitely not a rect. The collide_rect function of sprites must be called on individual sprites. A possible solution is something like the following, iterating over all the sprites in h_box:
if any([plr_g.colliderect(sp) for sp in h_box]):
print("collide")
block_hit_list = plr_g.colliderect(h_box)
for block in block_hit_list:
print ("collide")
I basically loop through the list to see if there are any collisions between the sprites in plr_g and the sprites in hitbox.

Categories

Resources