pygame class structure - python

My goal is to make a module that will make a grid on a pygame canvas and allow you to highlight boxes by their x and y coords.
Here is a simple example usage.
from grid import Grid
g = Grid(100, 100, 10) # width and height in cells, cell width in pixels
g.highlightBox(2, 2, (0, 255, 0)) # cell x and y, rgb color tuple
g.clearGrid()
Here is the code I have so far. The problem is, I need to have an event loop to keep the window open and make the close button functional, but I also need to allow the other functions to draw to the screen.
import pygame
import sys
class Grid:
colors = {"blue":(0, 0, 255), "red":(255, 0, 0), "green":(0, 255, 0), "black":(0, 0, 0), "white":(255, 255, 255)}
def __init__(self, width, height, cellSize, borderWidth=1):
self.cellSize = cellSize
self.borderWidth = borderWidth
self.width = width * (cellSize + borderWidth)
self.height = height * (cellSize + borderWidth)
self.screen = pygame.display.set_mode((self.width, self.height))
running = True
while running:
event = pygame.event.poll()
if event.type == pygame.QUIT:
running = False
def clearGrid(self):
pass
def highlightBox(self, x, y, color):
xx = x * (self.cellSize + self.borderWidth)
yy = y * (self.cellSize + self.borderWidth)
pygame.draw.rect(self.screen, color, (xx, yy, self.cellSize, self.cellSize), 0)
When I run the first sample, the code will be stuck in the loop, not allowing me to run the highlightBox function until the loop is done(the exit button is pressed).

For starters, I wouldn't put the game loop inside the initialization function; find some other place for it. To solve this, simply put the code you want to execute in the game loop, next to the code for handling events:
running = True
while running:
event = pygame.event.poll()
if event.type == pygame.QUIT:
running = False
# Print your screen in here
# Also do any other stuff that you consider appropriate

I think what you need is to disconnect the Grid class from the display of it. You sould make it generate surfaces, that would be printed to the screen Surface by the main game-loop. Your init , highlight_cell, and clear_grid methods could return Surfaces for example, or make a get_surface method that would be called once every game-loop
This would give much more flexibility

I got a working version with the multiprocessing library and pipes. It seems kinda unpythonic but it will work for this project.
import pygame
import sys
from multiprocessing import Process, Pipe
class Grid:
colors = {"blue":(0, 0, 255), "red":(255, 0, 0), "green":(0, 255, 0), "black":(0, 0, 0), "white":(255, 255, 255)}
def __init__(self, width, height, cellSize, borderWidth=1):
self.cellSize = cellSize
self.borderWidth = borderWidth
self.width = width * (cellSize + borderWidth)
self.height = height * (cellSize + borderWidth)
#pygame.draw.rect(self.screen, todo[1], (todo[2], todo[3], todo[4], todo[5]), 0)
self.parent_conn, self.child_conn = Pipe()
self.p = Process(target=self.mainLoop, args=(self.child_conn, self.width, self.height,))
self.p.start()
def close():
self.p.join()
def clearGrid(self):
pass
def highlightBox(self, x, y, color):
xx = x * (self.cellSize + self.borderWidth)
yy = y * (self.cellSize + self.borderWidth)
self.parent_conn.send(["box", color, xx, yy, self.cellSize, self.cellSize])
def mainLoop(self, conn, width, height):
#make window
screen = pygame.display.set_mode((self.width, self.height))
running = True
while running:
# is there data to read
if conn.poll():
#read all data
todo = conn.recv()
print("Recived " + str(todo))
#do the drawing
if todo[0] == "box":
print("drawing box")
pygame.draw.rect(screen, todo[1], (todo[2], todo[3], todo[4], todo[5]), 0) #color, x, y, width, height
todo = ["none"]
#draw to screen
pygame.display.flip()
#get events
event = pygame.event.poll()
if event.type == pygame.QUIT:
running = False

Related

Why does this bug happen when I click on two sprites at the same time?

