How to cycle image sprite in pygame - python

I am looking to create characters in a game using pygame and python version 2.7.6. I have a simple python script which allows me to create a window and print an image to the screen. This works successfully, however it is simply a static image.
import pygame
class Game(object):
def main(self, screen):
#load first sprite to image for blit"
image = pygame.image.load('sprites/sprite1.png')
image2 = pygame.image.load('sprites/sprite2.png')
image3 = pygame.image.load('sprites/sprite3.png')
while 1:
for event in pygame.event.get():
if event.type == pygame.QUIT:
return
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
return
screen.fill((200, 200, 200))
screen.blit(image, (320, 240))
pygame.display.flip()
if __name__ == '__main__':
pygame.init()
screen = pygame.display.set_mode((640, 480))
Game().main(screen)
I am looking to create a more dynamic image by changing the first sprite with the second and so on for at least three sprites possibly more (as sprite1.png, sprite2.png, sprite3.png). How can I cycle images after a given time interval has expired, for example: 1 second 1/2 seconds...

Lets go there from the top, when writing a game it's very important to keep the rendering loop away from the main loop of your program (the one that handles window events). This way even when something will get stuck in your rendering process, your window will be much less likely to become unresponsive. Among other benefits, for example people will be able to resize, and otherwise manipulate it, without interrupting the graphics thread.
There are some nice tutorials for it around the web, and while I don't have one at hand for python, SFML tutorials can teach you the right concepts which you can then use in python.
With that public service out of the way, when you already have the application split into separate threads, we are left with the issue of animations. Very simple implementation, and certainly not the best, would be to spawn a separate thread for each image that will cosist only of something like that:
def shake_it():
while True:
time.sleep(3)
#do rotation here
While this will work quite well, it is clearly not the best solution because it's very non-flexible. Imagine that now you would also like to do something else periodically, for example make your NPCs patrol the fort. This will require another thread, and this would quickly escalate to quite a high number of threads (which isn't bad in itself, but not a desired effect).
So to the rescue would be a much more generic thread - event handler. A class that will control all your functions that need to be executed at specific intervals. And then, in that additional thread you would use for animating images, you would run an instance of event_handler in the loop instead, which would work as an abstract for actions like animation.

Slightly more detail on #Puciek's suggestion in the comments, one could use a loop such as
import time
for img in [image, image1, image2]:
screen.fill((200, 200, 200))
screen.blit(img, (320, 240))
time.sleep(1) # sleep 1 second before continuing

