Reputation: 7845
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:
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
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