Speed up double for loop PyGame draw - python

I have a pygame game and it is currently bottlenecking in the Draw to screen process. This is the code (pg is pygame):
def draw_living_cells(self):
self.screen.fill(BLACK)
for x in range(0, GRID_WIDTH + 1):
for y in range(0, GRID_HEIGHT):
if self.grid[x + 1][y + 1] == 1:
pos = (int(x * CELL_SIZE), int(y * CELL_SIZE), int(CELL_SIZE), int(CELL_SIZE))
pg.draw.rect(self.screen, LIFE_COLOR, pos, 0)
pg.display.flip()
I thought multiprocessing could help, but I'm not sure how to implement, if it is possible (due to possible shared memory issues) or if it would help at all.
This process takes about 20ms with a self.grid of size 200x150 in a 800x600 display. I think its odd to have ~50fps in such a simple process.

Use pygame.PixelArray for direct pixel access of the target Surface. Set the pixels directly, instead of drawing each cell separately by pygame.draw.rect():
def draw_living_cells(self):
self.screen.fill(BLACK)
pixel_array = pg.PixelArray(self.screen)
size = self.screen.get_size()
for x in range(0, GRID_WIDTH + 1):
for y in range(0, GRID_HEIGHT):
if self.grid[x + 1][y + 1] == 1:
rect = pygame.Rect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE)
pixel_array[rect.left : rect.right, rect.top : rect.bottom] = LIFE_COLOR
pixel_array.close()
pg.display.flip()

I would suggest that you modify your code to use pygame's Sprite mechanics and in particular look at the sprite group pygame.sprite.DirtySprite. You can then mark the cells that have changed as dirty and have it only redraw those cells instead of all the cells.
This change would also require you to not redraw the entire background with the self.screen.fill(BLACK).
Since your method is named draw_living_cells(), that implies that there are dead cells that you do not redraw. Since you would not be filling the entire background that means that you have to draw the background onto the screen where the dead cell used to be.
This of course only helps if some the of the cells do not change each pass. Otherwise you are just adding overhead without saving drawing.
Though I recommend Sprites and pygame.sprite.DirtySprite, you can of course do something similar yourself by just marking your cells and doing that yourself in your redraw.

Finally, I tried something similar to what Glenn suggested. I used numpy to obtain where the living cells are and then iterate through those coordinates instead of the whole grid. This gave 2.5~3x performance increase. New Code:
def draw_living_cells(self):
self.screen.fill(BLACK)
living_cells_xy = np.where(self.grid == 1)
living_cells = len(living_cells_xy[0])
for i in range(0, living_cells):
x = living_cells_xy[0][i] - 1
y = living_cells_xy[1][i] - 1
pos = (int(x * CELL_SIZE), int(y * CELL_SIZE), int(CELL_SIZE), int(CELL_SIZE))
pg.draw.rect(self.screen, LIFE_COLOR, pos, 0)
pg.display.flip()

Related

Making a sliding puzzle game using turtle (not tkinter or pygame)

