1

I'm making a 2D game in python using the module pygame. I would like to create a red vignette/bleed effect whenever the player takes damage in my game. This is seen in many games today, where the edges of the screen will flash red for a second and quickly disappear.

I have tried blitting an image I made in photoshop and scaling it accordingly during an animation cycle, but this was a really performance heavy operation, subsequently causing a lot of lag. I'm looking for alternatives to this method.

Code declaring a few variables:

bgX = 0
bgY = 0
damage = pygame.image.load("defensiveGameHUD.png").convert_alpha()
dimensions = [1920,1080]

Then I have this in the main loop of my game:

win.blit(background,(0,0))

if dimensions[0] != 4020:
    dimensions[0] += 30
    bgX -= 15
if dimensions[1] != 4600:
    dimensions[1] += 40
    bgY -= 20

if dimensions[1] != 4600:
    screenDamage = pygame.transform.scale(damage, dimensions)
    win.blit(screenDamage, (bgX, bgY))
else:
    screenDamage = None

That is simply an animation that will scale the image in, however, the scaling is improper and this is very costly on performance.

def smmothstep(edge0, edge1, x):
    t = min(1, max(0, (x - edge0) / (edge1 - edge0)))
    return t * t * (3.0 - 2.0 * t)

def gen_damage_image(scale, source):
    dest = source.copy()
    img_size = dest.get_size()
    for i in range(img_size[0]):
        for j in range(img_size[1]):
            fx = smmothstep(0, img_size[0]/2*scale, min(i, img_size[0]-i))
            fy = smmothstep(0, img_size[1]/2*scale, min(j, img_size[1]-j))
            color =  dest.get_at((i, j))
            fade_color = [int(255 - (1-fx*fy)*(255 - c)) for c in color]
            dest.set_at((i, j), fade_color)
    return dest

def tintDamage(surface, scale):
    i = min(len(dmg_list)-1, max(0, int(scale*(len(dmg_list)-0.5))))
    c.blit(dmg_list[i], (0, 0), special_flags = pygame.BLEND_MULT)

damage = pygame.image.load("defensiveGameHUD.png").convert_alpha()
max_dmg_img = 10
dmg_list = [gen_damage_image((i+1)/max_dmg_img, damage) for i in range(max_dmg_img)]


start_time = 0
tint = 0
damage_effect = False
Rabbid76
  • 202,892
  • 27
  • 131
  • 174
