How to find the distance between sprites? - python

I want to check when the worker object comes closely to any of the fence objects in order to print a notification message. By "closely" I mean the distance of 20 pixels between the worker and the border of fence. For example, something like "distances_list = worker.get_dist(fences)", where "distances_list" would contain the current distances to all fences.
I could adopt the approach proposed here, but maybe there is some built-in function for my task?
In pygame we can use different collision detection functions, but in the case described above there is no collision. Is there any built-in function to find the distance between the sprites?
import pygame, random
import sys
WHITE = (255, 255, 255)
GREEN = (20, 255, 140)
GREY = (210, 210 ,210)
RED = (255, 0, 0)
PURPLE = (255, 0, 255)
SCREENWIDTH=1000
SCREENHEIGHT=578
IMG_BACKGROUND = "background.jpg"
IMG_WORKER_RUNNING = "images/workers/worker_1.png"
IMG_WORKER_IDLE = "images/workers/worker_2.png"
IMG_WORKER_ACCIDENT = "images/workers/accident.png"
class Background(pygame.sprite.Sprite):
def __init__(self, image_file, location, *groups):
# we set a _layer attribute before adding this sprite to the sprite groups
# we want the background to be actually in the back
self._layer = -1
pygame.sprite.Sprite.__init__(self, groups)
# let's resize the background image now and only once
self.image = pygame.transform.scale(pygame.image.load(image_file).convert(), (SCREENWIDTH, SCREENHEIGHT))
self.rect = self.image.get_rect(topleft=location)
class GeoFenceInfluenceZone(pygame.sprite.Sprite):
def __init__(self, rect, *groups):
# we set a _layer attribute before adding this sprite to the sprite groups
self._layer = 0
pygame.sprite.Sprite.__init__(self, groups)
self.image = pygame.surface.Surface((rect.width, rect.height))
self.image.fill(GREY)
self.rect = rect
class GeoFence(pygame.sprite.Sprite):
def __init__(self, rect, risk_level, *groups):
# we set a _layer attribute before adding this sprite to the sprite groups
self._layer = 1
pygame.sprite.Sprite.__init__(self, groups)
self.image = pygame.surface.Surface((rect.width, rect.height))
self.image.fill(GREEN)
self.rect = rect
self.risk_level = risk_level
self.font = pygame.font.SysFont('Arial', 20)
text = self.font.render(risk_level, 1, (255,0,0), GREEN)
text_rect = text.get_rect(center=(rect.width/2, rect.height/2))
self.image.blit(text, text_rect)
class Worker(pygame.sprite.Sprite):
# we introduce to possible states: RUNNING and IDLE
RUNNING = 0
IDLE = 1
ACCIDENT = 2
NUMBER_OF_ACCIDENTS = 0
def __init__(self, image_running, image_idle, image_accident, location, *groups):
self.font = pygame.font.SysFont('Arial', 10)
# each state has it's own image
self.images = {
Worker.RUNNING: pygame.transform.scale(get_image(image_running), (45, 45)),
Worker.IDLE: pygame.transform.scale(get_image(image_idle), (20, 45)),
Worker.ACCIDENT: pygame.transform.scale(get_image(image_accident), (40, 40))
}
# we set a _layer attribute before adding this sprite to the sprite groups
# we want the workers on top
self._layer = 2
pygame.sprite.Sprite.__init__(self, groups)
# let's keep track of the state and how long we are in this state already
self.state = Worker.IDLE
self.ticks_in_state = 0
self.image = self.images[self.state]
self.rect = self.image.get_rect(topleft=location)
self.direction = pygame.math.Vector2(0, 0)
self.speed = random.randint(1, 3)
self.set_random_direction()
def set_random_direction(self):
# random new direction or standing still
vec = pygame.math.Vector2(random.randint(-100,100), random.randint(-100,100)) if random.randint(0, 5) > 1 else pygame.math.Vector2(0, 0)
# check the new vector and decide if we are running or fooling around
length = vec.length()
speed = sum(abs(int(v)) for v in vec.normalize() * self.speed) if length > 0 else 0
if (length == 0 or speed == 0) and (self.state != Worker.ACCIDENT):
new_state = Worker.IDLE
self.direction = pygame.math.Vector2(0, 0)
elif self.state != Worker.ACCIDENT:
new_state = Worker.RUNNING
self.direction = vec.normalize()
else:
new_state = Worker.ACCIDENT
self.ticks_in_state = 0
self.state = new_state
# use the right image for the current state
self.image = self.images[self.state]
def update(self, screen):
self.ticks_in_state += 1
# the longer we are in a certain state, the more likely is we change direction
if random.randint(0, self.ticks_in_state) > 70:
self.set_random_direction()
# now let's multiply our direction with our speed and move the rect
vec = [int(v) for v in self.direction * self.speed]
self.rect.move_ip(*vec)
# if we're going outside the screen, change direction
if not screen.get_rect().contains(self.rect):
self.direction = self.direction * -1
# spritecollide returns a list of all sprites in the group that collide with
# the given sprite, but if the sprite is in this group itself, we have
# to ignore a collision with itself
if any(s for s in pygame.sprite.spritecollide(self, building_materials, False) if s != self):
self.direction = self.direction * -1
if any(s for s in pygame.sprite.spritecollide(self, machines, False) if s != self):
self.direction = self.direction * -1
# Risk handling
self.handle_risks()
self.rect.clamp_ip(screen.get_rect())
def handle_risks(self):
for s in pygame.sprite.spritecollide(self, fences, False):
if s != self:
self.speed = 0
self.state = Worker.ACCIDENT
self.image = self.images[self.state]
Worker.NUMBER_OF_ACCIDENTS += 1
class BuildingMaterials(pygame.sprite.Sprite):
def __init__(self, image_file, location, *groups):
# we set a _layer attribute before adding this sprite to the sprite groups
self._layer = 2
pygame.sprite.Sprite.__init__(self, groups)
self.image = pygame.transform.scale(pygame.image.load(image_file).convert_alpha(), (40, 40))
self.rect = self.image.get_rect(topleft=location)
class Excavator(pygame.sprite.Sprite):
def __init__(self, image_file, location, *groups):
# we set a _layer attribute before adding this sprite to the sprite groups
self._layer = 3
pygame.sprite.Sprite.__init__(self, groups)
self.image = pygame.transform.scale(pygame.image.load(image_file).convert_alpha(), (170, 170))
self.rect = self.image.get_rect(topleft=location)
image_cache = {}
def get_image(key):
if not key in image_cache:
image_cache[key] = pygame.image.load(key)
return image_cache[key]
pygame.init()
# currently, one group would be enough
# but if you want to use some collision handling in the future
# it's best to group all sprites into special groups (no pun intended)
all_sprites = pygame.sprite.LayeredUpdates()
workers = pygame.sprite.Group()
building_materials = pygame.sprite.Group()
fences = pygame.sprite.Group()
fences_infl_zones = pygame.sprite.Group()
screen = pygame.display.set_mode((SCREENWIDTH, SCREENHEIGHT))
pygame.display.set_caption("TEST")
# create multiple workers
for pos in ((30,30), (50, 400), (200, 100), (700, 200)):
Worker(IMG_WORKER_RUNNING, IMG_WORKER_IDLE, IMG_WORKER_ACCIDENT, pos, all_sprites, workers, building_materials, machines, fences)
# create multiple building material stocks
for pos in ((50,460),(50,500),(100,500),(850,30),(800,30)):
BuildingMaterials("images/materials/building_blocks{}.png".format(random.randint(1,3)), pos, all_sprites, building_materials)
# create multiple geo-fences
risks = ["H","M","L"]
for rect in (pygame.Rect(510,150,75,52), pygame.Rect(450,250,68,40), pygame.Rect(450,370,68,48),
pygame.Rect(0,0,20,SCREENHEIGHT),pygame.Rect(0,0,SCREENWIDTH,20),
pygame.Rect(SCREENWIDTH-20,0,20,SCREENHEIGHT),pygame.Rect(0,SCREENHEIGHT-20,SCREENWIDTH,20)):
risk = risks[random.randint(0,2)]
GeoFence(rect, risk, all_sprites, fences)
# create influence zones for all geo-fences
for rect in (pygame.Rect(495,135,105,80), pygame.Rect(435,235,98,68), pygame.Rect(435,355,98,76)):
GeoFenceInfluenceZone(rect, all_sprites, fences_infl_zones)
# and the background
Background(IMG_BACKGROUND, [0,0], all_sprites)
carryOn = True
clock = pygame.time.Clock()
while carryOn:
for event in pygame.event.get():
if event.type==pygame.QUIT:
carryOn = False
pygame.display.quit()
pygame.quit()
quit()
all_sprites.update(screen)
all_sprites.draw(screen)
pygame.display.flip()
clock.tick(20)

