ScalaBoy
ScalaBoy

Reputation: 3392

How to find the distance between sprites?

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

Answers (1)

skrx
skrx

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

Upvotes: 1

Related Questions