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

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

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:

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

how should I rotate an image continuously using pygame?

I have an image which I want to rotate continuously.
I thought of rotating it through a certain angle after specific interval of time . However, I wanted to implement a feature of shooting a bullet in the direction my head of the image is pointing at that moment of time when I click a specific key.
So at that moment of time , how should I keep the track of my head in the rotating image?
Creating rotated images is computationally expensive. It is something that should be done once before your program enters its main loop.
A "continuously rotating" image is really a set of animation frames, each one advanced by some degree from the previous.
The pygame function pygame.transform.rotate() will rotate an image. It might be useful in your case to decide some step-angle, then make N images.
For example, if you desired 12 frames of rotation animation, create 11 more frames of animation each rotated by 360 / 12 degrees (is this off-by-one?).
This gives us a simple sprite class, which pre-creates the frames at the time of instantiation. Obviously if you had lots of sprites, it does not make sense to re-compute the same frames for each of them, but it serves as an example.
class RotatingSprite( pygame.sprite.Sprite ):
def __init__( self, bitmap, rot_count=12 ):
pygame.sprite.Sprite.__init__( self )
self.rect = bitmap.get_rect()
self.rect.center = ( random.randrange( 0, WINDOW_WIDTH ), random.randrange( 0, WINDOW_HEIGHT ) )
# start with zero rotation
self.rotation = 0
self.rotations = [ bitmap ]
self.angle_step = rot_count
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._setRotationImage( 0 ) # sets initial image & rect
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 ):
""" Set the sprite image to the correct rotation """
rot_index %= self.angle_slots
self.rotation = rot_index
# Select the pre-rotated image
self.image = self.rotations[ rot_index ]
# We need to preserve the centre-position 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 update( self ):
self.rotateRight() # do something
You could of-course pre-make the 12 frames of rotated bitmap in a graphics package, and just load them in at run-time. I like the above method because if you decide to move to say 24 frames, it all happens with the change of a parameter.
The direction the image is "facing" is simply the current index of rotation ( the self.rotation ) in the class above. For example, Imagine a "rotation" with just 4 frames - up/right/down/left.

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.

Sprite moving faster left than right pygame

