Optimize PhysicsElementsHandler.py by checking collisions only with closest sprites.

Improve UiElement.py to cache and reuse scaled images for better performance.
main
Yan Wittmann 2023-03-28 19:35:42 +02:00
parent 0778fef354
commit 9dfc487e9a
7 changed files with 137 additions and 144 deletions

View File

@ -11,7 +11,7 @@
#,#,S,,,,,,,,,,,,,,,,,,,,,#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,# #,#,S,,,,,,,,,,,,,,,,,,,,,#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
#,#,S,,,,,,,,,,,,,,,,,,,,,#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,# #,#,S,,,,,,,,,,,,,,,,,,,,,#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
#,#,,,,,,,,,,,,,,,,,,,,,,G,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,# #,#,,,,,,,,,,,,,,,,,,,,,,G,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,# #,,,,,,,,,,,,,,,M,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,# #,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
#,,,,,,,,,,,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,# #,,,,,,,,,,,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
#,L,,,,,,,,,,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,# #,L,,,,,,,,,,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#

1 # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
11 # # S # #
12 # # S # #
13 # # G #
14 # M #
15 # #
16 # + + + + + + + + + + + + + + + #
17 # L # # # # # # # # # # # # # # # #

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View File

@ -23,10 +23,12 @@ WIDTH = 12 * 71 * 1.5
HEIGHT = 12 * 40 * 1.5 HEIGHT = 12 * 40 * 1.5
# Background to test for level design # Background to test for level design
# test_background_castle = pygame.transform.scale(pygame.image.load('data/sprites/castle_bg.png'), (WIDTH, HEIGHT)) test_background_castle = pygame.transform.scale(pygame.image.load('data/sprites/castle_bg.png'), (WIDTH, HEIGHT))
# test_background_cave = pygame.transform.scale(pygame.image.load('data/sprites/cave_bg.png'), (WIDTH, HEIGHT)) # test_background_cave = pygame.transform.scale(pygame.image.load('data/sprites/cave_bg.png'), (WIDTH, HEIGHT))
# test_background_tutorial = pygame.transform.scale(pygame.image.load('data/sprites/tutorial_bg.png'), (WIDTH, HEIGHT)) # test_background_tutorial = pygame.transform.scale(pygame.image.load('data/sprites/tutorial_bg.png'), (WIDTH, HEIGHT))
def apply_frame_rate(number: float): def apply_frame_rate(number: float):
""" """
this function calculates a factor that will be multiplied with the this function calculates a factor that will be multiplied with the
@ -118,15 +120,15 @@ elif what_to_run == 'level':
screen_transform = PositionScale((0, 0), (1.5, 1.5)) screen_transform = PositionScale((0, 0), (1.5, 1.5))
pygame.init() pygame.init()
# optimize performance
screen = pygame.display.set_mode((12 * ConstantsParser.CONFIG.level_size[0] * screen_transform.scale[0], screen = pygame.display.set_mode((12 * ConstantsParser.CONFIG.level_size[0] * screen_transform.scale[0],
12 * ConstantsParser.CONFIG.level_size[1] * screen_transform.scale[1]), 12 * ConstantsParser.CONFIG.level_size[1] * screen_transform.scale[1]),
flags=pygame.HWSURFACE | pygame.DOUBLEBUF, flags=pygame.HWSURFACE | pygame.DOUBLEBUF,
vsync=1) vsync=1,
depth=1)
pygame.display.set_caption("PM GAME") pygame.display.set_caption("PM GAME")
clock = pygame.time.Clock() clock = pygame.time.Clock()
frame_rate = 30 frame_rate = 120
spritesheet_manager = SpritesheetManager("data/sprites", "data/sprites/sprites.json") spritesheet_manager = SpritesheetManager("data/sprites", "data/sprites/sprites.json")
sprite_manager = SpriteManager() sprite_manager = SpriteManager()
@ -145,6 +147,9 @@ elif what_to_run == 'level':
calculated_frame_rate_text.position_scale.scale = (0.3, 0.3) calculated_frame_rate_text.position_scale.scale = (0.3, 0.3)
sprite_manager.add_ui_element(DrawLayers.UI, calculated_frame_rate_text) sprite_manager.add_ui_element(DrawLayers.UI, calculated_frame_rate_text)
left_sprite = None
right_sprite = None
while True: while True:
clock.tick(frame_rate) clock.tick(frame_rate)
@ -159,92 +164,23 @@ elif what_to_run == 'level':
pygame.quit() pygame.quit()
quit() quit()
for event in click_events:
for layer in sprite_manager.layers:
for sprite in sprite_manager.layers[layer]:
if sprite.get_bounding_box().contains_point(event.world_position):
if event.is_click_down(ClickEvent.CLICK_LEFT):
left_sprite = sprite
if event.is_click_down(ClickEvent.CLICK_RIGHT):
right_sprite = sprite
if left_sprite is not None and right_sprite is not None:
print(left_sprite.get_bounding_box().distance(right_sprite.get_bounding_box()))
left_sprite = None
right_sprite = None
screen.fill((0, 0, 0)) screen.fill((0, 0, 0))
# Playground to test background on any level # Playground to test background on any level
# screen.blit(test_background_castle, (0, 0)) screen.blit(test_background_castle, (0, 0))
sprite_manager.tick(TickData(apply_frame_rate(1), pygame_events, key_manager, click_events, screen_transform))
sprite_manager.draw(screen, screen_transform)
pygame.display.update()
elif what_to_run == 'physics':
screen_transform = PositionScale((0, 0), (4, 4))
pygame.init()
screen = pygame.display.set_mode((600, 500))
pygame.display.set_caption("PM GAME")
clock = pygame.time.Clock()
frame_rate = 59.52
spritesheet_manager = SpritesheetManager("data/sprites", "data/sprites/sprites.json")
sprite_manager = SpriteManager()
key_manager = KeyManager()
# test_1_sprite = DynamicSprite(spritesheet_manager.get_sheet("test_1"))
# test_1_sprite.position_scale = PositionScale((10, -100), (1, 1))
# sprite_manager.add_ui_element(DrawLayers.OBJECTS, test_1_sprite)
# test_3_sprite = DynamicSprite(spritesheet_manager.get_sheet("test_1"))
# test_3_sprite.position_scale = PositionScale((130, 100), (1, 1))
# test_3_sprite.motion = (-9, -10)
# sprite_manager.add_ui_element(DrawLayers.OBJECTS, test_3_sprite)
# test_2_sprite = StaticSprite(spritesheet_manager.get_sheet("test_1"))
# test_2_sprite.position_scale = PositionScale((10, 80), (1, 1))
# sprite_manager.add_ui_element(DrawLayers.OBJECTS, test_2_sprite)
for x in range(0, 8):
floor_sprite = StaticSprite(spritesheet_manager.get_sheet("test_1"))
floor_sprite.position_scale = PositionScale((x * 16, 100), (1, 1))
sprite_manager.add_ui_element(DrawLayers.OBJECTS, floor_sprite)
for x in range(0, 8):
floor_sprite = StaticSprite(spritesheet_manager.get_sheet("test_1"))
floor_sprite.position_scale = PositionScale((x * 16, 0), (1, 1))
sprite_manager.add_ui_element(DrawLayers.OBJECTS, floor_sprite)
for x in range(0, 6):
floor_sprite = StaticSprite(spritesheet_manager.get_sheet("test_1"))
floor_sprite.position_scale = PositionScale((0, x * 16), (1, 1))
sprite_manager.add_ui_element(DrawLayers.OBJECTS, floor_sprite)
for x in range(0, 6):
floor_sprite = StaticSprite(spritesheet_manager.get_sheet("test_1"))
floor_sprite.position_scale = PositionScale((130, x * 16), (1, 1))
sprite_manager.add_ui_element(DrawLayers.OBJECTS, floor_sprite)
ghost_character = PlayerSprite(spritesheet_manager.get_sheet("ghost_character"))
ghost_character.position_scale = PositionScale((90, 50), (1, 1))
sprite_manager.add_ui_element(DrawLayers.OBJECTS, ghost_character)
text_1 = TextLabel("Frame: 0", 2, 110, 50, alignment="left")
text_1.position_scale.scale = (0.1, 0.1)
sprite_manager.add_ui_element(DrawLayers.UI, text_1)
ghost_character.debug_label = text_1
frame_counter = 0
while True:
clock.tick(frame_rate)
skip = False
pygame_events: list[pygame.event.Event] = pygame.event.get()
key_manager.update_key_events(pygame_events)
click_events: list[ClickEvent] = ClickEvent.create_events(pygame_events, screen_transform)
for event in pygame_events:
if event.type == pygame.QUIT:
pygame.quit()
quit()
if key_manager.is_keymap_down(KeyManager.KEY_RIGHT):
skip = False
if skip:
continue
frame_counter += 1
# text_1.set_text(f"Frame: {frame_counter}")
screen.fill((0, 0, 0))
sprite_manager.tick(TickData(apply_frame_rate(1), pygame_events, key_manager, click_events, screen_transform)) sprite_manager.tick(TickData(apply_frame_rate(1), pygame_events, key_manager, click_events, screen_transform))
sprite_manager.draw(screen, screen_transform) sprite_manager.draw(screen, screen_transform)
@ -283,41 +219,3 @@ elif what_to_run == 'textlabel':
test3.draw(screen, screen_transform) test3.draw(screen, screen_transform)
pygame.display.update() pygame.display.update()
elif what_to_run == 'sprite':
screen_transform = PositionScale((0, 0), (4, 4))
pygame.init()
screen = pygame.display.set_mode((300, 300))
pygame.display.set_caption("PE GAME")
clock = pygame.time.Clock()
spritesheet_manager = SpritesheetManager("data/sprites", "data/sprites/sprites.json")
test_1_sprite = Sprite(spritesheet_manager.get_sheet("test_1"))
test_2_sprite = Sprite(spritesheet_manager.get_sheet("test_1"))
test_1_sprite.position_scale = PositionScale((10, 10), (1, 1))
test_2_sprite.position_scale = PositionScale((60, 60), (1, 1))
# test_1_sprite.dump("debug.png")
while True:
clock.tick(5)
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
screen.fill((0, 0, 0))
test_1_sprite.tick(1)
test_1_sprite.draw(screen, screen_transform)
test_2_sprite.tick(1)
test_2_sprite.draw(screen, screen_transform)
pygame.display.update()
if random.randint(1, 10) == 1:
test_1_sprite.set_animation_state(random.choice(["walk_r", "walk_l", "idle", "other_test"]))
print(test_1_sprite.animation_state)

