How to dynamically generate shapes in Pygame? - python

I'm trying to generate multiple rectangles which translate across the screen with varying distance of separation between any two consecutive rectangles.
Here's the snippet of the code-
win = pygame.display.set_mode((500, 500)) #canvas size is 500x500
width = 40
height = 60
x = 500 - width
y = 500 - height
vel = 5
state = True
while(state):
pygame.time.delay(50)
x -= vel
pygame.draw.rect(win, (0, 0, 255), (x, y, width, height))
pygame.display.update()
#I have not included the pygame exit code
Now, how do I go about this, without making the rectangles disappear everytime I try to generate a new one?

Create a list of rectangles:
rect_list = []
When you want to add a new rectangle, the append an new pygame.Rect object to the list:
rect_list.append(pygame.Rect(x, y, width, height))
Change the location of the rectangle and draw the rectangles in a loop, in the main application loop:
state = True
while state:
# [...]
for rect_obj in rect_list:
rect_obj.x -= vel
pygame.draw.rect(win, (0, 0, 255), rect_obj)
# [...]

This answer is derived off Rabbid76's answer, but with a slight modification that allows each individual rect to have it's own vel speed and color:
class rectangle:
# You can add as many new values as you like, just be wary about changing the other magic methods
def __init__(top, left, width, height, vel, color=(0, 0, 255)):
self.vel = vel
self.pos = (top, left)
self.size = (width, height)
self.rect = self.pos + self.size
def __iter__(self):
return self.rect
def __getitem__(self, key):
if type(key)!=int:
raise TypeError('invalid key!')
return self.rect[key]
def __len__(self):
return len(self.rect)
def __reversed__(self):
return reversed(self.rect)
def update_rect(self): # This should be called every time you make an adjustment to pos or size
self.rect = self.pos+self.size
def move(self, x, y):
self.pos[0] += x
self.pos[1] += y
self.update_rect
def resize(self, width, height):
self.size = (width, height)
self.update_rect
Now, you store a list of instances:
rect_list = []
rect_list.append(rectangle(top, left, width, height, vel, color)) # You can call this as many times as you want
of these classes instead of a list of pygame.Rects, and when it comes time to draw it, the magic methods will come into play, and magically make it a pygame.Rect object:
for rect in rect_list:
rect.move(x=-rect.vel, y=0) # Move it negative x
pygame.draw.rect(win, rect.color, pygame.Rect(rect)) # Draw it.

Related

Is it possible to display a change of color to a pygame Sprite?

I have a pygame Sprite which is generated through Font. It's just a 16x16 surface with a letter printed on it and blitted.
The sprite has a timer (it's a powerup) and when it's near the end of it's life I want it to flash a random color on every update. I've been successful doing this with other text but that text isn't a sprite, just a string I blit to the score board. I figured this would be the same, but once the sprite is generated, no matter how much I change the sprite's color, the change doesn't translate to the screen (though if I print(self.color) I can see the updated color tuple in the console).
I've tried putting the random color picker inside the Class as well as trying outside the class in my while loop. I can change the color easily enough, but the sprite on screen doesn't actually change. I am not using an external sprite image, just a Font blitted to a pygame.Surface.
This is my item class.
class Item(pygame.sprite.Sprite):
def __init__(self, name, pos):
pygame.sprite.Sprite.__init__(self)
self.name = name
self.image = pygame.Surface([16, 16])
self.image.set_colorkey(black)
self.font = pygame.font.Font("./fonts/myfont.ttf", 16)
self.pos = pos
if self.name == "health":
self.color = (255, 0, 0)
self.text = self.font.render("H", True, self.color)
self.lifespan = 200
self.lifespan_counter = 0
self.image.blit(self.text, (0, 0))
def update(self):
# Update timer
self.lifespan_counter += 0.1
if self.lifespan_counter >= self.lifespan:
self.kill()
# Update position
self.rect.center = (int(self.pos[0]), int(self.pos[1]))
And then at the bottom of my def main() in the while loop, I have this stuff:
random_color_counter += 1
if random_color_counter > 3:
random_color = get_random_color()
random_color_counter = 0
screen.fill(background)
text_box.fill(blue)
game_box.fill(white)
# Update the sprites positions and then draw them to game_box surface
player_sprites.update()
player_bullet_sprites.update()
enemy_sprites.update()
enemy_bullet_sprites.update()
item_sprites.update()
player_sprites.draw(game_box)
player_bullet_sprites.draw(game_box)
enemy_sprites.draw(game_box)
enemy_bullet_sprites.draw(game_box)
item_sprites.draw(game_box)
...
for i in item_sprites:
game_box.blit(i.image, (int(i.pos[0]), int(i.pos[1])))
# Refresh everything
pygame.display.update()
And this is the function that picks a new color.
def get_random_color():
r = random.randint(0, 255)
g = random.randint(0, 255)
b = random.randint(0, 255)
return r, g, b
And then I can use the color random_color for most things, just not sprites apparently.
Like I said, this displays the sprite just fine at the position it is supposed to (where the baddie died), but I cannot seem to have a change to the item sprites color translate to the screen. I'm just not seeing what I'm doing wrong.
When you want to change the color of the text, you have to render the text again and update the text Surface. Write a change_color method:
class Item(pygame.sprite.Sprite):
# [...]
def change_color(self, color):
self.image = pygame.Surface([16, 16])
self.image.set_colorkey(black)
self.color = color
self.text = self.font.render("H", True, self.color)
self.image.blit(self.text, (0, 0))

Change color of Pygame Surface

I've made a Rectangle class:
class Rectangle(pygame.sprite.Sprite):
def __init__(self, len, x, y, color):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface((len, len), flags=pygame.SRCALPHA)
# self.image.set_colorkey(Constants.WHITE)
self.image.fill(color)
self.rect = self.image.get_rect()
self.rect.center = (x + len / 2, y + len / 2)
self.is_blocked = False
self.x_pos = x
self.y_pos = y
self.x = math.floor((x - Constants.PADTOPBOTTOM) / Constants.CELL_SIZE)
self.y = math.floor((y - Constants.PADLEFTRIGHT) / Constants.CELL_SIZE)
def update(self):
if self.is_blocked:
print("is update working?") # it is working.
self.image.fill(Constants.GREEN)
And when update() is called I want to change the color of the rectangle to green but it doesn't work.
I'm new to pygame and I don't know what to do.
Your problem is that you keep creating new sprites every frame, so when you change the color of one Rectangle to green, it will be covered by a white one.
You do this by calling the fill function (which creates new Rectangle instances) inside the drawGrid function.

Collision detection between pygame.Surface and mouse not working [duplicate]

This question already has answers here:
Why is my collision test always returning 'true' and why is the position of the rectangle of the image always wrong (0, 0)?
(1 answer)
How to detect collisions between two rectangular objects or images in pygame
(1 answer)
Closed 2 years ago.
I am trying to make a canvas for pixel art.
class Canvas:
def __init__(self):
self.__blocks = []
self.__positions = []
for i in range(1830):
self.__blocks.append(pygame.Surface((20, 20)).convert())
for y in range(30):
y *= 20
for x in range(61):
x = x* 20
self.__positions.append([x, y])
self.__color = False
def draw(self, window):
for i in range(1830):
self.__color = not self.__color
if self.__color:
self.__blocks[i].fill((200, 200, 200))
else:
self.__blocks[i].fill((50, 50, 50))
window.blit(self.__blocks[i], (self.__positions[i][0]
, self.__positions[i][1]))
Here I am trying to generate and draw 1830 unique surfaces and this works. I then tried implementing collision detection between each block and the mouse and failed.
def collided(self, pos):
for i in range(1380):
block = self.__blocks[i].get_rect()
if block.collidepoint(pos[0], pos[1]):
print(block.x, block.y)
Then I did different tests on why it might be failing. Here is one of them. I will change a single block's color, in our case the 10th block self.__blocks[10].fill((255, 0, 0)) to red so we know which box to click on. Then we will try to check for collision for that particular block.
def testBlock(self, pos):
block = self.__blocks[10].get_rect()
if block.collidepoint(pos[0], pos[1]):
print(block.x)
And it doesn't work, but the weird thing is it works for the first block(in the 0th index) and only the first block no matter which surface I test. Any idea on how to fix this would be appreciated. The following is copy and paste code.
import pygame
pygame.init()
win = pygame.display
D = win.set_mode((1220, 600))
class Canvas:
def __init__(self):
self.__blocks = []
self.__positions = []
for i in range(1830):
self.__blocks.append(pygame.Surface((20, 20)).convert())
for y in range(30):
y *= 20
for x in range(61):
x = x* 20
self.__positions.append([x, y])
self.__color = False
self.testBlock = 10
def draw(self, window):
for i in range(1830):
self.__color = not self.__color
if self.__color:
self.__blocks[i].fill((200, 200, 200))
else:
self.__blocks[i].fill((50, 50, 50))
self.__blocks[self.testBlock].fill((255, 0, 0)) # Changing the color for testing
window.blit(self.__blocks[i], (self.__positions[i][0]
, self.__positions[i][1]))
def test(self, pos):
block = self.__blocks[self.testBlock].get_rect()
if block.collidepoint(pos[0], pos[1]):
print(block.x, block.y)
canvas = Canvas()
while True:
D.fill((0, 0, 0))
pygame.event.get()
mousepos = pygame.mouse.get_pos()
canvas.draw(D)
canvas.test(mousepos)
win.flip()
When you call .get_rect() on a Surface, it does not know its current position, because that is not Surface information. So you need to assign the location to the Rect before collision detection.
With your current code layout, you could do this during the construction. With the Canvass blocks position now held in the __rects list, the __positions list becomes superfluous.
class Canvass:
def __init__(self):
self.__blocks = []
self.__rects = []
for y in range( 30 ):
for x in range( 61 ):
self.__blocks.append(pygame.Surface((20, 20)).convert())
self.__rects.append( self.__blocks[-1].get_rect() )
self.__rects[-1].topleft = ( x, y )
self.__color = False
self.testBlock = 10
This gives you a simple test:
def collided(self, pos):
hit = False
for i in range( len( self.__rects ) ):
if ( self.__rects[i].collidepoint( pos[0], pos[1] ) ):
print( "Click on block %d" % ( i ) )
hit = True
break
return hit, i
.get_rect() gives rect with block's size but with position (0, 0)
you have real position in __positions and you would need
.get_rect(topleft=self.__positions[self.testBlock])
def test(self, pos):
block = self.__blocks[self.testBlock].get_rect(topleft=self.__positions[self.testBlock])
if block.collidepoint(pos[0], pos[1]):
print(block.x, block.y)
But it would be better to get rect and set its position at start and later not use get_rect().
You could also create class Pixel similar to class Sprite with self.image to keep surface and self.rect to keep its size and position. And then you could use Group to check collision with all pixels.
EDIT:
Example which uses class pygame.sprite.Sprite to create class Pixel and it keeps all pixels in pygame.sprite.Group
It also handle events (MOUSEBUTTONDOWN) to change color in any pixel when it is clicked.
import pygame
# --- classes ---
class Pixel(pygame.sprite.Sprite):
def __init__(self, x, y, color, width=20, height=20):
super().__init__()
self.color_original = color
self.color = color
self.image = pygame.Surface((20, 20)).convert()
self.image.fill(self.color)
self.rect = pygame.Rect(x, y, width, height)
def handle_event(self, event):
if event.type == pygame.MOUSEBUTTONDOWN:
if self.rect.collidepoint(event.pos):
if self.color != self.color_original:
self.color = self.color_original
else:
self.color = (255,0,0)
self.image.fill(self.color)
# event handled
return True
# event not handled
return False
class Canvas:
def __init__(self):
# create group for sprites
self.__blocks = pygame.sprite.Group()
# create sprites
self.__color = False
for y in range(30):
y *= 20
for x in range(61):
x *= 20
self.__color = not self.__color
if self.__color:
color = (200, 200, 200)
else:
color = (50, 50, 50)
self.__blocks.add(Pixel(x, y, color))
# changing the color for testing
self.testBlock = 10
all_sprites = self.__blocks.sprites()
block = all_sprites[self.testBlock]
block.image.fill((255, 0, 0))
def draw(self, window):
# draw all sprites in group
self.__blocks.draw(window)
def test(self, pos):
# test collision with one sprite
all_sprites = self.__blocks.sprites()
block = all_sprites[self.testBlock]
if block.rect.collidepoint(pos):
print(block.rect.x, block.rect.y)
def handle_event(self, event):
for item in self.__blocks:
if item.handle_event(event):
# don't check other pixels if event already handled
return True
# --- main ---
pygame.init()
win = pygame.display
D = win.set_mode((1220, 600))
canvas = Canvas()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
exit()
canvas.handle_event(event)
#mousepos = pygame.mouse.get_pos()
#canvas.test(mousepos)
# draws (without updates, etc)
#D.fill((0, 0, 0)) # no need clean screen if it will draw all elements again
canvas.draw(D)
win.flip()

Trouble with rendering background boxes class from a 2d list in pygame

I am trying to make a background of boxes for a simple snake game by iterating through a 2d array and drawing boxes which I've stored as instances of a class BackgroundCube in each part of the array. When I run the program there are no errors, but nothing shows up on the pygame screen.
I've printed the length of each sublist which shows a length of 20, my desired grid size. I've also just printed the entire array which shows what I believe to be instances of the class, something like this: <main.BackgroundCube object at 0x11186e090> would be one entry in the list. So I believe the problem lies in how I'm drawing the rectangles.
python
WIDTH = 400
HEIGHT = 420
screen = pygame.display.set_mode((WIDTH, HEIGHT))
class BackgroundCube:
def __init__(self, x, y, width, height, color):
self.x = x
self.y = y
self.width = width
self.height = height
self.color = color
def draw(self, screen):
pygame.draw.rect(screen, self.color, (self.x, self.y, self.width, self.height), 2)
def redrawGameWindow():
for x in range(20):
for y in range(20):
cube2 = background_cube_list[x][y]
cube2.draw(screen)
run = True
background_cube_list = [[0 for x in range(int(WIDTH/20))] for x in range(int((HEIGHT-20)/20))]
while run:
for cube in range(int(WIDTH / 20)):
for cube1 in range(int((HEIGHT - 20) / 20)):
background_cube_list[cube][cube1] = BackgroundCube(cube * 20, cube1 * 20, 20, 20, (144, 144, 144))
clock.tick(30)
redrawGameWindow()
Again, no errors, just a blank white window. Thank you.
You forgot to add
pygame.display.update()
in your main loop. Add it just after redrawGameWindow().
You also need to define clock, which I guess is clock = pygame.time.Clock(). Add it before the main loop.

All rectangles in one iteration of a game loop drawing the same colour despite explicit colour arguments

I'm writing a simple toy in Pygame. When you press a key on the home row, it does a little burst of particles.
class Particle():
x = 0
y = 0
size = 0
colour = (255, 255, 255)
rect = None
def __init__(self, x, y, size, colour):
self.x = x
self.y = y
self.size = size
self.colour = colour # Particle has its own colour
self.rect = pygame.Rect(self.x, self.y, self.size, self.size)
class Burst():
x = 0
y = 0
colour = (255, 255, 255)
count = 0
sound = None
particles = []
def __init__(self, x, y, colour, count, sound):
self.x = x
self.y = y
self.colour = colour # Burst has its own colour, too - all its particles should have the same colour as it
self.count = count
self.sound = sound
self.particles.append(Particle(self.x, self.y, 5, self.colour))
def update(self):
self.particles.append(Particle(random.randint(1, 30) + self.x, random.randint(1, 30) + self.y, 5, self.colour))
def draw(self):
global screen
for p in self.particles:
pygame.draw.rect(screen, p.colour, p.rect) # This draws the particles with the correct colours
#pygame.draw.rect(screen, self.colour, (60, 60, 120, 120), 4) # This draws the particles all the same colour
#screen.fill(p.colour, p.rect) # This draws the particles all the same colour
The line you're looking for is in Burst.draw. For some reason, only the uncommented one works correctly. The other two lines, which should be the same as far as I can tell, only draw the first burst's particles correctly. Any subsequent bursts change all particles onscreen to match their colour.
I can provide more code, but there's not much more to it. Basically keypresses add Bursts to an array, and every tick I step through that array calling update() and draw().
Does anyone know what I did wrong, and then accidentally fixed?
Because all particles in the screen belong to the same collection Burst.particles.
And every time you process a Burst you are processing all the particles, and all gets painted with the last colour.
Just move the initialization particles = [] to the init method.
def __init__(self, x, y, colour, count, sound):
...
self.particles = []
self.particles.append(Particle(self.x, self.y, 5, self.colour))
Update
You are using a Java/C# style of coding classes. You shouldn't put any of the initializations at the class level, unless they are constants or class attributes.
IE:
class Burst():
class_attribute = 0 # declaration of class (static) attribute
def __init__(self, ...):
self.attribute = 0 # declaration of object (regular) attribute
You shouldn't make class declarations of attribute you will use a object attributes.
Just remove all the declarations previous to the init method in both classes.

Categories

Resources