diff --git a/assets/loaded.png b/assets/loaded.png new file mode 100644 index 0000000..f12e82f Binary files /dev/null and b/assets/loaded.png differ diff --git a/assets/unloaded.png b/assets/unloaded.png new file mode 100644 index 0000000..927f18c Binary files /dev/null and b/assets/unloaded.png differ diff --git a/src/engine/__init__.py b/src/engine/__init__.py index c7f39dd..7077e2e 100644 --- a/src/engine/__init__.py +++ b/src/engine/__init__.py @@ -3,7 +3,7 @@ Permet de lancer un jeu et de gérer la boucle principale de celui-ci. """ -from typing import Callable +from typing import Callable, Optional from engine.ecs import World @@ -46,7 +46,7 @@ class Scene: init_systems: list[Callable[[World], object]], update_systems: list[Callable[[World], object]], stop_systems: list[Callable[[World], object]], - ): + ) -> None: self.init_systems = init_systems self.update_systems = update_systems self.stop_systems = stop_systems @@ -68,13 +68,21 @@ class KeepAlive: """ -class CurrentScene(KeepAlive, str): +class CurrentScene(KeepAlive): """ Resource qui permet de savoir et de changer la scène actuelle. """ + def __init__(self, scene: Scene): + self.scene = scene -def start_game(global_scene: GlobalScene, scenes: dict[str, Scene], scene_name: str): + def __eq__(self, value: object) -> bool: + if isinstance(value, CurrentScene): + return self.scene == value.scene + return False + + +def start_game(global_scene: GlobalScene, scene: Optional[Scene]): """ Lance un jeu. @@ -84,14 +92,14 @@ def start_game(global_scene: GlobalScene, scenes: dict[str, Scene], scene_name: """ # On création du monde world = World() - world[CurrentScene] = CurrentScene(scene_name) + if scene is not None: + world[CurrentScene] = CurrentScene(scene) # On initialise la scène globale for system in global_scene.init_systems: system(world) # Boucle principale - scene = scenes.get(scene_name) while scene is not None: # On retire les ressources de l'ancienne scène for resource in world: @@ -107,7 +115,7 @@ def start_game(global_scene: GlobalScene, scenes: dict[str, Scene], scene_name: system(world) # Tant que la scène n'est pas terminé on la met à jour - while world.get(CurrentScene, "") == scene_name: + while world.get(CurrentScene, ()) == CurrentScene(scene): # On met à jour la scène globale for system in global_scene.pre_update_systems: system(world) @@ -124,8 +132,12 @@ def start_game(global_scene: GlobalScene, scenes: dict[str, Scene], scene_name: for system in scene.stop_systems: system(world) - # Récupération de la scène suivante - scene = scenes.get(world.get(CurrentScene, "")) + # On met à jour la scène + current_scene = world.get(CurrentScene, ()) + if isinstance(current_scene, tuple): + scene = None + else: + scene = current_scene.scene # On arrête la scène globale for system in global_scene.stop_systems: diff --git a/src/main.py b/src/main.py index 464773a..b1a7b68 100644 --- a/src/main.py +++ b/src/main.py @@ -3,13 +3,7 @@ Module d'exemple de l'utilisation du moteur de jeu. """ from engine import Scene, start_game -from plugins import defaults +from plugins import assets, defaults -start_game( - defaults.PLUGIN, - { - "menu": Scene([], [], []), - }, - "menu", -) +start_game(defaults.PLUGIN, assets.loading_scene(Scene([], [], []), "textures")) diff --git a/src/plugins/animation.py b/src/plugins/animation.py new file mode 100644 index 0000000..6738980 --- /dev/null +++ b/src/plugins/animation.py @@ -0,0 +1,23 @@ +""" +Un plugin qui permet de jouer des animations de sprites. +""" + +from engine import GlobalScene + + +class Animation: + """ + Composant qui contient toutes les informations d'une animation est en cour + sur l'entité. + """ + + def __init__(self) -> None: + pass + + +PLUGIN = GlobalScene( + [], + [], + [], + [], +) diff --git a/src/plugins/assets.py b/src/plugins/assets.py new file mode 100644 index 0000000..dc9f202 --- /dev/null +++ b/src/plugins/assets.py @@ -0,0 +1,161 @@ +""" +Un plugin qui gère les ressources du jeu. +""" + +import glob +import pygame +from engine import CurrentScene, GlobalScene, KeepAlive, Scene +from engine.ecs import World +from plugins import render + + +class Assets(KeepAlive): + """ + Ressource qui gère les assets du jeu. + """ + + def __init__(self): + # Création de la texture d'erreur + error_texture = pygame.Surface((256, 256)) + error_texture.fill((0, 0, 0)) + pygame.draw.rect(error_texture, (255, 0, 255), (0, 0, 128, 128)) + pygame.draw.rect(error_texture, (255, 0, 255), (128, 128, 128, 128)) + self.__error_texture = error_texture.convert() + + # Chargement des textures de chargement + self.__unloaded_texture = pygame.image.load("assets/unloaded.png").convert() + self.__loaded_texture = pygame.image.load("assets/loaded.png").convert() + + # Cache des ressources + self.__textures: dict[str, pygame.Surface] = {} + + @property + def unloaded_texture(self) -> pygame.Surface: + return self.__unloaded_texture + + @property + def loaded_texture(self) -> pygame.Surface: + return self.__loaded_texture + + def load_texture(self, name: str, path: str) -> pygame.Surface: + """ + Charge une texture et la renvoi. Si une texture existe déja dans le cache, + elle sera remplacée par la nouvelle. + """ + surface = pygame.image.load(path).convert_alpha() + self.__textures[name] = surface + return surface + + def get_texture(self, name: str) -> pygame.Surface: + """ + Renvoie la texture demandée. + """ + return self.__textures.get(name, self.__error_texture) + + def clear_cache(self): + """ + Vide le cache des assets. + """ + self.__textures.clear() + + +def __initialize(world: World): + """ + Ajoute la ressource `Assets` au monde. + """ + world.set(Assets()) + + +PLUGIN = GlobalScene( + [__initialize], + [], + [], + [], +) + + +def loading_scene(target: Scene, name: str): + """ + Retourne une scène de chargement des assets qui passe à la scène donné + en paramètres lorsque tous les assets de la scène sont chargées. + + Paramètres: + - `target`: la scène qui sera lancé après le chargement des assets. + - `name`: le nom de la scène, ce nom est utilisé pour savoir dans quel + dossier sont les assets de la scène. Les assets de la scène + seront récupéré dans le dossier `assets/`. + """ + + class AssetIterator: + """ + Une ressource qui contient un itérateur sur les fichiers des assets + de la scène à charger. + """ + + def __init__(self): + self.files = glob.glob(f"assets/{name}/**/*", recursive=True) + self.total = len(self.files) + + @staticmethod + def prepare_world(world: World): + """ + Retire toutes les ressource précédentes du monde puis + ajoute `ResourceIterator` et la barre de progression dans le monde. + """ + assets = world[Assets] + assets.clear_cache() + world.set(AssetIterator()) + world.new_entity().set( + render.Sprite(assets.unloaded_texture, order=100000000000) + ) + world.new_entity().set( + ProgessBar(), + render.Sprite( + assets.loaded_texture, + order=100000000001, + area=(0, 0, 0, render.HEIGHT), + ), + ) + + @staticmethod + def load_next(world: World): + """ + Charge le fichier suivant de l'itérateur. + """ + asset_iterator = world[AssetIterator] + if len(asset_iterator.files) == 0: + world[CurrentScene] = target + else: + file = asset_iterator.files.pop().replace("\\", "/") + ressource_extension = file.split(".")[-1] + ressource_name = "/".join(file.split("/")[2:])[ + : -len(ressource_extension) - 1 + ] + if ressource_extension in ("png", "jpg"): + world[Assets].load_texture(ressource_name, file) + + class ProgessBar: + """ + Composant marquant une entité comme étant une barre de progression. + """ + + @staticmethod + def render(world: World): + """ + Affiche une barre de progression du chargement des assets. + """ + # Calcul du pourcentage de chargement + asset_iterator = world[AssetIterator] + file_loaded = asset_iterator.total - len(asset_iterator.files) + progress = file_loaded / asset_iterator.total + pixels = int(render.WIDTH * progress) + + # Affichage de la barre de progression + progress_bar = world.query(ProgessBar).pop() + progress_bar[render.Sprite].area = (0, 0, pixels, render.HEIGHT) + + return Scene( + [AssetIterator.prepare_world], + [AssetIterator.load_next, ProgessBar.render], + [], + ) diff --git a/src/plugins/defaults.py b/src/plugins/defaults.py index e2c086f..d262dc1 100644 --- a/src/plugins/defaults.py +++ b/src/plugins/defaults.py @@ -2,7 +2,7 @@ Plugin qui rassemple tous les plugins globaux. """ -from plugins import display, inputs, render +from plugins import assets, display, inputs, render -PLUGIN = display.PLUGIN + inputs.PLUGIN + render.PLUGIN +PLUGIN = display.PLUGIN + assets.PLUGIN + inputs.PLUGIN + render.PLUGIN diff --git a/src/plugins/render.py b/src/plugins/render.py index 6237df2..dfa118e 100644 --- a/src/plugins/render.py +++ b/src/plugins/render.py @@ -2,6 +2,7 @@ Un plugin qui s'occupe de rendre des choses dans la fenetre. """ +from typing import Optional import pygame from engine import GlobalScene, KeepAlive from engine.ecs import World @@ -40,11 +41,24 @@ class Sprite: """ def __init__( - self, surface: pygame.Surface, position: Vec2 = Vec2(0), order: float = -1.0 + self, + surface: pygame.Surface, + position: Vec2 = Vec2(0), + order: float = -1.0, + area: Optional[tuple[float, float, float, float]] = None, ): self.surface = surface self.position = position self.order = order + if area is None: + self.area = ( + 0.0, + 0.0, + float(surface.get_width()), + float(surface.get_height()), + ) + else: + self.area = area def __initialize(world: World): @@ -62,7 +76,11 @@ def __render(world: World): surface = world[Surface] sprites = [entity[Sprite] for entity in world.query(Sprite)] for sprite in sorted(sprites, key=lambda sprite: sprite.order): - surface.blit(sprite.surface, (sprite.position.x, sprite.position.y)) + surface.blit( + sprite.surface, + (sprite.position.x, sprite.position.y), + sprite.area, + ) # On affiche la surface sur la fenetre rect = calculate_surface_rect()