How would I prevent two masks from overlapping each other when a collision is detected? I know how to detect mask collisions but I can't wrap my head around actually preventing them from colliding. I'm pretty sure the solution has to do something with mask.overlap_area, but when I try using the code provided, It doesn't seem to work at all:
example gif (the blue dot is [dx, dy] )
import pygame
import sprites
SCREEN_HEIGHT, SCREEN_WIDTH = 800, 800
running = True
pygame.init()
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
clock = pygame.time.Clock()
player = sprites.Block((100, 100))
block2 = sprites.Block((100, 100))
blocks = pygame.sprite.Group(block2)
block2.rect.topleft = 150, 150
while running:
events = pygame.event.get()
screen.fill((100, 100, 100))
for event in events:
if event.type == pygame.QUIT:
running = False
player.move(screen.get_rect())
screen.blit(player.image, player.rect)
for block in blocks:
offset = (player.rect.x - block.rect.x, player.rect.y - block.rect.y)
dx = player.mask.overlap_area(block.mask, (offset[0] + 1, offset[1])) - \
player.mask.overlap_area(block.mask, (offset[0] - 1, offset[1]))
dy = player.mask.overlap_area(block.mask, (offset[0], offset[1] + 1)) - \
player.mask.overlap_area(block.mask, (offset[0], offset[1] - 1))
screen.blit(block.image, block.rect)
print(dx, dy)
pygame.draw.circle(screen, (0, 0, 255), (dx + block.rect.x, dy + block.rect.y), 5)
clock.tick(144)
pygame.display.flip()
Do I just have the wrong idea?
I think the issue is that your program is allowing the overlap in the first place. Once they're colliding you can't do anything.
Before moving the object, check that the destination location is not already occupied by doing a "future collision" check. If there's going to be a collision, then either don't allow the movement at all, or handle it in some nicer way.
If you know the direction of movement - say the player pushed ←, and is moving left. The code can easily move the player as far left as possible, to the point just before colliding.
This way you never have to deal with objects atop each other.
It's not really clear to me what approach the program is taking. The API pygame.mask.overlap_area() returns the number of bits overlapping. The code is calculating the collision normal, not trying to prevent or undo the overlap. Maybe it can move each object by the inverse of this direction, or suchlike.
Related
The following code draws a small white disc orbiting the center of the screen. There is noticeable tearing on my machine (macOS Monterey) and 6 or 7 frames are dropped on average on each revolution. Is there any way to avoid that? I've tried adding flags such as vsync, fullscreen, scaled etc, nothing works. The opengl flag doesn't seem to work on macos.
import pygame
import sys
import math
pygame.init()
screen = pygame.display.set_mode((1920,1080))
clock = pygame.time.Clock()
while True:
for e in pygame.event.get():
if e.type == pygame.QUIT:
sys.exit()
t = pygame.time.get_ticks()
screen.fill('Black')
pygame.draw.circle(screen, 'White', (
960 + 300 * math.cos(t / 800),
540 + 300 * math.sin(t / 800)),
20)
pygame.display.update()
clock.tick(60)
To be clear: IMHO the frames are not dropping because of heavy or background CPU usage. A modified version of this code runs approximately 1000 such circles all over the screen pretty smoothly, with only the regular hiccup here and there (same as above). I only start to get real slowdowns when I go over 5000 circles.
Firstly - check that there is not some CPU-heavy task running on your machine. For the moment I would just write your program, and then worry about if/when it's dropping frames later. If it's still an issue at completion, then optimise it.
One way to speed this up is to pass the rectangle for changed screen area to the pygame.display.update( dirty_rectangle ) function. That way only the part of the screen that has been "dirtied" by changes will be updated. This means that typically a smaller part of the screen will be re-drawn.
I also modified the code to draw the circle to an off-screen surface, and just blit() that image to the screen, rather than re-calculate the circle each loop. That makes it easy to work out the corners of the changed area too.
For your example, it's possible to calculate this rectangle based on the position of the circle, and where it has been previously. So before repositioning the circle to the new co-ordinate, we make a copy of circle_rect into previous_rect. The new position is calculated, the drawing position updated. The maximum extent of the update is then the minimum top-left corner of both rectangles, down to the maximum of the bottom-right corners. The min & max is used to populate the update rectangle.
import pygame
import sys
import math
pygame.init()
screen = pygame.display.set_mode((1920,1080))
clock = pygame.time.Clock()
# Create a sprite for the circle
CIRCLE_RAD=20
circle = pygame.Surface( (CIRCLE_RAD*2, CIRCLE_RAD*2), pygame.SRCALPHA, 32 )
circle.fill( (0,0,0) )
pygame.draw.circle( circle, (255,255,255), (CIRCLE_RAD,CIRCLE_RAD), CIRCLE_RAD )
circle_rect = circle.get_rect()
update_rect = circle_rect.copy()
while True:
for e in pygame.event.get():
if e.type == pygame.QUIT:
sys.exit()
previous_rect = circle_rect.copy()
t = pygame.time.get_ticks()
t800 = t/800
circle_rect.center = ( 960 + 300 * math.cos(t800), 540 + 300 * math.sin(t800) )
# work out the size of the update
min_x = min( previous_rect.x, circle_rect.x )
min_y = min( previous_rect.y, circle_rect.y )
max_x = max( previous_rect.x + previous_rect.width, circle_rect.x + circle_rect.width )
max_y = max( previous_rect.y + previous_rect.height, circle_rect.y + circle_rect.height )
update_rect.update( min( previous_rect.x, circle_rect.x ),
min( previous_rect.y, circle_rect.y ),
max_x-min_x,
max_y-min_y )
screen.fill('Black')
screen.blit( circle, circle_rect )
pygame.display.update( update_rect )
ms_since_previous = clock.tick(60)
if ( ms_since_previous > 17 ): # 60FPS is about 17ms between frames
print( "Slow update: %d milliseconds" % ( ms_since_previous ) )
Fiddling around with pygame and I'm not sure what's happening.
code is just supposed to make a red box and bounce it around a gray screen. It works, but only when I quit the display.
I've checked out questions that are similar but none seem to have an answer that applies to me (might be wrong about that). Does anyone know how this code could be improved?
import pygame
from pygame.locals import *
from rect import *
##from pygame.font import *
RED = (255, 0, 0)
GRAY = (150, 150, 150)
width = 500
height = 200
pygame.init()
screen = pygame.display.set_mode((width, height))
rect = Rect(100, 50, 50, 50)
v = [2, 2]
##moving = False
running = True
while running:
for event in pygame.event.get():
if event.type == QUIT:
running = False
rect.move_ip(v)
if rect.left < 0 or rect.right > width:
v[0] *= -1
if rect.top < 0 or rect.bottom > height:
v[1] *= -1
screen.fill(GRAY)
pygame.draw.rect(screen, RED, rect)
pygame.display.flip()
pygame.quit()
It seems that the problem is the program running too fast. Since so little work is being done in each loop, the rectangle ends up moving around too quickly to clearly see what is happening.
You can restrict the speed of the program using a pygame.time.Clock object. Before the start of your loop, probably right after your screen definition, you can construct a clock.
clock = pygame.time.Clock()
Then in your main loop, as the very first step each iteration (right after while running:) you can put clock.tick(60) to restrict the loop to running 60 times per second (60fps). This makes the program run smoothly, and you get the nice bouncing rectangle!
The tick method works well, but on my system it seems to have small hitches every so often. If you also experience this or if you want to be more accurate, you can use clock.tick_busy_loop(60) instead.
Both of these tick methods work the same way: By measuring the amount of time that passed since the last call to clock.tick() and delaying a certain additional amount based on that so that the target fps will be met.
More info on Clock at Pygame docs.
I also needed to import pygame.rect instead of just import rect, but if you're not getting an import error you should be fine.
I've figured it out. Stupidly I had another file in my test directory named "rect.py", I changed the file name and my code to from pygame.rect import * and it's working fine now. Thank you Baked Potato for the help and for making me wonder where x=50, y=60, w=200, h=80 left=50, top=60, right=250, bottom=140 center=(150, 100) was coming from!
I am creating a multiplayer game with splitted screen.
I start by drawing the first player on the left-hand side (spaceship, fire bombs, stars in the background (scrolling at half speed) and finally the background), then I update the first part of the screen, for the first player. Then I do the same things for the second player, on the other part of the screen.
But most of the images overlap thoughout the two half-screens. (see image below)
So, basically I need to update one part of the screen using pygame.display.update(), then the other part.
But the command doesn’t work, and updates the entire screen. And everything overlaps.
I've tried the following:
pygame.display.update(Rect((pos, 0), size))
pygame.display.update(Rect((pos, 0, size[0], size[1])))
pygame.display.update((pos, 0, size[0], size[1]))
pygame.display.update(pos, 0, size[0], size[1])
pygame.display.update((pos, 0), size)
But all of these are doing exactly the same thing, and they don't work as expected.
When you are using pygame.display.update() there are two kinds of an optional argument, single rect (which defaults to None) and a list of rects. If no argument is passed, it updates the entire surface area - like display.flip() does.
update(rectangle=None) -> None
update(rectangle_list) -> None
To update only specific elements, either create a list of these elements if you want to update the same group at the same time
background_rects = [star_rect, star_rect, star_rect, some_other_rect]
foreground_rects = [player_rect, enemy1_rect, enemy2_rect, bullet1_rect, bullet2_rect]
pygame.display.update(background_rects)
pygame.display.update(foreground_rects)
or call update(rect) multiple times with the individual elements:
pygame.display.update(star1_rect)
pygame.display.update(star2_rect)
pygame.display.update(star3_rect)
pygame.display.update(character_rect)
pygame.display.update(enemy_rect)
Link to the documentation: https://www.pygame.org/docs/ref/display.html#pygame.display.update
There seems to be some (probably unintended as there is nothing about it in the docs) difference between the handling of pygame 1.9.6 and the 2.0.0.dev branches - below is a MRE which works with 1.9.6, but not with the 2.0.0.dev10 version. In 1.9.6 the difference in updating the display is easily visible. I suggest you install the stable 1.9.6 version if you need this exact functionality!
In case others want to try their luck, here is the MRE with which I tested:
import pygame
import time
screen = pygame.display.set_mode((720, 480))
rect = pygame.Rect((10, 50), (32, 32))
image = pygame.Surface((32, 32))
image.fill((0,100,0))
rect2 = pygame.Rect((10, 10), (32, 32))
image2 = pygame.Surface((32, 32))
image2.fill((100,100,0))
i = 0
while True:
i += 1
for event in pygame.event.get():
if event.type == pygame.QUIT:
quit()
screen.blit(image, rect)
screen.blit(image2, rect2)
rect.x += 1 # we update the position every cycle
rect2.x += 1 # we update the position every cycle
# but update the rect on screen at different times:
if i < 10:
pygame.display.update() # both
elif i > 50 and i < 75:
pygame.display.update(rect2) # only rect2
elif i >= 100:
pygame.display.update(rect) # only rect
time.sleep(0.1)
I am learning python using pygame and I am working on something that involves sprites and collisions. I've looked at some examples but I still don't quite understand it. What I am attempting to do is to be able to add sprites(a ball) when the user presses the "=" key and also be able to remove the last sprite added when pressing "-". I am not able to remove just the last one, I have only been able to remove all of them.
So far I have been able to add the balls to the window and have them bounce off the walls and one another(sort of). When 2 balls collide, they don't completely touch yet they bounce off. Sometimes the balls get stuck and won't move and sometimes the balls bounce off the frame which they aren't suppose to.
Its my first time working with sprite groups and would appreciate any help/guidance into making this work smoothly.Thanks.
The code:
ball.py
import pygame
from pygame.locals import *
class Ball(pygame.sprite.Sprite):
def __init__(self, x, y, vx, vy):
super().__init__();
self.image = pygame.image.load("ball.png").convert()
self.image.set_colorkey(pygame.Color(0, 0, 0))
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
self.vx = vx
self.vy = vy
def draw(self, SCREEN):
SCREEN.blit(self.image, (self.rect.x, self.rect.y))
def move(self, SCREEN, balls):
l_collide = self.rect.x + self.image.get_width() + self.vx > SCREEN.get_width()
r_collide = self.rect.x + self.vx < 0
t_collide = self.rect.y + self.vy < 0
b_collide = self.rect.y + self.image.get_height() + self.vy > SCREEN.get_height()
a = pygame.sprite.spritecollide(self, balls, False, False)
if len(a) > 1:
self.vx *= -1
self.vy *= -1
if l_collide or r_collide:
self.vx *= -1
if t_collide or b_collide:
self.vy *= -1
self.rect.x += self.vx
self.rect.y += self.vy
ball_animation.py
import pygame
import sys
import random
import math
from pygame.locals import *
from ball.ball import Ball
from random import randint
def ball_list(num):
ball_list = pygame.sprite.Group()
for x in range(num):
rand_x = random.randint(0,400)
rand_y = random.randint(0,400)
vx = 4
vy = 5
ball_list.add(Ball(rand_x, rand_y, vx, vy))
return ball_list
def main():
pygame.init()
FPS = 30
FPS_CLOCK = pygame.time.Clock()
# COLOR LIST
BLACK = pygame.Color(0, 0, 0)
# Code to create the initial window
window_size = (500, 500)
SCREEN = pygame.display.set_mode(window_size)
# set the title of the window
pygame.display.set_caption("Bouncing Ball Animation")
# change the initial background color to white
SCREEN.fill(BLACK)
balls = ball_list(0)
while True: # <--- main game loop
for event in pygame.event.get():
if event.type == QUIT: # QUIT event to exit the game
pygame.quit()
sys.exit()
if event.type == KEYDOWN:
if event.key == K_EQUALS:
balls.add(Ball(randint(0,400),randint(0,400), 4,5))
if event.key == K_MINUS:
try:
balls.remove()
except IndexError:
print('There is no balls to take!')
SCREEN.fill(BLACK)
for x in balls:
x.move(SCREEN,balls)
x.draw(SCREEN)
pygame.display.update() # Update the display when all events have been processed
FPS_CLOCK.tick(FPS)
if __name__ == "__main__":
main()
Removing Sprites on Press
The problem is sprite.Group.remove(sprites) wants you to specify which sprites it should remove. sprites here should be a sprite/list of sprites that you want to remove from the group. This means to remove the last ball added on key press you need to keep a list of the ball sprites and pop() the most recently added item from it, and then use the result of the pop() as the sprite to remove from the group. sprite.Group has a .sprites() method which returns a list of all sprites in the group, in the order they were added. This list is generated from the group and is not actually an interface with it, so doing things to this list won't affect the group. We can still however use it to get the last added sprite. Here is what it looks like:
elif event.key == K_0:
try:
sprite_list = balls.sprites()
to_remove = sprite_list[-1] # Get last element of list
balls.remove(to_remove)
except IndexError:
print('There is no balls to take!')
Collisions
So this is a bit more involved and not so simple to fix in your code. To understand what the problem is, look at what your collision velocity adjustments are actually doing for the screen border case.
l_collide = self.rect.x + self.image.get_width() + self.vx > SCREEN.get_width()
r_collide = self.rect.x + self.vx < 0
t_collide = self.rect.y + self.vy < 0
b_collide = self.rect.y + self.image.get_height() + self.vy > SCREEN.get_height()
#################
if l_collide or r_collide:
self.vx *= -1
if t_collide or b_collide:
self.vy *= -1
Consider a single time-step in your code. We check to see if the sprite is sitting over the edge of the boundaries by any amount. If its hanging over, we reverse the velocity. There is a case where your edge checking will get you into trouble. If your self.vx is less than the difference between your current position X and the boundary of the x dimension, you will reverse your speed, travel self.vx back towards the boundary, but not make it past. In the next time-step, you will see that you are still over the boundary, and your program will again reverse self.vx, actually sending you away from the boundary. In this case you will bound back and forth each time-step by self.vx. Normally this wouldn't happen in your code, except for when you spawn a new ball sprite over the boundary further than your self.vx or self.vy for that ball. This can be remedied by making sure you don't spawn balls off the edges, or better yet, only reversing your velocity if you need to.
if (l_collide and self.vx>0) or (r_collide and self.vx<0):
self.vx *= -1
if (t_collide and self.vy<0) or (b_collide and self.vy>0):
self.vy *= -1
Notice here we only reverse the velocity if we are over the edge AND the velocity is headed deeper in that direction. Now for your sprites you have two options, just like with the boundaries:
Only initiate a new ball in empty space where it cannot collide.
Implement some way to calculate the correct velocity adjustment and only apply it if the velocity is headed in the opposite direction.
From what I read in the documentation, sprite.Group looks like it is meant for checking if sprites are overlapping, and not for physics simulation. I recommend doing some research on 2d physics simulation to get a nice conceptualization of what information you should want to communicate between objects. I'm sure there are some nice tutorials out there.
Finally, to address your other question about why they are colliding when they don't appear to be touching. sprite.spritecollide is returning which sprites have rectangles that intersect. If your ball.png is color keyed for transparency, this does not affect the rect of the sprite. Pygame appears to have functionality implemented designed to handle this problem in the collided keyword of sprite.spritecollide:
pygame.sprite.spritecollide()
Find sprites in a group that intersect another sprite.
spritecollide(sprite, group, dokill, collided = None) -> Sprite_list
The collided argument is a callback function used to calculate if two sprites >are colliding. it should take two sprites as values, and return a bool value >indicating if they are colliding. If collided is not passed, all sprites must >have a “rect” value, which is a rectangle of the sprite area, which will be >used to calculate the collision.
collided callables:
collide_rect
collide_rect_ratio
collide_circle
collide_circle_ratio
collide_mask
That's from the pygame documentation. The documentation for the collide_circle function states that your sprite should have a radius attribute, or else one will be calculated to fit the entire rectangle inside a circle. As such, in your Ball.__init__ function I would recommend adding:
self.radius = self.rect.width/2
This will make collide_circle use a radius that approximates your ball image, assuming it is centered and circular and occupies the entire image. Next, you must add the collision specification to your collision check by changing:
a = pygame.sprite.spritecollide(self, balls, False, False)
to
a = pygame.sprite.spritecollide(self, balls, False, pygame.sprite.collide_circle)
If you solve the problem of not spawning new ball objects inside each other, this should all work nicely. If you can't get them to spawn inside each other, think about a different data-structure or different way of collision checking to get the results you want. Best of luck!
I can see two questions in your text
You want to only remove one sprite, rather than all the sprites in the spritegroup
If you look at the pygame documentation, you can see that spritegroup.remove has an optional argument. You can remove a single sprite by putting your desired sprite as the argument, such as myspritegroup.remove(mysprite).
You have issues with the colliding
Your collision works for me as long as the balls don't spawn on top of each other on creation which you can simply check. Good luck :)
I am trying to make a game with pygame but I can't figure out how to keep my character from going off screen(set a limit). I have a .png image controlled by user input, but it's possible for the character to go off the visible screen area normally. I can't figure out how to do this. I made a rectangle around the window, (pygame.draw.rect) but I can't assign the rect to a variable so I can create a collision. I also tried this:
if not character.get_rect() in screen.get_rect():
print("error")
But it didn't work, just spammed the python console with "error" messages.
(i checked the other post with this question but nothing worked/didn't get it)
So my question is, how can I keep my character from going offscreen, and which is the best way to do that?
~thanks
EDIT: My game doesn't have a scrolling playfield/camera. (just a fixed view on the whole window)
if not character.get_rect() in screen.get_rect():
print("error")
I see what you are trying here. If you want to check if a Rect is inside another one, use contains():
contains()
test if one rectangle is inside another
contains(Rect) -> bool
Returns true when the argument is completely inside the Rect.
If you simply want to stop the movement on the edges on the screen, an easy solution is to use clamp_ip():
clamp_ip()
moves the rectangle inside another, in place
clamp_ip(Rect) -> None
Same as the Rect.clamp() [Returns a new rectangle that is moved to be completely inside the argument Rect. If the rectangle is too large to fit inside, it is centered inside the argument Rect, but its size is not changed.] method, but operates in place.
Here's a simple example where you can't move the black rect outside the screen:
import pygame
pygame.init()
screen=pygame.display.set_mode((400, 400))
screen_rect=screen.get_rect()
player=pygame.Rect(180, 180, 20, 20)
run=True
while run:
for e in pygame.event.get():
if e.type == pygame.QUIT: run = False
keys = pygame.key.get_pressed()
if keys[pygame.K_w]: player.move_ip(0, -1)
if keys[pygame.K_a]: player.move_ip(-1, 0)
if keys[pygame.K_s]: player.move_ip(0, 1)
if keys[pygame.K_d]: player.move_ip(1, 0)
player.clamp_ip(screen_rect) # ensure player is inside screen
screen.fill((255,255,255))
pygame.draw.rect(screen, (0,0,0), player)
pygame.display.flip()
When you used pygame.draw.rect, you didn't actually create a "physical" boundary- you just set the colour of the pixels on the screen in a rectangular shape.
If you know the size of the screen, and the displacement of all of the objects on the screen (only applicable if your game has a scrolling playfield or camera), then you can do something like this:
# In the lines of code where you have the player move around
# I assume you might be doing something like this
if keys[pygame.K_RIGHT]:
player.move(player.getSpeed(),0) # giving the x and y displacements
if keys[pygame.K_LEFT]:
player.move(-player.getSpeed(),0)
...
class Player:
...
def move(self, dx, dy):
newX = self.x + dx
newY = self.y + dy
self.x = max(0, min(newX, SCREEN_WIDTH)) # you handle where to store screen width
self.y = max(0, min(newY, SCREEN_HEIGHT))
Note that a useful tool for you to get the size of the Pygame window is pygame.display.get_surface().get_size() which will give you a tuple of the width and height. It is still better, however, to avoid calling this every time you need to know the boundaries of the player. That is, you should store the width and height of the window for later retrieval.
Here's a simple control code that I use in my games to keep sprites from going off the screen:
# Control so Player doesn't go off screen
if self.rect.right > WIDTH:
self.rect.right = WIDTH
if self.rect.left < 0:
self.rect.left = 0
if self.rect.bottom > HEIGHT:
self.rect.bottom = HEIGHT
if self.rect.top < 0:
self.rect.top = 0
WIDTH and HEIGHT are constants that you define to set the size of your screen. I hope this helps.