So I'm currently trying to program a slidepuzzle game without importing tkinter or pygame. So far i've generated a board and populated it with working buttons (quit,load,reset) but I'm really lost on how to program the actual slide puzzle game with the images i've been provided.
This code generates the screen and buttons that makeup my board. clicking the load button (which i already have setup) allows the user to type in the puzzle they want to load and unscramble. The issue is that I don't know how to get all the images onto the board and Im not sure what direction i should go in to actually program the game elements (it's just a screen and buttons right now). I'm a newbie programmer so any help is really appreciated.
screen = turtle.Screen()
def generate_screen():
`os.chdir('Resources') # Changes directory to allow access to .gifs in Resources
screen.setup(700, 700)
screen.title("Sliding Puzzle Game")
screen.tracer(0)
generate_scoreboard()
generate_leaderboard()
iconturtle = turtle.Turtle()
iconturtle.penup()
for file in os.listdir():
screen.register_shape(file)
iconturtle.goto(280, -270)
iconturtle.shape('quitbutton.gif')
iconturtle.stamp()
iconturtle.goto(180, -270)
iconturtle.shape('loadbutton.gif')
iconturtle.stamp()
iconturtle.goto(80, -270)
iconturtle.shape('resetbutton.gif')
iconturtle.stamp()`
`def load_yoshi():
os.chdir('Images\\yoshi')
screen.tracer(1)
screen.register_shape('yoshi_thumbnail.gif')
t = turtle.Turtle()
t.penup()
t.shape('yoshi_thumbnail.gif')
t.goto(250,290)
t.stamp()
screen.update()
files = glob.glob('*.gif') # pulling out only .gif
images = files
print(images)
for file in images:
screen.register_shape(file)`
I've only seen turtle used to draw lines, not shapes, much less
movable game pieces. I think pygame would definitely be better for
this – OneCricketeer
Below is an example slide game simplified from an earlier answer I wrote about creating numbered tiles using turtle:
from turtle import Screen, Turtle
from functools import partial
from random import random
SIZE = 4
TILE_SIZE = 100
OFFSETS = [(-1, 0), (0, -1), (1, 0), (0, 1)]
CURSOR_SIZE = 20
def slide(tile, row, col, x, y):
tile.onclick(None) # disable handler inside handler
for dy, dx in OFFSETS:
try:
if row + dy >= 0 <= col + dx and matrix[row + dy][col + dx] is None:
matrix[row][col] = None
row, col = row + dy, col + dx
matrix[row][col] = tile
x, y = tile.position()
tile.setposition(x + dx * TILE_SIZE, y - dy * TILE_SIZE)
break
except IndexError:
pass
tile.onclick(partial(slide, tile, row, col))
screen = Screen()
matrix = [[None for _ in range(SIZE)] for _ in range(SIZE)]
offset = TILE_SIZE * 1.5
for row in range(SIZE):
for col in range(SIZE):
if row == SIZE - 1 == col:
break
tile = Turtle('square', visible=False)
tile.shapesize(TILE_SIZE / CURSOR_SIZE)
tile.fillcolor(random(), random(), random())
tile.penup()
tile.goto(col * TILE_SIZE - offset, offset - row * TILE_SIZE)
tile.onclick(partial(slide, tile, row, col))
tile.showturtle()
matrix[row][col] = tile
screen.mainloop()
Click on a tile next to the blank space to have it move into that space:

how to prevent two masks from overlapping in pygame?

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.

How to make objects move in pygame, without making the rendering slow?

