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()
It's me again...
As my most recent questions have been discussing, I am making a main menu navigation for a game. Everything was going fine and dandy until a recent bug that has turned all text, either in buttons or simply drawn on the screen, black. I'm not sure why this is and I was hoping that someone smarter than me could figure it out. I really need this done so any help is greatly greatly appreciated.
# Libraries #
import pygame # Imports the Pygame library.
from pygame.locals import * # Imports the Pygame Locals library.
import ResolutionMenu # Imports the ResolutionMenu file (must be stored in the same location as this file to work)
# Classes #
class GuiSettings: # Class used to define the colour and size of buttons.
def __init__(self):
self.TextSize = 26
self.button_color = (35, 65, 145)
self.button_color_hover = (70, 105, 150)
class Button():
def __init__(self, x, y, width, height, outline, settings, text = "", action = None): # Classes used to actually create a button.
self.x = x # Screen is divided into an axis, x being horizontal.
self.y = y # And Y being vertical. The maximum bound changes depending on screen resolution.
self.width = width # This determines the dimensions of the box that is going to be created.
self.height = height # Height and Width are measured in pixels, similar to X and Y.
self.text = text # Allows text to be printed over the top of the button.
self.settings = settings # Allows it to be manipulated by the GuiSettings.
self.action = action # Will be used to determine what 'action' the user is performing on the button.
self.outline = outline # Will be used to determine if an outline is or is not drawn around the button.
def draw(self, screen, outline = None):
if self.outline: # Used to draw a black outline around the perimeter of the square.
pygame.draw.rect(screen, self.outline, (self.x - 2, self.y - 2, self.width + 4, self.height + 4), 0)
if self.text != "":
font = pygame.font.SysFont('segoeuisemibold', self.settings.TextSize) # Settings for text inside of the button box.
text = font.render(self.text, 1, (0, 0, 0)) # Renders the font with the variables 'text' which is the text on the box, anti-aliasing is the second
# option, of which removes the jagged effect that might appear around the box.
# and finally the colour of the outline.
screen.blit(text, (self.x + (self.width / 2 - text.get_width() / 2), self.y + (self.height / 2 - text.get_height() / 2))) # Places the text in the middle of the box.
def update(self, events): # Simply used to determine what actions need to be taken when the button is pressed, if any.
for event in events:
if event.type == pygame.MOUSEBUTTONDOWN and self.isOver(pygame.mouse.get_pos()) and self.action:
self.action()
def isOver(self, pos): # Determines if the cursor is actually over the button box or not.
if pos[0] > self.x and pos[0] < self.x + self.width:
if pos[1] > self.y and pos[1] < self.y + self.height:
return True
return False
# Variables #
settings = GuiSettings() # Assigning class to variable to access parameters.
H, W = ResolutionMenu.resolution # Splits the tuple value of resolution into individual segments and assigns them their respective variables.
pygame.init() # Initialises the pygame library.
pygame.display.set_caption("Main Menu") # Creates a title for the program.
font = pygame.font.SysFont(None, settings.TextSize) # Sets the default font to that of the font on the System. Allows for cross platform usability.
screen = pygame.display.set_mode((ResolutionMenu.resolution)) # Sets the resolution to the resolution determined in ResolutionMenu.py.
# Functions #
def draw_text_center(text, font, colour, surface):
textobj = font.render(text, 1, colour) # Function for simple text creation. No button surrounding.
textrect = textobj.get_rect() # Creates a font taking the string 'text', sets antialiasing to true and sets the text colour.
textrect.center = ((H / 2), (W / 10)) # Creates a rectangle around 'textobj' which is simply the text.
surface.blit(textobj, textrect) # Finds the center of the screens
def draw_text(text, font, colour, surface, XandY): # Same function as above but with manipulatable positioning.
textobj = font.render(text, 1, colour)
textrect = textobj.get_rect()
textrect.topleft = (XandY)
surface.blit(textobj, textrect)
def MainMenu():
global font
while True:
pygame.display.set_mode((H, W)) # Sets the resolution to the resolution chosen before.
pygame.display.flip() # Updates the surface display.
buttons = [
Button(W / 10, H / 5, W / 3, H / 15, (0, 0, 0), settings, "Start Simulation", lambda: game()), # Buttons that will be displayed are placed inside of a list.
Button(W / 10, H / 2, W / 3, H / 15, (0, 0, 0), settings, "Options Menu", lambda: OptionsMenu()) # Lambda functions are one time use functions without names. This allows each
] # button to have a single use function that directs the program to the specified section.
running = True
while running:
events = pygame.event.get() # Gathers all events (user made inputs) that could occur.
for event in events:
if event.type == pygame.QUIT: # When the X is pressed in the corner of the application, it shall close.
pygame.quit()
return
if event.type == KEYDOWN: # When pressing Esc the function is broken and refreshes.
if event.key == K_ESCAPE:
running = False
for button in buttons:
button.update(events) # Enables the button to be interacted with.
screen.fill((100, 100, 100)) # Creates a grey background.
draw_text_center("Main Menu Navigation", font, (0, 0, 0), screen) # Creates Main Menu Navigation text in the top middle of the screen.
font = pygame.font.SysFont(None, settings.TextSize) # Makes the font size equal to that of settings.TextSize from GuiSettings class.
for button in buttons:
button.draw(screen) # Draws the buttons on the window.
pygame.display.flip() # Updates the surface display again.
pygame.display.update() # Updates a portion of the screen, allowing for text to render.
def game():
running = True
while running:
screen.fill((100, 100, 100))
draw_text_center("Simulation Navigation", font, (0, 0, 0), screen)
buttons = [
Button(W / 10, H / 5, W / 3, H / 15, (0, 0, 0), settings, "Enter Simulation", lambda: FileTraversal()),
Button(W / 10, H / 2, W / 3, H / 15, (0, 0, 0), settings, "Instructions", lambda: Instructions())
]
events = pygame.event.get()
for event in events:
if event.type == pygame.QUIT:
pygame.quit()
return
if event.type == KEYDOWN:
if event.key == K_ESCAPE:
running = False
for button in buttons:
button.update(events)
for button in buttons:
button.draw(screen)
pygame.display.flip()
pygame.display.update()
def Instructions():
running = True
while running:
screen.fill((100, 100, 100))
draw_text("This is the simulation manual.", font, (0, 0, 0), screen, (50, 50))
draw_text("There are a few button combinations that will perform different tasks:", font, (0, 0, 0), screen, (50, 100))
draw_text("P", font, (200, 0, 0), screen, (50, 150))
draw_text(": Pauses the simulation.", font, (0, 0, 0), screen, (70, 150))
draw_text("D", font, (200, 0, 0), screen, (50, 200))
draw_text(": Toggles drawing mode. Allows for faster computation.", font, (0, 0, 0), screen, (70, 200))
draw_text("F", font, (200, 0, 0), screen, (50, 250))
draw_text(": Toggles the display of food. Increases FPS.", font, (0, 0, 0), screen, (70, 250))
draw_text("+ and -", font, (200, 0, 0), screen, (50, 300))
draw_text(": Increases or Decreases the speed of the simulation. FPS impact.", font, (0, 0, 0), screen, (130, 300))
draw_text("Middle Mouse Click", font, (200, 0, 0), screen, (50, 350))
draw_text(": Zoom in and out.", font, (0, 0, 0), screen, (250, 350))
draw_text("Right Mouse Click", font, (200, 0, 0), screen, (50, 400))
draw_text(": Pan around the environment", font, (0, 0, 0), screen, (240, 400)) # All of these are displayed on the screen as instructions for the user.
events = pygame.event.get()
for event in events:
if event.type == pygame.QUIT:
pygame.quit()
return
if event.type == KEYDOWN:
if event.key == K_ESCAPE:
running = False
pygame.display.flip()
pygame.display.update()
def FileTraversal():
import os # Imports the Operating System library that allows for command line execution.
import platform # Imports the Platform library that allows for system analysis.
p = platform.system() # Assign the variable p the name of the operating system.
if p == "Linux":
os.chdir('/home/CHANGE/Desktop/School Work/New') # Directory traversal.
pygame.display.iconify() # Linux systems do not run EXE's. This file is an ELF file
os.system('./mygame') # and so must be run from terminal.
if p == "Windows":
os.chdir('C:/Users/CHANGE/Desktop/Project/') # Directory traversal.
# Uses EXE's as ELF's require Linux kernals.
if p == "OSX":
None
def OptionsMenu():
global settings # Functions use local variables. In order to manipulate the global value, global settings is specified.
pygame.display.set_mode((800, 600))
buttons = [
Button(100, 150, 250, 50, (0, 0, 0), settings, "Set Text Size: Small", lambda: settings.__setattr__('TextSize', 24)), # Button uses a lambda function to access
Button(450, 150, 250, 50, (0, 0, 0), settings, "Set Text Size: Regular", lambda: settings.__setattr__('TextSize', 26)), # the attributes of settings, which is the class
Button(100, 300, 250, 50, (0, 0, 0), settings, "Set Text Size: Large", lambda: settings.__setattr__('TextSize', 28)), # GuiSettings. It then sets the value of TextSize to
Button(450, 300, 250, 50, (0, 0, 0), settings, "Set Text Size: Larger", lambda: settings.__setattr__('TextSize', 30)) # the corresponding value, depending on the button pressed.
]
running = True
while running:
events = pygame.event.get()
for event in events:
if event.type == pygame.QUIT:
pygame.quit()
return
if event.type == KEYDOWN:
if event.key == K_ESCAPE:
running = False
pygame.display.set_mode((ResolutionMenu.resolution)) # Bug fix. Returns the resolution to the previously selected.
for button in buttons:
button.update(events)
screen.fill((100, 100, 100))
for button in buttons:
button.draw(screen)
pygame.display.flip()
MainMenu()
To whomever reads through all of this, good luck reading through all of that rubbish lol
**Added the code for ResolutionMenu below
# Libraries #
from tkinter import *
import time
# Variables List #
root = Tk() # Initialises Tkinter under the variable 'root'.
root.title("Resolution") # Applies a name to the window.
root.geometry("230x200") # Sets the window's geometry.
displays = 800, 600, 1024, 786, 1280, 1024, 1600, 1200, 1920, 1080 # Uses a string for resolutions.
running = True
v = IntVar() # Assignes the value of V to be an integer.
# Functions #
def CONFIRM(label1):
global resolution # Global variables used to access / manipulate outside of function.
global selection
selection = v.get() # Takes the value from the radio buttons.
if selection == 1:
resolution = (displays[0:2]) # Iterative Index naviation. 0 (0) indicates the first index up until the 2nd (2), but not including (, index.
time.sleep(0.2) # Pauses the program for .2 seconds for fluid transition appearance.
root.destroy() # Closes the TK window as it is no longer needed.
elif selection == 2:
resolution = (displays[2:4])
time.sleep(0.2)
root.destroy()
elif selection == 3:
resolution = (displays[4:6])
time.sleep(0.2)
root.destroy()
elif selection == 4:
resolution = (displays[6:8])
time.sleep(0.2)
root.destroy()
elif selection == 5:
resolution = (displays[8:10])
time.sleep(0.2)
root.destroy()
else:
resolution = (displays[0:2])
time.sleep(0.2)
root.destroy()
# Radio Button Creation #
v.set(1) # Validation. Sets the variable of V to 1 (radiobutton1) automatically.
Radio1 = Radiobutton(root, font = "Veranda 10", text = "800 x 600", variable = v, value = 1).pack(anchor = W)
Radio2 = Radiobutton(root, font = "Veranda 10", text = "1024 x 768", variable = v, value = 2).pack(anchor = W)
Radio3 = Radiobutton(root, font = "Veranda 10", text = "1280 x 1024", variable = v, value = 3).pack(anchor = W)
Radio4 = Radiobutton(root, font = "Veranda 10", text = "1600 x 1200", variable = v, value = 4).pack(anchor = W)
Radio5 = Radiobutton(root, font = "Veranda 10", text = "1920 x 1080", variable = v, value = 5).pack(anchor = W)
# Each of the above create a radio button. Using Tkinter's pre-made Radiobutton() function, you can simply link
# the radio button to the necessary window, in this case "root", the font size and the desired text message
# a variable that will be used to hold the value of the selection, followed by an integer that will be stored
# inside of said variable. Finally we pack it using .pack() function and give it an anchor, ie the direction it is aligned.
label1 = Label(root,text="") # Temp variable to hold information for lambda expression.
b = Button(root, font = "Veranda 10", text = "Confirm", command = lambda:CONFIRM(label1)) # Lambda creates temporary in-line function to pass selection to Confirm()
b.pack(anchor = SE)
label1.pack()
# Program Loop #
mainloop()
Okay! I solved the issue; it turns out some lines of code in my 'button' class had been deleted by accident. I had a backup of the code and after replacing the class with the working version, everything is back to normal.
The fixed class code will be at the bottom.
Thank you to #Nebulous29 for making the suggestion to change the value of the outline to (255, 0, 0). That's not actually the colour of the button but the outline surrounding the button. After I changed that I realised that something wasn't working with the outline function.
Fixed Code:
class Button():
def __init__(self, x, y, width, height, outline, settings, text = "", action = None):
self.x = x
self.y = y
self.width = width
self.height = height
self.text = text
self.settings = settings
self.action = action
self.outline = outline
def draw(self, screen, outline = None):
if self.outline:
pygame.draw.rect(screen, self.outline, (self.x - 2, self.y - 2, self.width + 4, self.height + 4), 0)
color = self.settings.button_color if not self.isOver(pygame.mouse.get_pos()) else self.settings.button_color_hover
pygame.draw.rect(screen, color, (self.x, self.y, self.width, self.height), 0)
if self.text != "":
font = pygame.font.SysFont('segoeuisemibold', self.settings.TextSize)
text = font.render(self.text, 1, (0, 0, 0))
screen.blit(text, (self.x + (self.width / 2 - text.get_width() / 2), self.y + (self.height / 2 - text.get_height() / 2)))
def update(self, events):
for event in events:
if event.type == pygame.MOUSEBUTTONDOWN and self.isOver(pygame.mouse.get_pos()) and self.action:
self.action()
def isOver(self, pos):
if pos[0] > self.x and pos[0] < self.x + self.width:
if pos[1] > self.y and pos[1] < self.y + self.height:
return True
return False
The problem of the black background lies when you create your buttons:
Button(W / 10, H / 5, W / 3, H / 15, (0, 0, 0), settings, "Start Simulation", lambda: game()), # Buttons that will be displayed are placed inside of a list.
After the width and heights are set you have (0, 0, 0) which sets the button colour to black. Changing this to something like (255, 0, 0) would cause the background to be red and the text to be black (making the button at least readable).
I'm not sure why the button hover colour and button colour you've set in GuiSettings isn't updating the button though, but at least its a somewhat temporary fix
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.