Optimize PhysicsElementsHandler.py by checking collisions only with closest sprites.
Improve UiElement.py to cache and reuse scaled images for better performance.main
parent
0778fef354
commit
9dfc487e9a
|
@ -11,7 +11,7 @@
|
|||
#,#,S,,,,,,,,,,,,,,,,,,,,,#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
|
||||
#,#,S,,,,,,,,,,,,,,,,,,,,,#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
|
||||
#,#,,,,,,,,,,,,,,,,,,,,,,G,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
|
||||
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
|
||||
#,,,,,,,,,,,,,,,M,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
|
||||
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
|
||||
#,,,,,,,,,,,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
|
||||
#,L,,,,,,,,,,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
|
||||
|
|
|
Binary file not shown.
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 80 KiB |
152
project/main.py
152
project/main.py
|
@ -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)
|
||||
|
|
|
@ -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 = []
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue