diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed8ebf5 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..7b0016e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Module", + "type": "python", + "request": "launch", + "module": "main", + "justMyCode": true + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c6b7e7a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter" + } +} \ No newline at end of file diff --git a/engine.py b/engine.py new file mode 100644 index 0000000..2907517 --- /dev/null +++ b/engine.py @@ -0,0 +1,312 @@ +from typing import Iterator, Callable +from logging import error + + +class World: + """ + Un monde contenant des entités et des ressources. + """ + + def __init__(self): + """ + Permet de créer un nouveau monde vide. + """ + self._to_apply: set[Entity] = set() + self._entities: set[Entity] = set() + self._mapping: dict[type, set[Entity]] = {} + self._to_apply_resources: list[tuple[type, object]] = [] + self._resources: dict[type, object] = {} + + def create_entity(self, *components): + """ + Crée une entité avec les composants donnés en paramètres. + + Paramètres: + *components: Les composants de l'entité. + + Retourne: + L'entité crée. + """ + return Entity(self, *components) + + def remove_entity(self, entity: "Entity"): + """ + Supprime une entité du monde. + + Paramètres: + entity: L'entité a supprimer. + """ + entity._deleted = True + self._to_apply.add(entity) + + def set(self, *resources): + """ + Définit les ressources données en paramètres. + Si les ressources existent deja, elles seront remplacées. + + Paramètres: + *resources: Les ressources a definir. + """ + for resource in resources: + self._to_apply_resources.append((type(resource), resource)) + + def remove(self, *resource_types): + """ + Supprime les ressources données en paramètres. + + Paramètres: + *resource_types: Les types des ressources à supprimer. + """ + for resource_type in resource_types: + if resource_type in self._resources: + self._to_apply_resources.append((resource_type, None)) + + def apply(self): + """ + Applique les changements réaliser dans le monde. + """ + for entity in self._to_apply: + if entity._deleted: + self._entities.remove(entity) + for component_type in entity._components: + self._mapping[component_type].remove(entity) + else: + self._entities.add(entity) + for component_type, component in entity._to_apply: + if component is None: + del entity._components[component_type] + self._mapping[component_type].remove(entity) + else: + entity._components[component_type] = component + self._mapping.setdefault(component_type, set()).add(entity) + entity._to_apply.clear() + self._to_apply.clear() + for resource_type, resource in self._to_apply_resources: + if resource is None: + del self._resources[resource_type] + else: + self._resources[resource_type] = resource + self._to_apply_resources.clear() + + def query(self, *needed: type, without: tuple[type] = []) -> Iterator["Entity"]: + """ + Renvoie les entités qui ont les composants de *needed et sans les composants de *without. + + Paramètres: + *needed: Le type de composants que les entités doivent avoir. + *without: Les type de composants que les entités ne doivent pas avoir. + + Retourne: + Les entités qui ont les composants de *needed et sans les composants de *without. + Si *needed est vide, on retourne toutes les entités qui ont les composants de *without. + """ + if not needed: + for entity in self._entities: + if all(without_type not in entity for without_type in without): + yield entity + else: + for entity in self._mapping.get(needed[0], set()): + if all( + entity in self._mapping.get(component_type, set()) + for component_type in needed[1:] + ) and all(without_type not in entity for without_type in without): + yield entity + + def __getitem__(self, resource_type: type) -> object: + """ + Renvoie la ressource de type *resource_type. + + Paramètres: + resource_type: Le type de ressource à récupérer. + + Retourne: + La ressource de type *resource_type. + """ + return self._resources[resource_type] + + def __contains__(self, resource_type: type) -> bool: + """ + Renvoie si la ressource de type *resource_type existe. + + Paramètres: + resource_type: Le type de ressource à tester. + + Retourne: + Si la ressource de type *resource_type existe. + """ + return resource_type in self._resources + + +class Entity: + """ + Une entité du monde. + """ + + def __init__(self, world: World, *components): + """ + Créer une entité avec les composants en paramètres et l'ajoute au monde. + + Paramètres: + world: Le monde auquel ajouter l'entité. + *components: Les composants de l'entité. + """ + self._world = world + self._to_apply: list[tuple[type, object]] = [ + (type(component), component) for component in components + ] + self._components: dict[type, object] = {} + self._deleted = False + self._world._to_apply.add(self) + + def set(self, *components): + """ + Définit les composants de l'entité donnés en paramètres. + + Paramètres: + *components: Les composants a definir. + """ + for component in components: + self._to_apply.append((type(component), component)) + self._world._to_apply.add(self) + + def remove(self, *component_types: type): + """ + Supprime les composants de l'entité donnés en paramètres. + + Paramètres: + *component_types: Le type des composants à supprimer. + """ + for component_type in component_types: + if component_type in self._components: + self._to_apply.append((component_type, None)) + self._world._to_apply.add(self) + + def __getitem__(self, component_type: type) -> object: + """ + Renvoie le composant de type *component_type. + + Paramètres: + component_type: Le type du composant à récupérer. + + Retourne: + Le composant de type *component_type. + """ + return self._components[component_type] + + def __contains__(self, component_type: type) -> bool: + """ + Renvoie si le composant de type *component_type existe. + + Paramètres: + component_type: Le type du composant à tester. + + Retourne: + Si le composant de type *component_type existe. + """ + return component_type in self._components + + +class Game: + """ + Permet de faire une simple boucle de jeu. + """ + + def __init__(self): + """ + Créer une un jeu. + """ + self._running = False + self._startup_tasks: dict[int, list[Callable[[World], None]]] = {} + self._update_tasks: dict[int, list[Callable[[World], None]]] = {} + self._shutdown_tasks: dict[int, list[Callable[[World], None]]] = {} + + def add_startup_tasks(self, priority: int, *tasks: Callable[[World], None]): + """ + Ajoute des taches qui s'executeront au démarrage du jeu. + + Paramètres: + priority: La priorité de la tache. + *tasks: Les taches à ajouter. + """ + if self._running: + raise RuntimeError("Cannot add startup task while the loop is running") + self._startup_tasks.setdefault(priority, []).extend(tasks) + + def add_tasks(self, priority: int, *tasks: Callable[[World], None]): + """ + Ajoute des taches qui s'executeront a chaque mise à jour du jeu. + + Paramètres: + priority: La priorité de la tache. + *tasks: Les taches à ajouter. + """ + if self._running: + raise RuntimeError("Cannot add task while the loop is running") + self._update_tasks.setdefault(priority, []).extend(tasks) + + def add_shutdown_tasks(self, priority: int, *tasks: Callable[[World], None]): + """ + Ajoute des taches qui s'executeront à la fin de la boucle de jeu. + + Paramètres: + priority: La priorité de la tache. + *tasks: Les taches à ajouter. + """ + if self._running: + raise RuntimeError("Cannot add shutdown task while the loop is running") + self._shutdown_tasks.setdefault(priority, []).extend(tasks) + + def _run_tasks(self, tasks: dict[int, list[Callable[[World], None]]], world: World): + """ + Execute toutes les taches donnes en paramètres en respectant la priorité. + """ + priorities = sorted(list(tasks.keys())) + for priority in priorities: + for task in tasks[priority]: + try: + task(world) + except Exception as e: + error(f"Error in task: {e}") + + def run(self) -> World: + """ + Lance la boucle de jeu. + """ + if self._running: + raise RuntimeError("The loop is already running") + self._running = True + + # On initialize le monde + world = World() + world.set(self) + + # On applique les moddifications pour l'ajout de la ressource + world.apply() + + # On execute les taches d'initialisation du monde + self._run_tasks(self._startup_tasks, world) + + # On applique les changements + world.apply() + + while self._running: + # On exécute les taches de mise a jour du monde + self._run_tasks(self._update_tasks, world) + + # On applique les changements + world.apply() + + # On exécute les taches de fin du monde + self._run_tasks(self._shutdown_tasks, world) + + # On applique les changements + world.apply() + + # On retourne le monde + return world + + def stop(self): + """ + Demande la fin de la boucle de jeu. La boucle s'arretera a la prochaine mise à jour. + """ + self._running = False diff --git a/examples/ecs.py b/examples/ecs.py new file mode 100644 index 0000000..69cf714 --- /dev/null +++ b/examples/ecs.py @@ -0,0 +1,90 @@ +from engine import World # Doit être mis dans le dossier principale pour fonctionner + + +# Création de composants pouvant être ajouté a des entitées +class Name(str): + pass + + +class Age(int): + pass + + +# Création d'un monde +world = World() + +# Création de plusieurs entités +david = world.create_entity(Name("David"), Age(25)) +fred = world.create_entity(Name("Fred"), Age(30)) +paul_sans_age = world.create_entity(Name("Paul")) +age_tout_cour = world.create_entity(Age(14)) + +# On applique les moddifications +world.apply() + +print("Récupération de toutes les entitées qui ont un nom") +for entity in world.query(Name): + print(entity[Name]) + +print("Récupération de toutes les entitées qui ont un age") +for entity in world.query(Age): + print(entity[Age]) + +# On change l'age de Fred +fred.set(Age(45)) + +# On applique les moddifications +world.apply() + +print("Récupération de toutes les entités qui ont un nom et un age") +for entity in world.query(Name, Age): + print(entity[Name], entity[Age]) + +print("Récupération de toutes les entitées qui ont un nom mais pas d'age") +for entity in world.query(Name, without=(Age,)): + print(entity[Name]) + +print("Récupération de toutes les entités qui ont un age mais pas de nom") +for entity in world.query(Age, without=(Name,)): + print(entity[Age]) + +print("Récupération de toutes les entités") +for entity in world.query(): + if Name in entity: + print(entity[Name], end=" ") + if Age in entity: + print(entity[Age], end=" ") + print() + + +# Création d'une ressource pouvant être ajoutée a un monde +class Gravity(float): + pass + + +# On peut aussi ajouter des ressources globales +world.set(Gravity(9.81)) + +print("On vérifie que la ressource Gravity existe") +print(Gravity in world) + +# On applique les moddifications +world.apply() + +print("On vérifie que la ressource Gravity existe après l'application") +print(Gravity in world) + +print("Récupération de la ressource Gravity") +print(world[Gravity]) + +# On supprime la ressource Gravity +world.remove(Gravity) + +print("On vérifie que la ressource Gravity n'existe plus") +print(Gravity in world) + +# On applique les moddifications +world.apply() + +print("On vérifie que la ressource Gravity n'existe plus") +print(Gravity in world) diff --git a/main.py b/main.py new file mode 100644 index 0000000..9f67c6f --- /dev/null +++ b/main.py @@ -0,0 +1,11 @@ +from engine import Game + + +game = Game() +game.add_startup_tasks(1, lambda world: print("Hello 1")) +game.add_startup_tasks(-5, lambda world: print("Hello -5")) +game.add_startup_tasks(6, lambda world: print("Hello 6")) +game.add_startup_tasks(0, lambda world: print("Hello 0")) +game.add_tasks(0, lambda world: world[Game].stop()) +game.add_shutdown_tasks(0, lambda world: print("Bye 0")) +game.run() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29