Here's this solution ported to pygame. The y-axis in pygame is flipped, so I had to swap the top and bottom variables and the distance can be calculated with math.hypot. You need to pass the rects of the two sprites to rect_distance where they will be unpacked into the x1, y1 (top left) and x1b, y1b (bottom right) variables. (You can see the distance in the window title.)
import math
import pygame as pg
class Player(pg.sprite.Sprite):
def __init__(self, pos, *groups):
super().__init__(*groups)
self.image = pg.Surface((30, 50))
self.image.fill(pg.Color('dodgerblue1'))
self.rect = self.image.get_rect(topleft=pos)
def rect_distance(rect1, rect2):
x1, y1 = rect1.topleft
x1b, y1b = rect1.bottomright
x2, y2 = rect2.topleft
x2b, y2b = rect2.bottomright
left = x2b < x1
right = x1b < x2
top = y2b < y1
bottom = y1b < y2
if bottom and left:
print('bottom left')
return math.hypot(x2b-x1, y2-y1b)
elif left and top:
print('top left')
return math.hypot(x2b-x1, y2b-y1)
elif top and right:
print('top right')
return math.hypot(x2-x1b, y2b-y1)
elif right and bottom:
print('bottom right')
return math.hypot(x2-x1b, y2-y1b)
elif left:
print('left')
return x1 - x2b
elif right:
print('right')
return x2 - x1b
elif top:
print('top')
return y1 - y2b
elif bottom:
print('bottom')
return y2 - y1b
else: # rectangles intersect
print('intersection')
return 0.
def main():
screen = pg.display.set_mode((640, 480))
clock = pg.time.Clock()
all_sprites = pg.sprite.Group()
player = Player((50, 80), all_sprites)
player2 = Player((100, 200), all_sprites)
done = False
while not done:
for event in pg.event.get():
if event.type == pg.QUIT:
done = True
elif event.type == pg.MOUSEMOTION:
player.rect.topleft = event.pos
distance = rect_distance(player.rect, player2.rect)
pg.display.set_caption(str(distance))
all_sprites.update()
screen.fill((30, 30, 30))
all_sprites.draw(screen)
pg.display.flip()
clock.tick(60)
if __name__ == '__main__':
pg.init()
main()
pg.quit()

Related

Pygame mask collision only putting damage on base on first collision

I am writing a simple invaders game. To add damage to the bases I figured I could blit a small, black surface on the base at bullet impact, and use a mask to check if the bullet was on the damage or the base, but it isn't working and I feel I am misunderstanding the mask. The first collision is detected but after that it also detects a collision but doesn't put any more damage on the base. I thought because the surface was black the base mask wouldn't include it, but it isn't working. Here is a short test to demo this. Press space (or any key) to fire a bullet at the base. I thought maybe I should generate a new mask for the base but that doesn't work. The mask collide is from the pygame sprite code on github.
import sys, pygame, random
from pygame.locals import *
screenwidth = 600
screenheight = 400
pygame.init()
screen = pygame.display.set_mode((screenwidth, screenheight))
pygame.display.set_caption("shoot 'em up")
screenrect = screen.get_rect()
black = (0, 0, 0)
blue = (10, 10, 255)
yellow = (238, 238, 0)
base_width = 80
base_height = 40
bullet_width = 3
bullet_height = 10
class Bullet(pygame.Surface):
def __init__(self, point):
super().__init__((bullet_width, bullet_height), pygame.SRCALPHA)
self.rect = self.get_rect()
self.rect.midbottom = point
self.fill(yellow)
self.velocity = -5
self.alive = True
self.mask = pygame.mask.from_surface(self)
def update(self):
self.rect.top += self.velocity
def draw(self, surf):
surf.blit(self, self.rect)
class Base(pygame.Surface):
def __init__(self, x, y, colour):
super().__init__((base_width, base_height), pygame.SRCALPHA)
self.rect = self.get_rect()
self.rect.x = x
self.rect.y = y
self.fill(colour)
self.alive = True
def add_damage(self, bullet):
width = random.randint(3, 6)
height = random.randint(8, 12)
damage = pygame.Surface((width, height), pygame.SRCALPHA)
damage.fill(black)
rect = damage.get_rect()
rect.x = bullet.rect.x - self.rect.x
rect.y = bullet.rect.top - self.rect.top
self.blit(damage, rect)
#self.mask = pygame.mask.from_surface(self)
def draw(self, surf):
surf.blit(self, self.rect)
class Test(pygame.Surface):
def __init__(self):
super().__init__((600, 400))
self. base = Base(50, 300, blue)
self.bullets = []
def run(self):
while 1:
self.get_events()
self.update()
self.draw()
def get_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
if event.type == pygame.KEYDOWN:
bullet = Bullet((60, 380))
self.bullets.append(bullet)
def update(self):
if self.bullets:
for bullet in self.bullets:
bullet.update()
self.collision_check(bullet)
for bullet in self.bullets:
if not bullet.alive:
self.bullets.remove(bullet)
def collision_check(self, bullet):
if bullet.rect.colliderect(self.base):
if self.collide_mask(bullet, self.base):
print("collide")
self.base.add_damage(bullet)
bullet.alive = False
def collide_mask(self, left, right):
xoffset = right.rect[0] - left.rect[0]
yoffset = right.rect[1] - left.rect[1]
try:
leftmask = left.mask
except AttributeError:
leftmask = pygame.mask.from_surface(left)
try:
rightmask = right.mask
except AttributeError:
rightmask = pygame.mask.from_surface(right)
return leftmask.overlap(rightmask, (xoffset, yoffset))
def draw(self):
self.fill(black)
self.base.draw(self)
for bullet in self.bullets:
bullet.draw(self)
screen.blit(self, (0,0))
pygame.display.flip()
if __name__=="__main__":
t = Test()
t.run()
As you can see this is not using pygame sprites.
if the pygame.Surface object is changed you need to recreate the mask with pygame.mask.from_surface. However, the mask is generated form the Surface's alpha channel. Therefore, you need to make the damaged area transparent. Create a completely transparent rectangle (RGBA = 0, 0, 0, 0) and blit the rectangle using the special flag BLEND_RGBA_MULT (or BLEND_RGBA_MIN). Finally recreate the mask:
damage = pygame.Surface((width, height), pygame.SRCALPHA)
self.blit(damage, rect, special_flags=pygame.BLEND_RGBA_MULT)
self.mask = pygame.mask.from_surface(self)
add_damage Mehtod:
class Base(pygame.Surface):
# [...]
def add_damage(self, bullet):
width = random.randint(3, 6)
height = random.randint(8, 12)
damage = pygame.Surface((width, height), pygame.SRCALPHA)
rect = damage.get_rect()
rect.x = bullet.rect.x - self.rect.x
rect.y = bullet.rect.top - self.rect.top
self.blit(damage, rect, special_flags=pygame.BLEND_RGBA_MULT)
self.mask = pygame.mask.from_surface(self)

Trying to switch bullet patterns in this game

