1

I have different screens/menus set as instances of the Scene class. Whenever I want to switch screens, my gameloop checks for a button click, and if successful, it calls a function that defines the components of a screen and combines it into a Scene instance. However, when I switch screens a black screen is rendered for 1 frame. Through a series of wait commands, I've found that the line

combatscreen = Scene("combatscreen", settings["resolution"]["x"], settings["resolution"]["y"], [50, 50, 50], [atkbtn, backbtn])

is responsible, though I'm not sure how, as no display update is called.

This is the necessary code to run the window and trigger the bug:

import pygame
import sys
import types
import json

pygame.init()

with open("settings.json") as file:
        settings = json.load(file)

class Scene:
    all_scenes = []
    
    def __init__(self, name, sizex, sizey, bgcolor, buttons=[], textboxes=[]):
        self.name = name
        self.sizex = sizex
        self.sizey = sizey
        self.bgcolor = bgcolor
        self.buttons = buttons
        self.textboxes = textboxes
        self.active = False
        Scene.all_scenes.append(self)
        self.surface = pygame.display.set_mode(size=[sizex, sizey])

class Button:
    def __init__(self, dest, destargs, posx, posy, width, height, text, color=[200,200,200], textcolor=[0,0,0], textsize=40):
        self.dest = dest
        self.destargs = destargs
        self.posx = posx
        self.posy = posy
        self.width = width
        self.height = height
        self.text = text
        self.color = color
        self.textcolor = textcolor
        self.textsize = textsize

    def btnclick(self, prev_scene):
        mpos = pygame.mouse.get_pos()
        clicks = pygame.mouse.get_just_released()
        btnbounds = pygame.draw.rect(self.surface, self.color, (self.posx, self.posy, self.width, self.height))
        # Checks if LMB was clicked within the bounds of the button
        if btnbounds.collidepoint(mpos):
            if clicks[0]:
                if isinstance(self.dest, types.FunctionType):
                    # Deactivates current scene and runs the function to define and activate the destination scene
                    prev_scene.active = False
                    self.dest()
                else:
                    pass
    
    def btnrender(self):
        mpos = pygame.mouse.get_pos()
        btnrect = pygame.draw.rect(self.surface, self.color, (self.posx, self.posy, self.width, self.height))
        if btnrect.collidepoint(mpos):
            # Makes the button lighter when hovered
            self.color[0] += 20
            self.color[1] += 20
            self.color[2] += 20
            pygame.draw.rect(self.surface, self.color, (self.posx, self.posy, self.width, self.height))
            # Resets button color
            self.color[0] -= 20
            self.color[1] -= 20
            self.color[2] -= 20
        btnlabel = pygame.font.Font(filename="Assets\\Fonts\\Adhynatha.otf", size=self.textsize).render(self.text, antialias=True, color=self.textcolor)
        labelrect = btnlabel.get_rect(center=(self.posx+(self.width/2), self.posy+(self.height/2)))
        self.surface.blit(btnlabel, labelrect)

def gameloop():
    scene = None
    while True:
        # Find which scene is active
        for i in Scene.all_scenes:
            if i.active:
                scene = i
        # Apply the active scene's surface to the buttons
        for i in scene.buttons:
            i.surface = scene.surface
        # Set the background color and render buttons
        scene.surface.fill(scene.bgcolor)
        for i in scene.buttons:
            i.btnrender()
        # Update the screen and restart the loop
        pygame.display.flip()
        # Pump the event queue
        for event in pygame.event.get():
            # Game close check
            if event.type == pygame.QUIT:
                sys.exit()
                pygame.quit()
        # Check if a button has been clicked this frame
        for i in scene.buttons:
            i.btnclick(scene)
        pygame.time.Clock().tick(60)

def startscreen():
    startbtn = Button(combatscreen, None, 100, 100, 300, 100, "Start", [30, 30, 30], "white")
    cnfgbtn = Button("settingsmenu", None, settings["resolution"]["x"]-400, 100, 300, 100, "Settings", [30, 30, 30], "white")
# PROBLEM LINE
    startscreen = Scene("startscreen", settings["resolution"]["x"], settings["resolution"]["y"], [100, 100, 100], [startbtn, cnfgbtn]) 
    startscreen.active = True

def combatscreen():
    atkbtn = Button("attack_select", None, 100, 500, 300, 100, "Combat", [100, 100, 100], textcolor="red")
    backbtn = Button(startscreen, None, settings["resolution"]["x"]-100, 0, 100, 50, "Back", textsize=20, textcolor="Black")
# PROBLEM LINE
    combatscreen = Scene("combatscreen", settings["resolution"]["x"], settings["resolution"]["y"], [50, 50, 50], [atkbtn, backbtn])
    combatscreen.active = True

startscreen()
gameloop()

I've tried reordering various events in the gameloop, but because the issue supposedly comes from an instance creation in a separate function, it hasn't changed anything.

4
  • 2
    in for-loops you could use more readable variable instead of i. For example for button in scene.buttons: instead of for i in scene.buttons: Commented Jun 28 at 10:38
  • 1
    often it is useful to have the same names in similar classes - Button.render instead of Button.btnrender (and later OtherObject.render or Player.render and maybe even Screne.render) - because it allows to put different objects on list and later run for-loop to execute render for all object. for item in all_objects: item.render(). Frankly, I would remove prefix btn from all variables in class Button. ie. rect = ... instead of btnrect = ... - to make similar code in different classes. Commented Jun 28 at 10:46
  • 1
    if Button doesn't change font size then you create Font() only once - in __init__ - instead of creating it again and again in every render Commented Jun 28 at 10:51
  • 1
    pygame has special class pygame.Rect() to keep position and size. self.rect = pygameRect(...)` and later you can use it in blit(..., self.rect) and when you check mouse clikc if self.rect.collidepoint(mpos):, and It has tuple self.rect.center instead of tuple (self.posx + self.width/2, self.posy + self.heigh/2) Commented Jun 28 at 10:54

1 Answer 1

3

Problem is that you have .set_mode() in Scene.__init__ which deletes and creates window again and this deletes all elements in window and you get black screen for short time.

Games usually don't need to change window's size and .set_mode() is needed only once - after pygame.init().

You can assign it to variable and send it to Scene as parameter.

class Scene:
    def __init__(self, name, surface, ...):
       self.surface = surface
       # ... code

# ---

pygame.init()
screen = pygame.display.set_mode(size=settings["resolution"]["x"], settings["resolution"]["y"])

# ... code

combatscreen = Scene("combatscreen", screen, ...)
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.