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

189 lines
9.3 KiB
Python

import math
from typing import Optional
from physics.CollisionDirection import CollisionDirection
from physics.TickData import TickData
from sprite.DynamicSprite import DynamicSprite
from sprite.PositionScale import PositionScale
from sprite.Sprite import Sprite
from sprite.StaticSprite import StaticSprite
from ui_elements.UiElement import UiElement
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]:
# if str(type(sprite)) == "<class 'level.elements.dynamic.PushableBoxLevelElement.PushableBoxLevelElement'>":
# print(f"Found pushable box")
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. 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
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])
for sprite in sorted_dynamic_sprites:
collides_with = self.attempt_move(tick_data, sprite, colliders, 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]
)
# print('Elements: ', list(filter(lambda spr: str(type(spr)) == "<class 'level.elements.LeverInputLevelElement.LeverInputLevelElement'>", colliders)))
collides_with = self.check_collides(sprite, colliders, tick_data.screen_transform)
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 = (0, 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, tick_data.screen_transform)
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], 0)
collides_with_last = collider
collided[1] = True
return collides_with_last
def check_collides(self, sprite: StaticSprite, colliders: list[StaticSprite], screen_transform: PositionScale) -> list[StaticSprite]:
collides_with = []
for collider in colliders:
if sprite is not collider:
distance = self.calculate_basic_distance(sprite, collider)
if distance > max(self.get_sprite_size_for_distance(sprite, screen_transform),
self.get_sprite_size_for_distance(collider, screen_transform)):
continue
if sprite.collides_with(collider, TOLERANCE):
collides_with.append(collider)
if len(collides_with) >= 2:
break
return collides_with
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])