I'm trying to make a bullet-hell style game and I've almost got it to work except the opponent won't change from shooting one bullet pattern to another.
It's supposed to shoot 3 blue bullets 8 times, then switch to shooting 2 purple bullets 8 times. There's a sequence of patterns but I've only got two.
So it should iterate through each pattern every time the current pattern shoots a certain amount of times. When all the patterns are done it should stop shooting completely.
I've seen people try to make these but it's always java and I'm on python.
The code is very long but I can't cut it down any more. The original is in multiple files but I've put it into one script. It's virtually impossible to simplify.
import sys
import time
import itertools
import pygame
from pygame.locals import *
class Player(pygame.sprite.Sprite):
#this sprite variable is a placeholder
sprite = pygame.image.load("Sprites/player.png")
def __init__(self, *groups):
super().__init__(*groups)
self.image = Player.sprite
self.rect = self.image.get_rect(topleft=(445, 550))
self.pos = pygame.Vector2(self.rect.topleft)
def update(self):
key = pygame.key.get_pressed()
dist = 3
if key[pygame.K_DOWN]:
self.rect.y += dist
elif key[pygame.K_UP]:
self.rect.y -= dist
if key[pygame.K_RIGHT]:
self.rect.x += dist
elif key[pygame.K_LEFT]:
self.rect.x -= dist
class Spell:
def __init__(self, bullet, pattern, speed, loop, tick_delay):
self.bullet = bullet
self.pattern = pattern
self.speed = speed
self.loop = loop
self.tick_delay = tick_delay
class Opponent(pygame.sprite.Sprite):
def __init__(self, sprite, sequence, *groups):
super().__init__(*groups)
self.image = sprite
self.rect = self.image.get_rect(topleft=(425, 30))
self.pos = pygame.Vector2(self.rect.topleft)
self.path = itertools.cycle((self.rect.topleft, ))
self.next_point = pygame.Vector2(next(self.path))
self.speed = 1
self.ticks = 1000
self.queue = []
self.sequence = sequence
self.spellno = 0
self.currentspell = sequence[self.spellno]
def update(self):
#this function basically does most of the stuff in this class
move = self.next_point - self.pos
move_length = move.length()
if move_length != 0:
move.normalize_ip()
move = move * self.speed
self.pos += move
#later on down the line i want to make the opponent sprite move
if move.length() == 0 or move_length < self.speed:
self.next_point = pygame.Vector2(next(self.path))
self.rect.topleft = self.pos
for i in range(0, self.currentspell.loop):
if pygame.time.get_ticks() - self.ticks > self.currentspell.tick_delay:
self.ticks = pygame.time.get_ticks()
self.shoot()
time_gone = pygame.time.get_ticks() - self.ticks
for bullet in self.queue:
if bullet[0] <= time_gone:
Bullet(self.rect.center, bullet[1], self.currentspell.bullet, sprites, bullets)
self.queue = [bullet for bullet in self.queue if bullet[0] > time_gone]
return
def shoot(self):
pattern = self.currentspell.pattern
self.queue = pattern
class Bullet(pygame.sprite.Sprite):
def __init__(self, pos, direction, image, *groups):
super().__init__(*groups)
self.image = image
self.rect = self.image.get_rect(topleft=pos)
self.direction = direction
self.pos = pygame.Vector2(self.rect.topleft)
def update(self):
self.pos += self.direction
self.rect.topleft = (self.pos.x, self.pos.y)
if not screen.get_rect().colliderect(self.rect):
self.kill()
sprites = pygame.sprite.Group()
bullets = pygame.sprite.Group()
opponentgroup = pygame.sprite.Group()
mi1 = Spell(pygame.image.load("Sprites/lightblue-glowey.png"),(
(0, pygame.Vector2(-0.5, 1) * 4),
(0, pygame.Vector2(0, 1) * 4),
(0, pygame.Vector2(0.5, 1) * 4)),
10, 8, 340
)
mi2 = Spell(pygame.image.load("Sprites/purple-glowey.png"),(
(0, pygame.Vector2(1, 1) * 4),
(0, pygame.Vector2(-1, 1) * 4)),
4, 8, 340
)
minty_spells = [mi1, mi2]
player = Player(sprites)
Minty = Opponent(pygame.image.load("Sprites/minty.png"), minty_spells, opponentgroup)
opponents = [Minty]
pygame.init()
SCREENWIDTH = 1000
SCREENHEIGHT = 650
screen = pygame.display.set_mode([SCREENWIDTH, SCREENHEIGHT])
screen.fill((255, 123, 67))
pygame.draw.rect(screen, (0, 255, 188), (50, 50, 900, 575), 0)
background = screen.copy()
clock = pygame.time.Clock()
currentopponent = 0
def closegame():
pygame.quit()
return
def stage(opponent, background, bgm):
currentopponent = opponent
for spell in opponents[opponent].sequence:
op = opponents[opponent]
op.update()
op.spellno += 1
def main():
running = True
while running:
for events in pygame.event.get():
if events.type == pygame.QUIT:
pygame.quit()
return
# update all sprites
sprites.update()
sprites.add(opponents[currentopponent])
# draw everything
screen.blit(background, (0, 0))
stage(0, "", "") # "" means placeholder. i'm working on them
sprites.draw(screen)
pygame.display.update()
clock.tick(100)
if __name__ == '__main__':
main()
Original Code and assets on my GitHub: https://github.com/E-Lee-Za/Eleeza-Crafter-The-Game
Here's a working (and simplified) version of your code. The loop attribute of the current spell gets decremented every time when the bullets are created. When loop is 0,
the self.spellno is incremented and the spell gets changed, otherwise if the spellno is >= len(self.sequence), self.currentspell gets set to None so that it stops shooting (just add if self.currentspell is not None to the conditional statement).
import pygame
class Spell:
def __init__(self, bullet, pattern, speed, loop, tick_delay):
self.bullet = bullet
self.pattern = pattern
self.speed = speed
self.loop = loop
self.tick_delay = tick_delay
class Opponent(pygame.sprite.Sprite):
def __init__(self, sprite, sequence, *groups):
super().__init__(*groups)
self.image = sprite
self.rect = self.image.get_rect(topleft=(425, 30))
self.start_time = pygame.time.get_ticks()
self.sequence = sequence
self.spellno = 0
self.currentspell = sequence[self.spellno]
def update(self):
time_gone = pygame.time.get_ticks() - self.start_time
# Only create bullets if self.currentspell is not None.
if self.currentspell is not None and time_gone > self.currentspell.tick_delay:
self.start_time = pygame.time.get_ticks()
for bullet in self.currentspell.pattern:
if bullet[0] <= time_gone:
Bullet(self.rect.center, bullet[1], self.currentspell.bullet, sprites, bullets)
# Decrement the loop attribute of the current spell and
# switch to the next spell when it's <= 0. When all spells
# are done, set self.currentspell to None to stop shooting.
self.currentspell.loop -= 1
if self.currentspell.loop <= 0:
self.spellno += 1
if self.spellno >= len(self.sequence):
self.currentspell = None
else:
self.currentspell = self.sequence[self.spellno]
class Bullet(pygame.sprite.Sprite):
def __init__(self, pos, direction, image, *groups):
super().__init__(*groups)
self.image = image
self.rect = self.image.get_rect(topleft=pos)
self.direction = direction
self.pos = pygame.Vector2(self.rect.topleft)
def update(self):
self.pos += self.direction
self.rect.topleft = (self.pos.x, self.pos.y)
if not screen.get_rect().colliderect(self.rect):
self.kill()
sprites = pygame.sprite.Group()
bullets = pygame.sprite.Group()
opponentgroup = pygame.sprite.Group()
img = pygame.Surface((30, 40))
img.fill((0, 100, 200))
mi1 = Spell(
img,
((0, pygame.Vector2(-0.5, 1) * 4), (0, pygame.Vector2(0, 1) * 4),
(0, pygame.Vector2(0.5, 1) * 4)),
10, 8, 340
)
img2 = pygame.Surface((30, 30))
img2.fill((110, 0, 220))
mi2 = Spell(
img2,
((0, pygame.Vector2(1, 1) * 4), (0, pygame.Vector2(-1, 1) * 4)),
4, 8, 340
)
minty_spells = [mi1, mi2]
img3 = pygame.Surface((30, 50))
img3.fill((220, 0, 200))
Minty = Opponent(img3, minty_spells, opponentgroup)
sprites.add(Minty)
pygame.init()
SCREENWIDTH = 1000
SCREENHEIGHT = 650
screen = pygame.display.set_mode([SCREENWIDTH, SCREENHEIGHT])
screen.fill((255, 123, 67))
pygame.draw.rect(screen, (0, 255, 188), (50, 50, 900, 575), 0)
background = screen.copy()
clock = pygame.time.Clock()
def main():
while True:
for events in pygame.event.get():
if events.type == pygame.QUIT:
pygame.quit()
return
sprites.update()
screen.blit(background, (0, 0))
sprites.draw(screen)
pygame.display.update()
clock.tick(100)
if __name__ == '__main__':
main()

