Issue finding side of collision for Circle-Rectangle collision - python

I have found a function that works quite well for detecting collisions between a circle and a rectangle, and also finding the point of contact. I then use that point to determine which side of the rectangle the circle hit so I can reflect the circle. However, when the center of the circle is inside the rectangle, the function gives the closest point as the center of circle, and it handles it as hitting a vertex rather than a side. Here is my code:
def collide_rect(box, ball_):
#convenience
left = box.rect.left
right = left + box.rect.width
top = box.rect.top
bottom = top + box.rect.height
#find the closest point
closest = (max(left, min(ball_.center[0], right)), max(top, min(ball_.center[1], bottom)))
dx = ball_.center[0] - closest[0]
dy = ball_.center[1] - closest[1]
#handle the collsion
if math.hypot(dx, dy) <= ball.radius:
#Hit on the top or bottom
if left <= closest[0] <= right and (closest[1] == top or closest[1] == bottom):
ball_.vector = (ball_.vector[0], -1*ball_.vector[1])
#Hit on the side
elif top <= closest[1] <= bottom and (closest[0] == left or closest[0] == right):
ball_.vector = (-1*ball_.vector[0], ball_.vector[1])
#Hit a vertex
else:
ball_.vector = (-1*ball_.vector[0], -1*ball_.vector[1])
return True
else:
return False
Note that ball_.vector is the circle's direction vector and ball.radius is a class variable.
Any help with a better way to find the side of collision would be greatly appreciated!

