I'm checking missileGroup to see if any instances of missile collided with any instances enemy in enemyGroup. When run, it prints "Hit" for the first loop, but it ignores the second for loop. Why is that?
#### Imagine this is in a game loop ####
for missile in missileGroup:
if pygame.sprite.spritecollide(missile, enemyGroup, False) :
print("Hit")
for enemy in enemyGroup:
if pygame.sprite.spritecollide(enemy, missileGroup, False):
print("HI")
Update: #Rabbid76 stated that spritecollide wouldn't work because the spriteGroup enemyGroup is a list of sprites within a group(enemyGroup <- enemyList <- Enemy(sprite)) instead of a group of sprites(enemyGroup <- Enemy(sprite)). How would I access that?
Update 2 #paxdiablo stated that the first loop maybe emptying the group after iterating. I switched the places of the loops and 2nd loop ran, while the 1st did not.
Update 3 In the full code, .reset() method runs .kill() which removes the sprite from the group. Since the first loop removes the missile sprite before the second loop couldn't detect any collisions:
for missile in missileGroup:
if pygame.sprite.spritecollide(missile, enemyGroup, False) :
missile.reset()
for eachEnemy in enemyGroup:
if pygame.sprite.spritecollide(eachEnemy, missileGroup, False):
eachEnemy.reset()
See pygame.sprite.spritecollide():
Return a list containing all Sprites in a Group that intersect with another Sprite.
Therefore the arguments to spritecollide() must be a pygame.sprite.Sprite object and a pygame.sprite.Group object.
A list of pygame.sprite.Sprite objects instead of the Group does not work.
missileGroup = pygame.sprite.Group()
enemyGroup = pygame.sprite.Group()
for missile in missileGroup:
if pygame.sprite.spritecollide(missile, enemyGroup, False):
print("Hit")
for enemy in enemyGroup:
if pygame.sprite.spritecollide(enemy, missileGroup, False):
print("HI")
Furthermore read about kill()
The Sprite is removed from all the Groups that contain it.
Hence if you call kill() in the 1st loop, the 2nd loop won't work, because the sprite is removed from all Groups.
You call kill() in the reset methods. missile.reset() respectively eachEnemy.reset() causes the 2nd loop to fail.
There's no obvious reason, based on the information provided(a), why the second collision check should fail. If there's a collision between (for example) enemy #7 and missile #3, there should also be a collision between missile #3 and enemy #7.
You're not using any edge-case stuff like providing your own (possibly non-commutative) collision function, so it will simply use the sprite rectangle to detect this.
I'd be curious to see the behaviour when you reverse the order of the two loops in the code.
Also, you should specify the types of those group variables. If enemyGroup were something like an exhaustible generator rather than a list, it would be "emptied" by the first loop and then the second loop would have no items to iterate over(b) (the spritecollide call will be iterating over the group to check each item against the sprite).
That's about the only way, short of a bug in spritecollide itself, that you would see the effects you're describing.
By way of example, here's a piece of code that tries to iterate over a generator twice:
class gen3(object):
def __init__(self): self._num = 0
def __iter__(self): return self
def __next__(self):
if self._num == 3: raise StopIteration()
self._num += 1
return self._num - 1
gen = gen3()
print("A: ")
for i in gen: print(" ", i)
print("B: ")
for i in gen: print(" ", i)
The output shows that the second loop does nothing:
A:
0
1
2
B:
Lastly, a definitive way to check the state of the groups is to simply put the following code before each loop:
print("loop X enemy ", len(enemyGroup), enemyGroup)
print("loop X missile", len(missileGroup), missileGroup)
using a suitable value of X to distinguish between the two loops.
(a) Of course, there's always the possibility the information you've given is not fully accurate or complete (no malicious intent is supposed but sometimes people inadvertently skip what they consider to be unimportant details, that ends up being very important).
Example: there may be something happening between those two loops which is causing the issue. I'd prefer to give people the benefit of the doubt but you should probably let us know if that's the case.
(b) It would actually be emptied by the first iteration of the first loop so you'd find that it would probably only ever match for the first missile.
Here's a quick example that shows (in PyGame 1.9.6) this reported behaviour does not happen.
The example creates two sprite groups, then collides them in the exact same way as the OP's example code.
As well as printing, the sprites change from outline -> filled, depending on whether they believe they have participated in a collision. There's a 1:1 mapping of an Enemy colliding with a Missile, and vice-versa.
Apologies for the poor frame-rate...
import pygame
import random
# Window size
WINDOW_WIDTH = 800
WINDOW_HEIGHT = 800
DARK_BLUE = ( 3, 5, 54 )
RED = ( 200, 0, 0 )
YELLOW = ( 240, 250, 0 )
BLACK = ( 0, 0, 0 )
GREY = ( 200, 200, 200 )
GREEN = ( 250, 0, 0 )
TRANSPARENT=( 0,0,0,0 )
class Shape(pygame.sprite.Sprite):
def __init__(self, width=48, height=48):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface( ( width, height ), pygame.SRCALPHA)
self.rect = self.image.get_rect()
# Start position is randomly across the screen, and a little off the top
self.rect.center = ( random.randrange( 0, WINDOW_WIDTH ), random.randrange( 0, WINDOW_HEIGHT ) )
# Movement
self.dx = random.randrange( -2, 2 )
self.dy = random.randrange( -2, 2 )
# Looks like
self.filled = 2
self.last_fill = -1
self.render()
def setFilled( self, value ):
if ( value == True ):
self.filled = 0
else:
self.filled = 2
def update( self ):
if ( self.last_fill != self.filled ):
self.last_fill = self.filled
self.render()
self.rect.move_ip( self.dx, self.dy )
if ( self.rect.left > WINDOW_WIDTH ):
self.rect.x = -self.rect.width
elif ( self.rect.right < 0 ):
self.rect.left = WINDOW_WIDTH
if ( self.rect.y > WINDOW_HEIGHT ):
self.rect.y = 0
elif ( self.rect.y < 0 ):
self.rect.y = WINDOW_HEIGHT
class Square( Shape ):
def render( self ):
# Something to draw
if ( self.filled == 0 ):
self.image.fill( RED )
else:
border=3
x, y = border, border
width = self.rect.width - border -1
height = self.rect.height - border -1
self.image.fill( TRANSPARENT )
pygame.draw.rect( self.image, RED, (x,y,width,height), self.filled )
class Circle( Shape ):
def render( self ):
self.image.fill( TRANSPARENT )
pygame.draw.circle( self.image, YELLOW, (self.rect.width//2, self.rect.height//2), self.rect.width//2, self.filled )
### initialisation
pygame.init()
window = pygame.display.set_mode( ( WINDOW_WIDTH, WINDOW_HEIGHT ) )
### Some sprite groups
missileGroup = pygame.sprite.Group()
for i in range( 3 ):
new_missile = Circle()
new_missile.render()
missileGroup.add( Circle() )
enemyGroup = pygame.sprite.Group()
for i in range( 12 ):
new_enemy = Square()
new_enemy.render()
enemyGroup.add( Square() )
### 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
# Move * collide the sprites
missileGroup.update()
enemyGroup.update()
# Test Collisions
for missile in missileGroup:
if pygame.sprite.spritecollide(missile, enemyGroup, False) :
print("Missile " + str(missile) + " Hits Enemy")
missile.setFilled( True )
else:
missile.setFilled( False )
for enemy in enemyGroup:
if pygame.sprite.spritecollide(enemy, missileGroup, False):
print("Enemy " + str(enemy) + " Hits Missile")
enemy.setFilled( True )
else:
enemy.setFilled( False )
# Paint the window, but not more than 60fps
window.fill( DARK_BLUE )
enemyGroup.draw( window )
missileGroup.draw( window )
pygame.display.flip()
# Clamp FPS
clock.tick(60)
pygame.quit()
Related
I don't know if my title was very clear, so I'm going to try to explain this more clearly here. So I have a Sprite in Pygame called spikes. I want there to be more than one spikes so I blit a second one. Problem is, my spike_collision box only works on the first one that I blit, and not the second one. Other than having to make a second collision box, how can I have the second spikes to have its own collision box as well?
Here's the blit code:
screen.blit(spikes, (spike_x, 500 - player_y + 476))
screen.blit(spikes, (spike_x + 200, 500 - player_y + 476))
Here's the collision-box code:
spike_collision = spikes.get_rect(topleft=(spike_x, 500 - player_y + 476))
Thanks.
I'm assuming when you write "sprite", you mean a bitmap-sprite, and not a pygame.sprite.Sprite.
It's probably best to maintain a sprite as a bitmap and a rectangle, then always draw the sprite at its rectangle, adjusting the rectangle to re-position the sprite, and using it for any collisions.
This could easily be done with list-pairs:
spike_image = pygame.image.load('spikes.png').convert_alpha()
spike_rect = spike_image.get_rect( )
spikes_a = [ spike_image, spike_rect( top_left=( 100, 100 ) )
spikes_b = [ spike_image, spike_rect( top_left=( 200, 200 ) )
# ...
screen.blit( spikes_a[0], spikes_a[1] )
screen.blit( spikes_b[0], spikes_b[1] )
# etc.
if ( spikes_a[1].colliderect( player_rect ) ):
print( "ouch!" )
However, it would behoove you to use a "proper" sprite object. Sure there's a bit of extra set-up, but it's repaid multiple times with ease of use:
class Spike( pygame.sprite.Sprite ):
def __init__( self, position=( 0, 0 ) ):
self.image = pygame.image.load('spikes.png').convert_alpha()
self.rect = self.image.get_rect( top_left = position )
def moveTo( self, position ):
self.rect = self.image.get_rect( top_left = position )
def moveBy( self, dx, dy ):
self.rect.move_ip( dx, dy )
spike_a = Spike( ( 100, 100 ) )
spike_b = Spike( ( 200, 200 ) )
spike_a.draw( window ) # get this for free
There's a whole lot of useful group & collision functionality that comes along with using Sprite objects, it's well worth a read: https://www.pygame.org/docs/ref/sprite.html
Working on my first project and I'm trying to get enemy movement sorted, the code below is my current implementation. The enemy using the distance between the player position (target.pos.x) and their pos.x. Want the enemy to move left 20 paces then change direction and move right 20 paces, rinse and repeat.
self.target = game.player
def movement(self):
self.acc = vec(0, BOSS_GRAVITY)
if (-17 >= (self.pos.x - self.target.pos.x) >= -200) and self.target.hit_rect.y == self.hit_rect.y:
self.vel.x -= BOSS_SPEED * -1
self.enemy_direction = 'R'
if (200 >= (self.pos.x - self.target.pos.x) >= 17) and self.target.hit_rect.y == self.hit_rect.y:
self.vel.x += BOSS_SPEED * -1
self.enemy_direction = 'L'
self.acc.x += self.vel.x * BOSS_FRICTION
self.vel += self.acc
self.pos += self.vel
self.pos += self.vel + 0.5 * self.acc
I want my enemies to move to the right a certain amount then change velocity and go the opposite way not remain idle.
I want the enemy to move left 20 paces then change direction and move right 20 paces, rinse and repeat.
Ok, so how do we achieve that? First some definitions:
What is a "pace"? Let's start with 5 pixels.
Left is -x; Right is +x.
There's a few extra things to take care of too. What about when the object can't move in the desired direction? It can turn around.
So we need to keep a bunch of stats about this Enemy: Location, Step Count, Direction of Travel. As soon as you have a few datum points, think: Data Structure. Now I'm going to put all this into a Python class, but it could also go into a simple list. But these get unwieldy with more than a few points of data.
# As a list
enemy_image = pygame.image.load( "doomba.png" ).convert_alpha()
enemy_rect = enemy_image.get_rect()
enemy_rect.center = ( x, y )
enemy1 = [ enemy_rect, enemy_image, PACE_SIZE, TURN_SIZE ]
Much better as a class:
# As a sprite class
class Enemy( pygame.sprite.Sprite ):
def __init__( self, x, y, bitmap, pace=5, turn_after=20 ):
""" Create a new Enemy at that is drawn at (x,y) as the /bitmap/.
It moves /pace/ pixels each step, left, then right """
pygame.sprite.Sprite.__init__( self )
self.image = pygame.image.load( bitmap ).convert_alpha()
self.rect = self.image.get_rect()
self.rect.center = ( x, y ) # location
self.pace_size = pace # How big each step is
self.pace_count = 0 # distance moved
self.direction = -1 # Start moving left (-x)
self.turn_after = turn_after # distance limit
(I've made the data structure based on a PyGame Sprite because it costs only 2 lines of code, and provides lots of pre-built functionality.)
So now we have a data structure (named Enemy) that contains a location, size, bitmap, and remembers how far it's walked, and in which direction. However it doesn't yet implement any sort of movement algorithm. So let's add this.
The Sprite class wants this algorithm to be written into a function named update(). This function is called every frame to decide on the movement for that frame. This might be no-movement, or something else. It can be anything.
Here you can see we're tallying the number of paces moved into self.pace_count, and then adjusting the bitmap's x position (held in self.rect) by the length of a pace (self.pace_size). If the enemy is moving left, the pace-size needs to be subtracted, and for right, added. We can do this automatically by multiplying the amount we add by the self.direction, either -1 or 1. The direction value is set whenever the Enemy turns around.
def update( self ):
""" Implement the movement algorithm """
# Walk pace in the current direction
self.pace_count += 1
self.rect.x += self.direction * self.pace_size # Move some pixels
# We need to turn around if walked enough paces in the same direction
if ( self.pace_count >= self.turn_after ):
# Turn around!
self.direction *= -1 # reverses the pixel distance
self.pace_count = 0 # reset the pace count
# We also should change direction if we hit the screen edge
if ( self.rect.x <= 0 ):
self.direction = 1 # turn right
self.pace_count = 0
elif ( self.rect.x >= WINDOW_WIDTH - self.rect.width ):
self.direction = -1
self.pace_count = 0
So when the Enemy walks the set number of paces, the direction is reversed and the paces-tally zeroed. But we also need to turn around if we hit the side of the screen. When this happens, there is only a single obvious way to turn, so the direction is changed absolutely, rather than being reversed. This code could probably be made simpler, since it basically does almost the same things each time. But I've left it a bit longer to clearly illustrate the steps involved.
And that it's, the algorithm is implemented. Looking at a demo, it's way too fast. So let's also incorporate a real-time speed.
An easy way to control movement speed is to put in a delay between steps. First decide how often the Enemy will move (e.g.: every 100 milliseconds), stored in self.speed and then the time the last step was taken in self.pace_time. Then when it comes time to update, look at the PyGame clock to see if enough milliseconds have elapsed, and only then move the Enemy. Otherwise do nothing.
def update( self ):
""" Implement the movement algorithm """
time_now = pygame.time.get_ticks() # what time is it
if ( time_now > self.pace_time + self.speed ): # time to move again?
self.pace_time = time_now # remember move time
# Walk pace in the current direction
self.pace_count += 1
This gives a much more sedate movement. I tweaked the Enemy to move more often, but in smaller steps. So now it also does not traverse as much of the window. It's important to control speed as a function of time rather than frame-rate. For example, if I'd just made the pace size 0.2 pixels above, sure that would slow the mushroom down to some speed. But it's only accurate on my computer. What if the frame-rate was only 21 FPS, suddenly it moving 2/3 slower again. And what if the frame rate was 160 FPS? It would be back to super-fast, that's what. So keep any sort of speed and movement controlled by real-time milliseconds, not frame-rate & distance.
Anyway, that should be enough to get you going on your own movement algorithms. Please comment if there's questions about the code.
Reference Code:
import pygame
# Window size
WINDOW_WIDTH = 600
WINDOW_HEIGHT = 400
WINDOW_SURFACE = pygame.HWSURFACE|pygame.DOUBLEBUF
DARK_BLUE = ( 3, 5, 54)
class Enemy( pygame.sprite.Sprite ):
def __init__( self, x, y, pace, bitmap, turn_after=20, speed=100 ):
""" Create a new Enemy at that is drawn at (x,y) as the /bitmap/.
It moves /pace/ pixels left, then right """
pygame.sprite.Sprite.__init__( self )
self.image = pygame.image.load( bitmap ).convert_alpha()
self.rect = self.image.get_rect()
self.rect.center = ( x, y ) # location
self.pace_size = pace # How big each step is
self.pace_count = 0 # distance moved
self.direction = -1 # Start moving left (-x)
self.turn_after = turn_after # distance limit
self.speed = speed # Milliseconds per pace
self.pace_time = 0 # time of last step
def update( self ):
""" Implement the movement algorithm """
time_now = pygame.time.get_ticks() # what time is it
if ( time_now > self.pace_time + self.speed ): # is it time to move again
self.pace_time = time_now
# Walk pace in the current direction
self.pace_count += 1
self.rect.x += self.direction * self.pace_size # Move some pixels
# We need to turn around if walked enough paces in the same direction
if ( self.pace_count >= self.turn_after ):
# Turn around!
self.direction *= -1 # reverses the pixel distance
self.pace_count = 0 # reset the pace count
# We also should change direction if we hit the screen edge
if ( self.rect.x <= 0 ):
self.direction = 1 # turn right
self.pace_count = 0
elif ( self.rect.x >= WINDOW_WIDTH - self.rect.width ):
self.direction = -1
self.pace_count = 0
### initialisation
pygame.init()
pygame.mixer.init()
window = pygame.display.set_mode( ( WINDOW_WIDTH, WINDOW_HEIGHT ), WINDOW_SURFACE )
pygame.display.set_caption("Movement Algorithm Example")
### Sprite and Sprite Group
pos_x = WINDOW_WIDTH//2
pos_y = WINDOW_HEIGHT//2
pace_size = 7
enemy = Enemy( pos_x, pos_y, pace_size, "mushroom.png" )
all_sprites_group = pygame.sprite.Group()
all_sprites_group.add( enemy )
### 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
elif ( event.type == pygame.KEYUP ):
pass
# Movement keys
#keys = pygame.key.get_pressed()
#if ( keys[pygame.K_UP] ):
# print("up")
# Update the window, but not more than 60fps
all_sprites_group.update()
window.fill( DARK_BLUE )
all_sprites_group.draw( window )
pygame.display.flip()
# Clamp FPS
clock.tick_busy_loop(60)
pygame.quit()
I am creating tetris using pygame. i want to use collision detection so that when the shape in play comes into contact with any other previously played shapes, i can stop the shape, as per the logic of tetris. i came across pixel perfect collision using masks. i have followed some tutorials online, however the pixel detection returns true every time a new shape comes into play, not when any shapes collide. sorry in advance for the long code, its the bare minimum for the code to actually and still containing the game element of it. i think there is something wrong with my approach which is causing this error. I basically have a function that everytime the shape in play comes into contact with the 'floor' that shape is held in that position and a new shape is created. i think ive overcomplicated it, in turn creating this error. thanks in advance
import pygame
import sys
import shapelogic
pygame.init()
screensize = width, height = 800, 595
screen = pygame.display.set_mode(screensize)
background_image =pygame.image.load("/Users/marceason/PycharmProjects/Tetris/Wooden_background.jpg").convert_alpha()
myshape = 0
stop_movement = 0
blit_count = 0
stored_shapes = pygame.sprite.Group()
stored_shapes_with_coords = []
extra_blit_required = False
index = 0
count = 0
listofshapes = []
class shapemanager():
def __init__(self):
self.listofshapes = []
def create_another_instance(self):
global count
count += 1
string = "Shape_{0},".format(count)
another_shape = Shape(string)
self.listofshapes.append(another_shape)
global index
object = self.listofshapes[index]
index += 1
return object
def load_shape(self):
shape = self.create_another_instance()
shape.load_shapes()
class Shape(pygame.sprite.Sprite):
def __init__(self, name):
pygame.sprite.Sprite.__init__(self)
self.name = name
self.x = 50
self.y = 100
self.move_event = pygame.USEREVENT + 1
self.reached_bottom_event = pygame.USEREVENT + 2
self.one_sec_timer = 1000
self.half_sec_timer = 500
self.reachbottomflag = False
self.movement_possible = True
self.image = pygame.image.load(
"/Users/marceason/PycharmProjects/Tetris/Tetris_Shapes/Green_Shape_1_Position_1.png")
self.mask = pygame.mask.from_surface(self.image)
self.rect = self.image.get_rect()
def move_shape(self):
if self.movement_possible:
key_input = pygame.key.get_pressed()
if key_input[pygame.K_LEFT]:
self.x -= 16
if key_input[pygame.K_RIGHT]:
self.x += 16
if not self.reachbottomflag:
if key_input[pygame.K_DOWN]:
self.y += 16
def reachbottom(self):
if self.y >= 560:
self.reachbottomflag = True
def no_movement_possible(self):
self.movement_possible = False
def assign_shape():
global myshape
global stop_movement
myshape = sl.create_another_instance()
pygame.time.set_timer(myshape.move_event, myshape.one_sec_timer)
stop_movement = pygame.time.set_timer(myshape.reached_bottom_event, myshape.half_sec_timer)
def blit_used_shapes():
global screen
global blit_count
blit_count = len(stored_shapes_with_coords)
local_count = 0
while local_count < blit_count:
screen.blit(stored_shapes_with_coords[local_count][0], (stored_shapes_with_coords[local_count][1], stored_shapes_with_coords[local_count][2]))
local_count += 1
sl = shapemanager()
##### HERE IS THE PIXEL DETECTION #####
result = pygame.sprite.spritecollide(myshape, stored_shapes, False, pygame.sprite.collide_mask)
## Main loop ##
assign_shape()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT: sys.exit()
screen.blit(background_image, (0, 0))
screen.blit(myshape.image, (myshape.x, myshape.y))
myshape.move_shape()
key_input = pygame.key.get_pressed()
if key_input[pygame.K_SPACE]:
myshape.rotate_shape()
myshape.reachbottom()
if myshape.reachbottomflag:
if event.type == myshape.reached_bottom_event:
myshape.no_movement_possible()
stored_shape_tuple = [myshape.image, myshape.x, myshape.y]
stored_shapes_with_coords.append(stored_shape_tuple)
stored_shapes.add(myshape)
extra_blit_required = True
assign_shape()
####### PIXEL DETECTION IS HERE IN FOR LOOP ####
if result:
print("this should only execute when two shapes touch!!")
if extra_blit_required:
blit_used_shapes()
pygame.display.update()
The issue is that you are not updating the sprites rect attribute. The sprites rects all have position (0, 0) (since you do not set it in the call to self.image.get_rect()) and as a result the masks will all overlap and collide.
If you read the docs for pygame.sprite.collide_mask you will note that it says that your sprites need to have mask and rect attributes. You have a rect in your sprite and you set it in the __init__(), but you do not keep it updated when you move the sprite. You just change the x and y attributes without adjusting the rect position. The reason that the collide_mask wants a rect is that it uses that to determine the offset parameter for the pygame.mask.Mask.overlap() call that it uses. The important thing to realize is that masks themselves do not have a position, they need the rects to determine the relative positions of the masks.
This is similar to images/surfaces not having a position and needing a rect to track that for them.
On a separate issue, the way you are blit'ing the sprites to the screen makes no sense. You are not using the abilities of the sprite groups to draw and worse you are keeping the image, x and y of the sprite in a separate list and not containing it in the sprite itself. You should go look at some examples of pygame sprite based code. There are lots of examples out there.
I have 2 sprite groups, one for the enemies, and one for the players weapon. An example of how I set up the enemy sprite group... (The weapon sprite group is done the same way)
class Bat(pygame.sprite.Sprite):
def __init__(self, bat_x, bat_y, bat_image, bat_health, bat_immune, const, dash_counter, dash, dash_time, fly_count, fly_time):
pygame.sprite.Sprite.__init__(self)
self.bat_health = bat_health
self.bat_immune = bat_immune
self.const = const
self.dash_counter = dash_counter
self.dash = dash
self.dash_time = dash_time
self.fly_count = fly_count
self.fly_time = fly_time
self.image = bat_image
self.rect = self.image.get_rect()
self.mask = pygame.mask.from_surface(self.image)
self.rect.topleft = (bat_x, bat_y)
self.bat_x = bat_x
self.bat_y = bat_y
def update(self):
if pygame.sprite.groupcollide(all_bats, all_sword, False, True):
self.bat_health -= 1
if self.bat_health == 0:
self.kill()
... #Irrelevant
all_bats = pygame.sprite.Group()
def bat_create():
bat_x = r_x*40
bat_y = r_y*40
bat_health = 5
bat_immune = False
const = 3
dash_counter = 0
dash = False
dash_time = 0
fly_count = 0
fly_time = 0
new_bat = Bat(bat_x, bat_y, bat_image, bat_health, bat_immune, const, dash_counter, dash, dash_time, fly_count, fly_time)
all_bats.add(new_bat)
When the players weapon hits a bat, regardless of which is being hit, after 5 hits the first bat to spawn is killed. I have no clue why this is happening.
It looks like this is the issue:
def update(self):
if pygame.sprite.groupcollide(all_bats, all_sword, False, True):
self.bat_health -= 1
groupcollide returns a list of collisions. The above code is checking all_bats against all_swords, so everything Vs everything. And if ( non-empty-list ) always returns True.
So if any bat+sword collision is made, this sprite's health is is decreased. This is probably decreasing every bat's self.bat_health each time any bat collides. Thus the first item created will reach zero first.
You need to check if "this sprite" is part of the collision. It's a fairly simply change:
def update(self):
hit_list = pygame.sprite.groupcollide(all_bats, all_sword, False, True):
if ( self in hit_list ):
self.bat_health -= 1
But it's not efficient to do a groupcollide for every bat, during each bat's update(). Perhaps move the groupcollide call to somewhere outside the .update() function, and just perform these calculations once. Pass the collision result to the sprite-update function, or suchlike.
I'm learning Object Orientated Python and understand the main principals of classes and creating objects from classes however I need something explained Re: the pygame code below. I'm struggling to get my head around what's happening when sprite lists are being created and the two lines of code under the code which creates the ball object (allsprites.add etc). In other words what are sprites and why are lists of them created? Why isn't the ball object just created from the class on its own? Why does it need to be added to a sprite list?? What's going on? Any explanation would be greatly appreciated.
"""
Sample Breakout Game
Sample Python/Pygame Programs
Simpson College Computer Science
http://programarcadegames.com/
http://simpson.edu/computer-science/
"""
# --- Import libraries used for this program
import math
import pygame
# Define some colors
black = (0, 0, 0)
white = (255, 255, 255)
blue = (0, 0, 255)
# Size of break-out blocks
block_width = 23
block_height = 15
class Block(pygame.sprite.Sprite):
"""This class represents each block that will get knocked out by the ball
It derives from the "Sprite" class in Pygame """
def __init__(self, color, x, y):
""" Constructor. Pass in the color of the block,
and its x and y position. """
# Call the parent class (Sprite) constructor
pygame.sprite.Sprite.__init__(self)
# Create the image of the block of appropriate size
# The width and height are sent as a list for the first parameter.
self.image = pygame.Surface([block_width, block_height])
# Fill the image with the appropriate color
self.image.fill(color)
# Fetch the rectangle object that has the dimensions of the image
self.rect = self.image.get_rect()
# Move the top left of the rectangle to x,y.
# This is where our block will appear..
self.rect.x = x
self.rect.y = y
class Ball(pygame.sprite.Sprite):
""" This class represents the ball
It derives from the "Sprite" class in Pygame """
# Speed in pixels per cycle
speed = 10.0
# Floating point representation of where the ball is
x = 0.0
y = 180.0
# Direction of ball (in degrees)
direction = 200
width = 10
height = 10
# Constructor. Pass in the color of the block, and its x and y position
def __init__(self):
# Call the parent class (Sprite) constructor
pygame.sprite.Sprite.__init__(self)
# Create the image of the ball
self.image = pygame.Surface([self.width, self.height])
# Color the ball
self.image.fill(white)
# Get a rectangle object that shows where our image is
self.rect = self.image.get_rect()
# Get attributes for the height/width of the screen
self.screenheight = pygame.display.get_surface().get_height()
self.screenwidth = pygame.display.get_surface().get_width()
def bounce(self, diff):
""" This function will bounce the ball
off a horizontal surface (not a vertical one) """
self.direction = (180 - self.direction) % 360
self.direction -= diff
def update(self):
""" Update the position of the ball. """
# Sine and Cosine work in degrees, so we have to convert them
direction_radians = math.radians(self.direction)
# Change the position (x and y) according to the speed and direction
self.x += self.speed * math.sin(direction_radians)
self.y -= self.speed * math.cos(direction_radians)
# Move the image to where our x and y are
self.rect.x = self.x
self.rect.y = self.y
# Do we bounce off the top of the screen?
if self.y <= 0:
self.bounce(0)
self.y = 1
# Do we bounce off the left of the screen?
if self.x <= 0:
self.direction = (360 - self.direction) % 360
self.x = 1
# Do we bounce of the right side of the screen?
if self.x > self.screenwidth - self.width:
self.direction = (360 - self.direction) % 360
self.x = self.screenwidth - self.width - 1
# Did we fall off the bottom edge of the screen?
if self.y > 600:
return True
else:
return False
class Player(pygame.sprite.Sprite):
""" This class represents the bar at the bottom that the player controls. """
def __init__(self):
""" Constructor for Player. """
# Call the parent's constructor
pygame.sprite.Sprite.__init__(self)
self.width = 75
self.height = 15
self.image = pygame.Surface([self.width, self.height])
self.image.fill((white))
# Make our top-left corner the passed-in location.
self.rect = self.image.get_rect()
self.screenheight = pygame.display.get_surface().get_height()
self.screenwidth = pygame.display.get_surface().get_width()
self.rect.x = 0
self.rect.y = self.screenheight-self.height
def update(self):
""" Update the player position. """
# Get where the mouse is
pos = pygame.mouse.get_pos()
# Set the left side of the player bar to the mouse position
self.rect.x = pos[0]
# Make sure we don't push the player paddle
# off the right side of the screen
if self.rect.x > self.screenwidth - self.width:
self.rect.x = self.screenwidth - self.width
# Call this function so the Pygame library can initialize itself
pygame.init()
# Create an 800x600 sized screen
screen = pygame.display.set_mode([800, 600])
# Set the title of the window
pygame.display.set_caption('Breakout')
# Enable this to make the mouse disappear when over our window
pygame.mouse.set_visible(0)
# This is a font we use to draw text on the screen (size 36)
font = pygame.font.Font(None, 36)
# Create a surface we can draw on
background = pygame.Surface(screen.get_size())
# Create sprite lists
blocks = pygame.sprite.Group()
balls = pygame.sprite.Group()
allsprites = pygame.sprite.Group()
# Create the player paddle object
player = Player()
allsprites.add(player)
# Create the ball
ball = Ball()
allsprites.add(ball)
balls.add(ball)
# The top of the block (y position)
top = 80
# Number of blocks to create
blockcount = 32
# --- Create blocks
# Five rows of blocks
for row in range(5):
# 32 columns of blocks
for column in range(0, blockcount):
# Create a block (color,x,y)
block = Block(blue, column * (block_width + 2) + 1, top)
blocks.add(block)
allsprites.add(block)
# Move the top of the next row down
top += block_height + 2
# Clock to limit speed
clock = pygame.time.Clock()
# Is the game over?
game_over = False
# Exit the program?
exit_program = False
# Main program loop
while exit_program != True:
# Limit to 30 fps
clock.tick(30)
# Clear the screen
screen.fill(black)
# Process the events in the game
for event in pygame.event.get():
if event.type == pygame.QUIT:
exit_program = True
# Update the ball and player position as long
# as the game is not over.
if not game_over:
# Update the player and ball positions
player.update()
game_over = ball.update()
# If we are done, print game over
if game_over:
text = font.render("Game Over", True, white)
textpos = text.get_rect(centerx=background.get_width()/2)
textpos.top = 300
screen.blit(text, textpos)
# See if the ball hits the player paddle
if pygame.sprite.spritecollide(player, balls, False):
# The 'diff' lets you try to bounce the ball left or right
# depending where on the paddle you hit it
diff = (player.rect.x + player.width/2) - (ball.rect.x+ball.width/2)
# Set the ball's y position in case
# we hit the ball on the edge of the paddle
ball.rect.y = screen.get_height() - player.rect.height - ball.rect.height - 1
ball.bounce(diff)
# Check for collisions between the ball and the blocks
deadblocks = pygame.sprite.spritecollide(ball, blocks, True)
# If we actually hit a block, bounce the ball
if len(deadblocks) > 0:
ball.bounce(0)
# Game ends if all the blocks are gone
if len(blocks) == 0:
game_over = True
# Draw Everything
allsprites.draw(screen)
# Flip the screen and show what we've drawn
pygame.display.flip()
pygame.quit()
You don't need to add the balls and blocks to sprite lists - it's just a matter of convenience. You could manually check each ball for a collision, but it's easier to just tell pygame to check them all for you
# See if the ball hits the player paddle
if pygame.sprite.spritecollide(player, balls, False):
# The 'diff' lets you try to bounce the ball left or right
# depending where on the paddle you hit it
diff = (player.rect.x + player.width/2) - (ball.rect.x+ball.width/2)
# Set the ball's y position in case
# we hit the ball on the edge of the paddle
ball.rect.y = screen.get_height() - player.rect.height - ball.rect.height - 1
ball.bounce(diff)
You could draw each thing to the screen separately on each frame, but it's easier just to tell pygame to do it for you:
# Draw Everything
allsprites.draw(screen)
Things can be in more than one list as required, for example a ball is added to the balls list so that you can easily check for collisions, but also added to the allsprites list so that you can easily draw everything on the screen
# Create the ball
ball = Ball()
allsprites.add(ball)
balls.add(ball)
Edit:
An important distinction is that allsprites is actually a sprite.Group. It has a list of sprites inside it, but it also has other methods like draw.
To address your question of "what is a Sprite", it's simply a thing that gets drawn on screen. pygame methods like sprite.Group.draw expect a list of things with certain attributes - such as update. The easiest way to make sure that you provide all of those attributes with the right names is to subclass Sprite, however this is also a (strongly recommended) convenience thing - for instance, this is from the pygame source code:
While it is possible to design sprite and group classes that don't
derive from the Sprite and AbstractGroup classes below, it is strongly
recommended that you extend those when you add a Sprite or Group
class.
So what specifically does subclassing Sprite get you? Let's take a look at the source. Here's how to find the source code for a python module:
>>> import pygame.sprite
>>> pygame.sprite.__file__
'c:\\Python27\\lib\\site-packages\\pygame\\sprite.py'
>>>
Every python module has a __file__ attribute that tells you where the source is located (well not quite every). If you open it up in your editor, and scroll down, you see the class definition for Sprite:
class Sprite(object):
"""simple base class for visible game objects
pygame.sprite.Sprite(*groups): return Sprite
The base class for visible game objects. Derived classes will want to
override the Sprite.update() and assign a Sprite.image and
Sprite.rect attributes. The initializer can accept any number of
Group instances to be added to.
When subclassing the Sprite, be sure to call the base initializer before
adding the Sprite to Groups.
"""
def __init__(self, *groups):
self.__g = {} # The groups the sprite is in
if groups: self.add(groups)
def add(self, *groups):
"""add the sprite to groups
Sprite.add(*groups): return None
Any number of Group instances can be passed as arguments. The
Sprite will be added to the Groups it is not already a member of.
"""
has = self.__g.__contains__
for group in groups:
if hasattr(group, '_spritegroup'):
if not has(group):
group.add_internal(self)
self.add_internal(group)
else: self.add(*group)
def remove(self, *groups):
"""remove the sprite from groups
Sprite.remove(*groups): return None
Any number of Group instances can be passed as arguments. The Sprite will
be removed from the Groups it is currently a member of.
"""
has = self.__g.__contains__
for group in groups:
if hasattr(group, '_spritegroup'):
if has(group):
group.remove_internal(self)
self.remove_internal(group)
else: self.remove(*group)
def add_internal(self, group):
self.__g[group] = 0
def remove_internal(self, group):
del self.__g[group]
def update(self, *args):
"""method to control sprite behavior
Sprite.update(*args):
The default implementation of this method does nothing; it's just a
convenient "hook" that you can override. This method is called by
Group.update() with whatever arguments you give it.
There is no need to use this method if not using the convenience
method by the same name in the Group class.
"""
pass
def kill(self):
"""remove the Sprite from all Groups
Sprite.kill(): return None
The Sprite is removed from all the Groups that contain it. This won't
change anything about the state of the Sprite. It is possible to continue
to use the Sprite after this method has been called, including adding it
to Groups.
"""
for c in self.__g.keys():
c.remove_internal(self)
self.__g.clear()
def groups(self):
"""list of Groups that contain this Sprite
Sprite.groups(): return group_list
Return a list of all the Groups that contain this Sprite.
"""
return self.__g.keys()
def alive(self):
"""does the sprite belong to any groups
Sprite.alive(): return bool
Returns True when the Sprite belongs to one or more Groups.
"""
return (len(self.__g) != 0)
def __repr__(self):
return "<%s sprite(in %d groups)>" % (self.__class__.__name__, len(self.__g))
So in summary, you don't have to subclass Sprite - you could just provide all of these methods on your own - but it's easier if you do ;)