How to animate multiple objects of the same class?

I want to have 3 workers (objects of the class Worker) that move on the screen in different random directions and with a different speed. In other words, I just want to run something like this: worker1.makeRandomStep(x,y,1), worker2.makeRandomStep(x,y,2) and worker1.makeRandomStep(x,y,3).
This is my current code in which I have only one worker:
import pygame, random
import sys
WHITE = (255, 255, 255)
GREEN = (20, 255, 140)
GREY = (210, 210 ,210)
RED = (255, 0, 0)
PURPLE = (255, 0, 255)
SCREENWIDTH=1000
SCREENHEIGHT=578
class Background(pygame.sprite.Sprite):
def __init__(self, image_file, location):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.image.load(image_file)
self.rect = self.image.get_rect()
self.rect.left, self.rect.top = location
class Worker(pygame.sprite.Sprite):
def __init__(self, image_file, location):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.image.load(image_file)
self.rect = self.image.get_rect()
self.direction = 2
self.rect.left, self.rect.top = location
def makeRandomStep(self,x,y,step):
# there is a less than 1% chance every time that direction is changed
if random.uniform(0,1)<0.005:
self.direction = random.randint(1,4)
if self.direction == 1:
return x, y+step # up
elif self.direction == 2:
return x+step, y # right
elif self.direction == 3:
return x, y-step # down
elif self.direction == 4:
return x-step, y # left
else:
return x, y # stop
pygame.init()
size = (SCREENWIDTH, SCREENHEIGHT)
screen = pygame.display.set_mode(size)
screen_rect=screen.get_rect()
pygame.display.set_caption("TEST")
worker = Worker("worker.png", [0,0])
w_x = worker.rect.left
w_y = worker.rect.top
bg = Background("background.jpg", [0,0])
#sprite_group = pygame.sprite.Group()
#sprite_group.add(worker)
carryOn = True
clock=pygame.time.Clock()
while carryOn:
for event in pygame.event.get():
if event.type==pygame.QUIT:
carryOn=False
pygame.display.quit()
pygame.quit()
quit()
# Draw floor layout
screen.blit(pygame.transform.scale(bg.image, (SCREENWIDTH, SCREENHEIGHT)), bg.rect)
# Draw geo-fences
geofence1 = pygame.draw.rect(screen, GREEN, [510,150,75,52])
geofence2 = pygame.draw.rect(screen, GREEN, [450,250,68,40])
w_x,w_y = worker.makeRandomStep(w_x,w_y,1)
# Worker should not go outside the screen area
if (w_x + worker.rect.width > SCREENWIDTH): w_x = SCREENWIDTH - worker.rect.width
if (w_x < 0): w_x = 0
if (w_y + worker.rect.height > SCREENHEIGHT): w_y = SCREENHEIGHT - worker.rect.height
if (w_y < 0): w_y = 0
worker.rect.clamp_ip(screen_rect)
screen.blit(worker.image, (w_x,w_y))
# Refresh Screen
pygame.display.flip()
#sprite_group.update()
#sprite_group.draw(screen)
#Number of frames per secong e.g. 60
clock.tick(20)
pygame.display.quit()
pygame.quit()
quit()
It is not very clear to me how should I proceed to my goal. I was thinking to use sprite_group, but I misunderstand how to correctly update all sprites (workers) inside the while loop.
Any help is highly appreciated. Thanks.
You have to move all worker logic into the Worker class (setting random values, updating position etc). Then create multiple instances.
Here's a running example. Note the comments for explanations:
import pygame, random
import sys
WHITE = (255, 255, 255)
GREEN = (20, 255, 140)
GREY = (210, 210 ,210)
RED = (255, 0, 0)
PURPLE = (255, 0, 255)
SCREENWIDTH=1000
SCREENHEIGHT=578
class Background(pygame.sprite.Sprite):
def __init__(self, image_file, location, *groups):
# we set a _layer attribute before adding this sprite to the sprite groups
# we want the background to be actually in the back
self._layer = -1
pygame.sprite.Sprite.__init__(self, groups)
# let's resize the background image now and only once
# always call convert() (or convert_alpha) after loading an image
# so the surface will have to correct pixel format
self.image = pygame.transform.scale(pygame.image.load(image_file).convert(), (SCREENWIDTH, SCREENHEIGHT))
self.rect = self.image.get_rect(topleft=location)
# we do everthing with sprites now, because that will make our live easier
class GeoFence(pygame.sprite.Sprite):
def __init__(self, rect, *groups):
# we set a _layer attribute before adding this sprite to the sprite groups
self._layer = 0
pygame.sprite.Sprite.__init__(self, groups)
self.image = pygame.surface.Surface((rect.width, rect.height))
self.image.fill(GREEN)
self.rect = rect
class Worker(pygame.sprite.Sprite):
def __init__(self, image_file, location, *groups):
# we set a _layer attribute before adding this sprite to the sprite groups
# we want the workers on top
self._layer = 1
pygame.sprite.Sprite.__init__(self, groups)
self.image = pygame.transform.scale(pygame.image.load(image_file).convert_alpha(), (40, 40))
self.rect = self.image.get_rect(topleft=location)
# let's call this handy function to set a random direction for the worker
self.change_direction()
# speed is also random
self.speed = random.randint(2, 4)
def change_direction(self):
# let's create a random vector as direction, so we can move in every direction
self.direction = pygame.math.Vector2(random.randint(-100, 100), random.randint(-100, 100))
# we don't want a vector of length 0, because we want to actually move
# it's not enough to account for rounding errors, but let's ignore that for now
while self.direction.length() == 0:
self.direction = pygame.math.Vector2(random.randint(-100, 100), random.randint(-100, 100))
# always normalize the vector, so we always move at a constant speed at all directions
self.direction = self.direction.normalize()
def update(self, screen):
# there is a less than 1% chance every time that direction is changed
if random.uniform(0,1)<0.005:
self.change_direction()
# now let's multiply our direction with our speed and move the rect
vec = [int(v) for v in self.direction * self.speed]
self.rect.move_ip(*vec)
# if we're going outside the screen, move back and change direction
if not screen.get_rect().contains(self.rect):
self.change_direction()
self.rect.clamp_ip(screen.get_rect())
pygame.init()
# currently, one group would be enough
# but if you want to use some collision handling in the future
# it's best to group all sprites into special groups (no pun intended)
all_sprites = pygame.sprite.LayeredUpdates()
workers = pygame.sprite.Group()
fences = pygame.sprite.Group()
screen = pygame.display.set_mode((SCREENWIDTH, SCREENHEIGHT))
pygame.display.set_caption("TEST")
# create multiple workers
for pos in ((0,0), (100, 100), (200, 100)):
Worker("worker.png", pos, all_sprites, workers)
# create multiple of these green thingies
for rect in (pygame.Rect(510,150,75,52), pygame.Rect(450,250,68,40)):
GeoFence(rect, all_sprites, fences)
# and the background
Background("background.jpg", [0,0], all_sprites)
carryOn = True
clock = pygame.time.Clock()
while carryOn:
for event in pygame.event.get():
if event.type==pygame.QUIT:
carryOn = False
# see how clean our main loop is
# just calling update and draw on the all_sprites group
all_sprites.update(screen)
all_sprites.draw(screen)
pygame.display.flip()
clock.tick(20)

How to find the distance (in pixels) between the Sprite and screen corners?

I need to find the distance (in pixels) between the animated worker objects and the 4 corners of the screen (upper left, upper right, lower left, lower right).
Which function of pygame gives this information? I need to get this information at each update iteration.
import pygame, random
import sys
WHITE = (255, 255, 255)
GREEN = (20, 255, 140)
GREY = (210, 210 ,210)
RED = (255, 0, 0)
PURPLE = (255, 0, 255)
SCREENWIDTH=1000
SCREENHEIGHT=578
IMG_BACKGROUND = "background.jpg"
IMG_WORKER_RUNNING = "images/workers/worker_1.png"
IMG_WORKER_IDLE = "images/workers/worker_2.png"
IMG_WORKER_ACCIDENT = "images/workers/accident.png"
class Background(pygame.sprite.Sprite):
def __init__(self, image_file, location, *groups):
# we set a _layer attribute before adding this sprite to the sprite groups
# we want the background to be actually in the back
self._layer = -1
pygame.sprite.Sprite.__init__(self, groups)
# let's resize the background image now and only once
self.image = pygame.transform.scale(pygame.image.load(image_file).convert(), (SCREENWIDTH, SCREENHEIGHT))
self.rect = self.image.get_rect(topleft=location)
class GeoFenceInfluenceZone(pygame.sprite.Sprite):
def __init__(self, rect, *groups):
# we set a _layer attribute before adding this sprite to the sprite groups
self._layer = 0
pygame.sprite.Sprite.__init__(self, groups)
self.image = pygame.surface.Surface((rect.width, rect.height))
self.image.fill(GREY)
self.rect = rect
class GeoFence(pygame.sprite.Sprite):
def __init__(self, rect, risk_level, *groups):
# we set a _layer attribute before adding this sprite to the sprite groups
self._layer = 1
pygame.sprite.Sprite.__init__(self, groups)
self.image = pygame.surface.Surface((rect.width, rect.height))
self.image.fill(GREEN)
self.rect = rect
self.risk_level = risk_level
self.font = pygame.font.SysFont('Arial', 20)
text = self.font.render(risk_level, 1, (255,0,0), GREEN)
text_rect = text.get_rect(center=(rect.width/2, rect.height/2))
self.image.blit(text, text_rect)
class Worker(pygame.sprite.Sprite):
# we introduce to possible states: RUNNING and IDLE
RUNNING = 0
IDLE = 1
ACCIDENT = 2
NUMBER_OF_ACCIDENTS = 0
def __init__(self, image_running, image_idle, image_accident, location, *groups):
self.font = pygame.font.SysFont('Arial', 10)
# each state has it's own image
self.images = {
Worker.RUNNING: pygame.transform.scale(get_image(image_running), (45, 45)),
Worker.IDLE: pygame.transform.scale(get_image(image_idle), (20, 45)),
Worker.ACCIDENT: pygame.transform.scale(get_image(image_accident), (40, 40))
}
# we set a _layer attribute before adding this sprite to the sprite groups
# we want the workers on top
self._layer = 2
pygame.sprite.Sprite.__init__(self, groups)
# let's keep track of the state and how long we are in this state already
self.state = Worker.IDLE
self.ticks_in_state = 0
self.image = self.images[self.state]
self.rect = self.image.get_rect(topleft=location)
self.direction = pygame.math.Vector2(0, 0)
self.speed = random.randint(1, 3)
self.set_random_direction()
def set_random_direction(self):
# random new direction or standing still
vec = pygame.math.Vector2(random.randint(-100,100), random.randint(-100,100)) if random.randint(0, 5) > 1 else pygame.math.Vector2(0, 0)
# check the new vector and decide if we are running or fooling around
length = vec.length()
speed = sum(abs(int(v)) for v in vec.normalize() * self.speed) if length > 0 else 0
if (length == 0 or speed == 0) and (self.state != Worker.ACCIDENT):
new_state = Worker.IDLE
self.direction = pygame.math.Vector2(0, 0)
elif self.state != Worker.ACCIDENT:
new_state = Worker.RUNNING
self.direction = vec.normalize()
else:
new_state = Worker.ACCIDENT
self.ticks_in_state = 0
self.state = new_state
# use the right image for the current state
self.image = self.images[self.state]
def update(self, screen):
self.ticks_in_state += 1
# the longer we are in a certain state, the more likely is we change direction
if random.randint(0, self.ticks_in_state) > 70:
self.set_random_direction()
# now let's multiply our direction with our speed and move the rect
vec = [int(v) for v in self.direction * self.speed]
self.rect.move_ip(*vec)
# if we're going outside the screen, change direction
if not screen.get_rect().contains(self.rect):
self.direction = self.direction * -1
# spritecollide returns a list of all sprites in the group that collide with
# the given sprite, but if the sprite is in this group itself, we have
# to ignore a collision with itself
if any(s for s in pygame.sprite.spritecollide(self, building_materials, False) if s != self):
self.direction = self.direction * -1
if any(s for s in pygame.sprite.spritecollide(self, machines, False) if s != self):
self.direction = self.direction * -1
# Risk handling
self.handle_risks()
self.rect.clamp_ip(screen.get_rect())
def handle_risks(self):
for s in pygame.sprite.spritecollide(self, fences, False):
if s != self:
self.speed = 0
self.state = Worker.ACCIDENT
self.image = self.images[self.state]
Worker.NUMBER_OF_ACCIDENTS += 1
class BuildingMaterials(pygame.sprite.Sprite):
def __init__(self, image_file, location, *groups):
# we set a _layer attribute before adding this sprite to the sprite groups
self._layer = 2
pygame.sprite.Sprite.__init__(self, groups)
self.image = pygame.transform.scale(pygame.image.load(image_file).convert_alpha(), (40, 40))
self.rect = self.image.get_rect(topleft=location)
class Excavator(pygame.sprite.Sprite):
def __init__(self, image_file, location, *groups):
# we set a _layer attribute before adding this sprite to the sprite groups
self._layer = 3
pygame.sprite.Sprite.__init__(self, groups)
self.image = pygame.transform.scale(pygame.image.load(image_file).convert_alpha(), (170, 170))
self.rect = self.image.get_rect(topleft=location)
image_cache = {}
def get_image(key):
if not key in image_cache:
image_cache[key] = pygame.image.load(key)
return image_cache[key]
pygame.init()
# currently, one group would be enough
# but if you want to use some collision handling in the future
# it's best to group all sprites into special groups (no pun intended)
all_sprites = pygame.sprite.LayeredUpdates()
workers = pygame.sprite.Group()
building_materials = pygame.sprite.Group()
fences = pygame.sprite.Group()
fences_infl_zones = pygame.sprite.Group()
screen = pygame.display.set_mode((SCREENWIDTH, SCREENHEIGHT))
pygame.display.set_caption("TEST")
# create multiple workers
for pos in ((30,30), (50, 400), (200, 100), (700, 200)):
Worker(IMG_WORKER_RUNNING, IMG_WORKER_IDLE, IMG_WORKER_ACCIDENT, pos, all_sprites, workers, building_materials, machines, fences)
# create multiple building material stocks
for pos in ((50,460),(50,500),(100,500),(850,30),(800,30)):
BuildingMaterials("images/materials/building_blocks{}.png".format(random.randint(1,3)), pos, all_sprites, building_materials)
# create multiple geo-fences
risks = ["H","M","L"]
for rect in (pygame.Rect(510,150,75,52), pygame.Rect(450,250,68,40), pygame.Rect(450,370,68,48),
pygame.Rect(0,0,20,SCREENHEIGHT),pygame.Rect(0,0,SCREENWIDTH,20),
pygame.Rect(SCREENWIDTH-20,0,20,SCREENHEIGHT),pygame.Rect(0,SCREENHEIGHT-20,SCREENWIDTH,20)):
risk = risks[random.randint(0,2)]
GeoFence(rect, risk, all_sprites, fences)
# create influence zones for all geo-fences
for rect in (pygame.Rect(495,135,105,80), pygame.Rect(435,235,98,68), pygame.Rect(435,355,98,76)):
GeoFenceInfluenceZone(rect, all_sprites, fences_infl_zones)
# and the background
Background(IMG_BACKGROUND, [0,0], all_sprites)
carryOn = True
clock = pygame.time.Clock()
while carryOn:
for event in pygame.event.get():
if event.type==pygame.QUIT:
carryOn = False
pygame.display.quit()
pygame.quit()
quit()
all_sprites.update(screen)
all_sprites.draw(screen)
pygame.display.flip()
clock.tick(20)
You could create point vectors for the corners and then just subtract the position of the sprite (I just use the mouse pos here) to get vectors that point to the corners and finally call the length method to get the lengths of these vectors.
import pygame as pg
from pygame.math import Vector2
pg.init()
screen = pg.display.set_mode((640, 480))
width, height = screen.get_size()
clock = pg.time.Clock()
BG_COLOR = pg.Color('gray12')
# Create point vectors for the corners.
corners = [
Vector2(0, 0), Vector2(width, 0),
Vector2(0, height), Vector2(width, height),
]
done = False
while not done:
for event in pg.event.get():
if event.type == pg.QUIT:
done = True
mouse_pos = pg.mouse.get_pos()
# Subtract the position from the point vectors and call the `length`
# method of the resulting vectors to get the distances to the points.
# Subtract the position from the point vectors and call the `length`
# method of the resulting vectors to get the distances to the points.
distances = []
for vec in corners:
distance = (vec - mouse_pos).length()
distances.append(distance)
# The 4 lines above can be replaced by a list comprehension.
# distances = [(vec - mouse_pos).length() for vec in corners]
# I just display the distances in the window title here.
pg.display.set_caption('{:.1f}, {:.1f}, {:.1f}, {:.1f}'.format(*distances))
screen.fill(BG_COLOR)
pg.display.flip()
clock.tick(60)

