Search code examples
pythonpygamegame-development

Pygame: how to reset alpha layer without .fill((0, 0, 0, 255)) every frame


I'm programming a 2D game with a background and a fog of war. Both are black pygame.Surface, one with an alpha chanel (fog of war) and one without (background). Their sizes are the same and are constants. The only thing that change is the alpha channel of some pixels in fog of war.

Profiling my code, I observed that 15% of execution time was spent in filling those surfaces with black. I go from 3.5s/1000 frames to 3s/1000 frames with/without filling. I was wondering: is there any more optimized way to compute things?

Here is a minimal example which goes from 2sec to 1.5 sec with or without filling on my machine :

import pygame
import random
import cProfile
from pstats import Stats

pygame.init()

wh = 1000

screen = pygame.display.set_mode((wh, wh))
fog_of_war = pygame.Surface((wh, wh), pygame.SRCALPHA)

pr = cProfile.Profile()
pr.enable()

for i in range(1000):
    screen.fill((255, 255, 255))
    fog_of_war.fill((0, 0, 0, 255))
    pygame.draw.circle(fog_of_war, (0, 0, 0, 0),
                       (wh/2+random.randint(-5,5),
                        wh/2+random.randint(-5,5)), 50)
    
    screen.blit(fog_of_war, (0, 0))
    pygame.display.flip()

pr.disable()
s = Stats(pr)
s.strip_dirs()
s.sort_stats('tottime').print_stats(5)

pygame.quit()

I see maybe two ways of doing so but I don't know how to implement them:

  1. Maybe create the filled surfaces once, and find a way to say "at the begining of each frame, screen is this and fog of war is this"

  2. Specifically for the fog of war layer: resetting only the alpha layer to 255 everywhere without touching to RGB which will always be 0

Thanks for your help :)

Edit: On the real thing, there are several circles which are moving from one frame to another, so that screen has to be reseted to black in some way. One solution could have been to create a third surface outside the loop called "starting_black_screen", to fill it black and to blit it to screen every frame but it's actually slower than filling the screen black.


Solution

  • I found a solution for the second option from https://github.com/pygame/pygame/issues/1244, which explains how to modify the alpha layer. The key function goes like this:

    def reset_alpha(s):
       surface_alpha = np.array(s.get_view('A'), copy=False)
       surface_alpha[:,:] = 255
       return s
    

    Though doing so requires to create a numpy array with individual alpha pixel values, which takes about as much time as fog_of_war.fill((0, 0, 0, 255)). Pygame's fill method seems optimized as hell to do 4x the job of Numpy's fill in the same time.

    Here is complete code:

    import pygame
    import numpy as np
    import random
    import cProfile
    from pstats import Stats
    
    pygame.init()
    
    wh = 1000
    
    def reset_alpha(s):
        surface_alpha = np.array(s.get_view('A'), copy=False)
        surface_alpha[:,:] = 255
        return s
    
    screen = pygame.display.set_mode((wh, wh))
    fog_of_war = pygame.Surface((wh, wh), pygame.SRCALPHA)
    
    pr = cProfile.Profile()
    pr.enable()
    
    fog_of_war.fill((0, 0, 0, 255))
    
    for i in range(1000):
        screen.fill((255, 255, 255))
        fog_of_war = reset_alpha(fog_of_war)
        pygame.draw.circle(fog_of_war, (0, 0, 0, 0),
                           (wh/2+random.randint(-5,5),
                            wh/2+random.randint(-5,5)), 50)
        
        screen.blit(fog_of_war, (0, 0))
        pygame.display.flip()
    
    pr.disable()
    s = Stats(pr)
    s.strip_dirs()
    s.sort_stats('tottime').print_stats(5)
    
    pygame.quit()