I'm making a game launcher in Python 3.7, and I'm using the Pygame ("pg") library. I made a class for clickable text ("links"), and I have an event handler which checks for the pg.QUIT function.
I have a while loop set up that begins with the event handler function that checks for pg.QUIT, and later on in the loop, I have a class function that listens to see if the link has been clicked.
Unfortunately, when they are both able to run within the loop, they interfere with each other and some input gets ignored, such as pg.MOUSEBUTTONDOWN (which is crucial here).
I've tried disabling the event handler (I just commented the line that calls it), and that made it work; every input was registered. However, I don't want to check for pg.QUIT inside of an object class, and I don't want to look for specific objects in my event handler: I want to keep these things separate.
The code in my class is as follows:
(Inside TextObject class)
def link():
for e in pg.event.get():
if e.type == pg.MOUSEBUTTONDOWN and {mouse is over link}:
print('click!')
The code in my event handler is as follows:
def handle():
for e in pg.event.get():
if e.type == pg.QUIT:
running = False
The code in my while loop is as follows:
while running:
handle()
{update screen and draw text}
textObject.link()
clock.tick(fps)
I want the program to listen for pg.QUIT, and then if that hasn't happened, move on and listen to see if the link has been clicked.
When I run it, the program only prints 'click!' after I've clicked the link about twenty times. It seems almost random.
I'm predicting that the two functions are interfering with each other, and I could fix it with some kind of joint function, but I'd really prefer to keep these functions vague without hard-coding in some text coordinates, y'know?
Thanks.
P.S. I used pseudo-code to give some context, but I've already confirmed that the issue lies in the code I've written explicitly.
Move all the event handling into your main loop. You're losing events because some are being handled in one loop (and discarding the un-handled events), and similarly in the other event loop, but for other event types.
Spreading the event processing throughout various parts of the code makes the logic and debugging harder. Ideally it works well to have a single logical item handled in a single place. So when you have an issue with user-input, you only need to debug the user-input section - rather than this-function, that-function, and this-thing-in-a-file-over-there.
Sure, if you need to see which button was pressed or whatever, write a function to do that, then call the function (passing it the mouse position) during the click event handling.
while running:
# Handle user input
for e in pg.event.get():
if e.type == pg.QUIT:
running = False
elif e.type == pg.MOUSEBUTTONDOWN:
if {mouse is over link}:
print('click!')
# Handle screen painting
...
Although I consider Kingsley answer as really good and valuable, I find splitting event execution onto separate files/functions useful in some cases (as sort of categorisation making code cleaner - especially if this goes with clean visualisation, like in this workspace of mine where use of events is seen easily by additional variable and restricted to few functions).
My way of doing that is by putting events into variable, so those are called once, and referring them further:
while running:
events = pg.event.get() # "variablised" event, so we call it once per frame
for e in events: # stuff can be used explicitly
if e.type == pg.QUIT:
running = False
textObject.link(events) #or passed to another function
This is how another file/function will look then:
(Inside TextObject class)
def link(events):
for e in events:
if e.type == pg.MOUSEBUTTONDOWN and {mouse is over link}:
print('click!')
I wanted to revive this topic because it gave me idea how to solve the problem the way I portrayed, yet I disagreed (only to some extent) with Kingsley's answer which can be really messy in some cases.
Related
I'm only starting to grasp this idea of abstracting complex stuff via functions, so I decided to practice it a bit in Pygame.
So, this code right here works just fine, the pygame window is present, and you can close it by pressing X button:
#pygame initialization code and etc.
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
#code for drawing stuff out
But let's say I want to make a function to just handle closing the window just for the sake of it:
#pygame initialization code and etc.
running = True
def handle_quit_event():
for event in pygame.event.get():
if event.type == pygame.QUIT:
return False
else:
return True
while running:
running = handle_quit_event()
#code for drawing stuff out
Now it doesn't work the same way anymore, the pygame window appears for a blink moment, and then program finishes, leaving me with this unease feeling that my idea of abstracting stuff using functions is all wrong.
[EDIT]: So in my code the function checks for only the first event in the list, but it still doesn't explain why the program finishes right after running it, because even if it checks for the first event, it still should return true by running else clause. So what am I missing?
Abstracting functionality into functions is a great idea! Unfortunately your re-write has introduced a bug that might be causing your broken game. Without a stacktrace or error messages, it's hard to say exactly why the game is broken.
Onto the bug:
The function handle_quit_event doesn't iterate through all the events in pygame.event.get(). It returns True or False after checking the first event.
You probably wanted to write it more like:
def handle_quit_event():
for event in pygame.event.get():
if event.type == pygame.QUIT:
return False
return True
A more pythonic approach that uses list-comprehension and any():
def handle_quit_event():
return not any([event.type == pygame.QUIT for event in pygame.event.get()])
Note the flipped logic with not is required because your function is currently returning True if there is not a QUIT event.
I was writing a function to make it easy to detect a key press without having to use the for loop that is normally used:
for event in pygame.event.get():
if event.type == KEYDOWN:
if event.key == key:
And I found that the function that I had written didn't work, and this is because I have this test_for_quit function in all my projects that runs every frame. I've found this to be useful as I can just copy this into any program that I'm writing:
def test_for_quit():
'''Shuts down the game if it's closed'''
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
if event.type == KEYDOWN:
if event.key == K_ESCAPE:
pygame.quit()
sys.exit()
Surprisingly this is the first game I've made since implementing that function to all my programs, that doesn't use key presses. But I found that the reason that the new function that I've written didn't work because pygame.event.get() had already been called in the test_for_quit function.
After some more testing I found that this method cannot be called twice, for instance:
while True:
print(pygame.event.get())
print() #Leaves an empty line
print(pygame.event.get())
returns:
[<Event(17-VideoExpose {})>, <Event(16-VideoResize {'h': 600, 'size': (800, 600), 'w': 800})>, <Event(1-ActiveEvent {'gain': 0, 'state': 1})>, <Event(4-MouseMotion {'rel': (538, 315), 'pos': (537, 314), 'buttons': (0, 0, 0)})>]
[]
So why is it that pygame.event.get() can only be called once, this really intrigues me and I couldn't find anything about it on the internet?
Also I've decided to not continue using the test_for_quit function and the key_down function in favour of just using the traditional for loop.
event.get can be called as many times as you want. The matter is it does take the events out of the event queue - and is up to your program to consume them. When it is called a second time with no interval (pygame itself knows nothing about "frames" - you give the delay between frames) - all events are gone, and no others have been generated.
The documentation for event.get reads:
This will get all the messages and remove them from the queue. If a type or sequence of types is given only those messages will be removed from the queue.
If you are only taking specific events from the queue, be aware that the queue could eventually fill up with the events you are not interested.
You have two choices: keep your "copy and pasted" call to event.get and read the keyboard state by using other calls - pygame.key.get_pressed for one - or to use a smarter way to consume the events in the queue.
There are even ways to check for evetns on the event queue without consuming them - so you could place those calls before a call to events.get.
One is that the function itself allows you to specify event types you are interested in. So let's suppose you have a function to deal with mouse events and a function to deal with keybard events:
def do_keys():
for event in pygame.event.get(KEYDOWN):
...
def do_mouse():
for event in pygame.event.get((MOUSEMOTION, MOUSEBUTTONDOWN)):
..
def main():
while True:
do_keys()
do_mouse()
# discard other events:
pygame.event.get()
# draw secreen
...
pygame.time.delay(...)
Or, which is even easier, you can simply assign the return value of the call to pygame.event.get to a variable - that will be a single list over which you can iterate as many times as you wish.
events = pygame.event.get()
for event in events():
# treat some evetns here
# some other logic here
...
do_keyboard_things(events)
def do_keyboard_things(events):
for event in events:
...
I'm creating a game in pygame that requires waves of enemies being sent out consistently. My wave-sending code works perfectly when used just once by itself, but when I try to repeat it with set_timer(), nothing happens.
Necessary code:
def game():
WAVE_EVENT = USEREVENT + 1
pygame.time.set_timer(WAVE_EVENT, 1000)
and
for event in pygame.event.get():
if pygame.event.get(WAVE_EVENT):
wave1()
print 'Wave1 sent'
Result? Nothing at all. My player just sits there in the middle of the screen looking bored.
How would I make the set_timer event actuall work?
Your event loop code is incorrect. When you call pygame.event.get() (with no argument), you'll get all events currently in the event queue, including the timer driven WAVE_EVENTs. Your second call tries to get only the WAVE_EVENTs, but since they've already been collected by the first call, there won't be any left in the queue.
Generally the way to deal with this is to check the type of each event as you iterate over them:
for event in pygame.event.get(): # gets all events, loops over them
if event.type == WAVE_EVENT:
wave1()
print 'Wave1 sent'
Often you'll also want to check for other event types (such as QUIT events that are generated when the user tries to close your window). For simple code with a few different event types, you'll probably just use elif statements, but for complicated stuff you might want to use a dictionary to map event types to handler functions.
An alternative solution would be to separately request each type of event you need to handle, and never request all event types:
if pygame.event.get(WAVE_EVENT):
wave1()
print 'Wave1 sent'
if pygame.event.get(pygame.event.QUIT):
# ...
for key_event in pygame.event.get(pygame.event.KEYDOWN):
# ...
This later form is good for separating out different parts of your code that consume different events, but it may cause problems if you're not requesting all of the event types that are being queued. You'll probably need to be using some of the filtering functions (e.g. pygame.event.set_allowed) to make sure your event queue doesn't fill up with events you're not checking for.
Consider these lines in the pygame loop:
ev = pygame.event.poll()
ev.type == pygame.QUIT
From: http://openbookproject.net/thinkcs/python/english3e/pygame.html
From what I understand the function pygame.event.poll() creates an instance of the Event class in the event module of the pygame package.
I. Now ev.type is an attribute call(?) but how do I know which values it can have? How can you even tell from the pygame documentation that it has the possibility to equal pygame.QUIT?
II. What exactly is pygame.QUIT? How does it get a value?
III. help('pygame.QUIT') says pygame.QUIT = class int(object). How do you call this construction?
ev = pygame.event.poll()
is a call to a function that returns a single event from the event queue (basically, a list of things that have happened that your application might want to know about). It assigns that event (which is an Event object) to the variable ev.
ev.type
gets the value of the type attribute of that Event object, which is a numerical constant.
== pygame.QUIT
checks to see if it's equal the numerical constant defined as pygame.QUIT.
The possible event types are listed at http://www.pygame.org/docs/ref/event.html - I've copy-pasted the list here as well (which also lists the associated attributes passed with each event):
QUIT none
ACTIVEEVENT gain, state
KEYDOWN unicode, key, mod
KEYUP key, mod
MOUSEMOTION pos, rel, buttons
MOUSEBUTTONUP pos, button
MOUSEBUTTONDOWN pos, button
JOYAXISMOTION joy, axis, value
JOYBALLMOTION joy, ball, rel
JOYHATMOTION joy, hat, value
JOYBUTTONUP joy, button
JOYBUTTONDOWN joy, button
VIDEORESIZE size, w, h
VIDEOEXPOSE none
USEREVENT code
pygame.QUIT is sent when the user clicks the window's "X" button, or when the system 'asks' for the process to quit. If ignored, it can still be killed by the system. It lets you save, before quitting.
pygame.QUIT is just a constant int that happens to be defined inside the pygame module.
>>> import pygame
>>> pygame.QUIT
12
This is the relevant page in the documentation: http://www.pygame.org/docs/ref/event.html. You can see all the possible event types (just above the comments).
Im not entirely sure what pygame.event.poll() actually does, but you can detect when you window gets closed with for e in pygame.event.get(): if e.type == pygame.QUIT: print('the cross has been clicked', don't know if this is helpfull but it could be a workaround your problem
I would like to know if there is a way of using poll() or get() without removing the events from the queue.
In my game, I check input at different places (not only in the main loop) and sometimes I need to check the same event at different places but when i check it once it removes it from the queue. I tried using peek() but the problem is that I can't get the key corresponding to the event done.
while 1:
event = pygame.event.poll()
if event.type == KEYDOWN:
return event.key
else:
pass
#works but removes event from the queue
This can get the key corresponding to the event but with peek() it can't:
pygame.event.peek(pygame.KEYDOWN).key
#dosent work
However I can't use the first method because removes the event from the queue so I can't check key events elsewhere in the program.
I don't understand well how the queue works so maybe I'm just mistaking but I tried the first one at different location and only the first time i checked the event it worked.
My goal is to check events in different classes in my game.
Thanks for your help
I think a better design would be to check events in a single place - even if in a factored out function or method outside the mainloop code, and keep all the relevnt event data in other objetcts (as attributes) or variables.
For example, you can keep a reference to a Python set with all current pressed keys, current mouse position and button state, and pass these variables around to functions and methods.
Otherwise, if your need is to check only for keys pressed and mouse state (and pointer posistion) you may bypass events entirely (only keeping the calls to pygame.event.pump() on the mainloop). The pygame.key.get_pressed function is my favorite way of reading the keyboard - it returns a sequence with as many positions as there are key codes, and each pressed key has its correspondent position set to True in this vector. (The keycodes are available as constants in pygame.locals, like K_ESC, K_a, K_LEFT, and so on).
Ex:
if pygame.key.get_pressed()[pygame.K_ESCAPE]:
pygame.quit()
The mouse module (documented in http://www.pygame.org/docs/ref/mouse.html) allows you to get the mouse state without consuming events as well.
And finally, if you really want to get events, the possibility I see is to repost events to the Queue if they are not consumed, with a call to pygame.event.post - this call could be placed, for example at the else clause in an if/elif sequence where you check for some state in the event queue.
I don't know if it is good style, but what I did was just saving all the events in a variable and passing it to the objects that used their own event queues to detect "their" events.
while running:
events = pygame.event.get()
for event in events:
if event.type == pygame.QUIT:
running = False
self.allS.update(events)
and in the update method:
for event in events:
print("Player ", event)
As far as I can tell there is no one 'right' way to do this, but one option is to save all the events into a variable. Then you can access them as many times as you want.