To cycle through sprite images you should just blit the corresponding images in the loop. A good approach is to place all the images of the sequence in one image and load it when the program starts. Then use subsurface() function to get the image for blitting, so you can bind the "window" of the subsurface within the whole image with some variable which is changing in the loop. A separate question is, how to control the frequency, there are few approaches, here is one simple example of sprite animation. It is the same sprite sequence with 8 states, which is cycling at three different FPS: initial - 12 fps (1/12 second pause), then 1/3 of initial fps = 4 fps (3/12 second pause) and 1/12 of initial fps = 1 fps (12/12 = 1 second pause). I post the whole script, so you can run and see how it works, just put it with the image in one place and run the script.
Use this image with the script:
Source code:
import pygame, os
from pygame.locals import *
def Cycle(n, N) : # n - current state, N - max number of states
value = n+1
case1 = (value >= N)
case2 = (value < N)
result = case1 * 0 + case2 * value
return result
w = 600 # window size
h = 400
cdir = os.path.curdir # current directory in terminal
states = 8 # number of sprite states
pygame.init()
DISP = pygame.display.set_mode((w, h))
BG = (36, 0, 36) # background color
DISP.fill(BG)
band_1 = pygame.image.load(os.path.join(cdir, "band1.png")).convert()
K = 4 # global scale factor
band_1 = pygame.transform.scale(band_1, (K * band_1.get_width(), K * band_1.get_height()))
band_1.set_colorkey((0,0,0))
sw = K*8 # sprite width
sh = K*8 # sprite height
r0 = (100, 100, sw, sh) # sprite 0 rectangle
r1 = (150, 100, sw, sh) # sprite 1 rectangle
r2 = (200, 100, sw, sh) # sprite 2 rectangle
s0 = 0 # initial state of sprite 0
s1 = 0 # initial state of sprite 1
s2 = 0 # initial state of sprite 2
t0 = 1 # main ticker
t1 = 1 # ticker 1
t2 = 1 # ticker 2
Loop_1 = True
FPS = 12
clock = pygame.time.Clock( )
while Loop_1 :
clock.tick(FPS)
DISP.fill(BG, r0)
sprite_0 = band_1.subsurface((s0*sw, 0, sw, sh))
DISP.blit(sprite_0, r0)
s0 = Cycle(s0, states)
if t1 == 3 :
DISP.fill(BG, r1)
sprite_1 = band_1.subsurface((s1*sw, 0, sw, sh))
DISP.blit(sprite_1, r1)
s1 = Cycle(s1, states)
t1 = 0
if t2 == 12 :
DISP.fill(BG, r2)
sprite_2 = band_1.subsurface((s2*sw, 0, sw, sh))
DISP.blit(sprite_2, r2)
s2 = Cycle(s2, states)
t2 = 0
for event in pygame.event.get( ):
if event.type == QUIT : Loop_1 = False
if event.type == KEYDOWN:
if event.key == K_ESCAPE : Loop_1 = False
# t0 = t0 + 1
t1 = t1 + 1
t2 = t2 + 1
pygame.display.update()
pygame.quit( )

Related

Trouble making a Simple video renderer with the python module Pygame

I need some help creating a video renderer in Pygame. It has a 48x36 display and has 3 colours. Black, white and grey. The data that goes into the project is in the form of a .txt file and each frame of the video in a line(as an example my video uses 6567 frames so there are 6567 lines in the text file). To render the frames I am using the rectangle function.
pygame.draw.rect(canvas, rect_color, pygame.Rect(30,30,60,60))
(By the way in the .txt file 0=black, 1=white and 2=grey)
After finally making a window pop up it stayed a rather boring black color...
After finally making a window pop up it stayed a rather boring black color...
If you could give any help it would be greatly needed!
(The code is messy i know)
import time
from datetime import datetime
import pygame
file= open(r"C:\Users\User\Downloads\video Data.txt","r")
lines = file.readlines()
current_l = 1
start_time = time.perf_counter()
pygame.init()
surface = pygame.display.set_mode((480,360))
color = (150,75,75)
def start_vid():
current_l = 1
for frame in range(1, 6572):
xpos = 0
ypos = 0
now = datetime.now()
count = 0
seconds = now.second
frame_data = lines[current_l]
current = frame_data[count]
for y in range(0, 36):
for x in range(0, 48):
if current == '0':
pygame.draw.rect(surface, (0, 0, 255),[xpos, xpos+10, ypos, ypos+10], 0)
elif current == '1':
pygame.draw.rect(surface, (255, 255, 255),[xpos, ypos, xpos, ypos], 0)
else:
pygame.draw.rect(surface, (130, 130, 130),[xpos, ypos, xpos, ypos], 0)
#print(current)
#pygame.display.update()
xpos = xpos + 10
current = frame_data[count]
count = count + 1
timer = round(abs((start_time - time.perf_counter())), 1)
current_l = seconds*30
current_l = int(timer*30)
ypos = ypos + -10
print(current_l)
pygame.display.update()
start_vid()
The reason why you can't see anything is because in Pygame, the origin (0, 0) of the screen is in the top left. The right side of the screen is where the x increases, and the bottom is where the y increases. So, the line:
ypos = ypos + -10
draws everything "above" the screen, hence invisible. You need to remove the - sign.
I also saw a couple of things in your code that could be improved, such as:
The fact that you never close the data file. To do that automatically, you could use the with statement:
with open('video.txt') as file:
lines = file.readlines()
# the file is closed
This will allow other applications to access the file.
You are drawing empty rects
You are using the pygame.draw.rect method incorrectly. You should use rect objects like such:
rect = pygame.Rect(x, y, width, height)
pygame.draw.rect(surface, color, rect)
You don't need the time and datetime modules, Pygame handles time like this. For example:
framerate = 30
start = pygame.time.get_ticks()
... # main loop
current_frame = int((pygame.time.get_ticks()-start) / 1000 * framerate)
And, most importantly
In Pygame, you need a main loop with events handling:
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
...
pygame.quit()
quit()
A correct implementation of your code could look like this:
import pygame
with open('video.txt') as file:
lines = file.readlines()
pygame.init()
surface = pygame.display.set_mode((480,360))
framerate = 30
start = pygame.time.get_ticks()
run = True
while run:
# get pygame events
for event in pygame.event.get():
if event.type == pygame.QUIT: # user closes the window
run = False
current_frame = int((pygame.time.get_ticks()-start) / 1000 * framerate)
if current_frame < len(lines): # is the video still running?
count = 0
for y in range(36):
for x in range(48):
current_pixel = lines[current_frame][count] # current pixel data
if current_pixel == '0':
color = (0, 0, 0)
elif current_pixel == '1':
color = (127, 127, 127)
else:
color = (255, 255, 255)
pygame.draw.rect(surface, color, pygame.Rect(x*10, y*10, 10, 10))
count += 1 # next pixel
pygame.display.flip()
# user closed the window
pygame.quit()
quit()
(In this example, you can reduce the amount of resources used since you know the framerate of the video using pygame.time.Clock)

