Enemy Movement in Pygame Platformer 2D - python

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

Related

pygame top down shooting [duplicate]

This question already has answers here:
Problems with moving an enemy towards a character in pygame
(1 answer)
Pygame doesn't let me use float for rect.move, but I need it
(2 answers)
Closed 8 months ago.
So I have a functioning shooting mechanic in python pygame for a top down shooter, where I am using the mouse position to aim the bullets by working out the angles, however when I do this, the bullets are shooting slightly off where the mouse position is. for instance: the mouse would be where the red arrow is drawn and the bullets will be shooting by a small amount in the wrong direction
Any help would be appreciated
code below:
main.py:
#-------------Imports-------------#
import pygame,sys
#import globals
from background import*
from player import*
#-------------Constants-------------#
WIDTH,HEIGHT = 500,500
WINDOW = pygame.display.set_mode((WIDTH,HEIGHT))
CLOCK = pygame.time.Clock()
BLACK = (0, 0, 0)
#-------------Instances-------------#
bg = Background()
player = Player()
#-------------Functions-------------#
def draw():
WINDOW.fill(BLACK)
bg.update(WINDOW)
player.update(WINDOW)
pygame.display.update()
#-------------Main Game Loop-------------#
def main():
#globals.intialise()
while 1:
CLOCK.tick(1)
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
draw()
#globals.game_ticks += 1
if __name__ == "__main__":
main()
player.py
#-------------Imports-------------#
import pygame,math
#import globals
#-------------Constants-------------#
WIDTH,HEIGHT = 500,500
PLAYER_COLOUR = (255, 212, 112)
BLACK = (0,0,0)
PI = 3.14159265359
#-------------Classes-------------#
class Bullet:
def __init__(self,origin,angle):
self.speed = 20
self.x_speed,self.y_speed = self.speed*math.cos(math.radians(angle)),self.speed*math.sin(math.radians(angle))
self.rect = pygame.Rect(origin[0],origin[1],5,5)
def __del__(self):
pass
def update(self,window):
# move bullet
self.rect.x += self.x_speed
self.rect.y += self.y_speed
# draw bullet
pygame.draw.rect(window,BLACK,self.rect)
# check if bullet is out of the screen
if self.rect.x > WIDTH or self.rect.x < 0:
return -1
elif self.rect.y > HEIGHT or self.rect.y < 0:
return -1
class Player:
def __init__(self):
self.sprite = pygame.transform.scale(pygame.image.load("sprites/temp_player.png"),(50,50))
self.rect = pygame.Rect(250,250,50,50)
self.center = (self.rect.x,self.rect.y)
self.bullets = []
self.fire_rate = 12
def shoot(self,angle,window):
# update all bullets and delete if bullet is out of screen
for bullet in self.bullets:
if bullet.update(window) == -1:
self.bullets.remove(bullet)
del bullet
# instantiate bullet if mouse button pressed
#if pygame.mouse.get_pressed()[0] and globals.game_ticks % self.fire_rate == 0:
if pygame.mouse.get_pressed()[0]:
self.bullets.append(Bullet(self.rect.center,-angle))
def update(self,window):
mx,my = pygame.mouse.get_pos()
# find distance between mouse position and player position
diff_x,diff_y = mx-self.rect.x,my-self.rect.y
# word out angle between mouse and player
angle_rad = math.atan2(diff_y,diff_x)
angle = -360*angle_rad/(2*PI)
# adjust angle according to where we want to rotate the player
# when angle is bottom left
if abs(angle) > 90 and angle < 0:
a = 270-abs(angle)
# when angle is top left
elif abs(angle) > 90:
a = angle-90
# when angle is to the right
else:
a = angle - 90
# create new sprite that is rotated
rotated_image = pygame.transform.rotate(self.sprite,a)
# replace current rectangle with rotated sprite
self.rect = rotated_image.get_rect(center = self.center)
self.shoot(angle,window)
# add image to the screen
#window.blit(pygame.transform.rotate(self.sprite,a),self.rect)
background.py:
#-------------Imports-------------#
import pygame,random,ast,time,globals
#-------------Constants-------------#
WIDTH,HEIGHT = 500,500
TILE_DIMENSION = 9
TILE_SIZE = int(round(WIDTH/TILE_DIMENSION,0))
TO_EDGE = int((TILE_DIMENSION+1)/2)
#-------------Classes-------------#
class Background:
def __init__(self):
self.tiles = self.generate_screen()
self.centre = [2,2]
self.right = 0
self.up = 0
self.speed = 5
def generate_screen(self):
# generate original chunk of tiles
tiles = [[random.randint(100,200) for i in range(TILE_DIMENSION)] for j in range(TILE_DIMENSION)]
# eventually use image instead of random RGB value
return tiles
def movement(self,tile_rects):
keys = pygame.key.get_pressed()
if keys[pygame.K_a] or keys[pygame.K_LEFT]:
# if player is on tile to the left of centre
if (self.right - self.speed) < -TILE_SIZE:
# reset movement and adjust centre
self.right = 0
self.centre[0] -= 1
else:
# add to movement if not on next tile
self.right -= self.speed
# move all rectangles in background to simulate player moving
for i in range(len(tile_rects)):
for j in range(len(tile_rects[0])):
tile_rects[i][j].x += self.speed
if keys[pygame.K_d] or keys[pygame.K_RIGHT]:
# if player is on tile to the right of centre
if (self.right + self.speed) > TILE_SIZE:
# reset movement and adjust centre
self.right = 0
self.centre[0] += 1
else:
# add to movement if not on next tile
self.right += self.speed
# move all rectangles in background to simulate player moving
for i in range(len(tile_rects)):
for j in range(len(tile_rects[0])):
tile_rects[i][j].x -= self.speed
if keys[pygame.K_w] or keys[pygame.K_UP]:
# if player is on tile above the centre
if (self.up + self.speed) > TILE_SIZE:
# reset movement and adjust centre
self.up = 0
self.centre[1] -= 1
else:
# add to movement if not on next tile
self.up += self.speed
# move all rectangles in background to simulate player moving
for i in range(len(tile_rects)):
for j in range(len(tile_rects[0])):
tile_rects[i][j].y += self.speed
if keys[pygame.K_s] or keys[pygame.K_DOWN]:
# if player is on tile below the centre
if (self.up - self.speed) < -TILE_SIZE:
# reset movement and adjust centre
self.up = 0
self.centre[1] += 1
else:
# add to movement if not on next tile
self.up -= self.speed
# move all rectangles in background to simulate player moving
for i in range(len(tile_rects)):
for j in range(len(tile_rects[0])):
tile_rects[i][j].y -= self.speed
return tile_rects
def update(self,window):
# rendering in brand new map chunks
# if part of the chunk trying to be rendered in is non-existant in the 2D map array to the left
if self.centre[0]-TO_EDGE < 0:
# check how many tiles it is offset by
for i in range(0-(self.centre[0]-TO_EDGE)):
# add new column of values at the beginning of the 2D array
for i in range(len(self.tiles)):
self.tiles[i].insert(0,random.randint(120,230))
# due to whole array being shifted to the right, adjust the centre accordingly
self.centre[0] += 1
# if part of the chunk trying to be rendered is non-existant in the 2D map array to the right
if self.centre[0]+TO_EDGE >= len(self.tiles[0]):
# check how many tiles it is offset by
for i in range((self.centre[0]+TO_EDGE)-(len(self.tiles[0])-1)):
# add a new column of values at the end of the 2D array
for i in range(len(self.tiles)):
self.tiles[i].append(random.randint(120,230))
# if part of the chunk trying to be rendered in is non-existant in the 2D array at the top
if self.centre[1]-TO_EDGE < 0:
# check how many tiles it is offset by
for i in range(0-(self.centre[1]-TO_EDGE)):
# add a new row at the top of the 2D array
self.tiles.insert(0,[random.randint(120,230) for i in range(len(self.tiles[0]))])
# due to whole array shifting downwards, adjust the centre accordingly
self.centre[1] += 1
# if part of the chunk trying to be rendered in is non-existant in the 2D array at the bottom
if self.centre[1]+TO_EDGE >= len(self.tiles):
# check how many tiles it is offset by
for i in range((self.centre[1]+TO_EDGE)-(len(self.tiles)-1)):
# add a new row at the bottom of the 2D array
self.tiles.append([random.randint(120,230) for i in range(len(self.tiles[0]))])
# determining which tiles should be rendered in according to the centre(where player would be)
t = []
for i in range(TILE_DIMENSION+2):
t.append([])
for j in range(TILE_DIMENSION+2):
try:
t[i].append(self.tiles[i+(self.centre[1]-TO_EDGE)][j+(self.centre[0]-TO_EDGE)])
except:
pass
# create a rectangle for each tile that is rendered in
tile_rects = [[pygame.Rect((i-1)*TILE_SIZE-self.right,(j-1)*TILE_SIZE+self.up,TILE_SIZE,TILE_SIZE) for i in range(TILE_DIMENSION+2)] for j in range(TILE_DIMENSION+2)]
tile_rects = self.movement(tile_rects)
# draw all rectangles
for i in range(TILE_DIMENSION+2):
for j in range(TILE_DIMENSION+2):
try:
pygame.draw.rect(window,(0,int(t[i][j]),0),tile_rects[i][j])
except:
pass
the background script doesnt affect anything, its just there as a background to make it easier to see, and you may have to make your own temp_player.png image to make it compatible

