import random 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]) skip_sprites = [] for sprite in sorted_dynamic_sprites: if sprite.last_effective_motion[1] == 0 and sprite.last_effective_motion[0] == 0 \ and random.randint(0, 100) > 50: skip_sprites.append(sprite) continue sorted_dynamic_sprites = [sprite for sprite in sorted_dynamic_sprites if sprite not in skip_sprites] 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] effective_motion = [0, 0] 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) if len(collides_with) == 0: effective_motion[0] += motion_step[0] 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) if len(collides_with) == 0: effective_motion[1] += motion_step[1] 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 sprite.last_effective_motion = (sprite.motion[0], sprite.motion[1]) 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