I am making a simple Breakout/Arkanoid game to learn pygame. I'm running into an issue where the rectangles of the paddle and the ball are not properly colliding. I also noticed that the ball doesn't collide with the bricks if I shoot the ball between two bricks, even when the ball sprite visually overlaps the bricks. This snippet if from the ball's .update method, which passes in the paddle and a list of bricks.
new_pos = self.__calc_pos()
# Check for collision with walls
if not self.area.contains(new_pos):
self.angle = -self.angle
new_pos = self.__calc_pos()
else:
# Check for collision with paddle
if paddle.rect.contains(new_pos):
self.angle = -self.angle
new_pos = self.__calc_pos()
# Check for collision with bricks
for brick in bricks:
if brick.rect.contains(new_pos):
self.angle = -self.angle
new_pos = self.__calc_pos()
brick.kill()
bricks.remove(brick)
self.rect = new_pos
The .__calc_pos method:
def __calc_pos(self):
new_x = int(math.cos(math.radians(self.angle))) * self.speed
new_y = -int(math.sin(math.radians(self.angle))) * self.speed
return self.rect.move(new_x, new_y)
contains() checks if one rect if fully inside another rect - and it doesn't true if one object only partially touch other object. Use colliderect()
contains()
test if one rectangle is inside another
contains(Rect) -> bool
Returns true when the argument is completely inside the Rect.
-
colliderect()
test if two rectangles overlap
colliderect(Rect) -> bool
Returns true if any portion of either rectangle overlap (except the top+bottom or left+right edges).
Related
I have a problem with my pygame script in which the way I check for collisions doesn't seem to work with the x axis and presents some very frustrating and downright stupid errors. The way it works is I first check for movement in the axis of choice, here is an example in the y axis. I then get a list of tiles I collide with and check to see if there are any collisions and the tile's collision index is for the top of itself:
if self.velocity.y > 0:
collide_rect = pygame.sprite.spritecollide(self, self.tile_group, False)
if collide_rect and collide_rect[0].top_collide == True:
self.position.y = collide_rect[0].rect.top + 1
self.velocity.y = 0
The last two lines set the position of the player, set at the bottom left of it's rectangle, to the top of the tile + 1 pixel to avoid jittering. I then set the y velocity to 0. But here is the code I have for the x axis:
if self.velocity.x > 0:
collide_rect = pygame.sprite.spritecollide(self, self.tile_group, False)
if collide_rect and collide_rect[0].left_collide == True:
self.rect.right = collide_rect[0].rect.left
If I run this code and collide to the left of a tile, I stop for a moment and go through the tile. If I also set the x velocity to zero, the same thing happens. If I set the x velocity to -5, the player jitters inside of the tile and in the ground as well. And worst of all, if the tile has top and side collision, the player glitches out on top of it due to how the player is technically inside the tile to avoid the horrible jittering the collision causes otherwise, and ends up being shot to the left most area of the tile. This is most frustrating as I just want to make a platformer game in pygame, like Mario or Sonic, with basic collision that makes sense.
I've searched google and stack overflow for answers to my problem, but the most I found was a tutorial for collision in the y axis, which works for me just fine. The second best was saying basically just search up proper game design docs, which is the most unhelpful advice I have heard since that's why I'm here. (BTW, I use a 2D vector system for my movement) The expected result I wish to have is the player staying in place when running up against a wall without clipping into the ground or wall visibly. Here is the link to the file I'm using to create this.
Similar questions have been asked many times, and the answer is still the same. You need to separate the motion and collision detection along the X-axis and Y-axis. First, perform the fall and jump and limit the vertical position according to the obstacles. Then move the player horizontally and limit the horizontal position of the player according to the obstacles.
If you do not separate the directions, the direction you treat first always "wins". e.g. if you fall (even minimally) and move to the left at the same time, the player is always automatically levitated onto each object, since the vertical collision control also detects a collision and lifts the player to the next level.
Minimal example based on your code:
class Player(pygame.sprite.Sprite):
# [...]
def update(self):
self.acceleration = vector(0, self.VERTICAL_ACCELERATION)
keys = pygame.key.get_pressed()
if keys[pygame.K_a]:
self.acceleration.x -= self.HORIZONTAL_ACCELERATION
if keys[pygame.K_d]:
self.acceleration.x += self.HORIZONTAL_ACCELERATION
self.acceleration.x -= self.velocity.x * self.HORIZONTAL_FRICTION
self.velocity += self.acceleration
# Motion and collision detection along the y-axis
self.position.y += self.velocity.y + .5 * self.acceleration.y
self.rect.bottomleft = self.position
collide_rect = pygame.sprite.spritecollide(self, self.tile_group, False)
if collide_rect:
if self.velocity.y > 0:
self.velocity.y = 0
self.position.y = collide_rect[0].rect.top
self.rect.bottom = self.position.y
elif self.velocity.y < 0:
self.velocity.y = 0
self.rect.top = collide_rect[0].rect.bottom
self.rect.bottom = self.position.y
# Motion and collision detection along the x-axis
self.position.x += self.velocity.x + .5 * self.acceleration.x
self.rect.bottomleft = self.position
collide_rect = pygame.sprite.spritecollide(self, self.tile_group, False)
if collide_rect:
if self.velocity.x > 0:
self.rect.right = collide_rect[0].rect.left
self.position.x = self.rect.x
self.velocity.x = 0
elif self.velocity.x < 0:
self.rect.left = collide_rect[0].rect.right
self.position.x = self.rect.x
self.velocity.x = 0
def jump(self):
self.rect.y += 1
if pygame.sprite.spritecollide(self, self.tile_group, False):
print("jump")
self.velocity.y -= 10
self.rect.y -= 1
This is my collision code. i made a temp_rect cause after colliding with the ground it started teleporting around. it works fine, but when i collide witha tile from left/right it teleports to the top
def collision_test(rect,tiles):
hit_list=[]
for tile in tiles:
if rect.colliderect(tile):
hit_list.append(tile)
return hit_list
def collision_move(rect,movement,tiles):
collision_types={"Top":False,"Bottom":False,"Left":False,"Right":False}
rect_temp=pygame.Rect(rect.x,rect.y,atom.width,atom.height-1)
rect_temp.x+=movement[0]
hit_list=collision_test(rect_temp,tiles)
for tile in hit_list:
if movement[0]>0:
rect.right=tile.left
collision_types["Right"]=True
elif movement[0]<0:
rect.left=tile.right
collision_types["Left"]=True
rect_temp.y+=movement[1]
hit_list=collision_test(rect_temp,tiles)
for tile in hit_list:
if movement[1]>0:
rect.bottom=tile.top
collision_types["Bottom"]=True
elif movement[1]<0:
rect.top=tile.bottom
collision_types["Top"]=True
return rect,collision_types
First you test the collision along the x-axis. If a collision is detected, the position of the rectangle is corrected. Therefore you have to set rect_temp again, a after the collision handling along the x-axis and before the collision handling for the y-axis:
def collision_move(rect,movement,tiles):
collision_types={"Top":False,"Bottom":False,"Left":False,"Right":False}
rect_temp=pygame.Rect(rect.x,rect.y,atom.width,atom.height-1)
rect_temp.x+=movement[0]
hit_list=collision_test(rect_temp,tiles)
for tile in hit_list:
if movement[0]>0:
rect.right=tile.left
collision_types["Right"]=True
elif movement[0]<0:
rect.left=tile.right
collision_types["Left"]=True
rect_temp=pygame.Rect(rect.x,rect.y,atom.width,atom.height-1) # <- this is missing
rect_temp.y+=movement[1]
hit_list=collision_test(rect_temp,tiles)
for tile in hit_list:
if movement[1]>0:
rect.bottom=tile.top
collision_types["Bottom"]=True
elif movement[1]<0:
rect.top=tile.bottom
collision_types["Top"]=True
return rect,collision_types
I am trying to implement a pong game in python tkinter. I have paddle on the left side of the canvas and I want to detect the collision of the ball with that paddle. I am unable to get the collision detected properly as sometimes the ball just passed through the paddle. please any help is appreciated , Below is the code for the collision detection i used, not sure whats wrong with it. I am trying to implement is if the ball position is between paddle top and bottom and between left and right then there is collision
def hit_paddle(self, pos):
""" pos(left, top, right, bottom)"""
paddle_pos = self.canvas.coords(self.shape)
if ball_pos[1] >= paddle_pos[1] and ball_pos[1] <= paddle_pos[3]:
if ball_pos[2] >= paddle_pos[0] and ball_pos[2] <= paddle_pos[2]:
return True
return False
You have the right idea, but there are some rare cases when the ball could pass through the paddle.
The first if statement checks for the y position of the ball and paddle. This should return true iff any part of the ball is in between paddle_pos[1] and paddle_pos[3].
The second if statement checks for the x position, and has the same logic. You need to check if the LEFT side of the ball is within range since the that's the part of the ball that will hit the LEFT paddle.
Try out this code:
def hit_paddle(self, pos):
""" pos(left, top, right, bottom)"""
paddle_pos = self.canvas.coords(self.shape)
if ball_pos[3] >= paddle_pos[1] and ball_pos[1] <= paddle_pos[3]:
if ball_pos[0] >= paddle_pos[0] and ball_pos[0] <= paddle_pos[2]:
return True
return False
Assuming that ball_pos[1] is the y coordinate of the lower left vertex of the ball's bounding box, Shouldn't you also put a constraint on the y coordinate of the upper-right vertex of the ball's bounding box? For example, if the ball is just bellow the lower end of the paddle I think your code won't count it as a hit.
Hope this helps!
I have a game where the bullet is moving so fast that in a single frame it is already across the screen. That being said, it has already collided with multiple walls. Currently, I have a rectangular image that spans from where the bullet currently is, to where the bullet will be in the next frame, in order to not miss any zombies that may be in between.
I also kill the bullet if it collided with any wall, before checking if it collided with any of the zombies because what happened was that if there was a zombie behind the wall and I checked for the zombie collisions first, it would kill the zombie and then kill the bullet.
So basically, I would like to know a way to find the coordinates of where the bullet collided with the wall so that instead of advancing the bullet at its full speed, I will just advance it to just before where the collision is, check for zombie collisions, and then kill the bullet.
I am using mask collision.
If the bullets travel too fast to collide with the walls or enemies, you need ray casting (or alternatively move the bullets in multiple small steps). Here's a simple ray casting example that returns the closest collision point. I use vectors and pygame.Rect.collidepoint to see if a point along the heading vector collides with an obstacle.
import sys
import pygame as pg
from pygame.math import Vector2
class Wall(pg.sprite.Sprite):
def __init__(self, x, y, w, h, *groups):
super().__init__(*groups)
self.image = pg.Surface((w, h))
self.image.fill(pg.Color('goldenrod4'))
self.rect = self.image.get_rect(topleft=(x, y))
def ray_cast(origin, target, obstacles):
"""Calculate the closest collision point.
Adds the normalized `direction` vector to the `current_pos` to
move along the heading vector and uses `pygame.Rect.collidepoint`
to see if `current_pos` collides with an obstacle.
Args:
origin (pygame.math.Vector2, tuple, list): Origin of the ray.
target (pygame.math.Vector2, tuple, list): Endpoint of the ray.
obstacles (pygame.sprite.Group): A group of obstacles.
Returns:
pygame.math.Vector2: Closest collision point or target.
"""
current_pos = Vector2(origin)
heading = target - origin
# A normalized vector that points to the target.
direction = heading.normalize()
for _ in range(int(heading.length())):
current_pos += direction
for sprite in obstacles:
# If the current_pos collides with an
# obstacle, return it.
if sprite.rect.collidepoint(current_pos):
return current_pos
# Otherwise return the target.
return Vector2(target)
def main():
screen = pg.display.set_mode((640, 480))
clock = pg.time.Clock()
all_sprites = pg.sprite.Group()
walls = pg.sprite.Group()
Wall(100, 170, 90, 20, all_sprites, walls)
Wall(200, 100, 20, 140, all_sprites, walls)
Wall(400, 60, 150, 100, all_sprites, walls)
pos = Vector2(320, 440)
done = False
while not done:
for event in pg.event.get():
if event.type == pg.QUIT:
done = True
all_sprites.update()
collision_point = ray_cast(pos, pg.mouse.get_pos(), walls)
screen.fill((30, 30, 30))
all_sprites.draw(screen)
pg.draw.line(screen, (50, 190, 100), pos, pg.mouse.get_pos(), 2)
pg.draw.circle(screen, (40, 180, 250), [int(x) for x in collision_point], 5)
pg.display.flip()
clock.tick(30)
if __name__ == '__main__':
pg.init()
main()
pg.quit()
sys.exit()
Unfortunately PyGame doesn't give us a built-in means of returning collision points easy way of doing this so there is a bit of leg work to do.
However, before I explain that, you mentioned in your question that the bullet is moving very fast. I'm not sure how fast we are talking so this might not apply, but in my experience collision becomes a hit and miss at high speeds, especially if you're working on a slower computer.
Assuming that ^ isn't applicable:
We can use pygame.Rect.colliderect to trigger an if statement.
if <bullet-rect>.collidepoint(<wall-rect>):
print(bullet_x, bullet_y)
Simply swap out and for the actual rects and you should be good to go. One thing to note is that if the bullet is moving right to left, you will have to add the bullet's width to the x value, and if the bullet is moving top to bottom, you will have to add the bullet's height to the y value.
Note: Remember to add pygame.Rect(<bullet-rect>) and pygame.Rect(<wall-rect>) to each value or you'll get an error.
I'm making a puzzle game that requires the user to 'draw' circles onto a background to get a ball to the exit. They create circles by holding their mouse button, the circle grows; when it is big enough, they let go and it is 'punched' into the physical space and balls then react to it.
I have a problem, however, that when two circles are intersecting (so a ball should pass through), if the intersection is not larger than the diameter of the ball the ball collides with the interior of the circle as usual.
This may be a little hard to comprehend, so here's a link to the screencast showing the problem (You can't embed videos on Stack Overflow): http://www.youtube.com/watch?v=3dKyPzqTDhs
Hopefully that made my problem clear. Here is the Python / PyGame code for the Ball and Circle classes:
class Ball():
def __init__(self, (x,y), size, colourID):
"""Setting up the new instance"""
self.x = x
self.y = y
self.size = size
self.exited = False
self.colour = setColour(colourID)
self.thickness = 0
self.speed = 0.01
self.angle = math.pi/2
def display(self, surface):
"""Draw the ball"""
# pygame.gfxdraw.aacircle(screen,cx,cy,new_dist,settings['MINIMAP_RINGS'])
if self.exited != True:
pygame.draw.circle(surface, self.colour, (int(self.x), int(self.y)), self.size, self.thickness)
def move(self):
"""Move the ball according to angle and speed"""
self.x += math.sin(self.angle) * self.speed
self.y -= math.cos(self.angle) * self.speed
(self.angle, self.speed) = module_physicsEngine.addVectors((self.angle, self.speed), gravity)
self.speed *= drag
And the Circle class:
class Circle():
def __init__(self, (x,y), size, colourID):
"""Set up the new instance of the Circle class"""
self.x = x
self.y = y
self.size = size
self.colour = setColour(colourID)
self.thickness = 2
self.angle = 0 # Needed for collision...
self.speed = 0 # detection against balls
def display(self, surface):
"""Draw the circle"""
pygame.draw.circle(surface, self.colour, (int(self.x), int(self.y)), self.size, self.thickness)
Within the main loop of the game (while running == True: etc.), this code is used to perform actions on each ball:
for b in balls:
b.move()
for i, ball in enumerate(balls):
for ball2 in balls[i+1:]:
collideBalls(ball, ball2)
collideCircle(b) # <---------------- This is the important line
collideExit(b)
b.display(screen)
And finally, the collideCircle(b) function, which is called once per ball to check for collisions with the interior of a circle, and also to check if the circles are intersecting.
def collideCircle(ball):
"""Check for collision between a ball and a circle"""
hit = False
closestDist = 0
for c in circles:
# Code cannot be replaced with physicsEngine.collideTest because it
# is slightly differnt, testing if ball [ball] inside a circle [c]
dx = c.x - ball.x
dy = c.y - ball.y
distance = math.hypot(dx, dy)
if distance <= c.size - ball.size:
# If BALL inside any CIRCLE
hit = False
break
else:
# If we're outside of a circle.
if closestDist < c.size - (distance - ball.size):
hit = c
closestDist = (c.size - (distance - ball.size))
if hit:
module_physicsEngine.circleBounce(hit, ball)
Ok, so I know that this has been a bit of a long and talky question, but I think you have all the information needed. Is the solution to make the balls interact correctly something to do with the line if distance <= c.size - ball.size:?
Anyway, thanks in advance!
Nathan out.
TL;DR - Watch the youtube video, and let me know why it's not working.
The problem is with unintended hits rather than missed ones. What you really want to check is if all parts of the ball are covered by some circle, while the check you're doing is if any circle only partially overlaps - but an override if any circle fully covers the ball.
I figure for any potential hit point, i.e. closest inner wall of a circle, let that point "walk" along the wall by checking its distance from all other circles. Should it then leave the ball, it was a false hit.
First you find the list of circles that touch the ball at all. As before, if any of them cover it, you can skip the rest of the checks. Also find the closest wall point to the ball for the circles. For each of those closest wall points, if it overlaps another circle, move it to the intersection point which is closest to the ball but further away than the current point. Discard it if it's outside the ball. Repeat the procedure for all circles, since more than two may overlap. Also note that the moving of the point may cause it to enter new circles.
You could precompute the intersection points and discard any that are a ball radius inside of any other circle.
This can surely be improved on, but it's a start, I think. I suspect a bug involving the case when both intersection points of a pair of circles overlap the ball, but a walk chain leads one of them outside the ball. Perhaps the initial collision points should be replaced only by both intersection points, not the closest.
I watched the video and I like the game principle. :)
Maybe the problem is that you break out of the loop as soon as you encounter a circle that encloses the ball. I'm referring to the snippet
if distance <= c.size - ball.size:
# If BALL inside any CIRCLE
hit = False
break
Why would you not check all of the other circles, in that case? There might be another circle yet unchecked that causes a hit.
Btw, I wouldn't say if condition == True:, that's unpythonic. Just say if condition:.