I'm making a simple game using pygame where you keep clicking on tiles as fast as you can until you miss a tile. this is the progress I've made so far. sometimes when I click on a tile (usually when 2 tiles are next to each other and you click between them) one of them does what they're supposed to while the other just disappears from the screen.
import pygame
import random
import sys
#Setting up all possible Tile positions
grid = [[0,0], [0,150], [0,300], [0,450], [0,600],
[150,0],[150,150],[150,300],[150,450],[150,600],
[300,0],[300,150],[300,300],[300,450],[300,600],
[450,0],[450,150],[450,300],[450,450],[450,600],
[600,0],[600,150],[600,300],[600,450],[600,600]]
taken = []
#Classes
class Cursor(pygame.sprite.Sprite):
def __init__(self, pic):
super().__init__()
self.image = pygame.image.load(pic).convert_alpha()
self.image = pygame.transform.scale(self.image, (50,50))
self.rect = self.image.get_rect()
def destroyTile(self):
pygame.sprite.spritecollide(cursor, tileGroup, True)
def update(self):
self.rect.topleft = pygame.mouse.get_pos()
class Tiles(pygame.sprite.Sprite):
def __init__(self, tileSize, color, x, y):
super().__init__()
self.image = pygame.Surface(([tileSize, tileSize]))
self.image.fill(color)
self.rect = self.image.get_rect()
self.rect.topleft = [x, y]
def drawTiles():
takenLen = len(taken)
while takenLen != 3:
m = random.randint(0,24)
x, y = grid[m]
if grid[m] not in taken:
blackTile = Tiles(150, black, x, y)
blackTile.add(tileGroup)
taken.append(grid[m])
takenLen += 1
def handleTiles():
mx, my = pygame.mouse.get_pos()
modx = mx % 150
mody = my % 150
x = mx - modx
y = my - mody
taken.remove([x, y])
drawTiles()
def drawRedTile():
mx, my = pygame.mouse.get_pos()
modx = mx % 150
mody = my % 150
x = mx - modx
y = my - mody
redTile = Tiles(150, red, x, y)
redTile.add(tileGroup)
#Colours
white = (255, 255, 255)
black = (0, 0, 0)
red = (255, 0, 0)
blue = (0, 0, 255)
grey = (46, 46, 46)
#Initializing Pygame
pygame.init()
clock = pygame.time.Clock()
#Screen
screenWidth = 750
screenHeight = 900
screen = pygame.display.set_mode((screenWidth, screenHeight))
pygame.display.set_caption("Tiles Game")
whiteSurface = pygame.Surface((750, 750))
whiteSurface.fill(white)
pygame.mouse.set_visible(False)
#Blue line
line = pygame.Surface((750, 10))
line.fill(blue)
#Groups
tileGroup = pygame.sprite.Group()
cursor = Cursor("cursor.png")
cursorGroup = pygame.sprite.Group()
cursorGroup.add(cursor)
score = 0
drawTiles()
while True:
clock.tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
if event.type == pygame.MOUSEBUTTONDOWN:
score += 1
print(score)
print(taken)
print(tileGroup)
cursor.destroyTile()
handleTiles()
#Background
screen.fill(grey)
screen.blit(whiteSurface, (0,0))
screen.blit(line, (0,750))
tileGroup.draw(screen)
cursorGroup.draw(screen)
cursorGroup.update()
pygame.display.update()
In the code I tried using print statements to see if the tile that seems to have disappeared is still there. When this happens, I assume that the tile is not in its group anymore since the number of sprites in the tile group went from 3 to 2. But the list showing all the taken positions still shows that there are 3 positions that are taken. I can still click on the tile if I just click on the space where there should be a tile and the tile comes back. I thought the game should exit when a tile isn't clicked on but it doesn't if there is an "invisible" tile in that position.
How do I make it so that this bug doesn't happen and every new tile made is visible?
The problem is that the cursor has an area and can hit more than one block at a time. So in destroyTile more than 1 block can be removed at once:
def destroyTile(self):
pygame.sprite.spritecollide(cursor, tileGroup, True)
However, the function handleTiles cannot handle this, because it can only remove one block position from the taken list. I suggest to simplify the code and recreate the taken list completely from tileGroup when blocks are removed:
def handleTiles():
taken.clear()
for tile in tileGroup:
x, y = tile.rect.topleft
taken.append([x, y])
drawTiles()

My screen glitches out when I try to run my subprogram code

