The following code draws a small white disc orbiting the center of the screen. There is noticeable tearing on my machine (macOS Monterey) and 6 or 7 frames are dropped on average on each revolution. Is there any way to avoid that? I've tried adding flags such as vsync, fullscreen, scaled etc, nothing works. The opengl flag doesn't seem to work on macos.
import pygame
import sys
import math
pygame.init()
screen = pygame.display.set_mode((1920,1080))
clock = pygame.time.Clock()
while True:
for e in pygame.event.get():
if e.type == pygame.QUIT:
sys.exit()
t = pygame.time.get_ticks()
screen.fill('Black')
pygame.draw.circle(screen, 'White', (
960 + 300 * math.cos(t / 800),
540 + 300 * math.sin(t / 800)),
20)
pygame.display.update()
clock.tick(60)
To be clear: IMHO the frames are not dropping because of heavy or background CPU usage. A modified version of this code runs approximately 1000 such circles all over the screen pretty smoothly, with only the regular hiccup here and there (same as above). I only start to get real slowdowns when I go over 5000 circles.
Firstly - check that there is not some CPU-heavy task running on your machine. For the moment I would just write your program, and then worry about if/when it's dropping frames later. If it's still an issue at completion, then optimise it.
One way to speed this up is to pass the rectangle for changed screen area to the pygame.display.update( dirty_rectangle ) function. That way only the part of the screen that has been "dirtied" by changes will be updated. This means that typically a smaller part of the screen will be re-drawn.
I also modified the code to draw the circle to an off-screen surface, and just blit() that image to the screen, rather than re-calculate the circle each loop. That makes it easy to work out the corners of the changed area too.
For your example, it's possible to calculate this rectangle based on the position of the circle, and where it has been previously. So before repositioning the circle to the new co-ordinate, we make a copy of circle_rect into previous_rect. The new position is calculated, the drawing position updated. The maximum extent of the update is then the minimum top-left corner of both rectangles, down to the maximum of the bottom-right corners. The min & max is used to populate the update rectangle.
import pygame
import sys
import math
pygame.init()
screen = pygame.display.set_mode((1920,1080))
clock = pygame.time.Clock()
# Create a sprite for the circle
CIRCLE_RAD=20
circle = pygame.Surface( (CIRCLE_RAD*2, CIRCLE_RAD*2), pygame.SRCALPHA, 32 )
circle.fill( (0,0,0) )
pygame.draw.circle( circle, (255,255,255), (CIRCLE_RAD,CIRCLE_RAD), CIRCLE_RAD )
circle_rect = circle.get_rect()
update_rect = circle_rect.copy()
while True:
for e in pygame.event.get():
if e.type == pygame.QUIT:
sys.exit()
previous_rect = circle_rect.copy()
t = pygame.time.get_ticks()
t800 = t/800
circle_rect.center = ( 960 + 300 * math.cos(t800), 540 + 300 * math.sin(t800) )
# work out the size of the update
min_x = min( previous_rect.x, circle_rect.x )
min_y = min( previous_rect.y, circle_rect.y )
max_x = max( previous_rect.x + previous_rect.width, circle_rect.x + circle_rect.width )
max_y = max( previous_rect.y + previous_rect.height, circle_rect.y + circle_rect.height )
update_rect.update( min( previous_rect.x, circle_rect.x ),
min( previous_rect.y, circle_rect.y ),
max_x-min_x,
max_y-min_y )
screen.fill('Black')
screen.blit( circle, circle_rect )
pygame.display.update( update_rect )
ms_since_previous = clock.tick(60)
if ( ms_since_previous > 17 ): # 60FPS is about 17ms between frames
print( "Slow update: %d milliseconds" % ( ms_since_previous ) )
Related
How would I prevent two masks from overlapping each other when a collision is detected? I know how to detect mask collisions but I can't wrap my head around actually preventing them from colliding. I'm pretty sure the solution has to do something with mask.overlap_area, but when I try using the code provided, It doesn't seem to work at all:
example gif (the blue dot is [dx, dy] )
import pygame
import sprites
SCREEN_HEIGHT, SCREEN_WIDTH = 800, 800
running = True
pygame.init()
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
clock = pygame.time.Clock()
player = sprites.Block((100, 100))
block2 = sprites.Block((100, 100))
blocks = pygame.sprite.Group(block2)
block2.rect.topleft = 150, 150
while running:
events = pygame.event.get()
screen.fill((100, 100, 100))
for event in events:
if event.type == pygame.QUIT:
running = False
player.move(screen.get_rect())
screen.blit(player.image, player.rect)
for block in blocks:
offset = (player.rect.x - block.rect.x, player.rect.y - block.rect.y)
dx = player.mask.overlap_area(block.mask, (offset[0] + 1, offset[1])) - \
player.mask.overlap_area(block.mask, (offset[0] - 1, offset[1]))
dy = player.mask.overlap_area(block.mask, (offset[0], offset[1] + 1)) - \
player.mask.overlap_area(block.mask, (offset[0], offset[1] - 1))
screen.blit(block.image, block.rect)
print(dx, dy)
pygame.draw.circle(screen, (0, 0, 255), (dx + block.rect.x, dy + block.rect.y), 5)
clock.tick(144)
pygame.display.flip()
Do I just have the wrong idea?
I think the issue is that your program is allowing the overlap in the first place. Once they're colliding you can't do anything.
Before moving the object, check that the destination location is not already occupied by doing a "future collision" check. If there's going to be a collision, then either don't allow the movement at all, or handle it in some nicer way.
If you know the direction of movement - say the player pushed ←, and is moving left. The code can easily move the player as far left as possible, to the point just before colliding.
This way you never have to deal with objects atop each other.
It's not really clear to me what approach the program is taking. The API pygame.mask.overlap_area() returns the number of bits overlapping. The code is calculating the collision normal, not trying to prevent or undo the overlap. Maybe it can move each object by the inverse of this direction, or suchlike.
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.
How can I get rid of white spots when drawing multiple circles close to each other in pygame?
Here is my code:
import pygame
from pygame import gfxdraw
from math import pow, atan2
def getColor(r, col1, col2, fun="lin"):
if fun =="pol2":
r = pow(r,2)
col1_r = tuple([r*x for x in col1])
col2_r = tuple([(1-r)*x for x in col2])
final_col = tuple(sum(i) for i in zip(col1_r, col2_r))
return final_col
def draw(sizeX, sizeY):
# Initialize the game engine
pygame.init()
screen = pygame.display.set_mode([sizeX, sizeY])
#Loop until the user clicks the close button.
done = False
clock = pygame.time.Clock()
while not done:
# This limits the while loop to a max of 10 times per second.
# Leave this out and we will use all CPU we can.
clock.tick(10)
for event in pygame.event.get(): # User did something
if event.type == pygame.QUIT: # If user clicked close
done=True # Flag that we are done so we exit this loop
screen.fill(WHITE)
for y in range(200,500):
for x in range(0,10):
gfxdraw.arc(screen, 400, 400, y, x*15, (x+1)*15, getColor(x/10,(0,0,(y-200)/2),(255,255,(y-200)/2), fun="lin"))
pygame.display.flip()
pygame.quit()
This phenomenon is called aliasing and happens when you take a continuous signal and samples it. In your case, gfx.draw() uses continuous functions (the trigonometric functions) to calculate which pixel to draw the color onto. Since theses calculations are in floats and have to be rounded to integers, it may happen that some pixels are missed.
To fix this you need an anti-aliasing filter. There are many different types such as low pass (blurring), oversampling etc.
Since these holes almost always are one pixel I'd create a function that identifies these holes and fills them with the average of it's neighbours colors. The problem is that Pygame is not very good at manually manipulating pixels, so it can be slow depending on the size of the image. Although, Pygame has a module called surfarray that's built on numpy which allows you to access pixels easier and faster, so that will speed it up some. Of course, it'll require you to install numpy.
I couldn't get your program to work, so next time make sure you really have a Minimal, Complete, and Verifiable example. The following code is just based on the image you provided.
import numpy as np
import pygame
pygame.init()
RADIUS = 1080 // 2
FPS = 30
screen = pygame.display.set_mode((RADIUS * 2, RADIUS * 2))
clock = pygame.time.Clock()
circle_size = (RADIUS * 2, RADIUS * 2)
circle = pygame.Surface(circle_size)
background_color = (255, 255, 255)
circle_color = (255, 0, 0)
pygame.draw.circle(circle, circle_color, (RADIUS, RADIUS), RADIUS, RADIUS // 2)
def remove_holes(surface, background=(0, 0, 0)):
"""
Removes holes caused by aliasing.
The function locates pixels of color 'background' that are surrounded by pixels of different colors and set them to
the average of their neighbours. Won't fix pixels with 2 or less adjacent pixels.
Args:
surface (pygame.Surface): the pygame.Surface to anti-aliasing.
background (3 element list or tuple): the color of the holes.
Returns:
anti-aliased pygame.Surface.
"""
width, height = surface.get_size()
array = pygame.surfarray.array3d(surface)
contains_background = (array == background).all(axis=2)
neighbours = (0, 1), (0, -1), (1, 0), (-1, 0)
for row in range(1, height-1):
for col in range(1, width-1):
if contains_background[row, col]:
average = np.zeros(shape=(1, 3), dtype=np.uint16)
elements = 0
for y, x in neighbours:
if not contains_background[row+y, col+x]:
elements += 1
average += array[row+y, col+x]
if elements > 2: # Only apply average if more than 2 neighbours is not of background color.
array[row, col] = average // elements
return pygame.surfarray.make_surface(array)
def main():
running = True
image = pygame.image.load('test.png').convert()
# image = circle
pos = image.get_rect(center=(RADIUS, RADIUS))
while running:
clock.tick(FPS)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
elif event.key == pygame.K_1:
print('Reset circle.')
image = circle
elif event.key == pygame.K_2:
print('Starting removing holes.')
time = pygame.time.get_ticks()
image = remove_holes(image, background=(255, 255, 255))
time = pygame.time.get_ticks() - time
print('Finished removing holes in {:.4E} s.'.format(time / 1000))
screen.fill(background_color)
screen.blit(image, pos)
pygame.display.update()
if __name__ == '__main__':
main()
Result
Before
After
Time
As I said before, it's not a very fast operation. Here are some benchmarks based on the circle in the example:
Surface size: (100, 100) | Time: 1.1521E-02 s
Surface size: (200, 200) | Time: 4.3365E-02 s
Surface size: (300, 300) | Time: 9.7489E-02 s
Surface size: (400, 400) | Time: 1.7257E-01 s
Surface size: (500, 500) | Time: 2.6911E-01 s
Surface size: (600, 600) | Time: 3.8759E-01 s
Surface size: (700, 700) | Time: 5.2999E-01 s
Surface size: (800, 800) | Time: 6.9134E-01 s
Surface size: (900, 900) | Time: 9.1454E-01 s
And with your image:
Time: 1.6557E-01 s
I'm writing a class in pygame to create a sprite object, and I'd like to be able to rotate it. It works fine with an image, and rotates without issue. But when rotating a surface with a plain colour, the box appears to grow and shrink. I know that this is a result of the surface changing size to fit the vertices of the rectangle inside, but how do I stop it? I'd like to see a visual rotation.
I've created some sample code to show the problem that I'm facing, running it causes the box to simply change in size.
import sys, pygame
from pygame.locals import *
pygame.init()
SCREEN = pygame.display.set_mode((200, 200))
CLOCK = pygame.time.Clock()
surface = pygame.Surface((50 , 50))
surface.fill((0, 0, 0))
rotated_surface = surface
rect = surface.get_rect()
angle = 0
while True:
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
SCREEN.fill((255, 255, 255))
angle += 5
rotated_surface = pygame.transform.rotate(surface, angle)
rect = rotated_surface.get_rect(center = (100, 100))
SCREEN.blit(rotated_surface, (rect.x, rect.y))
pygame.display.update()
CLOCK.tick(30)
How do I fix this issue, to make the surface rotate how I want?
Any help would be appreciated!
You have to create the Surface objects you create to stamp on the display surface in a way they use transparency information (That is - they have to have an alpha channel).
To do that, is just a question of passing the appropriate flag when creating your surface objects - simply replace this:
surface = pygame.Surface((50 , 50))
with:
surface = pygame.Surface((50 , 50), pygame.SRCALPHA)
and it should work.
I managed to string together a script that receives commands from an iOS app setting velocity and a direction.
The thing is I do not have the actual device, so my app instead sends commands to a little python web socket server I built that uses tornado...
Essentially what I would ideally need is a way to:
Display a window
Every 17ms, clear the window, read a global variable with x and y and draw a point or circle at x and y.
Is there a convenient way to do this so I can visually see what's going on?
If I can get something to draw a circle in a window every X ms, I can handle the rest.
What needs to be added:
-create a window
-create a timer
on timer callback: clear screen and draw a circle in the window.
You should try using pygame for graphics work.
First download pygame
Here is a sample code
import pygame,sys
from pygame import *
WIDTH = 480
HEIGHT = 480
WHITE = (255,255,255) #RGB
BLACK = (0,0,0) #RGB
pygame.init()
screen = display.set_mode((WIDTH,HEIGHT),0,32)
display.set_caption("Name of Application")
screen.fill(WHITE)
timer = pygame.time.Clock()
pos_on_screen, radius = (50, 50), 20
while True:
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
timer.tick(60) #60 times per second you can do the math for 17 ms
draw.circle(screen, BLACK, pos_on_screen, radius)
display.update()
HOPE THAT HELPS. Remember you need to download pygame first.
You should also read up on pygame. It is really helpful.
You could use your terminal as a "window" and draw a "circle" in it. As a very simple (and unreliable) "timer", time.sleep() function could be used:
#!/usr/bin/env python
"""Print red circle walking randomly in the terminal."""
import random
import time
from blessings import Terminal # $ pip install blessings colorama
import colorama; colorama.init() # for Windows support (not tested)
directions = [(-1, -1), (-1, 0), (-1, 1),
( 0, -1), ( 0, 1),
( 1, -1), ( 1, 0), ( 1, 1)]
t = Terminal()
with t.fullscreen(), t.hidden_cursor():
cur_y, cur_x = t.height // 2, t.width // 2 # center of the screen
nsteps = min(cur_y, cur_x)**2 # average distance for random walker: sqrt(N)
for _ in range(nsteps):
y, x = random.choice(directions)
cur_y += y; cur_x += x # update current coordinates
print(t.move(cur_y, cur_x) +
t.bold_red(u'\N{BLACK CIRCLE}')) # draw circle
time.sleep(6 * 0.017) # it may sleep both less and more time
print(t.clear) # clear screen
To try it, save the code into random-walker.py and run it:
$ python random-walker.py
I don't know whether it works on Windows.
1. Create a window
2. Set a timer instance and repetition tasks
3. Draw a circle