This question already has answers here:
Changing ememy's color to show that it is aking damage?
(1 answer)
Is it possible to change sprite colours in Pygame?
(1 answer)
Changing colour of a surface without overwriting transparency
(1 answer)
Closed 2 years ago.
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
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
Related
I have code to move a rectangle in pygame from left to right, up and down.
But I want to move my rectangle around the screen that I created..
can someone help me please?
import pygame
from pygame.locals import *
pygame.init()
FPS = 70
fpsclock = pygame.time.Clock()
SIZE = (1000, 700)
form1 = pygame.display.set_mode(SIZE)
WHITE=(255, 255, 255)
x = 0
y = 0
w = 50
h = 60
direction = 'right'
while True:
for event in pygame.event.get():
if event.type == QUIT:
exit()
form1.fill(WHITE)
pygame.draw.rect(form1, (255, 0, 0), (x, y, w, h), 1)
pygame.display.update()
fpsclock.tick(FPS)
if x,y==0,0:
direction='right'
if x,y==1200-50,0:
direction='down'
if x,y==1200-50,700-60:
direction='left'
if x,y==0,1200-50:
direction='right'
So the first thing you have to look at is the spacing. Even though your code works (after proper indentation) the square goes out of bounds.
The same thing applies to y as well if the square should go up and down.
If you want the square to go around you just need to go left, right, up, or down at the correct time. So if you want to start at the left upper corner and go around you just need to check if the square is in a corner and then change the direction accordingly.
Keep in mind that going down actually increases and going up decreases y.
EDIT:
Here you can see the result of my proposed concept
EDIT 2:
I've copied your code and refactored and completed it. I tried to explain why I did what I did.
import pygame
# Only import the things you need it makes debugging and documenting
# alot easier if you know where everything is coming from
from pygame.locals import QUIT
FPS = 70
# Use variables to define width and height in case you want to
# change it later
width = 200
height = 200
# I would group variables by usage, for example, I would group width,
# height of the screen and w, h of the box so I can easily manipulate
# the numbers if want to.
w = 50
h = 60
# Using an offset variable reduces the hardcoded numbers even more
# if its 0 it will just move along the border but if it >0 it will move
# the square to the centre
offset = 20
# You can declare it as a variable if you need the SIZE tuple somewhere
# else, if you don't need it you can just set it as
# pygame.display.set_mode((width, height))
SIZE = (width, height)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
x = offset
y = offset
direction = 'right'
# This is just my preference but I like to have the variables that are
# changeable at the top for easy access. I think this way the code is
# cleaner.
pygame.init()
fpsclock = pygame.time.Clock()
form1 = pygame.display.set_mode(SIZE)
while True:
for event in pygame.event.get():
if event.type == QUIT:
exit()
form1.fill(WHITE)
# Try to avoid hardcoded numbers as much as possible, hardcoded
# numbers are hard to change later on when the code gets to certain
# size and complexity.
pygame.draw.rect(form1, RED, (x, y, w, h), 1)
pygame.display.update()
fpsclock.tick(FPS)
# Don't harcode conditions, use variables so you can easily change
# them later
if x == offset and y == offset:
direction='right'
if x == width - w - offset and y == offset:
direction='down'
if x == width - w - offset and y == height - h - offset:
direction='left'
if x == offset and y == height - h - offset:
direction='up'
if direction == 'right':
x += 5
elif direction == 'down':
#Keep in mind going down actually means to increment y
y += 5
elif direction == 'left':
x -= 5
elif direction == 'up':
y -= 5
I'm french so pardon my language error.
I try to make a little pygame-zero game with ball physic.
So I want to control precisely a ball object in time.
I created a Ball's class, which implement built-in Rect object (to use default collision in case).
import pygame
WIDTH = 300
HEIGHT = 300
class Color:
WHITE = (255, 255, 255)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
class Ball(pygame.Rect):
def __init__(self, cx, cy, radius, color: Color = Color.WHITE):
position = (cx - radius, cy - radius)
size = (radius * 2, radius * 2)
super(Ball, self).__init__(position, size)
self.radius = radius
self.color = color
#property
def cx(self):
return self.x + self.radius
#cx.setter
def cx(self, cx):
self.x = cx - self.radius
#property
def cy(self):
return self.y + self.radius
#cy.setter
def cy(self, cy):
self.y = cy - self.radius
def draw(self):
position = (self.cx, self.cy)
screen.draw.filled_circle(position, self.radius, self.color);
# screen.draw.rect(self, Color.RED) # draw hitbox
I instance a ball object and want make it go at x position 0 to 300. (the width of the window).
I use the default delta time (it's the time in millisecond elapsed between each frame 1/60) in update's function.
With a speed of 100 pixel * delta time, the ball will traverse the screen at 3 seconds (logical).
But it's not ... after some manipulation I created a second ball (I switch ball's name) and when I use variable instead all works fine.
cl = 0 # get time elpased at launch
ball = Ball(0, 10, 10, Color.RED)
x = ball.cx
ball2 = Ball(0, 30, 10, Color.BLUE)
speed = 100
def draw():
screen.clear()
ball.draw()
ball2.draw()
screen.draw.text(f"{cl:7.2f}", (0, 150))
def update(dt):
global cl, x
if ball.cx <= WIDTH:
cl += dt
# the ball make 3 seconds exactly
x += speed * dt
ball.cx = x
ball2.cx += speed * dt # don't make 3 seconds WHY ?
My question ... why ?
Create a custom property, it's much slower than classic variable definition ?
If you want to ensure time precise calculations, then you've to do all the calculations with floating point values. Only convert the coordinates to integral data types when drawing the objects on is integer pixel position.
The .cx property has an integral data type. Every time when the position is incremented, then the fraction part of the number which is added is lost. This causes an increasing inaccuracy. The object appears to be slower than expected.
Note, if the frame rate would be higher, then the object will even stand still, if the step per frame is smaller than 1.0. The integer part of a value < 1.0 is 0.
Note
ball2.cx += speed * dt
does the same as
ball2.cx += int(speed * dt)
The x variable is a floating point value. It is incremented with full accuracy. The exact value is cast to int when it is assigned to the .cx property, but the inaccuracy is always smaller than 1.0 and thus almost negligible.
x += speed * dt
ball.cx = x
does the same as
x += speed * dt
ball.cx = int(x)
This question already has answers here:
Is it possible to change sprite colours in Pygame?
(1 answer)
Trying to make sections of sprite change colour, but whole sprite changes instead
(1 answer)
Closed 2 years ago.
I'm trying to code the typical dvd bouncing screensaver. I am happy with it but I want to change the color of the logo everytime it hits the wall. I have used fill(), but the logo changes to a colored rectangle. I want to change the color of the logo, respecting the alpha channel of the image.
from pygame import *
import random
#set canvas size variables
width = 700
height = 450
#draw canvas
screen = display.set_mode((width,height))
display.set_caption('Graphics')
#initial XY coordinates where game starts
x = random.randint(1, width)
y = random.randint(1, height)
#import logo
logo_img = image.load('dvd_logo_alpha.png')
R = 255
G = 255
B = 255
def logo(x,y):
screen.blit(logo_img, (x,y))
def Col():
R = random.randint(100,255)
G = random.randint(100,255)
B = random.randint(100,255)
#speed of the logo
dx = 3
dy = 3
endProgram = False
while not endProgram:
for e in event.get():
if e.type == QUIT:
endProgram = True
#speed changes position XY
x += dx
y += dy
#detection of collision with border of screen
if y<0 or y>height-47:
dy *= -1
R = random.randint(100,255)
G = random.randint(100,255)
B = random.randint(100,255)
if x<0 or x>width-100:
dx *= -1
R = random.randint(100,255)
G = random.randint(100,255)
B = random.randint(100,255)
screen.fill((0))
logo_img.fill((R,G,B)) #here is the problem I can not solve
logo(x,y)
display.update()
First you have to create an image with an alpha channel, to make the transparent area of the png image invisible. Use pygame.Surface.convert_alpha() To create an surface with an alpha channel:
tintImage = image.convert_alpha()
To tint an image by pygame.Surface.fill(), the special_flags has too be set to BLEND_RGBA_MULT. This causes that the all the pixels of the image are multiplied by the color, rather then set by the color:
tintImage.fill((R, G, B, 255), None, BLEND_RGBA_MULT)
Note, since the image has to be tint by different colors, the original image has to be kept. Use the function logo to copy the image, and "tint" an "blit" the copy of the image:
def logo(x, y, color):
tintImage = logo_img.convert_alpha()
tintImage.fill((R, G, B, 255), None, BLEND_RGBA_MULT)
screen.blit(tintImage, (x, y))
Call the function in the main loop of the program:
endProgram = False
while not endProgram:
for e in event.get():
if e.type == QUIT:
endProgram = True
#speed changes position XY
x += dx
y += dy
#detection of collision with border of screen
if y<0 or y>height-47:
dy *= -1
R = random.randint(100,255)
G = random.randint(100,255)
B = random.randint(100,255)
if x<0 or x>width-100:
dx *= -1
R = random.randint(100,255)
G = random.randint(100,255)
B = random.randint(100,255)
screen.fill((0))
#logo_img.fill((R,G,B)) #here is the problem I can not solve
logo(x, y, (R, G, B))
display.update()
I'm running a python game with approximately 2k rectangles being drawn on screen each frame.
The problem I have is that it runs on 12 fps and I have no idea how to fix that. When I remove all the rectangles it shifts to 100 fps. I'm not rendering all of them at once, but only the ones camera can see currently. How to fix this lag spike issue, is it because I'm using pygame rectangles or because I'm using them wrong?
here's the code
import pygame
black = (0,0,0)
pygame.init()
gameDisplay = pygame.display.set_mode((0,0),pygame.FULLSCREEN)
gameDisplay.fill(black)
gameDisplay.convert()
clock = pygame.time.Clock()
display_width = 1920
display_height = 1080
from opensimplex import OpenSimplex
tmp = OpenSimplex()
dimensions = [100,100]
size = 40
def mapping(x):
y = (x + 1) * 10 + 40
return y
class GroundCell:
def __init__(self,x,y,dim):
self.x = x
self.y = y
self.dim = dim
tempcells = []
allCells = []
for a in range(0,dimensions[0]):
tempcells = []
for b in range(0,dimensions[1]):
tempcells.append(GroundCell(a*size,b*size,mapping(tmp.noise2d(a*0.11,b*0.11))))
allCells.append(tempcells)
font = pygame.font.Font("freesansbold.ttf", 20)
while True:
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
pygame.quit()
quit()
for a in allCells:
for b in a:
if b.x in range(0,display_width) \
and b.y in range(0,display_height) or\
b.x+size in range(0,display_width) \
and b.y+size in range(0,display_height) :
pygame.draw.rect(gameDisplay,(b.dim,b.dim,b.dim),(b.x,b.y,size,size))
fps = font.render('FPS: ' + str(int(clock.get_fps())), 1, (0, 0, 0))
gameDisplay.blit(fps, (20, 20))
pygame.display.update()
clock.tick(120)
gameDisplay.fill(black)
Instead of drawing all those rects to the screen every tick, create a Surface with the noise once, and reuse it.
Some more notes:
Text rendering is expensive. If you use a lot of text rendering, better cache the surfaces created by Font.render. Note there's also the new freetype module in pygame, which is better than the font module in every way.
Your check if a rect is inside the screen is very strange. You could just use something like if 0 < b.x < display_width ... or even use pygame's Rect class, which offers nice methods like contains().
A good and fast way to create a Surface from arbitary data is to use numpy and pygame's surfarray module. Don't get intimidated, it isn't that hard to use.
Here's a running example based on your code:
import pygame
import numpy as np
black = (0,0,0)
pygame.init()
display_width = 1000
display_height = 1000
gameDisplay = pygame.display.set_mode((display_width, display_height))
gameDisplay.fill(black)
clock = pygame.time.Clock()
from opensimplex import OpenSimplex
tmp = OpenSimplex()
dimensions = [100,100]
size = 16
def mapping(x):
y = (x + 1) * 10 + 40
return y
# create an 2d array from the noise
def get_array():
rgbarray = np.zeros((dimensions[0], dimensions[1]))
for x in range(dimensions[0]):
for y in range(dimensions[1]):
c = int(mapping(tmp.noise2d(x*0.11, y*0.11)))
# simple way to convert the color value to all three (r,g,b) channels
rgbarray[x, y] = c | c << 8 | c << 16
return rgbarray
# create the array and copy it into a Surface
data = get_array()
surface = pygame.Surface((dimensions[0], dimensions[1]))
pygame.surfarray.blit_array(surface, data)
# scale the Surface to the desired size
scaled = pygame.transform.scale(surface, (dimensions[0]*size, dimensions[1]*size))
# simple way to cache font rendering
font = pygame.font.Font("freesansbold.ttf", 20)
cache = {}
def render(text):
if not text in cache:
cache[text] = font.render(text, 1, (0, 0, 0))
return cache[text]
x = 0
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
x-=1
if x < -1000:
x = 0
gameDisplay.blit(scaled, (x, 0))
fps = render('FPS: ' + str(int(clock.get_fps())))
gameDisplay.blit(fps, (20, 20))
pygame.display.update()
clock.tick(120)
As #sloth explained, there are better ways to do the same, but if you want to know what the actual problems are:
You're not drawing 2000 rectangles, you're drawing 10.000 of them, because your dimension is 100x100
You're doing a check if the rectangle is visible in the worst possible way in terms of performance. Just check what happens if you leave the check and don't draw the rectangles. You'll see that the performance is improved but it's still far from 120fps. That's because for every rectangle you generate a list of numbers from 0 to the width of the screen and another list from zero to the height of the screen. You also do that twice. That means, on a 1920x1080 screen: (1920 * 10000) + (1920 * 10000) + (1080 * 10000) + (1080*10000) = 60000000. So, 60 million checks. If you have 120fps, that means 60 million * 120 = 7.2 billion checks per second.
Just changing the check to something similar to if b.x+size < display_width and b.y+size < display_height and b.x > 0 and b.y > 0: will already improve the performance.
That said, it's still 10000 rectangles, and it's still 120fps, which means 1200000 rectangles per second, done with basically no HW acceleration and with a high level language. Don't expect top performance.
I have problem with this code, cause I want 4 points, (which are circles) to make vertex of square, but I don't know what the difference should be between those vertexes (variable "change"). I left this variable empty, please, can you give me value I should insert there and explain why?
Here's the code:
import pygame
from math import sin, cos
pygame.init()
screen = pygame.display.set_mode((800,600))
BLACK = (0,0,0)
WHITE = (255,255,255)
BLUE = (0,0,255)
GRAY = (175,175,175)
clock = pygame.time.Clock()
Font = pygame.font.SysFont(None, 50)
angle = 0
angle_c = 0
ex = False
a = (0,0)
b = (0,0)
c = (0,0)
d = (0,0)
change =
size = 95
x_c = 400
y_c = 200
while not ex:
for event in pygame.event.get():
if event.type == pygame.QUIT:
ex = True
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_LEFT:
angle_c = 0.05
if event.key == pygame.K_RIGHT:
angle_c = -0.05
if event.type == pygame.KEYUP:
if event.key == pygame.K_RIGHT or event.key == pygame.K_LEFT:
angle_c = 0
angle += angle_c
a = (round(sin(angle)*size+x_c), round(cos(angle)*size+y_c))
b = (round(sin(angle+change)*size+x_c), round(cos(angle+change)*size+y_c))
c = (round(sin(angle+change*2)*size+x_c), round(cos(angle+change*2)*size+y_c))
d = (round(sin(angle+change*3)*size+x_c), round(cos(angle+change*3)*size+y_c))
screen.fill(WHITE)
pygame.draw.circle(screen, BLUE, (400,200), round(sin(360)*100), 3)
pygame.draw.circle(screen, BLUE, a, 10)
pygame.draw.circle(screen, WHITE, a, 8)
pygame.draw.circle(screen, BLUE, b, 10)
pygame.draw.circle(screen, WHITE, b, 8)
pygame.draw.circle(screen, BLUE, c, 10)
pygame.draw.circle(screen, WHITE, c, 8)
pygame.draw.circle(screen, BLUE, d, 10)
pygame.draw.circle(screen, WHITE, d, 8)
pygame.display.update()
clock.tick(50)
pygame.quit()
The angles from the center of a square to the square's vertices differ by 90 degrees, or pi / 2 radians (which is the unit expected by Python's sin and cos functions).
So you could set your change variable to could be pi / 2 (after adding pi to the list of names to import from the math module), and your code would probably work.
But it's even easier to calculate the coordinates than that, as rotations by 90 degrees change sine and cosine values in a predictable way:
sin(a + pi/2) is cos(a)
cos(a + pi/2) is -sin(a).
Applying this transformation repeatedly lets you figure out what the sines and cosines should be after further rotations.
You only need to call sin and cos once each and then you can use the values to find all the coordinates.
y = round(sin(angle) * size)
x = round(cos(angle) * size)
a = ( x + x_c, y + y_c)
b = (-y + x_c, x + y_c)
c = (-x + x_c, -y + y_c)
d = ( y + x_c, -x + y_c)
Note that in the code above I'm following the mathematical convention that angles start at zero along the positive x-axis and increase as you initially start rotating towards the positive y-axis (which will be clockwise in pygame's coordinate system). Your previous code seemed to measure angles from the positive y-axis and increase in the opposite direction. If that's what you really want, you can simply reverse x and y in the initial assignments (assign the cosine to y and the sine to x).