View File

@ -1,14 +1,17 @@
import math import math
import time
from typing import Optional from typing import Optional
from physics.CollisionDirection import CollisionDirection from physics.CollisionDirection import CollisionDirection
from physics.TickData import TickData from physics.TickData import TickData
from sprite.BoundingBox import BoundingBox
from sprite.DynamicSprite import DynamicSprite from sprite.DynamicSprite import DynamicSprite
from sprite.PositionScale import PositionScale from sprite.PositionScale import PositionScale
from sprite.Sprite import Sprite from sprite.Sprite import Sprite
from sprite.StaticSprite import StaticSprite from sprite.StaticSprite import StaticSprite
from ui_elements.UiElement import UiElement from ui_elements.UiElement import UiElement
MAX_COLLIDER_CHECK_SPRITES = 15
MOTION_STEPS = 10 MOTION_STEPS = 10
TOLERANCE = 1 TOLERANCE = 1
@ -27,8 +30,6 @@ class PhysicsElementsHandler:
sprites = [] sprites = []
for layer in layers: for layer in layers:
for sprite in layers[layer]: for sprite in layers[layer]:
# if str(type(sprite)) == "<class 'level.elements.dynamic.PushableBoxLevelElement.PushableBoxLevelElement'>":
# print(f"Found pushable box")
sprite.tick(tick_data) sprite.tick(tick_data)
if isinstance(sprite, Sprite): if isinstance(sprite, Sprite):
sprites.append(sprite) sprites.append(sprite)
@ -37,6 +38,8 @@ class PhysicsElementsHandler:
# 1. Find all sprites that have collision enabled and store them in a list # 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 # 2. Create a list of all sprites that are dynamic sprites
# 3. Sort the sprites by their y position # 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
# 4. For each sprite: # 4. For each sprite:
# 4.1. Divide the motion into MOTION_STEPS steps # 4.1. Divide the motion into MOTION_STEPS steps
# 4.2. For each step: # 4.2. For each step:
@ -53,8 +56,59 @@ class PhysicsElementsHandler:
dynamic_sprites = [sprite for sprite in sprites if isinstance(sprite, DynamicSprite)] 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]) 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: for sprite in sorted_dynamic_sprites:
collides_with = self.attempt_move(tick_data, sprite, colliders, MOTION_STEPS) 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)
if collides_with is not None: if collides_with is not None:
for callback in self.collision_callbacks: for callback in self.collision_callbacks:
callback(sprite, collides_with) callback(sprite, collides_with)
@ -74,9 +128,7 @@ class PhysicsElementsHandler:
sprite.position_scale.position[1] 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)
collides_with = self.check_collides(sprite, colliders, tick_data.screen_transform)
for collider in collides_with: for collider in collides_with:
if collider is not None: if collider is not None:
if sprite.is_collider and collider.is_collider: if sprite.is_collider and collider.is_collider:
@ -120,7 +172,7 @@ class PhysicsElementsHandler:
sprite.position_scale.position[1] + motion_step[1] sprite.position_scale.position[1] + motion_step[1]
) )
collides_with = self.check_collides(sprite, colliders, tick_data.screen_transform) collides_with = self.check_collides(sprite, colliders)
for collider in collides_with: for collider in collides_with:
if collider is not None: if collider is not None:
if sprite.is_collider and collider.is_collider: if sprite.is_collider and collider.is_collider:
@ -160,7 +212,7 @@ class PhysicsElementsHandler:
return collides_with_last return collides_with_last
def check_collides(self, sprite: StaticSprite, colliders: list[StaticSprite], screen_transform: PositionScale) -> \ def check_collides(self, sprite: StaticSprite, colliders: list[StaticSprite]) -> \
list[StaticSprite]: list[StaticSprite]:
collides_with = [] collides_with = []