Hydra
  • 373
  • 3
  • 18
  • Why should displaying the vignette effect cause lag? I'm assuming you want an effect similar to [this one](https://i.ytimg.com/vi/WMA73ukITj4/maxresdefault.jpg), which is just a case of drawing the vignette effect sprite – 0liveradam8 May 27 '19 at 23:55
  • 1
    You should show your code. A [MCVE](https://stackoverflow.com/help/minimal-reproducible-example) possibly. Maybe we can suggest you how to improve it and make it faster. – Valentino May 28 '19 at 00:15
  • @Oilveradam8 There is no problem blitting the image I created to the screen, but as I said, when I started scaling the image in pygame, I immediately noticed a dip in performance. – Hydra May 28 '19 at 02:47
  • 1
    @Valentino I added parts of my code that seemed relevant to the question to my post now. – Hydra May 28 '19 at 03:10

1 Answers1

1

To tint the screen in red can be achieved by pygame.Surface.fill(), by setting special_flags = BLEND_MULT.
The following function "tints" the entire surface in red, by a scale from 0 to 1. If scale is 0, the surface is not tinted and if scale is 1 the entire surface is tinted by the (red) color (255, 0, 0):

def tintDamage(surface, scale):
    GB = min(255, max(0, round(255 * (1-scale))))
    surface.fill((255, GB, GB), special_flags = pygame.BLEND_MULT)

The function has to be called right before pygame.display.flip() or pygame.display.update():

e.g.

tintDamage(win, 0.5)
pygame.display.flip()

Note, the special_flags = BLEND_MULT can also be set when using pygame.Surface.blit():

win.blit(damage, (bgX, bgY), special_flags = pygame.BLEND_MULT)

Or even both effects can be combined.


That's not exactly the effect I was looking for [...] I would like this effect to sort of scale itself inwards and then outwards, ...

What you want to do is tricky, because you would have to change each pixel of the damage surface dynamically. That would be much to slow.

But you can precalculate different damage surfaces, depending on an effect scale:

def smmothstep(edge0, edge1, x):
    t = min(1, max(0, (x - edge0) / (edge1 - edge0)))
    return t * t * (3.0 - 2.0 * t)

def gen_damage_image(scale, source):
    dest = source.copy()
    img_size = dest.get_size()
    for i in range(img_size[0]):
        for j in range(img_size[1]):
            fx = smmothstep(0, img_size[0]/2*scale, min(i, img_size[0]-i))
            fy = smmothstep(0, img_size[1]/2*scale, min(j, img_size[1]-j))
            color =  dest.get_at((i, j))
            fade_color = [int(255 - (1-fx*fy)*(255 - c)) for c in color]
            dest.set_at((i, j), fade_color)
    return dest

damage = pygame.image.load("defensiveGameHUD.png").convert_alpha()
max_dmg_img = 10
dmg_list = [gen_damage_image((i+1)/max_dmg_img, damage) for i in range(max_dmg_img)]

tintDamage choose a damage image of the list, dependent on the scale:

def tintDamage(surface, scale):
    i = min(len(dmg_list)-1, max(0, int(scale*(len(dmg_list)-0.5))))
    c.blit(dmg_list[i], (0, 0), special_flags = pygame.BLEND_MULT)

The inwards / outwards effect can be achieved by a sine function. See the example, which starts the effect when x is pressed:

run = True
start_time = 0
tint = 0
damage_effect = False
while run:
    clock.tick(60)

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_x:
                damage_effect = True
                tint = 0

    win.fill((196, 196, 196))

    # [...]

    if damage_effect:
        scale = math.sin(tint)
        tintDamage(win, scale)
        tint += 0.1
        damage_effect = scale >= 0
    pygame.display.flip()

Since the computation of the images is very slow, I provide a solution, which generated a scale mask on a 20x20 image. The mask is scaled to the size of the damage image and blended with the damage image:

def gen_damage_image(scale, source):
    scale_size = (20, 20)
    scale_img = pygame.Surface(scale_size, flags = pygame.SRCALPHA)
    for i in range(scale_size[0]):
        for j in range(scale_size[1]):
            fx = smmothstep(0, scale_size[0]/2*scale, min(i, scale_size[0]-i))
            fy = smmothstep(0, scale_size[1]/2*scale, min(j, scale_size[1]-j))
            fade_color = [int(max(0, 255 - (1-fx*fy)*255)) for c in range(4)]
            scale_img.set_at((i, j), fade_color)
    dest = source.copy()
    scale_img = pygame.transform.smoothscale(scale_img, dest.get_size())
    dest.blit(scale_img, (0, 0), special_flags = pygame.BLEND_ADD)  
    return dest
Rabbid76
  • 202,892
  • 27
  • 131
  • 174
  • That's not exactly the effect I was looking for. [This](https://imgur.com/zLwEFyd) is an image that I made in photoshop, it's a damage/bleed effect as I mentioned. I would like this effect to sort of scale itself inwards and then outwards, similar to [this](https://youtu.be/tF8wVKUKFjQ?t=42) type of animation or [this](https://youtu.be/6ccDLy9eNMw?t=996). – Hydra May 30 '19 at 03:54
  • When I tried this out, declaring the variable dmg_list caused my program is stuck on a black screen. – Hydra Jun 03 '19 at 15:58
  • I edited my original post. My code is there. There wasn't a need to include the main loop since the code freezes before it even gets there. – Hydra Jun 03 '19 at 16:23
  • @Hydra The application doesn't stuck! It takes time to compute the images pixel by pixel. You've to wait. – Rabbid76 Jun 03 '19 at 16:26
  • Wait for how long? I left the program running for about 10 min and still nothing. And if I do have to wait, is this something that will take a single time to load, since I can't see it as being practical having to wait for this to compute pixel by pixel every time the program is loaded. – Hydra Jun 03 '19 at 16:27
  • @Hydra Probably the solution is to slow for big surfaces. You've to "paint" different images. Or write a program which generates the images one by one and store them to files. – Rabbid76 Jun 03 '19 at 16:50
  • If this is not a good solution for big surfaces, then how would I paint different images? – Hydra Jun 03 '19 at 17:28
  • @Hydra Does the program run through if you set `max_dmg_img = 1` or `= 2`? – Rabbid76 Jun 03 '19 at 17:36
  • The program black screens for about 5 seconds and then does the animation in about 3 frames. I also noticed that the image is very distorted/pixelated and the look of the image itself is darker than it is supposed to be. Maybe this is because my surface is transparent and I am blitting it overtop of my game environment? – Hydra Jun 03 '19 at 18:05
  • @Hydra I added a different algorithm to the answer (last part). – Rabbid76 Jun 03 '19 at 18:10