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)
Related
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()
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!!
Currently, My next project is going to be a platformer and when I look around stackoverflow for research on several mechanics, I see many people doing the same thing: They save a layout with some variable, then go and unload it somewhere and it just renders in the game. I was interested, so I looked further and I found nothing on how to load/unload states like that, or maybe I'm just not wording my search correctly.
Either way, How do I do this?
ex: I would save a level layout as either an array or a single multi-line string and then somehow generate a single tile sprite for each letter, like T.
import pygame
# Storage method A
level = '''
X X X X X
X X X X X
T T X X X
X X X T T
T T T T T
'''
# Storage Method B
level2 = [
'XXXXX',
'XXXXX',
'TTXXX',
'XXXTT',
'TTTTT'
]
# X is blank space, T is tiles
# Then what? Thats what I need to know.
# If someone already answered this and I'm just not using the right keywords let me know.
You will need to calculate the pixel-positions for each tile. To draw any tile, you need to know
the size of the canvas
the size of your grid
the position of the tile in your grid
1: Finding the size of your canvas should be trivial.
2: For the second storage method you can do
height = len(level2)
width = len(level2[0]) #Assuming all rows are of equal length and there's at least one row
3: We're going to iterate through the rows and characters which will keep track of our position in the grid on the side.
def draw_tiles(canvas_width, canvas_height, width, height, level2):
for row in range(height):
for column in range(width):
if list(level2[row])[column] == 'T':
pixel_x = int(canvas_width/width)*column
pixel_y = int(canvas_height/height)*row
draw_tile(pixel_x, pixel_y)
Now all you need to do is define the draw_tile(x, y) function to draw a tile on the canvas with its top-left corner being on the pixel co-ordinates (x, y). I'm sure pygame has something for that.
Make sure you set the grid width/height so that canvas_width/width and canvas_height/height are both integers. Otherwise your tiles will be slightly offset due to rounding.
You could iterate over the enumerated rows and characters in the layout, create the tile instances and add them to a sprite group.
In the example I just give the tiles different colors depending on the character in the layout (X=blue, T=green) before I add them to the group, but you could also create completely different Tile types or subclasses if the character is a 'T' or an 'X'.
import pygame
class Tile(pygame.sprite.Sprite):
def __init__(self, pos, color):
super().__init__()
self.image = pygame.Surface((50, 50))
self.image.fill(color)
self.rect = self.image.get_rect(topleft=pos)
def create_tile_group(layout):
"""Turn the layout into a sprite group with Tile instances."""
group = pygame.sprite.Group()
for y, row in enumerate(layout):
for x, tile in enumerate(row):
if tile == 'T':
color = (50, 150, 50)
else:
color = (0, 0, 200)
group.add(Tile((x*tile_size, y*tile_size), color))
return group
pygame.init()
screen = pygame.display.set_mode((250, 250))
clock = pygame.time.Clock()
layout1 = [
'XXXXX',
'XTXXX',
'XXXXT',
'XXXXX',
'TTTTT',
]
tile_size = 50
tile_group = create_tile_group(layout1)
loop = True
while loop:
for event in pygame.event.get():
if event.type == pygame.QUIT:
loop = False
tile_group.update()
screen.fill((30, 30, 30))
tile_group.draw(screen)
pygame.display.flip()
clock.tick(30)
If you get performance problems because you blit too many small surfaces, you could blit them onto a big background surface before the while loop starts and then just blit the background once each frame.
There is no magic here: "it just renders in the game" is not accurate. There's more software behind the rendering call, a module that defines the tile sprites, scans the level character by character, and places the sprites accordingly. The developer (e.g. you) decides on the level representation, sprite form, sprite placement, etc.
You have several bits of code to write. The good news is that you decide the format; you just have to stay consistent when you write those modules.
I am trying to create a simulator. (referring to John Zelle's graphics.py)
Basically, my object will make use of graphics.py to display the object as a circle. Then, using the .move method in the class in graphics.py, the object will move in the x direction and y direction. If the object is currently drawn, the circle is adjusted to the new position.
Moving just one object can easily be done with the following codes:
win = GraphWin("My Circle", 100, 100)
c = Circle(Point(50,50), 10)
c.draw(win)
for i in range(40):
c.move(30, 0) #speed=30
time.sleep(1)
win.close()
However, I want the program to display multiple circles at once that moves at different speed. I've created a Circle object class which takes speed as an input, and a list with 3 Circle objects in it
circle = []
circle1 = Car(40)
circle2= Car(50)
circle3 = Car(60)
In summary, my question is, how do make use of this list such that I am able to display and move multiple circles in one window at once using the methods available in graphics.py?
That all depends on how you create your Car class, but nothing stops you from using the same code to move multiple circles in the same refresh cycle, e.g.:
win = GraphWin("My Circle", 1024, 400)
speeds = [40, 50, 60] # we'll create a circle for each 'speed'
circles = [] # hold our circles
for speed in speeds:
c = Circle(Point(50, speed), 10) # use speed as y position, too
c.draw(win) # add it to the window
circles.append((c, speed)) # add it to our circle list as (circle, speed) pair
for i in range(40): # main animation loop
for circle in circles: # loop through the circles list
circle[0].move(circle[1], 0) # move the circle on the x axis by the defined speed
time.sleep(1) # wait a second...
win.close()
Of course, if you're already going to use classes, you might as well implement move() in it so your Car instances can remember their speed and then just apply it when you call move() on them in a loop.
creating an image of a cube for minecraft.
Trying to get the colors inside the box to change by themselves. Is there some kind of integer algorithm I can use to achieve this besides using random? Because right now, it creates random colors, but I want the little boxes to change colors by themselves. Any Ideas?
import turtle
import random
minecraft = turtle.Turtle()
minecraft.ht()
minecraft.speed(9999999999999) #I guess there is a max speed??? wanted it to make the mini cubes a lot faster.
#centers the box
minecraft.up()
minecraft.goto(-50,50)
minecraft.down()
#end of center box
for i in range(4): #Creates the box
minecraft.forward(100)
minecraft.right(90)
for i in range(1000): #Repeats all this code over and over
for i in range(10): #makes the 10 cubes going down, then it comes back up and repeates making cubes until it gets to the last cube.
for i in range(10): #initiate the random colors
red = random.random()
blue = random.random()
yellow = random.random()
minecraft.color(red, blue, yellow)
for i in range(1): #the little boxes
minecraft.begin_fill()
minecraft.forward(10)
minecraft.right(90)
minecraft.forward(10)
minecraft.right(90)
minecraft.forward(10)
minecraft.right(90)
minecraft.forward(10)
minecraft.right(90)
minecraft.end_fill()
minecraft.right(90) #little boxes changing directions
minecraft.forward(10)
minecraft.right(-90)
minecraft.forward(10) #little boxes changing directions...again
minecraft.right(-90)
minecraft.forward(100)
minecraft.right(90)
minecraft.right(180) #and again...
minecraft.forward(100)
minecraft.right(180)
Based on your description of the problem, this is what I believe you're asking for -- a grid of randomly colored boxes that appear to individually change color at random:
from turtle import Turtle, Screen
import random
BOX_SIZE = 100
SQUARE_SIZE = 10
DELAY = 100 # milliseconds
minecraft = Turtle(shape="square", visible=False)
minecraft.shapesize(SQUARE_SIZE / 20)
minecraft.speed("fastest")
# Center the box
minecraft.up()
minecraft.goto(-BOX_SIZE//2, BOX_SIZE//2)
minecraft.down()
# Create the box
for _ in range(4):
minecraft.forward(BOX_SIZE)
minecraft.right(90)
minecraft.up()
# Move turtle inside box
minecraft.forward(SQUARE_SIZE//2)
minecraft.right(90)
minecraft.forward(SQUARE_SIZE//2)
minecraft.left(90)
squares = []
for i in range(BOX_SIZE // SQUARE_SIZE):
# makes the 10 cubes going across, then it backs up and
# repeats making cubes until it gets to the last cube.
for j in range(BOX_SIZE // SQUARE_SIZE):
# initiate the random colors
red = random.random()
blue = random.random()
yellow = random.random()
minecraft.color(red, blue, yellow)
squares.append((minecraft.position(), minecraft.stamp()))
minecraft.forward(SQUARE_SIZE)
minecraft.backward(SQUARE_SIZE)
minecraft.sety(minecraft.ycor() - SQUARE_SIZE)
minecraft.right(180) # little boxes changing directions
def change():
random_choice = random.choice(squares)
squares.remove(random_choice)
position, stamp = random_choice
minecraft.goto(position)
red = random.random()
blue = random.random()
yellow = random.random()
minecraft.color(red, blue, yellow)
minecraft.clearstamp(stamp)
squares.append((minecraft.position(), minecraft.stamp()))
screen.ontimer(change, DELAY)
screen = Screen()
screen.ontimer(change, DELAY)
screen.exitonclick()
The key to this, like many turtle problems, is to stamp, not draw. Stamped images can do a number of things that drawn images can't -- in this case they can be individually removed and replace by other stamps.
Also, never let your turtle code run forever, nor any anywere close to it -- use an ontimer() event instead so that other turtle events (like properly closing the window) can trigger correctly.
random.random() create a random number falls between 0 and 1. So when you need a random number generate between 0 and MAX, just simply multiply with it, in your case, like this way:
int(random.random()*256)
BTW, I checked the turtle docs again. turtle.color(*args) expects two color args, "Return or set pencolor and fillcolor". That means you need to pass it turtle.color((40, 80, 120), (160, 200, 240)) this way.