I'm coding some custom GUI objects for usage in pygame menus, while coding a scrollable box I hit an error.
This box works by moving a surface (which contains the components which are moved when scrolling) within a smaller surface which acts like a window to the confined surface. The surfaces mostly display correctly: the contents of the inner surface which are visible initially (the parts which fit within the window surface) display correctly, but when the inner surface is moved to reveal previously hidden components they are not displayed, the initial visible move correctly and are displayed when they return.
I think the issue is with the outer surface's clipping area thinking that only the already revealed components should be displayed and that the others are still hidden but I don't know.
The custom GUI components always have a Rect (returns the bounding rect for that component) and Draw (blits the component to the screen) functions.
Here is the code for the scroll area (and it's parent class):
class ScrollArea(BaseComponent):
"Implements a section of screen which is operable by scroll wheel"
def __init__(self,surface,rect,colour,components):
"""surface is what this is drawn on
rect is location + size
colour is colour of screen
components is iterable of components to scroll through (they need Draw and Rect functions), this changes the objects location and surface
"""
super().__init__(surface)
self.rect = pygame.Rect(rect)
self.colour = colour
self.components = components
self.Make()
def HandleEvent(self, event):
"Pass events to this; it enables the area to react to them"
if event.type == pygame.MOUSEBUTTONDOWN and self.rect.collidepoint(event.pos) and self._scroll_rect.h > self.rect.h:
if event.button == 4: self.scroll_y = min(self.scroll_y + 15,self._scroll_y_min)
if event.button == 5: self.scroll_y = max(self.scroll_y - 15,self._scroll_y_max)
def Make(self):
"Updates the area, activates any changes made"
_pos = self.rect.topleft
self._sub_surface = pygame.Surface(self.rect.size,pygame.SRCALPHA)
self.rect = pygame.Rect(_pos,self._sub_surface.get_rect().size)
self._sub_surface.unlock()#hopefully fixes issues
self._scroll_surf = pygame.Surface(self.rect.size)
self._scroll_rect = self._scroll_surf.get_rect()
scroll_height = 5
for component in self.components:
component.surface = self._scroll_surf
component.Rect().y = scroll_height
component.Rect().x = 5
component.Draw()
scroll_height += component.Rect().h + 5
self._scroll_rect.h = max(self.rect.h,scroll_height)
self.scroll_y = 0
self._scroll_y_min = 0
self._scroll_y_max = -(self._scroll_rect.h - self.rect.h)
def Draw(self):
"Draw the area and its inner components"
self._sub_surface.fill((255, 255, 255, 0))
self._sub_surface.blit(self._scroll_surf,(0,self.scroll_y))
pygame.draw.rect(self._sub_surface,self.colour,((0,0),self.rect.size),2)
self.surface.blit(self._sub_surface,self.rect.topleft)
def Rect(self):
"Return the rect of this component"
return self.rect
class BaseComponent:
def __init__(self,surface):
"surface is what this is drawn on"
self.surface = surface
def HandleEvent(self,event):
"Pass events into this for the component to react ot them"
raise NotImplementedError()
def Make(self):
"Redo calculations on how component looks"
raise NotImplementedError()
def Draw(self):
"Draw component"
raise NotImplementedError()
def ReDraw(self):
"Call Make then draw functions of component"
self.Make()
self.Draw()
def Rect(self):
"Return the rect of this component"
raise NotImplementedError()
To test this I used this code and a label component:
screen_width = 640
screen_height = 480
font_label = pygame.font.Font("freesansbold.ttf",22)
screen = pygame.display.set_mode((screen_width,screen_height))
grey = (125,125,125)
def LoadLoop():
#objects
scroll_components = []
for i in range(20):
scroll_components.append(Components.Label(screen,(0,0),str(i),font_label,grey))
scroll_area = Components.ScrollArea(screen,Components.CenterRect(screen_width/2,3*screen_height/16 + 120,300,200),grey,scroll_components)
clock = pygame.time.Clock()
running = True
while running:
#events
for event in pygame.event.get():
scroll_area.HandleEvent(event)
if event.type == pygame.QUIT:
running = False
pygame.quit()
exit()
#graphics
screen.fill(black)
scroll_area.Draw(components)
#render
pygame.display.update()
clock.tick(60)
This is the label component's code (it basically just prints text to screen with the location given as it's center):
class Label(BaseComponent):
"Class which implements placing text on a screen"
def __init__(self,surface,center,text,font,text_colour):
"""surface is what this is drawn on
center is the coordinates of where the text is to be located
text is the text of the label
font is the font of the label
text_colour is the text's colour
"""
super().__init__(surface)
self.center = center
self.text = text
self.font = font
self.text_colour = text_colour
self.Make()
def HandleEvent(self,event):
"Labels have no events they react to,\nso this does nothing"
def Make(self):
"(Re)creates the label which is drawn,\nthis must be used if any changes to the label are to be carried out"
self._text_surf = self.font.render(self.text, True, self.text_colour)
self._text_rect = self._text_surf.get_rect()
self._text_rect.center = self.center
def Draw(self):
"Draw the label , will not react to any changes made to the label"
self.surface.blit(self._text_surf,self._text_rect)
def Rect(self):
"Return the rect of this component"
return self._text_rect
This is the window produced by this code:
Before scrolling
After scrolling
I also did it with a different size of ScrollArea, one of the Labels was positioned through the bottom and it was cut in half, when scrolled the cut remained.
Please help.
Sidenote on conventions
First, a sidenote on conventions: class names should start with an uppercase letter, function and method names should be all lowercase.
They are conventions, so you are free to not follow them, but following the conventions will make your code more readable to people used to them.
The quick fix
The error is in the ScrollArea.Make() method. Look carefully at these two lines:
self._sub_surface = pygame.Surface(self.rect.size,pygame.SRCALPHA)
self._scroll_surf = pygame.Surface(self.rect.size)
self._sub_surface is the surface of the window of the scroll area. self._scroll_surf is the scrolling surface. The latter should be higher, but you set them to the same size (same width is fine, same height not).
Obviously when you loop over your component list to blit the Label, the ones which are outside self._sub_surface are also outside self._scroll_surf and hence are not blit at all. You should make self._scroll_surf higher. Try for example:
self._scroll_surf = pygame.Surface((self.rect.width, self.rect.height*10)
Better would be to estimate the proper height to contains all your labels, which should be scroll_height, but you calculate it later in the method, so you should figure how to do properly this part.
A general advice
In general, I think you have a design problem here:
for i in range(20):
scroll_components.append(Label(screen,(0,0),str(i),font_label,grey))
scroll_area = ScrollArea(screen, pygame.Rect(screen_width/2,3*screen_height/16 + 120,300,200),grey,scroll_components)
When you create each label, you pass the screen as the reference surface where the Draw method blits.
But these labels should be blitted on the scroll_surf of your ScrollArea. But you cannot do it because you have not instantiated yet the ScrollArea, and you cannot instantiate before the scroll area because you require the Labels to be passed as an argument.
And in fact in the ScrollArea.Make() method you overwrite each label surface attribute with the _scroll_surf Surface.
I think would be better to pass to ScrollArea a list of strings, and let the ScrollArea.__init__() method to create the labels.
It will look less patched and more coherent.
Related
I should point out that I'm a beginner with PyGame. I have made a program that displays some simple graphics on the screen using PyGame. It blits every graphic on a dummy surface and the dummy surface gets scaled and blit to a 'real' surface that gets displayed on the screen in the end. This allows the program to have a resizable window without messing the graphics and UI.
I have also made my own 'Button' class that allows me to draw clickable buttons on the screen. Here it is:
import pygame
pygame.font.init()
dfont = pygame.font.Font('font/mfdfont.ttf', 64)
#button class button(x, y, image, scale, rot, text_in, color, xoff, yoff)
class Button():
def __init__(self, x, y, image, scale = 1, rot = 0, text_in = '', color = 'WHITE', xoff = 0, yoff = 0):
self.xoff = xoff
self.yof = yoff
self.x = x
self.y = y
self.scale = scale
width = image.get_width()
height = image.get_height()
self.image = pygame.transform.rotozoom(image, rot, scale)
self.text_in = text_in
self.text = dfont.render(self.text_in, True, color)
self.text_rect = self.text.get_rect(center=(self.x +width/(2/scale) + xoff, self.y + height/(2/scale) + yoff))
self.rect = self.image.get_rect()
self.rect.topleft = (x, y)
self.clicked = False
def draw(self, surface):
action = False
#get mouse position
pos = pygame.mouse.get_pos()
#check mouseover and clicked conditions
if self.rect.collidepoint(pos):
if pygame.mouse.get_pressed()[0] == 1 and self.clicked == False:
self.clicked = True
action = True
if pygame.mouse.get_pressed()[0] == 0:
self.clicked = False
#draw button on screen
surface.blit(self.image, (self.rect.x, self.rect.y))
surface.blit(self.text, self.text_rect)
return action
When I need to draw one of these buttons on the screen I firstly define it like this:
uparrow = button.Button(128, 1128, arrow_img, 0.5, 0, "SLEW", WHITE, 0, 128)
Then I call it's draw function like this:
if uparrow.draw(screen):
print('UP')
It works reasonably well when drawing it to a surface that doesn't get scaled. This is the problem. When I scale the dummy surface that it gets drawn to, the button's image and text scale just fine but it's collider does not. So when I click on it nothing happens, but if I click on the location of the screen the button would have been on the unscaled dummy surface it works.
Just for context, the dummy surface is 2048x1024 and the 'real' surface is much smaller, starting at 1024x512 and going up and down however the user resizes the window. The game maintains a 2:1 aspect ratio though, so any excess pixels in the game window are black. You can see this in the screenshot below:
Above is a screenshot of the game window. You can see the 'NORM' button at the top of the game screen, and the red box that roughly represents the same 'NORM' button's actual collider. It's basically where it would be on the dummy surface.
(I have previously posted a question on somewhat the same problem as this one, but at that time I didn't know the colliders actually worked and I thought my clicks just didn't register on the buttons, which is not the case).
I'd like to know what part of my button class causes this and how it should be refactored to fix this issue. Alternatively, if you think it's caused by my double surface rendering technique or anything else really, please do point me in the right direction.
In your setup you draw the buttons on an surface, scale the surface and blit that surface on the display. So you do something like the following:
dummy_surface = pygame.Surface((dummy_width, dummy_height)
while True:
# [...]
scaled_surface = pygame.transform.scale(dummy_surface, (scaled_width, scaled_height))
screen.blit(scaled_surface, (offset_x, offset_y))
For click detection to work on the original buttons, you must scale the position of the mouse pointer by the reciprocal scale and shift the mouse position by the inverse offset:
def draw(self, surface):
action = False
# get mouse position
pos = pygame.mouse.get_pos()
scale_x = scaled_width / dummy_surface.get_width()
scale_y = scaled_height / dummy_surface.get_height()
mx = int((pos[0] - offset_x) / scale_x)
my = int((pos[1] - offset_y) / scale_y)
pos = (mx, my)
# [...]
This question already has answers here:
How do I detect collision in pygame?
(5 answers)
Pygame mouse clicking detection
(4 answers)
How to use Pygame touch events in a mobile game?
(1 answer)
Closed 7 days ago.
I'm making a PyGame game and I have made a Button class to make on-screen buttons. This is the code:
import pygame
pygame.font.init()
dfont = pygame.font.Font('mfdfont.ttf', 64)
#button
class Button():
def __init__(self, x, y, image, scale = 1, rot = 0, text_in = '', color = 'WHITE', xoff = 0, yoff = 0):
self.xoff = xoff
self.yof = yoff
self.x = x
self.y = y
self.scale = scale
width = image.get_width()
height = image.get_height()
self.image = pygame.transform.rotozoom(image, rot, scale)
self.text_in = text_in
self.text = dfont.render(self.text_in, True, color)
self.text_rect = self.text.get_rect(center=(self.x +width/(2/scale) + xoff, self.y + height/(2/scale) + yoff))
self.rect = self.image.get_rect()
self.rect.topleft = (x, y)
self.clicked = False
def draw(self, surface):
action = False
#get mouse position
pos = pygame.mouse.get_pos()
#check mouseover and clicked conditions
if self.rect.collidepoint(pos):
if pygame.mouse.get_pressed()[0] == 1 and self.clicked == False:
self.clicked = True
action = True
if pygame.mouse.get_pressed()[0] == 0:
self.clicked = False
#draw button on screen
surface.blit(self.image, (self.rect.x, self.rect.y))
surface.blit(self.text, self.text_rect)
return action
The problem is that I'm using two surfaces to draw the game to the screen. One is a dummy surface at 1024x2048 resolution that I draw everything to, and the second one is a surface that I can resize to any resolution. The dummy surface then gets scaled and blit to the real surface and the real surface is drawn on the screen. This allows me to have a resizable window without messing the screen positions of UI and game elements.
The actual problem is that after implementing this second surface, click and touch input on buttons doesn't work anymore because I'm basically clicking on the real surface and not the dummy surface the buttons are drawn on. I wonder if there is a way to redirect clicks from a certain position on the real surface to clicks to the relative position on the dummy surface. Or maybe have the button class listen for input on the real surface instead of the dummy surface it's drawn on.
I added a "mobile mode", so when the game is running on a mobile device the entire two surface rendering process is not used and instead uses the classic one surface rendering, which makes the buttons work on mobile devices (or devices that don't allow window resizing). This is a temporary fix for the mobile version, but that still leaves the desktop application unusable because the on screen buttons don't work. I must mention that the OSBs are needed, I won't add keyboard controls for those buttons instead. They must be on screen.
I'm trying to make a scroll bar and at the moment, the scroll works by changing the coordinates when blitting (as opposed to changing the actual rect coordinates). This means that rect collisions for buttons do not work when they are moved. I am attempting to combat this by calculating the percentage that the scroll bar has scrolled, converting that to some multiplier or screen coordinate, and then getting the mouse position.
Some notes:
Self.bar is the actual slider handle (the small thing you use to scroll)
Self.rect is the entire slider, and its height is equal to screen height
Self.total_h is the total height that the scroll bar needs to scroll, for example if it needed to scroll to 2x the screen height then total_h would equal screen_height * 2.
Some code I have tried so far:
# Calculate the distance between the top of the handle and the top of the overall bar and divide by the handle height
# (shortened from ((self.bar.rect.top - self.rect.top) / self.rect.h) * (self.rect.h / self.bar.rect.h) which makes more intuitive sense.
self.scroll_percent = ((self.bar.rect.top - self.rect.top) / self.bar.rect.h)
# These all do not work:
# pos_y = pg.mouse.get_pos()[1] * self.scroll_percent
# pos_y = pg.mouse.get_pos()[1] * (self.total_h / self.scroll_percent)
# pos_y = (self.total_h / self.scroll_percent) * pg.mouse.get_pos()[1]
# etc
The logic just doesn't make sense to me, and I've got no idea how to do this. To clarify, my goal is to allow the user to scroll the screen using a scroll bar, and depending on the scroll bar's position, we change the mouse pos accordingly.
I don't really understand why you bother with some percentage ? If I understood correctly you are only scrolling up and down so the only thing you need to know is the y offset, which is 0 when the scroll bar is at the top and then it is just the y value at which you are blitting your surface. So simply remove the y offset to your mouse y when you check for collision.
Maybe I missed something ?
If I understood corretly, here is an simple example of what to do :
(I didn't recreate the scroll bar since you said you've got this part working. I just made the surface go up automatically. I'm sure you will figure out a way to integrate this solution to your own code)
# General import
import pygame as pg
import sys
# Init
pg.init()
# Display
screen = pg.display.set_mode((500, 500))
FPS = 30
clock = pg.time.Clock()
# Surface declaration
drawing_surface = pg.Surface((screen.get_width(), screen.get_height() * 2))
drawing_surface.fill((255,0,0))
drawing_surface_y = 0
# Button
test_btn = pg.Rect(20, 400, 100, 40)
# Main functions
def update():
global drawing_surface_y
drawing_surface_y -= 1
def draw():
# Clear the screen
screen.fill((0,0,0))
# Render the button
pg.draw.rect(drawing_surface, (0,0,255), test_btn)
# Blit the drawing surface
screen.blit(drawing_surface, (0, drawing_surface_y))
def handle_input():
for evt in pg.event.get():
if evt.type == pg.QUIT:
exit()
if evt.type == pg.MOUSEBUTTONDOWN:
if evt.button == 1:
on_click()
def on_click():
mx, my = pg.mouse.get_pos()
if test_btn.collidepoint(mx, my - drawing_surface_y):
print("Test button has been clicked")
def exit():
pg.quit()
sys.exit()
# Other functions
# Main loop
if __name__ == "__main__":
while True:
handle_input()
update()
draw()
pg.display.update()
clock.tick(FPS)
Test this code and let me know if it answers your question !
I seem to have problems with displaying text on the screen
The code draws text on the screen but half 'S' of 'Score' gets cut for reason.
However, if I change screen.blit(text, self.score_rect, self.score_rect) to screen.blit(text, self.score_rect), it works fine. I would like to know why is this happening and how can I fix this.
Thanks.
Here's the code:
class Score(object):
def __init__(self, bg, score=100):
self.score = score
self.score_rect = pygame.Rect((10,0), (200,50))
self.bg = bg
def update(self):
screen = pygame.display.get_surface()
font = pygame.font.Font('data/OpenSans-Light.ttf', 30)
WHITE = (255, 255, 255)
BG = (10, 10, 10)
score = "Score: " + str(self.score)
text = font.render(score, True, WHITE, BG)
text.set_colorkey(BG)
screen.blit(
self.bg,
self.score_rect,
self.score_rect)
screen.blit(text,
self.score_rect,
self.score_rect)
def main():
pygame.init()
#initialize pygame
pygame.init()
screen = pygame.display.set_mode((640, 480))
pygame.display.set_caption('Score Window')
#initialize background
bg = pygame.Surface((screen.get_size())).convert()
bg.fill((30, 30, 30))
screen.blit(bg, (0, 0))
#initialize scoreboard
score_board = Score(bg)
while True:
for event in pygame.event.get():
if event.type == QUIT:
exit(0)
score_board.update()
pygame.display.flip()
Well - it looks like the third parameter on the call do blit, where you repeat the core_rect` parameter is designed exactly to do that: it selects a rectangular area on the
source image (in this case your rendered text) to be pasted in the destination (in this case, the screen).
Text in Pygame is rendered with nice margins, you should not need the source-crop parameter at all - and if you thinbk ou do, you should pass it a suitable set of coordinates, relevant inside the rendered text, not therectangle with the destination coordinates on the screen.
From http://www.pygame.org/docs/ref/surface.html#pygame.Surface.blit:
blit() draw one image onto another blit(source, dest, area=None,
special_flags = 0) -> Rect Draws a source Surface onto this Surface.
The draw can be positioned with the dest argument. Dest can either be
pair of coordinates representing the upper left corner of the source.
A Rect can also be passed as the destination and the topleft corner of
the rectangle will be used as the position for the blit. The size of
the destination rectangle does not effect the blit.
An optional area rectangle can be passed as well. This represents a
smaller portion of the source Surface to draw.
...
I'm in the middle of working on a simple typing tutor using pygame. My problem is that I'm using an image that has a white background, waves1.png. Now's I've specified that I want white to be transparent in the image (self.image.set_colorkey((255, 255, 255))) and it is for everything except the text block. When the waves intersect with the text object, the white background of the waves show on top of the text. You can try running this if you have pygame (with the exception of the waves1.png image).
import pygame
from pygame.locals import *
class TextSprite(pygame.sprite.Sprite):
def __init__(self):
pygame.sprite.Sprite.__init__(self)
self.wordList = ['words yes', 'hello', 'this is a sentence', 'this is another sentence'] # read directly from external file
self.pos = 0
self.wordNum = 0
self.update1()
def update1(self):
# Render the given word
self.image = pygame.font.Font(None, 36).render(self.wordList[self.wordNum], 1, (0, 0, 0))
# Render the correctly guessed letters
self.correct = pygame.font.Font(None, 36).render(self.wordList[self.wordNum][:self.pos], 1, (255, 0, 0))
# Copy correct letters onto given word
self.image.blit(self.correct, (0, 0))
self.rect = self.image.get_rect()
# set the center of the center the given word to the center of the screen
self.rect.center = pygame.display.get_surface().get_rect().center
def keyin(self, key):
word = self.wordList[self.wordNum]
letter = word[self.pos]
if letter == key:
self.pos = self.pos + 1
if self.pos == len(word):
self.reset()
self.update1()
def reset(self):
self.pos = 0
self.wordNum = self.wordNum + 1
self.update1()
class Waves(pygame.sprite.Sprite):
# Constructor. Pass in the color of the block,
# and its x and y position
def __init__(self, filename):
# Call the parent class (Sprite) constructor
pygame.sprite.Sprite.__init__(self)
# Create an image of the block, and fill it with a color.
# This could also be an image loaded from the disk.
self.image = pygame.image.load(filename).convert()
# makes any white in the image transparent
self.image.set_colorkey((255, 255, 255))
self.rect = self.image.get_rect()
# Decrease the y coordinate so the waves look like they're moving up
def update(self, text):
self.rect.y = self.rect.y - 6
if self.rect.y <= 200:
text.reset()
self.rect.y = 485
def main():
#I - Import and initialize
pygame.init()
#D - Display configuration
# The screen variable is a pygame Surface object
# Note that the set_mode() method creates a Surface object for you automatically
screen = pygame.display.set_mode((640, 480))
pygame.display.set_caption("Typing Game")
#E - Entities (just background for now)
background = pygame.Surface(screen.get_size())
background = background.convert()
background.fill((255, 255, 255))
screen.blit(background, (0,0))
#A - Action (broken into ALTER steps)
#A - Assign values to key variables
clock = pygame.time.Clock()
keepGoing = True
# Collect the sprite in a list
all = pygame.sprite.RenderPlain()
waveList = pygame.sprite.RenderPlain()
text = TextSprite()
all.add(text)
waves = Waves("waves1.png")
waveList.add(waves)
waves.rect.x = 0
waves.rect.y = 485
#L - Set up main loop
while keepGoing:
#T - Timer to set frame rate
# Tick is a method in the Clock class that determines the maximum frame rate
clock.tick(30)
#E - Event handling
for event in pygame.event.get():
if event.type == QUIT:
keepGoing = False
elif event.type == KEYDOWN:
if event.key == K_ESCAPE:
keepGoing = False
else:
text.keyin(event.unicode)
# update position of waves
waves.update(text)
# clears screen
all.clear(screen, background)
# update screen
all.draw(screen)
waveList.clear(screen, background)
waveList.draw(screen)
# display.flip is a method that copies everything from the screen object to the actual visual display
pygame.display.flip()
pygame.quit ()
if __name__ == '__main__': main()
I don't know if it's an option for you, but you should get better results with png's native alpha transparency.
If you can edit/recreate the png yourself, then try using a transparent background.
From there, you can use convert_alpha() arfter loading the image. (instead of using a colorkey)
http://pygame.org/docs/ref/surface.html#Surface.convert_alpha
EDIT: one other aspect, is that the image may have an alpha channel interfering with the colorkey. Best to ensure you're not trying to use both.
I'm told that you can detect an image's alpha channel programmatically. Something like ...
if self.image.get_masks()[3]!=0:
print "image has alpha!"
See here http://pygame.org/docs/ref/surface.html#Surface.get_masks
HTH
Well done! You've actually done everything correctly to take advantage of transparency and colorkey (ie, making sure to call convert on the surface, making sure to pass the color into the set_colorkey method, etc).
The problem is with the order of calls to draw and clear on your respective sprite groups, "all" and "waveList". After you've rendered the text blocks by calling all.draw, you then follow it with the call to waveList.clear.
Here's the problem: once you've drawn the text sprites, you don't want to clear the space underneath the wave sprites, or that will wipe out the area that overlaps the already-drawn text blocks.
If you want to do this properly, try doing it in this order:
waves.update()
all.clear(screen,background)
waveList.clear(screen,background)
all.draw(screen)
waveList.draw(screen)
(more simply, just move waveList.clear(screen, background) to the line just below all.clear(screen, background); that should do it)
When I'm working with sprite groups, I usually try to group it so that each sprite group calls the same method in this order: clears, updates, collision checks (if any), draws.
This usually handles things in the right order. Then you still may have to pay attention to whether there is any layering of sprites, but that's another story for another day.