How can i render non rectangular part of an image?

Question:

I’m learning how to create a multiplayer game with pygame by recreating Among-us.
I’m currently trying to recreate the vision system.

To make sure that the vision is blocked by walls, i’ve seen that a simple method is to place points at each corner of walls and to only draw the inner part of the triangles made between those points and the player.

I came up with this: points are in purple, walls in red and the range of vision in green

(I’m aware that i’ll have to stop the purple lines when they hit a wall.)
But my issue now is how can i draw the background (its an image of the Among_us map) only on those triangles and shade the rest of the image ?

PS: If you know how to integrate the shape bewteen the triangle and the and of the vision circle(like on the right of the picture) it would help to.

At first i thought about using the area parameter of blit but it only work with rectangles.
Maybe i could draw each pixel one by one but i don’t think that it’s a great idea.
I hope there is a better way.

Asked By: Noah

||

Answers:

To make sure that the vision is blocked by walls, i’ve seen that a simple method is to place points at each corner of walls and to only draw the inner part of the triangles made between those points and the player.

That’s the way to go. Maybe you’ll find this tutorial helpful.

But my issue now is how can i draw the background … only on those triangles and shade the rest of the image ?

Create a black surface, "cut out" the visible area (for example, drawing a polygon with a color key will do), and just draw it on top; like Woodford already said in a comment.

PS: If you know how to integrate the shape bewteen the triangle and the and of the vision circle(like on the right of the picture) it would help to.

Here you can use the same idea. Create a black surface, draw a transparent circle with center = postion of the player and radius = ‘max visible distance’, and slap it on top.

Here I quickly ported the tutorial I linked to python/pygame:

 # A port of https://ncase.me/sight-and-light/ to pygame

import pygame, math
from itertools import cycle,islice
from collections import namedtuple

FPS = 60
SCREEN_SIZE = 800, 600
SCREEN_RECT = pygame.Rect(0, 0, *SCREEN_SIZE)
COLOR_KEY = (12, 23, 34)

Point = namedtuple("Point", "x y")
Line = namedtuple("Ray", "a b")
Intersection = namedtuple("Intersection", "x y param angle", defaults=[0])

class Obstacle(pygame.sprite.Sprite):
    def __init__(self, points, *args):
        super().__init__(*args)
        self.image = pygame.Surface(SCREEN_RECT.size)
        self.image.set_colorkey(COLOR_KEY)
        self.image.fill(COLOR_KEY)
        self.rect = self.image.get_rect()
        self.points = points
        pygame.draw.polygon(self.image, (30, 255, 30), self.points, 4)

    def __iter__(self):
        return iter(self.points)

class Actor(pygame.sprite.Sprite):
    def __init__(self, *args):
        super().__init__(*args)
        self.image = pygame.Surface((20, 20))
        self.image.fill((255, 30, 30))
        self.rect = self.image.get_rect(center=SCREEN_RECT.center)

def get_intersection(ray, segment):
    """Find intersection of RAY & SEGMENT"""

    # RAY in parametric: Point + Delta*T1
    r_px = ray.a.x
    r_py = ray.a.y
    r_dx = ray.b.x-ray.a.x
    r_dy = ray.b.y-ray.a.y
    
    # SEGMENT in parametric: Point + Delta*T2
    s_px = segment.a.x
    s_py = segment.a.y
    s_dx = segment.b.x-segment.a.x
    s_dy = segment.b.y-segment.a.y

    # Are they parallel? If so, no intersect
    r_mag = math.sqrt(r_dx*r_dx+r_dy*r_dy);
    s_mag = math.sqrt(s_dx*s_dx+s_dy*s_dy);
    if r_dx/r_mag==s_dx/s_mag and r_dy/r_mag==s_dy/s_mag:
        return

    try:
        # SOLVE FOR T1 & T2
        # r_px+r_dx*T1 = s_px+s_dx*T2 && r_py+r_dy*T1 = s_py+s_dy*T2
        # ==> T1 = (s_px+s_dx*T2-r_px)/r_dx = (s_py+s_dy*T2-r_py)/r_dy
        # ==> s_px*r_dy + s_dx*T2*r_dy - r_px*r_dy = s_py*r_dx + s_dy*T2*r_dx - r_py*r_dx
        # ==> T2 = (r_dx*(s_py-r_py) + r_dy*(r_px-s_px))/(s_dx*r_dy - s_dy*r_dx)
        T2 = (r_dx*(s_py-r_py) + r_dy*(r_px-s_px))/(s_dx*r_dy - s_dy*r_dx)
        T1 = (s_px+s_dx*T2-r_px)/r_dx

        # Must be within parametic whatevers for RAY/SEGMENT
        if T1<0: return
        if T2<0 or T2>1: return
        
        # Return the POINT OF INTERSECTION
        return Intersection(r_px+r_dx*T1, r_py+r_dy*T1, T1)
    except ZeroDivisionError:
        pass

