from typing import Optional from physics.CollisionDirection import CollisionDirection from physics.TickData import TickData from sprite.BoundingBox import BoundingBox from sprite.DynamicSprite import DynamicSprite from sprite.Sprite import Sprite from sprite.StaticSprite import StaticSprite from ui_elements.UiElement import UiElement MAX_COLLIDER_DISTANCE = 50 MAX_COLLIDER_CHECK_SPRITES = 10 MOTION_STEPS = 10 TOLERANCE = 1 class PhysicsElementsHandler: def __init__(self): self.collision_callbacks = [] def add_collision_callback(self, callback): self.collision_callbacks.append(callback) def remove_collision_callback(self, callback): self.collision_callbacks.remove(callback) def tick(self, tick_data: TickData, layers: dict[str, list[UiElement]]): sprites = [] for layer in layers: for sprite in layers[layer]: sprite.tick(tick_data) if isinstance(sprite, Sprite): sprites.append(sprite) # handle motion and collisions. To do this: # 1. Find all sprites that have collision enabled and store them in a list # 2. Create a list of all sprites that are dynamic sprites # 3. Sort the sprites by their y position # 4. Find the MAX_COLLIDER_CHECK_SPRITES sprites that are closest to the sprite that are colliders but not the # sprite itself # 4.1 Filter out all that are further than MAX_COLLIDER_DISTANCE # 5. For each sprite: # 5.1. Divide the motion into MOTION_STEPS steps # 5.2. For each step: # 5.2.1. Check if the sprite collides with any other sprite from the list of colliders generated in step 4 # 5.2.2. If it does, move the sprite back to the previous position and stop the motion # 5.2.3. If it doesn't, move the sprite to the new position colliders = [sprite for sprite in sprites if isinstance(sprite, StaticSprite) and (sprite.is_collider or sprite.register_collisions)] for collider in colliders: collider.reset_collides_with() dynamic_sprites = [sprite for sprite in sprites if isinstance(sprite, DynamicSprite)] sorted_dynamic_sprites = sorted(dynamic_sprites, key=lambda spr: spr.position_scale.position[1]) closest_sprites: dict[UiElement, list[tuple[int, StaticSprite]]] = {} buffered_bounding_boxes: dict[UiElement, BoundingBox] = {} for collider in colliders: buffered_bounding_boxes[collider] = collider.get_bounding_box() for sprite in sorted_dynamic_sprites: closest_sprites[sprite] = [] current_closest_sprites = closest_sprites[sprite] if sprite not in buffered_bounding_boxes: buffered_bounding_boxes[sprite] = sprite.get_bounding_box() for collider in colliders: if collider is sprite: continue distance = int(buffered_bounding_boxes[collider].distance(buffered_bounding_boxes[sprite])) if len(current_closest_sprites) < MAX_COLLIDER_CHECK_SPRITES: current_closest_sprites.append((distance, collider)) else: max_index = -1 max_distance = -1 for i in range(len(current_closest_sprites)): if distance < current_closest_sprites[i][0]: if current_closest_sprites[i][0] > max_distance: max_distance = current_closest_sprites[i][0] max_index = i if max_index != -1: current_closest_sprites[max_index] = (distance, collider) for sprite in closest_sprites: closest_sprites[sprite] = [closest_sprite for closest_sprite in closest_sprites[sprite] if closest_sprite[0] < MAX_COLLIDER_DISTANCE] # set visible false for all those that are not in the closest_sprites list # for collider in colliders: # found = False # for sprite in sorted_dynamic_sprites: # for _, c in closest_sprites[sprite]: # if c is collider: # found = True # break # if found: # break # collider.visible = not found # for sprite in sorted_dynamic_sprites: # sprite.visible = True for sprite in sorted_dynamic_sprites: collides_with = self.attempt_move(tick_data, sprite, [collider for _, collider in closest_sprites[sprite]], MOTION_STEPS) if collides_with is not None: for callback in self.collision_callbacks: callback(sprite, collides_with) def attempt_move(self, tick_data: TickData, sprite: DynamicSprite, colliders: list[StaticSprite], motion_steps: int) -> Optional[StaticSprite]: total_motion = sprite.motion motion_step = ((total_motion[0] * tick_data.dt) / motion_steps, (total_motion[1] * tick_data.dt) / motion_steps) collides_with_last = None collided = [False, False] for i in range(motion_steps): if not collided[0]: sprite.position_scale.position = ( sprite.position_scale.position[0] + motion_step[0], sprite.position_scale.position[1] ) collides_with = self.check_collides(sprite, colliders) for collider in collides_with: if collider is not None: if sprite.is_collider and collider.is_collider: if sprite.motion[0] > 0: if sprite.register_collisions: sprite.add_collides_with( CollisionDirection(CollisionDirection.RIGHT, sprite, collider)) if collider.register_collisions: collider.add_collides_with( CollisionDirection(CollisionDirection.LEFT, collider, sprite)) if sprite.motion[0] < 0: if sprite.register_collisions: sprite.add_collides_with( CollisionDirection(CollisionDirection.LEFT, sprite, collider)) if collider.register_collisions: collider.add_collides_with( CollisionDirection(CollisionDirection.RIGHT, collider, sprite)) else: if sprite.register_collisions: sprite.add_collides_with( CollisionDirection(CollisionDirection.INSIDE, sprite, collider)) if collider.register_collisions: collider.add_collides_with( CollisionDirection(CollisionDirection.INSIDE, collider, sprite)) if collider.is_collider: sprite.position_scale.position = ( sprite.position_scale.position[0] - motion_step[0], sprite.position_scale.position[1] ) sprite.motion = (sprite.motion[0] * sprite.bounce_factor, sprite.motion[1]) collides_with_last = collider collided[0] = True if not collided[1]: sprite.position_scale.position = ( sprite.position_scale.position[0], sprite.position_scale.position[1] + motion_step[1] ) collides_with = self.check_collides(sprite, colliders) for collider in collides_with: if collider is not None: if sprite.is_collider and collider.is_collider: if sprite.motion[1] > 0: if sprite.register_collisions: sprite.add_collides_with( CollisionDirection(CollisionDirection.BOTTOM, sprite, collider)) if collider.register_collisions: collider.add_collides_with( CollisionDirection(CollisionDirection.TOP, collider, sprite)) if sprite.motion[1] < 0: if sprite.register_collisions: sprite.add_collides_with( CollisionDirection(CollisionDirection.TOP, sprite, collider)) if collider.register_collisions: collider.add_collides_with( CollisionDirection(CollisionDirection.BOTTOM, collider, sprite)) else: if sprite.register_collisions: sprite.add_collides_with( CollisionDirection(CollisionDirection.INSIDE, sprite, collider)) if collider.register_collisions: collider.add_collides_with( CollisionDirection(CollisionDirection.INSIDE, collider, sprite)) if collider.is_collider: sprite.position_scale.position = ( sprite.position_scale.position[0], sprite.position_scale.position[1] - motion_step[1] ) sprite.motion = (sprite.motion[0], sprite.motion[1] * sprite.bounce_factor) collides_with_last = collider collided[1] = True return collides_with_last def check_collides(self, sprite: StaticSprite, colliders: list[StaticSprite]) -> list[StaticSprite]: collides_with = [] for collider in colliders: if sprite is not collider: if sprite.collides_with(collider, TOLERANCE): collides_with.append(collider) if len(collides_with) > 5: break return collides_with