I'm working on a subprogram code that will make this happy face bounce around the screen and turn different colours. For some reason, the screen turns into that black glitchy screen and when I press exit at the top the face shows for a quick second before the program shuts down. I can't figure out why this is, here is my code and I've included a picture of what happens at first when I run it:
""" Program to show a very basic function
Most of the program is exactly the same as other programs we have done
The main difference is the grouping of code into a function called
drawHappy() to draw a few shapes together
In the main loop we "call" this function whenever we want to draw this
group of shapes
"""
# import the necessary modules
import pygame
import sys
import math
import random
from random import randint
# initialize pygame
pygame.init()
# set the size for the surface (screen)
# note this screen is resizable by the user
screen = pygame.display.set_mode((800, 600), pygame.RESIZABLE)
# set the caption for the screen
pygame.display.set_caption("Happy Face")
#screen width and height
screenW = screen.get_width()
screenH = screen.get_height()
# define colours you will be using
WHITE = (255, 255, 255)
GREEN = (0, 255, 0)
RED = (255, 0, 0)
BLUE = (0, 0, 255)
BLACK = (0, 0, 0)
YELLOW = (255, 255, 0)
# funtion to draw a the "happy face"
# it has 4 parameters passed to it xPos, yPos, radius, and colour
# notice all the shapes are drawn "relative" to the xPos and yPos and the radius
def drawHappy(xPos,yPos,r,colour):
pygame.draw.circle(screen,colour,(xPos,yPos),r,1)
eyeRadius = int(1/6*r)
eyeX = int(xPos-1/3*r)
eyeY = int(yPos- 1/3*r)
pygame.draw.circle(screen,colour,(eyeX,eyeY),eyeRadius,1)
eyeX = int(xPos + 1/3*r)
pygame.draw.circle(screen,colour,(eyeX,eyeY),eyeRadius,1)
wMouth = 1.5*r
xMouth = xPos - 3/4*r
yMouth = yPos - 3/4*r
pygame.draw.arc(screen,colour,(xMouth,yMouth,wMouth,wMouth),math.pi,2*math.pi,1)
randomR = randint(1,300)
r = randomR
randomX = randint(r, 800-r)
randomY = randint(r, 600-r)
dx = 0
dy = 0
x = 100
y = 100
speed = 3
x2 = randomX
y2 = randomY
dx2 = speed
dy2 = -speed
colour_list = [YELLOW, BLACK, BLUE, RED, GREEN]
randomcolour = random.choice(colour_list)
colour = RED
# set up clock to control frames per second
clock = pygame.time.Clock()
FPS = 120
# set main loop to True so it will run
main = True
# main loop
while main:
for event in pygame.event.get(): # check for any events (i.e key press, mouse click etc.)
if event.type == pygame.QUIT: # check to see if it was "x" at top right of screen
main = False # set the "main" variable to False to exit while loop
clock.tick(FPS)
screen.fill(WHITE)
oldx = x
oldy = y
x += dx
y += dy
if x >= 800-r or x <= 0+r:
x = oldx
if y >= 600-r or y <= 0+r:
y = oldy
x2 += dx2
y2 += dy2
if x >= 800-r or x <= 0+r:
dx2 = -dx2
randomcolour = random.choice(colour_list)
colour = randomcolour
if y2 >= 600-r or y2 <= 0+r:
dy2 = -dy2
randomcolour = random.choice(colour_list)
colour = randomcolour
# "call" the function "drawHappy()" to draw the happy face
# this is where we would normally do a pygame.draw or a screen.blit()
# we are "passing" the function 4 values to use(x,y,radius, colour)
# it will use these to know where to draw the happy face
drawHappy(x2,y2,r,colour)
pygame.display.flip()
# quit pygame and exit the program (i.e. close everything down)
pygame.quit()
sys.exit()
First of all, you need to call your draw function inside the loop. Your current code shows only a glimpse of "drawing" because it gets executed once you exit the main loop.
So, put your drawHappy() inside of main loop:
while main:
for event in pygame.event.get(): # check for any events (i.e key press, mouse click etc.)
if event.type == pygame.QUIT: # check to see if it was "x" at top right of screen
main = False # set the "main" variable to False to exit while loop
drawHappy(x2,y2,r,colour)
pygame.display.update()
clock.tick(FPS)
screen.fill(WHITE)
Now you will get a random size "smiley" on the screen, But now it will move on exit only, for the same reason it wouldn't display earlier. Next thing is to make it bounce (move). For this you'll need some kind of update of the coordinates, just like you did in the last part of your code, except they also need to be updated during the loop, not after it.
I suggest making a Class because then it will be easier to manipulate the object.
Also, I found it easier to separate draw and update_coordinates code into separate functions and them call them from main loop for example.
Hope this helps, and if you need more help, ask.
Here, I made a quick solution using parts of your code, there is plenty room for improvement especially for update_smiley_position() method where you can control how "smiley" moves.
Also, if you need multiple objects, a list should be passed instead of single object.
import pygame as pg
import math
import random
pg.init()
clock = pg.time.Clock()
window = pg.display.set_mode((800, 600), pg.RESIZABLE)
pg.display.set_caption("Happy Face")
SCREEN_W = window.get_width()
SCREEN_H = window.get_height()
class Smiley:
def __init__(self, x, y, r, color):
self.x = x
self.y = y
self.r = r
self.color = color
self.create_smiley()
def create_smiley(self):
self.eye_radius = int(1/6 * self.r)
self.eye_x1 = int(self.x - 1/3 * self.r)
self.eye_x2 = int(self.x + 1/3 *self.r)
self.eye_y = int(self.y - 1/3 *self.r)
self.mouth_width = 1.5 * self.r
self.mouth_x = self.x - self.r * 0.75
self.mouth_y = self.y - self.r * 0.75
def draw_smiley(self, win):
pg.draw.circle(win, self.color, (self.x, self.y), self.r, 1)
pg.draw.circle(win, self.color, (self.eye_x1, self.eye_y), self.eye_radius, 1)
pg.draw.circle(win, self.color, (self.eye_x2, self.eye_y), self.eye_radius, 1)
pg.draw.arc(win, self.color, (self.mouth_x, self.mouth_y, self.mouth_width, self.mouth_width), math.pi, 2*math.pi, 1)
def update_smiley_position(self):
if self.x >= SCREEN_H - self.r or self.x <= 0 + self.r:
self.x = random.randint(100, 400)
else:
self.x += 5
if self.y >= SCREEN_W - self.r or self.y <= 0 + self.r:
self.y = random.randint(100, 400)
else:
self.y -= 5
self.create_smiley()
def draw(win, smiley):
win.fill(pg.Color("white"))
smiley.draw_smiley(win)
smiley.update_smiley_position()
pg.display.update()
def main_loop(win, smiley):
clock.tick(30)
for event in pg.event.get():
if event.type == pg.QUIT:
return False
draw(win, smiley)
return True
r = random.randint(1, 300)
x = random.randint(r, SCREEN_W - r)
y = random.randint(r, SCREEN_H - r)
smiley = Smiley(x, y, r, pg.Color("red"))
while main_loop(window, smiley):
pass
pg.quit()