My game keeps calling a method for the wrong sprite

When i try to run the game the code tries to run a method for the wrong sprite. I think the line "player.handle_keys()" is the problem as when i run it, it says that it can't find a "handle_keys()" method for the "meteor" class. I haven't got a line to run a "meteor.handle_keys()" as this class should not have this method.
Here is the code:
import pygame
import random
# Define some colors
BLACK = ( 0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
bg = pygame.image.load("bg1.png")
class space_ship(pygame.sprite.Sprite):
def __init__(self, color, width, height):
super().__init__()
# Create an image of the space_ship1, and fill it with a color.
# This could also be an image loaded from the disk.
self.image = pygame.Surface([width, height])
self.image.fill(WHITE)
self.image.set_colorkey(WHITE)
self.rect = self.image.get_rect()
#draw image
self.image = pygame.image.load("player1.gif").convert()
# Draw the ellipse
#pygame.draw.ellipse(self.image, color, [0, 0, width, height])
# x and y coordinates
self.x = 500
self.y = 450
def handle_keys(self):
""" Handles Keys """
key = pygame.key.get_pressed()
dist = 5 # distance moved in 1 frame
if key[pygame.K_RIGHT]: # right key
self.x += dist # move right
elif key[pygame.K_LEFT]: # left key
self.x -= dist # move left
def draw(self, surface):
""" Draw on surface """
# blit yourself at your current position
surface.blit(self.image, (self.x, self.y))
class asteroid(pygame.sprite.Sprite):
def __init__(self, color, width, height):
super().__init__()
# Create an image of the space_ship1, and fill it with a color.
# This could also be an image loaded from the disk.
self.image = pygame.Surface([width, height])
self.image.fill(WHITE)
self.image.set_colorkey(WHITE)
self.rect = self.image.get_rect()
# Draw the ellipse
#pygame.draw.ellipse(self.image, color, [0, 0, width, height])
self.image = pygame.image.load("ast1.gif").convert()
# x and y coordinates
self.x = random.randint(50,950)
self.y = 10
def draw(self, surface):
""" Draw on surface """
# blit yourself at your current position
surface.blit(self.image, (self.x, self.y))
def fall(self):
dist = 5
self.y +=dist
if self.y > 600:
self.x = random.randint(50,950)
self.y = random.randint(-2000, -10)
def respawn(self):
self.y = -10
# Initialize Pygame
pygame.init()
# Set the height and width of the screen
screen_width = 1000
screen_height = 600
screen = pygame.display.set_mode([screen_width, screen_height])
# This is a list of 'sprites.' Each sprite in the program is
# added to this list.
# The list is managed by a class called 'Group.'
asteroid_list = pygame.sprite.Group()
# This is a list of every sprite.
# All asteroids and the player as well.
all_sprites_list = pygame.sprite.Group()
player = space_ship(RED, 20, 15)
all_sprites_list.add(player)
asteroid_1 = asteroid(BLACK, 40, 40)
asteroid_list.add(asteroid_1)
all_sprites_list.add(asteroid_1)
asteroid_2 = asteroid(BLACK, 40, 40)
asteroid_list.add(asteroid_2)
all_sprites_list.add(asteroid_2)
asteroid_3 = asteroid(BLACK,40, 40)
asteroid_list.add(asteroid_3)
all_sprites_list.add(asteroid_3)
asteroid_4 = asteroid(BLACK,40, 40)
asteroid_list.add(asteroid_4)
all_sprites_list.add(asteroid_4)
asteroid_5 = asteroid(BLACK,40, 40)
asteroid_list.add(asteroid_5)
all_sprites_list.add(asteroid_5)
asteroid_6 = asteroid(BLACK,40, 40)
asteroid_list.add(asteroid_6)
all_sprites_list.add(asteroid_6)
asteroid_7 = asteroid(BLACK,40, 40)
asteroid_list.add(asteroid_7)
all_sprites_list.add(asteroid_7)
asteroid_8 = asteroid(BLACK,40, 40)
asteroid_list.add(asteroid_8)
all_sprites_list.add(asteroid_list)
# Loop until the user clicks the close button.
done = False
# Used to manage how fast the screen updates
clock = pygame.time.Clock()
score = 0
# ----------------- Main Program Loop --------------------
while not done:
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
#Call upon function
player.handle_keys()
# Clear the screen
screen.fill(WHITE)
#INSIDE OF THE GAME LOOP
screen.blit(bg, (0, 0))
# See if the player space_ship1 has collided with anything.
blocks_hit_list = pygame.sprite.spritecollide(player, asteroid_list, True)
# Check the list of collisions.
for player in blocks_hit_list:
score +=1
print(score)
# Draw all the spites
player.draw(screen)
asteroid_1.draw(screen)
asteroid_1.fall()
asteroid_2.draw(screen)
asteroid_2.fall()
asteroid_3.draw(screen)
asteroid_3.fall()
asteroid_4.draw(screen)
asteroid_4.fall()
asteroid_5.draw(screen)
asteroid_5.fall()
asteroid_6.draw(screen)
asteroid_6.fall()
asteroid_7.draw(screen)
asteroid_7.fall()
asteroid_8.draw(screen)
asteroid_8.fall()
#all_sprites_list.draw(screen)
# Limit to 60 frames per second
clock.tick(60)
# Go ahead and update the screen with what we've drawn.
pygame.display.flip()
pygame.quit()
You are overriding player in your for loop
# Check the list of collisions.
for player in blocks_hit_list:
score +=1
print(score)
change it to something else and all will be good
# Check the list of collisions.
for something_else in blocks_hit_list:
score +=1
print(score)
Enjoy

Categories