Related
I need to create a base rectangle, and the blitted rectangle (default_rect and rect) for both scaling and scrolling purposes. I'm trying to change the y value of the RECT individually, but it somehow changes the value of default_rect also? I have no idea why. Perhaps the issue is coming from somewhere else. I have tried to create a minimum reproducible example to show what I'm talking about.
class Element(pg.sprite.Sprite):
def __init__(self, img, x, y, placement="center"):
super().__init__()
self.image = img
if placement == "topright":
self.rect = self.image.get_rect(topright=(x, y))
if placement == "center":
self.rect = self.image.get_rect(center=(x, y))
self.default_rect = self.rect
print("Test")
def update_rect(self, y_offset):
self.rect.centery = self.default_rect.centery + y_offset
When calling this update_rect() function, I see no reason why the value of self.default_rect.centery should be affected at all. This is the only place I reference default_rect.centery. I do not think that the issue is coming from the __init__ running multiple times because "Test" is only printed when initalising.
Rest of the minimum reproducible example. The rectangle continuously increases it's y position to -1,000,000 in seconds (without fps limit).
import pygame as pg
pg.init()
screen_width, screen_height = 800, 600
screen = pg.display.set_mode((screen_width, screen_height))
grey = (150, 150, 150)
dark_grey = (60, 60, 60)
class Element(pg.sprite.Sprite):
def __init__(self, img, x, y, placement="center"):
super().__init__()
self.image = img
if placement == "topright":
self.rect = self.image.get_rect(topright=(x, y))
if placement == "center":
self.rect = self.image.get_rect(center=(x, y))
self.default_rect = self.rect
print("Test")
def update_rect(self, y_offset):
self.rect.centery = self.default_rect.centery + y_offset
class ScrollBar(Element):
def __init__(self, x, y, total_bar_w, total_bar_h, total_h):
img = pg.Surface((total_bar_w, total_bar_h))
img.fill(dark_grey)
super().__init__(img, x, y, "topright")
bar_w = 0.98*total_bar_w
bar_h = (total_bar_h / total_h) * total_bar_h
bar_img = pg.Surface((bar_w, bar_h))
bar_img.fill(grey)
self.bar = Element(bar_img, x, y, "topright")
self.total_h = total_h
self.ratio = self.total_h / self.rect.h
self.offset = 0
self.y_offset = 0
self.pressed = False
self.active = False
def update(self):
pos = pg.mouse.get_pos()
click = pg.mouse.get_pressed()[0]
# Check if the slider bar was pressed
if click and self.rect.collidepoint(pos):
self.active = True
# Checking if mouse was released or mouse left the allowed zone
if not click or abs(pos[0] - self.rect.midleft[0]) > self.rect.w*3:
self.active = False
if self.active:
self.bar.rect.centery = pos[1]
if self.bar.rect.top < 0:
self.bar.rect.top = 0
if self.bar.rect.bottom > self.rect.top + self.rect.h:
self.bar.rect.bottom = self.rect.top + self.rect.h
# Calculate y offset for elements. Multiply by negative
# to allow us to add the offset rather than subtract
self.y_offset = (self.bar.rect.centery - self.bar.rect.h / 2) * -self.ratio
def draw(self):
screen.blit(self.image, (self.rect.x, self.rect.y))
screen.blit(self.bar.image, (self.bar.rect.x, self.bar.rect.y))
class Button(Element):
def __init__(self, x, y, w, h):
super().__init__(pg.Surface((w, h)), x, y, "center")
self.clicked = False
def update(self):
self.update_rect(scroll_bar.y_offset)
action = False
if self.rect.collidepoint(pg.mouse.get_pos()):
if pg.mouse.get_pressed()[0] and not self.clicked:
self.clicked = True
action = True
if not pg.mouse.get_pressed()[0]:
self.clicked = False
return action
def draw(self):
pg.draw.rect(screen, "blue", self.rect)
button1 = Button(400, 100, 200, 150)
button2 = Button(400, 300, 200, 150)
button3 = Button(400, 500, 200, 150)
buttons = [button1, button2, button3]
total_height = screen_height * 2
scroll_bar = ScrollBar(screen_width, 0, 0.05 * screen_width, screen_height, total_height)
run = True
while run:
screen.fill("white")
for button in buttons:
button.draw()
if button.update():
print("Button's y value is: " + str(button.rect.y))
scroll_bar.update()
scroll_bar.draw()
for e in pg.event.get():
if e.type == pg.QUIT:
run = False
if e.type == pg.KEYDOWN:
if e.key == pg.K_SPACE:
print(f"{button1.rect.y} / {button2.rect.y} / {button3.rect.y}")
if e.key == pg.K_r:
print(scroll_bar.y_offset)
pg.display.update()
When you assign self.default_rect = self.rect in __init__() both variables are references to the same object. So later in update_rect() when you change self.rect.centery, the change is reflected in self.default_rect because they both refer to the same object.
To understand how this works, check out the excelent talk by Ned Batchelder about names and values.
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.
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()
I wrote this physics simulation in pygame and the collision mechanism is not working properly. It seems to work when I collide the player character with the wall from above the wall or from the left and not work for collisions from the bottom or from the right
I have been trying to find this bug for some time but I just have no clue as to what might cause this. I am using python 3.7.3 and pygame 1.9.5 (latest versions as of date)
I am sorry for pasting an entire file but I just have no Idea where the problem is
import pygame # import the pygame library to have access to game building tools
import math
# these variables will be used to hold game objects and draw them
rigid_bodies = []
g = 100
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
PURPLE = (127, 0, 255)
RED = (200, 0, 0)
GREEN = (0, 200, 0)
BLUE = (0, 0, 200)
class RigidBody(pygame.Rect):
"""
represents a rectangular object that acts according to newton's laws of motion
"""
def __init__(self, canvas, color, m, u, x, y, w, h):
"""
called automatically when a new object is created to initialize the object
:param canvas: the canvas on which to draw the object
:param color: the color of the object
:param m: the mass of the object
:param u: Coefficient of friction
:param x: the starting position of the object on the x axis
:param y: the starting position of the object on the y axis
:param w: the width of the object
:param h: the height of the object
"""
super().__init__(x, y, w, h) # initialize the parent Rect object
self.canvas = canvas
self.color = color
self.m = m
self.u = u
self.x_speed = 0 # the speed of the object on the x axis
self.y_speed = 0 # the speed of the object on the y axis
def apply_force(self, axis, F, initiator=None):
"""
used to apply force on the object
:param axis: the axis of the force
:param F: the amount of force to apply
:param initiator: the object that is applying the force
"""
a = F / self.m # calculate the acceleration the object should have
if axis == 'y':
self.y_speed += a
elif axis == 'x':
self.x_speed += a
if initiator:
initiator.apply_force(axis, -1 * F) # apply normal force
print('colliding')
def inertia(self):
"""
shall be run every frame to make the object move according to its speed
if possible and take the necessary steps if not
"""
# go:
self.x += self.x_speed
self.y += self.y_speed
for body in rigid_bodies:
if self.colliderect(body): # if collide with another object:
self.x -= self.x_speed # go back
self.y -= self.y_speed
body.apply_force('x', self.m * self.x_speed, self) # and apply force on that object
body.apply_force('y', self.m * self.y_speed, self)
break
def draw(self):
"""
shall be run every frame to draw the object on the canvas
"""
pygame.draw.rect(self.canvas, self.color, (self.x, self.y, self.w, self.h))
class Controller:
def __init__(self, character, F):
"""
initialize the controller object
:param character: the character to control
:param F: the force to apply to the character for every frame a button is being pressed
"""
self.character = character
self.up = 0 # whether to move up or not
self.down = 0 # whether to move down or not
self.left = 0 # whether to move left or not
self.right = 0 # whether to move right or not
self.F = F
def stop(self):
"""
stops applying force on the object
"""
self.up = 0
self.down = 0
self.left = 0
self.right = 0
def run(self):
"""
shall be run every frame to apply force on the character according to user input
"""
self.character.apply_force('y', -self.F * self.up)
self.character.apply_force('y', self.F * self.down)
self.character.apply_force('x', -self.F * self.left)
self.character.apply_force('x', self.F * self.right)
def main():
"""
the main function contains the main loop
that runs repeatedly while the game is running
"""
crashed = False # tells if the program crashed or if the window was closed
pygame.init() # required to use pygame
canvas = pygame.display.set_mode((1000, 700)) # define the canvas
clock = pygame.time.Clock() # will be used to limit the number of times a loop runs per second
pygame.display.set_caption('the dot game V2')
character = RigidBody(canvas, WHITE, 1000, 0.3, 500, 500, 20, 50) # initialize the character
player = Controller(character, 500) # initialize the controller
rigid_bodies.append(RigidBody(canvas, WHITE, math.inf, 0, 300, 300, 300, 20)) # initialize the wall
while not crashed:
# handle inputs:
for event in pygame.event.get():
if event.type == pygame.QUIT:
crashed = True
elif event.type == pygame.MOUSEBUTTONUP:
pass
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_UP:
player.up = 1
elif event.key == pygame.K_DOWN:
player.down = 1
elif event.key == pygame.K_LEFT:
player.left = 1
elif event.key == pygame.K_RIGHT:
player.right = 1
elif event.type == pygame.KEYUP:
player.stop()
player.run()
character.inertia()
canvas.fill(BLACK)
character.draw()
for body in rigid_bodies:
body.draw()
pygame.display.update()
clock.tick(60)
if __name__ == '__main__':
main()
I suspect that the problem is with either the "inertia" or the "apply_force" functions but I just cant figure out WHAT is the problem with those functions
The character should stop moving every time it hits the wall, but when it hits the wall from below or from the right it gets stuck and can only move up or to the left
The issue is caused by casting a floating point value to int and can be solved by:
stored_pos = (self.x, self.y)
self.x += self.x_speed
self.y += self.y_speed
for body in rigid_bodies:
if self.colliderect(body): # if collide with another object:
self.x, self.y = stored_pos
Note, that
self.x -= self.x_speed
self.y -= self.y_speed
is not the inverse operation of
self.x += self.x_speed
self.y += self.y_speed
e.g: a = 2 and b = 0.5
int(a + b) == int(2 + 0.5) == 2
int(a - b) == int(2 - 0.5) == 1
The solution is to store the original values of self.x and self.y
stored_pos = (self.x, self.y)
and to restore it in the case of a collision:
self.x, self.y = stored_pos
I have no clue how I would implement a game menu into the game I've made, I was thinking about having buttons for instructions credits and a 'play game' button. So would someone mind helping me out in figuring how to make a simple menu in pygame or livewires? Thanks in advance :)
This is the complete code to my game:
# Asteroid Dodger
# Player must avoid asteroids
# make the score a global variable rather than tied to the asteroid.
import pygame
from livewires import games, color
import math, random
#score
games.init(screen_width = 640, screen_height = 480, fps = 50)
score = games.Text(value = 0, size = 25, color = color.green,
top = 5, right = games.screen.width - 10)
games.screen.add(score)
#lives
lives = games.Text(value = 3, size = 25, color = color.green,
top = 5, left = games.screen.width - 620)
games.screen.add(lives)
#inventory
inventory=[]
#Asteroid images
images = [games.load_image("asteroid_small.bmp"),
games.load_image("asteroid_med.bmp"),
games.load_image("asteroid_big.bmp")]
class Ship(games.Sprite):
"""
A Ship controlled by player that explodes when it by Asteroids.
"""
image = games.load_image("player.bmp")
VELOCITY_STEP = .05
def __init__(self):
""" Initialize Ship object """
super(Ship, self).__init__(image = Ship.image,
bottom = games.screen.height)
def update(self):
global inventory
""" uses A and D keys to move the ship """
if games.keyboard.is_pressed(games.K_a):
self.dx -= Ship.VELOCITY_STEP * 2
if games.keyboard.is_pressed(games.K_d):
self.dx += Ship.VELOCITY_STEP * 2
if self.left < 0:
self.left = 0
if self.right > games.screen.width:
self.right = games.screen.width
self.check_collison()
def ship_destroy(self):
self.destroy()
new_explosion = Explosion(x = self.x, y = self.y)
games.screen.add(new_explosion)
def check_collison(self):
""" Check for overlapping sprites in the ship. """
global lives
for items in self.overlapping_sprites:
items.handle_caught()
if lives.value <=0:
self.ship_destroy()
class Explosion(games.Animation):
sound = games.load_sound("explosion.wav")
images = ["explosion1.bmp",
"explosion2.bmp",
"explosion3.bmp",
"explosion4.bmp",
"explosion5.bmp",
"explosion6.bmp",
"explosion7.bmp",
"explosion8.bmp",
"explosion9.bmp"]
def __init__(self, x, y):
super(Explosion, self).__init__(images = Explosion.images,
x = x, y = y,
repeat_interval = 4, n_repeats = 1,
is_collideable = False)
Explosion.sound.play()
class Asteroid(games.Sprite):
global lives
global score
global inventory
"""
A asteroid which falls through space.
"""
image = games.load_image("asteroid_med.bmp")
speed = 3
def __init__(self, x,image, y = 10):
""" Initialize a asteroid object. """
super(Asteroid, self).__init__(image = image,
x = x, y = y,
dy = Asteroid.speed)
def update(self):
""" Check if bottom edge has reached screen bottom. """
if self.bottom>games.screen.height:
self.destroy()
score.value+=10
def handle_caught(self):
if lives.value>0:
lives.value-=1
self.destroy_asteroid()
if lives.value <= 0:
self.destroy_asteroid()
self.end_game()
def destroy_asteroid(self):
self.destroy()
def die(self):
self.destroy()
def end_game(self):
""" End the game. """
end_message = games.Message(value = "Game Over",
size = 90,
color = color.red,
x = games.screen.width/2,
y = games.screen.height/2,
lifetime = 5 * games.screen.fps,
after_death = games.screen.quit)
games.screen.add(end_message)
class Spawner(games.Sprite):
global images
"""
Spawns the asteroids
"""
image = games.load_image("spawner.bmp")
def __init__(self, y = 10, speed = 5, odds_change = 50):
super(Spawner, self).__init__(image = Spawner.image,
x = games.screen.width / 2,
y = y,
dx = speed)
self.odds_change = odds_change
self.time_til_drop = 0
def update(self):
""" Determine if direction needs to be reversed. """
if self.left < 0 or self.right > games.screen.width:
self.dx = -self.dx
elif random.randrange(self.odds_change) == 0:
self.dx = -self.dx
self.check_drop()
self.check_for_lives()
def check_drop(self):
""" Decrease countdown or drop asteroid and reset countdown. """
if self.time_til_drop > 0:
self.time_til_drop -= 0.7
else:
asteroid_size = random.choice(images)
new_asteroid = Asteroid(x = self.x,image = asteroid_size)
games.screen.add(new_asteroid)
# makes it so the asteroid spawns slightly below the spawner
self.time_til_drop = int(new_asteroid.height * 1.3 / Asteroid.speed) + 1
def check_for_lives(self):
droplives = random.randrange(0, 4000)
if droplives == 5:
lifetoken = Extralives(x = self.x)
g ames.screen.add(lifetoken)
class Extralives(games.Sprite):
global lives
image = games.load_image('addlives.png')
speed = 2
sound = games.load_sound("collectlives.wav")
def __init__(self,x,y = 10):
""" Initialize a asteroid object. """
super(Extralives, self).__init__(image = Extralives.image,
x = x, y = y,
dy = Extralives.speed)
def update(self):
""" Check if bottom edge has reached screen bottom. """
if self.bottom>games.screen.height:
self.destroy()
def handle_caught(self):
Extralives.sound.play()
lives.value+=1
self.destroy()
def main():
""" Play the game. """
bg = games.load_image("space.jpg", transparent = False)
games.screen.background = bg
the_spawner = Spawner()
games.screen.add(the_spawner)
pygame.mixer.music.load("Jumpshot.ogg")
pygame.mixer.music.play()
the_ship = Ship()
games.screen.add(the_ship)
games.mouse.is_visible = False
games.screen.event_grab = True
games.screen.mainloop()
#starts the game
main()
The Pygbutton module provides a way to create buttons in Pygame programs. You can download it via "pip install pygbutton". There are demos on the github: https://github.com/asweigart/pygbutton
Try this code out:
Game Menu function
def game_intro():
intro = True
while intro:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
quit()
gameDisplay.fill(white)
titleText = gameDisplay.blit(title, (170, 200)) # title is an image
titleText.center = ((display_width / 2), (display_height / 2))
# button(x, y, w, h, inactive, active, action=None)
button(100, 350, 195, 80, startBtn, startBtn_hover, game_loop)
button(300, 350, 195, 80, creditsBtn, creditsBtn_hover, #Your function)
pygame.display.update()
clock.tick(15)
you can call this menu above your game loop.
Button function
Pygame doesn't have buttons but it is pretty easy to make one!
def button(x, y, w, h, inactive, active, action=None):
mouse = pygame.mouse.get_pos()
click = pygame.mouse.get_pressed()
if x + w > mouse[0] > x and y + h > mouse[1] > y:
gameDisplay.blit(active, (x, y))
if click[0] == 1 and action is not None:
action()
else:
gameDisplay.blit(inactive, (x, y))
you can call this function inside your game menu like this:
#Example function call
button(340, 560, 400, 200, randomBtn, randomBtn_hover, random_func)
Here's what each parameter means in button():
x: x-coordinate of button
y: y-coordinate of button
w: button width(in pixels)
h: button height(in pixels)
active: the picture of the button when it is active(e.g when the mouse is hovering ver it)
inactive: the picture of the button when it is idle
action: the function to be executed when the button is pressed
Note: It is better to make a button function since it is easier to make one and it saves a lot of time
Hope this helped!