Related
I am trying to make a logic in which i can subtract the geometrical shapes. Lets say i have to subtract a circle from a rectangle.
The idea is to draw each point of the rectangle as a single individual point using win.set_at(). And then when we need to subtract the circle from the rectangle we an simply calculate that the point on which we are drawing and completing our square lies or not in the circle. If it lies then don't set pixels else draw the pixel. This is my code:
import pygame
WIDTH, HEIGHT = 700, 500
win = pygame.display.set_mode((WIDTH, HEIGHT))
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
def drawer(window, x, y, radius, s_l, s_w, s_x, s_y):
for i in range(0, HEIGHT):
for j in range(0, WIDTH):
distance = (x - j)**2 + (y - i)**2
if not distance <= radius**2 and i >= s_y and j >= s_x and i <= s_w + s_x and j <= s_l + s_y:
window.set_at((j, i), BLACK)
c_x, c_y = 130, 150
run = True
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
win.fill(WHITE)
drawer(win, c_x, c_y, 30, 100, 100, 100, 100)
pygame.display.flip()
pygame.quit()
But is it is quite slow. Can you suggest a better way (more optimized algorithm) of doing it.
For a good performance I suggest to use pygame.mask. Draw the shapes on pygame.Surface objects and create masks from the surfaces:
mask1 = pygame.mask.from_surface(shape1)
mask2 = pygame.mask.from_surface(shape2)
Invert the mask of the second shape and create the overlapping mask. Finally, create a new _Surface: from the overlapping mask:
mask2.invert()
subtract_maks = mask1.overlap_mask(mask2, (0, 0))
subtract_shape = subtract_maks.to_surface(setcolor = "red", unsetcolor=(0, 0, 0, 0))
Minimal example
import pygame
pygame.init()
window = pygame.display.set_mode((300, 300))
clock = pygame.time.Clock()
shape1 = pygame.Surface((200, 200), pygame.SRCALPHA)
pygame.draw.circle(shape1, "white", (100, 100), 100)
shape2 = pygame.Surface((200, 200), pygame.SRCALPHA)
pygame.draw.rect(shape2, "white", (25, 50, 150, 100))
mask1 = pygame.mask.from_surface(shape1)
mask2 = pygame.mask.from_surface(shape2)
mask2.invert()
subtract_maks = mask1.overlap_mask(mask2, (0, 0))
subtract_shape = subtract_maks.to_surface(setcolor = "red", unsetcolor=(0, 0, 0, 0))
background = pygame.Surface(window.get_size())
ts, w, h, c1, c2 = 50, *background.get_size(), (128, 128, 128), (64, 64, 64)
tiles = [((x*ts, y*ts, ts, ts), c1 if (x+y) % 2 == 0 else c2) for x in range((w+ts-1)//ts) for y in range((h+ts-1)//ts)]
[pygame.draw.rect(background, color, rect) for rect, color in tiles]
run = True
while run:
clock.tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
if event.type == pygame.KEYDOWN:
print(pygame.key.name(event.key))
window.blit(background, (0, 0))
window.blit(subtract_shape, (50, 50))
pygame.display.flip()
pygame.quit()
exit()
Here is the board I drew using pygame: https://i.stack.imgur.com/Hne6A.png
I'm facing a bug with the thickness of the last two lines as I marked them on the image. I believe there is something wrong with the if statement in my code but I can't quite figure it out, it just won't take effect.
here is the code that has drawn the board above:
import pygame, sys
pygame.init()
width, height = 750, 750
rows, cols = 9, 9
BLACK = (0,0,0)
screen = pygame.display.set_mode((width, height))
screen.fill((255, 255, 255, 255))
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
sys.exit()
surf = pygame.Surface((600, 600))
surf.fill((255,255,255))
padding = surf.get_width()/9
for i in range(rows+1):
if i % 3 == 0:
thick = 4
else:
thick = 1
pygame.draw.line(surf, BLACK, (0, i*padding), (width, i*padding), width=thick)
pygame.draw.line(surf, BLACK, (i*padding, 0), (i*padding, height), width=thick)
surf_center = (
(width-surf.get_width())/2,
(height-surf.get_height())/2
)
screen.blit(surf, surf_center)
pygame.display.update()
pygame.display.flip()
To draw a thick line, half the thickness is applied to both sides of the line. The simplest solution is to make the target surface a little larger and add a small offset to the coordinates of the lines.
For performance reasons, I also recommend creating the surface before the application loop and continuously blit it in the application loop:
import pygame, sys
pygame.init()
width, height = 750, 750
rows, cols = 9, 9
BLACK = (0,0,0)
screen = pygame.display.set_mode((width, height))
surf_size = 600
surf = pygame.Surface((surf_size+4, surf_size+4))
surf.fill((255,255,255))
padding = surf_size/9
for i in range(rows+1):
thick = 4 if i % 3 == 0 else 1
offset = i * padding + 1
pygame.draw.line(surf, BLACK, (0, offset), (width, offset), width=thick)
pygame.draw.line(surf, BLACK, (offset, 0), (offset, height), width=thick)
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
screen.fill((255, 255, 255, 255))
screen.blit(surf, surf.get_rect(center = screen.get_rect().center))
pygame.display.update()
pygame.display.flip()
pygame.quit()
sys.exit()
The code below should do the trick. The thing is that the width in pygame.draw.line is drawn in the middle of the line. So, the border lines are actually cropped in half because the half on one side and another half on the other side are not taken into account. But the catch is that if the thickness is even there is an offset of 1 on the borders as the width is larger on one of the sides.
Even if the thickness (bold_think) is raised or lowered the algorithm below will work flawlessly without any gaps outside the grid.
Thickness of 4
Thickness of 8
Thinkness of 15
import pygame
import sys
pygame.init()
width, height = 750, 750
rows, cols = 9, 9
BLACK = (0, 0, 0)
screen = pygame.display.set_mode((width, height))
screen.fill((255, 255, 255, 255))
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
sys.exit()
surf = pygame.Surface((600, 600))
surf.fill((255, 255, 255))
bold_thick = 4
# Calculates the offset of 1 for the even widths
thick_offset = (bold_thick - 1) % 2
offset = (bold_thick / 2) - thick_offset
# The padding should take into account the borders
padding = (surf.get_width() - bold_thick) / 9
for i in range(rows+1):
if i % 3 == 0:
thick = bold_thick
else:
thick = 1
pygame.draw.line(surf, BLACK, (0, i*padding + offset),
(width, i*padding + offset), width=thick)
pygame.draw.line(surf, BLACK, (i*padding + offset, 0),
(i*padding + offset, height), width=thick)
surf_center = (
(width-surf.get_width())/2,
(height-surf.get_height())/2
)
screen.blit(surf, surf_center)
pygame.display.update()
pygame.display.flip()
So I read the documentation of pygame but I could not understand it clearly. I recently asked a question about bitmap fonts and I got some code as my answer; here is the code:
import pygame
pygame.init()
win = pygame.display.set_mode((800, 600))
font = pygame.font.Font("freesansbold.ttf", 32)
i = 0
text = "hello how are you?"
run = True
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
letter = text[i]
text_1 = font.render(letter, True, (255, 255, 255))
bw, bh = font.size(letter)
glyph_rect = pygame.mask.from_surface(text_1).get_bounding_rects()
# print(glyph_rect)
if glyph_rect:
gh = glyph_rect[0].height
print(f'letter {letter} bitmap height: {bh} glyph height: {gh}')
win.fill((0, 0, 0))
win.blit(text_1, (0, 0))
pygame.display.update()
i += 1
run = i < len(text)
pygame.quit()
So, my questions are on the line glyph_rect = pygame.mask.from_surface(text_1).get_bounding_rects().
What does the pygame.mask.from_surface() function do?
What does the line glyph_rect = pygame.mask.from_surface(text_1).get_bounding_rects() do?
What arguments does the variable glyph_rect return, and what is the meaning of those arguments?
pygame.mask.from_surface creates a pygame.mask.Mask object form a pygame.Surface.
A Surface is bitmap. A Mask is an 2 dimensional array with Boolean values. The Mask created is the size of the _Surface. A field is True if the corresponding pixel in the surface is not transparent, and False if it is transparent.
pygame.mask.Mask.get_bounding_rects creates a list of pygame.Rect objects. Each rectangle describes a bounding area of connected pixles.
If the Surface contains exactly 1 connected image, you will get exactly 1 rectangle surrounding the image.
See the example. The black rectangle is the Surface rectangle and the red rectangle is the bound rectangle of the connected component:
repl.it/#Rabbid76/ImageHitbox
import pygame
def getMaskRect(surf, top = 0, left = 0):
surf_mask = pygame.mask.from_surface(surf)
rect_list = surf_mask.get_bounding_rects()
surf_mask_rect = rect_list[0].unionall(rect_list)
surf_mask_rect.move_ip(top, left)
return surf_mask_rect
pygame.init()
window = pygame.display.set_mode((400, 400))
clock = pygame.time.Clock()
try:
my_image = pygame.image.load('Bomb-256.png')
except:
my_image = pygame.Surface((200, 200), pygame.SRCALPHA)
pygame.draw.circle(my_image, (0, 128, 0), (60, 60), 40)
pygame.draw.circle(my_image, (0, 0, 128), (100, 150), 40)
run = True
while run:
clock.tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
pos = window.get_rect().center
my_image_rect = my_image.get_rect(center = pos)
my_image_mask_rect = getMaskRect(my_image, *my_image_rect.topleft)
window.fill((255, 255, 255))
window.blit(my_image, my_image_rect)
pygame.draw.rect(window, (0, 0, 0), my_image_rect, 3)
pygame.draw.rect(window, (255, 0, 0), my_image_mask_rect, 3)
pygame.display.flip()
pygame.quit()
exit()
For this, if you're familiar with it, think the dark mode in the boo levels in Super Mario Maker 2. I'm trying to create a circular spotlight around the character that will also make anything within the circles range visible (eg part of the floor being stood on, an enemy or anything else from the scene). My plan to do that is to first draw the circle/spotlight, then the scene and then the character. Then I want anything not highlighted by the spotlight to be blacked out.
So my question is:
Does anybody know how to fill the entire screen with the exception of what's within the circle?
I suggest a solution, which combines a clipping region pygame.Surface.set_clip and drawing a black rectangle with a circular transparent area in the center.
Define a radius and create a square pygame.Surface with twice the radius.
radius = 50
cover_surf = pygame.Surface((radius*2, radius*2))
Set a white color key which identifies the transparent color (set_colorkey) a nd draw a white (transparent) circle on the surface:
cover_surf.set_colorkey((255, 255, 255))
pygame.draw.circle(cover_surf, (255, 255, 255), (radius, radius), radius)
Define the center of the circular region which you want to see (in the following clip_center).
In the main application loop, clear the display and set the clipping region, the draw the scene. Before you update the display draw cover_surf in the clipping region:
while run:
# [...]
# clear screen and set clipping region
screen.fill(0)
clip_rect = pygame.Rect(clip_center[0]-radius, clip_center[1]-radius, radius*2, radius*2)
screen.set_clip(clip_rect)
# draw the scene
# [...]
# draw transparent circle and update display
screen.blit(cover_surf, clip_rect)
pygame.display.flip()
Minimal example: repl.it/#Rabbid76/PyGame-ClipCircularRegion-2
import pygame
pygame.init()
screen = pygame.display.set_mode((500, 500))
clock = pygame.time.Clock()
radius = 50
cover_surf = pygame.Surface((radius*2, radius*2))
cover_surf.fill(0)
cover_surf.set_colorkey((255, 255, 255))
pygame.draw.circle(cover_surf, (255, 255, 255), (radius, radius), radius)
run = True
while run:
clock.tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
clip_center = pygame.mouse.get_pos()
# clear screen and set clipping region
screen.fill(0)
clip_rect = pygame.Rect(clip_center[0]-radius, clip_center[1]-radius, radius*2, radius*2)
screen.set_clip(clip_rect)
# draw the scene
for x in range(10):
for y in range(10):
color = (255, 255, 255) if (x+y) % 2 == 0 else (255, 0, 0)
pygame.draw.rect(screen, color, (x*50, y*50, 50, 50))
# draw transparent circle and update display
screen.blit(cover_surf, clip_rect)
pygame.display.flip()
If you want multiple circular drawing areas, then create a pygame.Surface.set_clip with the same size as the display and set whit color key:
cover_surf = pygame.Surface((400, 400))
cover_surf.set_colorkey((255, 255, 255))
Fill the entire surface black and draw white circles on the surface:
cover_surf.fill(0)
pygame.draw.circle(cover_surf, (255, 255, 255), (100, 100), 50)
pygame.draw.circle(cover_surf, (255, 255, 255), (300, 300), 70)
Blit the cover_surf on the window, before updating the display:
while run:
# [...]
# draw transparent circle and update display
screen.blit(cover_surf, (0, 0))
pygame.display.flip()
Minimal example: repl.it/#Rabbid76/PyGame-ClipCircularRegion-3
import pygame
pygame.init()
screen = pygame.display.set_mode((400, 400))
clock = pygame.time.Clock()
cover_surf = pygame.Surface((400, 400))
cover_surf.set_colorkey((255, 255, 255))
px = [100, 200, 300]
dx = [1, 2, 3]
run = True
while run:
clock.tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
# create cover surface
cover_surf.fill(0)
for i in range(3):
radius = 40 + i*20
pygame.draw.circle(cover_surf, (255, 255, 255), (px[i], 100+(i*100)), radius)
px[i] += dx[i]
if px[i] < radius or px[i] > 400 - radius:
dx[i] = -dx[i]
# draw the scene
for x in range(10):
for y in range(10):
color = (255, 255, 255) if (x+y) % 2 == 0 else (255, 0, 0)
pygame.draw.rect(screen, color, (x*50, y*50, 50, 50))
# draw transparent circle and update display
screen.blit(cover_surf, (0, 0))
pygame.display.flip()
With pygame, I created a 20x20 pixel window and added a 2x2 pixel rectangle.
When I run the program, the window size is super small and I can barely see the rectangle. How can I increase the window size whilst keeping the number of pixels constant, i.e. increase the pixel size? I am aware of this similar question, but there a somewhat more complicated case is discussed.
import pygame
screen_width, screen_height = 20, 20
x, y = 10, 10
rect_width, rect_height = 2, 2
vel = 2
black = (0, 0, 0)
white = (255, 255, 255)
pygame.init()
win = pygame.display.set_mode((screen_width, screen_height))
run = True
while run:
pygame.time.delay(100)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
win.fill(black)
pygame.draw.rect(win, white, (x, y, rect_width, rect_height))
pygame.display.update()
pygame.quit()
Don't draw directly to the screen, but to another Surface.
Then scale that new Surface to the size of the screen and blit it onto the real screen surface.
Here's an example:
import pygame
screen_width, screen_height = 20, 20
scaling_factor = 6
x, y = 10, 10
rect_width, rect_height = 2, 2
vel = 2
black = (0, 0, 0)
white = (255, 255, 255)
pygame.init()
win = pygame.display.set_mode((screen_width*scaling_factor, screen_height*scaling_factor))
screen = pygame.Surface((screen_width, screen_height))
run = True
while run:
pygame.time.delay(100)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
screen.fill(black)
pygame.draw.rect(screen, white, (x, y, rect_width, rect_height))
win.blit(pygame.transform.scale(screen, win.get_rect().size), (0, 0))
pygame.display.update()
pygame.quit()