For Loop blocking other For Loop

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

Understand object orientated python - initialization of objects in pygame

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

Pygame: move a spinning image around the edge of the window

I have an assignment that asks me to do the following:
Use Google's advanced image search to find a reasonably-sized image of a ball that is free to reuse and that includes transparency. Modify the sample code so that your ball slides back and forth across the bottom of the screen. It should take 2 seconds for the ball to go from the left side to the right.
Improve your animation for question 5 so that the ball rotates, accurately, as if it were rolling back and forth.
Modify your animation for question 6 so that the ball travels counterclockwise around the edge of the screen
I am at the last part. Trying to modify the animation for question 6 to do this: (1:24)
http://www.youtube.com/watch?v=CEiLc_UFNLI&feature=c4-overview&list=UUpbgjjXBL3hdTKDZ0gZvdWg
I'm stumped pretty bad. I just can't seem to understand how I will get the ball to slowly move from one point to another. The ball is an image. This is what I have so far, but it doesn't work.
"""Some simple skeleton code for a pygame game/animation
This skeleton sets up a basic 800x600 window, an event loop, and a
redraw timer to redraw at 30 frames per second.
"""
from __future__ import division
import math
import sys
import pygame
class MyGame(object):
def __init__(self):
"""Initialize a new game"""
pygame.mixer.init()
pygame.mixer.pre_init(44100, -16, 2, 2048)
pygame.init()
# set up a 640 x 480 window
self.width = 800
self.height = 600
self.img = pygame.image.load('ball.png')
self.screen = pygame.display.set_mode((self.width, self.height))
self.x = 0
self.y = 0
self.angle = 0
self.rotate_right=True
self.first = True
#0: Move bottomleft to bottomright 1: Move from bottomright to topright 2:Move from topright to topleft 3:Move from topleft to bottomleft
self.mode = 0
# use a black background
self.bg_color = 0, 0, 0
# Setup a timer to refresh the display FPS times per second
self.FPS = 30
self.REFRESH = pygame.USEREVENT+1
pygame.time.set_timer(self.REFRESH, 1000//self.FPS)
def get_mode(self):
rect = self.img.get_rect()
if self.first == True:
self.first = False
return
if (self.x, self.y) == (0, self.height - rect.height):
#Our starting point, bottom left
self.mode = 0
elif (self.x, self.y) == (self.width-rect.width, self.height-rect.height):
#Bottom right
self.mode = 1
elif (self.x, self.y) == (self.width-rect.width, 0):
#Top Right
self.mode = 2
elif (self.x, self.y) == (0,0):
#Top Left
self.mode = 3
def get_target(self):
rect = self.img.get_rect()
if self.mode == 0:
targetPosition = (0, self.height - rect.height)
elif self.mode == 1:
targetPosition = (self.width-rect.width, self.height-rect.height)
elif self.mode == 2:
targetPosition = (self.width-rect.width, 0)
elif self.mode == 3:
targetPosition = (0,0)
return targetPosition
def get_angle(self):
if self.angle == 360:
self.rotate_right = False
elif self.angle == 0:
self.rotate_right = True
if self.rotate_right == True:
self.angle+=12
else:
self.angle-=12
def run(self):
"""Loop forever processing events"""
running = True
while running:
event = pygame.event.wait()
# player is asking to quit
if event.type == pygame.QUIT:
running = False
# time to draw a new frame
elif event.type == self.REFRESH:
self.draw()
else:
pass # an event type we don't handle
def draw(self):
"""Update the display"""
# everything we draw now is to a buffer that is not displayed
self.screen.fill(self.bg_color)
#Draw img
rect = self.img.get_rect()
#Note: this can be made dynamic, but right now since this is typically a poor structure, we will use static values.
#80 is the padding, so it hits right before.
#0,0 : top left
#self.width-rect.width, 0 : top right
#0, self.height-rect.height : bottom left
#self.width-rect.width, self.height-rect.height : bottom right
targetPosition = ()
#img = pygame.transform.rotate(self.img, self.angle)
img = self.img
self.get_angle()
self.get_mode()
targetPosition = self.get_target()
print targetPosition
print self.x, self.y
if self.x < targetPosition[0]:
self.x+= targetPosition[0]-self.x//self.FPS
elif self.x > targetPosition[0]:
self.x-= targetPosition[0]+self.x//self.FPS
if self.y < targetPosition[1]:
print "s"
self.y+= targetPosition[1]-self.y//self.FPS
elif self.y > targetPosition[1]:
self.y-= targetPosition[1]+self.y//self.FPS
rect = rect.move(self.x, self.y)
self.screen.blit(img, rect)
# flip buffers so that everything we have drawn gets displayed
pygame.display.flip()
MyGame().run()
pygame.quit()
sys.exit()
What's happening is that your ball is starting at (0,0) (top left) with a target of (0,550) (bottom left), discovers that it's at a lower y than its target, and promptly proceeds to increment its position by
targetPosition[1] - (self.y // self.FPS)
which is of course equal to 550, so it immediately snaps to the bottom of the screen.
Then during the next draw loop, get_mode() comes along and says 'okay, I'm at (0, 550), so I'll go ahead and set the mode to 0'. Then get_target() comes along and says 'okay, I'm in mode 0, let's go over to (0, 550).
And then this happens again during the next draw loop, and the next, and the next ... So of course your ball doesn't go anywhere.
You'll need to do a couple of things to fix your example:
Fix your target positions in get_target(). Right now they're targeting the same points where the transitions that trigger those modes happen, so your ball won't go anywhere.
Consider your velocity statements more carefully: right now they'll behave somewhat strangely. One way to do this properly is to determine (dx, dy) - that is, the absolute vector from you to your destination - and then normalize this vector such that it points in the same direction but has a magnitude equal to your desired speed. This approach will work for any target position you want.
To elaborate on the second point:
Suppose we're at (x, y) and we're trying to get to (target_x, target_y).
Let dx = target_x - x, dy = target_y - y. This should be uncontroversial: we're just taking the difference.
Then we remember the Pythagorean theorem: given a right triangle with sides a, b, c and hypotenuse c, we recall that len(c)**2 == len(a)**2 + len(b)**2. It's the same thing with vectors: the length of a vector (x, y) is the hypotenuse of a right triangle with side lengths x and y. You can draw this on a piece of paper if you want to prove this to yourself.
Given that, we can find the length of (dx, dy): it's just L(dx, dy) = sqrt(dx*dx + dy*dy). This lends itself to a curious observation: if we multiply both dx and dy by a scalar k, we also multiply the length by k, since sqrt(dx*k*dx*k + dy*k*dy*k) == sqrt(k*k*(dx*dx + dy*dy)) == k*sqrt(dx*dx + dy*dy).
It follows that we can find a vector parallel to (dx, dy), but of length 1, by dividing both dx and dy by L(dx, dy). Precompute L to avoid some potential issues. Multiply this new vector by whatever you want your speed to be: this is your desired velocity.

Add collision detection to sprite, pyGame

bassically im trying to add collision detection to the sprite below, using the following:
self.rect = bounds_rect
collide = pygame.sprite.spritecollide(self, wall_list, False)
if collide:
# yes
print("collide")
However it seems that when the collide is triggered it continuously prints 'collide' over and over when instead i want them to simply not be able to walk through the object, any help?
def update(self, time_passed):
""" Update the creep.
time_passed:
The time passed (in ms) since the previous update.
"""
if self.state == Creep.ALIVE:
# Maybe it's time to change the direction ?
#
self._change_direction(time_passed)
# Make the creep point in the correct direction.
# Since our direction vector is in screen coordinates
# (i.e. right bottom is 1, 1), and rotate() rotates
# counter-clockwise, the angle must be inverted to
# work correctly.
#
self.image = pygame.transform.rotate(
self.base_image, -self.direction.angle)
# Compute and apply the displacement to the position
# vector. The displacement is a vector, having the angle
# of self.direction (which is normalized to not affect
# the magnitude of the displacement)
#
displacement = vec2d(
self.direction.x * self.speed * time_passed,
self.direction.y * self.speed * time_passed)
self.pos += displacement
# When the image is rotated, its size is changed.
# We must take the size into account for detecting
# collisions with the walls.
#
self.image_w, self.image_h = self.image.get_size()
global bounds_rect
bounds_rect = self.field.inflate(
-self.image_w, -self.image_h)
if self.pos.x < bounds_rect.left:
self.pos.x = bounds_rect.left
self.direction.x *= -1
elif self.pos.x > bounds_rect.right:
self.pos.x = bounds_rect.right
self.direction.x *= -1
elif self.pos.y < bounds_rect.top:
self.pos.y = bounds_rect.top
self.direction.y *= -1
elif self.pos.y > bounds_rect.bottom:
self.pos.y = bounds_rect.bottom
self.direction.y *= -1
self.rect = bounds_rect
collide = pygame.sprite.spritecollide(self, wall_list, False)
if collide:
# yes
print("collide")
elif self.state == Creep.EXPLODING:
if self.explode_animation.active:
self.explode_animation.update(time_passed)
else:
self.state = Creep.DEAD
self.kill()
elif self.state == Creep.DEAD:
pass
#------------------ PRIVATE PARTS ------------------#
# States the creep can be in.
#
# ALIVE: The creep is roaming around the screen
# EXPLODING:
# The creep is now exploding, just a moment before dying.
# DEAD: The creep is dead and inactive
#
(ALIVE, EXPLODING, DEAD) = range(3)
_counter = 0
def _change_direction(self, time_passed):
""" Turn by 45 degrees in a random direction once per
0.4 to 0.5 seconds.
"""
self._counter += time_passed
if self._counter > randint(400, 500):
self.direction.rotate(45 * randint(-1, 1))
self._counter = 0
def _point_is_inside(self, point):
""" Is the point (given as a vec2d) inside our creep's
body?
"""
img_point = point - vec2d(
int(self.pos.x - self.image_w / 2),
int(self.pos.y - self.image_h / 2))
try:
pix = self.image.get_at(img_point)
return pix[3] > 0
except IndexError:
return False
def _decrease_health(self, n):
""" Decrease my health by n (or to 0, if it's currently
less than n)
"""
self.health = max(0, self.health - n)
if self.health == 0:
self._explode()
def _explode(self):
""" Starts the explosion animation that ends the Creep's
life.
"""
self.state = Creep.EXPLODING
pos = ( self.pos.x - self.explosion_images[0].get_width() / 2,
self.pos.y - self.explosion_images[0].get_height() / 2)
self.explode_animation = SimpleAnimation(
self.screen, pos, self.explosion_images,
100, 300)
global remainingCreeps
remainingCreeps-=1
if remainingCreeps == 0:
print("all dead")
A check for collision is only a check to see if two rectangular sprites have a common area.
There isn't a built in collision that unables player input during collision. You have to write that yourself.
You should probably want to change the player coordinates when a collision takes place. An example:
Let's say we play mario. When the state of mario is JUMPING check for collision. Somewhere we will store the speed of mario in the y axis. When the collision returns True, with any of the blocks, we now set the speed to 0, and the y to the top/bottom of a block. If it will be the bottom, we still keep JUMPING, so it can fall back to the ground.
My tip for the creeper is to have some oldx and oldy value, to return to when the collision takes place. That way the creeper will never go into a wall. Another approach would be to simply change the direction when a collision takes place, but that may not always work.

Categories

Resources