def get_all_angles_from_points(start, points):
    angles = []
    for p in points:
        angle = math.atan2(p[1]-start[1], p[0]-start[0])
        angles.append(angle-0.0001)
        angles.append(angle)
        angles.append(angle+0.0001)
    return angles

def get_blocker_surface(intersects):
    intersects = sorted(intersects, key=lambda x: x.angle)
    as_points = list(map(lambda r: (r.x, r.y), intersects))
    blocker_surface = pygame.Surface(SCREEN_SIZE)
    blocker_surface.set_colorkey(COLOR_KEY)
    pygame.draw.polygon(blocker_surface, COLOR_KEY, as_points)
    return blocker_surface

def fov(screen, start, obstacles):
    points = set(p for o in obstacles for p in o)
    angles = get_all_angles_from_points(start, points)

    intersects = []
    for angle in angles:
        dx = math.cos(angle)
        dy = math.sin(angle)
        
        ray = Line(Point(start.x, start.y), Point(start.x + dx, start.y + dy))
        
        closest_intersection = None
        for o in obstacles:
            connected_points = zip(o.points, islice(cycle(o.points), 1, None))
            for segment in connected_points:
                intersection = get_intersection(ray, Line._make(segment))
                if not intersection: continue
                if not closest_intersection or intersection.param < closest_intersection.param:
                    closest_intersection = intersection

        if closest_intersection:
            intersects.append(closest_intersection._replace(angle=angle))

    return intersects

def main():
    pygame.init()
    pygame.display.set_caption("FOV demo")
    clock = pygame.time.Clock()
    screen = pygame.display.set_mode(SCREEN_SIZE)
    sprites = pygame.sprite.Group()
    obstacle_sprites = pygame.sprite.Group()
    font = pygame.font.SysFont("Consolas", 20)
    
    tmprect = SCREEN_RECT.inflate(-2, -2)
    data = [
            [tmprect.topleft, tmprect.topright, tmprect.bottomright, tmprect.bottomleft],
            [(229, 211), (336, 294), (271, 406), (126, 323)],
            [(386, 170), (487, 167), (393, 74), (309, 186)],
            [(631, 502), (576, 457), (690, 389), (764, 493)],
            [(519, 308), (489, 378), (589, 381), (532, 292)],
            [(349, 552), (347, 480), (441, 484), (475, 566)],
            [(637, 93), (592, 228), (726, 278), (661, 153)],
            [(186, 494), (88, 431), (29, 533), (174, 566)],
            [(97, 138), (32, 242), (150, 178), (161, 74)]
        ]

    obstacles = [Obstacle([Point._make(t) for t  in o], sprites, obstacle_sprites) for o in data]

    player = Actor(sprites)
    
    def stay_on_mouse(this):
        this.rect.center = pygame.mouse.get_pos()
    
    player.update = lambda *args: stay_on_mouse(player, *args)
    
    do_fov = True
    do_debug = False
    do_draw_obstacles_on_top = True
    fov_alpha = 255
    while True:
        evs = pygame.event.get()
        for e in evs:
            if e.type == pygame.QUIT:
                return
            if e.type == pygame.KEYDOWN:
                if e.key == pygame.K_d: do_debug = not do_debug
                if e.key == pygame.K_o: do_draw_obstacles_on_top = not do_draw_obstacles_on_top
                if e.key == pygame.K_SPACE: do_fov = not do_fov
                if e.key == pygame.K_ESCAPE: return
        
        sprites.update()
        screen.fill((50, 50, 50))
        
        # draw regular stuff
        sprites.draw(screen)
        
        # hide stuff due to FOV
        intersects = fov(screen, Point._make(player.rect.center), obstacles)
        blocker_surface = get_blocker_surface(intersects)
        fov_alpha = min(fov_alpha+5, 255) if do_fov else  max(fov_alpha-5, 0)
        blocker_surface.set_alpha(fov_alpha)
        screen.blit(blocker_surface, (0, 0))

        if do_debug:
            for i in intersects:
                pygame.draw.line(screen, (100, 100, 100), (i.x, i.y), player.rect.center)
        
        if do_draw_obstacles_on_top:
            obstacle_sprites.draw(screen)
        
        y = 24
        for line in ["[SPACE] - Toggle FOV", "[D] - Draw debug lines", "[O] - Draw obstascles on top"]:
            screen.blit(font.render(line, True, (0, 0, 0)), (22, y+2))
            screen.blit(font.render(line, True, (255, 255, 255)), (20, y))
            y+= 24

        pygame.display.flip()
        clock.tick(FPS)
main()

And it looks like this:

enter image description here

That should get you started.

Answered By: sloth
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.