I am currently designing an app, using pygame in which I have a number of circles connected through lines, with numerical text written in them. These circles are green, blue and red in color, while the other things are black. Background is white. (Imagine it like a network graph)
My Objective: I am trying to get an animation running, in which the user selects the two circles (let us call them nodes) and I find out the shortest path between the sender node (green) to the receiver node (red). So in this animation, I am making another moving circle on top of the line (or edge) that connects the two adjacent nodes (these may be the intermediate nodes).
So far all good, here's the code of what I am doing:
def runPathAnimation(path, colortype):
for i in range(len(path)-1):
#Calculation of the center of the nodes
x1, y1 = (gmd[path[i]].getNodePosition())[0], (gmd[path[i]].getNodePosition())[1]
x2, y2 = (gmd[path[i+1]].getNodePosition())[0], (gmd[path[i+1]].getNodePosition())[1]
#Get the slope
m = (y1-y2)/(x1-x2) if x1 != x2 else 'undefined'
if str(m) != 'undefined':
c = y2-(m*x2)
if m > 0.5 or (m <= -1 and m >= -1.5):
for y in range(min(y1,y2),max(y1,y2)):
#using the equation of the line
x = int((y-c)/m)
#redrawEverything(path) #OPTION 1
#TRY REDRAW LINE #TODO
pyg.draw.rect(screen, (255, 255, 255), (x-10,y-10,20,20)) #OPTION 2
pyg.draw.circle(screen, colortype, (x,y), 10) #Moving circle
pyg.display.update() #Update Display
#NEED: Redraw!
#The logic repeats....
else:
for x in range(min(x1,x2),max(x1,x2)):
y = int(m*x+c)
#redrawEverything(path)
#TRY REDRAW LINE
pyg.draw.rect(screen, (255, 255, 255), (x-10,y-10,20,20))
pyg.draw.circle(screen, colortype, (x,y), 10)
pyg.display.update()
#NEED: Redraw!
else:
cy = range(min(y1,y2),max(y1,y2))
if y1 > y2:
cy = reversed(cy)
for y in cy:
#redrawEverything(path)
#TRY REDRAW LINE
pyg.draw.rect(screen, (255, 255, 255), (x1-10,y-10,20,20))
pyg.draw.circle(screen, colortype, (x1,y), 10)
pyg.display.update()
#NEED: Redraw!
My Problem: There is a lot of lag with my method of simply updating a circle with another position, without disturbing anything that it covers. I had 2 options in my mind:
OPTION 1: Update everything on the screen (of course it did not give me a good performance)
OPTION 2: Update only the portion of the screen, which is what actually used. However, even with this method, I am not able to achieve a good performance for screen updation. I would like to later add a feature to control the speed of the animation, which may have a speed faster than the maximum performance of my code right now!
As you can see, I do not have any time.sleep() as of now. I would like to increase the performance of my code and then be able to add time.sleep() for a more controlled animation. My current pygame application is already running in parallel to another process, which I implemented using multiprocessing library.
Question: How do I make it faster?
My python version: 3.7.0, pygame version: 1.9.6
PS: Sorry for the length of the question
Try using
pygame.time.Clock().tick(**)
This is a command that allows you to choose the FPS you want to run your program with, allowing you to increase your rendering speed. If you decide to use this, put an integer that represents the FPS where I wrote the asterisks.
So, I found a workaround! Basically, I am unable a make the code o any faster due to pygame's own rendering abilities, even HW mode isn't improving the speed much.
Solution (more of a workaround):
I have added a layer of waiting period in which pygame takes snapshots of the rendered screen and stores the image in a self created cache, without updating the screen. Later, I just have a smooth operable screen which can be used to see the animation.
Here's the code:
def runPathAnimation(path, colortype):
index = 0
images = []
for i in range(len(path)-1):
x1, y1 = (gmd[path[i]].getNodePosition())[0], (gmd[path[i]].getNodePosition())[1]
x2, y2 = (gmd[path[i+1]].getNodePosition())[0], (gmd[path[i+1]].getNodePosition())[1]
m = (y1-y2)/(x1-x2) if x1 != x2 else 'undefined'
cx, cy = range(min(x1,x2),max(x1,x2)), range(min(y1,y2),max(y1,y2))
if y1 > y2:
cy = reversed(cy)
if x1 > x2:
cx = reversed(cx)
if str(m) != 'undefined':
con = y2-(m*x2)
if m > 0.5 or (m <= -1 and m >= -1.5):
for y in cy:
ev = pyg.event.get()
x = int((y-con)/m)
images.append(loadpath(x,y,path,colortype,index))
index += 1
r = pyg.draw.rect(screen, colortype, (md.WIDTH_NETWORKPLOT-250,md.PLOT_AREA[1]+30,index/5,20), 2)
pyg.display.update(r)
else:
for x in cx:
ev = pyg.event.get()
y = int(m*x+con)
images.append(loadpath(x,y,path,colortype,index))
index += 1
r = pyg.draw.rect(screen, colortype, (md.WIDTH_NETWORKPLOT-250,md.PLOT_AREA[1]+30,index/5,20), 2)
pyg.display.update(r)
else:
for y in cy:
ev = pyg.event.get()
images.append(loadpath(x1,y,path,colortype,index))
index += 1
r = pyg.draw.rect(screen, colortype, (md.WIDTH_NETWORKPLOT-250,md.PLOT_AREA[1]+30,index/5,20), 2)
pyg.display.update(r)
print('Loading...'+str((i+1)/len(path)*100)+'%')
runAnimation(images)
def runAnimation(images):
animate = True
img = 0
print('Start!')
while animate:
ev = pyg.event.get()
pyg.event.pump()
keys = pyg.key.get_pressed()
if keys[pyg.K_LEFT]:
img -= 1
if img < 0:
img = 0
if keys[pyg.K_RIGHT]:
img += 1
if img >= len(images) - 2:
img = len(images) - 2
if keys[pyg.K_q]:
animate = False
screen.blit(images[img],(0,0))
pyg.display.update((0, 0, md.WIDTH_NETWORKPLOT, md.PLOT_AREA[1]))
PS: In my code, md.xxx are the dimensions for my matplotlib and pygame screen.
IMPORTANT: This is just a workaround, not a solution!!

Drawing full screen chessboard pattern?

