Merge pull request 'Ajout du système de rendu, d'input et de fenêtre' (#7) from pygame into main

Reviewed-on: #7
This commit is contained in:
Tipragot 2023-10-25 17:59:13 +00:00 committed by Gitea
commit de14b06c55
No known key found for this signature in database
9 changed files with 596 additions and 21 deletions

View file

@ -147,6 +147,15 @@ class World:
resource: _T = self._resources[resource_type] # type: ignore[assignment]
return resource
def __delitem__(self, resource_type: Type[_T]) -> None:
"""
Supprime la ressource de type *resource_type.
Paramètres:
resource_type: Le type de ressource.
"""
self.remove(resource_type)
def __contains__(self, *resource_types: Type[_T]) -> bool:
"""
Renvoie True si le monde contient toutes les ressources de *resource_types.
@ -219,6 +228,15 @@ class Entity:
"""
return self._components[component_type]
def __delitem__(self, component_type: Type[_T]) -> None:
"""
Supprime le composant de type *component_type.
Paramètres:
component_type: Le type du composant.
"""
self.remove(component_type)
def __contains__(self, *component_types: Type[_T]) -> bool:
"""
Renvoie True si l'entité contient tous les composants de *component_types.
@ -264,15 +282,29 @@ class Game:
*plugins: Les plugins a ajouter au jeu.
"""
self._running = False
self._pre_startup_tasks: list[Callable[[World], None]] = []
self._startup_tasks: list[Callable[[World], None]] = []
self._post_startup_tasks: list[Callable[[World], None]] = []
self._pre_update_tasks: list[Callable[[World], None]] = []
self._update_tasks: list[Callable[[World], None]] = []
self._post_update_tasks: list[Callable[[World], None]] = []
self._pre_render_tasks: list[Callable[[World], None]] = []
self._render_tasks: list[Callable[[World], None]] = []
self._post_render_tasks: list[Callable[[World], None]] = []
self._pre_shutdown_tasks: list[Callable[[World], None]] = []
self._shutdown_tasks: list[Callable[[World], None]] = []
self._post_shutdown_tasks: list[Callable[[World], None]] = []
for plugin in plugins:
plugin.apply(self)
def add_pre_startup_tasks(self, *tasks: Callable[[World], None]) -> None:
"""
Ajoute des taches qui s'executeront en premier avant le démarrage du jeu.
"""
if self._running:
raise RuntimeError("Cannot add a task while the loop is running")
self._pre_startup_tasks.extend(tasks)
def add_startup_tasks(self, *tasks: Callable[[World], None]) -> None:
"""
Ajoute des taches qui s'executeront au démarrage du jeu.
@ -284,6 +316,14 @@ class Game:
raise RuntimeError("Cannot add a task while the loop is running")
self._startup_tasks.extend(tasks)
def add_post_startup_tasks(self, *tasks: Callable[[World], None]) -> None:
"""
Ajoute des taches qui s'executeront en dernier après le démarrage du jeu.
"""
if self._running:
raise RuntimeError("Cannot add a task while the loop is running")
self._post_startup_tasks.extend(tasks)
def add_pre_update_tasks(self, *tasks: Callable[[World], None]) -> None:
"""
Ajoute des taches qui s'executeront au debut de chaque mise à jour du jeu.
@ -317,6 +357,17 @@ class Game:
raise RuntimeError("Cannot add a task while the loop is running")
self._post_update_tasks.extend(tasks)
def add_pre_render_tasks(self, *tasks: Callable[[World], None]) -> None:
"""
Ajoute des taches qui s'executeront au début de chaque mise à jour du jeu pour le rendu.
Paramètres:
*tasks: Les taches à ajouter.
"""
if self._running:
raise RuntimeError("Cannot add a task while the loop is running")
self._pre_render_tasks.extend(tasks)
def add_render_tasks(self, *tasks: Callable[[World], None]) -> None:
"""
Ajoute des taches qui s'executeront après chaque mise à jour du jeu pour le rendu.
@ -328,6 +379,25 @@ class Game:
raise RuntimeError("Cannot add a task while the loop is running")
self._render_tasks.extend(tasks)
def add_post_render_tasks(self, *tasks: Callable[[World], None]) -> None:
"""
Ajoute des taches qui s'executeront après chaque mise à jour du jeu pour le rendu.
Paramètres:
*tasks: Les taches à ajouter.
"""
if self._running:
raise RuntimeError("Cannot add a task while the loop is running")
self._post_render_tasks.extend(tasks)
def add_pre_shutdown_tasks(self, *tasks: Callable[[World], None]) -> None:
"""
Ajoute des taches qui s'executeront au début de la fin de la boucle de jeu.
"""
if self._running:
raise RuntimeError("Cannot add a task while the loop is running")
self._pre_shutdown_tasks.extend(tasks)
def add_shutdown_tasks(self, *tasks: Callable[[World], None]) -> None:
"""
Ajoute des taches qui s'executeront à la fin de la boucle de jeu.
@ -339,6 +409,14 @@ class Game:
raise RuntimeError("Cannot add a task while the loop is running")
self._shutdown_tasks.extend(tasks)
def add_post_shutdown_tasks(self, *tasks: Callable[[World], None]) -> None:
"""
Ajoute des taches qui s'executeront à la fin de la fin de la boucle de jeu.
"""
if self._running:
raise RuntimeError("Cannot add a task while the loop is running")
self._post_shutdown_tasks.extend(tasks)
def run(self, world: World = World()) -> World:
"""
Lance la boucle de jeu.
@ -351,6 +429,14 @@ class Game:
world.set(self)
world.apply()
# On execute les taches de pré initialisation du monde
for task in self._pre_startup_tasks:
try:
task(world)
except Exception as e:
error(f"Error during pre-startup task: {e}")
world.apply()
# On execute les taches d'initialisation du monde
for task in self._startup_tasks:
try:
@ -359,6 +445,14 @@ class Game:
error(f"Error during startup task: {e}")
world.apply()
# On execute les taches de post initialisation du monde
for task in self._post_startup_tasks:
try:
task(world)
except Exception as e:
error(f"Error during post-startup task: {e}")
world.apply()
while self._running:
# On execute les taches de pré mise à jour du monde
for task in self._pre_update_tasks:
@ -392,6 +486,22 @@ class Game:
error(f"Error during render task: {e}")
world.apply()
# On execute les taches de fin de rendu du jeu
for task in self._post_render_tasks:
try:
task(world)
except Exception as e:
error(f"Error during post-render task: {e}")
world.apply()
# On execute les taches de pré fin de boucle
for task in self._pre_shutdown_tasks:
try:
task(world)
except Exception as e:
error(f"Error during pre-shutdown task: {e}")
world.apply()
# On exécute les taches de fin du monde
for task in self._shutdown_tasks:
try:
@ -400,6 +510,14 @@ class Game:
error(f"Error during shutdown task: {e}")
world.apply()
# On execute les taches de post fin de boucle
for task in self._post_shutdown_tasks:
try:
task(world)
except Exception as e:
error(f"Error during post-shutdown task: {e}")
world.apply()
# On retourne le monde
return world

101
engine/math.py Normal file
View file

@ -0,0 +1,101 @@
"""
Définis des classes utiles.
"""
from typing import SupportsFloat, Union
import math
class Vec2:
"""
Un vecteur 2D
"""
def __init__(self, *args: Union[SupportsFloat, "Vec2"]) -> None:
if (
len(args) == 2
and isinstance(args[0], SupportsFloat)
and isinstance(args[1], SupportsFloat)
):
self.x = float(args[0])
self.y = float(args[1])
elif len(args) == 1:
if isinstance(args[0], Vec2):
self.x = args[0].x
self.y = args[0].y
elif isinstance(args[0], SupportsFloat):
self.x = float(args[0])
self.y = float(args[0])
else:
raise ValueError("Invalid argument")
elif len(args) == 0:
self.x = 0.0
self.y = 0.0
else:
raise ValueError("Invalid number of arguments")
def __add__(self, other: object) -> "Vec2":
if isinstance(other, Vec2):
return Vec2(self.x + other.x, self.y + other.y)
elif isinstance(other, float):
return Vec2(self.x + other, self.y + other)
raise ValueError(
f"Unsupported operand type(s) for +: 'Vec2' and '{type(other)}'"
)
def __sub__(self, other: object) -> "Vec2":
if isinstance(other, Vec2):
return Vec2(self.x - other.x, self.y - other.y)
elif isinstance(other, float):
return Vec2(self.x - other, self.y - other)
raise ValueError(
f"Unsupported operand type(s) for -: 'Vec2' and '{type(other)}'"
)
def __mul__(self, other: object) -> "Vec2":
if isinstance(other, Vec2):
return Vec2(self.x * other.x, self.y * other.y)
elif isinstance(other, float):
return Vec2(self.x * other, self.y * other)
raise ValueError(
f"Unsupported operand type(s) for *: 'Vec2' and '{type(other)}'"
)
def __truediv__(self, other: object) -> "Vec2":
if isinstance(other, Vec2):
return Vec2(self.x / other.x, self.y / other.y)
elif isinstance(other, float):
return Vec2(self.x / other, self.y / other)
raise ValueError(
f"Unsupported operand type(s) for /: 'Vec2' and '{type(other)}'"
)
def __eq__(self, other: object) -> bool:
if isinstance(other, Vec2):
return self.x == other.x and self.y == other.y
return False
def __hash__(self) -> int:
return hash((self.x, self.y))
def __neg__(self) -> "Vec2":
return Vec2(-self.x, -self.y)
@property
def length(self) -> float:
"""
Retourne la longueur du vecteur.
"""
return math.sqrt(self.x**2 + self.y**2)
@property
def normalized(self) -> "Vec2":
"""
Retourne une version normalisé du vecteur.
"""
length = self.length
return Vec2(self.x / length, self.y / length)
def __repr__(self) -> str:
return f"Vec2({self.x}, {self.y})"

227
engine/plugins/pygame.py Normal file
View file

@ -0,0 +1,227 @@
"""
Définit un plugin qui gère les évenements pygame.
"""
from engine import *
from engine.math import Vec2
import pygame
class PygamePlugin(Plugin):
"""
Plugin qui gère les évenements pygame.
"""
@staticmethod
def _find_surface_rect() -> tuple[float, float, float, float]:
width, height = pygame.display.get_surface().get_size()
if width / height < Display.RATIO:
target_height = width * (Display.INVERT_RATIO)
offset = (height - target_height) / 2
rect = (0.0, offset, float(width), target_height)
else:
target_width = height * (Display.RATIO)
offset = (width - target_width) / 2
rect = (offset, 0.0, target_width, float(height))
return rect
@staticmethod
def _initialize(world: World) -> None:
"""
Initialize pygame et les ressources.
"""
pygame.init()
pygame.display.set_mode((800, 600), pygame.RESIZABLE)
# Initialisation des ressources
world.set(
Display(),
Keyboard(),
Mouse(),
)
@staticmethod
def _check_events(world: World) -> None:
"""
Met a jour les ressources avec les evenements pygame.
"""
keyboard = world[Keyboard]
keyboard.pressed.clear()
keyboard.released.clear()
mouse = world[Mouse]
mouse.pressed.clear()
mouse.released.clear()
for event in pygame.event.get():
if event.type == pygame.QUIT:
world[Game].stop()
elif event.type == pygame.KEYDOWN:
key_name = pygame.key.name(event.key)
if key_name == "f11":
pygame.display.toggle_fullscreen()
keyboard.keys.add(key_name)
keyboard.pressed.add(key_name)
elif event.type == pygame.KEYUP:
key_name = pygame.key.name(event.key)
if key_name == "f11":
continue
keyboard.keys.remove(key_name)
keyboard.released.add(key_name)
elif event.type == pygame.MOUSEBUTTONDOWN:
mouse.buttons.add(event.button)
mouse.pressed.add(event.button)
elif event.type == pygame.MOUSEBUTTONUP:
mouse.buttons.remove(event.button)
mouse.released.add(event.button)
elif event.type == pygame.MOUSEMOTION:
rect = PygamePlugin._find_surface_rect()
mouse.position = Vec2(
((event.pos[0] - rect[0]) / rect[2]) * Display.WIDTH,
((event.pos[1] - rect[1]) / rect[3]) * Display.HEIGHT,
)
@staticmethod
def _update_display(world: World) -> None:
"""
Met a jour le rendu de l'écran.
"""
display = world[Display]
rect = PygamePlugin._find_surface_rect()
pygame.transform.set_smoothscale_backend("MMX")
pygame.transform.smoothscale(
display._surface,
(rect[2], rect[3]),
pygame.display.get_surface().subsurface(rect),
)
pygame.display.flip()
display._surface.fill((0, 0, 0))
@staticmethod
def _terminate(world: World) -> None:
"""
Ferme pygame.
"""
pygame.quit()
def apply(self, game: Game) -> None:
"""
Applique le plugin a un jeu.
Paramètres:
game: Le jeu auquel appliquer le plugin.
"""
game.add_pre_startup_tasks(self._initialize)
game.add_pre_update_tasks(self._check_events)
game.add_post_render_tasks(self._update_display)
game.add_post_shutdown_tasks(self._terminate)
class Display:
"""
Ressource qui represente la fenetre du jeu.
"""
WIDTH = 1080.0
HEIGHT = 810.0
RATIO = WIDTH / HEIGHT
INVERT_RATIO = HEIGHT / WIDTH
def __init__(self) -> None:
self._surface = pygame.Surface((Display.WIDTH, Display.HEIGHT))
class Keyboard:
"""
Ressource qui représente les entrées utilisateurs sur le clavier à la frame actuelle.
"""
def __init__(self) -> None:
self.keys: set[str] = set()
self.pressed: set[str] = set()
self.released: set[str] = set()
def is_key_pressed(self, key_name: str) -> bool:
"""
Renvoie True si la touche *key_name* a commencé a être appuyée pendant la frame actuelle.
Paramètres:
key_name: Le nom de la touche à tester.
Retourne:
True si la touche *key_name* a commencé a être appuyée pendant la frame actuelle.
"""
return key_name in self.pressed
def is_key(self, key_name: str) -> bool:
"""
Renvoie True si la touche *key_name* est actuellement appuyée.
Paramètres:
key_name: Le nom de la touche à tester.
Retourne:
True si la touche *key_name* est actuellement appuyée.
"""
return key_name in self.keys
def is_key_released(self, key_name: str) -> bool:
"""
Renvoie True si la touche *key_name* a été relachée pendant la frame actuelle.
Paramètres:
key_name: Le nom de la touche à tester.
Retourne:
True si la touche *key_name* a été relachée pendant la frame actuelle.
"""
return key_name in self.released
class Mouse:
"""
Ressource qui représente l'état de la souris à la frame actuelle.
"""
def __init__(self) -> None:
self.buttons: set[int] = set()
self.pressed: set[int] = set()
self.released: set[int] = set()
self.position: Vec2 = Vec2(0.0, 0.0)
def is_button_pressed(self, button: int) -> bool:
"""
Renvoie True si le bouton *button* a commencé a être appuyée pendant la frame actuelle.
Paramètres:
button: Le numéro du bouton à tester.
Retourne:
True si le bouton *button* a commencé a être appuyée pendant la frame actuelle.
"""
return button in self.pressed
def is_button(self, button: int) -> bool:
"""
Renvoie True si le bouton *button* est actuellement appuyé.
Paramètres:
button: Le numéro du bouton à tester.
Retourne:
True si le bouton *button* est actuellement appuyé.
"""
return button in self.buttons
def is_button_released(self, button: int) -> bool:
"""
Renvoie True si le bouton *button* a été relaché pendant la frame actuelle.
Paramètres:
button: Le numéro du bouton à tester.
Retourne:
True si le bouton *button* aLongrightarrow relaché pendant la frame actuelle.
"""
return button in self.released

91
engine/plugins/render.py Normal file
View file

@ -0,0 +1,91 @@
"""
Définis un plugin permettant d'afficher des choses a l'écran.
"""
from engine import *
from engine.math import Vec2
import pygame, os
from engine.plugins.pygame import Display
class RenderPlugin(Plugin):
"""
Plugin permettant d'afficher des choses a l'écran.
"""
@staticmethod
def _initialize(world: World) -> None:
"""
Initialize le système de rendu.
"""
world.set(TextureManager(world[Display]))
@staticmethod
def _render(world: World) -> None:
"""
Fais le rendu des sprites.
"""
display = world[Display]
textures = world[TextureManager]
# Rendu de toutes les objects de rendu
entities = sorted(world.query(Order, Position), key=lambda e: e[Order])
for entity in entities:
# Récupération de la position des entitées
position = entity[Position]
# Affichage de la texture
if Texture in entity:
display._surface.blit(
textures[entity[Texture]], (position.x, position.y)
)
def apply(self, game: Game) -> None:
"""
Applique le plugin a un jeu.
Paramarters:
game: Le jeu auquel appliquer le plugin.
"""
game.add_post_startup_tasks(self._initialize)
game.add_render_tasks(self._render)
class Position(Vec2):
"""
Composant qui représente la position d'une entité.
"""
class Order(int):
"""
Composant qui represente l'ordre d'affichage d'un objet.
"""
class TextureManager:
"""
Ressource qui contient les textures du jeu.
"""
def __init__(self, display: Display) -> None:
self._textures: dict[str, pygame.Surface] = {}
for file in os.listdir("textures"):
self._textures[file] = pygame.image.load(f"textures/{file}").convert(
display._surface
)
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(display._surface)
def __getitem__(self, key: str) -> pygame.Surface:
return self._textures.get(key, self._error_texture)
class Texture(str):
"""
Composant qui represente la texture d'un sprite.
"""

View file

@ -27,7 +27,7 @@ class TimePlugin(Plugin):
Paramètres:
game: Le jeu auquel appliquer le plugin.
"""
game.add_startup_tasks(self._initialize_time)
game.add_pre_startup_tasks(self._initialize_time)
game.add_pre_update_tasks(self._update_time)

75
main.py
View file

@ -4,32 +4,69 @@ Ceci est un exemple de comment l'on peut utiliser le moteur du jeu.
from engine import *
from engine.plugins.timing import TimePlugin, Time
from engine.math import Vec2
from engine.plugins.render import Order, RenderPlugin, Position, Texture
from engine.plugins.timing import Delta, TimePlugin
from engine.plugins.pygame import Display, Keyboard, PygamePlugin
from random import random
# Initialisation
game = Game(TimePlugin())
game = Game(TimePlugin(), PygamePlugin(), RenderPlugin())
# Ajout de tache au démarage (l'ordre d'ajout est important)
game.add_startup_tasks(lambda world: print("Hello first"))
game.add_startup_tasks(lambda world: print("Hello second"))
game.add_startup_tasks(lambda world: print("Hello third"))
game.add_startup_tasks(lambda world: print("Hello last"))
# Ajoute de tache au mise à jour (malgré le world[Game].stop(), la boucle termine les taches suivantes)
game.add_pre_update_tasks(lambda world: print("Pre Update"))
game.add_pre_update_tasks(lambda world: print(world[Time]))
game.add_update_tasks(lambda world: world[Game].stop())
game.add_post_update_tasks(lambda world: print("Post Update"))
# On créer une tache pour afficher des sprites
def spawn_sprites(world: World) -> None:
"""
Ajoute des sprites au monde.
"""
for i in range(100):
red = random() < 0.1
world.create_entity(
Position(random() * Display.WIDTH, random() * Display.HEIGHT),
Texture("test2.png") if red else Texture("test.png"),
Order(1 if red else 0),
)
# Ajout de tache au rendu
game.add_render_tasks(lambda world: print("Render task 1"))
game.add_render_tasks(lambda world: print("Render task 2"))
game.add_render_tasks(lambda world: print("Render task 3"))
# Ajout de tache à la fin
game.add_shutdown_tasks(lambda world: print("Bye first"))
game.add_shutdown_tasks(lambda world: print("Bye second"))
# On ajoutant la tache
game.add_startup_tasks(spawn_sprites)
def move_sprites(world: World) -> None:
"""
Change la position des sprites.
"""
move = Vec2(
(-1.0 if world[Keyboard].is_key("q") else 0.0)
+ (1.0 if world[Keyboard].is_key("d") else 0.0),
(-1.0 if world[Keyboard].is_key("z") else 0.0)
+ (1.0 if world[Keyboard].is_key("s") else 0.0),
)
for entity in world.query(Position):
if entity[Order] == 1:
continue
entity.set(Position(entity[Position] + (move * world[Delta] * 1000.0)))
# On ajoute la tache
game.add_update_tasks(move_sprites)
# On créer une tache pour tester si les plugins fonctionnent
def salutations(world: World) -> None:
"""
Affiche "Bonjour" si la touche B est pressé et "Au revoir" si la touche B est relachée.
"""
if world[Keyboard].is_key_pressed("b"):
print("Bonjour")
if world[Keyboard].is_key_released("b"):
print("Au revoir")
# On ajoute la tache de test
game.add_update_tasks(salutations)
# On lance la boucle
game.run()

View file

@ -1,2 +1,3 @@
mypy
pylint
pylint
pygame

BIN
textures/test.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
textures/test2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB