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

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

Related

Creating two pygame screens using multiprocessing

I am trying to develop an app using pygame and I want to create a popup window.
I am trying to keep it all in one file, so I am using multiprocessing for this task.
I have two classes, the app and the popup window. Each one is inside a function that creates the windows and start both of their loops.
This is some simplified code:
main.py
from multiprocessing import Process
def popup():
import pygame as pg
pg.init()
class PopUp:
def __init__(self):
self.screen = pg.display.set_mode((300,300))
def update(self):
pg.display.flip()
p = PopUp()
while True:
p.update()
def app():
import pygame as pg
pg.init()
class App:
def __init__(self):
self.screen = pg.display.set_mode((800,600))
self.create_popup()
def create_popup(self):
p = Process(target=popup)
p.start()
p.join()
def update(self):
pg.display.flip()
a = App()
while True:
a.update()
if __name__ == '__main__':
a = Process(target=app)
a.start()
a.join()
However, when I execute this code, only one window appears, with the size of the App, and then is resized to the size of the PopUp, even though it is a different process.
If I do it this other way, then two separate windows will appear with no problem at all.
Why is this happening and how can I get to create the popup window from the app class?
You're duplicating the process after pygame.init() is called. So probably they're sharing window handles, etc.
If the process is copied before the init(), it works OK.
import multiprocessing
import pygame
import os
def handleUpdates( window, colour ):
""" Simple pygame message loop.
Paints the window in a single colour,
handles quit event """
clock = pygame.time.Clock()
exiting = False
while not exiting:
# Handle user-input
for event in pygame.event.get():
if ( event.type == pygame.QUIT ):
exiting = True
# draw the window
window.fill( colour )
pygame.display.flip()
# save CPU
clock.tick( 30 )
pygame.quit()
def pyGameWindow( name, window_pos, colour ):
""" Initialise PyGame for a new window """
os.environ['SDL_VIDEO_WINDOW_POS'] = "%d,%d" % window_pos
pygame.init()
window = pygame.display.set_mode( ( 300, 300 ) )
pygame.display.set_caption( name )
handleUpdates( window, colour )
if __name__ == '__main__':
p1 = multiprocessing.Process(target=pyGameWindow, args=('Window One', ( 100, 100 ), (255, 0, 0 )))
p1.start()
p2 = multiprocessing.Process(target=pyGameWindow, args=('Window Two', ( 500, 100 ), (255, 255, 0 )))
p2.start()
p1.join()
p2.join()

How to play video in Pygame currently? [duplicate]