How to get a black screen and grids drawn? [duplicate]

This question already has an answer here:
Pygame unresponsive display
(1 answer)
Closed 2 years ago.
I have an issue. Here is my code :
import pygame
import tkinter as tk
class cube():
rows = 20
w = 500
def __init__(self, start, dirnx=1, dirny=0, color=(255, 0, 0)):
pass
class snake(object):
def __init__(self, color, pos):
pass
def drawGrid(w, rows, surface):
b = 255
sizeBtwn = w // rows
x = 0
y = 0
for l in range(rows):
x = x + sizeBtwn
y = y + sizeBtwn
pygame.draw.line(surface, (b, b, b), (x,0), (x,w))
pygame.draw.line(surface, (b, b, b), (0,y), (w,y))
def redrawWindow(surface):
global rows, width
surface.fill((0,0,0))
drawGrid(width, rows, surface)
pygame.display.update()
def main():
global width, rows
width = 1280
height = 720
rows = 40
win = pygame.display.set_mode((width, height))
s = snake((255, 0, 0), (10, 10))
run = True
clock = pygame.time.Clock()
while run:
clock.tick(30)
redrawWindow(win)
if __name__ == "__main__":
main()
When I launch I have nothing. I have a grey window. But I should have a black window with grids... Why does nothing appear ?
Thank you to help me !
You have to handle the events, by either pygame.event.pump() or pygame.event.get().
This functions do not handle the internal events, too. This is necessary to keep the system responding and for updating the display.
def main():
# [...]
clock = pygame.time.Clock()
while run:
# handle events
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
clock.tick(30)
redrawWindow(win)

How to create a pop-up in pygame with transparent background and how to control loop as a line is been drawn?