I'm working in OpenCV (camera calibration and then creating 3d model) and till now I always printed a checkerboard pattern on paper and then took pictures needed for calibration. I tried to find a way to draw the pattern on the full screen with pre-defined square sizes (so I could set that square size in the calibration process), but I only found the Python turtle module which seems to only be for drawing on part of screen, and it always draws an arrow on last square. I need to draw the pattern with some small offset from the screen borders and, inside those offsets, draw a checkerboard with uniform squares. Also, I saw some people are drawing patterns in GIMP, but not on the full screen.
OpenCV has the function drawChessboardCorners but it requires founded corners from previous imported images, which need to be calibrated, so I think it doesn't make sense.
If anybody has an idea how to solve this problem, either with some program or module in some programming language (Python if possible), I would be grateful.
Here is a simple code for generating the chessboard pattern. However, the diameter of the chessboard is in pixel unit.
import numpy as np
h = 6
w = 8
size = 100
checkerboard = 255.0 * np.kron([[1, 0] * (w//2), [0, 1] * (w//2)] * (h//2), np.ones((size, size)))
I only found turtle module which seems to be only for drawing on part
of screen and it always draws arrow on last square.
Let's dispense with these two issues by drawing a grid in a window the size of the screen with no arrow on the last square:
from turtle import Screen, Turtle
BLOCK_SIZE = 72 # pixels
CURSOR_SIZE = 20 # pixels
BORDER = 1 # blocks
screen = Screen()
screen.setup(1.0, 1.0) # display size window
width, height = screen.window_width(), screen.window_height()
screen.setworldcoordinates(0, 0, width, height)
block = Turtle('square', visible=False) # hide the cursor completely
block.pencolor('black')
block.shapesize(BLOCK_SIZE / CURSOR_SIZE)
block.penup()
x_count = width // BLOCK_SIZE - BORDER * 2
x_width = x_count * BLOCK_SIZE
x_start = (width - x_width) // 2
x_limit = x_width + (BORDER + 1) * BLOCK_SIZE
y_count = height // BLOCK_SIZE - BORDER * 2
y_height = y_count * BLOCK_SIZE
y_start = (height - y_height) // 2
y_limit = y_height + (BORDER + 1) * BLOCK_SIZE
screen.tracer(False)
for parity_y, y in enumerate(range(y_start, y_limit, BLOCK_SIZE)):
block.sety(y)
for parity_x, x in enumerate(range(x_start, x_limit, BLOCK_SIZE)):
block.fillcolor(['white', 'black'][(parity_y % 2) == (parity_x % 2)])
block.setx(x)
block.stamp()
screen.tracer(True)
screen.mainloop()
(Hide your dock in OS X if you want to cover even more of the screen.)
Unfortunately, this is drawing in pixel units which is arbitrary. See my answer about drawing in a standardized measure using the pixel pitch value of your display.
Prepare your chessboard pattern in your favourite graphics editor, save the file onto the computer you want to use to display it for your calibration, then just display it when needed. I think you might be over-thinking the problem...

Update one surface in Pygame

I have a game with a background made of tiles, some are static (grass, mud), but i want water to be flowing. i have created a surface called water, then i have a loop that iterates through a series of 10 pngs for the frames of the water flowing. i want to then update this surface 10x as often as the rest of the game, and blit it to the main surface at 30fps with the other objects.
However all i can achieve is no movement or the water flowing at insane speed(by updating the whole display in the water update loop.)
is there a way i can update just this surface?
here's my code:
#mud, grass and surface are defined earlier.
water = pygame.Surface((100,100))
#create mud tiles
for x in range(0,800,100):
for y in range(0, 500, 100):
screen.blit(mud,(x,y))
#create grass tiles
for x in range(400, 800, 100):
for y in range(0, 300, 100):
screen.blit(grass,(x,y))
#create filenames
for x in range(1,11):
if x < 10:
filename = "images\water\water1000" + str(x) + ".png "
else:
filename = "images\water\water100" + str(x) + ".png "
waterimg = pygame.image.load(filename)
#add to a surface, then tile the surface onto the game.
water.blit(waterimg,(0,0))
for x in range(100, 200, 100):
for y in range(0, 500, 100):
screen.blit(water, (x,y))
pygame.display.flip() #makes it update crazily. removing this line makes it not update at all.
allsprites.draw(screen)
pygame.display.flip()
It looks like you want to use pygame.display.update.
Just pass it a list of all the water tiles' rects, and it will only update those parts of the screen. The only thing is that you can't use it with pygame.OPENGL displays, apparently.
However, are you sure you want to animate your water at 300fps? It seems like you should just tell your draw method what tick you're up to, and use that to figure out which frame to display. e.g.
def draw(tick, (whatever other arguments you have...):
... #draw mud and grass
#the modulo operator % gets the remainder of the two numbers, so 12 % 10 = 2
filename = "images\water\water1000" + str(tick % 10) + ".png"
waterimg = pygame.image.load(filename)
... #blit the waterimg, but don't flip
Even better would be to load all your water tiles into a list before hand and use
waterimg = watertiles[tick % 10]
and number your images from 0-9 instead of 1-10.
Anyway, I hope this helps (and works).
Your code is not right. The general schema is (simplified: 1 update loop - 1 draw loop):
load_all_images_needed()
itime = time.time()
while 1:
now = time.time()
update(now, now - itime) # send absolute time and delta time
itime = now
draw()
flip()
You can use the absolute time to decide which frame water to use (i.e: water_images[int(now*10.0) % len(water_images)] for 10fps in water sprite)

Categories