Reputation: 3392
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)
Upvotes: 1
Views: 2725
Reputation: 20438
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 rect
s 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()
Upvotes: 1