diff --git a/project/data/levels/level-01.csv b/project/data/levels/level-01.csv index 457ccce..d5ff502 100644 --- a/project/data/levels/level-01.csv +++ b/project/data/levels/level-01.csv @@ -11,7 +11,7 @@ #,#,S,,,,,,,,,,,,,,,,,,,,,#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,# #,#,S,,,,,,,,,,,,,,,,,,,,,#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,# #,#,,,,,,,,,,,,,,,,,,,,,,G,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,# -#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,# +#,,,,,,,,,,,,,,,M,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,# #,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,# #,,,,,,,,,,,+,+,+,+,+,+,+,+,+,+,+,+,+,+,+,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,# #,L,,,,,,,,,,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,# diff --git a/project/data/sprites/castle_bg.png b/project/data/sprites/castle_bg.png index 8b21ac5..e0f5100 100644 Binary files a/project/data/sprites/castle_bg.png and b/project/data/sprites/castle_bg.png differ diff --git a/project/main.py b/project/main.py index 99ed5d4..20a4d25 100644 --- a/project/main.py +++ b/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) diff --git a/project/physics/PhysicsElementsHandler.py b/project/physics/PhysicsElementsHandler.py index 0b565c9..42f409d 100644 --- a/project/physics/PhysicsElementsHandler.py +++ b/project/physics/PhysicsElementsHandler.py @@ -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)) == "": - # 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)) == "", 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 = [] diff --git a/project/sprite/BoundingBox.py b/project/sprite/BoundingBox.py index fa5fe3f..8ff3ccb 100644 --- a/project/sprite/BoundingBox.py +++ b/project/sprite/BoundingBox.py @@ -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 diff --git a/project/sprite/Sprite.py b/project/sprite/Sprite.py index 2ab9185..9393eee 100644 --- a/project/sprite/Sprite.py +++ b/project/sprite/Sprite.py @@ -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): diff --git a/project/ui_elements/UiElement.py b/project/ui_elements/UiElement.py index 22cb598..84c88e5 100644 --- a/project/ui_elements/UiElement.py +++ b/project/ui_elements/UiElement.py @@ -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