sep-pm-platformer/project/physics/PhysicsElementsHandler.py

241 lines
11 KiB
Python
Raw Normal View History

2023-03-27 15:17:01 +02:00
import math
import time
2023-03-26 10:40:17 +02:00
from typing import Optional
2023-03-25 15:41:32 +01:00
2023-03-26 17:01:28 +02:00
from physics.CollisionDirection import CollisionDirection
from physics.TickData import TickData
from sprite.BoundingBox import BoundingBox
2023-03-25 15:41:32 +01:00
from sprite.DynamicSprite import DynamicSprite
from sprite.PositionScale import PositionScale
2023-03-25 15:41:32 +01:00
from sprite.Sprite import Sprite
from sprite.StaticSprite import StaticSprite
2023-03-26 09:51:11 +02:00
from ui_elements.UiElement import UiElement
2023-03-25 15:41:32 +01:00
MAX_COLLIDER_CHECK_SPRITES = 15
2023-03-25 15:41:32 +01:00
MOTION_STEPS = 10
2023-03-25 17:18:43 +01:00
TOLERANCE = 1
2023-03-25 15:41:32 +01:00
class PhysicsElementsHandler:
def __init__(self):
2023-03-26 10:40:17 +02:00
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)
2023-03-25 15:41:32 +01:00
def tick(self, tick_data: TickData, layers: dict[str, list[UiElement]]):
sprites = []
2023-03-26 09:51:11 +02:00
for layer in layers:
for sprite in layers[layer]:
sprite.tick(tick_data)
if isinstance(sprite, Sprite):
sprites.append(sprite)
2023-03-25 15:41:32 +01:00
# 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
# 3. Find the MAX_COLLIDER_CHECK_SPRITES sprites that are closest to the sprite that are colliders but not the
# sprite itself
2023-03-25 15:41:32 +01:00
# 4. For each sprite:
# 4.1. Divide the motion into MOTION_STEPS steps
# 4.2. For each step:
# 4.2.1. Check if the sprite collides with any other sprite
# 4.2.2. If it does, move the sprite back to the previous position and stop the motion
# 4.2.3. If it doesn't, move the sprite to the new position
2023-03-27 14:38:52 +02:00
colliders = [sprite for sprite in sprites
if isinstance(sprite, StaticSprite) and (sprite.is_collider or sprite.register_collisions)]
2023-03-26 17:01:28 +02:00
for collider in colliders:
collider.reset_collides_with()
2023-03-25 15:41:32 +01:00
2023-03-26 09:51:11 +02:00
dynamic_sprites = [sprite for sprite in sprites if isinstance(sprite, DynamicSprite)]
2023-03-25 15:41:32 +01:00
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()
2023-03-25 15:41:32 +01:00
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)
# 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)
2023-03-26 10:40:17 +02:00
if collides_with is not None:
for callback in self.collision_callbacks:
callback(sprite, collides_with)
2023-03-25 17:18:43 +01:00
def attempt_move(self, tick_data: TickData, sprite: DynamicSprite, colliders: list[StaticSprite],
2023-03-26 10:40:17 +02:00
motion_steps: int) -> Optional[StaticSprite]:
2023-03-25 17:18:43 +01:00
total_motion = sprite.motion
motion_step = ((total_motion[0] * tick_data.dt) / motion_steps, (total_motion[1] * tick_data.dt) / motion_steps)
2023-03-25 17:18:43 +01:00
2023-03-26 17:01:28 +02:00
collides_with_last = None
collided = [False, False]
2023-03-25 18:14:47 +01:00
2023-03-26 17:01:28 +02:00
for i in range(motion_steps):
if not collided[0]:
2023-03-25 18:14:47 +01:00
sprite.position_scale.position = (
2023-03-26 17:01:28 +02:00
sprite.position_scale.position[0] + motion_step[0],
2023-03-25 18:14:47 +01:00
sprite.position_scale.position[1]
)
collides_with = self.check_collides(sprite, colliders)
2023-03-27 14:38:52 +02:00
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]
)
2023-03-27 18:52:49 +02:00
sprite.motion = (sprite.motion[0] * sprite.bounce_factor, sprite.motion[1])
2023-03-27 14:38:52 +02:00
collides_with_last = collider
collided[0] = True
2023-03-26 17:01:28 +02:00
if not collided[1]:
2023-03-25 18:14:47 +01:00
sprite.position_scale.position = (
sprite.position_scale.position[0],
2023-03-26 17:01:28 +02:00
sprite.position_scale.position[1] + motion_step[1]
2023-03-25 18:14:47 +01:00
)
collides_with = self.check_collides(sprite, colliders)
2023-03-27 14:38:52 +02:00
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]
)
2023-03-27 18:52:49 +02:00
sprite.motion = (sprite.motion[0], sprite.motion[1] * sprite.bounce_factor)
2023-03-27 14:38:52 +02:00
collides_with_last = collider
collided[1] = True
2023-03-26 17:01:28 +02:00
return collides_with_last
2023-03-25 15:41:32 +01:00
def check_collides(self, sprite: StaticSprite, colliders: list[StaticSprite]) -> \
2023-03-27 18:52:49 +02:00
list[StaticSprite]:
2023-03-27 14:38:52 +02:00
collides_with = []
2023-03-25 18:14:47 +01:00
for collider in colliders:
2023-03-26 17:01:28 +02:00
if sprite is not collider:
2023-03-27 15:17:01 +02:00
distance = self.calculate_basic_distance(sprite, collider)
2023-03-28 17:17:51 +02:00
if distance > 50:
2023-03-27 15:17:01 +02:00
continue
2023-03-26 17:01:28 +02:00
if sprite.collides_with(collider, TOLERANCE):
2023-03-27 14:38:52 +02:00
collides_with.append(collider)
2023-03-27 18:52:49 +02:00
if len(collides_with) > 5:
2023-03-27 14:47:12 +02:00
break
2023-03-25 18:14:47 +01:00
2023-03-27 14:38:52 +02:00
return collides_with
2023-03-27 15:17:01 +02:00
def calculate_basic_distance(self, sprite1: StaticSprite, sprite2: StaticSprite) -> float:
return math.sqrt((sprite1.position_scale.position[0] - sprite2.position_scale.position[0]) ** 2 +
(sprite1.position_scale.position[1] - sprite2.position_scale.position[1]) ** 2)
def get_sprite_size_for_distance(self, ui_element: UiElement, screen_transform: PositionScale) -> int:
image = ui_element.render_sprite_image()
if image is None:
return 0
return max(image.get_width() * ui_element.position_scale.scale[0] * screen_transform.scale[0],
image.get_height() * ui_element.position_scale.scale[1] * screen_transform.scale[1])