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)
Related
I'm building a pong game trying to get better at programming but Im having trouble moving the ball. When the move_right method is called the ellipse stretches to the right instead of moving to the right. I've tried putting the ball variable in the init method but that just makes it not move at all even though the variables should be changing on account of the move_right method. I have also tried setting the x and y positions as parameters in the Ball class,but that just stretches it also.
I don't understand why when I run the following code the ball I'm trying to move stretches to the right instead of moves to the right. Can someone explain why this is happening? I have tried everything I can think of but i can't get it to do what I want.
import pygame,sys
import random
class Ball:
def __init__(self):
self.size = 30
self.color = light_grey
self.x_pos = width/2 -15
self.y_pos = height/2 -15
self.speed = 1
#self.ball = pygame.Rect(self.x_pos, self.y_pos,self.size,self.size)
def draw_ball(self):
ball = pygame.Rect(self.x_pos, self.y_pos,self.size,self.size)
pygame.draw.ellipse(screen,self.color,ball)
def move_right(self):
self.x_pos += self.speed
class Player:
def __init__(self,x_pos,y_pos,width,height):
self.x_pos = x_pos
self.y_pos = y_pos
self.width = width
self.height = height
self.color = light_grey
def draw_player(self):
player = pygame.Rect(self.x_pos,self.y_pos,self.width,self.height)
pygame.draw.rect(screen,self.color,player)
class Main:
def __init__(self):
self.ball=Ball()
self.player=Player(width-20,height/2 -70,10,140)
self.opponent= Player(10,height/2-70,10,140)
def draw_elements(self):
self.ball.draw_ball()
self.player.draw_player()
self.opponent.draw_player()
def move_ball(self):
self.ball.move_right()
pygame.init()
size = 30
clock = pygame.time.Clock()
pygame.display.set_caption("Pong")
width = 1000
height = 600
screen = pygame.display.set_mode((width,height))
bg_color = pygame.Color('grey12')
light_grey = (200,200,200)
main = Main()
#ball = pygame.Rect(main.ball.x_pos, main.ball.y_pos,main.ball.size,main.ball.size)
#player = pygame.Rect(width-20,height/2 -70,10,140)
#opponent = pygame.Rect(10,height/2-70,10,140)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
#ball = pygame.Rect(main.ball.x_pos, main.ball.y_pos,main.ball.size,main.ball.size)
#pygame.draw.rect(screen,light_grey,player)
#pygame.draw.rect(screen,light_grey,opponent)
#pygame.draw.ellipse(screen,light_grey,ball)
main.draw_elements()
main.move_ball()
main.ball.x_pos += main.ball.speed
pygame.display.flip()
clock.tick(60)
You have to clear the display in every frame with pygame.Surface.fill:
while True:
# [...]
screen.fill(0) # <---
main.draw_elements()
main.move_ball()
main.ball.x_pos += main.ball.speed
pygame.display.flip()
# [...]
Everything that is drawn is drawn on the target surface. The entire scene is redraw in each frame. Therefore the display needs to be cleared at the begin of every frame in the application loop. The typical PyGame application loop has to:
handle the events by either pygame.event.pump() or pygame.event.get().
update the game states and positions of objects dependent on the input events and time (respectively frames)
clear the entire display or draw the background
draw the entire scene (blit all the objects)
update the display by either pygame.display.update() or pygame.display.flip()
In my game I am trying to make it so when you walk into a object, it displays an image.
I'm pretty sure that pygame.display.update() is being called every frame because otherwise the game would be perfectly still.
However when I draw my new rect upon collision it doesn't appear, unless I put another pygame.display.update(rect) with it after it being drawn. This means that update is being called twice at one time, in the main game loop and after drawing the rect. This causes the rect (which has been drawn now) to flicker because of the multiple update calls.
I cannot figure it out why it doesn't get drawn without the second update call.
Main game loop call:
def events(self):
#game loop events
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.playing = False
self.running = False
def update(self):
self.all_sprites.update()
def main(self):
while self.playing:
self.events()
self.update()
self.draw()
self.running = False
def draw(self):
self.screen.fill(black)
self.all_sprites.draw(self.screen)
self.clock.tick(FPS)
pygame.display.update()
#create game instance
g= Game()
g.new()
while g.running:
#main gameloop
g.main()
pygame.quit()
sys.exit()
Here is when I call to draw the rect after collision with my object:
def update(self):
self.hitbox.center = numpy.add(self.rect.center,(8,22))
self.interactionhitbox.center = numpy.add(self.rect.center, (8,16))
if(self.showPopup):
# Initialwzng Color
color = (255,0,0)
# Drawing Rectangle
rect = pygame.Rect((0,0,60,60))
pygame.display.update(rect) # WITHOUT THIS LINE IT DOES NOT GET DRAWN, WITH IT IT FLICKERS
pygame.draw.rect(self.game.screen, color, rect)
So basically with the second pygame.display.update(rect) call it appears but flickers, and without it it doesn't show up at all
Any help is appreciated sorry if this is a bad question or not formatted right I haven't been here since 2017!
The rectangle is not drawn because the screen will later be cleared with self.screen.fill(black) later. You must draw the rectangle after self.screen.fill(black) and before pygame.display.update().
Create 2 images and choose the image to be drawn in update:
def __init__(self, ...)
# [...]
self.image = ...
self.original_image = self.image
self.image_and_rect = self.image.copy()
pygame.draw.rect(self.image_and_rect, (255,0,0), self.image_and_rect.get_rect(), 5)
def update(self):
self.hitbox.center = numpy.add(self.rect.center,(8,22))
self.interactionhitbox.center = numpy.add(self.rect.center, (8,16))
if self.showPopup:
self.image = self.image_and_rect
else:
self.image = self.original_image
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.
I am trying to build a game in which combining images (pygame sprites) is an essential tool.
I have set up my code such that I can move sprites across x,y with the mouse and rotate them. The sprites are blitted to the display surface and so this motion can be seen on screen.
Once the user has arranged two sprites as they wish within a square zone, I need them to be able to save this whole zone as a new sprite.
I cannot see a way currently on pygame to capture a region of the display and store this as a sprite. Is this possible? What functions should I use for this purpose?
You could check which sprites collide with the square area and pass them to a Combined sprite class, combine the rects with the union_ip method and create a new surface with the necessary size to blit the surfaces of the single sprites onto it. (Press C to combine the sprites.)
import pygame as pg
BLUE = pg.Color('dodgerblue1')
SIENNA = pg.Color('sienna1')
GREEN = pg.Color('green')
class Entity(pg.sprite.Sprite):
def __init__(self, pos, color):
super().__init__()
self.image = pg.Surface((42, 68))
self.image.fill(color)
self.rect = self.image.get_rect(topleft=pos)
def move(self, velocity):
self.rect.move_ip(velocity)
class Combined(pg.sprite.Sprite):
def __init__(self, sprites):
super().__init__()
# Combine the rects of the separate sprites.
self.rect = sprites[0].rect.copy()
for sprite in sprites[1:]:
self.rect.union_ip(sprite.rect)
# Create a new transparent image with the combined size.
self.image = pg.Surface(self.rect.size, pg.SRCALPHA)
# Now blit all sprites onto the new surface.
for sprite in sprites:
self.image.blit(sprite.image, (sprite.rect.x-self.rect.left,
sprite.rect.y-self.rect.top))
def move(self, velocity):
self.rect.move_ip(velocity)
def main():
pg.init()
screen = pg.display.set_mode((640, 480))
clock = pg.time.Clock()
entity = Entity((50, 80), BLUE)
entity2 = Entity((50, 180), SIENNA)
all_sprites = pg.sprite.Group(entity, entity2)
area = pg.Rect(200, 50, 200, 200)
selected = None
while True:
for event in pg.event.get():
if event.type == pg.QUIT:
return
elif event.type == pg.MOUSEBUTTONDOWN:
for sprite in all_sprites:
if sprite.rect.collidepoint(event.pos):
selected = sprite
elif event.type == pg.MOUSEBUTTONUP:
selected = None
elif event.type == pg.MOUSEMOTION:
if selected:
selected.move(event.rel)
elif event.type == pg.KEYDOWN:
if event.key == pg.K_c:
# A 'list comprehension' to find the colliding sprites.
colliding_sprites = [sprite for sprite in all_sprites
if sprite.rect.colliderect(area)]
combined = Combined(colliding_sprites)
all_sprites.add(combined)
# Kill the colliding sprites if they should be removed.
# for sprite in colliding_sprites:
# sprite.kill()
all_sprites.update()
screen.fill((30, 30, 30))
pg.draw.rect(screen, SIENNA, area, 2)
all_sprites.draw(screen)
for sprite in all_sprites: # Outlines.
pg.draw.rect(screen, GREEN, sprite.rect, 1)
pg.display.flip()
clock.tick(60)
if __name__ == '__main__':
main()
pg.quit()
Alternatively, you could try to add the combined sprites to another sprite group or a list and blit and move them together.
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.