How can I fix the slowness of my pygame event loop? - python-3.x

I'm creating a button class for a game, and I'm using the pygame event loop to detect mouse clicks (specifically when the mouse is released) (I hear it is better the method of using pygame.mousemget_pressed()[0]). However, the event loop seems to be slow, not responding and performing the buttons function when it is clicked. I think it may be because relate to how I created the event loop in a class, but I'm not sure. Here's a sample of my code:
class Button:
"""A Button class, built for all kinds of purposes"""
def __init__(self, window, rect, message, off_color, on_color, message_color, message_font_size):
pass # just a bunch of variables that use the parameters given
def in_button(self):
mouse_pos = pygame.mouse.get_pos()
if pygame.Rect(self.rect).collidepoint(mouse_pos):
return True
def clicked(self):
if self.in_button():
pygame.event.pump()
for e in pygame.event.get():
if e.type == pygame.MOUSEBUTTONUP:
return True
# I proceed to create 5 instances using this class.
I removed some unnecessary method information in my code. If you need anything more, please help me.

You have to implement the clicked method in a different way, because there should be only one event loop in your application, not one in every button instance. pygame.event.get() empties the event queue, so calling it multiple times per frame will cause problems.
I suggest to pass the events to the buttons. In this (very simple) example I pass the pygame.MOUSEBUTTONDOWN events to the clicked method and then check if the event.pos (mouse position) of the event collides with the rect. If it returns True, I do something in the event loop in the main function.
import pygame as pg
class Button:
def __init__(self, pos):
self.image = pg.Surface((100, 40))
self.image.fill(pg.Color('dodgerblue1'))
self.rect = self.image.get_rect(center=pos)
def clicked(self, event):
"""Check if the user clicked the button."""
# pygame.MOUSE* events have an `event.pos` attribute, the mouse
# position. You can use `pygame.mouse.get_pos()` as well.
return self.rect.collidepoint(event.pos)
def main():
screen = pg.display.set_mode((640, 480))
clock = pg.time.Clock()
button = Button((100, 60))
number = 0
done = False
while not done:
for event in pg.event.get():
if event.type == pg.QUIT:
done = True
elif event.type == pg.MOUSEBUTTONDOWN:
# Pass the MOUSEBUTTONDOWN event to the buttons.
if button.clicked(event):
number += 1 # Do something.
print('clicked', number)
screen.fill((30, 30, 30))
screen.blit(button.image, button.rect)
pg.display.flip()
clock.tick(60)
if __name__ == '__main__':
pg.init()
main()
pg.quit()
If you want a button with several images, you can use something similar to the button class here (the first addendum) or search for more sophisticated Button classes for pygame.

Related

Getting tkinter widget values outside main application class?