def timer_initial_state(screen):
'''Creates an thin line representing the timer'''
pygame.draw.line(screen,
COLOR_ORANGE_RED,
(0,screen.get_height()*0.35),
(screen.get_width(), screen.get_height()*0.35),
3)
def timing_sequence(screen):
'''Creates a timer in the form of a line'''
increment = 0.01
while increment >=0:
pygame.draw.line(
screen,
COLOR_BUTTON_UP,
(screen.get_width()*increment, screen.get_height()*0.35),
(0, screen.get_height()*0.35),
3)
pygame.draw.line(
screen,
COLOR_ORANGE_RED,
(0, screen.get_height()*0.35),
(screen.get_width(),screen.get_height()*0.35),
3)
increment += 0.01
The first function draws the line. The goal of the second is to draw over the first line in at a specific interval like that of a clock. ie. it should draw over a portion of the line after 1 second.
What I want to do looks just like this:
Furthermore, I want to show a popup after the timer is done with a transparent background.
Something like this will progressively draw a line:
import pygame, sys
from pygame.locals import *
FPS = 30 # frames per second
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
YELLOW = (255, 255, 0)
RED = (255, 0, 0)
class ProgressiveLine(object):
def __init__(self, x1, y1, x2, y2, color=WHITE, width=1):
self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2
self.color, self.width = color, width
self.dx = self.x2-self.x1
self.dy = self.y2-self.y1
def draw(self, surface, percentage):
percentage = min(abs(percentage), 1.0)
xcur = self.x1 + self.dx*percentage
ycur = self.y1 + self.dy*percentage
pygame.draw.line(surface, self.color, (self.x1, self.y1), (xcur, ycur),
self.width)
def main():
pygame.init()
fpsclock = pygame.time.Clock()
screen = pygame.display.set_mode((500,400), 0, 32)
screen.fill(WHITE)
prgrsline = ProgressiveLine(10, 390, 490, 10, RED, 3)
percent_complete = 0.0
while True: # display update loop
screen.fill(WHITE)
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
if percent_complete < 1.0:
percent_complete += 0.01
prgrsline.draw(screen, percent_complete)
pygame.display.update()
fpsclock.tick(FPS)
main()
As martineau said you need to fix your loop so it will complete.
You are drawing your partial line first, then drawing the full line over it. You need to draw the order of on top last.
You also need to flip (or update) the display after your draw.
This should all be done inside an event loops so your app will still responding to events.
Timing based on per frames is a bit ugly. I would have a number of seconds for animation and a start time and compute the increment = (now - start_time) / animation_time
maybe it would be better to have this just be a function that draws based on time and not have its own while loop.

How To Find Out If A .Rect() Has Been Clicked In Pygame

I'm trying to find out if a user has clicked on a .rect(). I've read a bunch of tutorials, but I'm still running into problems. I have two files: One that is the main python file, and one that defines the .rect().
#main.py
import pygame,os,TextBox
from pygame.locals import *
pygame.init()
myTextBox = TextBox.TextBox()
WHITE = (255, 255, 255)
size = (400, 200)
screen = pygame.display.set_mode(size)
done = False
boxes = [myTextBox]
while not done:
for event in pygame.event.get(): # User did something
if event.type == pygame.QUIT: # If user clicked close
done = True
elif event.type == MOUSEMOTION: x,y = event.pos
for box in boxes:
if myTextBox.rect.collidepoint(x,y): print ('yay')
screen.fill(WHITE)
myTextBox.display(screen, 150, 150, 20, 100)
pygame.display.flip()
pygame.quit()
#TextBox.py
import pygame
from pygame.locals import *
class TextBox:
def __init__(self):
self.position_x = 100
self.position_y = 100
self.size_height = 10
self.size_width = 50
self.outline_color = ( 0, 0, 0)
self.inner_color = (255, 255, 255)
def display(self, screen, x, y, height, width):
self.position_x = x
self.position_y = y
self.size_height = height
self.size_width = width
pygame.draw.rect(screen, self.outline_color, Rect((self.position_x - 1, self.position_y - 1, self.size_width + 2, self.size_height + 2)))
pygame.draw.rect(screen, self.inner_color, Rect((self.position_x, self.position_y, self.size_width, self.size_height)))
The error I get is AttributeError: 'TextBox' object has no attribute 'rect'
How do I solve this?
You're TextBox class doesn't have a rect. Add this somewhere at the bottom of the __init__ method of the TextBox class:
self.rect = pygame.Rect(self.position_x,self.position_y,self.size_width,self.size_height)
Do the same in the update method.

Categories