You can find the side of the rectangle by finding the point on the rectangle that lies on the straight line given by the center of the circle and the center of the rectangle.
The point on the rectangle and the circle can be computed by the minimum relation of the offset between the center points and the size of the rectangle.
In the following algorithm, the rectangle is defined by the center point (r_cpt) and the size (r_size) and the circle is defined by the center point (c_cpt) and the radius (c_rad):
def intersectRectangleCircle(r_cpt, r_size, c_cpt, c_rad):
v2_c_cpt = pygame.math.Vector2(c_cpt)
v2_r_cpt = pygame.math.Vector2(r_cpt)
offset = v2_c_cpt - v2_r_cpt
if offset.x == 0 and offset.y == 0:
return [v2_c_cpt, v2_r_cpt]
if offset.x == 0:
ratio = r_size[1] / abs(offset.y)
elif offset.y == 0:
ratio = r_size[0] / abs(offset.x)
else:
ratio = min(r_size[0] / abs(offset.x), r_size[1] / abs(offset.y))
ratio *= 0.5
p1 = v2_r_cpt + (offset * ratio)
offset.scale_to_length(c_rad)
p2 = v2_c_cpt - offset
return [p1, p2]
The direction to the circle is the given by the vector from the center point of the rectangle to the point on the rectangle contour:
isect_pts = intersectRectangleCircle(rect_center, rect_size, circle_center, circle_diameter/2)
dx, dy = isect_pts[0].x - rect_center[0], isect_pts[1].y - rect_center[1]
See the example, (dx, dy) is represented by the magenta colored line:
repl.it/#Rabbid76/PyGame-NearestPointOnRectangle
import pygame
import math
pygame.init()
screen = pygame.display.set_mode((500, 500))
def intersectRectangleCircle(r_cpt, r_size, c_cpt, c_rad):
v2_c_cpt = pygame.math.Vector2(c_cpt)
v2_r_cpt = pygame.math.Vector2(r_cpt)
offset = v2_c_cpt - v2_r_cpt
if offset.x == 0 and offset.y == 0:
return [v2_c_cpt, v2_r_cpt]
if offset.x == 0:
ratio = r_size[1] / abs(offset.y)
elif offset.y == 0:
ratio = r_size[0] / abs(offset.x)
else:
ratio = min(r_size[0] / abs(offset.x), r_size[1] / abs(offset.y))
ratio *= 0.5
p1 = v2_r_cpt + (offset * ratio)
offset.scale_to_length(c_rad)
p2 = v2_c_cpt - offset
return [p1, p2]
def inBetween(p1, p2, px):
v = pygame.math.Vector2(p2) - pygame.math.Vector2(p1)
d = v.length()
if d == 0:
return False
v.normalize_ip()
vx = pygame.math.Vector2(px) - pygame.math.Vector2(p1)
dx = v.dot(vx)
return dx >= 0 and dx <= d
done = False
while not done:
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
rect_center = screen.get_rect().center
rect_size = screen.get_width() // 5, screen.get_height() // 10
rect = pygame.Rect(rect_center[0] - rect_size[0] // 2, rect_center[1] - rect_size[1] // 2, *rect_size)
circle_center = pygame.mouse.get_pos()
circle_diameter = min(*screen.get_size()) // 5
isect_pts = intersectRectangleCircle(rect_center, rect_size, circle_center, circle_diameter/2)
dx, dy = isect_pts[0].x - rect_center[0], isect_pts[1].y - rect_center[1]
screen.fill((255,255,255))
pygame.draw.rect(screen, (0, 0, 0), rect, 3)
pygame.draw.circle(screen, (0, 0, 0), circle_center, circle_diameter // 2, 3)
pygame.draw.line(screen, (0, 0, 255), rect_center, circle_center, 1)
pygame.draw.line(screen, (255, 0, 255), rect_center, (round(isect_pts[0].x), round(isect_pts[0].y)), 3)
for i in range(2):
px, py = round(isect_pts[i].x), round(isect_pts[i].y)
col = (255, 0, 0) if inBetween(rect_center, circle_center, (px, py)) else (0, 255, 0)
pygame.draw.line(screen, col, (px-5, py), (px+5, py), 3)
pygame.draw.line(screen, col, (px, py-5), (px, py+5), 3)
pygame.display.flip()
pygame.quit()
quit()

Related

Python 2d Raycaster not using proper depth [duplicate]

This question already has answers here:
How do I fix wall warping in my raycaster?
(1 answer)
Problem with recognising where a ray in raycaster intersects a wall along the horizontal axis
(1 answer)
cant get raycast to work from angles 90 to 270 pygame
(1 answer)
Why my raycasting keeps going through walls?
(1 answer)
Closed 3 months ago.
I am trying to create a raycast visualizer. The lines are supposed to shoot out and stop when they collide with a wall. Currently the length is entirely random and sometimes the rays point in directions that are even behind me. I am using an scale of 47 when i draw things to the screen for tiling purposes. I have tried for 10 or so hours every scale in the raycast code and I can't see what I am missing.
import pygame
import numpy
from PygameEngine import GameEngine
import sys
import math
class RayCasting:
FOV = numpy.pi/5
HALF_FOV = FOV/2
NUM_RAYS = GameEngine.WIDTH//2
HALF_NUM_RAYS = NUM_RAYS//2
DELTA_ANGLE = FOV/NUM_RAYS
MAX_DEPTH = 20
def __init__(self, game):
self.game = game
def rayCast(self):
ox, oy = self.game.wasd.pos
x_map = int(ox)
y_map = int(oy)
ray_angle = self.game.wasd.angle - self.HALF_FOV + 0.000001
for ray in range(self.NUM_RAYS):
sin_a = math.sin(ray_angle)
cos_a = math.cos(ray_angle)
# horizontals
y_hor, dy = (y_map + 1, 1) if sin_a > 0 else (y_map - 1e-6, -1)
depth_hor = (y_hor - oy) / sin_a
x_hor = ox + depth_hor * cos_a
delta_depth = dy / sin_a
dx = delta_depth * cos_a
print("YHor: ",y_hor, " DY:", dy, " Depth Hor: ", depth_hor, "X Hor: ", x_hor,
" Delta Depth: ", delta_depth, " DX: ", dx)
for i in range(self.MAX_DEPTH):
tile_hor = int(x_hor), int(y_hor)
if tile_hor in self.game.MAP.wallMap:
# print("INSIDE HOR")
break
x_hor += dx
y_hor += dy
depth_hor += delta_depth
# verticals
x_vert, dx = (x_map + 1, 1) if cos_a > 0 else (x_map - 1e-6, -1)
depth_vert = (x_vert - ox) / cos_a
y_vert = oy + depth_vert * sin_a
delta_depth = dx / cos_a
dy = delta_depth * sin_a
for i in range(self.MAX_DEPTH):
tile_vert = int(x_vert), int(y_vert)
if tile_vert in self.game.MAP.wallMap:
# print("INSIDE VERT")
break
x_vert += dx
y_vert += dy
depth_vert += delta_depth
# depth, texture offset
if depth_vert < depth_hor:
depth = depth_vert
#y_vert %= 1
#offset = y_vert if cos_a > 0 else (1 - y_vert)
else:
depth = depth_hor
#x_hor %= 1
#offset = (1 - x_hor) if sin_a > 0 else x_hor
# remove fishbowl effect
#depth *= math.cos(self.game.wasd.angle - ray_angle)
# projection
#proj_height = SCREEN_DIST / (depth + 0.0001)
# ray casting result
#self.ray_casting_result.append((depth, proj_height, texture, offset))
ray_angle += self.DELTA_ANGLE
pygame.draw.line(self.game.screen, "yellow", (ox*self.game.CELLSIZE,oy*self.game.CELLSIZE), (ox*self.game.CELLSIZE+depth*cos_a, oy*self.game.CELLSIZE+depth*sin_a), 1)
def update(self):
self.rayCast()
from PygameEngine import GameEngine
from Circle import Circle
import pygame
from pygame.locals import *
import sys
import numpy
from map import Map
from RaycastFunction import RayCasting
class RaycastGame(GameEngine):
# Space bar to place this circle which will connect to the WASD with a line
planet = Circle((0,0,0))
planet.keyX = 5
planet.keyY = 5
# Grid set up
def __init__(self):
super().__init__()
self.load()
self.MAP = Map()
self.CELLSIZE = self.MAP.CELLSIZE
# Circle controllable with WASD
self.wasd = Circle((123, 255, 123))
self.raycast = RayCasting(self)
def DDA(self):
# -
# * |
# Remember the Plane is - --m-- +
# * = target |
# m = mouse +
distX = self.wasd.keyX - self.planet.pos[0]
distY = self.wasd.keyY - self.planet.pos[1]
#hypotenuse = numpy.sqrt(distX**2+distY**2)
theta = numpy.arctan((distY/(distX+.0001)))
theta += numpy.deg2rad(90)
# print(numpy.rad2deg(theta), " THETA")
collisionPos = (0,0)
def draw(self):
# Draw MAP array
self.MAP.drawMap()
self.MAP.drawGrid()
# Draw mouse character
#pygame.draw.circle(self.screen, (0, 0, 0),
#(self.plane), Circle.radius)
# Draw planet
# self.planet.draw(self.screen)
# Draw wasd character
self.wasd.draw(self.screen)
# Connect mouse and wasd characters with a line
#pygame.draw.line(self.screen, (255, 255, 255), self.planet.pos, (self.wasd.keyX, self.wasd.keyY), 5)
def update(self):
self.planet.placePlanet()
self.wasd.move()
self.DDA()
self.raycast.update()
def run(self):
# Game loop.
while True:
#This gets written over. Only for clearing screen before each draw
self.screen.fill((0, 0, 0))
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
# Update.
self.update()
# Draw
self.draw()
pygame.display.flip()
self.fpsClock.tick(self.FPS)
I do not understand why the rays are not stopping in the proper area.

issue with defining new coordinates with walls [duplicate]

I try to implement beam collision detection with a predefined track mask in Pygame. My final goal is to give an AI car model vision to see a track it's riding on:
This is my current code where I fire beams to mask and try to find an overlap:
import math
import sys
import pygame as pg
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
pg.init()
beam_surface = pg.Surface((500, 500), pg.SRCALPHA)
def draw_beam(surface, angle, pos):
# compute beam final point
x_dest = 250 + 500 * math.cos(math.radians(angle))
y_dest = 250 + 500 * math.sin(math.radians(angle))
beam_surface.fill((0, 0, 0, 0))
# draw a single beam to the beam surface based on computed final point
pg.draw.line(beam_surface, BLUE, (250, 250), (x_dest, y_dest))
beam_mask = pg.mask.from_surface(beam_surface)
# find overlap between "global mask" and current beam mask
hit = mask.overlap(beam_mask, (pos[0] - 250, pos[1] - 250))
if hit is not None:
pg.draw.line(surface, BLUE, mouse_pos, hit)
pg.draw.circle(surface, GREEN, hit, 3)
surface = pg.display.set_mode((500, 500))
mask_surface = pg.image.load("../assets/mask.png")
mask = pg.mask.from_surface(mask_surface)
clock = pg.time.Clock()
while True:
for e in pg.event.get():
if e.type == pg.QUIT:
pg.quit()
sys.exit()
mouse_pos = pg.mouse.get_pos()
surface.fill((0, 0, 0))
surface.blit(mask_surface, mask_surface.get_rect())
for angle in range(0, 120, 30):
draw_beam(surface, angle, mouse_pos)
pg.display.update()
clock.tick(30)
Let's describe what happens in the code snippet. One by one, I draw beams to beam_surface, make masks from them, and find overlap with background mask defined by one rectangle and a circle (black color in gifs). If there is a "hit point" (overlap point between both masks), I draw it with a line connecting hit point and mouse position.
It works fine for angles <0,90>:
But it's not working for angles in range <90,360>:
Pygame's overlap() documentation tells this:
Starting at the top left corner it checks bits 0 to W - 1 of the first row ((0, 0) to (W - 1, 0)) then continues to the next row ((0, 1) to (W - 1, 1)). Once this entire column block is checked, it continues to the next one (W to 2 * W - 1).
This means that this approach will work only if the beam hits the mask approximately from the top left corner. Do you have any advice on how to make it work for all of the situations? Is this generally a good approach to solve this problem?
Your approach works fine, if the x and y component of the ray axis points in the positive direction, but it fails if it points in the negative direction. As you pointed out, that is caused by the way pygame.mask.Mask.overlap works:
Starting at the top left corner it checks bits 0 to W - 1 of the first row ((0, 0) to (W - 1, 0)) then continues to the next row ((0, 1) to (W - 1, 1)). Once this entire column block is checked, it continues to the next one (W to 2 * W - 1).
To make the algorithm work, you have to ensure that the rays point always in the positive direction. Hence if the ray points in the negative x direction, then flip the mask and the ray vertical and if the ray points in the negative y direction than flip the ray horizontal.
Use pygame.transform.flip() top create 4 masks. Not flipped, flipped horizontal, flipped vertical and flipped vertical and horizontal:
mask = pg.mask.from_surface(mask_surface)
mask_fx = pg.mask.from_surface(pg.transform.flip(mask_surface, True, False))
mask_fy = pg.mask.from_surface(pg.transform.flip(mask_surface, False, True))
mask_fx_fy = pg.mask.from_surface(pg.transform.flip(mask_surface, True, True))
flipped_masks = [[mask, mask_fy], [mask_fx, mask_fx_fy]]
Determine if the direction of the ray:
c = math.cos(math.radians(angle))
s = math.sin(math.radians(angle))
Get the flipped mask dependent on the direction of the ray:
flip_x = c < 0
flip_y = s < 0
filpped_mask = flipped_masks[flip_x][flip_y]
Compute the flipped target point:
x_dest = 250 + 500 * abs(c)
y_dest = 250 + 500 * abs(s)
Compute the flipped offset:
offset_x = 250 - pos[0] if flip_x else pos[0] - 250
offset_y = 250 - pos[1] if flip_y else pos[1] - 250
Get the nearest intersection point of the flipped ray and mask and unflip the intersection point:
hit = filpped_mask.overlap(beam_mask, (offset_x, offset_y))
if hit is not None and (hit[0] != pos[0] or hit[1] != pos[1]):
hx = 500 - hit[0] if flip_x else hit[0]
hy = 500 - hit[1] if flip_y else hit[1]
hit_pos = (hx, hy)
pg.draw.line(surface, BLUE, mouse_pos, hit_pos)
pg.draw.circle(surface, GREEN, hit_pos, 3)
See the example: repl.it/#Rabbid76/PyGame-PyGame-SurfaceLineMaskIntersect-2
import math
import sys
import pygame as pg
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
pg.init()
beam_surface = pg.Surface((500, 500), pg.SRCALPHA)
def draw_beam(surface, angle, pos):
c = math.cos(math.radians(angle))
s = math.sin(math.radians(angle))
flip_x = c < 0
flip_y = s < 0
filpped_mask = flipped_masks[flip_x][flip_y]
# compute beam final point
x_dest = 250 + 500 * abs(c)
y_dest = 250 + 500 * abs(s)
beam_surface.fill((0, 0, 0, 0))
# draw a single beam to the beam surface based on computed final point
pg.draw.line(beam_surface, BLUE, (250, 250), (x_dest, y_dest))
beam_mask = pg.mask.from_surface(beam_surface)
# find overlap between "global mask" and current beam mask
offset_x = 250 - pos[0] if flip_x else pos[0] - 250
offset_y = 250 - pos[1] if flip_y else pos[1] - 250
hit = filpped_mask.overlap(beam_mask, (offset_x, offset_y))
if hit is not None and (hit[0] != pos[0] or hit[1] != pos[1]):
hx = 499 - hit[0] if flip_x else hit[0]
hy = 499 - hit[1] if flip_y else hit[1]
hit_pos = (hx, hy)
pg.draw.line(surface, BLUE, pos, hit_pos)
pg.draw.circle(surface, GREEN, hit_pos, 3)
#pg.draw.circle(surface, (255, 255, 0), mouse_pos, 3)
surface = pg.display.set_mode((500, 500))
#mask_surface = pg.image.load("../assets/mask.png")
mask_surface = pg.Surface((500, 500), pg.SRCALPHA)
mask_surface.fill((255, 0, 0))
pg.draw.circle(mask_surface, (0, 0, 0, 0), (250, 250), 100)
pg.draw.rect(mask_surface, (0, 0, 0, 0), (170, 170, 160, 160))
mask = pg.mask.from_surface(mask_surface)
mask_fx = pg.mask.from_surface(pg.transform.flip(mask_surface, True, False))
mask_fy = pg.mask.from_surface(pg.transform.flip(mask_surface, False, True))
mask_fx_fy = pg.mask.from_surface(pg.transform.flip(mask_surface, True, True))
flipped_masks = [[mask, mask_fy], [mask_fx, mask_fx_fy]]
clock = pg.time.Clock()
while True:
for e in pg.event.get():
if e.type == pg.QUIT:
pg.quit()
sys.exit()
mouse_pos = pg.mouse.get_pos()
surface.fill((0, 0, 0))
surface.blit(mask_surface, mask_surface.get_rect())
for angle in range(0, 359, 30):
draw_beam(surface, angle, mouse_pos)
pg.display.update()
clock.tick(30)
Not,the algorithm can be further improved. The ray is always drawn on the bottom right quadrant of the beam_surface. Hence the other 3 quadrants are no longer needed and the size of beam_surface can be reduced to 250x250. The start of the ray is at (0, 0) rather than (250, 250) and the computation of the offsets hast to be slightly adapted:
beam_surface = pg.Surface((250, 250), pg.SRCALPHA)
def draw_beam(surface, angle, pos):
c = math.cos(math.radians(angle))
s = math.sin(math.radians(angle))
flip_x = c < 0
flip_y = s < 0
filpped_mask = flipped_masks[flip_x][flip_y]
# compute beam final point
x_dest = 500 * abs(c)
y_dest = 500 * abs(s)
beam_surface.fill((0, 0, 0, 0))
# draw a single beam to the beam surface based on computed final point
pg.draw.line(beam_surface, BLUE, (0, 0), (x_dest, y_dest))
beam_mask = pg.mask.from_surface(beam_surface)
# find overlap between "global mask" and current beam mask
offset_x = 499-pos[0] if flip_x else pos[0]
offset_y = 499-pos[1] if flip_y else pos[1]
hit = filpped_mask.overlap(beam_mask, (offset_x, offset_y))
if hit is not None and (hit[0] != pos[0] or hit[1] != pos[1]):
hx = 499 - hit[0] if flip_x else hit[0]
hy = 499 - hit[1] if flip_y else hit[1]
hit_pos = (hx, hy)
pg.draw.line(surface, BLUE, pos, hit_pos)
pg.draw.circle(surface, GREEN, hit_pos, 3)

Collision detection between an ellipse and a circle

I want to make a collision detection between an ellipse and circle. The way I am doing this is:
Calculate angle from centre of the ellipse to the circle
Calculate where the point lies in the ellipse at that angle
Check for collision against that point
However, I am having one small problem. When I calculate the angle, it seems like it is 90 degrees off. I did a dirty fix by simply adding 1.5 radians to account for the 90 degrees, and it kind of works but there are inconsistencies and doesn't work properly at certain angles, particularly at around 0.7 and -2.6 radians . Here is the code (all the collision stuff is in collision method in Ellipse class)
import pygame
from math import sin, cos, atan2, radians
pygame.init()
SW = 1200
SH = 600
WIN = pygame.display
D = WIN.set_mode((SW, SH))
class Circle:
def __init__(self, radius):
self.x = 0
self.y = 0
self.radius = radius
def update(self, pos):
self.x = pos[0]
self.y = pos[1]
def draw(self, display):
pygame.draw.circle(display, (255, 0, 0), (int(self.x), int(self.y)), self.radius, 2)
circle = Circle(30)
class Ellipse:
def __init__(self, centre, rx, ry):
self.centre = centre
self.collided = False
self.rx = rx
self.ry = ry
def draw(self, display):
angle = 0
while angle < 6.28:
angle += 0.001
x = self.centre[0] + sin(angle)* self.rx
y = self.centre[1] + cos(angle)* self.ry
if self.collided:
display.set_at((int(x), int(y)), (255, 0, 0))
else:
display.set_at((int(x), int(y)), (0, 0, 255))
pygame.draw.circle(D, (0, 255, 0), (int(self.centre[0]), int(self.centre[1])), 5)
def collision(self, circle):
#angle to the circle
dx = circle.x - self.centre[0]
dy = circle.y - self.centre[1]
angle = atan2(-dy, dx)
print(angle)
#where the point lies in the ellipse at that angle
x = sin(angle + 1.5)* self.rx + self.centre[0]
y = cos(angle + 1.5)* self.ry + self.centre[1]
#print(x, y)
#drawing the point just to make sure its working
# (debugging)
pygame.draw.circle(D, (0, 255, 0), (int(x), int(y)), 5)
# distance between the point we just
# calculated and the circle's centre
distance = ((x-circle.x)**2 + (y-circle.y)**2)**0.5
#print(distance)
#collision condition
if distance < circle.radius:
self.collided = True
else:
self.collided = False
ellipse = Ellipse([600, 300], 300, 200)
while True:
events = pygame.event.get()
mousePos = pygame.mouse.get_pos()
for event in events:
if event.type == pygame.QUIT:
pygame.quit()
D.fill((255, 255, 255))
circle.update(mousePos)
circle.draw(D)
ellipse.draw(D)
ellipse.collision(circle)
pygame.display.flip()
The fist mistake is, that 1.5 is not equal to pi/2:
from math import pi
x = sin(angle + 1.5)* self.rx + self.centre[0]
y = cos(angle + 1.5)* self.ry + self.centre[1]
x = sin(angle + pi/2)* self.rx + self.centre[0]
y = cos(angle + pi/2)* self.ry + self.centre[1]
Second, calculating the point on an ellipse by an angle is a little more complicated. See the answers to the questions How to get a point on an ellipse's outline given an angle? and Calculating a Point that lies on an Ellipse given an Angle:
from math import pi, sin, cos, atan2, radians, copysign, sqrt
class Ellipse:
# [...]
def pointFromAngle(self, a):
c = cos(a)
s = sin(a)
ta = s / c ## tan(a)
tt = ta * self.rx / self.ry ## tan(t)
d = 1. / sqrt(1. + tt * tt)
x = self.centre[0] + copysign(self.rx * d, c)
y = self.centre[1] - copysign(self.ry * tt * d, s)
return x, y
def collision(self, circle):
# [...]
#where the point lies in the ellipse at that angle
x, y = self.pointFromAngle(angle)
# [...]
Minimal example: repl.it/#Rabbid76/PyGame-IntersectCircleEllipse
import math
import pygame
class Circle:
def __init__(self, center_x, center_y, radius):
self.center = center_x, center_y
self.radius = radius
def update(self, center_x, center_y):
self.center = center_x, center_y
def draw(self, surface):
pygame.draw.circle(surface, (255, 0, 0), (round(self.center[0]), round(self.center[1])), self.radius, 3)
class Ellipse:
def __init__(self, center, vertex):
self.center = center
self.collided = False
self.vertex = vertex
def draw(self, surface):
bounding_rect = pygame.Rect(0, 0, self.vertex[0] * 2, self.vertex[1] * 2)
bounding_rect.center = round(self.center[0]), round(self.center[1])
pygame.draw.ellipse(surface, (0, 255, 0), bounding_rect, 3)
def pointFromAngle(self, a):
c = math.cos(a)
s = math.sin(a)
ta = s / c ## tan(a)
tt = ta * self.vertex[0] / self.vertex[1] ## tan(t)
d = 1. / math.sqrt(1. + tt * tt)
x = self.center[0] + math.copysign(self.vertex[0] * d, c)
y = self.center[1] - math.copysign(self.vertex[1] * tt * d, s)
return x, y
def intersect_circle_ellipse(circle, ellipse):
dx = circle.center[0] - ellipse.center[0]
dy = circle.center[1] - ellipse.center[1]
angle = math.atan2(-dy, dx)
x, y = ellipse.pointFromAngle(angle)
distance = math.hypot(x - circle.center[0], y-circle.center[1])
return distance <= circle.radius, (x, y)
pygame.init()
window = pygame.display.set_mode((500, 300))
circle = Circle(0, 0, 30)
ellipse = Ellipse(window.get_rect().center, (150, 100))
run = True
while run:
events = pygame.event.get()
mousePos = pygame.mouse.get_pos()
for event in events:
if event.type == pygame.QUIT:
run = False
circle.update(*mousePos)
isect = intersect_circle_ellipse(circle, ellipse)
window.fill((255, 255, 255))
circle.draw(window)
ellipse.draw(window)
color = (255, 0, 255) if isect[0] else (0, 0, 255)
pygame.draw.circle(window, color, (round(isect[1][0]), round(isect[1][1])), 5)
pygame.display.flip()
pygame.quit()
exit()

My A* pathfinding algorithm does not always get the shortest path

thanks in advance for any help.
I am making a pathfinder visualiser using python in pygame.
I have tried to make the A* algorithm, but sometimes it does not find the shortest path. I have been looking through several previous questions with the same issue, which has led me to believe it may be a problem with the heuristic. If I set the hueristic value to 0, then the algorithm becomes dijkstra's and always gets the shortest path.
A grid is used for the algorithm, with x being the number row and y being the number column (I believe might be the other way around but it doesnt matter)
Each square on the grid is an object, with x and y values, as well as a gScore, hScore and fScore. On initialisation these are all set to None.
I also have some functions at the bottom to do calculations, such as find the lowest fScore node from an array, find the gScore, find the hScore, fScore and get the distance between two grid squares.
I think the problem is in the hueristic function and have tried several different methods of fixing to no avail. From looking at the code below would anyone be able to see the problem, or point me in the right direction? Any help is much appreciated .
For simplicity I have only included the A* function without any of the pygame stuff, but I can add the entire program if need be, including the gridsquare object.
def a_star():
for row in grid:
for square in row:
if square.state == "start_pos":
start_pos = square
elif square.state == "end_pos":
end_pos = square
start_pos.gScore = find_g(start_pos, start_pos)
start_pos.hScore = find_h(start_pos, end_pos)
start_pos.fScore = find_f(start_pos.gScore, start_pos.hScore)
openList = [start_pos]
closedList = []
while len(openList) > 0:
current_node = get_lowest_f_node(openList)
if current_node.state == "end_pos":
print("found")
path = [end_pos]
node = current_node
while node.parent != None:
time.sleep(SHORTEST_PATH_DELAY)
node = node.parent
path.append(node)
return
openList.remove(current_node)
closedList.append(current_node)
x = current_node.x
y = current_node.y
# get nodes around current node
node1 = grid[x][y - 1]
node2 = grid[x][y + 1]
node3 = grid[x - 1][y]
node4 = grid[x + 1][y]
successor_nodes = [node1, node2, node3, node4]
for node in successor_nodes:
# check if walkable
if (node.state == "wall") or (node in closedList):
continue
if node.gScore == None:
node.gScore = current_node.gScore
tentative_g_score = current_node.gScore + get_distance(node, current_node)
if (node in closedList) and (tentative_g_score >= node.gScore):
continue
if (node not in openList) or (tentative_g_score < node.gScore):
node.parent = current_node
node.gScore = tentative_g_score
node.fScore = node.gScore + find_h(node, end_pos)
if node not in openList:
openList.append(node)
def get_lowest_f_node(array):
min_f = min(array, key = attrgetter("fScore"))
return min_f
# distance from current node and start node
def find_g(current, start_pos):
g = get_distance(current, start_pos)
return g
# distance from current node and target / destination / finish node
def find_h(current, end_pos):
h = get_distance(current, end_pos)
return h
# hscore and gscore added together
def find_f(score1, score2):
return score1 + score2
# distance from 2 points
def get_distance(start, end):
x1 = start.x
y1 = start.y
x2 = end.x
y2 = end.y
distancex = sqr(x2 - x1)
distancey = sqr(y2 - y1)
#distance = sqrt(distancex + distancey)
distance = distancex + distancey
return distance
def sqr(number):
return number * number
Below are some images of the result of the path finding, with different patters. The starting node is always the bottom red square.
^^^ This is where the A* algorithm finds the correct, shortest path. All good.
^^^This is where A* finds a path, but it is not the shortest path. This is what I am trying to fix, any help is much appreciated.
^^^ this is dijkstra's finding the correct path when presented with the same arrangement of walls.
I am very grateful for any help.
<==== Download This image
I'm no expert on A* but awhile I wrote a script for a YouTube video I was going to produce.
If you want it then it's here: https://pastebin.com/WycrpAfZ
You can also view it here:
import math, random, sys
import pygame
from pygame.locals import *
# exit the program
def events():
for event in pygame.event.get():
if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE):
pygame.quit()
sys.exit()
# define display surface
W, H = 1920, 1080
HW, HH = W / 2, H / 2
AREA = W * H
# initialise display
pygame.init()
pygame.font.init()
CLOCK = pygame.time.Clock()
FONT_SMALL = pygame.font.Font(None, 26)
FONT_LARGE = pygame.font.Font(None, 50)
DS = pygame.display.set_mode((W, H))
pygame.display.set_caption("code.Pylet - Template")
FPS = 1
# define some colors
BLACK = (0, 0, 0, 255)
WHITE = (255, 255, 255, 255)
RED = (255, 0, 0, 255)
GREEN = (0, 128, 0, 255)
BLUE = (0, 0, 255, 255)
PURPLE = (255, 255, 0, 255)
# define node class
class node:
def __init__(self, x, y, obstacle):
self.x = x
self.y = y
self.pos = (x, y)
self.h = 0
self.g = 0
self.f = 0
self.obstacle = obstacle
self.other = None
self.parent = None
def neighbourPos(self, offset):
return (self.x + offset[0], self.y + offset[1])
def draw(self, size, color = None, id = None, surface = None):
global text, FONT_SMALL, FONT_LARGE
if not surface: surface = pygame.display.get_surface()
pos = (self.x * size[0], self.y * size[1])
if not color:
if not self.obstacle:
if not self.other: pygame.draw.rect(surface, BLACK, pos + size, 0)
else: pygame.draw.rect(surface, BLUE, pos + size, 0)
else:
pygame.draw.rect(surface, WHITE, pos + size, 0)
else:
pygame.draw.rect(surface, color, pos + size, 0)
pygame.draw.rect(surface, WHITE, pos + size, 1)
if self.f:
text(FONT_SMALL, "G:{0}".format(self.g), pos[0] + 5, pos[1] + 5, 0, 0, surface)
text(FONT_SMALL, "H:{0}".format(self.h), pos[0] + size[0] - 5, pos[1] + 5, 1, 0, surface)
text(FONT_LARGE, "F:{0}".format(self.f), pos[0] + size[0] / 2, pos[1] + size[1] / 2 , 2, 2, surface)
if not id == None:
text(FONT_SMALL, "{0}".format(id), pos[0] + 5, pos[1] + size[1] - 5, 0, 1, surface)
def drawNodes(n, ms, cs):
for x in range(ms[0]):
for y in range(ms[1]):
n[x][y].draw(cs)
def drawNodeList(node_list, cs, color):
id = 0
for n in node_list:
n.draw(cs, color, id)
id += 1
def heuristics(pos1, pos2):
return int(math.hypot(pos1[0] - pos2[0], pos1[1] - pos2[1]) * 10)
def text(font, string, x, y, xJustify = None, yJustify = None, surface = None):
global WHITE
if not surface: surface = pygame.display.get_surface()
textSurface = font.render(string, 1, WHITE)
textRect = textSurface.get_rect()
if xJustify == 1:
x -= textRect.width
elif xJustify == 2:
x -= textRect.center[0]
if yJustify == 1:
y -= textRect.height
elif yJustify == 2:
y -= textRect.center[1]
surface.blit(textSurface, (x, y))
map = pygame.image.load("test.png").convert()
map_size = map_width, map_height = map.get_rect().size
cell_size = (W / map_width, H / map_height)
#create list of nodes
nodes = list([])
for x in range(map_width):
nodes.append(list([]))
for y in range(map_height):
color = map.get_at((x, y))
if color != WHITE:
nodes[x].append(node(x, y, False))
if color == BLUE:
start = nodes[x][y]
start.other = True
elif color == RED:
end = nodes[x][y]
end.other = True
else:
nodes[x].append(node(x, y, True))
# This list contains relative x & y positions to reference a node's neighbour
NEIGHBOURS = list([(-1, -1), (0, -1), (1, -1), (1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0)])
# the closed list contains all the nodes that have been considered economical viable.
# By that I mean a node that has been closer to the end node than any other in the open list at one time
closed = list([])
# The open list contains all the closed list's neighbours that haven't been identified as being economically sound node yet
open = list([])
open.append(start) # add the start node so that we can then add it's neighbours
# if the algorithm finds the end node then pathFound will be true otherwise it's false.
# Once it becomes true there's no more calculations to do so the path finding script will be skipped over
pathFound = False
completedPath = list([]) #
# main loop
while True:
DS.fill(BLACK)
drawNodes(nodes, map_size, cell_size)
drawNodeList(open, cell_size, GREEN)
drawNodeList(closed, cell_size, RED)
if pathFound: drawNodeList(completedPath, cell_size, PURPLE)
pygame.display.update()
# wait for user to press mouse button
while not pygame.mouse.get_pressed()[0]:
events()
while pygame.mouse.get_pressed()[0]:
events()
# if we've found the quickest path from start node to end node then just draw, no need continue path finding
if pathFound: continue
if not open: continue
# get lowest f from the open list, the node with the lowest f is the most economical in terms of the path towards the end node
openNodeWithlowestF = open[0]
for o in open:
if o.f < openNodeWithlowestF.f: openNodeWithlowestF = o
mostEconomicalNodeSoFar = openNodeWithlowestF # let's make this more readable! Economical means the best path to the end given the choices but not definitive.
# remove the mostEconomicalNodeSoFar from the open list
open.remove(mostEconomicalNodeSoFar)
# add mostEconomicalNodeSoFar to the closed list
closed.append(mostEconomicalNodeSoFar)
# if the mostEconomicalNodeSoFar is equal to the end node then we've reach our target
if mostEconomicalNodeSoFar == end:
temp = end
while temp.parent:
completedPath.append(temp)
temp = temp.parent
completedPath.append(start)
pathFound = True
# get the path etc
# iterate through the list of neighbours belonging to the mostEconomicalNodeSoFar. Why?
for neighbourOffset in NEIGHBOURS:
nx, ny = mostEconomicalNodeSoFar.neighbourPos(neighbourOffset)
if nx < 0 or nx >= map_width or ny < 0 or ny >= map_height: continue
neighbour = nodes[nx][ny] # create a variable to represent the mostEconomicalNodeSoFar's neighbour
if neighbour.obstacle: continue # if the mostEconomicalNodeSoFar's neighbouring node is an obstacle then we can't ...?
if neighbour in closed: continue # if the mostEconomicalNodeSoFar's neighbouring node is in the closed list then we can't ...?
# now we need to see if the mostEconomicalNodeSoFar's neighbour is more economical ...?
hypotheticalFScore = mostEconomicalNodeSoFar.g + heuristics(neighbour.pos, mostEconomicalNodeSoFar.pos)
NeighbourIsBetterThanMostEconomicalNodeSoFar = False # Yes it's a long variable name but it describes what it is so all is good!
# is this neighbour already in open list? if it is then we don't want to be adding it again. to chec
if not neighbour in open:
NeighbourIsBetterThanMostEconomicalNodeSoFar = True
neighbour.h = heuristics(neighbour.pos, end.pos)
open.append(neighbour)
elif hypotheticalFScore < neighbour.g:
NeighbourIsBetterThanMostEconomicalNodeSoFar = True
if NeighbourIsBetterThanMostEconomicalNodeSoFar:
neighbour.parent = mostEconomicalNodeSoFar
neighbour.g = hypotheticalFScore
neighbour.f = neighbour.g + neighbour.h
#sys.exit()

Elastic collision between moving particles in python: why is kinetic energy not conserved?

I am trying to code a particle simulation in pygame but am having trouble coding the collisions between particles. All the collisions are elastic so kinetic energy should be conserved. However, I am experiencing two main problems:
Particles speed up and up until they get out of control
Particles clump together and stop moving
I would really appreciate any insight to help solve these problems.
I used this document (http://www.vobarian.com/collisions/2dcollisions2.pdf) to help calculate the new velocities of the particles after the collision. The mass of all the particles is the same so I have ignored their mass in my calculations.
Here is my code so far:
import pygame
pygame.init()
import random
import numpy
HEIGHT = 500
WIDTH = 500
NUM_BALLS = 2
#setup
win = pygame.display.set_mode((WIDTH,HEIGHT))
pygame.display.set_caption("Simulation")
class Particle(object):
def __init__(self, x, y , radius):
self.x = x
self.y = y
self.radius = radius
self.xvel = random.randint(1,5)
self.yvel = random.randint(1,5)
def Draw_circle(self):
pygame.draw.circle(win, (255,0,0), (self.x, self.y), self.radius)
def redrawGameWindow():
win.fill((0,0,0))
for ball in balls:
ball.Draw_circle()
pygame.display.update()
balls = []
for turn in range(NUM_BALLS):
balls.append(Particle(random.randint(20,WIDTH-20),random.randint(20,HEIGHT-20),20))
run = True
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
for ball in balls:
if (ball.x + ball.radius) <= WIDTH or (ball.x - ball.radius) >= 0:
ball.x += ball.xvel
if (ball.x + ball.radius) > WIDTH or (ball.x - ball.radius) < 0:
ball.xvel *= -1
ball.x += ball.xvel
if (ball.y + ball.radius) <= HEIGHT or (ball.y - ball.radius) >= 0:
ball.y += ball.yvel
if (ball.y + ball.radius) > HEIGHT or (ball.y - ball.radius) < 0:
ball.yvel *= -1
ball.y += ball.yvel
#code for collision between balls
for ball in balls:
for i in range(len(balls)):
for j in range (i+1, len(balls)):
d = (balls[j].x-balls[i].x)**2 + (balls[j].y-balls[i].y)**2
if d < (balls[j].radius + balls[i].radius)**2:
print("collision")
n = (balls[i].x-balls[j].x, balls[i].y-balls[j].y) #normal vector
un = n/ numpy.sqrt((balls[i].x-balls[j].x)**2 + (balls[i].y-balls[j].y)**2) #unit normal vector
ut = numpy.array((-un[1],un[0])) #unit tangent vector
u1n = numpy.vdot(un, (balls[j].xvel, balls[j].yvel)) #initial velocity in normal direction for first ball
v1t = numpy.vdot(ut, (balls[j].xvel, balls[j].yvel)) #initial velocity in tangent direction (remains unchanged)
u2n = numpy.vdot(un, (balls[i].xvel, balls[i].yvel)) #second ball
v2t = numpy.vdot(ut, (balls[i].xvel, balls[i].yvel))
v1n = u2n
v2n = u1n
v1n *= un
v2n *= un
v1t *= ut
v2t *= ut
v1 = v1n + v1t #ball 1 final velocity
v2 = v2n + v2t #ball 2 final velocity
balls[j].xvel = int(numpy.vdot(v1, (1,0))) #splitting final velocities into x and y components again
balls[j].yvel = int(numpy.vdot(v1, (0,1)))
balls[i].xvel = int(numpy.vdot(v2, (1,0)))
balls[i].yvel = int(numpy.vdot(v2, (0,1)))
redrawGameWindow()
pygame.time.delay(20)
pygame.quit()
In v1 = v1n + v1t, v1n is a vector, but v1t is a scalar.
So, the value v1t gets added to both components of the v1n vector, which causes the error.
You want to add the tangential velocity as a vector, so you should do just like you did for the normal velocities:
v1n *= un
v2n *= un
v1t *= ut
v2t *= ut
Note that using the same variable for two very different things, the vector and its norm, makes your code more difficult to understand and can lead to errors - as it just did. You should probably rename your variables in a more consistent way, in order to make the differences clearer.
Also, ut must be a numpy array, but you made it a tuple. Consider the difference:
With a tuple
>>> t = (3, 4)
>>> 3*t
(3, 4, 3, 4, 3, 4)
With a np.array:
>>> t = np.array((3, 4))
>>> 3*t
array([ 9, 12])
So, you have to change
ut = (-un[1],un[0])
into
ut = numpy.array((-un[1],un[0]))
The problem where the things would stick together is simple, even though you always checked for collision, the time where it would actually recognise it would be when the circles would be past touching but slightly into each other. The circles now bounce and move a little. However they have not moved enough to completely to not be touching, this mean the bounce again and it goes in a loop. I made code that fixes this, but you have to change the bounce function and use your code, as mine is not that perfect, but functional.
import pygame
pygame.init()
import random
import numpy
HEIGHT = 500
WIDTH = 500
NUM_BALLS = 2
#setup
win = pygame.display.set_mode((WIDTH,HEIGHT))
pygame.display.set_caption("Simulation")
class Particle(object):
def __init__(self, x, y , radius):
self.x = x
self.y = y
self.radius = radius
self.xvel = random.randint(1,5)
self.yvel = random.randint(1,5)
self.colideCheck = []
def Draw_circle(self):
pygame.draw.circle(win, (255,0,0), (int(self.x), int(self.y)), self.radius)
def redrawGameWindow():
win.fill((0,0,0))
for ball in balls:
ball.Draw_circle()
pygame.display.update()
def bounce(v1,v2, mass1 = 1, mass2 = 1):
#i Tried my own bouncing mechanism, but doesn't work completly well, add yours here
multi1 = mass1/(mass1+mass2)
multi2 = mass2/(mass1+mass2)
deltaV2 = (multi1*v1[0]-multi2*v2[0],multi1*v1[1]-multi2*v2[1])
deltaV1 = (multi2*v2[0]-multi1*v1[0],multi2*v2[1]-multi1*v1[1])
print("preVelocities:",v1,v2)
return deltaV1,deltaV2
def checkCollide(circ1Cord,circ1Rad,circ2Cord,circ2Rad):
tryDist = circ1Rad+circ2Rad
actualDist = dist(circ1Cord,circ2Cord)
if dist(circ1Cord,circ2Cord) <= tryDist:
return True
return False
def dist(pt1,pt2):
dX = pt1[0]-pt2[0]
dY = pt1[1]-pt2[1]
return (dX**2 + dY**2)**0.5
balls = []
for turn in range(NUM_BALLS):
balls.append(Particle(random.randint(60,WIDTH-60),random.randint(60,HEIGHT-60),60))
run = True
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
for ball in balls:
if (ball.x + ball.radius) <= WIDTH and (ball.x - ball.radius) >= 0:
ball.x += ball.xvel
else:
ball.xvel *= -1
ball.x += ball.xvel
if (ball.y + ball.radius) <= HEIGHT and (ball.y - ball.radius) >= 0:
ball.y += ball.yvel
else:
ball.yvel *= -1
ball.y += ball.yvel
for ball in balls:
for secBallCheck in balls:
if secBallCheck not in ball.colideCheck and ball!= secBallCheck and checkCollide((ball.x,ball.y),ball.radius,(secBallCheck.x,secBallCheck.y),secBallCheck.radius):
print("COLLIDE")
vel1,vel2 = bounce((ball.xvel,ball.yvel),(secBallCheck.xvel,secBallCheck.yvel))
print(vel1,vel2)
ball.xvel = vel1[0]
ball.yvel = vel1[1]
ball.colideCheck.append(secBallCheck)
secBallCheck.xvel = vel2[0]
secBallCheck.yvel = vel2[1]
secBallCheck.colideCheck.append(ball)
elif not checkCollide((ball.x,ball.y),ball.radius,(secBallCheck.x,secBallCheck.y),secBallCheck.radius):
if ball in secBallCheck.colideCheck:
secBallCheck.colideCheck.remove(ball)
ball.colideCheck.remove(secBallCheck)
redrawGameWindow()
pygame.time.delay(20)
pygame.quit()

Categories