how should I rotate an image continuously using pygame? - python

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.

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:

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

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