From bac035f00d2c4b0627661e88ea0bbc2f622e16b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=C3=A9o=20C=C3=A9zard?= Date: Wed, 3 Jan 2024 03:14:53 +0100 Subject: [PATCH 1/3] WIP --- assets/{global => textures}/background.png | Bin src/engine/ecs.py | 32 ++---- src/main.py | 26 ++++- src/plugins save/__init__.py | 10 ++ src/{plugins => plugins save}/animation.py | 0 src/{plugins => plugins save}/assets.py | 0 src/plugins save/click.py | 48 +++++++++ src/{plugins => plugins save}/coroutine.py | 0 src/plugins save/defaults.py | 34 +++++++ src/plugins save/display.py | 32 ++++++ src/plugins save/hover.py | 92 +++++++++++++++++ src/plugins save/inputs.py | 90 +++++++++++++++++ src/{plugins => plugins save}/multisound.py | 0 src/plugins save/render.py | 106 ++++++++++++++++++++ src/{plugins => plugins save}/smooth.py | 0 src/{plugins => plugins save}/text.py | 0 src/plugins save/timing.py | 52 ++++++++++ src/plugins/click.py | 5 +- src/plugins/defaults.py | 21 +--- src/plugins/hover.py | 22 ++-- src/plugins/render.py | 84 ++++++++-------- src/plugins/sound.py | 8 +- src/plugins/timing.py | 24 +---- src/plugins/writing.py | 56 ----------- src/scenes/__init__.py | 5 - 25 files changed, 557 insertions(+), 190 deletions(-) rename assets/{global => textures}/background.png (100%) create mode 100644 src/plugins save/__init__.py rename src/{plugins => plugins save}/animation.py (100%) rename src/{plugins => plugins save}/assets.py (100%) create mode 100644 src/plugins save/click.py rename src/{plugins => plugins save}/coroutine.py (100%) create mode 100644 src/plugins save/defaults.py create mode 100644 src/plugins save/display.py create mode 100644 src/plugins save/hover.py create mode 100644 src/plugins save/inputs.py rename src/{plugins => plugins save}/multisound.py (100%) create mode 100644 src/plugins save/render.py rename src/{plugins => plugins save}/smooth.py (100%) rename src/{plugins => plugins save}/text.py (100%) create mode 100644 src/plugins save/timing.py delete mode 100644 src/plugins/writing.py diff --git a/assets/global/background.png b/assets/textures/background.png similarity index 100% rename from assets/global/background.png rename to assets/textures/background.png diff --git a/src/engine/ecs.py b/src/engine/ecs.py index 8e58b65..a94a1f0 100644 --- a/src/engine/ecs.py +++ b/src/engine/ecs.py @@ -6,7 +6,7 @@ Pour moddifier le monde, on n'agis que sur les composants. """ -from typing import Iterator, Optional, Sequence, TypeVar +from typing import Iterator, Optional, Sequence class Entity: @@ -17,15 +17,6 @@ class Entity: utilitaire pour acceder au monde plus facilement. """ - __T = TypeVar("__T") - """ - Ce type est utilisé pour permettre la gestion des types - pour certaines fonctions. Par exemple, la fonction `__getitem__` - utilise cette variable pour retourner un object du type demandé. - Cela permet d'avoir des vérifications de types et de l'autocomplétion - en utilisant notre IDE. - """ - def __init__(self, world: "World", identifier: int) -> None: self.__world = world self.__identifier = identifier @@ -55,13 +46,13 @@ class Entity: def __repr__(self) -> str: return f"Entity({self.__identifier})" - def __getitem__(self, component_type: type[__T]) -> __T: + def __getitem__[T](self, component_type: type[T]) -> T: return self.__world.get_component(self, component_type) def __delitem__(self, component_type: type[object]): self.__world.remove_component(self, component_type) - def __setitem__(self, component_type: type[__T], component: __T): + def __setitem__[T](self, component_type: type[T], component: T): if component_type != type(component): component = component_type(component) self.__world.set_component(self, component) @@ -75,7 +66,7 @@ class Entity: def __iter__(self) -> Iterator[object]: return iter(self.__world.all_components(self)) - def get(self, component_type: type[__T], default: Optional[__T] = None) -> __T: + def get[T](self, component_type: type[T], default: Optional[T] = None) -> T: """ Renvoie le composant de type `component_type` de l'entité. Si aucun composant de type `component_type` n'est dans l'entité: @@ -126,15 +117,6 @@ class World(Entity): globaux, relatif au monde. """ - __T = TypeVar("__T") - """ - Ce type est utilisé pour permettre la gestion des types - pour certaines fonctions. Par exemple, la fonction `get_component` - utilise cette variable pour retourner un object du type demandé. - Cela permet d'avoir des vérifications de types et de l'autocomplétion - en utilisant notre IDE. - """ - def __init__(self): super().__init__(self, 0) self.__components: dict[int, dict[type[object], object]] = {} @@ -167,9 +149,9 @@ class World(Entity): self.__components.setdefault(entity.identifier, {})[type(component)] = component self.__entities.setdefault(type(component), set()).add(entity.identifier) - def get_component( - self, entity: "Entity", component_type: type[__T], default: Optional[__T] = None - ) -> __T: + def get_component[T]( + self, entity: "Entity", component_type: type[T], default: Optional[T] = None + ) -> T: """ Renvoie le composant de type `component_type` de l'entité `entity`. Si aucun composant de type `component_type` n'est dans l'entité: diff --git a/src/main.py b/src/main.py index 58ac896..06f1d8f 100644 --- a/src/main.py +++ b/src/main.py @@ -2,9 +2,29 @@ Module d'exemple de l'utilisation du moteur de jeu. """ -from engine import start_game +from engine import Scene, start_game +from engine.ecs import World from plugins import defaults -from scenes import menu +from plugins.render import Origin, Position, Scale, Texture -start_game(defaults.PLUGIN, menu.SCENE) +def __initialize(world: World): + """ + Initialise les ressources pour le moteur de jeu. + """ + world.new_entity().set( + Texture("background.png", 0), + # Scale(1000, 1000), + # Origin(0.5, 0.5), + # Position(600, 600), + ) + + +MENU = Scene( + [__initialize], + [], + [], +) + + +start_game(defaults.PLUGIN, MENU) diff --git a/src/plugins save/__init__.py b/src/plugins save/__init__.py new file mode 100644 index 0000000..63ddc3a --- /dev/null +++ b/src/plugins save/__init__.py @@ -0,0 +1,10 @@ +""" +Module contenant tous les plugins du jeu. + +Un plugin est une scène pouvant être ajouté a d'autres scènes +afin d'ajouter des fonctionnalités au jeu. + +Le but est de faire en sorte que les plugins soient génériques +afin de pouvoir les utilisers dans plusieurs scènes et donc +éviter de répéter plusieurs fois le même code. +""" diff --git a/src/plugins/animation.py b/src/plugins save/animation.py similarity index 100% rename from src/plugins/animation.py rename to src/plugins save/animation.py diff --git a/src/plugins/assets.py b/src/plugins save/assets.py similarity index 100% rename from src/plugins/assets.py rename to src/plugins save/assets.py diff --git a/src/plugins save/click.py b/src/plugins save/click.py new file mode 100644 index 0000000..d4548fa --- /dev/null +++ b/src/plugins save/click.py @@ -0,0 +1,48 @@ +""" +Un plugin permettant de savoir si l'on a cliqué sur une entité. +""" + +from typing import Callable +from engine import GlobalPlugin +from engine.ecs import Entity, World +from plugins.hover import Hovered +from plugins.inputs import Pressed +from plugins.render import Sprite + + +class Clicked: + """ + Component ajouté a toutes les entitées qui viennent d'être cliqué. + """ + + +class Clickable: + """ + Composant qui permet d'executer une fonction lorsqu'une entité est cliquee. + """ + + def __init__(self, callback: Callable[[World, Entity], object]): + self.callback = callback + + +def __update_clicked(world: World): + """ + Met à jour les composants `Clicked`. + """ + mouse_click = "button_1" in world[Pressed] + sprite_entities = world.query(Sprite) + for entity in sprite_entities: + if Hovered in entity and mouse_click: + entity[Clicked] = Clicked() + if Clickable in entity: + entity[Clickable].callback(world, entity) + else: + del entity[Clicked] + + +PLUGIN = GlobalPlugin( + [], + [__update_clicked], + [], + [], +) diff --git a/src/plugins/coroutine.py b/src/plugins save/coroutine.py similarity index 100% rename from src/plugins/coroutine.py rename to src/plugins save/coroutine.py diff --git a/src/plugins save/defaults.py b/src/plugins save/defaults.py new file mode 100644 index 0000000..8545e55 --- /dev/null +++ b/src/plugins save/defaults.py @@ -0,0 +1,34 @@ +""" +Plugin qui rassemple tous les plugins globaux. +""" + +from plugins import ( + animation, + assets, + click, + coroutine, + display, + hover, + inputs, + multisound, + render, + sound, + text, + timing, +) + + +PLUGIN = ( + display.PLUGIN + + timing.PLUGIN + + assets.PLUGIN + + inputs.PLUGIN + + hover.PLUGIN + + click.PLUGIN + + coroutine.PLUGIN + + multisound.PLUGIN + + sound.PLUGIN + + text.PLUGIN + + animation.PLUGIN + + render.PLUGIN +) diff --git a/src/plugins save/display.py b/src/plugins save/display.py new file mode 100644 index 0000000..cc0c1c2 --- /dev/null +++ b/src/plugins save/display.py @@ -0,0 +1,32 @@ +""" +Un plugin pour la gestion de la fenetre du jeu. +""" + + +import pygame +from engine.ecs import World +from engine import GlobalPlugin + + +def __initialize(_world: World): + """ + Initialise pygame et les ressources pour la gestion de la fenetre. + """ + pygame.init() + pygame.display.set_caption("Guess The Number") + pygame.display.set_mode((800, 600), pygame.RESIZABLE) + + +def __terminate(_world: World): + """ + Arrête pygame. + """ + pygame.quit() + + +PLUGIN = GlobalPlugin( + [__initialize], + [], + [], + [__terminate], +) diff --git a/src/plugins save/hover.py b/src/plugins save/hover.py new file mode 100644 index 0000000..c73ea65 --- /dev/null +++ b/src/plugins save/hover.py @@ -0,0 +1,92 @@ +""" +Un plugin permettant de savoir si la souris est par dessus une entité. +""" + + +import pygame + +from engine import GlobalPlugin +from engine.ecs import World +from engine.math import Vec2 +from plugins.inputs import MousePosition +from plugins.render import Sprite + + +class HoverEnter: + """ + Composant indicant que l'entité vient de commencer a être survolée par la souris. + """ + + +class Hovered: + """ + Composant indicant que l'entité est survolée par la souris. + """ + + +class HoverExit: + """ + Composant indicant que la souris viens d'arreter de survoler l'entité. + """ + + +class HoveredTexture: + """ + Composant permettant de changer la texture d'une entité lorsque + celle-ci est survolée par la souris. + """ + + def __init__(self, normal: pygame.Surface, hovered: pygame.Surface): + self.normal = normal + self.hovered = hovered + + +def __update_hovered(world: World): + """ + Vérifie le survol de la souris sur les entitées. + """ + # On met à jour les composants + mouse_position = world[MousePosition] + for entity in world.query(Sprite): + # Récupération de la position et taille de l'entité + sprite = entity[Sprite] + size = Vec2(*sprite.texture.get_size()) * Vec2(*sprite.area[2:]) + position = sprite.position - (sprite.origin * size) + + # On détermine si la souris est sur l'entité + if ( + mouse_position.x >= position.x + and mouse_position.x <= position.x + size.x + and mouse_position.y >= position.y + and mouse_position.y <= position.y + size.y + ): + if Hovered not in entity: + entity[HoverEnter] = HoverEnter() + else: + del entity[HoverEnter] + entity[Hovered] = Hovered() + else: + if Hovered in entity: + entity[HoverExit] = HoverExit() + else: + del entity[HoverExit] + del entity[Hovered] + + # On affiche la bonne texture + for entity in world.query(HoveredTexture): + if Hovered in entity: + texture = entity[HoveredTexture].hovered + else: + texture = entity[HoveredTexture].normal + if Sprite in entity: + entity[Sprite].texture = texture + else: + entity[Sprite] = Sprite(texture) + + +PLUGIN = GlobalPlugin( + [], + [__update_hovered], + [], + [], +) diff --git a/src/plugins save/inputs.py b/src/plugins save/inputs.py new file mode 100644 index 0000000..7c7f59b --- /dev/null +++ b/src/plugins save/inputs.py @@ -0,0 +1,90 @@ +""" +Un plugin permettant de gérer les entrées utilisateur du jeu. +""" + +import pygame +from engine import CurrentScene, GlobalPlugin, KeepAlive +from engine.ecs import World +from engine.math import Vec2 +from plugins import render + + +class Pressed(KeepAlive, set[str]): + """ + Ressource qui correspond aux touches qui viennent d'être préssées par l'utilisateur. + + Pour les boutons de la souris la touche sera nommée `button_x` avec `x` le numéro du bouton. + """ + + +class Held(KeepAlive, set[str]): + """ + Ressource qui correspond aux touches qui sont actuellement préssées par l'utilisateur. + """ + + +class Released(KeepAlive, set[str]): + """ + Ressource qui correspond aux touches qui viennent d'être relachées par l'utilisateur. + """ + + +class MousePosition(KeepAlive, Vec2): + """ + Ressource qui correspond à la position de la souris. + """ + + +def __initialize(world: World): + """ + Initialise des ressources pour la gestion des entrées utilisateur. + """ + world.set(Pressed(), Held(), Released(), MousePosition()) + + +def __update_input(world: World): + """ + Met à jour les ressources qui permettent de savoir quels sont les + entrées utilisateur. + """ + # On récupère les ressources + pressed = world[Pressed] + held = world[Held] + released = world[Released] + + # On clear les touches pressées et relachées + pressed.clear() + released.clear() + + # On récupère les évenements de pygame + for event in pygame.event.get(): + if event.type == pygame.QUIT: + del world[CurrentScene] + elif event.type == pygame.KEYDOWN: + key_name = pygame.key.name(event.key) + held.add(key_name) + pressed.add(key_name) + elif event.type == pygame.KEYUP: + key_name = pygame.key.name(event.key) + held.remove(key_name) + released.add(key_name) + elif event.type == pygame.MOUSEBUTTONDOWN: + held.add(f"button_{event.button}") + pressed.add(f"button_{event.button}") + elif event.type == pygame.MOUSEBUTTONUP: + held.remove(f"button_{event.button}") + released.add(f"button_{event.button}") + elif event.type == pygame.MOUSEMOTION: + x, y, w, h = render.calculate_surface_rect() + world[MousePosition] = Vec2( + ((event.pos[0] - x) / w) * render.WIDTH, + ((event.pos[1] - y) / h) * render.HEIGHT, + ) + + +PLUGIN = GlobalPlugin( + [__initialize], + [__update_input], + [], + [], +) diff --git a/src/plugins/multisound.py b/src/plugins save/multisound.py similarity index 100% rename from src/plugins/multisound.py rename to src/plugins save/multisound.py diff --git a/src/plugins save/render.py b/src/plugins save/render.py new file mode 100644 index 0000000..f8aec72 --- /dev/null +++ b/src/plugins save/render.py @@ -0,0 +1,106 @@ +""" +Un plugin qui s'occupe de rendre des choses dans la fenetre. +""" + +from typing import Optional +import pygame +from engine import GlobalPlugin, KeepAlive +from engine.ecs import World +from engine.math import Vec2 + + +WIDTH = 1440 +HEIGHT = 1080 +RATIO = WIDTH / HEIGHT +INVERT_RATIO = HEIGHT / WIDTH + + +def calculate_surface_rect() -> tuple[float, float, float, float]: + """ + Calcule et renvoie un rectangle qui corresponed a la zone dans laquelle le jeu est rendu. + """ + width, height = pygame.display.get_surface().get_size() + if width / height < RATIO: + target_height = width * INVERT_RATIO + offset = (height - target_height) / 2 + return 0.0, offset, float(width), target_height + target_width = height * RATIO + offset = (width - target_width) / 2 + return offset, 0.0, target_width, float(height) + + +class Surface(KeepAlive, pygame.Surface): + """ + Ressource qui stocke la surface de rendu du jeu. + """ + + +class Sprite: + """ + Composant donnant la texture d'une entité, sa position et son ordre de rendu. + """ + + def __init__( + self, + texture: pygame.Surface, + position: Vec2 = Vec2(0), + order: float = -1.0, + area: Optional[tuple[float, float, float, float]] = None, + origin: Vec2 = Vec2(0), + ): + self.texture = texture + self.position = position + self.order = order + if area is None: + self.area = (0.0, 0.0, 1.0, 1.0) + else: + self.area = area + self.origin = origin + + +def __initialize(world: World): + """ + Prépare le monde pour la gestion du rendu. + """ + world.set(Surface((WIDTH, HEIGHT))) + + +def __render(world: World): + """ + Rend le monde du jeu sur la surface puis l'affiche sur la fenetre. + """ + # On rend le monde sur la surface + surface = world[Surface] + sprites = [entity[Sprite] for entity in world.query(Sprite)] + for sprite in sorted(sprites, key=lambda sprite: sprite.order): + original_size = Vec2(*sprite.texture.get_size()) + size = original_size * Vec2(*sprite.area[2:]) + position = sprite.position - (sprite.origin * size) + surface.blit( + sprite.texture, + (position.x, position.y), + ( + sprite.area[0] * original_size.x, + sprite.area[1] * original_size.y, + sprite.area[2] * original_size.x, + sprite.area[3] * original_size.y, + ), + ) + + # On affiche la surface sur la fenetre + rect = calculate_surface_rect() + pygame.transform.set_smoothscale_backend("MMX") + pygame.transform.smoothscale( + surface, + (rect[2], rect[3]), + pygame.display.get_surface().subsurface(rect), + ) + pygame.display.flip() + + +PLUGIN = GlobalPlugin( + [__initialize], + [], + [__render], + [], +) diff --git a/src/plugins/smooth.py b/src/plugins save/smooth.py similarity index 100% rename from src/plugins/smooth.py rename to src/plugins save/smooth.py diff --git a/src/plugins/text.py b/src/plugins save/text.py similarity index 100% rename from src/plugins/text.py rename to src/plugins save/text.py diff --git a/src/plugins save/timing.py b/src/plugins save/timing.py new file mode 100644 index 0000000..cfdbdcf --- /dev/null +++ b/src/plugins save/timing.py @@ -0,0 +1,52 @@ +""" +Un plugin permettant de connaitre le temps depuis le +lancement du jeu et le temps depuis la dernière frame. +""" + + +from time import time +from engine import GlobalPlugin, KeepAlive +from engine.ecs import World + + +class GlobalTime(KeepAlive, float): + """ + Ressource qui représente le temps global de l'ordinateur sur lequel tourne le jeu. + """ + + +class Time(KeepAlive, float): + """ + Ressource qui représente le temps depuis le lancement du jeu. + """ + + +class Delta(KeepAlive, float): + """ + Ressource qui détermine le temps depuis la première frame. + """ + + +def __initialize(world: World): + """ + Initialise les ressources pour la gestion du temps. + """ + world.set(GlobalTime(time()), Time(0.0)) + + +def __update(world: World): + """ + Met à jour les ressources de temps. + """ + now = time() + world[Delta] = delta = now - world[GlobalTime] + world[GlobalTime] = now + world[Time] += delta + + +PLUGIN = GlobalPlugin( + [__initialize], + [__update], + [], + [], +) diff --git a/src/plugins/click.py b/src/plugins/click.py index d4548fa..eb58691 100644 --- a/src/plugins/click.py +++ b/src/plugins/click.py @@ -2,12 +2,13 @@ Un plugin permettant de savoir si l'on a cliqué sur une entité. """ +from tkinter import Scale from typing import Callable from engine import GlobalPlugin from engine.ecs import Entity, World from plugins.hover import Hovered from plugins.inputs import Pressed -from plugins.render import Sprite +from plugins.render import Position class Clicked: @@ -30,7 +31,7 @@ def __update_clicked(world: World): Met à jour les composants `Clicked`. """ mouse_click = "button_1" in world[Pressed] - sprite_entities = world.query(Sprite) + sprite_entities = world.query(Position, Scale) for entity in sprite_entities: if Hovered in entity and mouse_click: entity[Clicked] = Clicked() diff --git a/src/plugins/defaults.py b/src/plugins/defaults.py index 8545e55..fe5b434 100644 --- a/src/plugins/defaults.py +++ b/src/plugins/defaults.py @@ -2,33 +2,14 @@ Plugin qui rassemple tous les plugins globaux. """ -from plugins import ( - animation, - assets, - click, - coroutine, - display, - hover, - inputs, - multisound, - render, - sound, - text, - timing, -) +from plugins import display, inputs, sound, render, timing, hover PLUGIN = ( display.PLUGIN + timing.PLUGIN - + assets.PLUGIN + inputs.PLUGIN + hover.PLUGIN - + click.PLUGIN - + coroutine.PLUGIN - + multisound.PLUGIN + sound.PLUGIN - + text.PLUGIN - + animation.PLUGIN + render.PLUGIN ) diff --git a/src/plugins/hover.py b/src/plugins/hover.py index c73ea65..d5c32e3 100644 --- a/src/plugins/hover.py +++ b/src/plugins/hover.py @@ -3,13 +3,10 @@ Un plugin permettant de savoir si la souris est par dessus une entité. """ -import pygame - from engine import GlobalPlugin from engine.ecs import World -from engine.math import Vec2 from plugins.inputs import MousePosition -from plugins.render import Sprite +from plugins.render import Origin, Position, Scale, Texture class HoverEnter: @@ -36,7 +33,7 @@ class HoveredTexture: celle-ci est survolée par la souris. """ - def __init__(self, normal: pygame.Surface, hovered: pygame.Surface): + def __init__(self, normal: str, hovered: str): self.normal = normal self.hovered = hovered @@ -47,11 +44,10 @@ def __update_hovered(world: World): """ # On met à jour les composants mouse_position = world[MousePosition] - for entity in world.query(Sprite): + for entity in world.query(Position, Scale): # Récupération de la position et taille de l'entité - sprite = entity[Sprite] - size = Vec2(*sprite.texture.get_size()) * Vec2(*sprite.area[2:]) - position = sprite.position - (sprite.origin * size) + size = entity[Scale] + position = entity[Position] - (entity.get(Origin, Origin(0)) * size) # On détermine si la souris est sur l'entité if ( @@ -75,13 +71,9 @@ def __update_hovered(world: World): # On affiche la bonne texture for entity in world.query(HoveredTexture): if Hovered in entity: - texture = entity[HoveredTexture].hovered + entity[Texture] = entity[HoveredTexture].hovered else: - texture = entity[HoveredTexture].normal - if Sprite in entity: - entity[Sprite].texture = texture - else: - entity[Sprite] = Sprite(texture) + entity[Texture] = entity[HoveredTexture].normal PLUGIN = GlobalPlugin( diff --git a/src/plugins/render.py b/src/plugins/render.py index f8aec72..9bf659b 100644 --- a/src/plugins/render.py +++ b/src/plugins/render.py @@ -2,7 +2,6 @@ Un plugin qui s'occupe de rendre des choses dans la fenetre. """ -from typing import Optional import pygame from engine import GlobalPlugin, KeepAlive from engine.ecs import World @@ -29,35 +28,42 @@ def calculate_surface_rect() -> tuple[float, float, float, float]: return offset, 0.0, target_width, float(height) +class Texture(str): + """ + Composant donnant le nom de la texture d'une entité. + """ + + +class Order(float): + """ + Composant donnant l'ordre d'affichage d'une entité. + """ + + +class Position(Vec2): + """ + Composant donnant la position de l'origine d'un objet. + """ + + +class Scale(Vec2): + """ + Composant donnant la taille de la texture d'une entité. + """ + + +class Origin(Vec2): + """ + Composant définissant l'emplacement de l'origine d'un objet sur sa texture. + """ + + class Surface(KeepAlive, pygame.Surface): """ Ressource qui stocke la surface de rendu du jeu. """ -class Sprite: - """ - Composant donnant la texture d'une entité, sa position et son ordre de rendu. - """ - - def __init__( - self, - texture: pygame.Surface, - position: Vec2 = Vec2(0), - order: float = -1.0, - area: Optional[tuple[float, float, float, float]] = None, - origin: Vec2 = Vec2(0), - ): - self.texture = texture - self.position = position - self.order = order - if area is None: - self.area = (0.0, 0.0, 1.0, 1.0) - else: - self.area = area - self.origin = origin - - def __initialize(world: World): """ Prépare le monde pour la gestion du rendu. @@ -65,27 +71,25 @@ def __initialize(world: World): world.set(Surface((WIDTH, HEIGHT))) -def __render(world: World): +def __render(world: World, cache: dict[str, pygame.Surface] = {}): """ Rend le monde du jeu sur la surface puis l'affiche sur la fenetre. """ # On rend le monde sur la surface - surface = world[Surface] - sprites = [entity[Sprite] for entity in world.query(Sprite)] - for sprite in sorted(sprites, key=lambda sprite: sprite.order): - original_size = Vec2(*sprite.texture.get_size()) - size = original_size * Vec2(*sprite.area[2:]) - position = sprite.position - (sprite.origin * size) - surface.blit( - sprite.texture, - (position.x, position.y), - ( - sprite.area[0] * original_size.x, - sprite.area[1] * original_size.y, - sprite.area[2] * original_size.x, - sprite.area[3] * original_size.y, - ), + surface: Surface = world[Surface] + entities = sorted(world.query(Texture), key=lambda entity: entity.get(Order, -1)) + for entity in entities: + texture_name = entity[Texture] + texture = cache.get(texture_name) + if texture is None: + texture = pygame.image.load(f"assets/textures/{texture_name}") + cache[texture_name] = texture + scale = entity.get(Scale, Scale(128)) + texture = pygame.transform.scale(texture, (scale.x, scale.y)) + position = ( + entity.get(Position, Position(0)) - (entity.get(Origin, Origin(0))) * scale ) + surface.blit(texture, (position.x, position.y)) # On affiche la surface sur la fenetre rect = calculate_surface_rect() diff --git a/src/plugins/sound.py b/src/plugins/sound.py index 06597d7..5b358f3 100644 --- a/src/plugins/sound.py +++ b/src/plugins/sound.py @@ -22,7 +22,7 @@ class Sound: def __init__( self, - sound: pygame.mixer.Sound, + sound: str, loop: bool = False, volume: float = 1.0, fade_ms: int = 0, @@ -42,7 +42,11 @@ def __initialize(world: World): world.set(Channels()) -def __update_sounds(world: World): +def __update_sounds( + world: World, + channels: dict[Entity, pygame.mixer.Channel] = {}, + cache: dict[str, pygame.mixer.Sound] = {}, +): """ Met à jour les sons du jeu. """ diff --git a/src/plugins/timing.py b/src/plugins/timing.py index 4d5d3d0..cfdbdcf 100644 --- a/src/plugins/timing.py +++ b/src/plugins/timing.py @@ -5,9 +5,8 @@ lancement du jeu et le temps depuis la dernière frame. from time import time -from typing import Callable from engine import GlobalPlugin, KeepAlive -from engine.ecs import Entity, World +from engine.ecs import World class GlobalTime(KeepAlive, float): @@ -28,16 +27,6 @@ class Delta(KeepAlive, float): """ -class TimedEvent: - """ - Composant permettant d'executer un callback après un certain temps. - """ - - def __init__(self, timer: float, callback: Callable[[World, Entity], object]): - self.timer = timer - self.callback = callback - - def __initialize(world: World): """ Initialise les ressources pour la gestion du temps. @@ -47,22 +36,13 @@ def __initialize(world: World): def __update(world: World): """ - Met à jour les ressources de temps et execute les `TimedEvent`. + Met à jour les ressources de temps. """ - # On met à jour le temps now = time() world[Delta] = delta = now - world[GlobalTime] world[GlobalTime] = now world[Time] += delta - # On met à jour les `TimedEvent` - for entity in world.query(TimedEvent): - event = entity[TimedEvent] - event.timer -= delta - if event.timer <= 0: - del entity[TimedEvent] - event.callback(world, entity) - PLUGIN = GlobalPlugin( [__initialize], diff --git a/src/plugins/writing.py b/src/plugins/writing.py deleted file mode 100644 index d3c1926..0000000 --- a/src/plugins/writing.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -Definit un plugin qui crée un texte avec les touches frappées -""" - -from engine import Scene, World -from plugins.inputs import Pressed -from plugins.text import Text -from scenes import CLICK_SOUND - - -class Writing: - """ - Marque une entité comme un texte qui s'ecrit en fonction du clavier - """ - - def __init__( - self, accepted_chars: str, max_chars: int = 10, base_text: str = "" - ) -> None: - self.accepted_chars = accepted_chars - self.max_chars = max_chars - self.base_text = base_text - - -def __update(world: World): - """ - Met a jour les entitées contenant le composant Typing - """ - pressed = world[Pressed] - for entity in world.query(Writing, Text): - writing = entity[Writing] - text = entity[Text] - for key in pressed: - if key == "backspace" and text.text != writing.base_text: - text.text = text.text[:-1] - world.new_entity().set(CLICK_SOUND) - if text.text == "": - text.text = writing.base_text - if key.startswith("["): # pavé numerique - key = key[1] - if key in writing.accepted_chars and ( - text.text == writing.base_text or len(text.text) < writing.max_chars - ): - if text.text == writing.base_text: - text.text = key - else: - text.text += key - world.new_entity().set(CLICK_SOUND) - if text.text == "": - text.text = writing.base_text - - -PLUGIN = Scene( - [], - [__update], - [], -) diff --git a/src/scenes/__init__.py b/src/scenes/__init__.py index e3b14b0..44fb452 100644 --- a/src/scenes/__init__.py +++ b/src/scenes/__init__.py @@ -1,8 +1,3 @@ """ Module contenant toutes les scènes du jeu. """ - -from plugins.multisound import MultiSound - - -CLICK_SOUND = MultiSound("click/0", "click/1", "click/2") -- 2.43.4 From d8e64348cdd4ef9f2443d199274a7241cd15d41b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=C3=A9o=20C=C3=A9zard?= Date: Wed, 3 Jan 2024 19:58:49 +0100 Subject: [PATCH 2/3] Good --- assets/{ => sounds}/error.mp3 | Bin assets/textures/background.png | Bin 44359 -> 8648 bytes assets/textures/dada.png | Bin 0 -> 8645 bytes assets/textures/dodo.png | Bin 0 -> 8681 bytes src/engine/ecs.py | 9 +- src/main.py | 203 +++++++++++++++++++++++++++++++-- src/plugins/assets.py | 45 ++++++++ src/plugins/click.py | 5 +- src/plugins/defaults.py | 4 +- src/plugins/hover.py | 4 +- src/plugins/physics.py | 194 +++++++++++++++++++++++++++++++ src/plugins/render.py | 94 +++++++++++---- src/plugins/sound.py | 63 +++++----- 13 files changed, 552 insertions(+), 69 deletions(-) rename assets/{ => sounds}/error.mp3 (100%) create mode 100644 assets/textures/dada.png create mode 100644 assets/textures/dodo.png create mode 100644 src/plugins/assets.py create mode 100644 src/plugins/physics.py diff --git a/assets/error.mp3 b/assets/sounds/error.mp3 similarity index 100% rename from assets/error.mp3 rename to assets/sounds/error.mp3 diff --git a/assets/textures/background.png b/assets/textures/background.png index 7341a339515140cdca71f774da7ec9c3309827d7..01be4e0a65e28d0627e850fee643a7332b2b8227 100644 GIT binary patch literal 8648 zcmeAS@N?(olHy`uVBq!ia0y~yU|qn#z+%C{1{C@Ks2V81z*rpQ?!>U}oXkrghb7(7 z*O7r?V?XzwL{=c5v%n*=n1Ml034|F}J-R!Ifk83W)5S5QBJS-CL&gRJfddEFWo2LA zmy}9yblQAZ_v3`uk-ueuS{WED-u&YRGu$OFfN2$;3@|OkW&)-;nWaEh;}&C z0CE?DLZSmRh@N1ufDuG{NQ`P94LSyf(PRV6kfT{>X zO{3AK5vUv(Z5oaCq=3m_v==$rGy)X|qoW6-BPXDAFxosAZ61Kq!D#bfw0Qtb2BXb` z(dGfDI2dgnj5ZHI>0q>ZFxor-rGwGt!D#aUm<&dn2cyjcP;oFo%>#qaj7uJI%}G;A QT?%rAr>mdKI;Vst0GY|yI{*Lx literal 44359 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 diff --git a/assets/textures/dada.png b/assets/textures/dada.png new file mode 100644 index 0000000000000000000000000000000000000000..773c20d79ef0541ccbf31a9b8d86350d4453cd21 GIT binary patch literal 8645 zcmeI!F-yZx6b0ZDlSoquh&UAotIkdi1rd@EX(Cvp2%>}NAi60OL2$5&AWlkuK}U5c zWa;AK>>%RgB1$)hF0K~!o%{%=GlZM>;1LMt-uGRu)T(~5R0QCsNn8iqHsDkW!)E8V zcWgFbHR{y}ou`RUvv799US7wB=JM)U zCD!ISeAXb+as4{QhP|Mc(YRm^G%C%_q-#P~q6K$RS0YeZ|Y5)KL literal 0 HcmV?d00001 diff --git a/assets/textures/dodo.png b/assets/textures/dodo.png new file mode 100644 index 0000000000000000000000000000000000000000..88f011c63cd0261f4b025766313d4df8e2b946e6 GIT binary patch literal 8681 zcmeAS@N?(olHy`uVBq!ia0y~yU|qn#z+%C{1{C@Ks2V81z*rpQ?!>U}oXkrghb7(7 z*O7r?V?XzwL{=c5v%n*=n1Ml034|F}J-R!IfkCm()5S5QBJS;tg@R0u3@itmwOFPJ zbg(;%w73h3alG+15I=N&_ItDF*@tA-t-qZQG=_oUz^T4sMi3(*l*t4{GjK9WfvAQS z23`>D;M4$e6oZ1I12c%8Ag}0q>ZFxor-CWF!D!D#aUR2+;p4@R2@ zpmZ?WJQ!^rfYQNe^I)`j089p>&4bbA0jM|_Z61s^4?yW)w0SU0n+FFZ>Z45r`w}^q Q90A3%r>mdKI;Vst0Ba{;{Qv*} literal 0 HcmV?d00001 diff --git a/src/engine/ecs.py b/src/engine/ecs.py index a94a1f0..717eba1 100644 --- a/src/engine/ecs.py +++ b/src/engine/ecs.py @@ -146,10 +146,17 @@ class World(Entity): - `entity`: l'entité dans lequelle ajouter le composant. - `component`: le composant à ajouter. """ + if isinstance(component, tuple): + for c in component: # type: ignore + if c is not None: + self.set_component(entity, c) # type: ignore + return self.__components.setdefault(entity.identifier, {})[type(component)] = component self.__entities.setdefault(type(component), set()).add(entity.identifier) - def get_component[T]( + def get_component[ + T + ]( self, entity: "Entity", component_type: type[T], default: Optional[T] = None ) -> T: """ diff --git a/src/main.py b/src/main.py index 06f1d8f..33f6a2d 100644 --- a/src/main.py +++ b/src/main.py @@ -3,9 +3,58 @@ Module d'exemple de l'utilisation du moteur de jeu. """ from engine import Scene, start_game -from engine.ecs import World -from plugins import defaults -from plugins.render import Origin, Position, Scale, Texture +from engine.ecs import Entity, World +from engine.math import Vec2 +from plugins import defaults, physics +from plugins import render +from plugins.inputs import Held +from plugins.render import ( + Origin, + Position, + Scale, + SpriteBundle, +) +from plugins.timing import Delta + + +def lol(a: Entity, b: Entity): + if Bounce in b: + speed = a[physics.Velocity].length + a[physics.Velocity] = a[physics.Velocity].normalized + a[physics.Velocity].y = (a[Position].y - b[Position].y) * 0.005 + a[physics.Velocity] = a[physics.Velocity].normalized * min( + (speed * 1.5), 1000.0 + ) + return True + + +class Bounce: + pass + + +def lol_simul(a: Entity, b: Entity): + lol(a, b) + return RightWall not in b + + +class Simulated: + pass + + +class Bar: + pass + + +class RightWall: + pass + + +class BallFollow(int): + pass + + +class Mine: + pass def __initialize(world: World): @@ -13,16 +62,154 @@ def __initialize(world: World): Initialise les ressources pour le moteur de jeu. """ world.new_entity().set( - Texture("background.png", 0), - # Scale(1000, 1000), - # Origin(0.5, 0.5), - # Position(600, 600), + SpriteBundle( + "background.png", + -1, + scale=Vec2(render.WIDTH, render.HEIGHT), + ), ) + # world.new_entity().set( + # SpriteBundle( + # "dodo.png", + # 0, + # position=Vec2(800, 500), + # origin=Vec2(0.5, 0.5), + # scale=Vec2(400, 300), + # ), + # physics.Solid(), + # ) + + world.new_entity().set( + SpriteBundle( + "dodo.png", + 0, + position=Vec2(100, 100), + origin=Vec2(0, 0), + scale=Vec2(render.WIDTH - 200, 10), + ), + physics.Solid(), + ) + + world.new_entity().set( + SpriteBundle( + "dodo.png", + 0, + position=Vec2(100, render.HEIGHT - 100), + origin=Vec2(0, 1), + scale=Vec2(render.WIDTH - 200, 10), + ), + physics.Solid(), + ) + + world.new_entity().set( + SpriteBundle( + "dodo.png", + 0, + position=Vec2(render.WIDTH - 100, 100), + origin=Vec2(1, 0), + scale=Vec2(10, render.HEIGHT - 200), + ), + physics.Solid(), + RightWall(), + ) + world.new_entity().set( + SpriteBundle( + "dodo.png", + 0, + position=Vec2(100, 100), + origin=Vec2(0, 0), + scale=Vec2(10, render.HEIGHT - 200), + ), + physics.Solid(), + ) + + world.new_entity().set( + SpriteBundle( + "dodo.png", + 0, + position=Vec2(render.WIDTH - 130, render.HEIGHT / 2), + origin=Vec2(0.5, 0.5), + scale=Vec2(10, 200), + ), + physics.Solid(), + Bar(), + Bounce(), + ) + + world.new_entity().set( + SpriteBundle( + "dodo.png", + 0, + position=Vec2(130, render.HEIGHT / 2), + origin=Vec2(0.5, 0.5), + scale=Vec2(10, 200), + ), + physics.Solid(), + Mine(), + Bounce(), + ) + + world.new_entity().set( + SpriteBundle( + "dada.png", + 1, + scale=Vec2(10), + position=Vec2(500, 500), + origin=Vec2(0.5, 0.5), + ), + physics.Velocity(Vec2(200, 100)), + physics.CollisionHandler(lol), + ) + + +def __update(world: World): + """ + Test. + """ + for entity in world.query(Mine, Position): + if "z" in world[Held]: + entity[Position].y -= 300 * world[Delta] + if "s" in world[Held]: + entity[Position].y += 300 * world[Delta] + + ball = max( + world.query(Position, physics.Velocity, physics.CollisionHandler), + key=lambda e: e[Position].x, + ) + for bar in world.query(Bar): + bar.remove(physics.Solid) + entity = world.new_entity() + entity.set( + Position(ball[Position]), + Scale(ball[Scale]), + physics.Velocity(ball[physics.Velocity]), + Origin(ball[Origin]), + physics.CollisionHandler(lol_simul), + ) + physics.move_entity(entity, entity[physics.Velocity] * 500) + target = entity[Position].y + for bar in world.query(Bar): + diff = target - bar[Position].y + bar[Position].y += (diff / abs(diff)) * 300 * world[Delta] + bar.set(physics.Solid()) + entity.destroy() + + # ball.set(Simulated()) + # for entity in world.query(Bar): + # entity.remove(physics.Solid) + # last_position = Vec2(ball[Position]) + # last_velocity = Vec2(ball[physics.Velocity]) + # physics.move_entity(ball, ball[physics.Velocity] * 5000) + # ball[Position] = last_position + # ball[physics.Velocity] = last_velocity + # for entity in world.query(Bar): + # entity.set(physics.Solid()) + # ball.remove(Simulated) MENU = Scene( [__initialize], - [], + [__update], [], ) diff --git a/src/plugins/assets.py b/src/plugins/assets.py new file mode 100644 index 0000000..852e6f4 --- /dev/null +++ b/src/plugins/assets.py @@ -0,0 +1,45 @@ +""" +Ce module contient des utilitaires pour le chargement des ressources du jeu. +""" + +import pygame + + +def load_texture(name: str, cache: dict[str, pygame.Surface] = {}) -> pygame.Surface: + """ + Charge une texture et la renvoi. + """ + surface = cache.get(name) + if surface is None: + surface = pygame.image.load(f"assets/textures/{name}").convert_alpha() + cache[name] = surface + return surface + + +def load_sound( + name: str, cache: dict[str, pygame.mixer.Sound] = {} +) -> pygame.mixer.Sound: + """ + Charge un son et le renvoi. + """ + sound = cache.get(name) + if sound is None: + sound = pygame.mixer.Sound(f"assets/sounds/{name}") + cache[name] = sound + return sound + + +def load_text( + text: str, + size: int, + color: pygame.Color, + cache: dict[tuple[str, int, tuple[int, int, int]], pygame.Surface] = {}, +) -> pygame.Surface: + """ + Charge un texte et le renvoi. + """ + surface = cache.get((text, size, (color.r, color.g, color.b))) + if surface is None: + surface = pygame.font.Font("assets/font.ttf", size).render(text, True, color) + cache[(text, size, (color.r, color.g, color.b))] = surface + return surface diff --git a/src/plugins/click.py b/src/plugins/click.py index eb58691..47db260 100644 --- a/src/plugins/click.py +++ b/src/plugins/click.py @@ -2,13 +2,12 @@ Un plugin permettant de savoir si l'on a cliqué sur une entité. """ -from tkinter import Scale from typing import Callable from engine import GlobalPlugin from engine.ecs import Entity, World from plugins.hover import Hovered from plugins.inputs import Pressed -from plugins.render import Position +from plugins.render import Origin, Position, Scale class Clicked: @@ -31,7 +30,7 @@ def __update_clicked(world: World): Met à jour les composants `Clicked`. """ mouse_click = "button_1" in world[Pressed] - sprite_entities = world.query(Position, Scale) + sprite_entities = world.query(Position, Scale, Origin) for entity in sprite_entities: if Hovered in entity and mouse_click: entity[Clicked] = Clicked() diff --git a/src/plugins/defaults.py b/src/plugins/defaults.py index fe5b434..2ad6ca4 100644 --- a/src/plugins/defaults.py +++ b/src/plugins/defaults.py @@ -2,14 +2,16 @@ Plugin qui rassemple tous les plugins globaux. """ -from plugins import display, inputs, sound, render, timing, hover +from plugins import click, display, inputs, physics, sound, render, timing, hover PLUGIN = ( display.PLUGIN + timing.PLUGIN + inputs.PLUGIN + + physics.PLUGIN + hover.PLUGIN + + click.PLUGIN + sound.PLUGIN + render.PLUGIN ) diff --git a/src/plugins/hover.py b/src/plugins/hover.py index d5c32e3..c458e70 100644 --- a/src/plugins/hover.py +++ b/src/plugins/hover.py @@ -44,10 +44,10 @@ def __update_hovered(world: World): """ # On met à jour les composants mouse_position = world[MousePosition] - for entity in world.query(Position, Scale): + for entity in world.query(Position, Scale, Origin): # Récupération de la position et taille de l'entité size = entity[Scale] - position = entity[Position] - (entity.get(Origin, Origin(0)) * size) + position = entity[Position] - (entity[Origin] * size) # On détermine si la souris est sur l'entité if ( diff --git a/src/plugins/physics.py b/src/plugins/physics.py new file mode 100644 index 0000000..8879d4f --- /dev/null +++ b/src/plugins/physics.py @@ -0,0 +1,194 @@ +""" +Plugin implémentant une physique exacte pour des collisions AABB. +""" + +from typing import Callable +from engine import GlobalPlugin +from engine.ecs import Entity, World +from engine.math import Vec2 +from plugins.render import Origin, Position, Scale +from plugins.timing import Delta + + +class Solid: + """ + Composant représentant un objet (de préférence imobille pour que la simulation soit prédictible) qui ne laisse pas passer les objets dynamiques. + """ + + +class Velocity(Vec2): + """ + Composant donnant la vélocité d'un objet. + """ + + +class CollisionHandler: + """ + Composant permettant de traiter les collisions. + """ + + def __init__(self, callback: Callable[[Entity, Entity], bool]): + self.callback = callback + + +class AABB: + """ + Définit une boite. + """ + + def __init__(self, min: Vec2, max: Vec2, entity: Entity): + self.min = min + self.max = max + self.entity = entity + + @staticmethod + def from_entity(entity: Entity): + min = entity[Position] - entity[Origin] * entity[Scale] + return AABB(min, min + entity[Scale], entity) + + def entity_position(self, entity: Entity): + scale = self.max - self.min + entity[Position] = self.min + entity[Origin] * scale + entity[Scale] = scale + + def __contains__(self, point: Vec2): + return ( + self.min.x <= point.x <= self.max.x and self.min.y <= point.y <= self.max.y + ) + + def move(self, movement: Vec2): + self.min += movement + self.max += movement + + +def line_to_line(sa: Vec2, ea: Vec2, sb: Vec2, eb: Vec2): + """ + Renvoie la collision entre deux lignes. + """ + if sa.x == ea.x: + sa.x += 0.0001 + if sb.x == eb.x: + sb.x += 0.0001 + if sa.y == ea.y: + sa.y += 0.0001 + if sb.y == eb.y: + sb.y += 0.0001 + + divisor = (eb.y - sb.y) * (ea.x - sa.x) - (eb.x - sb.x) * (ea.y - sa.y) + if divisor == 0: + uA = 0 + else: + uA = ((eb.x - sb.x) * (sa.y - sb.y) - (eb.y - sb.y) * (sa.x - sb.x)) / (divisor) + divisor = (eb.y - sb.y) * (ea.x - sa.x) - (eb.x - sb.x) * (ea.y - sa.y) + if divisor == 0: + uB = 0 + else: + uB = ((ea.x - sa.x) * (sa.y - sb.y) - (ea.y - sa.y) * (sa.x - sb.x)) / (divisor) + if uA >= 0 and uA <= 1 and uB >= 0 and uB <= 1: + return ( + Vec2((uA * (ea.x - sa.x)), (uA * (ea.y - sa.y))).length / (ea - sa).length + ) + return 1.0 + + +def line_to_aabb(start: Vec2, end: Vec2, aabb: AABB): + """ + Renvoie la collision entre une ligne et une AABB. + """ + left = line_to_line(start, end, aabb.min, Vec2(aabb.min.x, aabb.max.y)) + right = line_to_line(start, end, Vec2(aabb.max.x, aabb.min.y), aabb.max) + bottom = line_to_line(start, end, aabb.min, Vec2(aabb.max.x, aabb.min.y)) + top = line_to_line(start, end, Vec2(aabb.min.x, aabb.max.y), aabb.max) + t = min([left, right, bottom, top]) + if t == left: + normal = Vec2(-1, 0) + elif t == right: + normal = Vec2(1, 0) + elif t == bottom: + normal = Vec2(0, -1) + elif t == top: + normal = Vec2(0, 1) + else: + normal = Vec2(0, 0) + return t, normal + + +def aabb_to_aabb(moving: AABB, static: AABB, movement: Vec2): + """ + Renvoie la collision entre deux AABB. + """ + size = (moving.max - moving.min) / 2 + static = AABB(static.min - size, static.max + size, static.entity) + start_pos = moving.min + size + return line_to_aabb(start_pos, start_pos + movement, static) + + +def aabb_to_aabbs(moving: AABB, statics: list[AABB], movement: Vec2): + """ + Renvoie la collision entre deux AABB. + """ + t = 1.0 + normal = Vec2(0, 0) + entity = None + for static in statics: + if static.entity == moving.entity: + continue + result = aabb_to_aabb(moving, static, movement) + if result[0] < t: + t = result[0] + normal = result[1] + entity = static.entity + return t, normal, entity + + +def move_entity(entity: Entity, movement: Vec2, disable_callback: bool = False): + world = entity.world + aabb = AABB.from_entity(entity) + others = [ + AABB.from_entity(other) for other in world.query(Solid, Position, Scale, Origin) + ] + counter = 0 + while movement.length > 0.0001 and counter < 50: + t, normal, obstacle = aabb_to_aabbs(aabb, others, movement) + if t == 1.0: + step = movement + else: + step = movement * max(t - 0.000001, 0) + aabb.move(step) + aabb.entity_position(entity) + movement -= step + if normal.x != 0: + movement.x *= -1 + entity[Velocity].x *= -1 + if normal.y != 0: + movement.y *= -1 + entity[Velocity].y *= -1 + movement /= entity[Velocity] + if obstacle is not None and not disable_callback: + if not entity.get( + CollisionHandler, CollisionHandler(lambda e, o: True) + ).callback(entity, obstacle): + break + if not obstacle.get( + CollisionHandler, CollisionHandler(lambda e, o: True) + ).callback(obstacle, entity): + break + movement *= entity[Velocity] + counter += 1 + + +def __apply_velocity(world: World): + """ + Applique la vélocité a toutes les entitées. + """ + delta = world[Delta] + for entity in world.query(Velocity, Position, Scale, Origin): + move_entity(entity, entity[Velocity] * delta) + + +PLUGIN = GlobalPlugin( + [], + [__apply_velocity], + [], + [], +) diff --git a/src/plugins/render.py b/src/plugins/render.py index 9bf659b..299b450 100644 --- a/src/plugins/render.py +++ b/src/plugins/render.py @@ -3,9 +3,10 @@ Un plugin qui s'occupe de rendre des choses dans la fenetre. """ import pygame -from engine import GlobalPlugin, KeepAlive +from engine import GlobalPlugin from engine.ecs import World from engine.math import Vec2 +from plugins import assets WIDTH = 1440 @@ -28,6 +29,52 @@ def calculate_surface_rect() -> tuple[float, float, float, float]: return offset, 0.0, target_width, float(height) +class SpriteBundle: + """ + Un assemblage de composants permettant de faire une sprite. + """ + + def __new__( + cls, + texture: str, + order: float, + position: Vec2 = Vec2(0), + scale: Vec2 = Vec2(128), + origin: Vec2 = Vec2(0), + ): + return ( + Texture(texture), + Order(order), + Position(position), + Scale(scale), + Origin(origin), + ) + + +class TextBundle: + """ + Un assemblage de composants permettant de faire un texte. + """ + + def __new__( + cls, + text: str, + order: float, + size: int = 50, + color: pygame.Color = pygame.Color(255, 255, 255), + position: Vec2 = Vec2(0), + origin: Vec2 = Vec2(0), + ): + return ( + Text(text), + TextSize(size), + TextColor(color), + Position(position), + Order(order), + Origin(origin), + ) + + class Texture(str): """ Composant donnant le nom de la texture d'une entité. @@ -58,37 +105,44 @@ class Origin(Vec2): """ -class Surface(KeepAlive, pygame.Surface): +class Text(str): """ - Ressource qui stocke la surface de rendu du jeu. + Composant donnant le texte d'une entité. """ -def __initialize(world: World): +class TextSize(int): """ - Prépare le monde pour la gestion du rendu. + Composant donnant la taille du texte d'une entité. """ - world.set(Surface((WIDTH, HEIGHT))) -def __render(world: World, cache: dict[str, pygame.Surface] = {}): +class TextColor(pygame.Color): + """ + Composant donnant la couleur du texte d'une entité. + """ + + +def __render(world: World, surface: pygame.Surface = pygame.Surface((WIDTH, HEIGHT))): """ Rend le monde du jeu sur la surface puis l'affiche sur la fenetre. """ # On rend le monde sur la surface - surface: Surface = world[Surface] - entities = sorted(world.query(Texture), key=lambda entity: entity.get(Order, -1)) + entities = world.query(Texture, Position, Order, Scale, Origin) + entities.update(world.query(Text, Position, Order, Origin, TextSize, TextColor)) + entities = sorted(entities, key=lambda entity: entity[Order]) for entity in entities: - texture_name = entity[Texture] - texture = cache.get(texture_name) - if texture is None: - texture = pygame.image.load(f"assets/textures/{texture_name}") - cache[texture_name] = texture - scale = entity.get(Scale, Scale(128)) - texture = pygame.transform.scale(texture, (scale.x, scale.y)) - position = ( - entity.get(Position, Position(0)) - (entity.get(Origin, Origin(0))) * scale - ) + if Text in entity: + texture = assets.load_text( + entity[Text], entity[TextSize], entity[TextColor] + ) + scale = Scale(texture.get_width(), texture.get_height()) + else: + texture = entity[Texture] + texture = assets.load_texture(texture) + scale = entity[Scale] + texture = pygame.transform.scale(texture, (scale.x, scale.y)) + position = entity[Position] - entity[Origin] * scale surface.blit(texture, (position.x, position.y)) # On affiche la surface sur la fenetre @@ -103,7 +157,7 @@ def __render(world: World, cache: dict[str, pygame.Surface] = {}): PLUGIN = GlobalPlugin( - [__initialize], + [], [], [__render], [], diff --git a/src/plugins/sound.py b/src/plugins/sound.py index 5b358f3..f095244 100644 --- a/src/plugins/sound.py +++ b/src/plugins/sound.py @@ -5,43 +5,38 @@ Un plugin permettant de jouer des sons. from typing import Callable import pygame -from engine import GlobalPlugin, KeepAlive +from engine import GlobalPlugin from engine.ecs import Entity, World +from plugins import assets -class Channels(KeepAlive, dict[Entity, pygame.mixer.Channel]): - """ - Ressource qui stoque les sons actuellement joués dans le jeu. - """ - - -class Sound: +class Sound(str): """ Composant permettant de jouer un son. """ - def __init__( - self, - sound: str, - loop: bool = False, - volume: float = 1.0, - fade_ms: int = 0, - callback: Callable[[World, Entity], object] = lambda _w, _e: None, - ): - self.sound = sound - self.loop = loop - self.volume = volume - self.fade_ms = fade_ms + +class Volume(float): + """ + Composant donnant le volume d'un son. + """ + + +class Loop: + """ + Composant indiquant si le son joué par l'entité doit se relancer en boucle. + """ + + +class SoundCallback: + """ + Composant donnant une fonction qui sera appelée à la fin du son. + """ + + def __init__(self, callback: Callable[[World, Entity], object]): self.callback = callback -def __initialize(world: World): - """ - Ajoute les ressources utiles pour le plugin. - """ - world.set(Channels()) - - def __update_sounds( world: World, channels: dict[Entity, pygame.mixer.Channel] = {}, @@ -51,23 +46,23 @@ def __update_sounds( Met à jour les sons du jeu. """ # Ajout des sons non gérés - channels = world[Channels] sound_entities = world.query(Sound) for entity in sound_entities: if entity not in channels: - sound = entity[Sound] - channel = sound.sound.play(sound.loop, fade_ms=sound.fade_ms) + sound_name = entity[Sound] + sound = assets.load_sound(sound_name) + channel = sound.play(Loop in entity) if channel is not None: # type: ignore - channel.set_volume(sound.volume) + channel.set_volume(entity.get(Volume, 1.0)) channels[entity] = channel # On supprime les sons qui sont arrêtés ou qui n'ont plus d'entité channels_to_remove: list[Entity] = [] for entity, channel in channels.items(): if not channel.get_busy() and Sound in entity: - callback = entity[Sound].callback + callback = entity.get(SoundCallback, SoundCallback(lambda w, e: None)) del entity[Sound] - callback(world, entity) + callback.callback(world, entity) channels_to_remove.append(entity) elif entity not in sound_entities: channel.stop() @@ -77,7 +72,7 @@ def __update_sounds( PLUGIN = GlobalPlugin( - [__initialize], + [], [], [__update_sounds], [], -- 2.43.4 From 5952cf8931bc32a9e05cceb58b71f47258dc2774 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=C3=A9o=20C=C3=A9zard?= Date: Thu, 4 Jan 2024 01:12:25 +0100 Subject: [PATCH 3/3] Finalize --- src/main.py | 31 +++- src/plugins save/__init__.py | 10 -- src/plugins save/animation.py | 110 ------------- src/plugins save/assets.py | 271 --------------------------------- src/plugins save/click.py | 48 ------ src/plugins save/coroutine.py | 85 ----------- src/plugins save/defaults.py | 34 ----- src/plugins save/display.py | 32 ---- src/plugins save/hover.py | 92 ----------- src/plugins save/inputs.py | 90 ----------- src/plugins save/multisound.py | 65 -------- src/plugins save/render.py | 106 ------------- src/plugins save/smooth.py | 40 ----- src/plugins save/text.py | 65 -------- src/plugins save/timing.py | 52 ------- src/plugins/render.py | 6 +- src/scenes/menu.py | 69 --------- src/scenes/one_player.py | 22 --- 18 files changed, 32 insertions(+), 1196 deletions(-) delete mode 100644 src/plugins save/__init__.py delete mode 100644 src/plugins save/animation.py delete mode 100644 src/plugins save/assets.py delete mode 100644 src/plugins save/click.py delete mode 100644 src/plugins save/coroutine.py delete mode 100644 src/plugins save/defaults.py delete mode 100644 src/plugins save/display.py delete mode 100644 src/plugins save/hover.py delete mode 100644 src/plugins save/inputs.py delete mode 100644 src/plugins save/multisound.py delete mode 100644 src/plugins save/render.py delete mode 100644 src/plugins save/smooth.py delete mode 100644 src/plugins save/text.py delete mode 100644 src/plugins save/timing.py delete mode 100644 src/scenes/menu.py delete mode 100644 src/scenes/one_player.py diff --git a/src/main.py b/src/main.py index 33f6a2d..3e3c67c 100644 --- a/src/main.py +++ b/src/main.py @@ -17,13 +17,13 @@ from plugins.render import ( from plugins.timing import Delta -def lol(a: Entity, b: Entity): +def lol(a: Entity, b: Entity, simul: bool = False): if Bounce in b: speed = a[physics.Velocity].length a[physics.Velocity] = a[physics.Velocity].normalized a[physics.Velocity].y = (a[Position].y - b[Position].y) * 0.005 a[physics.Velocity] = a[physics.Velocity].normalized * min( - (speed * 1.5), 1000.0 + (speed * 1.1), 1000.0 ) return True @@ -33,7 +33,7 @@ class Bounce: def lol_simul(a: Entity, b: Entity): - lol(a, b) + lol(a, b, True) return RightWall not in b @@ -161,6 +161,18 @@ def __initialize(world: World): physics.CollisionHandler(lol), ) + for i in range(20): + world.new_entity().set( + SpriteBundle( + "dodo.png", + 10, + scale=Vec2(10), + ), + physics.CollisionHandler(lol), + BallFollow(i + 1), + # physics.CollisionHandler(lol_simul), + ) + def __update(world: World): """ @@ -190,10 +202,21 @@ def __update(world: World): target = entity[Position].y for bar in world.query(Bar): diff = target - bar[Position].y - bar[Position].y += (diff / abs(diff)) * 300 * world[Delta] + # bar[Position].y += (diff / abs(diff)) * 300 * world[Delta] + bar[Position].y += diff bar.set(physics.Solid()) entity.destroy() + # for bar in world.query(Bar): + # bar.remove(physics.Solid) + for entity in world.query(BallFollow): + entity[Position] = Vec2(ball[Position]) + entity[physics.Velocity] = Vec2(ball[physics.Velocity]) + physics.move_entity(entity, entity[physics.Velocity] * entity[BallFollow] * 0.5) + del entity[physics.Velocity] + # for bar in world.query(Bar): + # bar.set(physics.Solid()) + # ball.set(Simulated()) # for entity in world.query(Bar): # entity.remove(physics.Solid) diff --git a/src/plugins save/__init__.py b/src/plugins save/__init__.py deleted file mode 100644 index 63ddc3a..0000000 --- a/src/plugins save/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -Module contenant tous les plugins du jeu. - -Un plugin est une scène pouvant être ajouté a d'autres scènes -afin d'ajouter des fonctionnalités au jeu. - -Le but est de faire en sorte que les plugins soient génériques -afin de pouvoir les utilisers dans plusieurs scènes et donc -éviter de répéter plusieurs fois le même code. -""" diff --git a/src/plugins save/animation.py b/src/plugins save/animation.py deleted file mode 100644 index 9aa251a..0000000 --- a/src/plugins save/animation.py +++ /dev/null @@ -1,110 +0,0 @@ -""" -Un plugin qui permet de jouer des animations de sprites. -""" - -from typing import Callable - -import pygame -from engine import GlobalPlugin -from engine.ecs import Entity, World -from plugins.assets import Assets -from plugins.render import Sprite -from plugins.timing import Delta - - -class Animation: - """ - Composant qui contient toutes les informations d'une animation est en cour - sur l'entité. - """ - - def __init__( - self, - name: str, - fps: float = 60.0, - loop: bool = False, - callback: Callable[[World, Entity], object] = lambda _w, _e: None, - ) -> None: - self.name = name - self.fps = fps - self.loop = loop - self.callback = callback - self.timer = 0.0 - self.ended = False - - def wait(self) -> Callable[[World], bool]: - """ - Utilitaire de `Coroutine` permettant d'attendre que l'animation soit finie. - """ - return lambda world: self.ended - - -def __update_sprite(entity: Entity, texture: pygame.Surface, assets: Assets): - """ - Change la texture de la `Sprite` d'une entité. - - Si l'entité n'as pas de `Sprite`, une nouvelle `Sprite` sera ajoutée. - Si la texture est la texture d'erreur, la texture de la `Sprite` ne - sera pas changée. - """ - if texture is assets.error_texture: - return - if Sprite not in entity: - entity[Sprite] = Sprite(texture) - else: - entity[Sprite].texture = texture - - -def __update_animations(world: World) -> None: - """ - Met à jour les sprites des animations. - """ - assets = world[Assets] - delta = world[Delta] - for entity in world.query(Animation): - animation = entity[Animation] - animation.timer += delta - - # On récupère la texture correspondante a la frame de l'animation - frame_index = int(animation.timer * animation.fps) - texture = assets.get_texture(f"{animation.name}/{frame_index:04d}") - - # Si la texture n'existe pas, l'animation est finie - if texture is assets.error_texture: - if animation.loop: - # Si l'animation est une boucle on met la première image de l'animation - animation.timer = 0.0 - __update_sprite( - entity, assets.get_texture(f"{animation.name}/0000"), assets - ) - - # Et on appelle la fonction de callback - animation.callback(world, entity) - else: - # Sinon on supprime le composant `Animation` - del entity[Animation] - animation.ended = True - - # On trouve la dernière image de l'animation et on met à jour la texture - while texture is assets.error_texture: - frame_index -= 1 - texture = assets.get_texture(f"{animation.name}/{frame_index:04d}") - if frame_index < 0: - break - if texture is not assets.error_texture: - __update_sprite(entity, texture, assets) - - # Et on appelle la fonction de callback - animation.callback(world, entity) - else: - # Si l'animation n'est pas finie, on met à jour la texture - __update_sprite(entity, texture, assets) - animation.ended = False - - -PLUGIN = GlobalPlugin( - [], - [], - [__update_animations], - [], -) diff --git a/src/plugins save/assets.py b/src/plugins save/assets.py deleted file mode 100644 index 8adf9f9..0000000 --- a/src/plugins save/assets.py +++ /dev/null @@ -1,271 +0,0 @@ -""" -Un plugin qui gère les assets du jeu. -""" - -import glob -import random -import pygame -from engine import CurrentScene, GlobalPlugin, KeepAlive, Scene -from engine.ecs import World -from plugins import render -from plugins.sound import Sound - - -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() - - # Chragement du son d'erreur - self.__error_sound = pygame.mixer.Sound("assets/error.mp3") - # self.__waiting_sound = pygame.mixer.Sound("assets/waiting.mp3") - - # 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] = {} - self.__fonts: dict[int, pygame.font.Font] = {} - self.__texts: dict[tuple[int, str], pygame.Surface] = {} - self.__sounds: dict[str, pygame.mixer.Sound] = {} - - @property - def error_texture(self) -> pygame.Surface: - """ - La texture d'erreur. - - Cette texture est utilisé lorsque la texture demandée n'existe pas. - """ - return self.__error_texture - - @property - def error_sound(self) -> pygame.mixer.Sound: - """ - Le son d'erreur. - - Cette texture est utilisé lorsque le son demandé n'existe pas. - """ - return self.__error_sound - - @property - def waiting_sound(self) -> pygame.mixer.Sound: - """ - Le son de chargement. - """ - return self.__waiting_sound - - @property - def unloaded_texture(self) -> pygame.Surface: - """ - La texture de chargement qui s'affiche au début du chargement et qui - est progressivement remplacé par la texture `loaded_texture`. - """ - return self.__unloaded_texture - - @property - def loaded_texture(self) -> pygame.Surface: - """ - La texture de chargement qui s'affiche progressivement lors d'un chargement. - """ - 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. - - Si la texture n'existe pas dans le cache, la texture d'erreur sera renvoyée. - """ - return self.__textures.get(name, self.__error_texture) - - def get_font(self, size: int) -> pygame.font.Font: - """ - Renvoie la police d'ecriture du jeu avec la taille demandée. - - Cette fonction charge le fichier `assets/font.ttf` pour la taille demandée - et la met dans le cache. Si la police d'ecriture existe déjà dans le cache, - elle sera renvoyée directement. - """ - font = self.__fonts.get(size) - if font is None: - font = self.__fonts[size] = pygame.font.Font("assets/font.ttf", size) - return font - - def get_text(self, size: int, text: str, color: pygame.Color) -> pygame.Surface: - """ - Renvoie une image correspondant à la chaîne de caractères demandée avec - la taille de police demandée et la couleur demandée. - - Si l'image du texte demandé n'est pas dans le cache, elle sera créer - puis mis dans le cache et enfin renvoyée. - """ - surface = self.__texts.get((size, text)) - if surface is None: - surface = self.__texts[(size, text)] = self.get_font(size).render( - text, True, color - ) - return surface - - def load_sound(self, name: str, path: str) -> pygame.mixer.Sound: - """ - Charge un son et le renvoi. Si un son existe déja dans le cache, - il sera remplacé par le nouveau. - """ - sound = pygame.mixer.Sound(path) - self.__sounds[name] = sound - return sound - - def get_sound(self, name: str) -> pygame.mixer.Sound: - """ - Renvoie le son demandé. - - Si le son n'existe pas dans le cache, le son d'erreur sera renvoyé. - """ - return self.__sounds.get(name, self.__error_sound) - - def clear_cache(self): - """ - Vide le cache des assets. - - Les fonts ne sont pas effacés car ils n'y a normalement pas énormément - de taille de police différentes utilisées. - """ - self.__textures.clear() - self.__texts.clear() - self.__sounds.clear() - - -def __initialize(world: World): - """ - Ajoute la ressource `Assets` au monde. - """ - world.set(Assets()) - - -PLUGIN = GlobalPlugin( - [__initialize], - [], - [], - [], -) - - -def loading_scene(target: Scene, name: str, clear_cache: bool = True): - """ - 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.files.extend(glob.glob("assets/global/**/*", recursive=True)) - random.shuffle(self.files) - 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] - if clear_cache: - assets.clear_cache() - asset_iterator = AssetIterator() - world.set(AssetIterator()) - if asset_iterator.total <= 30: - for _ in range(asset_iterator.total): - asset_iterator.load_next(world) - else: - world.new_entity().set( - render.Sprite(assets.unloaded_texture, order=1000000000) - ) - world.new_entity().set( - ProgessBar(), - render.Sprite( - assets.loaded_texture, - order=1000000001, - area=(0, 0, 0, render.HEIGHT), - ), - Sound(assets.waiting_sound, loop=True), - ) - - @staticmethod - def load_next(world: World): - """ - Charge le fichier suivant de l'itérateur. - """ - assets = world[Assets] - 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] - prefix = ( - "assets/global/" - if file.startswith("assets/global/") - else f"assets/{name}/" - ) - ressource_name = file[len(prefix) : -len(ressource_extension) - 1] - if ressource_extension in ("png", "jpg"): - assets.load_texture(ressource_name, file) - if ressource_extension in ("mp3", "wav", "ogg"): - assets.load_sound(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] - if asset_iterator.total == 0: - progress = 0.0 - else: - file_loaded = asset_iterator.total - len(asset_iterator.files) - progress = file_loaded / asset_iterator.total - - # Affichage de la barre de progression - for progress_bar in world.query(ProgessBar): - progress_bar[render.Sprite].area = (0, 0, progress, 1.0) - - return Scene( - [AssetIterator.prepare_world], - [AssetIterator.load_next, ProgessBar.render], - [], - ) diff --git a/src/plugins save/click.py b/src/plugins save/click.py deleted file mode 100644 index d4548fa..0000000 --- a/src/plugins save/click.py +++ /dev/null @@ -1,48 +0,0 @@ -""" -Un plugin permettant de savoir si l'on a cliqué sur une entité. -""" - -from typing import Callable -from engine import GlobalPlugin -from engine.ecs import Entity, World -from plugins.hover import Hovered -from plugins.inputs import Pressed -from plugins.render import Sprite - - -class Clicked: - """ - Component ajouté a toutes les entitées qui viennent d'être cliqué. - """ - - -class Clickable: - """ - Composant qui permet d'executer une fonction lorsqu'une entité est cliquee. - """ - - def __init__(self, callback: Callable[[World, Entity], object]): - self.callback = callback - - -def __update_clicked(world: World): - """ - Met à jour les composants `Clicked`. - """ - mouse_click = "button_1" in world[Pressed] - sprite_entities = world.query(Sprite) - for entity in sprite_entities: - if Hovered in entity and mouse_click: - entity[Clicked] = Clicked() - if Clickable in entity: - entity[Clickable].callback(world, entity) - else: - del entity[Clicked] - - -PLUGIN = GlobalPlugin( - [], - [__update_clicked], - [], - [], -) diff --git a/src/plugins save/coroutine.py b/src/plugins save/coroutine.py deleted file mode 100644 index 6b30659..0000000 --- a/src/plugins save/coroutine.py +++ /dev/null @@ -1,85 +0,0 @@ -""" -Plugin permettant d'executer des coroutine. - -Une coroutine est une fonction qui est executée sur plusieurs frames, -cela permet d'executer du code comme si il s'agissait d'une fonction -classique tout en laissant tourner la boucle du jeu. Il est a noter que -cette fonction n'est pas lancé dans une autre thread, la fonction est -executé dans le thread principal, mais lorsqu'un `yield` est utilisé, -l'execution de la fonction est arrêté jusqu'a ce que la condition donné -par le `yield` soit remplie, ce qui permet au thread principal de pouvoir -continuer de faire le rendu et la logique du jeu. Faire un traivail bloquant -dans une coroutine auras donc un impacte sur le nombre d'images par secondes -du jeu. -""" - - -from time import time -from typing import Callable, Generator, Optional -from engine import GlobalPlugin -from engine.ecs import Entity, World -from plugins.timing import GlobalTime - - -def wait(seconds: float) -> Callable[[World], bool]: - """ - Utilitaire de `Coroutine` permettant d'attendre un certain temps. - """ - stop_time = time() + seconds - return lambda world: world[GlobalTime] >= stop_time - - -def condition(condition_function: Callable[[World], bool]) -> Callable[[World], bool]: - """ - Utilitaire de `Coroutine` permettant d'attendre que la condition soit réalisé avant - de continuer l'execution. - """ - return condition_function - - -class Coroutine: - """ - Composant permettant d'executer une coroutine. - """ - - def __init__(self, generator: Generator[Callable[[World], bool], None, None]): - self.generator = generator - self.__condition: Optional[ # pylint: disable=unused-private-member - Callable[[World], bool] - ] = None - - @staticmethod - def update(entity: Entity): - """ - Met à jour la coroutine d'une entité. - - Si l'entité n'as pas de `Coroutine` la fonction ne fait rien. - """ - if Coroutine not in entity: - return - coroutine = entity[Coroutine] - if coroutine.__condition is None: - coroutine.__condition = next( # pylint: disable=unused-private-member - coroutine.generator, None - ) - if coroutine.__condition is None: - del entity[Coroutine] - return - if coroutine.__condition(entity.world): - coroutine.__condition = None # pylint: disable=unused-private-member - - -def __update_coroutines(world: World): - """ - Met à jour les coroutine du jeu. - """ - for entity in world.query(Coroutine): - Coroutine.update(entity) - - -PLUGIN = GlobalPlugin( - [], - [__update_coroutines], - [], - [], -) diff --git a/src/plugins save/defaults.py b/src/plugins save/defaults.py deleted file mode 100644 index 8545e55..0000000 --- a/src/plugins save/defaults.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -Plugin qui rassemple tous les plugins globaux. -""" - -from plugins import ( - animation, - assets, - click, - coroutine, - display, - hover, - inputs, - multisound, - render, - sound, - text, - timing, -) - - -PLUGIN = ( - display.PLUGIN - + timing.PLUGIN - + assets.PLUGIN - + inputs.PLUGIN - + hover.PLUGIN - + click.PLUGIN - + coroutine.PLUGIN - + multisound.PLUGIN - + sound.PLUGIN - + text.PLUGIN - + animation.PLUGIN - + render.PLUGIN -) diff --git a/src/plugins save/display.py b/src/plugins save/display.py deleted file mode 100644 index cc0c1c2..0000000 --- a/src/plugins save/display.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -Un plugin pour la gestion de la fenetre du jeu. -""" - - -import pygame -from engine.ecs import World -from engine import GlobalPlugin - - -def __initialize(_world: World): - """ - Initialise pygame et les ressources pour la gestion de la fenetre. - """ - pygame.init() - pygame.display.set_caption("Guess The Number") - pygame.display.set_mode((800, 600), pygame.RESIZABLE) - - -def __terminate(_world: World): - """ - Arrête pygame. - """ - pygame.quit() - - -PLUGIN = GlobalPlugin( - [__initialize], - [], - [], - [__terminate], -) diff --git a/src/plugins save/hover.py b/src/plugins save/hover.py deleted file mode 100644 index c73ea65..0000000 --- a/src/plugins save/hover.py +++ /dev/null @@ -1,92 +0,0 @@ -""" -Un plugin permettant de savoir si la souris est par dessus une entité. -""" - - -import pygame - -from engine import GlobalPlugin -from engine.ecs import World -from engine.math import Vec2 -from plugins.inputs import MousePosition -from plugins.render import Sprite - - -class HoverEnter: - """ - Composant indicant que l'entité vient de commencer a être survolée par la souris. - """ - - -class Hovered: - """ - Composant indicant que l'entité est survolée par la souris. - """ - - -class HoverExit: - """ - Composant indicant que la souris viens d'arreter de survoler l'entité. - """ - - -class HoveredTexture: - """ - Composant permettant de changer la texture d'une entité lorsque - celle-ci est survolée par la souris. - """ - - def __init__(self, normal: pygame.Surface, hovered: pygame.Surface): - self.normal = normal - self.hovered = hovered - - -def __update_hovered(world: World): - """ - Vérifie le survol de la souris sur les entitées. - """ - # On met à jour les composants - mouse_position = world[MousePosition] - for entity in world.query(Sprite): - # Récupération de la position et taille de l'entité - sprite = entity[Sprite] - size = Vec2(*sprite.texture.get_size()) * Vec2(*sprite.area[2:]) - position = sprite.position - (sprite.origin * size) - - # On détermine si la souris est sur l'entité - if ( - mouse_position.x >= position.x - and mouse_position.x <= position.x + size.x - and mouse_position.y >= position.y - and mouse_position.y <= position.y + size.y - ): - if Hovered not in entity: - entity[HoverEnter] = HoverEnter() - else: - del entity[HoverEnter] - entity[Hovered] = Hovered() - else: - if Hovered in entity: - entity[HoverExit] = HoverExit() - else: - del entity[HoverExit] - del entity[Hovered] - - # On affiche la bonne texture - for entity in world.query(HoveredTexture): - if Hovered in entity: - texture = entity[HoveredTexture].hovered - else: - texture = entity[HoveredTexture].normal - if Sprite in entity: - entity[Sprite].texture = texture - else: - entity[Sprite] = Sprite(texture) - - -PLUGIN = GlobalPlugin( - [], - [__update_hovered], - [], - [], -) diff --git a/src/plugins save/inputs.py b/src/plugins save/inputs.py deleted file mode 100644 index 7c7f59b..0000000 --- a/src/plugins save/inputs.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -Un plugin permettant de gérer les entrées utilisateur du jeu. -""" - -import pygame -from engine import CurrentScene, GlobalPlugin, KeepAlive -from engine.ecs import World -from engine.math import Vec2 -from plugins import render - - -class Pressed(KeepAlive, set[str]): - """ - Ressource qui correspond aux touches qui viennent d'être préssées par l'utilisateur. - - Pour les boutons de la souris la touche sera nommée `button_x` avec `x` le numéro du bouton. - """ - - -class Held(KeepAlive, set[str]): - """ - Ressource qui correspond aux touches qui sont actuellement préssées par l'utilisateur. - """ - - -class Released(KeepAlive, set[str]): - """ - Ressource qui correspond aux touches qui viennent d'être relachées par l'utilisateur. - """ - - -class MousePosition(KeepAlive, Vec2): - """ - Ressource qui correspond à la position de la souris. - """ - - -def __initialize(world: World): - """ - Initialise des ressources pour la gestion des entrées utilisateur. - """ - world.set(Pressed(), Held(), Released(), MousePosition()) - - -def __update_input(world: World): - """ - Met à jour les ressources qui permettent de savoir quels sont les - entrées utilisateur. - """ - # On récupère les ressources - pressed = world[Pressed] - held = world[Held] - released = world[Released] - - # On clear les touches pressées et relachées - pressed.clear() - released.clear() - - # On récupère les évenements de pygame - for event in pygame.event.get(): - if event.type == pygame.QUIT: - del world[CurrentScene] - elif event.type == pygame.KEYDOWN: - key_name = pygame.key.name(event.key) - held.add(key_name) - pressed.add(key_name) - elif event.type == pygame.KEYUP: - key_name = pygame.key.name(event.key) - held.remove(key_name) - released.add(key_name) - elif event.type == pygame.MOUSEBUTTONDOWN: - held.add(f"button_{event.button}") - pressed.add(f"button_{event.button}") - elif event.type == pygame.MOUSEBUTTONUP: - held.remove(f"button_{event.button}") - released.add(f"button_{event.button}") - elif event.type == pygame.MOUSEMOTION: - x, y, w, h = render.calculate_surface_rect() - world[MousePosition] = Vec2( - ((event.pos[0] - x) / w) * render.WIDTH, - ((event.pos[1] - y) / h) * render.HEIGHT, - ) - - -PLUGIN = GlobalPlugin( - [__initialize], - [__update_input], - [], - [], -) diff --git a/src/plugins save/multisound.py b/src/plugins save/multisound.py deleted file mode 100644 index f42e44b..0000000 --- a/src/plugins save/multisound.py +++ /dev/null @@ -1,65 +0,0 @@ -""" -Plugin pour les sons multiples. -""" - - -import random -from typing import Callable -from engine import GlobalPlugin - -from engine.ecs import Entity, World -from plugins.assets import Assets -from plugins.sound import Sound - - -class MultiSound: - """ - Composant qui quand il est ajouté a une entité, il joue un son aléatoire - parmis la liste de ses sons, il est ensuite retiré de l'entité. - - Ce composant est fait pour être définis en tant que constante - puis utilisé ensuite, pour cela il prend seulement le nom des - sons au lieu des sons eux memes. Les sons seront donc récupérés - a l'aide de `Assets`. - """ - - def __init__( - self, - *sound_names: str, - loop: bool = False, - volume: float = 1.0, - fade_ms: int = 0, - callback: Callable[[World, Entity], object] = lambda _w, _e: None, - ): - self.sound_names = sound_names - self.loop = loop - self.volume = volume - self.fade_ms = fade_ms - self.callback = callback - - -def __update_sounds(world: World): - """ - Met à jour les sons du jeu. - """ - # Ajout des sons aléatoires - assets = world[Assets] - for entity in world.query(MultiSound): - multi_sound = entity[MultiSound] - sound = assets.get_sound(random.choice(multi_sound.sound_names)) - entity[Sound] = Sound( - sound, - multi_sound.loop, - multi_sound.volume, - multi_sound.fade_ms, - multi_sound.callback, - ) - del entity[MultiSound] - - -PLUGIN = GlobalPlugin( - [], - [], - [__update_sounds], - [], -) diff --git a/src/plugins save/render.py b/src/plugins save/render.py deleted file mode 100644 index f8aec72..0000000 --- a/src/plugins save/render.py +++ /dev/null @@ -1,106 +0,0 @@ -""" -Un plugin qui s'occupe de rendre des choses dans la fenetre. -""" - -from typing import Optional -import pygame -from engine import GlobalPlugin, KeepAlive -from engine.ecs import World -from engine.math import Vec2 - - -WIDTH = 1440 -HEIGHT = 1080 -RATIO = WIDTH / HEIGHT -INVERT_RATIO = HEIGHT / WIDTH - - -def calculate_surface_rect() -> tuple[float, float, float, float]: - """ - Calcule et renvoie un rectangle qui corresponed a la zone dans laquelle le jeu est rendu. - """ - width, height = pygame.display.get_surface().get_size() - if width / height < RATIO: - target_height = width * INVERT_RATIO - offset = (height - target_height) / 2 - return 0.0, offset, float(width), target_height - target_width = height * RATIO - offset = (width - target_width) / 2 - return offset, 0.0, target_width, float(height) - - -class Surface(KeepAlive, pygame.Surface): - """ - Ressource qui stocke la surface de rendu du jeu. - """ - - -class Sprite: - """ - Composant donnant la texture d'une entité, sa position et son ordre de rendu. - """ - - def __init__( - self, - texture: pygame.Surface, - position: Vec2 = Vec2(0), - order: float = -1.0, - area: Optional[tuple[float, float, float, float]] = None, - origin: Vec2 = Vec2(0), - ): - self.texture = texture - self.position = position - self.order = order - if area is None: - self.area = (0.0, 0.0, 1.0, 1.0) - else: - self.area = area - self.origin = origin - - -def __initialize(world: World): - """ - Prépare le monde pour la gestion du rendu. - """ - world.set(Surface((WIDTH, HEIGHT))) - - -def __render(world: World): - """ - Rend le monde du jeu sur la surface puis l'affiche sur la fenetre. - """ - # On rend le monde sur la surface - surface = world[Surface] - sprites = [entity[Sprite] for entity in world.query(Sprite)] - for sprite in sorted(sprites, key=lambda sprite: sprite.order): - original_size = Vec2(*sprite.texture.get_size()) - size = original_size * Vec2(*sprite.area[2:]) - position = sprite.position - (sprite.origin * size) - surface.blit( - sprite.texture, - (position.x, position.y), - ( - sprite.area[0] * original_size.x, - sprite.area[1] * original_size.y, - sprite.area[2] * original_size.x, - sprite.area[3] * original_size.y, - ), - ) - - # On affiche la surface sur la fenetre - rect = calculate_surface_rect() - pygame.transform.set_smoothscale_backend("MMX") - pygame.transform.smoothscale( - surface, - (rect[2], rect[3]), - pygame.display.get_surface().subsurface(rect), - ) - pygame.display.flip() - - -PLUGIN = GlobalPlugin( - [__initialize], - [], - [__render], - [], -) diff --git a/src/plugins save/smooth.py b/src/plugins save/smooth.py deleted file mode 100644 index 051f9eb..0000000 --- a/src/plugins save/smooth.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -Un plugin permettant de faire des déplacements fluides des entités. -""" - -from engine import Plugin, World -from engine.math import Vec2 -from plugins.render import Sprite -from plugins.timing import Delta - - -class Target(Vec2): - """ - Composant donnant la position voulue de l'entité. - """ - - -class Speed(float): - """ - Composant donnant la vittesse de déplacement de l'entité. - """ - - -def __update_positions(world: World): - """ - Met à jour la position des entités pour se rapprocher de la position voulue. - """ - for entity in world.query(Sprite, Target): - sprite = entity[Sprite] - target = entity[Target] - speed = entity[Speed] if Speed in entity else Speed(10) - sprite.position = ( - sprite.position + (target - sprite.position) * world[Delta] * speed - ) - - -PLUGIN = Plugin( - [], - [__update_positions], - [], -) diff --git a/src/plugins save/text.py b/src/plugins save/text.py deleted file mode 100644 index 8d08c5c..0000000 --- a/src/plugins save/text.py +++ /dev/null @@ -1,65 +0,0 @@ -""" -Un plugin permettant d'afficher du texte à l'écran. -""" - - -from typing import Optional -import pygame - -from engine import GlobalPlugin -from engine.ecs import World -from engine.math import Vec2 -from plugins.assets import Assets -from plugins.render import Sprite - - -class Text: - """ - Composant donnant le texte d'une entité, sa taille et sa couleur. - """ - - def __init__( - self, - text: str, - size: int = 50, - color: pygame.Color = pygame.Color(255, 255, 255), - position: Optional[Vec2] = None, - order: Optional[float] = None, - origin: Optional[Vec2] = None, - ): - self.text = text - self.size = size - self.color = color - self.position = position - self.order = order - self.origin = origin - - -def __render_texts(world: World): - """ - Rend les textes à l'écran. - - Pour rendre les textes, on moddifie le composant `Sprite` des entités. - """ - assets = world[Assets] - for entity in world.query(Text): - text = entity[Text] - texture = assets.get_text(text.size, text.text, text.color) - if Sprite in entity: - entity[Sprite].texture = texture - else: - entity[Sprite] = Sprite(texture) - if text.position is not None: - entity[Sprite].position = text.position - if text.order is not None: - entity[Sprite].order = text.order - if text.origin is not None: - entity[Sprite].origin = text.origin - - -PLUGIN = GlobalPlugin( - [], - [], - [__render_texts], - [], -) diff --git a/src/plugins save/timing.py b/src/plugins save/timing.py deleted file mode 100644 index cfdbdcf..0000000 --- a/src/plugins save/timing.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -Un plugin permettant de connaitre le temps depuis le -lancement du jeu et le temps depuis la dernière frame. -""" - - -from time import time -from engine import GlobalPlugin, KeepAlive -from engine.ecs import World - - -class GlobalTime(KeepAlive, float): - """ - Ressource qui représente le temps global de l'ordinateur sur lequel tourne le jeu. - """ - - -class Time(KeepAlive, float): - """ - Ressource qui représente le temps depuis le lancement du jeu. - """ - - -class Delta(KeepAlive, float): - """ - Ressource qui détermine le temps depuis la première frame. - """ - - -def __initialize(world: World): - """ - Initialise les ressources pour la gestion du temps. - """ - world.set(GlobalTime(time()), Time(0.0)) - - -def __update(world: World): - """ - Met à jour les ressources de temps. - """ - now = time() - world[Delta] = delta = now - world[GlobalTime] - world[GlobalTime] = now - world[Time] += delta - - -PLUGIN = GlobalPlugin( - [__initialize], - [__update], - [], - [], -) diff --git a/src/plugins/render.py b/src/plugins/render.py index 299b450..1827d77 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 GlobalPlugin from engine.ecs import World @@ -39,9 +40,12 @@ class SpriteBundle: texture: str, order: float, position: Vec2 = Vec2(0), - scale: Vec2 = Vec2(128), + scale: Optional[Vec2] = None, origin: Vec2 = Vec2(0), ): + if scale is None: + surface = assets.load_texture(texture) + scale = Vec2(surface.get_width(), surface.get_height()) return ( Texture(texture), Order(order), diff --git a/src/scenes/menu.py b/src/scenes/menu.py deleted file mode 100644 index 8256c88..0000000 --- a/src/scenes/menu.py +++ /dev/null @@ -1,69 +0,0 @@ -""" -La scène du menu principal du jeu. - -Dans cette scène nous pouvons choisir le mode de jeu. -""" -from scenes import CLICK_SOUND, one_player -from engine import CurrentScene, KeepAlive, Scene -from engine.ecs import Entity, World -from engine.math import Vec2 -from plugins import render -from plugins import assets as plugin_assets -from plugins.assets import Assets -from plugins.click import Clickable -from plugins.hover import HoveredTexture -from plugins.render import Sprite - - -def __create_button(world: World, assets: Assets, i: int, name: str): - """ - Ajoute un bouton au monde. - """ - world.new_entity().set( - Sprite( - assets.error_texture, - Vec2(500 + 450 * i, render.HEIGHT / 2), - 1, - origin=Vec2(0.5), - ), - HoveredTexture( - assets.get_texture(f"button_{name}"), - assets.get_texture(f"button_{name}_hover"), - ), - Clickable(lambda world, entity: __on_click_butons(world, entity, name)), - ) - - -def __on_click_butons(world: World, _entity: Entity, name: str): - """ - Fonction qui s'execute quand on clique sur un bouton. - """ - match name: - case "one_player": - world[CurrentScene] = one_player.SCENE - case "two_player": - pass - case _: - pass - world.new_entity().set(KeepAlive(), CLICK_SOUND) - - -def __spawn_elements(world: World): - """ - Ajoute les éléments du menu dans le monde. - """ - assets = world[Assets] - world.new_entity().set(Sprite(assets.get_texture("background"))) - scenes_name = ["one_player", "two_player"] - for i, name in enumerate(scenes_name): - __create_button(world, assets, i, name) - - -SCENE = plugin_assets.loading_scene( - Scene( - [__spawn_elements], - [], - [], - ), - "menu", -) diff --git a/src/scenes/one_player.py b/src/scenes/one_player.py deleted file mode 100644 index 834e9ff..0000000 --- a/src/scenes/one_player.py +++ /dev/null @@ -1,22 +0,0 @@ -from engine import Scene -from engine.ecs import World -from plugins import assets as plugin_assets -from plugins.render import Sprite - - -def __spawn_elements(world: World): - """ - Ajoute les éléments du menu dans le monde. - """ - assets = world[plugin_assets.Assets] - world.new_entity().set(Sprite(assets.get_texture("background"))) - - -SCENE = plugin_assets.loading_scene( - Scene( - [], - [], - [], - ), - "one_player", -) -- 2.43.4