Système d'entités et de composants #1
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
__pycache__
|
12
.vscode/launch.json
vendored
Normal file
12
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Python: Module",
|
||||||
|
"type": "python",
|
||||||
|
"request": "launch",
|
||||||
|
"module": "main",
|
||||||
|
"justMyCode": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"[python]": {
|
||||||
|
"editor.defaultFormatter": "ms-python.black-formatter"
|
||||||
|
}
|
||||||
|
}
|
312
engine.py
Normal file
312
engine.py
Normal file
|
@ -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
|
90
examples/ecs.py
Normal file
90
examples/ecs.py
Normal file
|
@ -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)
|
11
main.py
Normal file
11
main.py
Normal file
|
@ -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()
|
0
requirements.txt
Normal file
0
requirements.txt
Normal file
Loading…
Reference in a new issue