so, Im trying to get a reaction when the sprite player pass around (not over) another sprite.
So far my idea is something like:
if (sprite.x +100) > player.x > (sprite.x -100) and (sprite.y +100) > player.y > (sprite.y -100):
print("hit")
# Sprite.x and y are the position of the rect, not the whole image
This works but there is some way to simplify it? Also Im thinking how to get rid of the 100 pixels
If you want to use a scaled rect for the collision detection, you can inflate your original rect (or create a new one) and assign it as a separate attribute to your sprite (I call it hitbox here). The original rect will only be used to store the position.
Create a custom collision detection function which has to be passed as the collided argument to pygame.sprite.spritecollide or groupcollide. In this function you can use the colliderect method of the hitbox rect to check if it collides with the rect of the other sprite.
def hitbox_collision(sprite1, sprite2):
"""Check if the hitbox of the first sprite collides with the
rect of the second sprite.
`spritecollide` will pass the player object as `sprite1`
and the sprites in the enemies group as `sprite2`.
"""
return sprite1.hitbox.colliderect(sprite2.rect)
Then call spritecollide in this way:
collided_sprites = pg.sprite.spritecollide(
player, enemies, False, collided=hitbox_collision)
Here's a complete example (the red rect is the hitbox and the green rect the self.rect which is used as the blit position):
import pygame as pg
class Player(pg.sprite.Sprite):
def __init__(self, pos, *groups):
super().__init__(*groups)
self.image = pg.Surface((30, 50))
self.image.fill(pg.Color('dodgerblue1'))
self.rect = self.image.get_rect(center=pos)
# Give the sprite another rect for the collision detection.
# Scale it to the desired size.
self.hitbox = self.rect.inflate(100, 100)
def update(self):
# Update the position of the hitbox.
self.hitbox.center = self.rect.center
class Enemy(pg.sprite.Sprite):
def __init__(self, pos, *groups):
super().__init__(*groups)
self.image = pg.Surface((30, 50))
self.image.fill(pg.Color('sienna1'))
self.rect = self.image.get_rect(center=pos)
def hitbox_collision(sprite1, sprite2):
"""Check if the hitbox of the first sprite collides with the
rect of the second sprite.
`spritecollide` will pass the player object as `sprite1`
and the sprites in the enemies group as `sprite2`.
"""
return sprite1.hitbox.colliderect(sprite2.rect)
def main():
screen = pg.display.set_mode((640, 480))
clock = pg.time.Clock()
all_sprites = pg.sprite.Group()
enemies = pg.sprite.Group()
player = Player((100, 300), all_sprites)
enemy = Enemy((320, 240), all_sprites, enemies)
done = False
while not done:
for event in pg.event.get():
if event.type == pg.QUIT:
done = True
elif event.type == pg.MOUSEMOTION:
player.rect.center = event.pos
all_sprites.update()
collided_sprites = pg.sprite.spritecollide(
player, enemies, False, collided=hitbox_collision)
for enemy_sprite in collided_sprites:
print(enemy_sprite)
screen.fill((30, 30, 30))
all_sprites.draw(screen)
pg.draw.rect(screen, (0, 255, 0), player.rect, 1)
pg.draw.rect(screen, (255, 0, 0), player.hitbox, 1)
pg.display.flip()
clock.tick(30)
if __name__ == '__main__':
pg.init()
main()
pg.quit()
If you want a circular collision area, you can just pass the pygame.sprite.collide_circle function as the collided argument.
Give your sprites a self.radius attribute,
class Player(pg.sprite.Sprite):
def __init__(self, pos, *groups):
super().__init__(*groups)
self.radius = 100
and in the main loop pass collide_circle to spritecollide.
collided_sprites = pg.sprite.spritecollide(
player, enemies, False, collided=pg.sprite.collide_circle)
So, there's a couple things here. Starting with your second question (as I understand it from how you wrote it), you're correct to want to get rid of the 100 pixels. Having "100" randomly in your code is what's known as a Magic Number/Constant and is bad programming style for a few reasons:
There is no indication anywhere what that 100 is supposed to represent
In many cases (and yours specifically) Magic Numbers get replicated in multiple places. When that number changes (9-out-of-10 times, it does), you'll have to dig through all the code as well as related scripts to update the number and hope you didn't miss it anywhere
Related to 2, you may find out that you want that number (in this case, the hitbox/aura radius) to change often. Hardcoding it makes that difficult or impossible.
Chances are, this section of code is better off as its own function anyway, so you can simply have "hitradius" as a keyword parameter and then substitute "hitradius" for 100.
Either alongside or in place of this, you can track hitradius as an attribute of another object (i.e., the player or other sprite) and then substitute it in the same manner (e.g.- sprite.x - player.hitradius > player.x [etc]). If you're sure that it's never going to change, then have it as an accessible variable somewhere and use the same technique.
def check_aura_collision(player,sprite,hitradius = 100):
""" Checks if the player sprite is within hitradius (default 100)
pixels of another sprite.
"""
if (sprite.x + hitradius > player.x > sprite.x - hitradius )\
and (sprite.y + hitradius > player.y > sprite.y - hitradius ):
print("hit")
return True
return False
As for simplifying the logic, it's simple enough already; I might have used absolute value instead, but that's not really a meaningful change. Conversely, however, you may be interested in making it slightly more complex by using some basic trig to check a circular aura instead of the square aura you have right now.
Related
I am working on a game in pygame and I have got the basic drawing on the screen down, but user input is confusing me. I have a player class:
class Player(pygame.sprite.Sprite):
def __init__(self):
super(Player, self).__init__()
self.surf = pygame.Surface((75, 25))
self.surf.fill((255, 255, 255))
self.rect = self.surf.get_rect()
def update(self, pressed_keys):
if pressed_keys[K_UP]:
self.rect.move_ip(0, -5)
if pressed_keys[K_DOWN]:
self.rect.move_ip(0, 5)
if pressed_keys[K_LEFT]:
self.rect.move_ip(-5, 0)
if pressed_keys[K_RIGHT]:
self.rect.move_ip(5, 0)
pygame.event.pump()
But whenever I press the keys down, nothing happens. This is the end of the script where the stuff gets drawn:
while running:
for event in pygame.event.get():
if event.type == KEYDOWN:
if event.key == K_ESCAPE:
running = False
elif event.type == QUIT:
running = False
pressed_keys = pygame.key.get_pressed()
player.update(pressed_keys)
screen.fill((0, 0, 0))
surf = pygame.Surface((50, 50))
surf.fill((0, 0, 0))
rect = surf.get_rect()
screen.blit(player.surf, (SCREEN_WIDTH/2, SCREEN_HEIGHT/2))
pygame.display.flip()
I don't know what is going on, but I can't find anything about this to fix it. Everything draws right, but nothing else.
Issue: logical bug when bliting on screen
Inside your game-loop:
player.update is moving the player by keys-pressed
blit is drawing the player always fixed at the center (SCREEN_WIDTH/2, SCREEN_HEIGHT/2) - although player's position moved
Thus your player stucks visually centered on screen, although you moved it and its position (in rect) was changed by arrow-keys.
Debugging
Add some debugging print statements
before the blit to monitor if players position moved after update: .
after blit to monitor its return value as drawn rectangle
print("player's rect after update:", player.rect)
drawn_rect = screen.blit(player.surf, (SCREEN_WIDTH/2, SCREEN_HEIGHT/2))
print("rect drawn by blit:", drawn_rect)
Research
Suppose your intention with blit was to:
center the player on the screen when drawn initially
draw the player on the screen using blit
Let's consult PyGame docs for some support.
Centering a Surface object
Read the Surface docs on how to center with get_rect():
You can pass keyword argument values to this function. These named values will be applied to the attributes of the Rect before it is returned. An example would be mysurf.get_rect(center=(100, 100)) to create a rectangle for the Surface centered at a given position.
Using blit
From the docs on blit(source, dest, area=None, special_flags=0)
Draws a source Surface onto this Surface. The draw can be positioned with the dest argument. The dest argument can either be a pair of coordinates representing the position of the upper left corner of the blit or a Rect
Fix by design
Spawn the player at the initial position, e.g. centered (on the screen), during creation.
Then your game-loop has the single responsibility to draw the player on the screen.
Player: Center, Move, Draw
class Player(pygame.sprite.Sprite):
def __init__(self):
super(Player, self).__init__()
self.surf = pygame.Surface((75, 25))
self.surf.fill((255, 255, 255))
self.rect = self.centered() # spawn it centerd
def update(self, pressed_keys):
if pressed_keys[K_UP]:
self.move(0, -5)
if pressed_keys[K_DOWN]:
self.move(0, 5)
if pressed_keys[K_LEFT]:
self.move(-5, 0)
if pressed_keys[K_RIGHT]:
self.move(5, 0)
pygame.event.pump()
## added some methods
def centered():
return self.surf.get_rect(center=(SCREEN_WIDTH/2, SCREEN_HEIGHT/2))
def move(x, y):
self.rect.move_ip(x,y)
def draw_on(surface): # visitor-pattern
surface.blit(self.surf, self.rect)
Note: added some functions to Player
centered to position in the center within a constant screen dimension
move to move the player with vector (x,y)
draw_on to draw the player sprite/surface on a given surface like your screen
Draw
Then inside your game-loop replace:
screen.blit(player.surf, (SCREEN_WIDTH/2, SCREEN_HEIGHT/2))
by the new command:
# new player is centered, moved player is located anywhere
player.draw_on(screen)
Note: the _visitor-pattern- changed statement to human-readable sentence in form <subject>.<verb>(<object>) and has one argument less than before (internalized).
Bonus: Advice
Object-oriented design can be improved to enhance readability, ease testing and simplify usage:
control the player by functions like commanding actions
choose a simple game-language (domain terminology)
and use it for naming your abstractions (objects, functions, variables)
Examples: Player, screen, move, centered, draw, _to, _on (nouns, adjectives or verbs alike, with prepositions to express relation)
Often design-improvements make it easier to recognize logical bugs or to analyze the control flow during debugging - hopefully, like in this case.
You have to blit the player at the position stored in the rect attribute:
screen.blit(player.surf, (SCREEN_WIDTH/2, SCREEN_HEIGHT/2))
screen.blit(player.surf, player.rect)
For my project i am recreating donkey kong in pygame, and ive got to the stage where i need sprites for my ladders, platforms and my character, but im unsure how to make sprites and then use these in pygame.
Here is a very basic example for using a Sprite in pygame (see also Sprite):
repl.it/#Rabbid76/PyGame-Sprite
import pygame
pygame.init()
window = pygame.display.set_mode((400, 400))
clock = pygame.time.Clock()
class Player(pygame.sprite.Sprite):
def __init__(self, center_pos, image):
super().__init__()
self.image = image
self.rect = self.image.get_rect(center = center_pos)
def update(self, surf):
keys = pygame.key.get_pressed()
self.rect.x += (keys[pygame.K_d]-keys[pygame.K_a]) * 5
self.rect.y += (keys[pygame.K_s]-keys[pygame.K_w]) * 5
self.rect.clamp_ip(surf.get_rect())
player_surf = pygame.image.load('Bird64.png').convert_alpha()
player = Player(window.get_rect().center, player_surf)
all_sprites = pygame.sprite.Group([player])
run = True
while run:
clock.tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
all_sprites.update(window)
window.fill(0)
all_sprites.draw(window)
pygame.display.flip()
pygame.quit()
exit()
An image can be loaded to a pygame.Surface object with pygame.image.load. See How to draw images and sprites in pygame?.
Pygame uses pygame.sprite.Sprite objects and pygame.sprite.Group objects to manage Sprites. pygame.sprite.Group.draw() and pygame.sprite.Group.update() are methods which are provided by pygame.sprite.Group.
The former delegates the to the update method of the contained pygame.sprite.Sprites - you have to implement the method. See pygame.sprite.Group.update():
Calls the update() method on all Sprites in the Group [...]
The later uses the image and rect attributes of the contained pygame.sprite.Sprites to draw the objects - you have to ensure that the pygame.sprite.Sprites have the required attributes. See pygame.sprite.Group.draw():
Draws the contained Sprites to the Surface argument. This uses the Sprite.image attribute for the source surface, and Sprite.rect. [...]
I'm new at pygame, so I don't know so much about sprites. I wanted to make my code more clean so I used a sprite. And to display my image with rect I wrote:
self.frame_index = 0
self.surf = self.frames[self.frame_index]
self.frame_rect = self.surf.get_rect(midtop = (self.x_pos, self.y_pos))
But it raised AttributeError: 'Enemy' object has no attribute 'image' error. Enemy is my class' name. When I used self.image and self.rect instead of self.surf and self.frame_rect my code worked properly.
My main question is: Why we have to use self.rect and self.image when we use a sprite to determine our surface and rect?
My main question is: Why we have to use self.rect and self.image when we use a sprite to determine our surface and rect?
This is related to the pygame.sprite.Group. Groups are used to manage Sprites. pygame.sprite.Group.draw() and pygame.sprite.Group.update() are methods which are provided by pygame.sprite.Group.
The former delegates the to the update method of the contained pygame.sprite.Sprites - you have to implement the method. See pygame.sprite.Group.update():
Calls the update() method on all Sprites in the Group [...]
The later uses the image and rect attributes of the contained pygame.sprite.Sprites to draw the objects - you have to ensure that the pygame.sprite.Sprites have the required attributes. See pygame.sprite.Group.draw():
Draws the contained Sprites to the Surface argument. This uses the Sprite.image attribute for the source surface, and Sprite.rect. [...]
Minimal example:
import pygame
pygame.init()
window = pygame.display.set_mode((400, 400))
clock = pygame.time.Clock()
class Player(pygame.sprite.Sprite):
def __init__(self, center_pos):
super().__init__()
self.image = pygame.Surface((40, 40))
self.image.fill((0, 255, 0))
self.rect = self.image.get_rect(center = center_pos)
def update(self, surf):
keys = pygame.key.get_pressed()
self.rect.x += (keys[pygame.K_d]-keys[pygame.K_a]) * 5
self.rect.y += (keys[pygame.K_s]-keys[pygame.K_w]) * 5
self.rect.clamp_ip(surf.get_rect())
all_sprites = pygame.sprite.Group([Player(window.get_rect().center)])
run = True
while run:
clock.tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
all_sprites.update(window)
window.fill(0)
all_sprites.draw(window)
pygame.display.flip()
pygame.quit()
exit()
I need help deleting an object, and I mean delete, not draw over or other things.
My code so far:
def detect_collision(player_pos, enemy_pos):
p_x = player_pos[0]
p_y = player_pos[1]
e_x = enemy_pos[0]
e_y = enemy_pos[1]
if (e_x >= p_x and e_x < (p_x + player_size)) or (p_x >= e_x and p_x < (e_x+enemy_size)):
if (e_y >= p_y and e_y < (p_y + player_size)) or (p_y >= e_y and p_y < (e_y+enemy_size)):
return True
return False
def bullets():
b_x = player_pos[0]
b_y = player_pos[1]
keep_going = True
pygame.draw.rect(screen, TEAL, (b_x, b_y, 15, 50))
while keep_going:
b_y += 75
if detect_collision(player_pos, enemy_pos):
# deleting part here
Here is what makes my player and enemy:
enemy_size = 50
enemy_pos = [random.randint(0,WIDTH-enemy_size), 0]
enemy_list = [enemy_pos]
def drop_enemies(enemy_list):
delay = random.random()
if len(enemy_list) < 10 and delay < 0.1:
x_pos = random.randint(0,WIDTH-enemy_size)
y_pos = 0
enemy_list.append([x_pos, y_pos])
def draw_enemies(enemy_list):
for enemy_pos in enemy_list:
pygame.draw.rect(screen, RED, (enemy_pos[0], enemy_pos[1],
enemy_size, enemy_size))
def update_enemy_positions(enemy_list, score):
for idx, enemy_pos in enumerate(enemy_list):
if enemy_pos[1] >= 0 and enemy_pos[1] < HEIGHT:
enemy_pos[1] += SPEED
else:
enemy_list.pop(idx)
score += 1
return score
Player part:
player_size = 50
player_pos = [WIDTH/2, HEIGHT-2*player_size]
pygame.draw.rect(screen, TEAL, (player_pos[0], player_pos[1], player_size,
player_size))
The best way to solve your issue is to learn how to use Sprite objects in pygame. Together with Group objects, they can already do what you want, right out of the box.
In brief, your "enemy" should be an instance of some Sprite subclass, and you'd add it to an instance of Group (rather than building your own enemy_list). When you want the enemy to die, you can call the kill() method on it, which will remove it from the Group. This serves to delete it from the game, since you should be using methods on the Group object to update and draw all the sprites it contains (but not ones that have been killed).
It would be beneficial to look into Sprites and SpriteGroups to track entities in your game. They have a bunch of functionality built in that will make things easier.
Here's a demo I have that groups sprites, removing those that collide with the mouse pointer:
import random
import pygame
screen_width, screen_height = 640, 480
def get_random_position():
"""return a random (x,y) position in the screen"""
return (
random.randint(0, screen_width - 1), # randint includes both endpoints.
random.randint(0, screen_height - 1),
)
color_list = ["red", "orange", "yellow", "green", "cyan", "blue", "blueviolet"]
colors = [pygame.color.Color(c) for c in color_list]
class PowerUp(pygame.sprite.Sprite):
def __init__(self):
pygame.sprite.Sprite.__init__(self)
width, height = 64, 32
self.color = random.choice(colors)
self.image = pygame.Surface([width, height])
self.image.fill(self.color)
# Fetch the rectangle object that has the dimensions of the image
self.rect = self.image.get_rect()
# then move to a random position
self.update()
def update(self):
# move to a random position
self.rect.center = get_random_position()
if __name__ == "__main__":
pygame.init()
screen = pygame.display.set_mode((screen_width, screen_height))
pygame.display.set_caption("Sprite Group Collision Demo")
clock = pygame.time.Clock() # for limiting FPS
FPS = 60
exit_demo = False
# create a sprite group to track the power ups.
power_ups = pygame.sprite.Group()
for _ in range(10):
power_ups.add(PowerUp()) # create a new power up and add it to the group.
# main loop
while not exit_demo:
for event in pygame.event.get():
if event.type == pygame.QUIT:
exit_demo = True
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
exit_demo = True
elif event.key == pygame.K_SPACE:
power_ups.update()
elif event.type == pygame.MOUSEBUTTONUP:
for _ in range(10):
power_ups.add(PowerUp())
# Update State: check for collisions
for p in power_ups:
if p.rect.collidepoint(pygame.mouse.get_pos()):
power_ups.remove(p)
# draw background
screen.fill(pygame.Color("black")) # use black background
# draw sprites
power_ups.draw(screen)
# update screen
pygame.display.update()
clock.tick(FPS)
pygame.quit()
quit()
Clicking a mouse button creates more sprites and pressing the space bar randomises their position.
The update() method of a bullet sprite would adjust the sprite position by its speed, e.g. self.rect.x += SPEED and you would need to call the bullet sprite group .update() method every game loop.
Right, so you have a list of enemy positions in enemy_list. That's a good start. I don't see a bullet_list, so I will assume only a single bullet at a time, positioned at b_x,b_y.
So a main loop for this program might look something like:
### Main Loop
while not game_over:
# handle user input
# move the player
# move the bullet (if any)
# move the enemies
# if there's a bullet, did it hit an enemy?
# remove enemy hit from enemy_list
# Did an enemy in enemy_list, hit the player?
# do game-over
# clear the screen
# paint the player
# paint the bullet (if any)
# paint every enemy in enemy_list
Where the enemy collision, and list removal might look something like the below. (I've tried to match the look of your code as much as possible.)
# if a bullet is on-screen (non 0,0), see if it hits an enemy
if ( b_x != 0 and b_y != 0 ):
hit_enemy_idx = None
for idx, enemy_pos in enumerate( enemy_list ):
# check for collision
if detect_collision( ( b_x, b_y ), enemy_pos ):
hit_enemy = idx # note the enemy index
b_x, b_y = 0, 0 # erase the bullet
break # exit the loop when hit found
# If the bullet hit an enemy, remove from list
if hit_enemy_idx != None:
del( enemy_list[ hit_enemy_idx ] ) # delete hit enemy from list
We do an iteration through the enemy list, checking for a collision. Once a hit is found, it save the index of which enemy was hit, and stops the loop.
The next step is to delete the enemy from the list. I have written this in a separate block, rather than placing it before the break in the loop. This is because unexpected results can happen when you change a list as you're iterating over it. In this particular case it would have been OK, but it's something to be wary of as a beginner.
I'm assuming there's only one bullet in screen at a time since you didn't mention a bullet list. You probably create bullet by hitting space or something which I'll leave out here. One thing to consider in you bullets() function is your doing moving, drawing and checking for collision in the same function. Note, it is always a good idea to make a function do only one thing.
def draw_bullet():
move_bullet()
pygame.draw.rect(screen, TEAL, (b_x, b_y, 15, 50))
def move_bullet():
if b_y < HEIGHT:
b_y += 75
else:
create_bullet = False # explained below
So to create a bullet, you should have a Boolean create_bullet variable. So:
# mainloop
if user hits spacebar (or the key used to create bullet) :
create_bullet = true
b_x = player_pos[0] # initialise bullet
b_y = player_pos[1]
if create_bullet:
draw_bullet()
if detect_collision((b_x, b_y), enemy_pos):
# assuming b_x, b_y are global variable or something, so that they are actually defined and equal to bullet's x and y pos
# if bullet and enemy collide, remove them both
enemy_list.remove(enemy)
create_bullet = False
if detect_collision(player_pos, enemy_pos):
# reset player pos, or whatever you want to do
you say you want to delete it and not just draw over it. However the way pygame generates a 'moving/ video-like' screen is by continuously drawing over. E.g: player is drawn at (10, 10) and then is drawn at (20, 10) so it appears like player moved. However, this is done very fast so you don't see him 'disappear' and 'reappear'. So here's what the above code does.
When spacebar is hit, it 'creates' the bullet by setting it's x and y value to the current player position and sets create_bullet = true. Then, on each iteration of the main loop, if create_bullet is true, it moves then draws the bullet. If the bullet moves outside the screen or collides with enemy, create_bullet = False, so it will stop drawing it and on the next iteration of main loop, bullet will be drawn over by the background and would 'disappear'.
I'm learning python. The idea of the app is that there are 8 rectangles which when clicked turn off or on representing binary. Each rectangle representing a bit, and the decimal equivalent will be displayed.
I'm having difficulty translating that into a well written app without hard coding everything. I hard coded the 8 rects using .fill but I would prefer to use a function which does that automatically. I don't want the full code I'd rather have someone point me in the right direction in regards to the structure of the code I should follow. Should I use a class to initiate a new rect then use a render method with a for loop to run through an array of the rectangles, if so, how would I display the rectangles in a row?
To draw the rectangles I am sticking to display.fill()
I considered to hardcore the properties of each rectangle into a tuple of tuples then render each with a for loop, is this a good approach?
Here is a template you can use:
import pygame
import sys
def terminate():
pygame.quit()
sys.exit()
pygame.init()
black = (0,0,0)
white = (0,0,0)
clock = pygame.time.Clock()
fps=30
screen_height = 520
screen_width = 650
screen = pygame.display.set_mode((screen_width,screen_height)
class Rect(pygame.sprite.Sprite):
def __init__(self,x,y,width,height,color,value):
super().__init__()
self.image = pygame.Surface([width, height])
self.image.fill(color)
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
self.value = value
def change_value(self,color,value):
self.image.fill(color)
self.value=value
rects = pygame.sprite.Group()
rect = Rect(50,50,100,100,black)
rects.add(rect)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
terminate()
screen.fill(white)
rect.draw(screen)
pygame.display.flip()
clock.tick(fps)
Using a group will allow you to draw all the rectangles at once. You can change the rectangles using the change_value function.