Make stars blink in Pygame

The following code generates random x,y coordinates, appending the coordinates to a list, then runs a for-loop through the list to blit star images to the screen. The same stars are constantly being redrawn while the code is running, but they're in the same location, so the screen looks static. Here is the code.
import time
import pygame
from random import randint
pygame.init()
display_width = 800
display_height = 600
gameDisplay = pygame.display.set_mode((display_width,display_height))
pygame.display.set_caption("My God, it's full of stars!")
med_star_img = pygame.image.load('images/medium_star.png')
tiny_star_img = pygame.image.load('images/tiny_star.png')
black = (0,0,0)
white = (255,255,255)
gray = (50,50,50)
tiny_stars_width = tiny_star_img.get_width()
tiny_stars_height = tiny_star_img.get_height()
med_stars_width = med_star_img.get_width()
med_stars_height = med_star_img.get_height()
tiny_stars_location = []
med_stars_location = []
clock = pygame.time.Clock()
running = True
gameDisplay.fill(gray)
# create random coordinates for stars
for i in range(25):
tiny_stars_location.append(pygame.Rect(randint(1,800),randint(1,600),tiny_stars_width,tiny_stars_height))
for i in range(10):
med_stars_location.append(pygame.Rect(randint(1,800),randint(1,600),med_stars_width,med_stars_height))
def make_med_star(x,y):
gameDisplay.blit(med_star_img, (x,y))
def make_tiny_star(x,y):
gameDisplay.blit(tiny_star_img, (x,y))
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
for star in med_stars_location:
make_med_star(star.x,star.y)
for star in tiny_stars_location:
make_tiny_star(star.x,star.y)
pygame.display.flip()
time.sleep(1)
clock.tick(60)
pygame.quit()
quit()
I'd like the stars to occasionally blink, which I think would be done by randomly removing one or two stars from the stars_location list during the main loop before adding them back when the loop comes back around. The loop probably executes very fast, but I think I can add a delay. Any help here would be greatly appreciated.
You can save a boolean for each star. If the value is True the star is visible.
Example for saving star data: (I wouldn't use the pygame.Rect and instead a simple list)
for i in range(25):
tiny_stars_location.append([randint(1,800),randint(1,600),tiny_stars_width,tiny_stars_height, True])
Also you should not use time.sleep in a pygame program. It does not delay a task. I delays the program. So you have to wait a whole second to close the program or interact with it in any way.
A lazy but working approach would be just to use randomness. You could just get a random number with random.randint(0, n), if it is 0 you set the boolean value of the star to True or False, depending on what state it currently is. You can set the variable n to some number like
n = maximum fps * average seconds before the star is hidden/shown
An other thing you probably just forgot is to clear the window in the loop with gameDisplay.fill(gray).
Your finished code could look like this:
import time
import pygame
from random import randint
pygame.init()
display_width = 800
display_height = 600
gameDisplay = pygame.display.set_mode((display_width,display_height))
pygame.display.set_caption("My God, it's full of stars!")
med_star_img = pygame.image.load('images/medium_star.png')
tiny_star_img = pygame.image.load('images/tiny_star.png')
black = (0,0,0)
white = (255,255,255)
gray = (50,50,50)
tiny_stars_width = tiny_star_img.get_width()
tiny_stars_height = tiny_star_img.get_height()
med_stars_width = med_star_img.get_width()
med_stars_height = med_star_img.get_height()
tiny_stars_location = []
med_stars_location = []
clock = pygame.time.Clock()
running = True
gameDisplay.fill(gray)
# create random coordinates for stars
for i in range(25):
tiny_stars_location.append([randint(1,800),randint(1,600),tiny_stars_width,tiny_stars_height, True])
for i in range(10):
med_stars_location.append([randint(1,800),randint(1,600),med_stars_width,med_stars_height, True])
def make_med_star(x,y):
gameDisplay.blit(med_star_img, (x,y))
def make_tiny_star(x,y):
gameDisplay.blit(tiny_star_img, (x,y))
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
for star in med_stars_location:
if star[2]:
make_med_star(star[0], star[1])
if not randint(0, 300): # 300 / 60(fps) = 5 -> every 5 seconds on average
star[2] = not star[2] # inverse
for star in tiny_stars_location:
if star[2]:
make_tiny_star(star[0], star[1])
if not randint(0, 300): # 300 / 60(fps) = 5 -> every 5 seconds on average
star[2] = not star[2] # inverse
pygame.display.flip()
#time.sleep(1) <- never do this in pygame
clock.tick(60)
gameDisplay.fill(gray) # reset display
pygame.quit()

Generate tiles in piano tiles consecutively in pygame

I have created a simple piano tiles game clone in pygame.
Everything working fine except the way i am generating tiles after every certain interval, but as the game speed increases this leaves a gap between two tiles.
In the original version of the game, there's no lag (0 distance ) between two incoming tiles.
Here's a preview of the game:
Currently I am generating tiles like this:
ADDBLOCK = pygame.USEREVENT + 1
ADDTIME = 650
pygame.time.set_timer(ADDBLOCK, ADDTIME)
if event.type == ADDBLOCK:
x_col = random.randint(0,3)
block = Block(win, (67.5 * x_col, -120))
block_group.add(block)
But with time, these tiles speed increase so there's remain a gap between generation of two tiles as shown by red line in the preview. Is there any way to generate tiles consecutively?
Source Code
Use a variable number to know how many tiles have been generated since the start. This variable will start with 0, then you will add 1 to this variable every time a tile is generated.
Then, you can use a variable like scrolling which increases continuously. You will add this scrolling to every tile y pos to render them.
Now you just have to add a tile which y position is like -tile_height - tile_height * number.
If that doesn't make sense to you, look at this MRE:
import pygame
from pygame.locals import *
from random import randint
pygame.init()
screen = pygame.display.set_mode((240, 480))
clock = pygame.time.Clock()
number_of_tiles = 0
tile_height = 150
tile_surf = pygame.Surface((60, tile_height))
tiles = [] # [column, y pos]
scrolling = 0
score = 0
speed = lambda: 200 + 5*score # increase with the score
time_passed = 0
while True:
click = None # used to click on a tile
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()
if event.type == MOUSEBUTTONDOWN:
click = event.pos
screen.fill((150, 200, 255))
if scrolling > number_of_tiles * tile_height:
# new tile
# use "while" instead of "if" if you want to go at really high speed
tiles.append([randint(0, 3), -tile_height - number_of_tiles * tile_height])
number_of_tiles += 1
for x, y in tiles:
screen.blit(tile_surf, (60 * x, y + scrolling))
if y + scrolling > 480: # delete any tile that is no longer visible
tiles.remove([x, y])
if click is not None and Rect((60 * x, y + scrolling), tile_surf.get_size())
.collidepoint(click):
tiles.remove([x, y]) # delete any tile that has been clicked
score += 1 # used to calculate speed
scrolling += speed() * time_passed
pygame.display.flip()
time_passed = clock.tick() / 1000

Faster drawing in python pygame

I made a program using Python, with pygame, that loads pictures of materials and then creates blocks and each block is assigned with random material.
Block is a class and in the drawing process, it iterates through the array with stored blocks, but that is very slow. Isn't there a faster method than storing them in array and iterating through?
class block:
def __init__(self, texture, x, y):
self.texture = texture
self.x = x
self.y = y
material = pygame.image
material.grass = pygame.image.load("textures/grass.png")
material.water = pygame.image.load("textures/water.png")
material.sand = pygame.image.load("textures/sand.png")
materials = [material.grass, material.water, material.sand]
white = (255,255,255);(width, height) = (2048, 1008);black = (0, 0, 0);screen = pygame.display.set_mode((width, height))
b_unit = 16
b = []
count = 0
cx = 0
cy = 0
while count < (width * height) / (b_unit * b_unit):
b.append(block(random.choice(materials), b_unit * cx, b_unit * cy))
cx += 1
count += 1
if cx == width / b_unit:
cx = 0
cy += 1
while True:
for block in b:
screen.blit(block.texture, (block.x + viewx, block.y + viewy))
pygame.display.flip()
I've already mentioned in the comments that you should (almost) always convert your images to improve the performance.
It can also help to blit separate images/pygame.Surfaces onto a big background surface and then just blit this background once per frame. I use two nested for loops here to get the coordinates and randomly blit one of two images.
I get around 120 fps if I use separate sprites (5184) here and ~430 fps with this single background image.
Of course I'm just blitting here and in a real game you'd probably have to store the rects of the tiles in a list or use pygame sprites and sprite groups, for example to implement collision detection or other map related logic, so the frame rate would be lower.
import itertools
import pygame as pg
from pygame.math import Vector2
BLUE_IMAGE = pg.Surface((20, 20))
BLUE_IMAGE.fill(pg.Color('lightskyblue2'))
GRAY_IMAGE = pg.Surface((20, 20))
GRAY_IMAGE.fill(pg.Color('slategray4'))
def main():
screen = pg.display.set_mode((1920, 1080))
clock = pg.time.Clock()
all_sprites = pg.sprite.Group()
images = itertools.cycle((BLUE_IMAGE, GRAY_IMAGE))
background = pg.Surface(screen.get_size())
# Use two nested for loops to get the coordinates.
for y in range(screen.get_height()//20):
for x in range(screen.get_width()//20):
# This alternates between the blue and gray image.
image = next(images)
# Blit one image after the other at their respective coords.
background.blit(image, (x*20, y*20))
next(images)
done = False
while not done:
for event in pg.event.get():
if event.type == pg.QUIT:
done = True
# Now you can just blit the background image once
# instead of blitting thousands of separate images.
screen.blit(background, (0, 0))
pg.display.set_caption(str(clock.get_fps()))
pg.display.flip()
clock.tick(1000)
if __name__ == '__main__':
pg.init()
main()
pg.quit()
Side notes: Don't add your images to the pygame.image module (that makes no sense at all).
material = pygame.image
material.grass = pygame.image.load("textures/grass.png")
Writing several statements in the same row separated with semicolons is really ugly and makes code less readable.
white = (255,255,255);(width, height) = (2048, 1008)

Pygame Rectangle and Mouse pos Collidepoint not working

I'm making a basic game where I have a surface and everytime I click on the surface it moves 5 pixels to the right. The program is working just fine without the checkCollide(event) function, but when I put the that condition it doesn't move. What is wrong?
My code until now is this
import pygame, sys
from pygame.locals import *
pygame.init()
DISPLAYSURF = pygame.display.set_mode((300,300))
def checkCollide(event):
k = 0
a,b = event.pos
x = P1[0].get_rect()
if x.collidepoint(a,b):
return True
return False
CP1 = [(150, 150)
,(155, 150)
,(160, 150)
,(165, 150)
,(170, 150)
,(175, 150)
,(180, 150)
,(185, 150)
,(190, 150)]
statp1_1 = 0
WHITE = (255,255,255)
DISPLAYSURF.fill(WHITE)
while True: # the main game loop
P1 = [pygame.image.load('PAzul.png'),CP1[statp1_1],statp1_1]
DISPLAYSURF.blit(P1[0], P1[1])
e = pygame.event.get()
for event in e:
if event.type == MOUSEBUTTONUP:
a = checkCollide(event)
if a:
DISPLAYSURF.fill(WHITE)
statp1_1 +=1
if event.type == QUIT:
pygame.quit()
sys.exit()
pygame.display.update()
Thank you
Check your logic in these lines of your function:
x = P1[0][0].get_rect()
if x.collidepoint(a,b):
return True
return False
Your code hinges on this bit:
a = checkCollide(event)
if a:
DISPLAYSURF.fill(WHITE)
So you're never evaluating this piece to be true.
I just realized what was wrong. When I do x = P1[0].get_rect() it creates a surface with topleft at (0,0).
What I needed to do was change the position of the rectangle using x.topleft = P1[1]
I've got some tips for you. First store the rect in the P1 list (it contains only the image and the rect in the following example, but maybe you could also add the statp1_1 index to it). Now we can just move this rect, if the user clicks on it (in the example I set the topleft attribute to the next point). Read the comments for some more tips. One thing you need to fix is to prevent the game from crashing when the statp1_1 index gets too big.
import sys
import pygame
pygame.init()
DISPLAYSURF = pygame.display.set_mode((300, 300))
WHITE = (255, 255, 255)
# Don't load images in your while loop, otherwise they have to
# be loaded again and again from your hard drive.
# Also, convert loaded images to improve the performance.
P1_IMAGE = pygame.image.load('PAzul.png').convert() # or .convert_alpha()
# Look up `list comprehension` if you don't know what this is.
CP1 = [(150+x, 150) for x in range(0, 41, 5)]
statp1_1 = 0
# Now P1 just contains the image and the rect which stores the position.
P1 = [P1_IMAGE, P1_IMAGE.get_rect(topleft=CP1[statp1_1])]
clock = pygame.time.Clock() # Use this clock to limit the frame rate.
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
if event.type == pygame.MOUSEBUTTONUP:
if P1[1].collidepoint(event.pos):
print('clicked')
statp1_1 += 1
# Set the rect.topleft attribute to CP1[statp1_1].
P1[1].topleft = CP1[statp1_1]
DISPLAYSURF.fill(WHITE)
DISPLAYSURF.blit(P1[0], P1[1]) # Blit image at rect.topleft.
pygame.display.update()
clock.tick(30) # Limit frame rate to 30 fps.

Categories

Resources