Collision detection between pygame.Surface and mouse not working [duplicate] - python

This question already has answers here:
Why is my collision test always returning 'true' and why is the position of the rectangle of the image always wrong (0, 0)?
(1 answer)
How to detect collisions between two rectangular objects or images in pygame
(1 answer)
Closed 2 years ago.
I am trying to make a canvas for pixel art.
class Canvas:
def __init__(self):
self.__blocks = []
self.__positions = []
for i in range(1830):
self.__blocks.append(pygame.Surface((20, 20)).convert())
for y in range(30):
y *= 20
for x in range(61):
x = x* 20
self.__positions.append([x, y])
self.__color = False
def draw(self, window):
for i in range(1830):
self.__color = not self.__color
if self.__color:
self.__blocks[i].fill((200, 200, 200))
else:
self.__blocks[i].fill((50, 50, 50))
window.blit(self.__blocks[i], (self.__positions[i][0]
, self.__positions[i][1]))
Here I am trying to generate and draw 1830 unique surfaces and this works. I then tried implementing collision detection between each block and the mouse and failed.
def collided(self, pos):
for i in range(1380):
block = self.__blocks[i].get_rect()
if block.collidepoint(pos[0], pos[1]):
print(block.x, block.y)
Then I did different tests on why it might be failing. Here is one of them. I will change a single block's color, in our case the 10th block self.__blocks[10].fill((255, 0, 0)) to red so we know which box to click on. Then we will try to check for collision for that particular block.
def testBlock(self, pos):
block = self.__blocks[10].get_rect()
if block.collidepoint(pos[0], pos[1]):
print(block.x)
And it doesn't work, but the weird thing is it works for the first block(in the 0th index) and only the first block no matter which surface I test. Any idea on how to fix this would be appreciated. The following is copy and paste code.
import pygame
pygame.init()
win = pygame.display
D = win.set_mode((1220, 600))
class Canvas:
def __init__(self):
self.__blocks = []
self.__positions = []
for i in range(1830):
self.__blocks.append(pygame.Surface((20, 20)).convert())
for y in range(30):
y *= 20
for x in range(61):
x = x* 20
self.__positions.append([x, y])
self.__color = False
self.testBlock = 10
def draw(self, window):
for i in range(1830):
self.__color = not self.__color
if self.__color:
self.__blocks[i].fill((200, 200, 200))
else:
self.__blocks[i].fill((50, 50, 50))
self.__blocks[self.testBlock].fill((255, 0, 0)) # Changing the color for testing
window.blit(self.__blocks[i], (self.__positions[i][0]
, self.__positions[i][1]))
def test(self, pos):
block = self.__blocks[self.testBlock].get_rect()
if block.collidepoint(pos[0], pos[1]):
print(block.x, block.y)
canvas = Canvas()
while True:
D.fill((0, 0, 0))
pygame.event.get()
mousepos = pygame.mouse.get_pos()
canvas.draw(D)
canvas.test(mousepos)
win.flip()

When you call .get_rect() on a Surface, it does not know its current position, because that is not Surface information. So you need to assign the location to the Rect before collision detection.
With your current code layout, you could do this during the construction. With the Canvass blocks position now held in the __rects list, the __positions list becomes superfluous.
class Canvass:
def __init__(self):
self.__blocks = []
self.__rects = []
for y in range( 30 ):
for x in range( 61 ):
self.__blocks.append(pygame.Surface((20, 20)).convert())
self.__rects.append( self.__blocks[-1].get_rect() )
self.__rects[-1].topleft = ( x, y )
self.__color = False
self.testBlock = 10
This gives you a simple test:
def collided(self, pos):
hit = False
for i in range( len( self.__rects ) ):
if ( self.__rects[i].collidepoint( pos[0], pos[1] ) ):
print( "Click on block %d" % ( i ) )
hit = True
break
return hit, i

