Ericson Willians
Ericson Willians

Reputation: 7845

How to detect if an object is in the "line of sight" of another object using math.atan2 function?

I'm having serious problems trying to understand how to "detect" if something is in the imaginary "line of sight" of the player. I've created a simple wall. The idea is to print something if the player is aiming at the wall and hit the mouse click button at the same time.

Here's my code:

import sys
import pygame
import math
pygame.init()

screen_width = 640
screen_height = 480
screen = pygame.display.set_mode((screen_width, screen_height))
running = True

class Actor:

    def __init__(self, x, y, w, h):
        self.x = x
        self.y = y
        self.w = w
        self.h = h

class Wall:

    def __init__(self):
        Actor.__init__(self, 300, 100, 128, 32)
        self.surface = pygame.Surface((self.w, self.h))
        self.surface.fill((0, 0, 0))

    def draw(self):
        screen.blit(self.surface, (self.x, self.y))

class Player(Actor):

    def __init__(self):
        Actor.__init__(self, 0, 0, 64, 64)
        self.surface = pygame.transform.scale(pygame.image.load("GFX/player.png"), (self.w, self.h))
        self.rotated_surface = self.surface.copy()
        self.rect = self.surface.get_rect()
        self.directions = [False, False, False, False]
        self.speed = 0.1
        self.running = False

    def rotate(self):
        mouse_x = pygame.mouse.get_pos()[0]
        mouse_y = pygame.mouse.get_pos()[1]
        angle = math.atan2(mouse_y - (self.y + (self.w / 2)), mouse_x - (self.x + (self.w / 2)))
        angle = angle * (180 / math.pi)
        rot_image = pygame.transform.rotozoom(self.surface, -angle + 270, 1)
        rot_rect = self.rect.copy()
        rot_rect.center = rot_image.get_rect().center
        self.rotated_surface = rot_image

    def move(self):
        if self.directions[0]:
            self.y -= self.speed
        if self.directions[1]:
            self.y += self.speed
        if self.directions[2]:
            self.x -= self.speed
        if self.directions[3]:
            self.x += self.speed
        if self.running:
            self.speed = 0.2
        else:
            self.speed = 0.1

    def draw(self):
        screen.blit(self.rotated_surface, (self.x, self.y))
        pygame.draw.aaline(screen, (0, 255, 0), (player.x + (player.w / 2), player.y), pygame.mouse.get_pos())

    def fire(self, actor):
        bullet_x_pos = self.x + (self.w / 2)
        bullet_y_pos = self.y
        # ...

player = Player()
wall = Wall()

def redraw():
    screen.fill((75, 0, 0))
    player.draw()
    player.move()
    wall.draw()
    pygame.display.flip()

while (running):
    for e in pygame.event.get():
        if e.type == pygame.QUIT:
            sys.exit()
        elif e.type == pygame.KEYDOWN:
            if e.key == pygame.K_ESCAPE:
                sys.exit()
            if e.key == pygame.K_w:
                player.directions[0] = True
            if e.key == pygame.K_s:
                player.directions[1] = True
            if e.key == pygame.K_a:
                player.directions[2] = True
            if e.key == pygame.K_d:
                player.directions[3] = True
            if e.key == pygame.K_LSHIFT:
                player.running = True
        elif e.type == pygame.KEYUP:
            if e.key == pygame.K_w:
                player.directions[0] = False
            if e.key == pygame.K_s:
                player.directions[1] = False
            if e.key == pygame.K_a:
                player.directions[2] = False
            if e.key == pygame.K_d:
                player.directions[3] = False
            if e.key == pygame.K_LSHIFT:
                player.running = False
        elif e.type == pygame.MOUSEMOTION:
            player.rotate()
        elif e.type == pygame.MOUSEBUTTONDOWN:
            print("Yep")

    redraw()

Here's the image:

enter image description here

There's not enough material on the web about it, especially for Python / Pygame (I guess shooters are not very common on the library). I have no idea how to return a boolean based on the angle returned from atan2, telling me that the player is aiming at the object.

Upvotes: 3

Views: 1771

Answers (1)

jsbueno
jsbueno

Reputation: 110301

Python's atan2 is kind of magical in which one does not have to play around with signedness of "x" and "y" directions in order to get a full 360 angle. Ant Python's math model even have a function ready to convert the radian result back to degrees (then, only add 360 to negative numbers to get 0:360 range instead of -180:180).

That given, with a little care on getting the positions correctly - you can check the minimum and maximum angles of a rectangle's "line of sight" relatively to a given position by checking all of the rectangles corners directions, and picking the most extreme values. That said, due to the cyclic vales found, there are lots of corner cases - I think I've covered them all.

class Actor(object):
    def __init__(self, x, y, w, h):
        self.x = x
        self.y = y
        self.w = w
        self.h = h

    def __len__(self):
        return 4

    def __getitem__(self, item):
        return {0: self.x, 1: self.y, 2: self.w, 3: self.h}[item]


def get_direction_range(rect, position):
    min_d = 181
    max_d = -181
    # Due to the cases where the target rect crosses the 180, -180 line, always keep the bottom corners last:
    for corner in (('top', 'left'), ('top', 'right'), ('bottom', 'left'), ('bottom', 'right')):
        tx, ty = getattr(rect, corner[1]), getattr(rect, corner[0])
        print(tx, ty)
        # Calculate vector from given position to target corner
        vx = tx - position[0]
        vy = -(ty - position[1])

        direction = math.degrees(math.atan2(vy, vx))
        if direction < min_d and (not (direction < 0 and min_d > 0 and min_d > 90) or min_d == 181) :
            min_d = direction
        if direction > max_d or (direction < 0 and max_d > 0 and max_d > 90):
            print(direction, "max")
            max_d = direction
    min_d += 0 if min_d > 0 else 360
    max_d += 0 if max_d > 0 else 360
    return min_d, max_d


def check_target(direction, position, rect):
    if not isinstance(rect, pygame.Rect):
        rect = pygame.Rect(rect)
    if position in rect:
        return True

    min_d, max_d = get_direction_range(rect, position)
    if max_d < min_d:
        max_d, min_d = min_d, max_d
    if abs(max_d - min_d) > 180:
        return max_d <= direction <= 360 or 0 <= direction <= min_d
    return min_d <= direction <= max_d


def dl(scr, pos, direction):
    xx = math.cos(math.radians(direction)) * 200
    yy = -math.sin(math.radians(direction)) * 200
    pygame.draw.line(scr, (0,255,0), pos, (pos[0] + xx, pos[1] + yy))
    pygame.display.flip()

You could make that "Actor" class derive from pygame.Rect (if you can't use Rect itself) - I added methods on it so it at least can be cast to Rect, which allows us to easily pick the corner coordinates. Anyway, note that if you are using Python 2.x (I believe so, due to the difficulty there is to install pygame under Python3), all your classes should inherit from object.

The dl function I've used to debug in aliving session - it ay be usefull to you as well.

Upvotes: 1

Related Questions