Gestion du survol, click et animations

This commit is contained in:
Tipragot 2023-10-28 23:23:23 +02:00
parent 19daf8ba15
commit 3108b4aa20
2 changed files with 312 additions and 64 deletions

View file

@ -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()

View file

@ -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",
) )