I think I'm having a rounding problem causing my sprite to move faster/jump farther while moving left.
My sprites update method is calling move, which calls move_single_axis for each axis. Inside this I'm doing some collision detection where I rely on pygame's rect class to both detect the collision, and set the new position.
I think this is the problem but I'm uncertain how to get around the rounding issue because pygame's rect uses integers under the hood.
Here's the update code:
def update(self, dt, game):
self.calc_grav(game, dt)
self.animate(dt, game)
self._old_position = self._position[:]
self.move(dt, game)
self.rect.topleft = self._position
def move(self, dt, game):
# Move each axis separately. Note that this checks for collisions both times.
dx = self.velocity[0]
dy = self.velocity[1]
if dx != 0:
self.move_single_axis(dx, 0, dt)
if dy != 0:
self.move_single_axis(0, dy, dt)
def move_single_axis(self, dx, dy, dt):
#print("hero_destination: ({}, {})".format(dx *dt, dy *dt))
self._position[0] += dx * dt
self._position[1] += dy * dt
#print("Game walls: {}".format(game.walls))
self.rect.topleft = self._position
body_sensor = self.get_body_sensor()
for wall in game.walls:
if body_sensor.colliderect(wall.rect):
if dx > 0: # Moving right; Hit the left side of the wall
#print(" -- Moving right; Hit the left side of the wall")
self.rect.right = wall.rect.left
if dx < 0: # Moving left; Hit the right side of the wall
#print(" -- Moving left; Hit the right side of the wall")
self.rect.left = wall.rect.right - self.COLLISION_BOX_OFFSET
if dy > 0: # Moving down; Hit the top side of the wall
#print(" -- Moving down; Hit the top side of the wall")
self.rect.bottom = wall.rect.top
if dy < 0: # Moving up; Hit the bottom side of the wall
#print(" -- Moving up; Hit the bottom side of the wall")
self.rect.top = wall.rect.bottom
self._position[0] = self.rect.topleft[0]
self._position[1] = self.rect.topleft[1]
Here is the whole source(https://github.com/davidahines/python_sidescroller):
import os.path
import pygame
from pygame.locals import *
from pytmx.util_pygame import load_pygame
import pyscroll
import pyscroll.data
from pyscroll.group import PyscrollGroup
# define configuration variables here
RESOURCES_DIR = 'data'
HERO_JUMP_HEIGHT = 180
HERO_MOVE_SPEED = 200 # pixels per second
GRAVITY = 1000
MAP_FILENAME = 'maps/dungeon_0.tmx'
# simple wrapper to keep the screen resizeable
def init_screen(width, height):
screen = pygame.display.set_mode((width, height), pygame.RESIZABLE)
return screen
# make loading maps a little easier
def get_map(filename):
return os.path.join(RESOURCES_DIR, filename)
# make loading images a little easier
def load_image(filename):
return pygame.image.load(os.path.join(RESOURCES_DIR, filename))
class Hero(pygame.sprite.Sprite):
""" Our Hero
The Hero has three collision rects, one for the whole sprite "rect" and
"old_rect", and another to check collisions with walls, called "feet".
The position list is used because pygame rects are inaccurate for
positioning sprites; because the values they get are 'rounded down'
as integers, the sprite would move faster moving left or up.
Feet is 1/2 as wide as the normal rect, and 8 pixels tall. This size size
allows the top of the sprite to overlap walls. The feet rect is used for
collisions, while the 'rect' rect is used for drawing.
There is also an old_rect that is used to reposition the sprite if it
collides with level walls.
"""
def __init__(self, map_data_object):
pygame.sprite.Sprite.__init__(self)
self.STATE_STANDING = 0
self.STATE_WALKING = 1
self.STATE_JUMPING = 2
self.FRAME_DELAY_STANDING =1
self.FRAME_DELAY_WALKING = 1
self.FRAME_DELAY_JUMPING = 1
self.FACING_RIGHT = 0
self.FACING_LEFT = 1
self.MILLISECONDS_TO_SECONDS = 1000.0
self.COLLISION_BOX_OFFSET = 8
self.time_in_state = 0.0
self.current_walking_frame = 0
self.current_standing_frame = 0
self.current_jumping_frame = 0
self.load_sprites()
self.velocity = [0, 0]
self.state = self.STATE_STANDING
self.facing = self.FACING_RIGHT
self._position = [map_data_object.x, map_data_object.y]
self._old_position = self.position
self.rect = pygame.Rect(8, 0, self.image.get_rect().width - 8, self.image.get_rect().height)
def set_state(self, state):
if self.state != state:
self.state = state
self.time_in_state = 0.0
def load_sprites(self):
self.spritesheet = Spritesheet('data/art/platformer_template_g.png')
standing_images = self.spritesheet.images_at((
pygame.Rect(0, 0, 32, 32),
), colorkey= (0,255,81))
self.standing_images = []
for standing_image in standing_images:
self.standing_images.append(standing_image.convert_alpha())
self.image = self.standing_images[self.current_standing_frame]
#property
def position(self):
return list(self._position)
#position.setter
def position(self, value):
self._position = list(value)
def get_floor_sensor(self):
return pygame.Rect(self.position[0]+self.COLLISION_BOX_OFFSET, self.position[1]+2, self.rect.width -self.COLLISION_BOX_OFFSET, self.rect.height)
def get_ceiling_sensor(self):
return pygame.Rect(self.position[0]+self.COLLISION_BOX_OFFSET, self.position[1]-self.rect.height, self.rect.width, 2)
def get_body_sensor(self):
return pygame.Rect(self.position[0]+self.COLLISION_BOX_OFFSET, self.position[1], self.rect.width -self.COLLISION_BOX_OFFSET, self.rect.height)
def calc_grav(self, game, dt):
""" Calculate effect of gravity. """
floor_sensor = self.get_floor_sensor()
collidelist = floor_sensor.collidelist(game.walls)
hero_is_airborne = collidelist == -1
if hero_is_airborne:
if self.velocity[1] == 0:
self.velocity[1] = GRAVITY * dt
else:
self.velocity[1] += GRAVITY * dt
def update(self, dt, game):
self.calc_grav(game, dt)
self._old_position = self._position[:]
self.move(dt, game)
def move(self, dt, game):
# Move each axis separately. Note that this checks for collisions both times.
dx = self.velocity[0]
dy = self.velocity[1]
if dx != 0:
self.move_single_axis(dx, 0, dt)
if dy != 0:
self.move_single_axis(0, dy, dt)
self.rect.topleft = self._position
def move_single_axis(self, dx, dy, dt):
#print("hero_destination: ({}, {})".format(dx *dt, dy *dt))
self._position[0] += dx * dt
self._position[1] += dy * dt
#print("Game walls: {}".format(game.walls))
self.rect.topleft = self._position
body_sensor = self.get_body_sensor()
for wall in game.walls:
if body_sensor.colliderect(wall.rect):
if dx > 0: # Moving right; Hit the left side of the wall
self.rect.right = wall.rect.left
if dx < 0: # Moving left; Hit the right side of the wall
self.rect.left = wall.rect.right - self.COLLISION_BOX_OFFSET
if dy > 0: # Moving down; Hit the top side of the wall
self.rect.bottom = wall.rect.top
if dy < 0: # Moving up; Hit the bottom side of the wall
self.rect.top = wall.rect.bottom
self._position[0] = self.rect.topleft[0]
self._position[1] = self.rect.topleft[1]
class Wall(pygame.sprite.Sprite):
"""
A sprite extension for all the walls in the game
"""
def __init__(self, map_data_object):
pygame.sprite.Sprite.__init__(self)
self._position = [map_data_object.x, map_data_object.y]
self.rect = pygame.Rect(
map_data_object.x, map_data_object.y,
map_data_object.width, map_data_object.height)
#property
def position(self):
return list(self._position)
#position.setter
def position(self, value):
self._position = list(value)
class Spritesheet(object):
def __init__(self, filename):
try:
self.sheet = pygame.image.load(filename).convert()
except pygame.error:
print ('Unable to load spritesheet image: {}').format(filename)
raise SystemExit
# Load a specific image from a specific rectangle
def image_at(self, rectangle, colorkey = None):
"Loads image from x,y,x+offset,y+offset"
rect = pygame.Rect(rectangle)
image = pygame.Surface(rect.size).convert()
image.blit(self.sheet, (0, 0), rect)
if colorkey is not None:
if colorkey is -1:
colorkey = image.get_at((0,0))
image.set_colorkey(colorkey, pygame.RLEACCEL)
return image
# Load a whole bunch of images and return them as a list
def images_at(self, rects, colorkey = None):
"Loads multiple images, supply a list of coordinates"
return [self.image_at(rect, colorkey) for rect in rects]
class QuestGame(object):
""" This class is a basic game.
It also reads input and moves the Hero around the map.
Finally, it uses a pyscroll group to render the map and Hero.
This class will load data, create a pyscroll group, a hero object.
"""
filename = get_map(MAP_FILENAME)
def __init__(self):
# true while running
self.running = False
self.debug = False
# load data from pytmx
self.tmx_data = load_pygame(self.filename)
# setup level geometry with simple pygame rects, loaded from pytmx
self.walls = list()
self.npcs = list()
for map_object in self.tmx_data.objects:
if map_object.type == "wall":
self.walls.append(Wall(map_object))
elif map_object.type == "guard":
print("npc load failed: reimplement npc")
#self.npcs.append(Npc(map_object))
elif map_object.type == "hero":
self.hero = Hero(map_object)
# create new data source for pyscroll
map_data = pyscroll.data.TiledMapData(self.tmx_data)
# create new renderer (camera)
self.map_layer = pyscroll.BufferedRenderer(map_data, screen.get_size(), clamp_camera=True, tall_sprites=1)
self.map_layer.zoom = 2
self.group = PyscrollGroup(map_layer=self.map_layer, default_layer=3)
# add our hero to the group
self.group.add(self.hero)
def draw(self, surface):
# center the map/screen on our Hero
self.group.center(self.hero.rect.center)
# draw the map and all sprites
self.group.draw(surface)
if(self.debug):
floor_sensor_rect = self.hero.get_floor_sensor()
ox, oy = self.map_layer.get_center_offset()
new_rect = floor_sensor_rect.move(ox * 2, oy * 2)
pygame.draw.rect(surface, (255,0,0), new_rect)
def handle_input(self, dt):
""" Handle pygame input events
"""
poll = pygame.event.poll
event = poll()
while event:
if event.type == QUIT:
self.running = False
break
elif event.type == KEYDOWN:
if event.key == K_ESCAPE:
self.running = False
break
# this will be handled if the window is resized
elif event.type == VIDEORESIZE:
init_screen(event.w, event.h)
self.map_layer.set_size((event.w, event.h))
event = poll()
# using get_pressed is slightly less accurate than testing for events
# but is much easier to use.
pressed = pygame.key.get_pressed()
floor_sensor = self.hero.get_floor_sensor()
floor_collidelist = floor_sensor.collidelist(self.walls)
hero_is_airborne = floor_collidelist == -1
ceiling_sensor = self.hero.get_ceiling_sensor()
ceiling_collidelist = ceiling_sensor.collidelist(self.walls)
hero_touches_ceiling = ceiling_collidelist != -1
if pressed[K_l]:
print("airborne: {}".format(hero_is_airborne))
print("hero position: {}, {}".format(self.hero.position[0], self.hero.position[1]))
print("hero_touches_ceiling: {}".format(hero_touches_ceiling))
print("hero_is_airborne: {}".format(hero_is_airborne))
if hero_is_airborne == False:
#JUMP
if pressed[K_SPACE]:
self.hero.set_state(self.hero.STATE_JUMPING)
# stop the player animation
if pressed[K_LEFT] and pressed[K_RIGHT] == False:
# play the jump left animations
self.hero.velocity[0] = -HERO_MOVE_SPEED
elif pressed[K_RIGHT] and pressed[K_LEFT] == False:
self.hero.velocity[0] = HERO_MOVE_SPEED
self.hero.velocity[1]= -HERO_JUMP_HEIGHT
elif pressed[K_LEFT] and pressed[K_RIGHT] == False:
self.hero.set_state(self.hero.STATE_WALKING)
self.hero.velocity[0] = -HERO_MOVE_SPEED
elif pressed[K_RIGHT] and pressed[K_LEFT] == False:
self.hero.set_state(self.hero.STATE_WALKING)
self.hero.velocity[0] = HERO_MOVE_SPEED
else:
self.hero.state = self.hero.STATE_STANDING
self.hero.velocity[0] = 0
def update(self, dt):
""" Tasks that occur over time should be handled here
"""
self.group.update(dt, self)
def run(self):
""" Run the game loop
"""
clock = pygame.time.Clock()
self.running = True
from collections import deque
times = deque(maxlen=30)
try:
while self.running:
dt = clock.tick(60) / 1000.
times.append(clock.get_fps())
self.handle_input(dt)
self.update(dt)
self.draw(screen)
pygame.display.flip()
except KeyboardInterrupt:
self.running = False
if __name__ == "__main__":
pygame.init()
pygame.font.init()
screen = init_screen(800, 600)
pygame.display.set_caption('Test Game.')
try:
game = QuestGame()
game.run()
except:
pygame.quit()
raise
I ripped out everything except for the hero and the QuestGame class and could see the incorrect movement, so the problem was not caused by pyscroll (unless there are more issues).
The reason for the movement problems is that you set the self._position in the update method of the hero to the topleft coords of the rect.
self._position[0] = self.rect.topleft[0]
self._position[1] = self.rect.topleft[1]
pygame.Rects can only store integers and truncate floats that you assign to them, so you shouldn't use them to update the actual position of the hero. Here's a little demonstration:
>>> pos = 10
>>> rect = pygame.Rect(10, 0, 5, 5)
>>> pos -= 1.4 # Move left.
>>> rect.x = pos
>>> rect
<rect(8, 0, 5, 5)> # Truncated the actual position.
>>> pos = rect.x # Pos is now 8 so we moved 2 pixels.
>>> pos += 1.4 # Move right.
>>> rect.x = pos
>>> rect
<rect(9, 0, 5, 5)> # Truncated.
>>> pos = rect.x
>>> pos # Oops, we only moved 1 pixel to the right.
9
The self._position is the exact position and should only be set to one of the rect's coords if the hero collides with a wall or another obstacle (because the rect is used for the collision detection).
Move the two mentioned lines into the if body_sensor.colliderect(wall.rect): clause in the wall collision for loop and it should work correctly.
for wall in game.walls:
if body_sensor.colliderect(wall.rect):
if dx > 0: # Moving right; Hit the left side of the wall
self.rect.right = wall.rect.left
self._position[0] = self.rect.left
if dx < 0: # Moving left; Hit the right side of the wall
self.rect.left = wall.rect.right - self.COLLISION_BOX_OFFSET
self._position[0] = self.rect.left
if dy > 0: # Moving down; Hit the top side of the wall
self.rect.bottom = wall.rect.top
self._position[1] = self.rect.top
if dy < 0: # Moving up; Hit the bottom side of the wall
self.rect.top = wall.rect.bottom
self._position[1] = self.rect.top

Categories