.get_rect() gives rect with block's size but with position (0, 0)
you have real position in __positions and you would need
.get_rect(topleft=self.__positions[self.testBlock])
def test(self, pos):
block = self.__blocks[self.testBlock].get_rect(topleft=self.__positions[self.testBlock])
if block.collidepoint(pos[0], pos[1]):
print(block.x, block.y)
But it would be better to get rect and set its position at start and later not use get_rect().
You could also create class Pixel similar to class Sprite with self.image to keep surface and self.rect to keep its size and position. And then you could use Group to check collision with all pixels.
EDIT:
Example which uses class pygame.sprite.Sprite to create class Pixel and it keeps all pixels in pygame.sprite.Group
It also handle events (MOUSEBUTTONDOWN) to change color in any pixel when it is clicked.
import pygame
# --- classes ---
class Pixel(pygame.sprite.Sprite):
def __init__(self, x, y, color, width=20, height=20):
super().__init__()
self.color_original = color
self.color = color
self.image = pygame.Surface((20, 20)).convert()
self.image.fill(self.color)
self.rect = pygame.Rect(x, y, width, height)
def handle_event(self, event):
if event.type == pygame.MOUSEBUTTONDOWN:
if self.rect.collidepoint(event.pos):
if self.color != self.color_original:
self.color = self.color_original
else:
self.color = (255,0,0)
self.image.fill(self.color)
# event handled
return True
# event not handled
return False
class Canvas:
def __init__(self):
# create group for sprites
self.__blocks = pygame.sprite.Group()
# create sprites
self.__color = False
for y in range(30):
y *= 20
for x in range(61):
x *= 20
self.__color = not self.__color
if self.__color:
color = (200, 200, 200)
else:
color = (50, 50, 50)
self.__blocks.add(Pixel(x, y, color))
# changing the color for testing
self.testBlock = 10
all_sprites = self.__blocks.sprites()
block = all_sprites[self.testBlock]
block.image.fill((255, 0, 0))
def draw(self, window):
# draw all sprites in group
self.__blocks.draw(window)
def test(self, pos):
# test collision with one sprite
all_sprites = self.__blocks.sprites()
block = all_sprites[self.testBlock]
if block.rect.collidepoint(pos):
print(block.rect.x, block.rect.y)
def handle_event(self, event):
for item in self.__blocks:
if item.handle_event(event):
# don't check other pixels if event already handled
return True
# --- main ---
pygame.init()
win = pygame.display
D = win.set_mode((1220, 600))
canvas = Canvas()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
exit()
canvas.handle_event(event)
#mousepos = pygame.mouse.get_pos()
#canvas.test(mousepos)
# draws (without updates, etc)
#D.fill((0, 0, 0)) # no need clean screen if it will draw all elements again
canvas.draw(D)
win.flip()

Related

Issue with overlapping Rects using Pygame