View File

@ -1,3 +1,6 @@
import math
class BoundingBox: class BoundingBox:
def __init__(self, x, y, width, height): def __init__(self, x, y, width, height):
self.x = x self.x = x
@ -5,6 +8,9 @@ class BoundingBox:
self.width = width self.width = width
self.height = height self.height = height
self.center_x = x + width / 2
self.center_y = y + height / 2
def get_dimensions(self): def get_dimensions(self):
return self.width, self.height return self.width, self.height
@ -16,3 +22,27 @@ class BoundingBox:
def __str__(self): def __str__(self):
return f"({self.x}, {self.y}, {self.width}, {self.height})" return f"({self.x}, {self.y}, {self.width}, {self.height})"
def distance(self, bounding_box: 'BoundingBox') -> float:
"""
Classmates the minimum distance between two bounding boxes by checking in what direction the bounding boxes are
in relation to each other.
:param bounding_box: The bounding box to compare to.
:return: The minimum distance between the two bounding boxes.
"""
if self.overlaps(bounding_box):
return 0
distance_x = max(0, abs(self.center_x - bounding_box.center_x) - (self.width + bounding_box.width) / 2)
distance_y = max(0, abs(self.center_y - bounding_box.center_y) - (self.height + bounding_box.height) / 2)
return math.sqrt(distance_x ** 2 + distance_y ** 2)
def overlaps(self, bounding_box: 'BoundingBox') -> bool:
"""
Checks if the bounding boxes overlap.
:param bounding_box: The bounding box to check.
:return: True if the bounding boxes overlap, False otherwise.
"""
return self.x < bounding_box.x + bounding_box.width and self.x + self.width > bounding_box.x and \
self.y < bounding_box.y + bounding_box.height and self.y + self.height > bounding_box.y

