How to run sound loops in sequence with pygame? - python

I've been experimenting with a weird idea that I got the other day, and I wanted to know if anyone knows a way to solve a problem that I encountered
So, what I wanted to do is a concatenation of sounds that loop a random amount of times and then moves to the next sound, which again loops for another random amount of times and then moves on to the next one and continues doing this for, let's say, 100 other sounds?
It's like a predetermined list of sounds that activates in a fixed order, but each sound has a "looping" variable, which essentially is a random integer between two numbers that determines how many times that sound loops before going to the next one.
I made this animation because I suck at explaining things.
I know this sounds stupid, pointless and a bit insane, but as I said, it's an experiment.
it seemed easy enough so I tried this:
(Btw I'm using Tkinter and pygame)
def activate():
random_number_1 = randint(0, 3)
sound1 = pygame.mixer.Sound("sound2.wav")
pygame.mixer.Sound.play(sound1, random_number_1)
random_number_2 = randint(0, 3)
sound2 = pygame.mixer.Sound("sound3.wav")
pygame.mixer.Sound.play(sound2, random_number_2)
random_number_3 = randint(0, 3)
sound3 = pygame.mixer.Sound("sound4.wav")
pygame.mixer.Sound.play(sound3, random_number_3)
random_number_4 = randint(0, 3)
sound4 = pygame.mixer.Sound("sound5.wav")
pygame.mixer.Sound.play(sound4, random_number_4)
random_number_5 = randint(0, 3)
sound5 = pygame.mixer.Sound("sound6.wav")
pygame.mixer.Sound.play(sound5, random_number_5)
button = Button(root, text="button", command=activate)
button.pack()
But the problem is that pygame doesn't wait for each of the loops to end, it plays every sound at the same time
Then, it occurred to me that using "For Loops" might solve the issue, maybe something like this but with sound:
def activate():
randomnumber1 = randint(0, 3)
for i in range(randomnumber1):
print(1)
randomnumber2 = randint(0, 3)
for i in range(randomnumber2):
print(2)
randomnumber3 = randint(0, 3)
for i in range(randomnumber3):
print(3)
randomnumber4 = randint(0, 3)
for i in range(randomnumber4):
print(4)
like this:
def activate():
randomnumber1 = randint(0, 3)
for i in range(randomnumber1):
sound1 = pygame.mixer.Sound("sound2.wav")
pygame.mixer.Sound.play(sound1)
randomnumber2 = randint(0, 3)
for i in range(randomnumber2):
sound2 = pygame.mixer.Sound("sound3.wav")
pygame.mixer.Sound.play(sound2)
randomnumber3 = randint(0, 3)
for i in range(randomnumber3):
sound3 = pygame.mixer.Sound("sound4.wav")
pygame.mixer.Sound.play(sound3)
randomnumber4 = randint(0, 3)
for i in range(randomnumber4):
sound4 = pygame.mixer.Sound("sound5.wav")
pygame.mixer.Sound.play(sound4)
but didn't work
I then remembered that winsound runs synchronously and, as far as I know, you can't specify how many times a sound loops, so I tried:
def activate():
randomnumber1 = randint(0, 3)
for i in range(randomnumber1):
winsound.PlaySound("sound2.wav", winsound.SND_ASYNC)
randomnumber2 = randint(0, 3)
for i in range(randomnumber2):
winsound.PlaySound("sound3.wav", winsound.SND_ASYNC)
randomnumber3 = randint(0, 3)
for i in range(randomnumber3):
winsound.PlaySound("sound4.wav", winsound.SND_ASYNC)
randomnumber4 = randint(0, 3)
for i in range(randomnumber4):
winsound.PlaySound("sound5.wav", winsound.SND_ASYNC)
but it also didn't work
Now I'm messing around with time.sleep and since each sound has a specific duration, I think I can calculate the time each loop has to wait.
like this, maybe:
sound2.wav lasts for 1.5s so let's say the random number is 2, that's 2 loops of a 1.5s sound clip
2 * 1.5s = 3s
random_number_1 = randint(0, 3)
sound1 = pygame.mixer.Sound("sound2.wav")
pygame.mixer.Sound.play(sound1, random_number_1)
time.sleep(random_number_1*1.5)
but so far it has been super wonky, messy and weird and the fact that time.sleep freezes Tkinter means it's almost entirely useless for me.
To wrap this up... if you know a way to make pygame sounds behave synchronously or if there's a simpler solution that I'm not seeing, please let me know.

An easy way of doing this is using the pygame.mixer.Channel() function set_endevent().
channel2 = pygame.mixer.Channel(1)
channel2.set_endevent( pygame.USEREVENT+1 )
This will send your event-queue whatever event you specify (here pygame.USEREVENT+1 ) when the sound stops playing.
I made a quick demonstration example. It plays a long background sound, and then a random short-sound continuously. When the pygame.USEREVENT+1 event is received, a new sound is started immediately.
import pygame
import random
# Window size
WINDOW_WIDTH = 400
WINDOW_HEIGHT = 400
WINDOW_SURFACE = pygame.HWSURFACE|pygame.DOUBLEBUF|pygame.RESIZABLE
DARK_BLUE = ( 3, 5, 54)
### initialisation
pygame.init()
pygame.mixer.init()
window = pygame.display.set_mode( ( WINDOW_WIDTH, WINDOW_HEIGHT ), WINDOW_SURFACE )
pygame.display.set_caption("Random Sound")
### sound
# create separate Channel objects for simultaneous playback
channel1 = pygame.mixer.Channel(0) # argument must be int
channel2 = pygame.mixer.Channel(1)
# define the event that's sent when a sound stops playing:
channel2.set_endevent( pygame.USEREVENT+1 )
# Rain sound from: https://www.freesoundslibrary.com/sound-of-rain-falling-mp3/ (CC BY 4.0)
rain_sound = pygame.mixer.Sound( 'rain-falling.ogg' )
channel1.play( rain_sound, -1 ) # loop the rain sound forever
# and ... All are (CC BY 4.0)
# https://www.freesoundslibrary.com/car-horn-sound-effect/
# https://www.freesoundslibrary.com/duck-quack/
# https://www.freesoundslibrary.com/cash-register-sound-effect/
# https://www.freesoundslibrary.com/turkey-gobble-call/
# https://www.freesoundslibrary.com/single-ding-sound-effect/
# https://www.freesoundslibrary.com/dog-bark-sound-effect/
#
horn = pygame.mixer.Sound( 'car-horn2.ogg' ) # converted from MP3 to OGG
quack = pygame.mixer.Sound( 'duck-quack.ogg' )
ca_ching = pygame.mixer.Sound( 'cash-register.ogg' )
bark = pygame.mixer.Sound( 'dog-bark.ogg' )
ding = pygame.mixer.Sound( 'single-ding.ogg' )
gobble = pygame.mixer.Sound( 'turkey-gobble.ogg' )
# Sound list
all_sounds = [ horn, quack, ca_ching, bark, ding, gobble ]
# Start with a random sound
channel2.play( random.choice( all_sounds ) )
### Main Loop
clock = pygame.time.Clock()
done = False
while not done:
# Handle user-input
for event in pygame.event.get():
if ( event.type == pygame.QUIT ):
done = True
elif ( event.type == pygame.USEREVENT+1 ):
# Channel2 sound ended, start another!
channel2.play( random.choice( all_sounds ) )
print( "Sound ended" )
# Update the window, but not more than 60fps
window.fill( DARK_BLUE )
pygame.display.flip()
# Clamp FPS
clock.tick_busy_loop(60)
pygame.quit()
A lot of the sounds from freesoundslibrary.com are quite long (e.g.: Multiple duck-quaks). I clipped them down to short-sounds with the tool Audacity, leaving a split-second of quiet on each side.
EDIT:
To change this to a pre-determined list, just keep a record of which is the current_sound.
# Sound list
all_sounds = [ horn, quack, ca_ching, bark, ding, gobble ]
# Start with the first sound
channel2.play( all_sounds[ 0 ] )
current_sound_index = 0
...
for event in pygame.event.get():
if ( event.type == pygame.QUIT ):
done = True
elif ( event.type == pygame.USEREVENT+1 ):
# Channel2 sound ended, start the next sound!
current_sound_index += 1
if ( current_sound_index < len( all_sounds ) ):
channel2.play( all_sounds[ current_sound_index ] )
print( "Sound ended" )
else:
print( "Played all sounds" )

Related

How to use multiprocessing in pygame

I am trying to print a million rectangles in 2-3 seconds, however, I can not understand how to use parallelization with GUI.
class Rectangle:
def __init__(self, pos, color, size):
self.pos = pos
self.color = color
self.size = size
def draw(self):
pygame.draw.rect(screen, self.color, Rect(self.pos, self.size))
I want to use something like open MP here to make this process faster
rectangles = []
for i in range(1000000):
random_color = (randint(0,255), randint(0,255), randint(0,255))
random_pos = (randint(0,639), randint(0,479))
random_size = (639-randint(random_pos[0], 639), 479-randint(random_pos[1],479))
rectangles.append(Rectangle(random_pos, random_color, random_size))
while True:
for event in pygame.event.get():
if event.type == QUIT:
exit()
screen.lock()
for rectangle in rectangles:
rectangle.draw()
screen.unlock()
pygame.display.update()
Here's an example that generates random rectangles in sub-processes. It's done this way, because it's not possible to draw to the main PyGame surface from anything other than the main thread/process. So if you want to generate items in real-time, do so, but then notify the main-thread somehow to include them. Perhaps by posting a PyGame Event.
The approach taken is to declare a shared subprocess.manager.list for the rectangles, then split the generation. The function rectGenerator() is given a position in the list from which to start, and a count to stop at.
With rectGenerator() as the target, /N/ sub-processes are created. We then loop waiting for the processes to all finish. A nicer approach might be to smarter about this and only paint the blocks that are complete.
Running on my workstation, it takes a while to paint one million rectangles (~10 seconds). To see for yourself, change BLOCK_SIZE to 100000. Probably this could be optimised (it was my first time using multiprocessing for some years), but I'd rather make the code illustrative.
So as I said in my comment, for simple rectangles it's much faster to just do it without sub-processes. But if each sub-process was doing some kind of CPU-bound complex rendering (fractal generating, image filtering, ray-tracing, etc. etc.) then this is a good way to spread the workload amongst your CPUs/cores.
Further reading: The python GIL.
#! /usr/bin/env python3
import pygame
import random
import multiprocessing
CHILDREN = 10 # processes to use
BLOCK_SIZE = 1000 # rectangles per process
RECT_COUNT = BLOCK_SIZE * CHILDREN # ensure an even factor / subprocess
# Window size
WINDOW_WIDTH = 800
WINDOW_HEIGHT = 800
def randomColour():
""" Generate a random "non dark" colour """
MIN = 80 # minimum brightness of colour channel
MAX = 255
return ( random.randint( MIN, MAX ),
random.randint( MIN, MAX ),
random.randint( MIN, MAX ) )
def randomRect( border=5 ):
""" Generate a random PyGame Rect, within the window bounds """
MAX_HEIGHT = WINDOW_HEIGHT - border
MAX_WIDTH = WINDOW_WIDTH - border
# Generate a rect that stays within the screen bounds
x1 = random.randint( border, MAX_WIDTH )
y1 = random.randint( border, MAX_HEIGHT )
x2 = random.randint( border, MAX_WIDTH )
y2 = random.randint( border, MAX_HEIGHT )
if ( x2 < x1 ):
xswap = x2
x2 = x1
x1 = xswap
if ( y2 < y1 ):
yswap = y2
y2 = y1
y1 = yswap
return pygame.Rect( x1, y1, x2-x1, y2-y1 )
class colourRect:
""" Simple class for holding components of a coloured rectangle """
def __init__( self ):
self.rect = randomRect()
self.colour = randomColour()
def draw( self, surface ):
pygame.draw.rect( surface, self.colour, self.rect, 1 )
def __str__( self ):
s = "rect[ %d,%d w=%d, h=%d ], colour[ %d, %d, %d ]" % ( self.rect.x, self.rect.y, self.rect.width, self.rect.height, self.colour[0], self.colour[1], self.colour[2] )
return s
###
### Multiprocessing Target Function
###
def rectGenerator( mp_list, index_from, count ):
""" Populate a section of the mp_list with randomly sized and coloured
rectangles, starting from the index, for the given count """
for i in range( count ):
mp_list[ index_from + i ] = colourRect()
###
### MAIN
###
pygame.init()
window = pygame.display.set_mode( ( WINDOW_WIDTH, WINDOW_HEIGHT ), pygame.HWSURFACE )
pygame.display.set_caption("Plenty o' Rectangles")
# Make a *Huge* list of rectangles
# Note: this runs much faster than using multiprocessing
#all_rects = [ None for i in range( RECT_COUNT ) ] # pre-allocate list
#rectGenerator( all_rects, 0, RECT_COUNT )
print( "### Setting-up Generation" )
manager = multiprocessing.Manager()
all_rects = manager.list( range( RECT_COUNT ) ) # pre-allocate list
subprocesses = [] # keep the process handles so we can watch them
print( "### Starting Generation" )
for i in range( CHILDREN ):
# each subprocess populates a separate region of the rectangle array
p = multiprocessing.Process( target=rectGenerator, args=( all_rects, i*BLOCK_SIZE, BLOCK_SIZE ) )
p.start()
subprocesses.append( p )
print( "### Waiting for Generation" )
# Wait for the generation to complete
found_running = True
while ( found_running ):
found_running = False
for p in subprocesses:
if ( not p.is_alive() ):
p.join()
else:
found_running = True
print( "### Generation Complete" )
print( "--------------------------" )
# 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 window with rects, a lot of rects takes a while
print( "### Painting starts" )
window.fill( ( 0,0,0 ) )
for r in all_rects:
r.draw( window )
pygame.display.flip()
print( "### Painting ends" )
# Clamp FPS
clock.tick(2) #slow
pygame.quit()

reading a tmx file (for pygame) while running the mainloop

so I'm making a game with pygame and I have some decently large tmx files for the map data. I was wondering if there is A, a way to speed it up or B, if I could simultaneously
run both the mainloop and load the map in. That way I could have a loading screen. I tried using multiprocessing but it said it cannot pickle pygame objects. Thanks!
Your code could load the TMX Map in a thread. This will allow the main loop to wait for a "finished loading" event, and do something else while waiting.
The example code below uses the pytmx library. Typically more code is needed to render the map into an image, but I left this out for the sake of making the answer shorter. Maybe it's useful to also post back more status information events during the load. This would allow the main-loop to display a progress indicator or suchlike.
Obviously this example does not do anything with the window, except fill it blue.
import pygame
import threading
import pytmx
WINDOW_WIDTH = 400
WINDOW_HEIGHT = 400
WINDOW_SURFACE = pygame.HWSURFACE|pygame.DOUBLEBUF|pygame.RESIZABLE
DARK_BLUE = ( 3, 5, 54 )
CREAM = ( 255, 254, 224 )
MAP_LOAD_DONE = pygame.USEREVENT + 1
MAP_LOAD_ERROR = pygame.USEREVENT + 2
# global to hold map
global_tmx_map = None
# Thread function to load map in the background
class MapLoadThread( threading.Thread ):
def __init__( self, map_name ):
threading.Thread.__init__(self)
self.daemon = True # exit with parent
self.map_name = map_name
def run( self ):
global global_tmx_map
new_event_args = {}
try:
global_tmx_map = pytmx.load_pygame( self.map_name, pixelalpha=True )
# TODO: do more TMX operations in the thread here.
new_event = pygame.event.Event( MAP_LOAD_DONE, new_event_args )
except Exception as e:
new_event_args[ 'exception' ] = e
new_event = pygame.event.Event( MAP_LOAD_ERROR, new_event_args )
# send the load/fail event to the main loop
pygame.event.post( new_event )
# ...
### Window initialisation
pygame.init()
pygame.mixer.init()
window = pygame.display.set_mode( ( WINDOW_WIDTH, WINDOW_HEIGHT ), WINDOW_SURFACE )
pygame.display.set_caption( "TMX Loader Thread Example" )
# Start the Map Loading thread
thread1 = MapLoadThread( "my_map.tmx" )
thread1.start()
### Main Loop
clock = pygame.time.Clock()
done = False
while not done:
# Handle user-input
for event in pygame.event.get():
if ( event.type == pygame.QUIT ):
done = True
elif ( event.type == MAP_LOAD_DONE ):
print( "MAP LOADED OK" );
elif ( event.type == MAP_LOAD_ERROR ):
print( "MAP LOAD FAILED" );
print( "EXCEPTION WAS " + str( event.exception ) )
# Update the window, but not more than 60 FPS
window.fill( DARK_BLUE )
pygame.display.flip()
# Clamp FPS
clock.tick_busy_loop(60)
pygame.quit()

Why does graphical window freeze after about 5 seconds?

Code is running correctly and as I expected but after 5 seconds the display for graphics freezes forever. It shows no error, nothing, just stops responding.
This is a program to simulate a movement of a large group of objects. They have to move randomly while aimless like Brownian motion. To make this I used Pygame to draw any object as a rectangle of random location, to move them I remove everything and draw them again with their location randomly changed by 1.
I am using pygame to show graphics.
Could you please also suggest a better solution for this problem?
import pygame, random, sys, time, numpy
from pygame.locals import *
black = (0,0,0)
white = (255,255,255)
clock = pygame.time.Clock()
class people():
def __init__(self):
screen = pygame.display.get_surface()
self.x = random.randint(0, 800)
self.y = random.randint(0, 600)
def move(self):
self.x += random.randint(-1, 1)
self.y += random.randint(-1, 1)
if self.x < 0:
self.x = 0
if self.x > 800:
self.x = 800
if self.y < 0:
self.y = 0
if self.y > 600:
self.y = 600
def place(x, y):
screen = pygame.display.get_surface()
pygame.draw.rect(screen, black, [x, y, 10, 10])
def main():
# Initialise screen
pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption('Test')
peoples = []
chosepopul = 1
while chosepopul == 1:
try:
population = abs(int(input("How many people would you like to have")))
chosepopul = 0
except:
print("Number, please")
for i in range(population):
peoples.append(people())
while True:
screen.fill(white)
for obj in peoples:
people.place(obj.x, obj.y)
people.move(obj)
pygame.display.update()
clock.tick(60)
if __name__ == '__main__':
main()
pygame.quit()
quit()
Everything is working as I expected but freezing inevitably.
UPDATE: If I change input script to constant number, everything is working correctly. So the problem is somehow linked with user interface interactions.
The program stops because the input() blocks the program flow. No further PyGame updates or events are sent & processed. Basically everything comes to a halt, waiting for the user to type.
The best way around this is to write code such that the user does some PyGame on-screen input, rather than in the console. Maybe make a slider or spinner-control to select the number, or plus/minus buttons, whatever.
Alternatively, the program can still use console input in a thread which uses the post() function to send the result to the main PyGame event-loop thread.
I must admit, this answer is of academic interest only, since using the console to input along with a PyGame window is pretty ugly!
Anyway, here is some code. The main python window simply changes colour every 0.5 seconds, while the user can input text in the console with the standard python input() function. The code uses it's own Event enumerated type for posting messages, but these could also just be plain numbers.
This works, as per the OP, because the input() function is called inside a sub-thread of execution. This leaves the main thread free to continually process the PyGame event queue, and paint updates to the window. It's important to only have a single event queue/loop (for reasons beyond the scope of this answer), so the sub-thread "posts" events back to the main thread, rather than acting on the window/events itself.
import threading
import pygame
import enum
# Window size
WINDOW_WIDTH = 200
WINDOW_HEIGHT = 200
DARK = ( 50, 50, 50 )
WHITE = ( 255,255,255 )
RED = ( 255, 55, 55 )
GREEN = ( 5,255, 55 )
BLUE = ( 5, 55,255 )
colour_cycle = [ DARK, WHITE, RED, GREEN, BLUE ]
class UserEvents( enum.IntEnum ):
CLIENT_NUMBER = pygame.USEREVENT + 1
CLIENT_QUIT = pygame.USEREVENT + 2
# ...
class ConsoleInputThread( threading.Thread ):
""" A thread that handles user input on the console.
Waits for user input, then posts messages
to the main PyGame thread for processing """
def __init__( self, prompt ):
threading.Thread.__init__(self)
self.daemon = True # exit with parent
self.done = False
self.prompt = prompt
def stop( self ):
self.done = True
def run( self ):
""" Loops until the user hangs-up """
while ( not self.done ):
# Get some input from the user
user_input = input( self.prompt ).strip()
new_event = None
if ( user_input == 'quit' ):
new_event = pygame.event.Event( UserEvents.CLIENT_QUIT, { } )
else:
try:
user_input = int( user_input )
new_event = pygame.event.Event( UserEvents.CLIENT_NUMBER, { "value":user_input } )
except:
print( "Syntax Error" )
# If we received valid input post it to the main thread
if ( new_event ):
pygame.event.post( new_event )
###
### MAIN
###
# Create the window
pygame.init()
pygame.display.set_caption("Socket Messages")
SURFACE = pygame.HWSURFACE|pygame.DOUBLEBUF|pygame.RESIZABLE
WINDOW = pygame.display.set_mode( ( WINDOW_WIDTH, WINDOW_HEIGHT ), SURFACE )
# Start the console-input thread
input_thread = ConsoleInputThread( "How many people would you like to have: " )
input_thread.start()
# Main paint / update / event loop
done = False
clock = pygame.time.Clock()
colour_index = 0
while ( not done ):
for event in pygame.event.get():
if ( event.type == pygame.QUIT ):
done = True
elif ( event.type == UserEvents.CLIENT_QUIT ):
print("\nCLIENT ASKED TO QUIT " )
done = True
elif ( event.type == UserEvents.CLIENT_NUMBER ):
print( "\nVALUE WAS INPUT: %d " % ( event.value, ) )
WINDOW.fill( colour_cycle[colour_index] )
# rotate the colours, just so the screen changes
colour_index += 1
if ( colour_index >= len( colour_cycle ) ):
colour_index = 0
pygame.display.flip()
clock.tick_busy_loop(2) # NOTE: 2 frames per second, no flashy-flashy
input_thread.stop()
pygame.quit()

when running code to display images on screen my game window freezes temporarily

In a game I am making, the user can take part in arena battles. Upon winning an arena they are meant to be congratulated via text on-screen and, depending on if they've cleared the arena before or not, get given a trophy or get told how many times they've won.
The code outlining what happens if the players' won before and if the player has only won for the first time is virtually similar, but the game freezes when it reaches the part of my if/elif statement outlining the conditions for if the player's already beaten the arena once.
Code for context [text is a separate file used for writing text to my screen, using booleans to determine if it's bold, it's position, etc]:
import pygame, time, text
if battleWon == True and x == len(enemyList)-1: #if the user won the fight and they were on the last enemy
screen.fill(bg_colour) #code for clearing screen
screen.blit(prevwindow,(150,100)) #drawing menus
text.textDisplay("arena champion!",300,140,True,14,"center",screen) #informing they've won
timesCompleted += 1 #incrementing their win count
pygame.display.update() #updating diaplay
time.sleep(2) #keeping message on screen
if arenaComplete == False: #if they havent beaten the arena before
arenaComplete = True #set to true
screen.fill(bg_colour)
screen.blit(prevwindow,(150,100))
text.textDisplay("you got a trophy!",300,140,False,14,"center",screen)# tell them they got a trophy
screen.blit(trophy, (250,160)) #show image of trophy
pygame.display.update()
time.sleep(2)
elif arenaComplete == True: #if already beaten
screen.fill(bg_colour)
screen.blit(prevwindow,(150,100))
text.textDisplay("you've now won "+str(timesCompleted)+" times!",300,140,False,14,"center",screen) #show how many times they've beaten arena
screen.blit(trophy, (250,160)) #trophy...
text.textDisplay("x" + str(timesCompleted),375,160,False,14,"topleft",screen) # ... x(no. of times won)
pygame.display.update()
time.sleep(2)
elif battleWon == False: #if player loses
print(":(") #placeholder
I've read over the code many times and I can't see why this is happening. There doesn't seem to be any errors in the syntax as it would've shown up in the shell. If the code was for some reason skipping out the elif arenaComplete == True part it would simply jump back to my game's main loop without freezing.
EDIT: due to some confusion in the comments I'll clarify that the time.sleep code, as far as I'm aware, does not cause the problem I'm describing. I want the message to be displayed on the screen for the given amount of time, the problem being that the code under the "elif arenaComplete == True" statement isn't making anything display at all. Here's what I said in a comment if it explains better:
What I mean is that the code under the "elif arenaComplete == True"
screen doesn't actually blit anything to the screen, despite the
"pygame.display.update" which comes BEFORE the time.sleep code,
meaning something must be happening beforehand but I am not sure what.
The code in the previous "if arenaComplete == False" section is almost
identical and for this reason I cannot think of any reason as to why
this is happening.
As commentators have pointed out, the game is locking up during the time.sleep(2) calls.
Really in any sort of event-driven program, it's not ideal to lock the whole process with a sleep-cycle. Especially in the loop that handles the events, as this can have other screen-paint consequences.
So how do you code around this? With a State Machine.
The game outlined above seems to have three states: "battle", "results" and "gameover" (presumably). Depending on what state the game is in, the display is different, and the user-input is handled differently.
First, lets define some states for the game, using a python Enumerated Type.
import enum
class GameState( enum.Enum ):
BATTLE = 1
WONGAME = 2
GAMEOVER = 3
STARTMENU = 4
This allows us to reference the current state with a human-readable name:
game_state = GameState.BATTLE
...
if ( game_state == GameState.GAMEOVER ):
...
It's not strictly necessary to use an enumerated type for this, just a string-description would suffice. But using an enumerated type is generally how this sort of thing is handled in a state machine (and is normal for software generally). As a bonus it catches typos with compile errors.
Anyway, so during the window drawing and event processing, the code checks the current game state to see how to paint the screen:
### Main Loop
# Re-draw the screen
if ( game_state == GameState.STARTMENU ):
WINDOW.fill( NAVY_BLUE )
drawMenu( start_menu )
elif ( game_state == GameState.BATTLE ):
SPRITES.update()
WINDOW.fill( INKY_BLACK )
SPRITES.draw( WINDOW )
...
elif ( game_state == GameState.GAMEOVER ):
WINDOW.fill( INKY_BLACK )
# tell user they failed
...
elif ( game_state == GameState.WONGAME ):
WINDOW.fill( bg_colour ) #code for clearing screen
WINDOW.blit( prevwindow, ( 150,100 ) ) #drawing menus
#informing they've won
text.textDisplay( "arena champion!", 300, 140, True, 14, "center", WINDOW )
if ( arenaComplete == False ):
...
# Update the window, but not more than 60fps
pygame.display.flip()
clock.tick_busy_loop( 60 )
Similarly in the event handling, for say a keypress, sometimes a different thing needs to happen depending on the state. For example if we're in GameState.STARTMENU pressing arrow keys might change the menu highlight-selection, but in GateState.BATTLE they move the player. It gets a bit involved, but it's not difficult. It also has the side-effect of enforcing a rigid simplicity on the handling, making for tidier source-code.
# Handle user input
for event in pygame.event.get():
if ( event.type == pygame.QUIT ):
done = True
elif (event.type == pygame.KEYDOWN):
keys = pygame.key.get_pressed()
if ( game_state == GameState.SOMEMENU ):
# Up/Down changes selecton
if ( keys[pygame.K_UP] ):
my_menu.selectPrevious()
elif ( keys[pygame.K_DOWN] ):
my_menu.selectNext()
elif ( keys[pygame.K_ENTER] ):
# Do item from menu
...
elif ( game_state == GameState.GAMEOVER ):
# any key-press go back to main menu
game_state = GameState.STARTMENU
elif ( game_state == GameState.BATTLE ):
if ( keys[pygame.K_UP] ):
player_sprite.moveUp()
elif ( keys[pygame.K_DOWN] ):
player_sprite.moveDown()
...
This state machine method allows for the program screen-context to switch easily. It means there's no need for time.sleep() delays to show something to the user. It can stay at the "won" screen until a key is pressed.
Here I present a simple example, where pressing any key cycles through the three states. Each state shows a different screen. Alas, I do not currently have time for implementing a state-driven key-press handler too.
import pygame
import random
import time
import enum
# Window size
WINDOW_WIDTH = 400
WINDOW_HEIGHT = 400
# background colours
NAVY_BLUE = ( 28, 20, 186)
INKY_BLACK = ( 0, 0, 0)
CRIMSON = (195, 11, 41)
BAD_SNOW = (255, 252, 216)
class GameState( enum.Enum ):
MOVING = 1
MENU = 2
GAMEOVER = 3
class MovingSprite( pygame.sprite.Sprite ):
""" A bouncing Eyeball, just because """
def __init__( self ):
pygame.sprite.Sprite.__init__( self )
self.image = pygame.image.load("eyeball_32.png").convert_alpha()
self.rect = self.image.get_rect()
self.newPosition()
def newPosition( self ):
# Position to somewhere random
self.rect.center = ( random.randrange( 0, WINDOW_WIDTH ), random.randrange( 0, WINDOW_HEIGHT ) )
def update( self ):
self.newPosition()
### MAIN
pygame.init()
pygame.font.init()
WINDOW = pygame.display.set_mode( ( WINDOW_WIDTH, WINDOW_HEIGHT ), pygame.HWSURFACE|pygame.DOUBLEBUF|pygame.RESIZABLE )
pygame.display.set_caption("State Machine Example")
# Add some sprites
MOVERS = pygame.sprite.Group() # a group, for a single sprite
for i in range(3):
MOVERS.add( MovingSprite() )
# Font for menus (and whatever)
text_font = pygame.font.Font( None, 60 ) # just a default font
clock = pygame.time.Clock()
done = False
game_state = GameState.MOVING
while not done:
# Handle user-input
for event in pygame.event.get():
if ( event.type == pygame.QUIT ):
done = True
elif ( event.type == pygame.VIDEORESIZE ):
WINDOW_WIDTH = event.w
WINDOW_HEIGHT = event.h
WINDOW = pygame.display.set_mode( ( WINDOW_WIDTH, WINDOW_HEIGHT ), pygame.HWSURFACE|pygame.DOUBLEBUF|pygame.RESIZABLE )
elif ( event.type == pygame.KEYDOWN ):
# Move to next state on any keypress, with loop
if ( game_state == GameState.MOVING ):
game_state = GameState.MENU
elif ( game_state == GameState.MENU ):
game_state = GameState.GAMEOVER
elif ( game_state == GameState.GAMEOVER ):
game_state = GameState.MOVING
# Repaint the screen, depending on the state
if ( game_state == GameState.MOVING ):
MOVERS.update() # re-position the flower-pot
WINDOW.fill( INKY_BLACK )
MOVERS.draw( WINDOW ) # draw the flower-pot
elif ( game_state == GameState.MENU ):
WINDOW.fill( NAVY_BLUE )
menu_items = [ "1. Something", "2. Something Else", "3. Third Choice" ]
total_height = 0 # use to tally menu height (for centering)
max_width = 0
# Make the menu images, work out the size it needs
for i in range( len( menu_items ) ):
# convert text into image, in-place
menu_items[i] = text_font.render( menu_items[i], True, BAD_SNOW )
max_width = max( max_width, menu_items[i].get_width() )
total_height = total_height + menu_items[i].get_height()
# Now draw to screen
cursor_x = ( WINDOW_WIDTH - max_width ) // 2 # centred on largest menu item
cursor_y = ( WINDOW_HEIGHT - total_height ) // 2
for i in range( len( menu_items ) ):
WINDOW.blit( menu_items[i], ( cursor_x, cursor_y ) )
cursor_y += 5 + menu_items[i].get_height() # move down height, plus a bit
elif ( game_state == GameState.GAMEOVER ):
WINDOW.fill( CRIMSON )
# Write "Game Over" text to middle of screen (image could be pre-generated)
game_over_text = text_font.render( "* Game Over *", True, BAD_SNOW )
centred_x = ( WINDOW_WIDTH - game_over_text.get_width() ) // 2
centred_y = ( WINDOW_HEIGHT - game_over_text.get_height() ) // 2
WINDOW.blit( game_over_text, ( centred_x, centred_y ) )
pygame.display.flip()
# Update the window, but not more than 60fps
clock.tick_busy_loop( 60 )
pygame.quit()
eyeball_32.png

How to cycle image sprite in pygame

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( )

Categories

Resources