PyGame colliders don't scale with window - python

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)
# [...]

Related

Pygame redirecting touch input to another surface [duplicate]

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.

Surface display able to properly represent opacity, but any other surface cannot

I am trying to make a tic-tac-toe game with pygame. An important thing I want is being able to make my images (eg. X and O) slightly translucent for when my user is only hovering over a grid tile. I also use opacity to visually show whose turn it is.
This is what I have tried:
x_tile = pygame.image.load('x_tile').convert()
x_tile.set_alpha(100)
This works fine when I'm blitting x_tile directly onto the display like this:
# This is for simplicity's sake. The actual blit process is all being done in an infinite loop
screen = pygame.display.set_mode((300, 300))
screen.blit(x_file, x_file.get_rect())
But my game is using another image that represents the grid, and that is what I'm blitting onto. So I'm blitting this board onto the display, then blitting the actual X and O tiles on the board.
screen = pygame.display.set_mode((300, 300))
screen.blit(board, board_rect)
board.blit(x_tile, x_tile.get_rect(center=grid[0].center)) # I have a list of Rects that make a grid on the board image. grid[0] is the top left
When I do it that way, x_tile.set_alpha(100) seems to have no effect and I don't know what to do.
Edit: I am using pygame 2.0.1. I'm on Windows 10.
Here is the entire code
import os
import pygame
from pygame.locals import *
# Game constants
WIN_SIZE = WIN_WIDTH, WIN_HEIGHT = 800, 600
BLACK = 0, 0, 0
WHITE = 255, 255, 255
RED = 255, 0, 0
BLUE = 0, 0, 255
# Game functions
class NoneSound:
"""dummy class for when pygame.mixer did not init
and there is no sound available"""
def play(self): pass
def load_sound(file):
"""loads a sound file, prepares it for play"""
if not pygame.mixer:
return NoneSound()
music_to_load = os.path.join('sounds', file)
try:
sound = pygame.mixer.Sound(music_to_load)
except pygame.error as message:
print('Cannot load following sound:', music_to_load)
raise SystemExit(message)
return sound
def load_image(file, colorkey=None, size=None):
"""loads image into game"""
image_to_load = os.path.join('images', file)
try:
image = pygame.image.load(image_to_load).convert()
except pygame.error as message:
print('Cannot load following image:', image_to_load)
raise SystemExit(message)
if colorkey is not None:
if colorkey == -1:
colorkey = image.get_at((0, 0))
image.set_colorkey(colorkey, RLEACCEL)
if size is not None:
image = pygame.transform.scale(image, size)
return image
# Game class
class TTTVisual:
"""Controls game visuals"""
def __init__(self, win: pygame.Surface):
self.win = win
# Load in game images
self.board = load_image('board.png', size=(600, 450), colorkey=WHITE)
self.x_tile = load_image('X_tile.png', size=(100, 100), colorkey=BLACK)
self.o_tile = load_image('O_tile.png', size=(100, 100), colorkey=BLACK)
# Translucent for disabled looking tile
self.x_tile_trans = self.x_tile.copy()
self.o_tile_trans = self.o_tile.copy()
self.x_tile_trans.set_alpha(100)
self.o_tile_trans.set_alpha(100)
# Used to let user know whose turn it is
self.x_turn = pygame.transform.scale(self.x_tile, (50, 50))
self.o_turn = pygame.transform.scale(self.o_tile, (50, 50))
self.x_turn_trans = pygame.transform.scale(self.x_tile_trans, (50, 50))
self.o_turn_trans = pygame.transform.scale(self.o_tile_trans, (50, 50))
self.get_rects()
self.grid = self.setup_grid()
def get_rects(self):
"""Creates coords for some visual game assets"""
self.board_rect = self.board.get_rect(
center=self.win.get_rect().center)
self.x_turn_rect = self.x_turn.get_rect(top=10, left=10)
self.o_turn_rect = self.o_turn.get_rect(top=10, left=WIN_WIDTH-60)
def setup_grid(self):
grid = []
left = 0
top = 150
row = 0
for i in range(9):
if (i != 0) and (i % 3 == 0):
row += 1
left = 0
grid.append(pygame.Rect(left, row*top, 200, 150))
left += 200
return grid
def update_turn_status(self):
"""Updates the X and O tiles on the top left and right to
let user know whose turn it is"""
self.win.blits((
(self.x_turn_trans, self.x_turn_rect),
(self.o_turn, self.o_turn_rect)
))
def update_grid(self):
"""Updates board"""
self.win.blit(self.board, self.board_rect)
# Here is where you could change board to win and see that the tile changes in opacity
self.board.blit(self.x_tile_trans, self.x_tile_trans.get_rect(center=self.grid[0].center))
def update(self):
self.win.fill(WHITE)
self.update_turn_status()
self.update_grid()
pygame.display.flip()
def main():
pygame.init()
win = pygame.display.set_mode(WIN_SIZE)
tttvisual = TTTVisual(win)
tttfunc = TTTFunc(tttvisual)
clock = pygame.time.Clock()
running = True
while running:
clock.tick(60)
for event in pygame.event.get():
if event.type == QUIT:
running = False
tttvisual.update()
pygame.quit()
if __name__ == "__main__":
main()
The issue is caused by the line:
self.board.blit(self.x_tile_trans, self.x_tile_trans.get_rect(center=self.grid[0].center))
You don't blit the image on the display Surface, but on the self.board Surface. When a Surface is blit, it is blended with the target. When you draw on a Surface, it changes permanently. Since you do that over and over again, in every frame, the source Surface appears to by opaque. When you decrease the alpha value (e.g. self.x_tile_trans.set_alpha(5)), a fade in effect will appear.
Never draw on an image Surface. Always draw on the display Surface. Cleat the display at begin of a frame. Draw the entire scene in each frame and update the display once at the end of the frame.
class TTTVisual:
# [...]
def update_grid(self):
"""Updates board"""
self.win.blit(self.board, self.board_rect)
# Here is where you could change board to win and see that the tile changes in opacity
x, y = self.grid[0].center
x += self.board_rect.x
y += self.board_rect.y
self.win.blit(self.x_tile_trans, self.x_tile_trans.get_rect(center=(x, y)))
The typical PyGame application loop has to:
handle the events by either pygame.event.pump() or pygame.event.get().
update the game states and positions of objects dependent on the input events and time (respectively frames)
clear the entire display or draw the background
draw the entire scene (blit all the objects)
update the display by either pygame.display.update() or pygame.display.flip()