View File

@ -26,6 +26,7 @@ class Sprite(UiElement):
self.animated = True self.animated = True
self.image = None self.image = None
self.last_image = None
self.is_collider = True self.is_collider = True
self.register_collisions = True self.register_collisions = True
@ -60,6 +61,7 @@ class Sprite(UiElement):
self.animation_delay -= animation['delays'][self.animation_frame % len(animation['delays'])] self.animation_delay -= animation['delays'][self.animation_frame % len(animation['delays'])]
self.animation_frame = (self.animation_frame + 1) % len(animation['images']) self.animation_frame = (self.animation_frame + 1) % len(animation['images'])
self.last_image = self.image
self.image = animation['images'][self.animation_frame % len(animation['images'])] self.image = animation['images'][self.animation_frame % len(animation['images'])]
def set_animation_state(self, state: str): def set_animation_state(self, state: str):

View File

@ -21,6 +21,9 @@ class UiElement:
self.uuid = uuid.uuid4() self.uuid = uuid.uuid4()
self.last_image = None
self.last_scaled_image = None
def add_click_listener(self, listener): def add_click_listener(self, listener):
self.click_listeners.append(listener) self.click_listeners.append(listener)
@ -49,10 +52,12 @@ class UiElement:
image = self.render_sprite_image() image = self.render_sprite_image()
if image is not None: target_image = None
target_position = CoordinateTransform.transform_world_to_screen(self.position_scale.position, target_position = CoordinateTransform.transform_world_to_screen(self.position_scale.position,
screen_transform) screen_transform)
if not self.last_image == image or self.last_scaled_image is None:
if image is not None:
screen_scale = screen_transform.scale screen_scale = screen_transform.scale
object_scale = self.position_scale.scale object_scale = self.position_scale.scale
@ -60,6 +65,12 @@ class UiElement:
int(screen_scale[1] * object_scale[1] * image.get_height())) int(screen_scale[1] * object_scale[1] * image.get_height()))
target_image = UiElement.get_scaled_image(image, target_size) target_image = UiElement.get_scaled_image(image, target_size)
self.last_scaled_image = target_image
self.last_image = image
else:
target_image = self.last_scaled_image
if target_image is not None:
screen.blit(target_image, target_position) screen.blit(target_image, target_position)
@staticmethod @staticmethod