so let me preface this by saying that I am only moderately experienced with Python and pygame. With that being said, I wanted to implement a collision system using SAT (Separating Axis Thoerem) so my player could collide with rotated rectangles. I have created code that will take the local coordinates of the rectangle and apply a rotation matrix so I can get the world coordinates of the rotated rectangle. Here is that code, it works pretty well.
def getRotation(angle):
angle = np.radians(angle)
return np.array(
[
[np.cos(angle), -np.sin(angle), 0],
[np.sin(angle), np.cos(angle), 0],
[0, 0, 1],
]
)
def getTranslation(Tx, Ty):
return np.array([[1, 0, Tx], [0, 1, Ty], [0, 0, 1]])
def getWorldCoord(points, Tx, Ty, angle):
transMat = getTranslation(Tx, Ty)
rotMat = getRotation(angle)
A = transMat @ rotMat @ np.linalg.inv(transMat)
for i in range(points.shape[0]):
A.dot(points[i, :])
points_world = A.dot(points.T)
return points_world
Now, my issue is implementing SAT and also getting the player to stop when collided (that is, finding the Minimum translation vector or MTV). And this is the part I am struggling with. The collision bool turns True when it gets close to the rectangle but not hitting. The MTV also has this odd behavior where it sort of bounces off the rectangle very unnaturally. I wanted it in such a way that the rotated rectangle behaves like a solid wall. Any help on this would be greatly appreciated, I've been at this for almost a week now.
Here is the code for my SAT collision system.
def normalize(v):
norm = np.sqrt(v[0] ** 2 + v[1] ** 2)
return (v[0] / norm, v[1] / norm)
def arrayToVector(p):
vecs = []
for i in range(p.shape[0]):
vecs.append((p[0][i], p[1][i]))
return vecs
def dot(a, b):
return a[0] * b[0] + a[1] * b[1]
def orthogonal(v):
return (v[1], -v[0])
def edge_direction(p0, p1):
return (p1[0] - p0[0], p1[1] - p1[0])
def vertices_to_edges(vertices):
return [
edge_direction(vertices[i], vertices[(i + 1) % len(vertices)])
for i in range(len(vertices))
]
def project(vertices, axis):
dots = [dot(vertex, axis) for vertex in vertices]
return [min(dots), max(dots)]
def contains(n, range_):
a = range_[0]
b = range_[1]
if b < a:
a = range_[1]
b = range_[0]
return (n >= a) and (n <= b)
def overlap(a, b):
if contains(a[0], b) or contains(a[1], b) or contains(b[0], a) or contains(b[1], a):
return True
return False
# region (SAT_coll_fxn)
# def SAT_collision(vertices_a, vertices_b):
# edges_a = vertices_to_edges(vertices_a)
# edges_b = vertices_to_edges(vertices_b)
# proj_a = [float("inf"), float("-inf")]
# proj_b = [float("inf"), float("-inf")]
# overlap_val = float("inf")
# edges = edges_a + edges_b
# axes = [normalize(orthogonal(edge)) for edge in edges]
# for i in range(len(axes)):
# proj_a = project(vertices_a, axes[i])
# proj_b = project(vertices_b, axes[i])
# overlapping = overlap(proj_a, proj_b)
# overlap_val = min(proj_a[1], proj_b[1]) - max(proj_a[0], proj_b[0])
# if not overlapping:
# return False
# return True
# endregion
class SAT_Collision:
def __init__(self, poly1, poly2):
self.poly1 = poly1
self.poly2 = poly2
self.collide = False
self.overlap_val = float("inf")
def collision_dect(self):
edges_a = vertices_to_edges(self.poly1)
edges_b = vertices_to_edges(self.poly2)
edges = edges_a + edges_b
axes = [normalize(orthogonal(edge)) for edge in edges]
for i in range(len(axes)):
proj_a = project(self.poly1, axes[i])
proj_b = project(self.poly2, axes[i])
overlapping = overlap(proj_a, proj_b)
self.overlap_val = min(proj_a[1], proj_b[1]) - max(proj_a[0], proj_b[0])
if not overlapping:
return False
d = vec()
return True
Here is my main loop. I have my MTV code commented out temporarily.
rect1 = Rectangle(10, 10, 50, 30)
rect2 = pg.Rect(100, 100, 10, 100)
rectangle_dragging = False
w = rect2.width
h = rect2.height
Tx = rect2.left
Ty = rect2.top
x0 = Tx
y0 = Ty
angle = 333.44
points = np.array([[x0, y0, 1], [x0 + w, y0, 1], [x0, y0 + h, 1], [x0 + w, y0 + h, 1]])
rect_coor = getWorldCoord(points, Tx, Ty, angle)
rect_coor_vec = arrayToVector(rect_coor)
rect_center = rect2.centerx
line_coor = [(200, 200), (100, 100)]
collision_font = pg.font.Font(None, 30)
collision_message = "collided!"
collision_render = collision_font.render(collision_message, True, BLACK)
collision_text_rect = collision_render.get_rect()
collision_text_rect.center = (WIDTH - 100, HEIGHT - 100)
# - mainloop -
clock = pg.time.Clock()
running = True
while running:
screen.fill(LIGHTGREY)
surf = pg.Surface(rect2.size).convert_alpha()
# - events -
for event in pg.event.get():
if event.type == pg.QUIT:
running = False
elif event.type == pg.MOUSEBUTTONDOWN:
if event.button == 1:
if rect1.rect.collidepoint(event.pos):
rectangle_dragging = True
mouse_x, mouse_y = event.pos
offset_x = rect1.rect.x - mouse_x
offset_y = rect1.rect.y - mouse_y
elif event.type == pg.MOUSEBUTTONUP:
if event.button == 1:
rectangle_dragging = False
elif event.type == pg.MOUSEMOTION:
if rectangle_dragging:
mouse_x, mouse_y = event.pos
rect1.rect.x = mouse_x + offset_x
rect1.rect.y = mouse_y + offset_y
# - updates (without draws) -
player_coor = [
(rect1.rect.left, rect1.rect.top),
(rect1.rect.right, rect1.rect.top),
(rect1.rect.left, rect1.rect.bottom),
(rect1.rect.right, rect1.rect.bottom),
]
collision_ob = SAT_Collision(player_coor, rect_coor_vec)
collision_chk = collision_ob.collision_dect()
overlap_val = collision_ob.overlap_val
if collision_chk:
screen.blit(collision_render, collision_text_rect)
# d = vec(rect2.left - rect1.rect.left, rect2.top - rect1.rect.top)
# s = np.sqrt(d.x * d.x + d.y * d.y)
# rect1.rect.left -= 10 * d.x / s
# rect1.rect.top -= 10 * d.y / s
# empty
# - draws (without updates) -
pg.draw.rect(surf, (RED), rect2)
rotSurf = pg.transform.rotate(surf, angle)
screen.blit(rotSurf, (Tx, Ty))
pg.draw.rect(screen, BLUE, rect1)
# pg.draw.line(screen, RED, line_coor[1], line_coor[0], 3)
# pg.draw.rect(screen, RED, rect2)
pg.display.flip()
rect_center = rect2.center
rect_coor = getWorldCoord(points, Tx, Ty, angle)
# - constant game speed / FPS -
clock.tick(FPS)
# - end -
pg.quit()
Here is an image of the results. Note that this is a test environment just to try out the code. I didn't want to alter the real game code.
Thanks!