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,,,,,,,,,,,,,,,,,,,,,#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
#,#,,,,,,,,,,,,,,,,,,,,,,G,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
#,,,,,,,,,,,,,,,M,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
#,,,,,,,,,,,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
#,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
# 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_tutorial = pygame.transform.scale(pygame.image.load('data/sprites/tutorial_bg.png'), (WIDTH, HEIGHT))
def apply_frame_rate(number: float):
"""
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))
pygame.init()
# optimize performance
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]),
flags=pygame.HWSURFACE | pygame.DOUBLEBUF,
vsync=1)
flags=pygame.HWSURFACE | pygame.DOUBLEBUF,
vsync=1,
depth=1)
pygame.display.set_caption("PM GAME")
clock = pygame.time.Clock()
frame_rate = 30
frame_rate = 120
spritesheet_manager = SpritesheetManager("data/sprites", "data/sprites/sprites.json")
sprite_manager = SpriteManager()
@ -145,6 +147,9 @@ elif what_to_run == 'level':
calculated_frame_rate_text.position_scale.scale = (0.3, 0.3)
sprite_manager.add_ui_element(DrawLayers.UI, calculated_frame_rate_text)
left_sprite = None
right_sprite = None
while True:
clock.tick(frame_rate)
@ -159,92 +164,23 @@ elif what_to_run == 'level':
pygame.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))
# Playground to test background on any level
# 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))
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)
@ -283,41 +219,3 @@ elif what_to_run == 'textlabel':
test3.draw(screen, screen_transform)
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 time
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.PositionScale import PositionScale
from sprite.Sprite import Sprite
from sprite.StaticSprite import StaticSprite
from ui_elements.UiElement import UiElement
MAX_COLLIDER_CHECK_SPRITES = 15
MOTION_STEPS = 10
TOLERANCE = 1
@ -27,8 +30,6 @@ class PhysicsElementsHandler:
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)
@ -37,6 +38,8 @@ class PhysicsElementsHandler:
# 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
# 4. For each sprite:
# 4.1. Divide the motion into MOTION_STEPS steps
# 4.2. For each step:
@ -53,8 +56,59 @@ class PhysicsElementsHandler:
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:
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:
for callback in self.collision_callbacks:
callback(sprite, collides_with)
@ -74,9 +128,7 @@ class PhysicsElementsHandler:
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)
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:
@ -120,7 +172,7 @@ class PhysicsElementsHandler:
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:
if collider is not None:
if sprite.is_collider and collider.is_collider:
@ -160,7 +212,7 @@ class PhysicsElementsHandler:
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]:
collides_with = []

View File

@ -1,3 +1,6 @@
import math
class BoundingBox:
def __init__(self, x, y, width, height):
self.x = x
@ -5,6 +8,9 @@ class BoundingBox:
self.width = width
self.height = height
self.center_x = x + width / 2
self.center_y = y + height / 2
def get_dimensions(self):
return self.width, self.height
@ -16,3 +22,27 @@ class BoundingBox:
def __str__(self):
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.image = None
self.last_image = None
self.is_collider = 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_frame = (self.animation_frame + 1) % len(animation['images'])
self.last_image = self.image
self.image = animation['images'][self.animation_frame % len(animation['images'])]
def set_animation_state(self, state: str):

View File

@ -21,6 +21,9 @@ class UiElement:
self.uuid = uuid.uuid4()
self.last_image = None
self.last_scaled_image = None
def add_click_listener(self, listener):
self.click_listeners.append(listener)
@ -49,17 +52,25 @@ class UiElement:
image = self.render_sprite_image()
if image is not None:
target_position = CoordinateTransform.transform_world_to_screen(self.position_scale.position,
screen_transform)
target_image = None
target_position = CoordinateTransform.transform_world_to_screen(self.position_scale.position,
screen_transform)
screen_scale = screen_transform.scale
object_scale = self.position_scale.scale
if not self.last_image == image or self.last_scaled_image is None:
if image is not None:
screen_scale = screen_transform.scale
object_scale = self.position_scale.scale
target_size = (int(screen_scale[0] * object_scale[0] * image.get_width()),
int(screen_scale[1] * object_scale[1] * image.get_height()))
target_size = (int(screen_scale[0] * object_scale[0] * image.get_width()),
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)
@staticmethod