I am new to Pygame and I am in the experimentation phase.
What I have here is a simple display surface onto which I have rendered a few overlapping Rectangles with different colors with the same width and height (96 by 144) to simulate playing cards.
I have written some code in order to be able to highlight a card with the color Yellow when I click on it. If you run the code, you'll notice that when I click on the visible part of one of the cards, all the other cards that are behind it, as well as all of those that are overlapping it are being highlighted. I cannot figure out, what should I do to click the visible part of the card and only that card to highlight.
import pygame, sys
class Card(pygame.sprite.Sprite):
def __init__(self, color):
super().__init__()
self.color = color
self.is_highlight = False
def show_card(self, x, y):
self.rect = pygame.rect.Rect(x, y, 96, 144)
pygame.draw.rect(display_surf, self.color, self.rect)
def highlight_card(self):
pos = pygame.mouse.get_pos()
if self.rect.collidepoint(pos):
self.is_highlight = True
else:
self.is_highlight = False
def update(self):
if self.is_highlight:
pygame.draw.rect(display_surf, 'Yellow', self.rect.inflate(5, 5), width=3, border_radius=5)
class Deck(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.card_deck = []
def build_deck(self):
self.card_deck.append(Card('Green'))
self.card_deck.append(Card('Red'))
self.card_deck.append(Card('Blue'))
self.card_deck.append(Card('brown'))
self.card_deck.append(Card('cadetblue'))
self.card_deck.append(Card('cornsilk'))
self.card_deck.append(Card('darkolivegreen'))
self.card_deck.append(Card('darkorchid'))
self.card_deck.append(Card('darksalmon'))
return self.card_deck
pygame.init()
WINDOW_WIDTH = 1280
WINDOW_HEIGHT = 720
display_surf = pygame.display.set_mode((WINDOW_WIDTH,WINDOW_HEIGHT))
pygame.display.set_caption('Simple Card Game')
clock = pygame.time.Clock()
card_deck = Deck().build_deck()
while True:
display_surf.fill((0, 150,0))
distance = 20
for card in card_deck:
card.show_card(distance, 20)
distance += 20
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
if event.type == pygame.MOUSEBUTTONDOWN:
for card in card_deck:
card.highlight_card()
for card in card_deck:
card.update()
pygame.display.update()
So I think I have solved my own question and wish to share the solution.
In the show_card method I have added a new scaled_rect which represents only the visible part of the colored card with a width of only 20, while I draw on the display surface the color using the original full sized rectangle:
def show_card(self, x, y):
self.rect = pygame.rect.Rect(x, y, 96, 144)
self.scaled_rect = pygame.rect.Rect((x, y), (20, 144))
pygame.draw.rect(display_surf, self.color, self.rect)
In the highlight_card method I have made only the visible part of the color interactable with the mouse click, by using the new scaled_rect:
def highlight_card(self):
pos = pygame.mouse.get_pos()
if self.scaled_rect.collidepoint(pos):
self.is_highlight = True
else:
self.is_highlight = False
In the end, in the update method that draws the yellow colored highlight, I use the original full sized rectangle:
def update(self):
if self.is_highlight:
pygame.draw.rect(display_surf, 'Yellow', self.rect.inflate(5, 5), width=3, border_radius=5)

Simple Box game not Moving python [duplicate]

This question already has answers here:
How can I make a sprite move when key is held down
(6 answers)
Closed 1 year ago.
Hi am new fairly new to programming so I tried to learn by making my hands dirty. I tried making a simple box game program all was working fine until I made my player Class which inherit from the base class of Box_User for some reasons my box(player class object) is no longer moving I tried to see what it prints and it seems like none of the keys work when I press them
Can anyone explain what happened?
import pygame
pygame.init()
# Classes
class Window():
def __init__(self, width, height):
self.width = width
self.height = height
self.window_init() # this functions is to get the window up
def window_init(self):
self.window = pygame.display.set_mode((self.width, self.height)) # this is the main window
self.background = pygame.Surface((self.window.get_size())) # this one is named background but should be use like the main window
self.background.fill((255, 255, 255))
#staticmethod
def draw_objects_to_Screen():
win.window.blit(win.background, (0,0))
win.window.blit(box.box, (box.pos_x - scroll[0], box.pos_y + scroll[1])) # the scroll is used to make it look like its moving
win.window.blit(box2.box, (box2.pos_x - scroll[0], box2.pos_y + scroll[1]))
pygame.display.update()
class Box_User():
jump = 10
jump_status = False
def __init__(self, x, y, height, width):
self.pos_x = x
self.pos_y = y
self.box_height = height
self.box_width = width
self.box = pygame.Surface((self.box_height, self.box_width))
self.color = (0, 20, 0)
self.draw_box()
def draw_box(self):
pygame.draw.rect(self.box, self.color, pygame.Rect(self.pos_x, self.pos_y, self.box_height, self.box_width))
#staticmethod
def _jump():
if Box_User.jump >= -10:
box.pos_y -= (Box_User.jump * abs(Box_User.jump)) * 0.3
scroll[1] += (Box_User.jump * abs(Box_User.jump)) * 0.3
Box_User.jump -= 1
else:
Box_User.jump = 10
Box_User.jump_status = False
class Player(Box_User):
key_pressed = pygame.key.get_pressed()
def __init__(self, x, y, height, width):
super().__init__(x, y, height, width)
# self.pos_x = x
#self.pos_y = y
def movements(self):
if self.key_pressed[pygame.K_a]:
self.pos_x -= 5
if self.key_pressed[pygame.K_d]:
self.pos_x += 5
# place here things that you dont want to move while the box is jumping
if not self.jump_status:
if self.key_pressed[pygame.K_w]:
self.jump_status = True
else:
self._jump() # the box jumps here
class Auto_Box(Box_User):
def __init__(self, x, y, height, width):
super().__init__(x, y, height, width)
pass
# Variables
# window
win = Window(700, 500)
clock = pygame.time.Clock()
FPS = 60
# boxes
box = Player(30, 200, 64, 64)
box2 = Box_User(300, 200, 100, 120)
# The Scroll which controls the things when the box is moving
scroll = [0, 0]
# Functions
# Main Loop
def main():
run = True
while run:
clock.tick(FPS)
# value of the scroll is updated here
scroll[0] += (box.pos_x - scroll[0]-250)
#print("coordinate are")
#print(scroll)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
box.movements()
print(box.pos_x, box.pos_y)
Window.draw_objects_to_Screen()
if __name__ == "__main__":
main()
pygame.key.get_pressed() returns a list of booleans that represent the state of the keyboard when you call the function.
If you continually check that list, no keys will ever change state. It's just a list of Trues and Falses.
You need to reset the values of key_pressed in the loop, updating it with fresh values from pygame.key.get_pressed().
This should do the trick:
def movements(self):
self.key_pressed = pygame.key.get_pressed()
if self.key_pressed[pygame.K_a]:
self.pos_x -= 5
...
The key_pressed class variables is only initialized once. It is never changed. Therefore the pressed keys are not detected.
pygame.key.get_pressed() returns a iterable with the current state of all keyboard buttons. You must get the states of the keys in every frame:
class Player(Box_User):
def __init__(self, x, y, height, width):
super().__init__(x, y, height, width)
def movements(self):
# get the current states of the keys
self.key_pressed = pygame.key.get_pressed()
if self.key_pressed[pygame.K_a]:
self.pos_x -= 5
if self.key_pressed[pygame.K_d]:
self.pos_x += 5
# place here things that you dont want to move while the box is jumping
if not self.jump_status:
if self.key_pressed[pygame.K_w]:
self.jump_status = True
else:
self._jump() # the box jumps here

Pygame Scroll Bar To Play Volume Low OR High.?

so I have a scroll bar in my game what Im trying to do is make it so if my mouse is over the bar1 button and we are on the moving_spot of the bar1 button then we can move it up and down on its y axis
how can I move the bar up and down and if its colliding with with any of the volume buttons I can change the volume of my background music either 0.1 or 0.2 or 0.3 so it controls my game volumepygame.mixer.music.set_volume(0.3)
my problem is im not sure how I could get this started I have everything in place but not sure where to start ***how can I move the bar with my mouse on the moving_spot on its y values only and if the bar1 is over and of the volume1 2 or 3 4 buttons then it should play the volume at defferent level
im not sure how to approach this problem any help is appreciated I just need a way to adjust my music of my game if the player moves the bar up or down
while run:
# Making game run with fps
clock.tick(fps)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
if event.type == pygame.MOUSEBUTTONDOWN:
pos = pygame.mouse.get_pos()
# this is our bar1 the gray part that we will be able to move
if bar1.isOver(pos):
bar1.y = pos
print("WORKING{")
here are my buttons and positions places the move_spot is where the bar1 can only move up and down
the bar1 is the bar that the player can control to control the volume
and also the volume 1 2 3 4 are where the defferent volume of our background music will be set
move_spot = button((colors),720,125,25,260, '')
bar1 = button((colors),715,125,30,60, '')
volume1 = button((colors2),715,145,30,60, '')
volume2 = button((colors2),715,210,30,60, '')
volume3 = button((colors2),715,280,30,60, '')
volume4 = button((colors2),715,350,30,60, '')
buttons = [bar1,move_spot,volume1,volume2,volume3,volume4]
this is my buttons class
# our buttons
class button():
def __init__(self, color, x,y,width,height, text=''):
self.color = color
self.x = x
self.y = y
self.width = width
self.height = height
self.text = text
self.over = False
def draw(self,window,outline=None):
#Call this method to draw the button on the screen
if outline:
pygame.draw.rect(window, outline, (self.x-2,self.y-2,self.width+4,self.height+4),0)
pygame.draw.rect(window, self.color, (self.x,self.y,self.width,self.height),0)
if self.text != '':
font = pygame.font.SysFont('image/abya.ttf', 60)
text = font.render(self.text, 1, (255,255,255))
window.blit(text, (self.x + (self.width/2 - text.get_width()/2), self.y + (self.height/2 - text.get_height()/2)))
def isOver(self, pos):
#Pos is the mouse position or a tuple of (x,y) coordinates
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
def playSoundIfMouseIsOver(self, pos, sound):
if self.isOver(pos):
if not self.over:
click.play()
self.over = True
else:
self.over = False
here a minimal code you can run and test with this bar image
heres the background music music
import pygame
pygame.init()
window = pygame.display.set_mode((800,800))
# our buttons
class button():
def __init__(self, color, x,y,width,height, text=''):
self.color = color
self.x = x
self.y = y
self.width = width
self.height = height
self.text = text
self.over = False
def draw(self,window,outline=None):
#Call this method to draw the button on the screen
if outline:
pygame.draw.rect(window, outline, (self.x-2,self.y-2,self.width+4,self.height+4),0)
pygame.draw.rect(window, self.color, (self.x,self.y,self.width,self.height),0)
if self.text != '':
font = pygame.font.SysFont('freesansbold.ttf', 60)
text = font.render(self.text, 1, (255,255,255))
window.blit(text, (self.x + (self.width/2 - text.get_width()/2), self.y + (self.height/2 - text.get_height()/2)))
def isOver(self, pos):
#Pos is the mouse position or a tuple of (x,y) coordinates
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
def playSoundIfMouseIsOver(self, pos, sound):
if self.isOver(pos):
if not self.over:
click.play()
self.over = True
else:
self.over = False
colors = 0,23,56
colors2 = 0,123,56
bar11 = pygame.image.load("bar.png").convert_alpha()
move_spot = button((colors),720,125,25,260, '')
bar1 = button((colors),715,125,30,60, '')
volume1 = button((colors2),715,145,30,60, '')
volume2 = button((colors2),715,210,30,60, '')
volume3 = button((colors2),715,280,30,60, '')
volume4 = button((colors2),715,350,30,60, '')
buttons = [bar1,move_spot,volume1,volume2,volume3,volume4]
# fos
fps = 60
clock = pygame.time.Clock()
# redraw
def redraw():
window.fill((40,100,200))
for button in buttons:
button.draw(window)
window.blit(bar11,(bar1.x,bar1.y))
# main loop
run = True
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
redraw()
pygame.display.update()
pygame.quit()
As a general rule of thumb, you want to delegate all the heavy lifting to classes, and leave the "good" stuff to the mainloop of your program. I have created a simple class here, which takes some inputs (width, height, number of slider options), and will take care of all the drawing, positioning, etc. for you. It also has an option of self.chosen, which tells you which option is picked. I then used this to set the volume outputted by the mixer, based on which option is chosen, in the update() function:
class VolumeBar(pygame.sprite.Sprite):
def __init__(self, options, width, height):
# Store these useful variables to the class
self.options = options
self.width = width
self.height = height
# The slider
self.slider = pygame.image.load('slider.png')
self.slider_rect = self.slider.get_rect()
# The background "green" rectangles, mostly for decoration
self.back = []
objectHeight = (height-options*6)/(options-1)
self.backHeight = objectHeight
for index in range(options-1):
self.back.append(pygame.Rect((0, rint((6*index+6)+index*objectHeight)), (width, rint(objectHeight))))
# The foreground "blue" rectangles, mostly for decoration
self.fore = []
for index in range(options):
self.fore.append(pygame.Rect((0, rint(index*(objectHeight+6))), (width, 6)))
# Get list of possible "snaps", which the slider can be dragged to
self.snaps = []
for index in range(options):
self.snaps.append((width/2, 3+(index*(objectHeight+6))))
# A simple variable to tell you which option has been chosen
self.chosen = 0
# Generate the image surface, then render the bar to it
self.image = pygame.Surface((width, height))
self.rect = self.image.get_rect()
self.render()
self.focus = False
def render(self):
self.image.fill([255, 255, 255])
for back in self.back:
pygame.draw.rect(self.image, [0, 192, 0], back)
for fore in self.fore:
pygame.draw.rect(self.image, [0, 0, 0], fore)
self.image.blit(self.slider, (rint((self.width-self.slider_rect.width)/2),
rint(self.snaps[self.chosen][1]-(self.slider_rect.height/2))))
def draw(self, surface):
surface.blit(self.image, self.rect.topleft)
def mouseDown(self, pos):
if self.rect.collidepoint(pos):
self.focus = True
return True
return False
def mouseUp(self, pos):
if not self.focus:
return
self.focus = False
if not self.rect.collidepoint(pos):
return
pos = list(pos)
# We've made sure the mouse started in our widget (self.focus), and ended in our widget (collidepoint)
# So there is no reason to care about the exact position of the mouse, only where it is relative
# to this widget
pos[0] -= self.rect.x
pos[1] -= self.rect.y
# Calculating max distance from a given selection, then comparing that to mouse pos
dis = self.backHeight/2 + 3
for index, snap in enumerate(self.snaps):
if abs(snap[1]-pos[1]) <= dis:
self.chosen = index
break
self.render()
def update(self):
pygame.mixer.music.set_volume((self.options-self.chosen)*0.1)
Most of the __init__ function is spent calculating positions for each of the green and black rectangles, which are drawn in render() and displayed in draw(). The other functions are there for the mouse input, the first checks if the mouseDown happened on said button, and if it did, it sets self.focus to True, so that the mouseUp handler knows that it should change the slider position.
All of this works together to make a working VolumeBar. Below is an example of how it would be used in a mainloop:
import pygame
pygame.init()
rint = lambda x: int(round(x, 0)) # Rounds to the nearest integer. Very useful.
class VolumeBar(pygame.sprite.Sprite):
def __init__(self, options, width, height):
# Store these useful variables to the class
self.options = options
self.width = width
self.height = height
# The slider
self.slider = pygame.image.load('slider.png')
self.slider_rect = self.slider.get_rect()
# The background "green" rectangles, mostly for decoration
self.back = []
objectHeight = (height-options*6)/(options-1)
self.backHeight = objectHeight
for index in range(options-1):
self.back.append(pygame.Rect((0, rint((6*index+6)+index*objectHeight)), (width, rint(objectHeight))))
# The foreground "blue" rectangles, mostly for decoration
self.fore = []
for index in range(options):
self.fore.append(pygame.Rect((0, rint(index*(objectHeight+6))), (width, 6)))
# Get list of possible "snaps", which the slider can be dragged to
self.snaps = []
for index in range(options):
self.snaps.append((width/2, 3+(index*(objectHeight+6))))
# A simple variable to tell you which option has been chosen
self.chosen = 0
# Generate the image surface, then render the bar to it
self.image = pygame.Surface((width, height))
self.rect = self.image.get_rect()
self.render()
self.focus = False
def render(self):
self.image.fill([255, 255, 255])
for back in self.back:
pygame.draw.rect(self.image, [0, 192, 0], back)
for fore in self.fore:
pygame.draw.rect(self.image, [0, 0, 0], fore)
self.image.blit(self.slider, (rint((self.width-self.slider_rect.width)/2),
rint(self.snaps[self.chosen][1]-(self.slider_rect.height/2))))
def draw(self, surface):
surface.blit(self.image, self.rect.topleft)
def mouseDown(self, pos):
if self.rect.collidepoint(pos):
self.focus = True
return True
return False
def mouseUp(self, pos):
if not self.focus:
return
self.focus = False
if not self.rect.collidepoint(pos):
return
pos = list(pos)
# We've made sure the mouse started in our widget (self.focus), and ended in our widget (collidepoint)
# So there is no reason to care about the exact position of the mouse, only where it is relative
# to this widget
pos[0] -= self.rect.x
pos[1] -= self.rect.y
# Calculating max distance from a given selection, then comparing that to mouse pos
dis = self.backHeight/2 + 3
for index, snap in enumerate(self.snaps):
if abs(snap[1]-pos[1]) <= dis:
self.chosen = index
break
self.render()
def update(self):
pygame.mixer.music.set_volume((self.options-self.chosen)*0.1)
screen = pygame.display.set_mode([500, 500])
test = VolumeBar(10, 30, 300)
test.rect.x = 50
test.rect.y = 50
clock = pygame.time.Clock()
game = True
while game:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
game = False
if event.type == pygame.MOUSEBUTTONDOWN:
test.mouseDown(pygame.mouse.get_pos())
if event.type == pygame.MOUSEBUTTONUP:
test.mouseUp(pygame.mouse.get_pos())
if not game:
break
screen.fill([255, 255, 255])
test.update()
test.draw(screen)
pygame.display.update()
clock.tick(60)
The final product:
https://i.gyazo.com/f6c2b5ede828f7715e5fd53a65c32c13.mp4
As long as your mouseDown happened on this widget, mouseUp will determine where the slider ends up. Thusly, you can click, drag, or tap the slider anywhere on it, and the slider will go to the correct position.
Accessing the position of the slider is quite simple, just look at self.chosen. It defaults to 0 (Because it was set to that in the __init__) function, which is at the very top.

Object oriented circular button in pygame?

I want to make a circular button in pygame.
I had made a class for the rectangle button. (below)
But I want to make it as a circle (and also other polygon).
I know there is no get_cir in Surface and it even doesn't work with it.
So how could I make it?
import pygame
class Button():
def __init__(self, text, x, y, width, height, normal_color, hovered_color,
label_color, font_type, font_size, command = None):
self.text = text
self.normal_color = normal_color
self.hovered_color = hovered_color
self.label_color = label_color
self.font_type = font_type
self.font_size = font_size
self.command = command
self.image_normal = pygame.Surface((width, height))
self.image_normal.fill(self.normal_color)
self.image_hovered = pygame.Surface((width, height))
self.image_hovered.fill(self.hovered_color)
self.image = self.image_normal
self.rect = self.image.get_rect()
font = pygame.font.SysFont(self.font_type, self.font_size)
text_image = font.render(text, True, self.label_color)
text_rect = text_image.get_rect(center = self.rect.center)
self.image_normal.blit(text_image, text_rect)
self.image_hovered.blit(text_image, text_rect)
self.rect.topleft = (x, y)
self.hovered = False
def update(self):
if self.hovered:
self.image = self.image_hovered
else:
self.image = self.image_normal
def draw(self, surface):
surface.blit(self.image, self.rect)
def handle_event(self, event):
if event.type == pygame.MOUSEMOTION:
self.hovered = self.rect.collidepoint(event.pos)
elif event.type == pygame.MOUSEBUTTONDOWN:
if self.hovered == True:
self.command()
You can keep the (fast and efficient) rectangular collision detection, and when that indicates a click, then check the event's (x,y) position distance from the centre-point of the circle.
The distance formula gives us a function:
def lineLength( pos1, pos2 ):
"""Return the distance between the points (pos1) and (pos2)"""
x1,y1 = pos1
x2,y2 = pos2
# length = sqrt((x2 - x1)^2 + (y2 - y1)^2)
x_squared = ( x2 - x1 ) * ( x2 - x1 )
y_squared = ( y2 - y1 ) * ( y2 - y1 )
length = math.sqrt( x_squared + y_squared )
return length
So then add an extra check to work out the distance between the centre of your button, and the mouse-click-point. This should be less than the button's radius for the hover/click to be on the button:
def handle_event( self, event ):
if event.type == pygame.MOUSEMOTION:
self.hovered = False
if ( self.rect.collidepoint( event.pos ) ):
# check click is over the circular button ~
radius = self.width / 2
click_distance = lineLength( self.rect.center, event.pos )
if ( click_distance <= radius ):
self.hovered = True
There is no problem to draw circle or use image with circle. The only problem is to check mouse collision with circle object.
There is sprite.collide_circle which uses circle areas to check collision between two sprites so you would need to create sprite with mouse position and check collision with button's sprite.
OR you can use circle definition
x**2 + y**2 =< R
so calculate distance between mouse and circle's center and it has to be equal or smaller then radius.
You can use even pygame.math.Vector2.distance_to() for this
For other figures the only idea is to use sprite.collide_mask which uses black&white bitmaps to check collisions between sprites. But it would need to create bitmap with filled figure and it can make problem if you want to draw polygon instead of use bitmap with polygon.

How not to render ball orientation in pymunk.Space.debug_draw()?

I am using Pymunk to generate videos of balls bouncing in a frame. To render the frames, I run:
import pygame
from pymunk.pygame_util import DrawOptions
screen = pygame.display.set_mode((500, 500))
draw_options = DrawOptions(screen)
# define `space` with six balls
# ...
space.debug_draw(draw_options)
which gives the following output:
As we can see, it draws a black line going from the center to the edge of the ball; I suppose it is to indicate the rotation of each ball.
I would like NOT to have this line drawn: i.e. explicitly pass a flag to prevent pygame from drawing it, or specify the color of this line to be the same as the rest of the ball.
Would anyone know how to do that?
The debug_draw method should really be used mainly for debugging. I usually create sprite subclasses for my physics objects and give them a Pymunk body and a shape as attributes. Then I can update the self.rect in the update method by setting the coordinates to the coords of the body and also use it to rotate the image.
import math
import random
import pygame as pg
import pymunk as pm
from pymunk import Vec2d
def flipy(p):
"""Convert chipmunk physics to pygame coordinates."""
return Vec2d(p[0], -p[1]+800)
class Entity(pg.sprite.Sprite):
def __init__(self, pos, space, radius, mass=1):
super().__init__()
# The surface is a bit bigger, so that the circle fits better.
self.orig_image = pg.Surface((radius*2+2, radius*2+2), pg.SRCALPHA)
self.image = self.orig_image
# Draw a circle onto the image.
pg.draw.circle(
self.image,
pg.Color(random.randrange(256),
random.randrange(256),
random.randrange(256)),
(radius+1, radius+1), # +1 looks a bit better.
radius)
self.rect = self.image.get_rect(topleft=pos)
# Create a Pymunk body and a shape and add them to the space.
moment = pm.moment_for_circle(mass, radius, radius)
self.body = pm.Body(mass, moment)
self.shape = pm.Circle(self.body, radius)
self.shape.friction = .1
self.shape.elasticity = .99
self.body.position = pos
self.space = space
self.space.add(self.body, self.shape)
def update(self):
# Update the rect because it's used to blit the image.
self.rect.center = flipy(self.body.position)
# Use the body's angle to rotate the image.
self.image = pg.transform.rotozoom(self.orig_image, math.degrees(self.body.angle), 1)
self.rect = self.image.get_rect(center=self.rect.center)
if self.rect.left < 0 or self.rect.right > 1280 or self.rect.y > 790:
self.space.remove(self.body, self.shape)
self.kill()
class Game:
def __init__(self):
pg.init()
self.screen = pg.display.set_mode((1280, 800))
self.done = False
self.clock = pg.time.Clock()
# Pymunk stuff
self.space = pm.Space()
self.space.gravity = Vec2d(0.0, -900.0)
self.space.damping = .9
self.static_lines = [
pm.Segment(self.space.static_body, flipy((60.0, 780.0)), flipy((650.0, 780.0)), .0),
pm.Segment(self.space.static_body, flipy((650.0, 780.0)), flipy((1218.0, 660.0)), .0)
]
for lin in self.static_lines:
lin.friction = 0.2
lin.elasticity = 0.99
self.space.add(self.static_lines)
self.all_sprites = pg.sprite.Group()
def run(self):
while not self.done:
self.dt = self.clock.tick(60) / 1000
self.handle_events()
self.run_logic()
self.draw()
self.current_fps = self.clock.get_fps()
pg.quit()
def handle_events(self):
for event in pg.event.get():
if event.type == pg.QUIT:
self.done = True
elif event.type == pg.MOUSEBUTTONDOWN:
if event.button == 1: # Left mouse button.
# Spawn an entity.
radius = random.randrange(20, 50)
self.all_sprites.add(Entity(flipy(pg.mouse.get_pos()), self.space, radius))
if pg.mouse.get_pressed()[2]: # Right mouse button.
radius = random.randrange(20, 50)
self.all_sprites.add(Entity(flipy(pg.mouse.get_pos()), self.space, radius))
def run_logic(self):
self.space.step(1/60)
self.all_sprites.update()
def draw(self):
self.screen.fill(pg.Color(140, 120, 110))
self.all_sprites.draw(self.screen) # Draw the images of all sprites.
# Draw the static lines.
for line in self.static_lines:
body = line.body
p1 = flipy(body.position + line.a.rotated(body.angle))
p2 = flipy(body.position + line.b.rotated(body.angle))
pg.draw.lines(self.screen, pg.Color('lightgray'), False, (p1, p2), 5)
pg.display.flip()
if __name__ == '__main__':
Game().run()
I also tried to find a way to hide these direction/rotation lines that debug_draw creates and found out that the lines are colored in the draw_options.shape_outline_color and the circles use the draw_options.shape_dynamic_color by default. So, when you set
draw_options.shape_outline_color = draw_options.shape_dynamic_color
the circles are completely blue. However, if you assign a specific color to each shape, it will still use the blue color for the lines.
And with the draw_options.flags attribute, I could only turn off the collision points or the shapes completely but not the lines:
# This is how you can turn off the collision points.
draw_options.flags ^= draw_options.DRAW_COLLISION_POINTS
# Stops drawing the constraints.
draw_options.flags ^= draw_options.DRAW_CONSTRAINTS
# Stops drawing all shapes.
draw_options.flags ^= draw_options.DRAW_SHAPES

Categories

Resources