[Python] physics of a ball on a slope (made of tiles) pygame

Discussão em 'Python' iniciado por Stack, Março 24, 2025.

  1. Stack

    Stack Membro Participativo

    So I have problems with my physics. I'm using a tilemap, with slopes and I do not manage to implement realistic movement to my ball. I use the SAT algorithm to detect collision and send back the penetration depth and the normal vector of the touching tile. the physics is completely broken, with the ball going up the slopes, going through the tiles, and I cannot identify the problem. If someone can help me here's the code i'm using. (the game loop is just drawing ball and tilemap, and ball.moving).

    '''

    def collision_check(vertices, circle_center, circle_radius):
    axes = []
    min_overlap = float('inf')
    collision_normal = None

    for i in range(len(vertices)):
    A = vertices
    B = vertices[(i + 1) % len(vertices)]
    axis = Axis(A, B)
    axes.append(axis)

    for axis in axes:
    min_poly = max_poly = projection(vertices[0], axis)
    for v in vertices:
    proj = projection(v, axis)
    min_poly = min(min_poly, proj)
    max_poly = max(max_poly, proj)

    circle_proj = projection(circle_center, axis)
    min_circle = circle_proj - circle_radius
    max_circle = circle_proj + circle_radius

    # Avoid false collisions
    tolerance = 0.5
    if max_poly < min_circle - tolerance or max_circle + tolerance < min_poly:
    return False # Pas de collision sur cet axe

    overlap = min(max_poly - min_circle, max_circle - min_poly)
    if overlap < min_overlap:
    min_overlap = overlap
    collision_normal = axis

    if collision_normal is not None:
    return collision_normal, min_overlap
    return False


    # ---------------------------
    # Class Spritesheet
    # ---------------------------
    class Spritesheet:
    def __init__(self, filename, tile_size=32, columns=9):
    self.spritesheet = pygame.image.load(filename).convert_alpha()
    self.tile_size = tile_size
    self.columns = columns

    def get_tile(self, index):
    x = (index % self.columns) * self.tile_size
    y = (index // self.columns) * self.tile_size
    return self.spritesheet.subsurface((x, y, self.tile_size, self.tile_size))

    # ---------------------------
    # Class Tile
    # ---------------------------
    class Tile(pygame.sprite.Sprite):
    def __init__(self, tile_index, x, y, spritesheet):
    pygame.sprite.Sprite.__init__(self)
    self.image = spritesheet.get_tile(tile_index)
    self.rect = self.image.get_rect()
    self.rect.x, self.rect.y = x, y
    self.x, self.y = x, y
    self.vertices = []
    self.angle = 0
    self.index = tile_index
    self.attribution()
    self.priority = 0

    def attribution(self):
    x, y = self.rect.x, self.rect.y
    tile_vertices = {
    0: [(x, y), (x + 32, y + 32), (x + 32, y), (x, y + 32)],
    1: [(x, y), (x + 32, y + 32), (x + 32, y), (x, y + 32)],
    2: [(x, y), (x + 32, y + 32), (x + 32, y), (x, y + 32)],
    3: [(x, y), (x + 32, y + 32), (x + 32, y), (x, y + 32)],
    4: [(x, y), (x + 32, y + 32), (x + 32, y), (x, y + 32)],
    5: [(x + 32, y + 32), (x, y + 32), (x + 32, y)],
    6: [(x + 32, y + 32), (x, y + 32), (x + 32, y + 17)],
    7: [(x + 32, y + 32), (x + 32, y), (x, y + 32), (x, y + 16)],
    8: [(x + 32, y + 32), (x, y + 32), (x + 32, y + 23)],
    9: [(x + 32, y + 32), (x + 32, y + 26), (x, y + 32), (x, y + 12)],
    10: [(x + 32, y + 32), (x + 32, y), (x, y + 32), (x, y + 11)],
    11: [(x + 32, y + 32), (x + 32, y), (x + 17, y + 32)],
    12: [(x + 32, y + 32), (x + 32, y), (x, y + 32), (x + 15, y)],
    13: [(x + 32, y + 32), (x + 32, y), (x + 23, y + 32)],
    14: [(x + 32, y + 32), (x + 32, y), (x + 12, y + 32), (x + 22, y)],
    15: [(x + 32, y + 32), (x + 32, y), (x + 10, y), (x, y + 32)],
    16: [(x + 32, y + 32), (x, y), (x, y + 32)],
    17: [(x + 32, y + 32), (x, y + 32), (x, y + 17)],
    18: [(x + 32, y + 32), (x, y), (x, y + 32), (x + 32, y + 16)],
    19: [(x + 32, y + 32), (x, y + 32), (x + 32, y + 23)],
    20: [(x + 32, y + 32), (x + 32, y + 12), (x, y + 32), (x, y + 22)],
    21: [(x + 32, y + 32), (x, y), (x, y + 32), (x + 32, y + 11)],
    22: [(x, y), (x, y + 32), (x + 15, y + 32)],
    23: [(x + 32, y + 32), (x, y), (x, y + 32), (x + 17, y)],
    24: [(x, y + 32), (x, y), (x + 10, y + 32)],
    25: [(x, y), (x, y + 32), (x + 11, y), (x + 21, y + 32)],
    26: [(x + 32, y + 32), (x, y), (x + 10, y), (x + 23, y)],
    }
    self.vertices = tile_vertices[self.index]
    if self.vertices != [(x, y), (x + 32, y + 32), (x + 32, y), (x, y + 32)] :
    self.priority = 1
    else :
    self.priority = 0

    # ---------------------------
    # Class Tilemap
    # ---------------------------
    class Tilemap:
    def __init__(self, filename, spritesheet):
    self.tile_size = 32
    self.tiles = self.load_tiles(filename, spritesheet)

    def read_csv(self, filename):
    map_data = []
    with open(filename) as data:
    reader = csv.reader(data, delimiter=',')
    for row in reader:
    map_data.append(list(row))
    return map_data

    def load_tiles(self, filename, spritesheet):
    tiles = []
    tile_map = self.read_csv(filename)
    y = 0
    for row in tile_map:
    x = 0
    for tile in row:
    tile_index = int(tile)
    if tile_index != -1:
    tiles.append(Tile(tile_index, x * self.tile_size, y * self.tile_size, spritesheet))
    x += 1
    y += 1
    return tiles

    def draw(self, surface):
    for tile in self.tiles:
    surface.blit(tile.image, (tile.rect.x, tile.rect.y))

    # ---------------------------
    # Class Ball
    # ---------------------------
    class Ball:
    def __init__(self, pos, radius, mass, retention, velocity, id, friction):
    self.pos = pos
    self.radius = radius
    self.mass = mass
    self.retention = retention
    self.velocity = velocity
    self.id = id
    self.friction = friction

    def draw(self):
    pygame.draw.circle(screen, (255, 255, 255), (int(self.pos.x), int(self.pos.y)), self.radius)

    def bounce(self, normal_vector):
    normal_velocity_component = self.velocity.dot(normal_vector) * normal_vector
    # Inversion and attenuation of the normal componante
    reflected_velocity = normal_velocity_component * -(1+self.retention)
    if reflected_velocity.length() < 0.2 : #Minimum threshold to avoid trembling
    return pygame.Vector2(0,0)
    return reflected_velocity

    def weight(self):
    gravity = pygame.Vector2(0, 0.2) # gravity
    return gravity * self.mass

    def frictions(self,normal_vector):
    if self.is_on_valid_surface(normal_vector):
    tangent_vector = pygame.Vector2(-normal_vector.y, normal_vector.x) # Vecteur tangent au sol
    # friction force
    friction_force = -self.velocity.dot(tangent_vector) * self.friction * tangent_vector
    # do not let the frction force invert the movement
    if friction_force.length() > self.velocity.length():
    friction_force = -self.velocity
    return friction_force
    return pygame.Vector2(0, 0)

    def is_on_valid_surface(self,normal_vector):
    slope_angle = self.getting_slope_angle(normal_vector)
    return abs(slope_angle) < 75 # (No frictions against a wall)

    def moving(self, tilemap):
    weight = self.weight()
    collision_info = self.handle_collision(tilemap) #THE Dictionnary CONTAINING INFOS ABOUT THE TILES THAT ARE TOUCHING
    if collision_info:
    for tile_key in collision_info.keys():
    normal_vector = collision_info[tile_key][0]
    tangent_vector = pygame.Vector2(-normal_vector.y, normal_vector.x) # tangeant vector to the normal
    penetration = collision_info[tile_key][1]
    self.repositioning(normal_vector, penetration)
    # forces decomposition
    normal_force = weight.dot(normal_vector) * normal_vector
    parallel_force = weight.dot(tangent_vector) * tangent_vector
    self.velocity += -parallel_force + normal_force
    self.velocity += self.bounce(normal_vector)
    self.velocity += -self.frictions(normal_vector)

    if abs(self.velocity.y) < 0.5:
    self.velocity.y -= 0.5 * normal_vector.y
    print("Collision detected",tile_key, "Current speed :",self.velocity)
    else:
    self.velocity += weight

    if self.velocity.length() < 0.1: # Seuil arbitraire, ajustable
    self.velocity = pygame.Vector2(0, 0)

    self.pos += self.velocity


    def repositioning(self, normal_vector, penetration):
    #to reposition the ball, the epsilon is to make sure the ball isn't stuck
    epsilon = 0.1
    self.pos += normal_vector * (penetration + epsilon)

    def getting_slope_angle(self, normal_vector):
    if normal_vector.x == 0:
    return 90 if normal_vector.y > 0 else -90
    return math.degrees(math.atan2(normal_vector.y, normal_vector.x))

    def handle_collision(self, tilemap): #Works as intended
    #Return 2 tiles max which are touching the ball, with a priority for slope tiles
    collision_tiles = {}
    for tile in tilemap.tiles:
    collision = collision_check(tile.vertices, (self.pos.x, self.pos.y), self.radius) # collision = (normal_vector, overlap)
    if collision:
    normal_vector = pygame.Vector2(collision[0]).normalize() # Normalisation ici
    overlap = collision[1]
    collision_tiles[tile] = (normal_vector, overlap)
    if len(collision_tiles) == 2: #If a tile touches, but without impacting it (Ex : the angle at several tiles' intersection)
    for key in collision_tiles.keys():
    if key.priority == 0 :#If we find a less important tile
    key_to_delete = key
    collision_tiles.pop(key_to_delete) #DELETE THE LESS IMPORTANT TILE
    collision_tiles[tile] = (normal_vector, overlap)

    if len(collision_tiles) == 1:
    return collision_tiles #Output : {Tile : (normal_vector, overlap)}
    elif len(collision_tiles) == 2 :
    list_keys = list(collision_tiles.keys())
    if collision_tiles[list_keys[0]][0] == collision_tiles[list_keys[1]][0] : #Check if the two tiles have the same normal vector
    collision_tiles.pop(list_keys[1]) #We only need one
    return collision_tiles
    return False


    '''

    Continue reading...

Compartilhe esta Página