I have a pygame project which has a main menu - in said menu I have it so that upon clicking the Options button it triggers a Tkinter UI to open. The UI has a OptionMenu widget affecting game speed. How do I retrieve the value of the OptionMenu outside the class I used to create the Tkinter window
Here is a simplified issue:
class GUI(object):
def __init__(self):
import Tk # etc...
# CODE FOR THE PROGRAM WINDOW
defaultSpeed = StringVar(root)
speedMenu = OptionMenu(root, defaultSpeed, 'Slow', 'Normal, 'Fast')
speedMenu.pack()
I know I need defaultSpeed.get() to get the value.
The Pygame menu has this:
click = pygame.mouse.get_pressed()
clicked = click[0] == 1
if "Play" clicked:
startGame(ticks)
if "Options" clicked:
options = GUI()
At this point ^^^ How do I somehow obtain the speed (defaultSpeed.get()) and have it as global variable so that I can use it in the startGame function to affect the number of ticks on clock?
Thinking about it, I'm essentially asking for how to have a global variable defined when initialising a Tkinter class?
I've tried creating a function in GUI() called getSpeed and calling options.getSpeed() but for some reason I still have a logic error that means it's never actually printed/displayed/returned.
I am aware you shouldn't return values when in __init__ - should I just move all my Tkinter app. to a function inside GUI() and leave the __init_ out? surely I could just refrain from initalising it? Would this be better off programmed procedurally over OOP (allowing me to simply call GUI() in the main menu)?
Any help solving is appreciated.
RECAP OF PROBLEM:
Main Menu has 2 options (1 runs game in Pygame, 1 runs Options window in Tkinter)
I need to get the option from the options menu and have it apply to a variable in the game.
EDIT: I was asked to provide a minimal code
I'm currently really busy but here is minimal pseudocode - all you need to do is create the dimensions for the game window and for the menu (they are both 940,500)
import pygame
import time
import math
import tkinter
from tkinter import *
pygame.init()
clock = pygame.tick.Clock()
ticks = 0
#Here is all the game code
#This function is the main game and includes the game loop
def StartGame(ticks):
running = True
myGame1 = teams()
myGame1.startTeam()
#Here is some functions providing the rules of the sport
def updateBall(ticks):
theBall.x += float(theBall.speed) * ticks / 1000
while running:
myGame1.displayPlayers()
updateBall(ticks)
#Calling alot of functions and the game running is here
ticks = clock.tick(30)
pygame.display.flip
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
pygame.quit()
quit
def game_intro(menu_image):
intro = True
while intro:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
pygame.quit()
quit
click = pygame.mouse.get_pressed()
clicked = click[0] == 1
if 62+288 > mouse[0] > 62 and 150+28 > mouse[1] > 150:
if clicked:
startGame(ticks)
pass
elif 62+288 > mouse[0] > 62 and 230+30 > mouse[1] > 230:
if clicked:
options = GUI()
class GUI(object):
def __init__(self):
root = self.root = tkinter.Tk
root.geometry('500x400')
root.configure(background = '#ffffff')
speedLabel = tkinter.Message(root, text = 'Game speed: ')
defaultSpeed = StringVar(root)
speedMenu = OptionMenu(root,defaultSpeed, 'Slow', 'Normal', 'Fast')
speedLabel.grid(row = 0, column = 0)
speedMenu.grid(row = 0, column = 1)
game_intro(menu_image)
Before you create defaultSpeed:
global defaultSpeed
Then before you access it:
global defaultSpeed
It basically means that you can access the variable from anywhere after using global.

Drag only one sprite in a group with the mouse

The problem that I cannot figure how to sort is that when I drag a piece over another the second piece also is dragged along with the first. I have tried several ways to limit the mouse selection to one piece at at time, but all have failed. Can anyone help - no doubt there is a simple way! The code shorn of all my failed attempts follows:
# In main loop:
# Watch for keyboard and mouse events
for event in pygame.event.get():
if event.type == pygame.MOUSEBUTTONDOWN:
mouse_held = True
if event.type == pygame.MOUSEBUTTONUP:
mouse_held = False
# Update pieces that are in a sprite.Group()
pieces.update(mouse_held)
# In sprite class:
def update(self, mouse_held):
if mouse_held == True:
self.mouse_coordinates = pygame.mouse.get_pos()
if self.rect.collidepoint(self.mouse_coordinates) == True:
self.rect.centerx = self.mouse_coordinates[0]
self.rect.centery = self.mouse_coordinates[1]
Your question is fairly difficult but by doing all of this you should be able t achieve what you want.
You should change your sprite class to have a new variable of type int named depth (the higher the value the 'deeper' it is).
Considering you have a list of all the sprite objects you want to check for clicks called spriteList you should add this:
from operator import attrgetter
then change these lines:
if event.type == pygame.MOUSEBUTTONDOWN:
mouse_held = True
to:
if event.type == pygame.MOUSEBUTTONDOWN:
sprites = []
for sprite in spriteList:
if sprite.rect.collidepoint(event.pos) == True:
sprites.append(sprite)
active = min(lists, key=attrgetter('depth'))
mouse_held = True
You should replace the sprite's update function to:
def update(self):
self.mouse_coordinates = pygame.mouse.get_pos()
if self.rect.collidepoint(self.mouse_coordinates) == True:
self.rect.centerx = self.mouse_coordinates[0]
self.rect.centery = self.mouse_coordinates[1]
And finally when you want to update the sprite's position simply type:
if mouse_held:
active.update()

Issues with surface.fill () in pygame

My program allows the an image to follow my mouse cursor but I am unable to draw the circle with the the Attack method because i have to suface.fill in the move method ( the move method is followMeLittleBoy) I can get the circle to draw for a split second but only while moving and it just barley. This is my full code other than my imports and such
class Hero ():
def __init__(self):
self.dead = False
pygame.mouse.set_visible(False)
self.X_MOVE_AMT = 5
self.Y_MOVE_AMT = 5
self.space = pygame.image.load ("hero_sprite.jpg")
self.spaceRect = self.space.get_rect()
self.spaceRect.topleft = (100,100)
surface.blit (self.space, self.spaceRect)
pygame.display.update()
def followMeLittleBoy (self):
amntTuple = pygame.mouse.get_pos()
self.X_MOVE_AMT = math.ceil((amntTuple[0] - self.spaceRect.left)*0.2)
self.Y_MOVE_AMT = math.ceil((amntTuple[1] - self.spaceRect.top)*0.2)
self.spaceRect.left += self.X_MOVE_AMT
self.spaceRect.top += self.Y_MOVE_AMT
surface.blit (self.space, self.spaceRect)
pygame.display.update()
def Attack (self):
surface.fill ((255,255,255))
amntTuple = pygame.mouse.get_pos()
pygame.draw.circle(surface, pygame.Color(0,0,255), amntTuple, 20, 2)
surface.blit (self.space, self.spaceRect)
var = Hero ()
while True:
surface.fill((255,255,255))
for event in pygame.event.get():
var.followMeLittleBoy ()
if event.type == QUIT:
pygame.quit()
sys.exit()
if event.type == KEYDOWN:
if event.key == K_SPACE:
var.Attack()
In a previous question I recommended you to update the state during events and call the draw routines from the main loop only once per iteration, this is specially important for the surface.fill call.
Now I strongly recommend to follow that aproach, otherwise this kind of problems will continue to arise.
There are few things that you need to fix.
var.followMeLittleBoy() should be called once per loop, not for every event.
You should have a seperate method for drawing in your Hero class.
Call pygame.display.update() only once per loop.
I am not sure what you are trying to accomplish, but you could create a list of circles that are to be drawn, and when you press spacebar, a circle is added to a list.
Then you can loop your circle list,and draw each of them without them dissapearing.

Pygame sprite clamping isse

So Ive been having issues with getting a sprite to stay withing the bounds of the screen. I got it to work with a simple rect(0,0,16,16), but i cant seem to get it to work with a sprite being blit onto the screen. What do i need to change in order to keep my sprite clamped within the screen res? I only just started today using classes to orgonize code so any input is appreciated and helpful.
import pygame
from pygame.locals import *
from pygame import Color
class Game():
""" Lets try to get this going by simple steps
One by one. First step, lets figure how to make a class
that can do the display stuff. Lord have mercy on my soul"""
def __init__(self, wi=256, hi=224, multii=3):
"""Initialization"""
pygame.init()
self.runGame = True
self.width = wi*multii
self.height = hi*multii
self.spritesize = 16*multii
self.clock = pygame.time.Clock()
self.fps = self.clock.get_fps()
self.screen = pygame.display.set_mode((self.width, self.height))
self.kl = []
self.walk = [0, 0]
self.speed = multii*1.5
self.x,self.y = self.width/2, self.height/2
self.playerSpr = pygame.image.load('images/'+'link1.png').convert_alpha()
self.playerRec = Rect(self.playerSpr.get_rect())
def mainLoop(self):
"""Loop through the main game routines
1. Drawing 2. Input handling 3. Updating
Then loop through it until user quits"""
while self.runGame:
self.clock.tick(60)
self.events()
self.draw()
def events(self):
"""Time to handle some events"""
for e in pygame.event.get():
if (e.type == pygame.QUIT) or (e.type == KEYDOWN and e.key == K_ESCAPE):
self.runGame = False
break
if e.type==KEYDOWN:
if e.key==pygame.K_a: self.kl.append(1)
if e.key==pygame.K_d: self.kl.append(2)
if e.key==pygame.K_w: self.kl.append(3)
if e.key==pygame.K_s: self.kl.append(4)
if e.type==pygame.KEYUP:
if e.key==pygame.K_a: self.kl.remove(1)
if e.key==pygame.K_d: self.kl.remove(2)
if e.key==pygame.K_w: self.kl.remove(3)
if e.key==pygame.K_s: self.kl.remove(4)
if self.kl[-1:]==[1]: self.walk=[-self.speed, 0]
elif self.kl[-1:]==[2]: self.walk=[ self.speed, 0]
elif self.kl[-1:]==[3]: self.walk=[0,-self.speed]
elif self.kl[-1:]==[4]: self.walk=[0, self.speed]
else: self.walk=[0, 0]
self.x+=self.walk[0]
self.y+=self.walk[1]
def draw(self):
"""Draw and update the main screen"""
self.fps = self.clock.get_fps()
self.screen.fill(Color('purple'))
#print self.screen.get_rect()
#print player_rect
self.playerSpr.clamp_ip(self.screen.get_rect())
#pygame.draw.rect(self.screen, (255, 255, 255), self.playerrect)
self.screen.blit(self.playerSpr, (self.x,self.y), self.playerRec)
pygame.display.set_caption('Grid2. FPS: '+str(self.fps))
pygame.display.update()
game = Game()
game.mainLoop()
Why not use playerRec to keep track of the position of your player instead of the additional x and y attributes?
I suggest also using the move method (or move_ip):
def events(self):
for e in pygame.event.get():
...
self.playerRec.move_ip(*self.walk) # instead of self.x+=self.walk[0] / self.y+=self.walk[1]
def draw(self):
...
# probably do this right after 'move_ip'
self.playerRec.clamp_ip(self.screen.get_rect())
# note that 'blit' accepts a 'Rect' as second parameter
self.screen.blit(self.playerSpr, self.playerRec)
as a side note: You should consider using a Sprite, since it basically combines an Image and a Rect.
Two things:
You are not stopping movement of the sprite when it comes off the screen.
Make an move functions that will get a direction and will decide if it can move more to the side. That way when the right side of the sprite will be of screen, you will not move more to the right.
Since you put your direction keys in a list that works like a stack, you are only getting 1 direction per keypress. If you also want to move diagonally either make two lists one for both directions or use a easier method such as this:
if KEYDOWN == K_LEFT: direction_x = -1
if KEYUP == K_LEFT AND direction_x == -1: direction_x = 0
do this for every key.

Pygame play a sound when click on certain spot on GUI

I'm an amateur, very inexperience programmer. I've been working on an art project that I am programming using Pygame. I've hit a road block, ad can't figure out how to do what I need it to do.
I need it to play a specific sound when clicking on a specific place on the GUI. For example, when you click on the red button, it plays an audio file that says "red"
I also need it to be able to play sounds with clicking and dragging on the canvas part.
I hope this is enough detail. Thanks for the help!
import pygame, sys, time, random
from pygame.locals import *
# set up pygame
pygame.init()
pygame.mixer.init
pygame.mixer.get_init
bgimg="GUIsmall.gif"
inst="instructionssm.gif"
white=(255,255,255)
screen=pygame.display.set_mode((800,600), pygame.RESIZABLE)
screen.fill(white)
bg=pygame.image.load(bgimg)
instrimg=pygame.image.load(inst)
screen.blit(bg, (0,0))
pygame.display.flip()
red=pygame.mixer.Sound("red.mp3")
while True:
for event in pygame.event.get():
if event.type==pygame.QUIT:
raise SystemExit
elif event.type==pygame.MOUSEBUTTONDOWN:
red.play(0,0,0)
I think you should have a class button and a collection of buttons:
class Button:
__init__(self, name, position, image_file, sound_file):
self.name = name
self.image = pygame.image.load(image_file)
self.sound = pygame.mixer.Sound(sound_file)
self.position = position
self.rect = pygame.Rect(position, self.image.get_size())
buttons = []
buttons.add( Button("red", (0,0), "red.png", "red.mp3") )
...
Then you can use it in the main loop:
while True:
for event in pygame.event.get():
if event.type==pygame.QUIT:
raise SystemExit
elif event.type==pygame.MOUSEBUTTONDOWN:
for b in buttons:
if b.rect.collidepoint(event.pos):
b.sound.play()

Resources