Moving surface within smaller surface doesn't show previously hidden components

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.

Pygame surface location seems to be different than what it is on the screen?

I am trying to make a virtual phone kind of program with pygame, just to experiment with it, but i ran into a problem. I have loaded an image and then blitted it to the bottom left of the screen. But when i do print(imagename.get_rect()) it prints out a location at 0, 0 of the screen.
also the mouse collides with it there. what am i not understanding?
def aloitus(): #goes back to the home screen
cls() #clears the screen
tausta = pygame.image.load("./pyhelin.png") #load background
tausta = pygame.transform.scale(tausta, (360, 640)) #scale it
screen.blit(tausta, (0, 0)) #blit it
alapalkki = pygame.Surface((600, 100)) #ignore
alapalkki.set_alpha(120)
alapalkki.fill(blonk)
screen.blit(alapalkki, (0, 560))
global messenger #this is the thing!
messenger = pygame.image.load("./mese.png").convert_alpha() #load image
print(messenger.get_rect()) #print its location
messenger = pygame.transform.scale(messenger, (60,65)) #scale it to the correct size
screen.blit(messenger, (10, 570)) # blit on the screen
update() #update screen
aloitus() # at the start of the program, go to the home screen
while loop: #main loop
for event in pygame.event.get():
if event.type == pygame.MOUSEBUTTONDOWN:
# Set the x, y postions of the mouse click
x, y = event.pos
if messenger.get_rect().collidepoint(x, y): #messenger is the image defined earlier
#do things
print("hi")
Expected result would be, when clicking on the image "hi" would be printed.
actual result is that when topleft corner is clicked hi is printed.
get_rect returns a pygame.Rect with the default top left coordinates (0, 0). You need to set the coordinates afterwards or pass them as a keyword argument to get_rect.
I suggest assigning the rect to another variable and set the coords in one of these ways:
messenger_rect = messenger.get_rect()
messenger_rect.x = 100
messenger_rect.y = 200
# or
messenger_rect.topleft = (100, 200)
# or pass the coords as an argument to `get_rect`
messenger_rect = messenger.get_rect(topleft=(100, 200))
There are even more rect attributes to which you can assign the coordinates:
x,y
top, left, bottom, right
topleft, bottomleft, topright, bottomright
midtop, midleft, midbottom, midright
center, centerx, centery

text being cut in pygame

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.
...

Categories

Resources