This question already has answers here:
How to load and play a video in pygame
(3 answers)
Closed 1 year ago.
I am looking to add videos in my game (such as cut scenes or an animated menu screen).
I looked around for a bit and it seems pygame doesn't support video playing anymore, so I was wondering if there was another way to get videos playing while being well integrated in my game, such as having the video playing in the background and having pygame elements (start button etc.) in the foreground.
There's an example somewhere (I was unable to find an original link) of using FFMPEG and another python module to decode the frames via a pipe and read these through into PyGame for displaying. I copied a code snippet (I thought from SO), and forgot about it.
I have now adapted that technique to make a VideoSprite. It uses FFMPEG to decode (and rescale) the video stream, where it's read during the sprites update() to get the next frame.
This is a very rough implementation, but I hope it gives you an idea of what's possible. While it would be nice if PyGame would just play the videos on its own, at least this method hands the video decoding and rescaling off to a subprocess where it hopefully runs on another CPU.
(EDIT: added handler for video ending, and proper FPS control)
import pygame
import subprocess
# Window size
WINDOW_WIDTH = 600
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("Video Sprite")
class VideoSprite( pygame.sprite.Sprite ):
FFMPEG_BIN = "/usr/bin/ffmpeg" # Full path to ffmpeg executable
def __init__(self, rect, filename, FPS=25 ):
pygame.sprite.Sprite.__init__(self)
command = [ self.FFMPEG_BIN,
'-loglevel', 'quiet',
'-i', filename,
'-f', 'image2pipe',
'-s', '%dx%d' % (rect.width, rect.height),
'-pix_fmt', 'rgb24',
'-vcodec', 'rawvideo', '-' ]
self.bytes_per_frame = rect.width * rect.height * 3
self.proc = subprocess.Popen( command, stdout=subprocess.PIPE, bufsize=self.bytes_per_frame*3 )
self.image = pygame.Surface( ( rect.width, rect.height ), pygame.HWSURFACE )
self.rect = self.image.get_rect()
self.rect.x = rect.x
self.rect.y = rect.y
# Used to maintain frame-rate
self.last_at = 0 # time frame starts to show
self.frame_delay = 1000 / FPS # milliseconds duration to show frame
self.video_stop = False
def update( self ):
if ( not self.video_stop ):
time_now = pygame.time.get_ticks()
if ( time_now > self.last_at + self.frame_delay ): # has the frame shown for long enough
self.last_at = time_now
try:
raw_image = self.proc.stdout.read( self.bytes_per_frame )
self.image = pygame.image.frombuffer(raw_image, (self.rect.width, self.rect.height), 'RGB')
#self.proc.stdout.flush() - doesn't seem to be necessary
except:
# error getting data, end of file? Black Screen it
self.image = pygame.Surface( ( self.rect.width, self.rect.height ), pygame.HWSURFACE )
self.image.fill( ( 0,0,0 ) )
self.video_stop = True
### Create Video Area
video_sprite1 = VideoSprite( pygame.Rect( 100, 100, 320, 240 ), '1975_test_pattern.mp4' )
video_sprite2 = VideoSprite( pygame.Rect( 100, 100, 160, 90 ), '/home/kingsley/Videos/rocket.avi' ) # 640x360
#sprite_group = pygame.sprite.GroupSingle()
sprite_group = pygame.sprite.Group()
sprite_group.add( video_sprite1 )
sprite_group.add( video_sprite2 )
### 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.MOUSEBUTTONUP ):
# On mouse-click
pass
# Movement keys
keys = pygame.key.get_pressed()
if ( keys[pygame.K_UP] ):
video_sprite2.rect.y -= 10
if ( keys[pygame.K_DOWN] ):
video_sprite2.rect.y += 10
if ( keys[pygame.K_LEFT] ):
video_sprite2.rect.x -= 10
if ( keys[pygame.K_RIGHT] ):
video_sprite2.rect.x += 10
# Update the window, but not more than 60fps
sprite_group.update()
window.fill( DARK_BLUE )
sprite_group.draw( window )
pygame.display.flip()
# Clamp FPS
clock.tick_busy_loop(25) # matching my video file
pygame.quit()
Obviously, since it's just the video stream, there's no sound. But for all intents and purposes it's just another sprite. When the video runs out we catch the error and go black.
NOTE: Sometimes when I run this, it breaks the terminal echoing in Linux. I suspect it's something to do with the subprocess and/or pipe. Running reset fixes this. It seems a common problem with subprocesses.
I wrote a code which seems to work very well with moviepy, I was inspired by that of Kingsley.
import moviepy.editor
import moviepy.video.fx.all
import pygame
class VideoSprite(pygame.sprite.Sprite):
def __init__(self, rect, filename):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface((rect.width, rect.height), pygame.HWSURFACE)
self.rect = self.image.get_rect()
self.rect.x = rect.x
self.rect.y = rect.y
self.video = moviepy.editor.VideoFileClip(filename).resize((self.rect.width, self.rect.height))
self.video_stop = False
def update(self, time=pygame.time.get_ticks()):
if not self.video_stop:
try:
raw_image = self.video.get_frame(time / 1000) # /1000 for time in s
self.image = pygame.image.frombuffer(raw_image, (self.rect.width, self.rect.height), 'RGB')
except:
self.video_stop = True
The advantage here is that the video is at the right speed

How to run sound loops in sequence with pygame?

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

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

Categories

Resources