From 9d80c111e94c990ddabcb11f4953fbd00530f22a Mon Sep 17 00:00:00 2001 From: Tipragot Date: Sun, 22 Oct 2023 19:26:06 +0200 Subject: [PATCH 1/9] =?UTF-8?q?Premi=C3=A8re=20version=20de=20l'ECS=20(pas?= =?UTF-8?q?=20termin=C3=A9e)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .vscode/launch.json | 12 ++++++++++ ecs.py | 57 +++++++++++++++++++++++++++++++++++++++++++++ main.py | 49 ++++++++++++++++++++++++++++++++++++++ requirements.txt | 0 5 files changed, 119 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 ecs.py create mode 100644 main.py create mode 100644 requirements.txt 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/ecs.py b/ecs.py new file mode 100644 index 0000000..8468a2e --- /dev/null +++ b/ecs.py @@ -0,0 +1,57 @@ +from typing import Iterator + + +class World: + def __init__(self): + self.entities: set[Entity] = set() + self.mapping: dict[type, set[Entity]] = {} + + def query(self, *needed: type, without: tuple[type] = []) -> Iterator['Entity']: + if not needed: + for entity in self.entities: + for without_type in without: + if without_type in entity: + break + else: + yield entity + else: + first_component = needed[0] + for entity in self.mapping.get(first_component, set()): + for component_type in needed[1:]: + if entity not in self.mapping.get(component_type, set()): + break + else: + for without_type in without: + if without_type in entity: + break + else: + yield entity + +class Entity: + def __init__(self, world: World): + self.world = world + self.world.entities.add(self) + self.components: dict[type, object] = {} + + def __getitem__(self, component_type): + return self.components[component_type] + + def __setitem__(self, component_type, component): + self.components[component_type] = component + self.world.mapping.setdefault(component_type, set()).add(self) + + def __delitem__(self, component_type): + del self.components[component_type] + self.world.mapping[component_type].remove(self) + if len(self.world.mapping[component_type]) == 0: + del self.world.mapping[component_type] + + def __contains__(self, component_type): + return component_type in self.components + + def __del__(self): # TODO: Réparer ça marche pas car il n'est pas détruit car il est toujours dans World + self.world.entities.remove(self) + for component_type in self.components: + self.world.mapping[component_type].remove(self) + if len(self.world.mapping[component_type]) == 0: + del self.world.mapping[component_type] diff --git a/main.py b/main.py new file mode 100644 index 0000000..cdc7757 --- /dev/null +++ b/main.py @@ -0,0 +1,49 @@ +from ecs import World, Entity + +# 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 d'une entité +david = Entity(world) +david[Name] = Name("David") +david[Age] = Age(25) + +# Création d'une autre entité +fred = Entity(world) +fred[Name] = Name("Fred") +fred[Age] = Age(30) + +# Création d'une autre entité +paul_sans_age = Entity(world) +paul_sans_age[Name] = Name("Paul") + +# Création d'une autre entité +age_tout_cour = Entity(world) +age_tout_cour[Age] = Age(14) + +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]) + +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]) + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 From ac19222744ebb91608d783d16f493d30ee69500c Mon Sep 17 00:00:00 2001 From: Tipragot Date: Sun, 22 Oct 2023 22:19:43 +0200 Subject: [PATCH 2/9] =?UTF-8?q?Am=C3=A9lioration=20du=20syst=C3=A8me?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ecs.py | 93 ++++++++++++++++++++++++++++++++------------------------- main.py | 45 +++++++++++++++------------- 2 files changed, 76 insertions(+), 62 deletions(-) diff --git a/ecs.py b/ecs.py index 8468a2e..1a6539b 100644 --- a/ecs.py +++ b/ecs.py @@ -3,55 +3,66 @@ from typing import Iterator class World: def __init__(self): + self.to_apply: set[Entity] = set() self.entities: set[Entity] = set() self.mapping: dict[type, set[Entity]] = {} + + def create_entity(self, *components): + return Entity(self, *components) + def remove_entity(self, entity: 'Entity'): + entity._deleted = True + self.to_apply.add(entity) + + def apply(self): + 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() + def query(self, *needed: type, without: tuple[type] = []) -> Iterator['Entity']: if not needed: for entity in self.entities: - for without_type in without: - if without_type in entity: - break - else: + if all(without_type not in entity for without_type in without): yield entity else: - first_component = needed[0] - for entity in self.mapping.get(first_component, set()): - for component_type in needed[1:]: - if entity not in self.mapping.get(component_type, set()): - break - else: - for without_type in without: - if without_type in entity: - break - else: - yield entity + 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 class Entity: - def __init__(self, world: World): - self.world = world - self.world.entities.add(self) - self.components: dict[type, object] = {} + def __init__(self, world: World, *components): + 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): + for component in components: + self._to_apply.append((type(component), component)) + self._world.to_apply.add(self) + + def remove(self, *component_types: type): + 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: + return self._components[component_type] - def __getitem__(self, component_type): - return self.components[component_type] - - def __setitem__(self, component_type, component): - self.components[component_type] = component - self.world.mapping.setdefault(component_type, set()).add(self) - - def __delitem__(self, component_type): - del self.components[component_type] - self.world.mapping[component_type].remove(self) - if len(self.world.mapping[component_type]) == 0: - del self.world.mapping[component_type] - - def __contains__(self, component_type): - return component_type in self.components - - def __del__(self): # TODO: Réparer ça marche pas car il n'est pas détruit car il est toujours dans World - self.world.entities.remove(self) - for component_type in self.components: - self.world.mapping[component_type].remove(self) - if len(self.world.mapping[component_type]) == 0: - del self.world.mapping[component_type] + def __contains__(self, component_type: type) -> bool: + return component_type in self._components \ No newline at end of file diff --git a/main.py b/main.py index cdc7757..e05b7eb 100644 --- a/main.py +++ b/main.py @@ -1,31 +1,20 @@ -from ecs import World, Entity +from ecs import World # Création de composants pouvant être ajouté a des entitées -class Name(str): - pass -class Age(int): - pass +class Name(str): pass +class Age(int): pass # Création d'un monde world = World() -# Création d'une entité -david = Entity(world) -david[Name] = Name("David") -david[Age] = Age(25) +# 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)) -# Création d'une autre entité -fred = Entity(world) -fred[Name] = Name("Fred") -fred[Age] = Age(30) - -# Création d'une autre entité -paul_sans_age = Entity(world) -paul_sans_age[Name] = Name("Paul") - -# Création d'une autre entité -age_tout_cour = Entity(world) -age_tout_cour[Age] = 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): @@ -35,6 +24,12 @@ 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]) @@ -47,3 +42,11 @@ 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() + From e9076fa726c1d2cc1b008b4e91c95354aa51978a Mon Sep 17 00:00:00 2001 From: Tipragot Date: Sun, 22 Oct 2023 22:22:17 +0200 Subject: [PATCH 3/9] =?UTF-8?q?On=20rend=20priv=C3=A9es=20les=20composants?= =?UTF-8?q?=20interne=20de=20l'ECS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ecs.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/ecs.py b/ecs.py index 1a6539b..886c77c 100644 --- a/ecs.py +++ b/ecs.py @@ -3,42 +3,42 @@ from typing import Iterator class World: def __init__(self): - self.to_apply: set[Entity] = set() - self.entities: set[Entity] = set() - self.mapping: dict[type, set[Entity]] = {} + self._to_apply: set[Entity] = set() + self._entities: set[Entity] = set() + self._mapping: dict[type, set[Entity]] = {} def create_entity(self, *components): return Entity(self, *components) def remove_entity(self, entity: 'Entity'): entity._deleted = True - self.to_apply.add(entity) + self._to_apply.add(entity) def apply(self): - for entity in self.to_apply: + for entity in self._to_apply: if entity._deleted: - self.entities.remove(entity) + self._entities.remove(entity) for component_type in entity._components: - self.mapping[component_type].remove(entity) + self._mapping[component_type].remove(entity) else: - self.entities.add(entity) + 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) + self._mapping[component_type].remove(entity) else: entity._components[component_type] = component - self.mapping.setdefault(component_type, set()).add(entity) + self._mapping.setdefault(component_type, set()).add(entity) entity._to_apply.clear() def query(self, *needed: type, without: tuple[type] = []) -> Iterator['Entity']: if not needed: - for entity in self.entities: + 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 \ + 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 @@ -48,18 +48,18 @@ class Entity: 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) + self._world._to_apply.add(self) def set(self, *components): for component in components: self._to_apply.append((type(component), component)) - self._world.to_apply.add(self) + self._world._to_apply.add(self) def remove(self, *component_types: type): 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) + self._world._to_apply.add(self) def __getitem__(self, component_type: type) -> object: return self._components[component_type] From 980badc60140b6760f97c80f9fa89db2bf778cb6 Mon Sep 17 00:00:00 2001 From: Tipragot Date: Sun, 22 Oct 2023 22:49:58 +0200 Subject: [PATCH 4/9] =?UTF-8?q?Ajout=20d'un=20syst=C3=A8me=20de=20ressourc?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ecs.py | 22 ++++++++++++++++++++++ main.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/ecs.py b/ecs.py index 886c77c..0a6340a 100644 --- a/ecs.py +++ b/ecs.py @@ -6,6 +6,8 @@ class World: 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): return Entity(self, *components) @@ -13,6 +15,15 @@ class World: def remove_entity(self, entity: 'Entity'): entity._deleted = True self._to_apply.add(entity) + + def set(self, *resources): + for resource in resources: + self._to_apply_resources.append((type(resource), resource)) + + def remove(self, *resource_types): + for resource_type in resource_types: + if resource_type in self._resources: + self._to_apply_resources.append((resource_type, None)) def apply(self): for entity in self._to_apply: @@ -30,6 +41,11 @@ class World: 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']: if not needed: @@ -41,6 +57,12 @@ class World: 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: + return self._resources[resource_type] + + def __contains__(self, resource_type: type) -> bool: + return resource_type in self._resources class Entity: def __init__(self, world: World, *components): diff --git a/main.py b/main.py index e05b7eb..60c66fc 100644 --- a/main.py +++ b/main.py @@ -50,3 +50,33 @@ for entity in world.query(): 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) + From 4e138af4ec95b43b097b94c01b11e7a91c57284c Mon Sep 17 00:00:00 2001 From: Tipragot Date: Sun, 22 Oct 2023 23:11:44 +0200 Subject: [PATCH 5/9] Ajout de commentaires --- ecs.py | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/ecs.py b/ecs.py index 0a6340a..0129649 100644 --- a/ecs.py +++ b/ecs.py @@ -2,7 +2,14 @@ from typing import Iterator 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]] = {} @@ -10,22 +17,53 @@ class World: 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) @@ -48,6 +86,17 @@ class World: 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): @@ -59,13 +108,41 @@ class World: 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] = {} @@ -73,18 +150,48 @@ class Entity: 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 \ No newline at end of file From 51c6dbc65afc0d337d675e02a7d5d8453f711e08 Mon Sep 17 00:00:00 2001 From: Tipragot Date: Mon, 23 Oct 2023 12:14:28 +0200 Subject: [PATCH 6/9] Fix formatting --- .vscode/settings.json | 5 +++++ ecs.py | 44 +++++++++++++++++++++++++------------------ main.py | 16 ++++++++++++---- tasks.py | 29 ++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 22 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 tasks.py 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/ecs.py b/ecs.py index 0129649..6b89957 100644 --- a/ecs.py +++ b/ecs.py @@ -15,7 +15,7 @@ class World: 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. @@ -28,7 +28,7 @@ class World: """ return Entity(self, *components) - def remove_entity(self, entity: 'Entity'): + def remove_entity(self, entity: "Entity"): """ Supprime une entité du monde. @@ -37,7 +37,7 @@ class World: """ entity._deleted = True self._to_apply.add(entity) - + def set(self, *resources): """ Définit les ressources données en paramètres. @@ -48,7 +48,7 @@ class World: """ 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. @@ -81,14 +81,16 @@ class World: 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 + 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']: + + 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. @@ -103,10 +105,12 @@ class World: 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): + 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. @@ -118,7 +122,7 @@ class World: 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. @@ -131,10 +135,12 @@ class World: """ 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. @@ -144,11 +150,13 @@ class Entity: *components: Les composants de l'entité. """ self._world = world - self._to_apply: list[tuple[type, object]] = [(type(component), component) for component in components] + 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. @@ -159,7 +167,7 @@ class Entity: 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. @@ -171,7 +179,7 @@ class Entity: 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. @@ -194,4 +202,4 @@ class Entity: Retourne: Si le composant de type *component_type existe. """ - return component_type in self._components \ No newline at end of file + return component_type in self._components diff --git a/main.py b/main.py index 60c66fc..0eea953 100644 --- a/main.py +++ b/main.py @@ -1,8 +1,14 @@ from ecs import World + # Création de composants pouvant être ajouté a des entitées -class Name(str): pass -class Age(int): pass +class Name(str): + pass + + +class Age(int): + pass + # Création d'un monde world = World() @@ -50,8 +56,11 @@ for entity in world.query(): print(entity[Age], end=" ") print() + # Création d'une ressource pouvant être ajoutée a un monde -class Gravity(float): pass +class Gravity(float): + pass + # On peut aussi ajouter des ressources globales world.set(Gravity(9.81)) @@ -79,4 +88,3 @@ world.apply() print("On vérifie que la ressource Gravity n'existe plus") print(Gravity in world) - diff --git a/tasks.py b/tasks.py new file mode 100644 index 0000000..437e391 --- /dev/null +++ b/tasks.py @@ -0,0 +1,29 @@ +from typing import Callable +from ecs import World + + +class TaskManager: + """ + Contient des fonctions de logique s'appliquant sur un monde. + """ + + def __init__(self): + """ + Créer un gestionnaire de taches vide. + """ + self.tasks: dict[int, list[Callable[[World], None]]] = {} + + def add(self, priority: int, task: Callable[[World], None]): + """ + Ajoute une tache à un gestionnaire de taches. + """ + self.tasks.setdefault(priority, []).append(task) + + def run(self, world: World): + """ + Exécute toutes les taches. + """ + priorities = sorted(list(self.tasks.keys())) + for priority in priorities: + for task in self.tasks[priority]: + task(world) From 94c496263867117785e2ebf603c67bf73813216e Mon Sep 17 00:00:00 2001 From: Tipragot Date: Mon, 23 Oct 2023 13:15:50 +0200 Subject: [PATCH 7/9] =?UTF-8?q?Syst=C3=A8me=20de=20boucle=20de=20jeu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ecs.py => engine.py | 102 +++++++++++++++++++++++++++++++++++++++++++- examples/ecs.py | 90 ++++++++++++++++++++++++++++++++++++++ main.py | 97 ++++------------------------------------- tasks.py | 29 ------------- 4 files changed, 200 insertions(+), 118 deletions(-) rename ecs.py => engine.py (67%) create mode 100644 examples/ecs.py delete mode 100644 tasks.py diff --git a/ecs.py b/engine.py similarity index 67% rename from ecs.py rename to engine.py index 6b89957..67bfbcc 100644 --- a/ecs.py +++ b/engine.py @@ -1,4 +1,5 @@ -from typing import Iterator +from typing import Iterator, Callable +from logging import error class World: @@ -203,3 +204,102 @@ class Entity: 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) + world.apply() + self._run_tasks(self._startup_tasks, world) + + 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 index 0eea953..9f67c6f 100644 --- a/main.py +++ b/main.py @@ -1,90 +1,11 @@ -from ecs import World +from engine import Game -# 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) +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/tasks.py b/tasks.py deleted file mode 100644 index 437e391..0000000 --- a/tasks.py +++ /dev/null @@ -1,29 +0,0 @@ -from typing import Callable -from ecs import World - - -class TaskManager: - """ - Contient des fonctions de logique s'appliquant sur un monde. - """ - - def __init__(self): - """ - Créer un gestionnaire de taches vide. - """ - self.tasks: dict[int, list[Callable[[World], None]]] = {} - - def add(self, priority: int, task: Callable[[World], None]): - """ - Ajoute une tache à un gestionnaire de taches. - """ - self.tasks.setdefault(priority, []).append(task) - - def run(self, world: World): - """ - Exécute toutes les taches. - """ - priorities = sorted(list(self.tasks.keys())) - for priority in priorities: - for task in self.tasks[priority]: - task(world) From ab57f1c69cb2ec0a18d5b8f85009595aea67a808 Mon Sep 17 00:00:00 2001 From: Tipragot Date: Mon, 23 Oct 2023 15:56:40 +0200 Subject: [PATCH 8/9] Fix startup update --- engine.py | 1 + 1 file changed, 1 insertion(+) diff --git a/engine.py b/engine.py index 67bfbcc..d65ceb9 100644 --- a/engine.py +++ b/engine.py @@ -281,6 +281,7 @@ class Game: world.set(self) world.apply() self._run_tasks(self._startup_tasks, world) + world.apply() while self._running: # On exécute les taches de mise a jour du monde From a6d5109832da9b9336c0cb2ed9263f37c7e9fb5f Mon Sep 17 00:00:00 2001 From: Tipragot Date: Mon, 23 Oct 2023 15:57:55 +0200 Subject: [PATCH 9/9] Ajout de commentaires --- engine.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/engine.py b/engine.py index d65ceb9..2907517 100644 --- a/engine.py +++ b/engine.py @@ -279,8 +279,14 @@ class Game: # 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: