Mask acts as it if it was a solid box - python

How can I make a mask that's alpha pixels don't collide?
I've tried everything, following tutorials, viewing other stackoverthrow questions/answers, nothing works. PLEASE SPECIFY THE PROBLEM AND THE SOLUTION.
Here's my code:
level = pygame.image.load(r'data\test lvl.png').convert_alpha()
rect = level.get_rect(center = (400,400))
levelmask = pygame.mask.from_surface(image)
This mask is just a solid box, drawing it yields a solid box. Here's the collision between the level and the player.
offset = (X2, Y2)
collide = levelmask.overlap(self.rect_mask, offset)
print(offset)
print(collide)
if collide == None:
collide = -1
else:
collide = 0
return collide
And here's the code for the initialization of self.rect_mask.
self.rect_mask = pygame.mask.Mask((75, 160))
self.rect_mask.fill()

There seems to be nothing wrong with your code. I have incorporated elements of it into an example below.
So where could the issue be:
The "level" image has weird transparency (probably not, see comment above)
The levelmask used in the overlap() test is somehow different to the one created in the OP's example.
The offset is wrong somehow.
The 75 x 160 filled comparison mask is always too big to be "inside" the levelmask.
The example below uses the exact operations presented in the OP's example code. It works. A mask is created for the maze, where the non-wall parts are transparent. Another mask is created for the moving object (alien_*), also based on transparency.
Demo:
Code:
import pygame
# Window size
WINDOW_WIDTH = 612
WINDOW_HEIGHT = 612
FPS = 60
# background colours
INKY_BLACK = ( 0, 0, 0 )
FIREY_RED = ( 203, 49, 7 )
class Coordinate:
def __init__( self, x, y=None ):
if ( type(x) is tuple ):
self.x = x[0]
self.y = x[1] # Pygame.Rect corner
else:
self.x = x
self.y = y
### MAIN
pygame.init()
pygame.font.init()
SURFACE = pygame.HWSURFACE|pygame.DOUBLEBUF
window = pygame.display.set_mode( ( WINDOW_WIDTH, WINDOW_HEIGHT ), SURFACE )
pygame.display.set_caption("Mask Example")
# Make some bitmaps with masks for collision
maze_image = pygame.image.load( "square_maze_10x10.png" ).convert_alpha()
maze_rect = maze_image.get_rect()
maze_mask = pygame.mask.from_surface( maze_image )
alien_image = pygame.image.load( "green_guy.png" ).convert_alpha()
alien_rect = alien_image.get_rect()
alien_rect.topleft = ( 20, 20 )
alien_mask = pygame.mask.from_surface( alien_image )
clock = pygame.time.Clock()
done = False
while not done:
# Handle user-input
for event in pygame.event.get():
if ( event.type == pygame.QUIT ):
done = True
# Handle continuous-keypresses
keys = pygame.key.get_pressed()
delta = Coordinate( 0,0 )
if ( keys[pygame.K_UP] ):
delta.y = -1
elif ( keys[pygame.K_DOWN] ):
delta.y = 1
elif ( keys[pygame.K_LEFT] ):
delta.x = -1
elif ( keys[pygame.K_RIGHT] ):
delta.x = 1
# move according to keys
alien_rect.x += delta.x
alien_rect.y += delta.y
# has the alien hit the walls use a Mask Check?
background = INKY_BLACK
if ( None != maze_mask.overlap( alien_mask, alien_rect.topleft ) ): # <<-- Mask check here
background = FIREY_RED
# Repaint the screen
window.fill( background )
window.blit( maze_image, maze_rect )
window.blit( alien_image, alien_rect )
pygame.display.flip()
clock.tick_busy_loop( FPS )
pygame.quit()
Resources:

Related

Assigning Variables to newly added Sprites

I'm trying to let the user press and hold the right mouse button to make squares, then move them around with the mouse by left clicking. I was able to make a sprite that could be moved around with left click and to make new sprites when the user right clicks:
while True:
for event in pg.event.get():
if event.type == pg.QUIT:
pg.quit()
elif event.type == pg.KEYDOWN:
if event.key == pg.K_ESCAPE:
quit()
elif event.type == pg.MOUSEBUTTONDOWN:
if event.button == 1:
if x <= m_x <= x + tilesize_w and y <= m_y <= y + tilesize_h:
m_xrom = m_x - x
m_yrom = m_y - y
down = True
elif event.button == 3:
m_xrom = m_x
m_yrom = m_y
elif event.type == pg.MOUSEBUTTONUP:
if event.button == 1:
down = False
elif event.button == 3:
tiles.add(Player((m_xrom,m_yrom), 876567, abs(m_x - m_xrom), abs(m_y - m_yrom))
m_x,m_y = pg.mouse.get_pos()
if down:
x = m_x - m_xrom
y = m_y - m_yrom
color = 279348
lvl = Level((x,y), color, tilesize_w, tilesize_h)
lvl.run()
However I don't know how to the move the sprites that the user makes. I would like to be able to make several different user-made sprite that can move independently.
This is a somewhat simple process once the code "saves" the newly drawn boxes somehow. I think a good way to do this is to create a PyGame sprite object for each new box, and keep them in a sprite Group.
At the end of a "mouse drag" operation, we have a start-position - the mouse-xy of the MOUSEBUTTONDOWN event, and the end-position is the mouse-xy of the MOUSEBUTTONUP event. With a bit of fiddling to handle negative-ranges, we calculate a PyGame rectangle object (Rect), using it as the basis for a BoxSprite:
class BoxSprite( pygame.sprite.Sprite ):
""" Rectangular, coloured box-sprite """
def __init__( self, drag_rect ):
super().__init__()
self.image = pygame.Surface( ( drag_rect.width, drag_rect.height ) )
self.rect = drag_rect.copy()
self.image.fill( GREEN )
Looking at the BoxSprite, it's really not much more than a coloured Surface, sized and positioned over the original drag_rect.
Once we have a Sprite to hold the box, PyGame has the super-useful Sprite Group class. This can be used to hold all the BoxSprites, and provides easy drawing, membership and collision functions.
So now the code can "remember" every box, how can we determine if the user clicked on one? An easy way to do this is to test if the click mouse-position is inside any of the group members. A simple approach would be to iterate through every sprite in the group, checking each sprite's Rect. Or if we can somehow treat the mouse-click as a sprite-object, we can use the existing Sprite Group's "Sprite Vs Sprite-Group" collision-test function. This operation returns a handy list of collided sprites from the group. Nice.
Once you have this list, how you treat them is up to you. In the example below I implemented a drag-existing sprite, so clicking on an existing Box "grabs" it until the mouse button is released.
import pygame
# window sizing
WIDTH = 500
HEIGHT = 500
MAX_FPS = 60
BLACK = ( 0, 0, 0)
RED = (255, 0, 0)
GREEN = ( 20, 200, 20)
YELLOW= ( 255,255, 0)
pygame.init()
window = pygame.display.set_mode( ( WIDTH, HEIGHT ) )
pygame.display.set_caption( "Box Dragging Demo" )
###
### This is a sprite we use to hold each rectangular box the User creates.
### Once the user completes an on-screen draw operation, the box gets stored in
### an instance of a BoxSprite
###
class BoxSprite( pygame.sprite.Sprite ):
""" Rectangular, coloured box-sprite """
def __init__( self, drag_rect ):
super().__init__()
self.image = pygame.Surface( ( drag_rect.width, drag_rect.height ) )
self.rect = drag_rect.copy()
self.image.fill( GREEN )
def moveBy( self, dx, dy ):
""" Reposition the sprite """
self.rect.x += dx
self.rect.y += dy
def setColour( self, c ):
self.image.fill( c )
###
### Sprite Groups have no function to test collision against a point
### So this object holds a position as a "dummy" 1x1 sprite, which
### can be used efficiently in collision functions.
###
class CollidePoint( pygame.sprite.Sprite ):
""" Simple single-point Sprite for easy collisions """
def __init__( self, point=(-1,-1) ):
super().__init__()
self.image = None # this never gets drawn
self.rect = pygame.Rect( point, (1,1) )
def moveTo( self, point ):
""" Reposition the point """
self.rect.topleft = point
# Sprite Group to hold all the user-created sprites
user_boxes_group = pygame.sprite.Group() # initially empty
rect_start = ( -1, -1 ) # start position of drawn rectangle
drag_start = ( -1, -1 ) # start position of a mouse-drag
mouse_click = CollidePoint() # create a sprite around the mouse-click for easier collisions
clicked_boxes = [] # the list of sprites being clicked and/or dragged
clock = pygame.time.Clock() # used to govern the max MAX_FPS
### MAIN
exiting = False
while not exiting:
# Handle events
mouse_pos = pygame.mouse.get_pos()
for event in pygame.event.get():
if ( event.type == pygame.QUIT ):
exiting = True
elif ( event.type == pygame.MOUSEBUTTONDOWN ):
# MouseButton-down signals beginning a drag or a draw
# Did the user click on an existing sprite?
mouse_click.moveTo( mouse_pos )
clicked_boxes = pygame.sprite.spritecollide( mouse_click, user_boxes_group, False ) # get the list of boxes clicked (if any)
# was anything clicked?
if ( len( clicked_boxes ) > 0 ):
drag_start = mouse_pos # yes, begin a drag operation
for box in clicked_boxes:
box.setColour( YELLOW )
else:
# Nothing clicked, not already drawing, start new rectangle-draw
rect_start = pygame.mouse.get_pos()
elif ( event.type == pygame.MOUSEBUTTONUP ):
# MouseButton-up signals both drag is complete, and draw is complete
# Was the user dragging any sprites?
if ( len( clicked_boxes ) > 0 ):
# drag is over, drop anything we were dragging
for box in clicked_boxes:
box.setColour( GREEN )
drag_start = ( -1, -1 )
clicked_boxes = []
# Was the user drawing a rectangle?
elif ( rect_start != ( -1, -1 ) ):
# Rects are always defined from a top-left point
# So swap the points if the user dragged up
if ( rect_start > mouse_pos ):
swapper = ( mouse_pos[0], mouse_pos[1] )
mouse_pos = rect_start
rect_start = swapper
# create a new sprite from the drag
new_width = abs( mouse_pos[0] - rect_start[0] )
new_height = abs( mouse_pos[1] - rect_start[1] )
if ( new_width > 0 and new_height > 0 ):
new_sprite = BoxSprite( pygame.Rect( rect_start, ( new_width, new_height ) ) )
user_boxes_group.add( new_sprite )
rect_start = ( -1, -1 ) # done drawing
elif ( event.type == pygame.MOUSEMOTION ):
# The mouse is moving, so move anything we're dragging along with it
# Note that we're moving the boxes bit-by-bit, not a start->final move
if ( len( clicked_boxes ) > 0 ):
x_delta = mouse_pos[0] - drag_start[0]
y_delta = mouse_pos[1] - drag_start[1]
for box in clicked_boxes:
box.moveBy( x_delta, y_delta )
drag_start = mouse_pos
# Handle keys
keys = pygame.key.get_pressed()
if ( keys[pygame.K_ESCAPE] ):
exiting = True
# paint the window
window.fill( BLACK ) # paint background
user_boxes_group.draw( window ) # paint the sprites
# If we're already dragging a box, paint a target-rectangle
if ( rect_start != ( -1, -1 ) ):
# Use a lines instead of a Rect, so we don't have to handle width/height co-ordinate position issues
box = [ rect_start, ( mouse_pos[0], rect_start[1] ), mouse_pos, ( rect_start[0], mouse_pos[1] ) ]
pygame.draw.lines( window, RED, True, box, 1 )
pygame.display.flip()
clock.tick( MAX_FPS )
pygame.quit()

Is there a way to use for loops to make a game map in pygame?

So I want to make a gamemap in my pygame using for loops, but apparently the way I am doing it does not work. The code is below and if you can take a look at it, that would be nice!
Pygame
import pygame
import sys
import random
import subprocess
# _______initiate Game______________ #
class Game:
pygame.init()
width = 800
height = 800
screen = pygame.display.set_mode((width, height))
pygame.display.set_caption("Maze Game")
pygame.draw.circle(screen, (0, 0, 255), (250, 250), 75)
white = [255, 255, 255]
black = [0, 0, 0]
lblue = [159, 210, 255]
background = input(
"What color background would you like? (White, Black, or Light Blue): ")
if background == "White":
screen.fill(white)
pygame.display.flip()
elif background == "Black":
screen.fill(black)
pygame.display.update()
elif background == "Light Blue":
screen.fill(lblue)
pygame.display.update()
else:
screen.fill(black)
pygame.display.update()
for "." in "map1.txt":
pygame.image.load("winter.Wall.png")
# ___________________TO RUN___________________________ #
run = True
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
if event.type == pygame.KEYDOWN:
command = "python JakeGame.py"
subprocess.call(command)
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
if event.type == pygame.K_ESCAPE:
pygame.quit()
# _______________________________________________ #
pygame.quit()
This is the map txt file and the error I am getting
...1...........................................
...11111111111111..............................
1111............11111111111111.................
1............................111...............
111............................................
1..............................................
111111111111111111111111111....................
..................1............................
1111111111111111111............................
..................1............................
..................11111111.....................
..........111111111......111111111.............
.................................1.............
So basically, what I am trying to do is convert all those periods to be loaded with an image called winterWall.png .
The error I am getting is "can't assign literal" Thank you guys for helping :p
It's a bit more complicated than just what you propose (which is invalid syntax).
The code needs to open the file, then iterate over the rows and columns of map data. Then, for each type of letter it finds, render some kind of tile-based representation - here I've just used red and green blocks. An alternate version could just as easily render bitmaps.
The first thing you need to do is open the file and read the text into a list of strings - one for each line:
map_data = open( map_filename, "rt" ).readlines()
One caveat to this method, is that each line still has its end-of-line on the end. And it would be better to handle errors (like the file being missing), rather than crashing.
Then we create a Surface, which is just an off-screen image. It needs to be as big as the map is, times the size of the tiles. Since this is created inside the TileMap class, this gets stored in TileMap.image (i.e: self.image).
Next the code iterates through each line, and then each letter drawing a rectangle. The code just uses the constant TILE_SIZE for this. You need to decide if this is going to work with coloured squares, or bitmaps, etc. - but obviously all your tiles need to use the same size.
# iterate over the map data, drawing the tiles to the surface
x_cursor = 0 # tile position
y_cursor = 0
for map_line in map_data: # for each row of tiles
x_cursor = 0
for map_symbol in map_line: # for each tile in the row
tile_rect = pygame.Rect( x_cursor, y_cursor, TileMap.TILE_SIZE, TileMap.TILE_SIZE )
if ( map_symbol == '.' ):
pygame.draw.rect( self.image, RED, tile_rect )
elif ( map_symbol == '1' ):
pygame.draw.rect( self.image, GREEN, tile_rect )
else:
pass # ignore \n etc.
x_cursor += TileMap.TILE_SIZE
y_cursor += TileMap.TILE_SIZE
Note that we make an x_cursor and y_cursor. This is the top-left corner of the map-tile about to be drawn. As we step through each character, we update this to the next position on the map.
At each point, the map has a TILE_SIZE by TILE_SIZE (16x16) "cell" into which we draw a coloured square, depending on the type of map item.
At the end of the operation, we have loaded in all the map tiles, and draw the map to an off-screen Surface (self.image), which can be draw to the screen quickly and easily.
The TileMap class, simply wraps all this together.
Reference Code:
import pygame
WINDOW_WIDTH = 800
WINDOW_HEIGHT= 600
SURFACE = pygame.HWSURFACE|pygame.DOUBLEBUF|pygame.RESIZABLE
#define colours
BLACK = (0,0,0)
RED = ( 200, 0, 0 )
GREEN = (0,255,0)
### Class to render map-data to a surface image
class TileMap:
TILE_SIZE=16 # size of map elements
def __init__( self, map_filename ):
""" Load in the map data, generating a tiled-image """
map_data = open( map_filename, "rt" ).readlines() # Load in map data TODO: handle errors
map_width = len( map_data[0] ) - 1
map_length = len( map_data )
# Create an image to hold all the map tiles
self.image = pygame.Surface( ( map_width * TileMap.TILE_SIZE, map_length * TileMap.TILE_SIZE ) )
self.rect = self.image.get_rect()
# iterate over the map data, drawing the tiles to the surface
x_cursor = 0 # tile position
y_cursor = 0
for map_line in map_data: # for each row of tiles
x_cursor = 0
for map_symbol in map_line: # for each tile in the row
tile_rect = pygame.Rect( x_cursor, y_cursor, TileMap.TILE_SIZE, TileMap.TILE_SIZE )
if ( map_symbol == '.' ):
pygame.draw.rect( self.image, RED, tile_rect )
elif ( map_symbol == '1' ):
pygame.draw.rect( self.image, GREEN, tile_rect )
else:
pass # ignore \n etc.
x_cursor += TileMap.TILE_SIZE
y_cursor += TileMap.TILE_SIZE
def draw( self, surface, position=(0,0) ):
""" Draw the map onto the given surface """
self.rect.topleft = position
surface.blit( self.image, self.rect )
### initialisation
pygame.init()
pygame.mixer.init()
window = pygame.display.set_mode( ( WINDOW_WIDTH, WINDOW_HEIGHT ), SURFACE )
pygame.display.set_caption("Render Tile Map")
### Load the map
tile_map = TileMap( "map1.txt" )
### Main Loop
clock = pygame.time.Clock()
done = False
while not done:
# Handle user-input
for event in pygame.event.get():
if ( event.type == pygame.QUIT ):
done = True
# Update the window, but not more than 60fps
window.fill( BLACK )
# Draw the map to the window
tile_map.draw( window )
pygame.display.flip()
clock.tick_busy_loop(60)
pygame.quit()

Can you repeatedly print an image for the entire length of a rect, in pygame?

Basically what the title says. I'm trying to create a 2d platformer so I can get more into game developement, and I need platforms that have an image. So Say you draw a rect (an actual rectangle for instance, 30 high, 120 along), could you fill that rectangle with a tile who's original size is 30 by 30? E.g a 30x120 rect, filled with 4, 30x30 tile sprites, from start to finish of the rectangle. If so, any tips on how I would go about this? Thanks.
I've heard of creating lists for making a platform game but for some that method confuses me and, this way seems a lot more straight forward.
Again, any help would be appreciated. Much Thanks. Anthony.
This is a reasonably straightforward operation.
First load in the image you want to tile:
tile_image = pygame.image.load( 'my_tile.png' ).convert_alpha()
Then create a new PyGame Surface of the required size:
tiled_image = pygame.Surface( ( width, height ) )
Then using two loops, step down & across the surface, "stamping" a copy of the image. After each stamp, calculate the location of the next placement. Continue stamping across the X-horizontals, and down the Y-vertical until all pixels are covered:
x_cursor = 0
y_cursor = 0
while ( y_cursor < height ):
while ( x_cursor < width ):
tiled_image.blit( tile, ( x_cursor, y_cursor ) )
x_cursor += tile.get_width()
y_cursor += tile.get_height()
x_cursor = 0
Now wrap all that into a handy-to-use function:
def makeTiledImage( image, width, height ):
x_cursor = 0
y_cursor = 0
tiled_image = pygame.Surface( ( width, height ) )
while ( y_cursor < height ):
while ( x_cursor < width ):
tiled_image.blit( image, ( x_cursor, y_cursor ) )
x_cursor += image.get_width()
y_cursor += image.get_height()
x_cursor = 0
return tiled_image
Which can be called:
tile_image = pygame.image.load( 'my_tile.png' ).convert_alpha()
platform = makeTiledImage( tile_image, 250, 30 )
Reference Code:
import pygame
# Window size
WINDOW_WIDTH = 400
WINDOW_HEIGHT = 400
WINDOW_FPS = 60
WINDOW_SURFACE = pygame.HWSURFACE|pygame.DOUBLEBUF|pygame.RESIZABLE
DARK_BLUE = ( 3, 5, 54)
def makeTiledImage( image, width, height ):
""" Given an image, make another image tiled with the given image """
x_cursor = 0
y_cursor = 0
tiled_image = pygame.Surface( ( width, height ) )
while ( y_cursor < height ):
while ( x_cursor < width ):
tiled_image.blit( image, ( x_cursor, y_cursor ) )
x_cursor += image.get_width()
y_cursor += image.get_height()
x_cursor = 0
return tiled_image
### initialisation
pygame.init()
pygame.mixer.init()
window = pygame.display.set_mode( ( WINDOW_WIDTH, WINDOW_HEIGHT ), WINDOW_SURFACE )
pygame.display.set_caption("Tiled Rect")
tile_image = pygame.image.load( 'rot_offcentre_1.png' ).convert_alpha()
platform = makeTiledImage( tile_image, 250, 30 )
### Main Loop
clock = pygame.time.Clock()
done = False
while not done:
# Handle user-input
for event in pygame.event.get():
if ( event.type == pygame.QUIT ):
done = True
elif ( event.type == pygame.MOUSEBUTTONUP ):
# On mouse-click
pass
# Update the window, but not more than 60fps
window.fill( DARK_BLUE )
window.blit( platform, ( 50, 150 ) )
pygame.display.flip()
# Clamp FPS
clock.tick_busy_loop( WINDOW_FPS )
pygame.quit()
Yea, you'd usally import an atlas, or spritesheet.
https://en.wikipedia.org/wiki/Texture_atlas
Then chop it into subsurfaces
http://sheep.art.pl/Tiled%20Map%20in%20PyGame
Specify the locations, and properties where to place them onscreen, and do so.
It's easier for me to imagine it in Lua
https://love2d.org/wiki/love.graphics.newQuad
But it's a similar process in Python
https://github.com/doyousketch2/KivyAtlasGenerator
https://github.com/doyousketch2/KivyTileMapper

For a pygame.Surface element, is there a way I use .fill() to change a specific colour?

I'm making a game and have a fish sprite with alpha colour pink, I want to change the colour pink on to something else, so fo instance here I tried to change it to orange but instead it made it red because I think it's blending somehow? Is there a way I can fill in just the alpha pixels or specify a colour to change?
Thanks, all the best
self.image = pygame.image.load(os.path.join(game_folder,"boxfish.png")).convert()
self.image.set_colorkey(Colour.pink)
self.rect = self.image.get_rect()
self.image.fill(Colour.orange,special_flags = 3)
A very quick way of doing it is just to use transform.threshold.
Or you can use this function
def change_color(img, oldcolor, newcolor):
for x in range(img.get_width()):
for y in range(img.get_height()):
pixel_color = img.get_at((x, y)) # Preserve the alpha value.
if oldcolor == pixel_color:
img.set_at((x, y), newcolor) # Set the color of the pixel.
Since the entire sprite "colour" is filled with pink, a neat way to solve this issue is to make the sprites main colour transparent, and then overlay it onto a same-sized rectangle of the desired hue.
First make a fish with a transparent background (I used TheGIMP to edit it).
clear_fish.png
Then in your sprite class (or however you want to implement it), create a pygame.Surface the same size, filling it with the desired colour. Then blit() the clear-fish image on top of the coloured surface.
fish_image = pygame.image.load( 'clear_fish.png' ) # un-coloured fish
fish_rect = fish_image.get_rect()
# Apply some coloured scales to the fish
fish_scales= pygame.Surface( ( fish_rect.width, fish_rect.height ) )
fish_scales.fill( colour )
fish_scales.blit( fish_image, (0,0) )
fish_image = fish_scales # use the coloured fish
Ref: Full Example Code ~
import pygame
import random
# Window size
WINDOW_WIDTH = 600
WINDOW_HEIGHT = 600
WINDOW_SURFACE = pygame.HWSURFACE|pygame.DOUBLEBUF|pygame.RESIZABLE
DARK_BLUE = ( 3, 5, 54)
### initialisation
pygame.init()
pygame.mixer.init()
window = pygame.display.set_mode( ( WINDOW_WIDTH, WINDOW_HEIGHT ), WINDOW_SURFACE )
pygame.display.set_caption("1-Fish, 2-Fish, Pink Fish, Clear Fish")
class FishSprite( pygame.sprite.Sprite ):
def __init__( self, colour ):
pygame.sprite.Sprite.__init__( self )
self.image = pygame.image.load( 'clear_fish.png' )
self.rect = self.image.get_rect()
# Apply some coloured scales to the fish
fish_scales= pygame.Surface( ( self.rect.width, self.rect.height ) )
fish_scales.fill( colour )
fish_scales.blit( self.image, (0,0) )
self.image = fish_scales # use the coloured fish
# Start position is randomly across the screen
self.rect.center = ( random.randint(0, WINDOW_WIDTH), random.randint(0,WINDOW_HEIGHT) )
def update(self):
# Just move a bit
self.rect.x += random.randrange( -1, 2 )
self.rect.y += random.randrange( -1, 2 )
### Sprites
fish_sprites = pygame.sprite.Group()
fish_sprites.add( FishSprite( ( 255, 200, 20 ) ) ) # add some fish
fish_sprites.add( FishSprite( ( 255, 20, 200 ) ) )
fish_sprites.add( FishSprite( ( 55, 00, 200 ) ) )
fish_sprites.add( FishSprite( ( 20, 200, 20 ) ) )
### Main Loop
clock = pygame.time.Clock()
done = False
while not done:
# Handle user-input
for event in pygame.event.get():
if ( event.type == pygame.QUIT ):
done = True
elif ( event.type == pygame.MOUSEBUTTONUP ):
# On mouse-click add a fish
mouse_pos = pygame.mouse.get_pos()
random_red = random.randint( 50, 250 )
random_green = random.randint( 50, 250 )
random_blue = random.randint( 50, 250 )
random_colour = ( random_red, random_green, random_blue )
fish_sprites.add( FishSprite( random_colour ) )
# Update the window, but not more than 60fps
fish_sprites.update()
window.fill( DARK_BLUE )
fish_sprites.draw( window )
pygame.display.flip()
# Clamp FPS
clock.tick_busy_loop(60)
pygame.quit()

Why is my program became really laggy after I added rotation, and how do I fix this?

I'm making a game using pygame, and I have an asteroid class. When I add rotation to the update method and I run the program, the asteroids move really slow and laggy, and even their images look worse then before.
I am not sure how to fix this and why this is happening. Here is the class:
class enemy(pygame.sprite.Sprite):
def __init__(self, x, y, width, height):
pygame.sprite.Sprite.__init__(self)
self.width = width
self.height = height
self.speedx = random.randrange(-3,3)
self.speedy = random.randrange(5,15)
self.image = random.choice(meteor_image)
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
self.rotation = 0
self.rotation_speed = random.randrange(-8,8)
self.last_update = pygame.time.get_ticks()
def draw(self,win):
win.blit(self.image,(self.rect.x,self.rect.y))
def rotate(self):
time_now = pygame.time.get_ticks()
if time_now - self.last_update > 50:
self.last_update = time_now
self.rotation = (self.rotation + self.rotation_speed) % 360
new_meteor_image = pygame.transform.rotate(self.image, self.rotation)
old_center = self.rect.center
self.image = new_meteor_image
self.rect = self.image.get_rect()
self.rect.center = old_center
def update(self):
self.rotate()
self.rect.y += self.speedy
self.rect.x += self.speedx
Before I added the rotate function and added "self.roatate()" to the update function, it was good, after that, it is all really laggy. How to fix that?
You're taking the original, rotating it, then rotating the rotated image. Don't do that. The rotation process loses information, so you want to rotate from the original, unmodified version each time.
Rotation is also a heavy operation. I suggest creating a cache to store the rotated images, building that at start, then just pulling from that cache when you need to display.
Bitmap rotation is a reasonably computationally heavy operation. Your code is slowing down because it's rotating the image every update, performing this huge bunch of maths every time, for every sprite.
It's possible (and convenient) to pre-rotate the bitmap in your sprite constructor, and simply put the resultant images into a cache. Then instead of performing the rotation calculations, the code need only determine which of the cached images to assign to sprite.image.
One of the issues with this approach, is that the programmer must decide how many pre-generated rotations to construct. In the example below I used integer angles to set rotation, so this forces a theoretical upper-limit of 360 frames. I can imagine in a vector-like game, a programmer may desire sub-degree rotation, but that's another answer. If you look at historical rotated-bitmap games, generally only a few angles were used, maybe 8 steps (360 / 8 → 45°). Anyway, my example uses 15° angles, giving 24 steps, this seems like a lot! If you are working in an embedded space, or using large bitmaps, the memory used may become a consideration. Obviously if you have many sprites that are the same, they should ideally share the cached images. This is not how this example works.
This example code also does bitmap-mask based collisions (as opposed to simple rectangle collisions), so the bitmap-masks needs to be rotated too.
import pygame
import random
# Window size
WINDOW_WIDTH = 400
WINDOW_HEIGHT = 400
FPS = 60
# background colours
INKY_BLACK = ( 0, 0, 0)
class MovingSprite( pygame.sprite.Sprite ):
ANGLE_STEP = 15 # degrees, makes 360/ANGLE_STEP frames
def __init__( self, bitmap ):
pygame.sprite.Sprite.__init__( self )
self.rect = bitmap.get_rect()
self.rect.center = ( random.randrange( 0, WINDOW_WIDTH ), random.randrange( 0, WINDOW_HEIGHT ) )
self.crashing = False
# start with zero rotation
self.rotation = 0
self.rotations = [ bitmap ]
self.masks = [ pygame.mask.from_surface( bitmap ) ]
self.angle_slots = 360 // self.ANGLE_STEP
# pre-compute all the rotated images, and bitmap collision masks
for i in range( 1, self.angle_slots ):
rotated_image = pygame.transform.rotate( bitmap, self.ANGLE_STEP * i )
self.rotations.append( rotated_image )
self.masks.append( pygame.mask.from_surface( rotated_image ) )
self._setRotationImage( 0 ) # sets initial image, mask & rect
def rotateTo( self, angle ):
# If the given angle is not an exact point we have, round to nearest
if ( angle % self.ANGLE_STEP != 0 ):
angle = round( angle / self.ANGLE_STEP ) * self.ANGLE_STEP
rot_index = ( angle // self.ANGLE_STEP )
# set the pre-rotated image
self._setRotationImage( rot_index )
def rotateRight( self ):
if ( self.rotation == 0 ):
self._setRotationImage( self.angle_slots - 1 )
else:
self._setRotationImage( self.rotation - 1 )
def rotateLeft( self ):
if ( self.rotation == self.angle_slots - 1 ):
self._setRotationImage( 0 )
else:
self._setRotationImage( self.rotation + 1 )
def _setRotationImage( self, rot_index ):
rot_index %= self.angle_slots
self.rotation = rot_index
# Select the pre-rotated image & mash
self.image = self.rotations[ rot_index ]
self.mask = self.masks[ rot_index ]
# We need to preserve the centre-poisiton of the bitmap,
# as rotated bitmaps will (probably) not be the same size as the original
centerx = self.rect.centerx
centery = self.rect.centery
self.rect = self.image.get_rect()
self.rect.center = ( centerx, centery )
def newPosition( self ):
# Wander Around
if ( not self.crashing ):
self.rect.x += random.randrange( -2, 3 )
self.rect.y += random.randrange( -2, 3 )
else:
self.rect.y += 3
def crash( self ):
self.crashing = True
def update( self ):
self.newPosition()
if ( self.rect.y > WINDOW_HEIGHT ):
self.kill()
elif ( self.crashing == True ):
# rotate as we fall
self.rotateRight()
### MAIN
pygame.init()
pygame.font.init()
SURFACE = pygame.HWSURFACE|pygame.DOUBLEBUF|pygame.RESIZABLE
WINDOW = pygame.display.set_mode( ( WINDOW_WIDTH, WINDOW_HEIGHT ), SURFACE )
pygame.display.set_caption("Sprite Rotation Example")
# Load resource images
sprite_image = pygame.image.load( "tiny_alien_space.png" )#.convert_alpha()
# Make some sprites from game-mode
SPRITES = pygame.sprite.Group() # a group, for a single sprite
for i in range( 50 ):
SPRITES.add( MovingSprite( sprite_image ) )
clock = pygame.time.Clock()
done = False
while not done:
# Handle user-input
for event in pygame.event.get():
if ( event.type == pygame.QUIT ):
done = True
elif ( event.type == pygame.KEYDOWN ):
if ( event.unicode == '+' or event.scancode == pygame.K_PLUS ):
# Pressing '+' adds a new sprite
SPRITES.add( MovingSprite( sprite_image ) )
# Handle continuous-keypresses, but only in playing mode
keys = pygame.key.get_pressed()
if ( keys[pygame.K_UP] ):
print("up")
elif ( keys[pygame.K_DOWN] ):
print("down")
elif ( keys[pygame.K_LEFT] ):
print("left")
elif ( keys[pygame.K_RIGHT] ):
print("right")
elif ( keys[pygame.K_ESCAPE] ):
# [Esc] exits too
done = True
# Repaint the screen
SPRITES.update() # re-position the game sprites
WINDOW.fill( INKY_BLACK )
SPRITES.draw( WINDOW ) # draw the game sprites
# Determine collisions - simple rect-based collision first
single_group = pygame.sprite.GroupSingle()
for s in SPRITES:
single_group.sprite = s
collisions = pygame.sprite.groupcollide( single_group, SPRITES, False, False )
# Now double-check collisions with the bitmap-mask to get per-pixel accuracy
for other in collisions[ s ]:
if ( other != s ): # we don't collide with ourselves
# Second step, do more complex collision detection
# using the sprites mask
if ( pygame.sprite.collide_mask( s, other ) ):
#print("Collision")
s.crash( )
other.crash( )
pygame.display.flip()
# Update the window, but not more than 60fps
clock.tick_busy_loop( FPS )
pygame.quit()
NOTE: The framerate of this animated .GIF is much lower than on-screen version, and thus does not reflect the true operation of the example.

Categories