Related
This question already has answers here:
Pygame: Draw single pixel
(6 answers)
Closed 4 months ago.
I'm trying to iterate through the numpy array and assigning a 0 - 255 value based on the distance to the mouse.
WIDTH and HEIGHT are in this case set to 400 and GRID[] is a numpy matrix with WIDTH and HEIGHT dimensions.
I'm using the window.set_at() function to draw each pixel on the screen with the color stored in the numpy matrix, I'm getting about 5 FPS.
Is there a more efficient way to handle this type of pixel processing, or should I switch to something like c++ & SFML
#update pixels
for y in range(HEIGHT):
for x in range(WIDTH):
#get color based on distance to mouse; 0 -> 255
mousePosition = pg.mouse.get_pos()
dx = mousePosition[0] - x
dy = mousePosition[1] - y
d = math.sqrt(abs(dx ** 2 + dy ** 2))
#constraining the distance value between 0 - 255
c = min(max(d, 0), 255)
GRID[x,y] = c
#draw pixels
for y in range(HEIGHT):
for x in range(WIDTH):
c = GRID[x,y]
window.set_at((x, y), (c, c, c))
What constitutes fast enough?
Tidying up your code to create a minimal example with your 400x400 resolution:
import math
import time
import pygame
width, height = 400, 400
pygame.init()
screen = pygame.display.set_mode((width, height))
screen.fill(pygame.Color("black"))
clock = pygame.time.Clock()
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# Graphics
# update pixels
start = time.time()
mx, my = pygame.mouse.get_pos()
for y in range(height):
for x in range(width):
distance = math.sqrt( (mx - x)**2 + (my - y)**2 ) # 2.2 FPS 0.455s
#constrain the distance value between 0 - 255
c = min(max(distance, 0), 255)
screen.set_at((x, y), (c, c, c))
# Update Screen
pygame.display.set_caption(f"FPS: {clock.get_fps():.1f} Pixel Processing {time.time() - start:.3f} s")
pygame.display.update()
pygame.quit()
This results in 2.2 FPS on my PC.
The Python math module has a function that calculates the hypotenuse, so it's probably a little more optimised. Change the calculation to:
distance = math.hypot(mx - x, my - y)
This increases my frame rate by 50% to 3, probably still too slow.
We can be smarter about the pixels we modify, instead of clamping the distance to 255, fill the screen with white and then if the distance is greater than 255, don't change the pixel.
screen.fill(pygame.Color("white"))
for y in range(height):
for x in range(width):
distance = math.hypot(mx - x, my - y)
if distance <= 255: # don't set far pixels
c = round(distance)
screen.set_at((x, y), (c, c, c))
This increases my frame rate to 12 when the mouse is in the corner, 8 in the middle. Perhaps this is approaching usable.
If you look at the documentation for surface.set_at(), it says that using get_at() and set_at is too slow and recommends using PixelArray or SurfArray. So we can create a SurfArray:
surfarray = pygame.surfarray.pixels3d(screen)
Then to set the pixel values, we replace screen.set_at():
surfarray[x, y] = (c,c,c)
Surprisingly and unfortunately this doesn't change the frame rate significantly. Maybe this requires hardware acceleration.
I also tried manually locking the surface before iterating through the pixels as suggested in the docs, but this made no significant improvement.
So lets consider what we're doing, drawing the same circle wherever the mouse is every frame. It will be faster if we draw the circle once, and then blit it every frame centered on the mouse position. To create the circle, it's similar to what's already been done:
size = 255 * 2
dist_image = pygame.Surface((size, size), pygame.SRCALPHA)
for y in range(size):
for x in range(size):
distance = math.hypot(255 - x, 255 - y)
if distance <= 255:
c = round(distance)
dist_image.set_at((x, y), (c, c, c))
Then our graphics update logic becomes:
screen.fill(pygame.Color("white"))
dist_rect = dist_image.get_rect(center=pygame.mouse.get_pos())
screen.blit(dist_image, dist_rect)
This runs at 60 FPS (max) and takes almost no processing time:
pygame pixel processing is slow
Yes it is. So don't process pixels.
Given your colour resolution of 256, this means we're really dealing with a bunch of coloured circles around the mouse cursor. By considering only these circles, you're specifically not processing all those other pixels that can never be anything other than colour-zero.
The code below implements the program by drawing circles about the mouse cursor where the circle radius is the known distance. So we're drawing a circle in the same "distance-colour". Assuming the circle is drawn using the midpoint circle algorithm, this means that it only needs to calculate 1/8 of the pixels, and the rest are just quadrant (octant?) reflected about an axis of circle-symmetry.
import pygame
import random
# Window size
WINDOW_WIDTH = 600
WINDOW_HEIGHT = 600
###
### MAIN
###
pygame.init()
window = pygame.display.set_mode( ( WINDOW_WIDTH, WINDOW_HEIGHT ) )
pygame.display.set_caption("Mouse Distance")
# Main loop
clock = pygame.time.Clock()
running = True
while running:
time_now = pygame.time.get_ticks()
# Handle user-input
for event in pygame.event.get():
if ( event.type == pygame.QUIT ):
running = False
# Paint the screen in a gradient centred about the mouse
window.fill( ( 0, 0, 0 ) ) # max distance
mouse_pos = pygame.mouse.get_pos()
for distance in range( 255 ):
colour = ( 255-distance, 255-distance, 255-distance )
pygame.draw.circle( window, colour, mouse_pos, distance, 2 ) # use a width of 2 so there's no "holes"
pygame.display.flip()
# Clamp FPS
clock.tick(60)
pygame.quit()
I don't have time to make the change right now, but this example should draw to a surface, and then blit() that surface to the window for painting. That way we only need to re-compute the surface when the mouse moves.
Say you move your object Upwards (0-degrees) that would be move_ip((1, 0))
or to the right 90-degrees (0, 1)
then 45-degrees would be (1, 1). You get my point.
But how would I move it, say 22 degrees? Math wise that would be around (0.5, 0.75) (not exactly accurate, but a float value is my point). Now how do I move_ip my unit to (0.5, 0.75)? You can't, because PyGame only moves in full integers, not floats. So the only solution you have is to make the value bigger, with for example (0.5 * 100, 0.75 * 100) so (50, 75). But this doesn't work because now my block is moving way too far/fast so it ends up jumping "through" walls and other objects. Sure I can just do * 2 or so, but you just end up with a smaller increment of the same "moving too fast" problem.
So how can I move character 10-degrees, but one-ish unit away (not 100 units away). I am using degrees, so a solution like move_degrees(degree=10, distance=1) would also be fine.
how do I move_ip my unit to (0.5, 0.75)
You can not.
Since pygame.Rect is supposed to represent an area on the screen, a pygame.Rect object can only store integral data.
The coordinates for Rect objects are all integers. [...]
The fraction part of the coordinates gets lost when the new position of the object is assigned to the Rect object. If this is done every frame, the position error will accumulate over time.
If you want to store object positions with floating point accuracy, you have to store the location of the object in separate variables respectively attributes and to synchronize the pygame.Rect object. round the coordinates and assign it to the location (e.g. .topleft) of the rectangle:
x += 0.5
y += 0.75
rect.topleft = round(x), round(y)
Minimal example:
replit.com live example PyGame-move-one-unit-in-degrees
import pygame
pygame.init()
window = pygame.display.set_mode((400, 400))
clock = pygame.time.Clock()
x, y = 200, 200
rect = pygame.Rect(0, 0, 20, 20)
run = True
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
x = (x + 0.5) % 400
y = (y + 0.75) % 400
rect.center = round(x), round(y)
window.fill(0)
pygame.draw.rect(window, (255, 0, 0), rect)
pygame.display.flip()
clock.tick(100)
pygame.quit()
exit()
I'm trying to make a visualisation for various sorting algorithms and I was playing around with Pygame to try and draw the rectangles that I need.
In the below code, the user is given multiples inputs: the lowest value of the list to be sorted, the highest value, and the number of elements the list is going to have. The elements are going to be randomly generated.
Then I'm getting the user's screen size so that I can have an appropriate window for the visualisation. Based on the visualisation window and the user's input, I'm setting up the width and height of the rectangles, so that each rectangle has the same width and that they are scaled based on the highest value.
Almost everything is nice and fine with this approach, but there's one thing that I can't figure out. It seems that setting the number of elements (n, in the code below) too high, the rectangles are not being drawn.
My asumption is that after a specific threshold, RECT_W, which is the width of the rectangles, becomes to small for Pygame to draw it.
What options do I have to solve it, except of having the number of elements smaller than a specific value?
import random
import pygame
import color_constants as colors
import ctypes
import copy
from pygame.locals import *
# Read data based on user's input
def readData():
listOfNumbers = []
data = dict()
print("Lowest Value: ")
numLow = int(input())
print("Highest Value: ")
numHigh = int(input())
print("Length of list: ")
n = int(input())
for i in range(0, n):
listOfNumbers.append(random.randint(numLow, numHigh))
origLst = copy.copy(listOfNumbers)
data.update({'lst': origLst})
data.update({'numLow': numLow})
data.update({'numHigh': numHigh})
data.update({'n': n})
data.update({'sorted': listOfNumbers})
return data
if __name__ == "__main__":
data = readData()
# Getting the user's screen size
user32 = ctypes.windll.user32
SCREENSIZE = user32.GetSystemMetrics(0)-100, user32.GetSystemMetrics(1)-100
SCREEN_W = SCREENSIZE[0]
SCREEN_H = SCREENSIZE[1]
# Setting and scaling the size of rectangle based on the number of elements (n)
# and the highest number (numHigh)
RECT_W = SCREEN_W // data['n']
RECT_H = SCREEN_H / (data['numHigh'])
# Setting up the color literals
RED = (255, 255, 255)
GRAY = (0, 0, 0)
pygame.init()
screen = pygame.display.set_mode(SCREENSIZE)
running = True
while running:
for event in pygame.event.get():
if event.type == QUIT:
running = False
screen.fill(GRAY)
for i in range(data['n']):
rect = Rect(i*RECT_W, 0, RECT_W, RECT_H * data['lst'][i])
rect.bottom = SCREEN_H
pygame.draw.rect(screen, RED, rect)
pygame.display.flip()
If data['n'] is greater than SCREEN_W, RECT_W is 0. A coordinate is truncated when drawing. You cannot draw a fraction of a pixel. The size of the rectangle can only be integral (0, 1, 2 ...). Hence, you cannot draw a rectangle with a size less than 1.
You can draw the rectangles on a large surface and scale down the surface. However, the rectangles get blurred. So, this is no good option.
I am currently creating a game in python with pygame and my AI is currently "seeing" my character through the walls and shoot at it, but the AI is not supposed to shoot. So my question is : how to prevent that ? I've thought about a line collision where the line goes from my AI to my character, and if this line collide a wall then this AI don't shoot.
Any help would be appreciated, thanks a lot !
This is a great question!
Your rectangle can be thought of as 4 lines:
(x, y) → (x+width, y) # top
(x+width, y) → (x+width, y+height) # right
(x, y+height) → (x+width, y+height) # bottom
(x, y) → (x, y+height) # left
Taking your intersecting line, it's possible to use the two-lines intersecting formula to determine if any of these lines intersect (but be careful of parallel lines!)
However the formula (specified in linked Wikipedia article) determines if the lines intersect anywhere on the 2D plane, so it needs to be further refined. Obviously the code can quickly throw away any intersections that occur outside the window dimensions.
Once the "infinite-plane" collision-point has been determined (which is a reasonably quick determination), then a more fine-grained intersection can be determined. Using Bresenham's algorithm, enumerate all the points in the intersecting line, and compare them with a 1-pixel rectangle based on each side of your square. This will tell you which side of the rectangle intersected.
If you only need to know if the rectangle was hit, just check the whole rectangle with pygame.Rect.collidepoint() for each point in the line.
Of course once you have all those points generated, it's easily to not bother with the 2D line collision, but for long lines the code must make a lot of checks. So testing the 2D intersection first really speeds it up.
Basically, is doesn't exist a method nor any pygame functionality to detect collisions with lines, that's why I had to come up with the solution I'm about to show.
Using the following link, at section formulas / Given two points on each line segment, you can find a formula to know if two lines intersect each other, and if they do, where exactly.
The basic idea is to check if for every ray in the lightsource there is an intersection with any of the four sides of the rectangle, if so, the lightray should end at that same side of the rectangle.
import pygame, math
pygame.init()
screen_width = 800
screen_height = 600
screen = pygame.display.set_mode((screen_width, screen_height))
pygame.display.set_caption('Rays')
pygame.mouse.set_visible(False)
DENSITY = 500
RADIUS = 1000
run = True
while run:
screen.fill('black')
rect = pygame.Rect(50, 200, 100, 50)
pygame.draw.rect(screen, 'red', rect)
for i in range(DENSITY):
mouse_pos = pygame.mouse.get_pos()
pos_fin = (RADIUS * math.cos(2*math.pi / DENSITY * i) + mouse_pos[0], RADIUS * math.sin(2*math.pi / DENSITY * i) + mouse_pos[1])
if rect.collidepoint(mouse_pos) == False:
for extrem_1, extrem_2 in [(rect.bottomright, rect.topright), (rect.topright, rect.topleft), (rect.topleft, rect.bottomleft), (rect.bottomleft, rect.bottomright)]:
deno = (mouse_pos[0] - pos_fin[0]) * (extrem_1[1] - extrem_2[1]) - (mouse_pos[1] - pos_fin[1]) * (extrem_1[0] - extrem_2[0])
if deno != 0:
param_1 = ((extrem_2[0] - mouse_pos[0]) * (mouse_pos[1] - pos_fin[1]) - (extrem_2[1] - mouse_pos[1]) * (mouse_pos[0] - pos_fin[0]))/deno
param_2 = ((extrem_2[0] - mouse_pos[0]) * (extrem_2[1] - extrem_1[1]) - (extrem_2[1] - mouse_pos[1]) * (extrem_2[0] - extrem_1[0]))/deno
if 0 <= param_1 <= 1 and 0 <= param_2 <= 1:
p_x = mouse_pos[0] + param_2 * (pos_fin[0] - mouse_pos[0])
p_y = mouse_pos[1] + param_2 * (pos_fin[1] - mouse_pos[1])
pos_fin = (p_x, p_y)
pygame.draw.aaline(screen, 'white', mouse_pos, pos_fin)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
pygame.display.update()
pygame.quit()
It is maybe not the best, and most optimised piece of code but at the end you should get something that works.
The easiest way to detect the collision between a rectangle and a line is to use pygame.Rect.clipline:
Returns the coordinates of a line that is cropped to be completely inside the rectangle. If the line does not overlap the rectangle, then an empty tuple is returned.
e.g.:
rect = pygme.Rect(x, y, width, height)
if rect.clipline((x1, y1), (x2, y2)):
print("hit")
Minimal example
import pygame
pygame.init()
window = pygame.display.set_mode((400, 400))
clock = pygame.time.Clock()
rect = pygame.Rect(180, 180, 40, 40)
speed = 5
lines = [((20, 300), (150, 20)), ((250, 20), (380, 250)), ((50, 350), (350, 300))]
run = True
while run:
clock.tick(100)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
keys = pygame.key.get_pressed()
rect.x += (keys[pygame.K_RIGHT] - keys[pygame.K_LEFT]) * speed
rect.y += (keys[pygame.K_DOWN] - keys[pygame.K_UP]) * speed
rect.centerx %= window.get_width()
rect.centery %= window.get_height()
color = "red" if any(rect.clipline(*line) for line in lines) else "green"
window.fill(0)
pygame.draw.rect(window, color, rect)
for line in lines:
pygame.draw.line(window, "white", *line)
pygame.display.flip()
pygame.quit()
exit()
I am trying to code a simple circle timer in Python using Pygame.
At the moment it looks like this:
As you can see, the blue line is very wavy and has white dots in it. I am achieving this blue line by using pygame.draw.arc() function, but it is not anti-aliased and looks bad. I would like it to be anti-aliased, but gfxdraw module which should let me achieve this, doesn't support arc width selection. Here's code snippet:
pygame.draw.arc(screen, blue, [center[0] - 120, center[1] - 120, 240, 240], pi/2, pi/2+pi*i*koef, 15)
pygame.gfxdraw.aacircle(screen, center[0], center[1], 105, black)
pygame.gfxdraw.aacircle(screen, center[0], center[1], 120, black)
I did it creating the arc with a polygon.
def drawArc(surface, x, y, r, th, start, stop, color):
points_outer = []
points_inner = []
n = round(r*abs(stop-start)/20)
if n<2:
n = 2
for i in range(n):
delta = i/(n-1)
phi0 = start + (stop-start)*delta
x0 = round(x+r*math.cos(phi0))
y0 = round(y+r*math.sin(phi0))
points_outer.append([x0,y0])
phi1 = stop + (start-stop)*delta
x1 = round(x+(r-th)*math.cos(phi1))
y1 = round(y+(r-th)*math.sin(phi1))
points_inner.append([x1,y1])
points = points_outer + points_inner
pygame.gfxdraw.aapolygon(surface, points, color)
pygame.gfxdraw.filled_polygon(surface, points, color)
The for loop could certainly be created more elegantly with a generator, but I am not very sophisticated with python.
The arc definitely looks nicer than pygame.draw.arc, but when I compare it to the screen rendering on my mac, there is room for improvement.
I am not aware of any pygame function that would solve this problem, meaning you basically have to program a solution yourself (or use something other than pygame), since draw is broken as you've noted and gfxdraw won't give you the thickness.
One very ugly but simple solution is to draw multiple times over the arc segments, always slightly shifted to "fill in" the missing gaps. This will still leave some aliasing at the very front of the timer arc, but the rest will be filled in.
import pygame
from pygame.locals import *
import pygame.gfxdraw
import math
# Screen size
SCREEN_HEIGHT = 350
SCREEN_WIDTH = 500
# Colors
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GREY = (150, 150, 150)
RED = (255,0,0)
# initialisation
pygame.init()
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
done = False
clock = pygame.time.Clock()
# We need this if we want to be able to specify our
# arc in degrees instead of radians
def degreesToRadians(deg):
return deg/180.0 * math.pi
# Draw an arc that is a portion of a circle.
# We pass in screen and color,
# followed by a tuple (x,y) that is the center of the circle, and the radius.
# Next comes the start and ending angle on the "unit circle" (0 to 360)
# of the circle we want to draw, and finally the thickness in pixels
def drawCircleArc(screen,color,center,radius,startDeg,endDeg,thickness):
(x,y) = center
rect = (x-radius,y-radius,radius*2,radius*2)
startRad = degreesToRadians(startDeg)
endRad = degreesToRadians(endDeg)
pygame.draw.arc(screen,color,rect,startRad,endRad,thickness)
# fill screen with background
screen.fill(WHITE)
center = [150, 200]
pygame.gfxdraw.aacircle(screen, center[0], center[1], 105, BLACK)
pygame.gfxdraw.aacircle(screen, center[0], center[1], 120, BLACK)
pygame.display.update()
step = 10
maxdeg = 0
while not done:
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
maxdeg = maxdeg + step
for i in range(min(0,maxdeg-30),maxdeg):
drawCircleArc(screen,RED,(150,200),119,i+90,max(i+10,maxdeg)+90,14)
#+90 will shift it from starting at the right to starting (roughly) at the top
pygame.display.flip()
clock.tick(2) # ensures a maximum of 60 frames per second
pygame.quit()
Note that I have copied degreesToRadians and drawCircleArc from https://www.cs.ucsb.edu/~pconrad/cs5nm/08F/ex/ex09/drawCircleArcExample.py
I do not generally recommend this solution, but it might do in a pinch.
You are right, some pygame rendering functions do indeed suck, so you can achieve something like this with PIL instead.
pie_size = (40, 40) # defining constants
pil_img = PIL.Image.new("RGBA", pie_size) # PIL template image
pil_draw = PIL.ImageDraw.Draw(pil_img) # drawable image
pil_draw.pieslice((0, 0, *[ps - 1 for ps in pie_size]), -90, 180, fill=(0, 0, 0)) # args: (x0, y0, x1, y1), start, end, fill
This will create a PIL shape. Now we can convert it to pygame.
data = pil_img.tobytes()
size = pil_img.size
mode = pil_img.mode
pygame_img = pygame.image.fromstring(data, size, mode).convert_alpha()
But don't forget to pip install pillow and
import PIL.Image
import PIL.ImageDraw
Ok, this is really old, but why not try to draw pies instead. For example draw a pie, then an unfilled circle as the outside ring and then a filled circle as the inside and another unfilled circle as the inside ring.
So pie -> unfilled circle -> filled circle -> unfilled.
The order is somewhat arbitrary but if u still have this problem give it a try. (Btw I haven't tried it but I think it will work)
For my own uses, I wrote a simple wrapper function, and to deal with the spotty arc drawing, I used an ugly loop to draw the same arc several times.
def DrawArc(surface, color, center, radius, startAngle, stopAngle, width=1):
width -= 2
for i in range(-2, 3):
# (2pi rad) / (360 deg)
deg2Rad = 0.01745329251
rect = pygame.Rect(
center[0] - radius + i,
center[1] - radius,
radius * 2,
radius * 2
)
pygame.draw.arc(
surface,
color,
rect,
startAngle * deg2Rad,
stopAngle * deg2Rad,
width
)
I'm aware this is not a great solution, but it works alright for my uses.
An important note is I added that "width -= 2" to hopefully preserve the intended size of the arc at least a little more accurately, but this results in increasing the minimum width by 2.
In your case, you might want to consider doing something more to fix the issues this results in.
If the start and end aren't all that important, one can create many circles following an arc trajectory and when done ie small circles drawn 360 time, you finally have a big circle with no wavy effect:
MWE:
#!/usr/bin/env python3
import pygame
import math
# Initialize pygame
pygame.init()
# Set the screen size
screen = pygame.display.set_mode((400, 300))
# Set the center point of the arc
center_x = 200
center_y = 150
arc_radius = 100
circle_radius = 6
# Set the start and stop angles of the arc
start_angle = 0
stop_angle = 360
angle_step = 1
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# Clear the screen
screen.fill((0, 0, 0))
# Draw the overlapping circles
for i in range(start_angle, stop_angle, angle_step):
angle = math.radians(i)
x = center_x + arc_radius * math.cos(angle)
y = center_y + arc_radius * math.sin(angle)
pygame.draw.circle(screen, "red", (int(x), int(y)), circle_radius)
# Update the display
pygame.display.flip()
pygame.quit()
Having a start_angle and stop_angle of 0 to 360 respectively yields a fill circle with an output:
To change it to a 1/3 of a circle, one would change the stop_angle from 360 to 120 (1/3 x 360 = 120) and this would then yield: