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).
Related
I need to rotate a triangle (Not an image) around at the center of the screen. I have seen other people answer this question, but the triangle could not point upwards.
I have tried to use other peoples functions, but they see to only work partly, like the function I mentioned above.
import pygame
disp=pygame.display.set_mode((200,200))
import math
def rotate_triange(mouse_pos,triangle_pos):
#The code here
import time
while True:
time.sleep(1)
pygame.Surface.fill(disp,(255,255,255))
center = (100,100)
radius = 10
mouse_position = pygame.mouse.get_pos()
for event in pygame.event.get():
pass
points = rotate_triangle((100,100),mouse_position)
pygame.draw.polygon(disp,(0,0,0),points)
pygame.display.update()
In pygame 2 dimensional vector arithmetic is implemented in pygame.math.Vector2.
Define a Vector2 object for the mouse position and the center of the triangle. Calculate the angle of vector form the center point to the mouse position (.angle_to()):
vMouse = pygame.math.Vector2(mouse_pos)
vCenter = pygame.math.Vector2(center)
angle = pygame.math.Vector2().angle_to(vMouse - vCenter)
Define the 3 points of the triangle around the (0, 0) and rotate them by the angle (.rotate())
points = [(-0.5, -0.866), (-0.5, 0.866), (2.0, 0.0)]
rotated_point = [pygame.math.Vector2(p).rotate(angle) for p in points]
To calculate the final points, the points have to b scaled and translated by the center of the triangle:
triangle_points = [(vCenter + p*scale) for p in rotated_point]
See the example:
import pygame
import math
def rotate_triangle(center, scale, mouse_pos):
vMouse = pygame.math.Vector2(mouse_pos)
vCenter = pygame.math.Vector2(center)
angle = pygame.math.Vector2().angle_to(vMouse - vCenter)
points = [(-0.5, -0.866), (-0.5, 0.866), (2.0, 0.0)]
rotated_point = [pygame.math.Vector2(p).rotate(angle) for p in points]
triangle_points = [(vCenter + p*scale) for p in rotated_point]
return triangle_points
disp=pygame.display.set_mode((200,200))
run = True
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
mouse_position = pygame.mouse.get_pos()
points = rotate_triangle((100, 100), 10, mouse_position)
pygame.Surface.fill(disp, (255,255,255))
pygame.draw.polygon(disp, (0,0,0), points)
pygame.display.update()
A version of the algorithm, without the use of pygame.math.Vector2, looks as follows:
def rotate_triangle(center, scale, mouse_pos):
dx = mouse_pos[0] - center[0]
dy = mouse_pos[1] - center[1]
len = math.sqrt(dx*dx + dy*dy)
dx, dy = (dx*scale/len, dy*scale/len) if len > 0 else (1, 0)
pts = [(-0.5, -0.866), (-0.5, 0.866), (2.0, 0.0)]
pts = [(center[0] + p[0]*dx + p[1]*dy, center[1] + p[0]*dy - p[1]*dx) for p in pts]
return pts
Note this version is probably faster. It needs a math.sqrt operation, in compare to math.atan2 which is probably used by .angle_to() and math.sin respectively math.cos which is probably used by .rotate(), of the former algorithm.
The result coordinates are the same.
I'm trying to create a game in python where one can drag a textbox around the screen, but whenever it touches the borders of a circle around it, I want the loop to start over, but with a different text (by storing all text-strings in a list, but I'm not that far, yet). This is how far I have come:
import pygame
import ptext
pygame.init()
gameDisplay = pygame.display.set_mode((500, 500))
gameDisplay.fill((255,255,255))
x = 190
y = 230
a = 250
b = 250
text = "ExampleText 1."
def textbox(x,y):
ptext.draw(text, (x,y), color = (0,0,0))
def circle(a,b):
pygame.draw.circle(gameDisplay, (0,0,0), (250, 250), 210, 5)
done = False
while not done:
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
elif event.type == pygame.MOUSEMOTION:
if event.buttons[0]:
x += event.rel[0]
y += event.rel[1]
textbox(x,y)
circle(a,b)
pygame.display.flip()
pygame.quit()
quit()
Now I understand I will need to detect collision of the borders of my objects, but here I'm pretty lost. I tried to store the variables of my objects in rectangles and then produce another if statement that recognizes whether or not my objects collide (I used a print command because I haven't gotten to the actual command I want, yet), but that won't print anything and I'm sure I'm on the wrong path, but it is my best effort...
For that I have defined:
text_rect = pygame.Rect(x, y, 10, 30)
circle_rect = pygame.Rect(a,b, 300, 300)
and then in my loop:
if circle_rect.colliderect(text_rect):
print("COLLIDE")
Does anybody have any tip on a better way to define the objects and to create the function I want?
(Edit: Btw.: I'm not too concerned about the fact that when I drag my textbox, it leaves a print of the text, since that doesn't happen in my original script, but would be thankful if anyone knows why it does that in my current example.)
A rectangle has 4 corner points. If the rectangle is "smaller" then the circle (the diameter of the circle is greater than the diagonal of the rectangle), then the rectangle collides with the contour of a circle, if at least one point is out of the circle and at least one point is in the circle.
Define the rectangle and setup a list of the corner points. Further you've to know the radius of the circle:
w, h = 10, 30
rect = pygame.Rect(x, y, 10, 30)
corners = [rect.bottomleft, rect.bottomright, rect.topleft, rect.topright]
radius = 210
Calculate the Euclidean distance of each corner point to the center of the circle (a, b):
import math
dist = [math.sqrt((p[0]-a)**2 + (p[1]-b)**2) for p in corners]
Create to lists, one with the points in the circle (p_in) and one with the points out of the circle (p_out):
p_out = [i for i, d in enumerate(dist) if d > radius]
p_in = [i for i, d in enumerate(dist) if d < radius]
If both list contain any element, then the rectangle intersects the circle contour:
if any(p_in) and any(p_out):
print("COLLIDE")
If len(p_in) is 4, then the rectangle is completely in the circle. If len(p_out) is 4 then the rectangle is completely out of the circle.
if any(p_in) and any(p_out):
print("COLLIDE")
elif len(p_in) == 4:
print("IN")
elif len(p_out) == 4:
print("OUT")
See the simple example, which is based on your code snippet and demonstrates the collision test. The rectangle is attached to the mouse:
import pygame
import math
pygame.init()
gameDisplay = pygame.display.set_mode((500, 500))
done = False
while not done:
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
x, y = pygame.mouse.get_pos()
w, h = 10, 30
rect = pygame.Rect(x, y, 10, 30)
a, b = 250, 250
radius = 210
corners = [rect.bottomleft, rect.bottomright, rect.topleft, rect.topright]
dist = [math.sqrt((p[0]-a)**2 + (p[1]-b)**2) for p in corners]
p_out = [i for i, d in enumerate(dist) if d > radius]
p_in = [i for i, d in enumerate(dist) if d < radius]
if any(p_in) and any(p_out):
print("COLLIDE")
elif len(p_in) == 4:
print("IN")
elif len(p_out) == 4:
print("OUT")
gameDisplay.fill((255,255,255))
pygame.draw.rect(gameDisplay, (255, 0, 0), rect)
pygame.draw.circle(gameDisplay, (0,0,0), (a, b), radius, 5)
pygame.display.flip()
pygame.quit()
quit()
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
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 am making a utility for myself to easily translate degrees to x and y cordinates in my games and I got stuck on a problem; trying to move the player in degrees across the screen. I found multiple formulas that didn't work and I need some help. Here is my code that I found:
def move(degrees, offset):
x = math.cos(degrees * 57.2957795) * offset # 57.2957795 Was supposed to be the
y = math.sin(degrees * 57.2957795) * offset # magic number but it won't work.
return [x, y]
I then ran this:
move(0, 300)
Output:
[300.0, 0.0]
and it worked just fine, but when I did this:
move(90, 300)
it outputted this:
[-89.8549554331319, -286.22733444608303]
Your approach is almost correct. You should use radians for sin/cos functions. Here is a method I commonly use in C++ (ported to python) for 2D movement.
import math
def move(degrees, offset)
rads = math.radians(degrees)
x = math.cos(rads) * offset
y = math.sin(rads) * offset
return x, y
The number is correct, but the operation is wrong. In order to convert degrees to radians you need to divide by 180 degrees per half-circle and then multiply by pi radians per half-circle. This is equivalent to dividing by the constant you have.
You can use the from_polar method of the pygame.math.Vector2 class to set the polar coordinates of a vector. Then you can use this vector to adjust the position of a sprite or rect.
import pygame as pg
from pygame.math import Vector2
def move(offset, degrees):
vec = Vector2() # Create a zero vector.
vec.from_polar((offset, degrees)) # Set its polar coordinates.
return vec
pg.init()
screen = pg.display.set_mode((640, 480))
clock = pg.time.Clock()
BG_COLOR = pg.Color('gray12')
BLUE = pg.Color('dodgerblue1')
rect = pg.Rect(300, 200, 30, 20)
done = False
while not done:
for event in pg.event.get():
if event.type == pg.QUIT:
done = True
elif event.type == pg.KEYDOWN:
if event.key == pg.K_SPACE:
# Use the vector that `move` returns to move the rect.
rect.move_ip(move(50, 90))
screen.fill(BG_COLOR)
pg.draw.rect(screen, BLUE, rect)
pg.display.flip()
clock.tick(30)
pg.quit()