Gestion du survol, click et animations
This commit is contained in:
parent
19daf8ba15
commit
3108b4aa20
342
src/engine.py
342
src/engine.py
|
@ -4,9 +4,11 @@ Un moteur de jeu inspiré de bevy.
|
||||||
|
|
||||||
|
|
||||||
import glob
|
import glob
|
||||||
|
import json
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
from typing import Callable, Optional, Sequence, SupportsFloat, TypeVar, Union
|
from typing import Callable, Optional, Sequence, SupportsFloat, TypeVar, Union
|
||||||
|
from time import time
|
||||||
import pygame
|
import pygame
|
||||||
|
|
||||||
|
|
||||||
|
@ -139,8 +141,8 @@ class Vec2:
|
||||||
def __add__(self, other: object) -> "Vec2":
|
def __add__(self, other: object) -> "Vec2":
|
||||||
if isinstance(other, Vec2):
|
if isinstance(other, Vec2):
|
||||||
return Vec2(self.x + other.x, self.y + other.y)
|
return Vec2(self.x + other.x, self.y + other.y)
|
||||||
elif isinstance(other, float):
|
elif isinstance(other, SupportsFloat):
|
||||||
return Vec2(self.x + other, self.y + other)
|
return Vec2(self.x + float(other), self.y + float(other))
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Unsupported operand type(s) for +: 'Vec2' and '{type(other)}'"
|
f"Unsupported operand type(s) for +: 'Vec2' and '{type(other)}'"
|
||||||
)
|
)
|
||||||
|
@ -148,8 +150,8 @@ class Vec2:
|
||||||
def __sub__(self, other: object) -> "Vec2":
|
def __sub__(self, other: object) -> "Vec2":
|
||||||
if isinstance(other, Vec2):
|
if isinstance(other, Vec2):
|
||||||
return Vec2(self.x - other.x, self.y - other.y)
|
return Vec2(self.x - other.x, self.y - other.y)
|
||||||
elif isinstance(other, float):
|
elif isinstance(other, SupportsFloat):
|
||||||
return Vec2(self.x - other, self.y - other)
|
return Vec2(self.x - float(other), self.y - float(other))
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Unsupported operand type(s) for -: 'Vec2' and '{type(other)}'"
|
f"Unsupported operand type(s) for -: 'Vec2' and '{type(other)}'"
|
||||||
)
|
)
|
||||||
|
@ -157,8 +159,8 @@ class Vec2:
|
||||||
def __mul__(self, other: object) -> "Vec2":
|
def __mul__(self, other: object) -> "Vec2":
|
||||||
if isinstance(other, Vec2):
|
if isinstance(other, Vec2):
|
||||||
return Vec2(self.x * other.x, self.y * other.y)
|
return Vec2(self.x * other.x, self.y * other.y)
|
||||||
elif isinstance(other, float):
|
elif isinstance(other, SupportsFloat):
|
||||||
return Vec2(self.x * other, self.y * other)
|
return Vec2(self.x * float(other), self.y * float(other))
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Unsupported operand type(s) for *: 'Vec2' and '{type(other)}'"
|
f"Unsupported operand type(s) for *: 'Vec2' and '{type(other)}'"
|
||||||
)
|
)
|
||||||
|
@ -166,8 +168,8 @@ class Vec2:
|
||||||
def __truediv__(self, other: object) -> "Vec2":
|
def __truediv__(self, other: object) -> "Vec2":
|
||||||
if isinstance(other, Vec2):
|
if isinstance(other, Vec2):
|
||||||
return Vec2(self.x / other.x, self.y / other.y)
|
return Vec2(self.x / other.x, self.y / other.y)
|
||||||
elif isinstance(other, float):
|
elif isinstance(other, SupportsFloat):
|
||||||
return Vec2(self.x / other, self.y / other)
|
return Vec2(self.x / float(other), self.y / float(other))
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Unsupported operand type(s) for /: 'Vec2' and '{type(other)}'"
|
f"Unsupported operand type(s) for /: 'Vec2' and '{type(other)}'"
|
||||||
)
|
)
|
||||||
|
@ -202,34 +204,9 @@ class Vec2:
|
||||||
return f"Vec2({self.x}, {self.y})"
|
return f"Vec2({self.x}, {self.y})"
|
||||||
|
|
||||||
|
|
||||||
class Game:
|
|
||||||
"""
|
|
||||||
Un jeu.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.stop_requested = False
|
|
||||||
self.next_scene: Optional[str] = None
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
"""
|
|
||||||
Demande l'arrêt du jeu.
|
|
||||||
"""
|
|
||||||
self.stop_requested = True
|
|
||||||
|
|
||||||
def change_scene(self, scene_name: str):
|
|
||||||
"""
|
|
||||||
Demande un changement de scène.
|
|
||||||
|
|
||||||
Paramètres:
|
|
||||||
scene_name: Le nom de la scène dans laquelle aller.
|
|
||||||
"""
|
|
||||||
self.next_scene = scene_name
|
|
||||||
|
|
||||||
|
|
||||||
class Display:
|
class Display:
|
||||||
"""
|
"""
|
||||||
La fenêtre d'un jeu.
|
Une classe utilitaire pour la gestion de la fenêtre du jeu.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
WIDTH = 1440.0
|
WIDTH = 1440.0
|
||||||
|
@ -254,6 +231,120 @@ class Display:
|
||||||
return rect
|
return rect
|
||||||
|
|
||||||
|
|
||||||
|
class Game:
|
||||||
|
"""
|
||||||
|
Un ressource qui représente le jeu actuel.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.stop_requested = False
|
||||||
|
self.next_scene: Optional[str] = None
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""
|
||||||
|
Demande l'arrêt du jeu.
|
||||||
|
"""
|
||||||
|
self.stop_requested = True
|
||||||
|
|
||||||
|
def change_scene(self, scene_name: str):
|
||||||
|
"""
|
||||||
|
Demande un changement de scène.
|
||||||
|
|
||||||
|
Paramètres:
|
||||||
|
scene_name: Le nom de la scène dans laquelle aller.
|
||||||
|
"""
|
||||||
|
self.next_scene = scene_name
|
||||||
|
|
||||||
|
|
||||||
|
class Assets:
|
||||||
|
"""
|
||||||
|
Resource qui permet la gestion des assets.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, surface: pygame.Surface):
|
||||||
|
# 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(surface)
|
||||||
|
|
||||||
|
# Chargement des textures
|
||||||
|
self.__textures: dict[str, pygame.Surface] = {}
|
||||||
|
for file in glob.iglob("assets/textures/**/*.png", recursive=True):
|
||||||
|
self.__textures[file[16:].replace("\\", "/")] = pygame.image.load(
|
||||||
|
file
|
||||||
|
).convert_alpha(surface)
|
||||||
|
|
||||||
|
# Création du cache pour les polices
|
||||||
|
self.__fonts: dict[int, pygame.font.Font] = {}
|
||||||
|
|
||||||
|
def get_texture(self, name: str) -> pygame.Surface:
|
||||||
|
"""
|
||||||
|
Renvoie la texture qui correspond au nom *name*.
|
||||||
|
|
||||||
|
Paramètres:
|
||||||
|
name: Le nom de la texture.
|
||||||
|
|
||||||
|
Retourne:
|
||||||
|
La texture qui correspond au nom *name*.
|
||||||
|
"""
|
||||||
|
return self.__textures.get(name, self.__error_texture)
|
||||||
|
|
||||||
|
def get_texture_size(self, name: str) -> Vec2:
|
||||||
|
"""
|
||||||
|
Renvoie la taille de la texture qui correspond au nom *name*.
|
||||||
|
|
||||||
|
Paramètres:
|
||||||
|
name: Le nom de la texture.
|
||||||
|
|
||||||
|
Retourne:
|
||||||
|
La taille de la texture qui correspond au nom *name*.
|
||||||
|
"""
|
||||||
|
return Vec2(*self.get_texture(name).get_size())
|
||||||
|
|
||||||
|
def get_font(self, size: int) -> pygame.font.Font:
|
||||||
|
"""
|
||||||
|
Renvoie la police qui correspond à la taille *size*.
|
||||||
|
|
||||||
|
Paramètres:
|
||||||
|
size: La taille de la police.
|
||||||
|
|
||||||
|
Retourne:
|
||||||
|
La police qui correspond à la taille *size*.
|
||||||
|
"""
|
||||||
|
font = self.__fonts.get(size)
|
||||||
|
if font is None:
|
||||||
|
font = pygame.font.Font("assets/font.ttf", size)
|
||||||
|
self.__fonts[size] = font
|
||||||
|
return font
|
||||||
|
|
||||||
|
def get_text_size(self, text: str, size: int) -> Vec2:
|
||||||
|
"""
|
||||||
|
Renvoie la taille d'un texte avec une certaine taille de police.
|
||||||
|
|
||||||
|
Paramètres:
|
||||||
|
text: Le texte.
|
||||||
|
size: La taille de la police.
|
||||||
|
|
||||||
|
Retourne:
|
||||||
|
La taille d'un texte avec une certaine taille de police.
|
||||||
|
"""
|
||||||
|
return Vec2(*self.get_font(size).size(text))
|
||||||
|
|
||||||
|
|
||||||
|
class Time(float):
|
||||||
|
"""
|
||||||
|
Ressource qui represente le temps depuis 1900.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Delta(float):
|
||||||
|
"""
|
||||||
|
Ressource qui represente le temps depuis la dernière frame.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class Keyboard:
|
class Keyboard:
|
||||||
"""
|
"""
|
||||||
Ressource qui représente les entrées utilisateurs sur le clavier à la frame actuelle.
|
Ressource qui représente les entrées utilisateurs sur le clavier à la frame actuelle.
|
||||||
|
@ -355,6 +446,12 @@ class Position(Vec2):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Centered:
|
||||||
|
"""
|
||||||
|
Composant permettant de dire que l'affichage de l'entité doit être centré.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class Offset(Vec2):
|
class Offset(Vec2):
|
||||||
"""
|
"""
|
||||||
Composant qui represente un décalage de la position d'une entité.
|
Composant qui represente un décalage de la position d'une entité.
|
||||||
|
@ -373,6 +470,38 @@ class Texture(str):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class HoveredTexture(Texture):
|
||||||
|
"""
|
||||||
|
Composant qui represente la texture lorsque l'entité est survolée.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Animation:
|
||||||
|
"""
|
||||||
|
Composant qui représente une animation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
callback: Callable[[World, Entity], object] = lambda _w, _e: None,
|
||||||
|
):
|
||||||
|
self.name = name
|
||||||
|
self.callback = callback
|
||||||
|
|
||||||
|
with open(
|
||||||
|
f"assets/textures/animations/{name}/info.json",
|
||||||
|
"r",
|
||||||
|
encoding="utf-8",
|
||||||
|
) as f:
|
||||||
|
info = json.load(f)
|
||||||
|
self.end_image: str = info.get("end_image", "")
|
||||||
|
self.offset = Offset(info["offset"]["x"], info["offset"]["y"])
|
||||||
|
self.frame_count: int = info["frame_count"]
|
||||||
|
self.fps: int = info["fps"]
|
||||||
|
self.time = 0.0
|
||||||
|
|
||||||
|
|
||||||
class Text(str):
|
class Text(str):
|
||||||
"""
|
"""
|
||||||
Composant qui represente un texte.
|
Composant qui represente un texte.
|
||||||
|
@ -391,6 +520,33 @@ class Color(pygame.Color):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class HoverEnter:
|
||||||
|
"""
|
||||||
|
Composant qui marque un entité comme commencée à être survolée.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Hovered:
|
||||||
|
"""
|
||||||
|
Composant qui marque un entité comme survolée.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class HoverExit:
|
||||||
|
"""
|
||||||
|
Composant qui marque un entité comme arreté d'être survolée.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Clickable:
|
||||||
|
"""
|
||||||
|
Composant qui marque un entité comme pouvant etre cliquer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, callback: Callable[[World, Entity], object]):
|
||||||
|
self.callback = callback
|
||||||
|
|
||||||
|
|
||||||
class Scene:
|
class Scene:
|
||||||
"""
|
"""
|
||||||
Une scène dans le jeu.
|
Une scène dans le jeu.
|
||||||
|
@ -433,22 +589,8 @@ def start_game(
|
||||||
keyboard = Keyboard()
|
keyboard = Keyboard()
|
||||||
mouse = Mouse()
|
mouse = Mouse()
|
||||||
|
|
||||||
# Création de la texture d'erreur
|
# Chargements des assets
|
||||||
error_texture = pygame.Surface((256, 256))
|
assets = Assets(surface)
|
||||||
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))
|
|
||||||
error_texture = error_texture.convert(surface)
|
|
||||||
|
|
||||||
# Chargement des textures
|
|
||||||
textures: dict[str, pygame.Surface] = {}
|
|
||||||
for file in glob.iglob("textures/**/*.png", recursive=True):
|
|
||||||
textures[file[9:].replace("\\", "/")] = pygame.image.load(file).convert_alpha(
|
|
||||||
surface
|
|
||||||
)
|
|
||||||
|
|
||||||
# Création du cache pour les polices
|
|
||||||
fonts: dict[int, pygame.font.Font] = {}
|
|
||||||
|
|
||||||
# On récupère la première scène
|
# On récupère la première scène
|
||||||
scene = scenes.get(start_scene)
|
scene = scenes.get(start_scene)
|
||||||
|
@ -458,8 +600,10 @@ def start_game(
|
||||||
# Initialisation du monde
|
# Initialisation du monde
|
||||||
world = World()
|
world = World()
|
||||||
world[Game] = Game()
|
world[Game] = Game()
|
||||||
|
world[Assets] = assets
|
||||||
world[Keyboard] = keyboard
|
world[Keyboard] = keyboard
|
||||||
world[Mouse] = mouse
|
world[Mouse] = mouse
|
||||||
|
world[Time] = Time(time())
|
||||||
|
|
||||||
# Initialisation de la scène
|
# Initialisation de la scène
|
||||||
for system in scene.init_systems:
|
for system in scene.init_systems:
|
||||||
|
@ -500,35 +644,111 @@ def start_game(
|
||||||
((event.pos[1] - rect[1]) / rect[3]) * Display.HEIGHT,
|
((event.pos[1] - rect[1]) / rect[3]) * Display.HEIGHT,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# On vérifie le survol des textures et textes
|
||||||
|
for entity in world.query(Position, Texture):
|
||||||
|
# Récupération de la position et taille de l'entité
|
||||||
|
position: Vec2 = entity[Position]
|
||||||
|
if Offset in entity:
|
||||||
|
position = position + entity[Offset]
|
||||||
|
size = assets.get_texture_size(entity[Texture])
|
||||||
|
if Centered in entity:
|
||||||
|
position -= size / 2
|
||||||
|
|
||||||
|
# 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 met à jour le temps et delta
|
||||||
|
now = time()
|
||||||
|
world[Delta] = Delta(now - world[Time])
|
||||||
|
world[Time] = Time(now)
|
||||||
|
|
||||||
|
# On execute les objets clickables
|
||||||
|
if mouse.is_button_pressed(1):
|
||||||
|
for entity in world.query(Clickable, Hovered):
|
||||||
|
entity[Clickable].callback(world, entity)
|
||||||
|
|
||||||
# On met à jour la scène
|
# On met à jour la scène
|
||||||
for system in scene.update_systems:
|
for system in scene.update_systems:
|
||||||
system(world)
|
system(world)
|
||||||
|
|
||||||
|
# Mise à jour des animations
|
||||||
|
for entity in world.query(Animation):
|
||||||
|
animation = entity[Animation]
|
||||||
|
if animation.time == 0:
|
||||||
|
if animation.end_image == "" and Texture in entity:
|
||||||
|
animation.end_image = entity[Texture]
|
||||||
|
animation.time += world[Delta]
|
||||||
|
frame_index = int(animation.time * animation.fps)
|
||||||
|
if frame_index >= animation.frame_count:
|
||||||
|
entity[Texture] = Texture(animation.end_image)
|
||||||
|
del entity[Animation]
|
||||||
|
del entity[Offset]
|
||||||
|
animation.callback(world, entity)
|
||||||
|
else:
|
||||||
|
entity[Offset] = Offset(animation.offset.x, animation.offset.y)
|
||||||
|
entity[Texture] = Texture(
|
||||||
|
f"animations/{animation.name}/{frame_index:04}.png"
|
||||||
|
)
|
||||||
|
|
||||||
# Rendu du monde
|
# Rendu du monde
|
||||||
entities = sorted(world.query(Order, Position), key=lambda e: e[Order])
|
for entity in sorted(world.query(Order, Position), key=lambda e: e[Order]):
|
||||||
for entity in entities:
|
|
||||||
# Récupération de la position de l'entité
|
# Récupération de la position de l'entité
|
||||||
position: Vec2 = entity[Position]
|
position: Vec2 = entity[Position]
|
||||||
if Offset in entity:
|
if Offset in entity:
|
||||||
position = position + entity[Offset]
|
position = position + entity[Offset]
|
||||||
|
centered = Centered in entity
|
||||||
|
|
||||||
# Affichage de la texture
|
# Affichage de la texture
|
||||||
if Texture in entity:
|
if Texture in entity:
|
||||||
|
if HoveredTexture in entity and Hovered in entity:
|
||||||
|
texture = assets.get_texture(entity[HoveredTexture])
|
||||||
|
else:
|
||||||
|
texture = assets.get_texture(entity[Texture])
|
||||||
surface.blit(
|
surface.blit(
|
||||||
textures.get(entity[Texture], error_texture),
|
texture,
|
||||||
(position.x, position.y),
|
(
|
||||||
|
position.x - texture.get_width() / 2
|
||||||
|
if centered
|
||||||
|
else position.x,
|
||||||
|
position.y - texture.get_height() / 2
|
||||||
|
if centered
|
||||||
|
else position.y,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Affichage des textes
|
# Affichage des textes
|
||||||
if Text in entity and TextSize in entity:
|
if Text in entity:
|
||||||
color = entity[Color] if Color in entity else Color(255, 255, 255)
|
color = entity[Color] if Color in entity else Color(255, 255, 255)
|
||||||
size = entity[TextSize]
|
size = entity[TextSize] if TextSize in entity else 50
|
||||||
font = fonts.get(size)
|
font = assets.get_font(size)
|
||||||
if font is None:
|
|
||||||
font = pygame.font.Font("font.ttf", size)
|
|
||||||
fonts[size] = font
|
|
||||||
font_surface = font.render(entity[Text], True, color)
|
font_surface = font.render(entity[Text], True, color)
|
||||||
surface.blit(font_surface, (position.x, position.y))
|
surface.blit(
|
||||||
|
font_surface,
|
||||||
|
(
|
||||||
|
position.x - font_surface.get_width() / 2
|
||||||
|
if centered
|
||||||
|
else position.x,
|
||||||
|
position.y - font_surface.get_height() / 2
|
||||||
|
if centered
|
||||||
|
else position.y,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
# Mise a jour de la fenêtre
|
# Mise a jour de la fenêtre
|
||||||
rect = Display._calculate_surface_rect()
|
rect = Display._calculate_surface_rect()
|
||||||
|
|
34
src/main.py
34
src/main.py
|
@ -2,11 +2,39 @@
|
||||||
Example de l'utilisation du moteur de jeu.
|
Example de l'utilisation du moteur de jeu.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from engine import Scene, start_game
|
from engine import (
|
||||||
|
Centered,
|
||||||
|
Clickable,
|
||||||
|
Display,
|
||||||
|
Entity,
|
||||||
|
Game,
|
||||||
|
HoveredTexture,
|
||||||
|
Order,
|
||||||
|
Position,
|
||||||
|
Scene,
|
||||||
|
Texture,
|
||||||
|
World,
|
||||||
|
start_game,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def initialize_world(world: World):
|
||||||
|
"""
|
||||||
|
Initialise le monde.
|
||||||
|
"""
|
||||||
|
Entity(
|
||||||
|
world,
|
||||||
|
Position(Display.WIDTH / 2, Display.HEIGHT / 2),
|
||||||
|
Order(0),
|
||||||
|
Centered(),
|
||||||
|
Texture("button_classique.png"),
|
||||||
|
HoveredTexture("button_classique_hover.png"),
|
||||||
|
Clickable(lambda w, e: w[Game].change_scene("test")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
start_game(
|
start_game(
|
||||||
{"menu": Scene([], [], [])},
|
{"example": Scene([initialize_world], [], []), "test": Scene([], [], [])},
|
||||||
"menu",
|
"example",
|
||||||
title="Guess The Number",
|
title="Guess The Number",
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue