Assigning Variables to newly added Sprites - python

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()

Related

Mask acts as it if it was a solid box

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:

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()

How can I rewrite my collision logic in my pygame platformer?

I can't seem to figure out how to write the collision logic for my platformer.
Project File: https://github.com/1NilusNilus/Pygame-Platformer
Player Movement Code:
def move(self):
print(self.POS)
if self.POS[1] > SCREEN_SIZE[1]:
self.POS[1] = SCREEN_SIZE[1] - self.SIZE[1]
self.RECT.x = self.POS[0]
self.RECT.y = self.POS[1]
self.VEL[0] = 0
if self.DIR["left"]:
self.VEL[0] = -5
if self.DIR["right"]:
self.VEL[0] = 5
self.POS[0] += self.VEL[0]
self.VEL[1] += self.GRAVITY
Tile Collision Test Code:
def testCollision(self, rect):
self.RECT.x = self.POS[0]
self.RECT.y = self.POS[1]
for tile in self.TILES:
if rect.colliderect(tile):
self.hitlist.append(self.RECT)
return self.hitlist
You don't describe how you want the collision to work. So I will make it up as I go along.
One of the simplest ways to do collision is to test during attempted movement. That is, decide if the proposed move is legal before changing the player's co-ordinates. This works well because the code knows the original player location, and the direction of travel. So an elegant solution partially moves the player in the desired direction up to the point of collision.
So for starters you seem to be keeping a player POS and a player RECT. Why keep two locations? Let's just use the RECT. But keeping Python Style Guide PEP8 in mind, we'll call it rect.
Looking at your existing function, the move() moves the player left-right, adds gravity, and handles being on-screen. IMHO a Player movement function should not be knowing about gravity, so this should be handled somewhere else. It can simply be passed as part of the y-change. I'll leave the on-screen test as an exercise for the reader.
So about collisions - I don't know anything about your map, but it's possible that a move could collide with more than 1 object in a single move. Imagine this single-jump of dx pixels, use-case:
We know this proposed single-jump right collides with 3 objects. In this implementation of movement, we can only move so-far as to be touching the left-side of left-most terrain element "T2".
Can you see how knowing the proposed movement was "right" helps with this? It allows us to say: "Well moving dx pixels right, we would hit 3 things. So stop at the left-most one". If your player has already moved, and then your collision report says: "Uh-oh, 3 collisions Boss", how can you fix it? You can't.
So we take the theoretical move, if there's no collisions, well the player can move all of it. But if there is a collision, we look at the direction of travel, and find the closest thing we hit. This becomes the limit of movement for that direction. But we can simply handle both dx and dy in the same manner as independent movements.
Reference Code:
import pygame
import random
WINDOW_WIDTH = 500
WINDOW_HEIGHT = 500
WHITE = ( 200, 200, 200 )
GREEN = ( 30, 240, 80 )
BLUE = ( 3, 5, 54 )
class DummyMap:
""" A random map of blockable terrain objects.
Being random, it sometimes unhelpfully puts blocks over the
initial player position. """
def __init__( self, point_count, x_size=32, y_size=32 ):
self.blockers = []
for i in range( point_count ):
random_x = random.randint( 0, WINDOW_WIDTH )
random_y = random.randint( 0, WINDOW_HEIGHT )
self.blockers.append( pygame.Rect( random_x, random_y, x_size, y_size ) )
def draw( self, surface ):
for tile in self.blockers:
pygame.draw.rect( surface, GREEN, tile )
def testCollision( self, rect ):
""" This function is very much NOT efficeient for large lists.
Consider using a quad-tree, etc. for faster collisions """
colliders = []
for tile in self.blockers:
if ( tile.colliderect( rect ) ):
colliders.append( tile )
return colliders
class Player:
""" Simple moveable player block, which collides with map elements """
def __init__( self, x, y ):
self.image = pygame.Surface( ( 32, 32 ) )
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
self.image.fill( WHITE )
def draw( self, surface ):
surface.blit( self.image, self.rect )
def move( self, dx, dy, game_map ):
""" Move the player, handling collisions """
# calculate the target position of any x-move
if ( dx != 0 ):
move_rect = player.rect.copy()
move_rect.move_ip( dx, 0 )
print( "DEBUG: proposed x-move to (%d, %d)" % ( move_rect.x, move_rect.y ) )
# Does this new position collide with the map elements?
collide_rects = game_map.testCollision( move_rect )
if ( len( collide_rects ) > 0 ):
# yes collided, determine which object is the nearest
if ( dx > 0 ):
# Going right, get the left-most x out of everything we hit
lowest_left_side = min( [ r.left for r in collide_rects ] )
# We can only move right as far as this lowest left-side, minus our width
final_dx = lowest_left_side - self.rect.right
else:
# Going left, get the right-most x out of everything we hit
highest_right_side = max( [ r.right for r in collide_rects ] )
# We can only move left as far as the highest right-side
final_dx = highest_right_side - self.rect.left # (this is a negative value)
else:
final_dx = dx # no collsiions, no worries
# Do the x-movement
self.rect.x += final_dx
print( "DEBUG: final x-move to (%d, %d)" % ( self.rect.x, self.rect.y ) )
if ( dy != 0 ):
move_rect = player.rect.copy()
move_rect.move_ip( 0, dy )
print( "DEBUG: proposed y-move to (%d, %d)" % ( move_rect.x, move_rect.y ) )
# Does this new position collide with the map elements?
collide_rects = game_map.testCollision( move_rect )
if ( len( collide_rects ) > 0 ):
# yes collided, determine which object is the nearest
if ( dy < 0 ):
# Going up, get the bottom-most y out of everything we hit
lowest_bottom_side = min( [ r.bottom for r in collide_rects ] )
# We can only move up as far as this lowest bottom
final_dy = lowest_bottom_side - self.rect.top
else:
# Going down, get the top-most y out of everything we hit
highest_top_side = max( [ r.top for r in collide_rects ] )
# We can only move down as far as the highest top-side, minus our height
final_dy = highest_top_side - self.rect.bottom # (this is a negative value)
else:
final_dy = dy # no collsiions, no worries
# Do the y-movement
self.rect.y += final_dy
print( "DEBUG: final x-move to (%d, %d)" % ( self.rect.x, self.rect.y ) )
### initialisation
pygame.init()
window = pygame.display.set_mode( ( WINDOW_WIDTH, WINDOW_HEIGHT ) )
pygame.display.set_caption("Collision Demo")
# Game elements
player = Player( WINDOW_WIDTH//2, WINDOW_HEIGHT//2 )
game_map = DummyMap( 37 )
### 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
# Movement keys
keys = pygame.key.get_pressed()
dx = 0
dy = 0 # 2 # gravity sucks
if ( keys[pygame.K_UP] ):
dy -= 5
if ( keys[pygame.K_DOWN] ):
dy += 5
if ( keys[pygame.K_LEFT] ):
dx -= 5
if ( keys[pygame.K_RIGHT] ):
dx += 5
# Try to move the player according to the human's wishes
player.move( dx, dy, game_map )
# Update the window, but not more than 60fps
window.fill( BLUE )
game_map.draw( window )
player.draw( window )
pygame.display.flip()
# Clamp FPS
clock.tick_busy_loop(60)
pygame.quit()

Working with classes (creating a class efficiently, drawing and updating all instances of the class every frame)

I've been working on a "bullet hell" game of my own, but I am struggling very much with working with classes (im new to python), below i've enclosed my code(I wouldnt include so much but i dont know howo to go about the problem). At the moment what i've been attempting to
1. Create different instances of the class
2. Draw them every frame
3. Update their position every frame according to the x and y speed
import pygame
# Define colors needed
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GREEN = (0, 255, 0)
RED = (255, 0, 0)
bulletArray = []
BulletSprite = ("Bullet4.png")
pygame.init()
class Bullet:
def __init__(self, sprite, x, y, xSpeed, ySpeed):
pygame.sprite.Sprite.__init__(self)
self.x = x
self.y = y
self.xSpeed = xSpeed
self.ySpeed = ySpeed
bulletArray.append(self)
def update(self):
pass
def draw(screen):
self.screen = screen
screen.blit(screen, (x, y))
#Set the width and height of the game screen
size = (700, 500)
screen = pygame.display.set_mode(size)
pygame.display.set_caption("Bullet stuff")
# Loop until the user clicks the close button.
done = False
# Used to manage how fast the screen updates
clock = pygame.time.Clock()
testBullet = Bullet(BulletSprite, 200, 200, 2, 2)
#Main Program Loop
while not done:
# --- Main event loop
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
# --- Game logic should go here
print(bulletArray)
# --- background image.
screen.fill(WHITE)
# --- Drawing code should go here
# --- Updates the screen every frame with whats been drawn
pygame.display.flip()
# --- Limit to 60 frames per second
clock.tick(60)
# Close the window and quit.
pygame.quit()
Much of the this mechanism is already handled automatically if you use the PyGame Sprite Class.
A PyGame sprite is essentially made of:
An Image to draw to represent the object
A rectangle around the object to remember the location and check for collisions
An update() function to handle any movement of the sprite
It's handy to work with the existing PyGame Sprite layout because a lot of functionality is already provided by the library code.
So let's make a BulletSprite:
class BulletSprite( pygame.sprite.Sprite ):
def __init__( self, bitmap, x, y ):
pygame.sprite.Sprite.__init__( self )
self.image = bitmap # How the sprite looks
self.rect = bitmap.get_rect() # Bounding box the size of the image
self.rect.centerx = x # position the bullet
self.rect.centery = y # about its centre
And that's it. This will give you a stationary sprite, painted as the given bitmap at (x,y) on-screen.
So let's now use it:
# Create a group of 100 bullets
sprite_image = pygame.image.load( "bullet.png" )
all_bullets = pygame.sprite.Group() # Group to hold the sprites
for i in range( 100 ):
random_x = 50 + random.randrange( 950 ) # TODO: use proper screen size
random_y = 50 + random.randrange( 950 )
new_sprite = BulletSprite( sprite_image, random_x, random_y )
all_bullets.add( new_sprite )
# Main Window loop
while True:
# Handle user-input
for event in pygame.event.get():
if ( event.type == pygame.QUIT ):
break
# Paint the window
window.fill( ( 0, 0, 0 ) ) # Paint it Black
all_bullets.update() # move all the bullets
all_bullets.draw( window ) # Paint all the bullets
pygame.display.flip()
pygame.quit()
And that's it. After the sprites are created, it takes two lines of code to move them all, and draw them all.
But they wont move yet. For them to move, the BulletSprite needs to have an update() function. This function does whatever is necessary to move the sprite - apply velocity, gravity, whatever. The net effect is the sprite's rectangle's (x,y) is changed.
So adding some changes to the sprite - first an x and y velocity, and then the movement code.
class BulletSprite( pygame.sprite.Sprite ):
def __init__( self, bitmap, x, y ):
pygame.sprite.Sprite.__init__( self )
self.image = bitmap # How the sprite looks
self.rect = bitmap.get_rect() # Bounding box the size of the image
self.rect.centerx = x # position the bullet
self.rect.centery = y # about its centre
self.x_move = random.randrange( -3, 3 ) # simple random velocity
self.y_move = random.randrange( -3, 3 )
def update( self ):
x = self.rect.centerx + self.x_move # calculate the new position
y = self.rect.centerx + self.y_move # by applying an offset
# has the sprite gone off-screen
if ( x < 0 or x > 1000 or y < 0 or y > 1000 )
self.kill() # remove from the group
else:
self.rect.centerx = x # RE-position the bullet
self.rect.centery = y
Every time the update() is called on your sprite group (all_bullets), the sprite library will go and call the update() function on every sprite in the group. The code checks to see if the bullet is off-screen (I lazily used 1000 as a placeholder for screen width & height), and if-so, the sprite.kill() function handles the removing from the group and cleaning up. It's very easy.

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