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 json
import math
import os
from typing import Callable, Optional, Sequence, SupportsFloat, TypeVar, Union
from time import time
import pygame
@ -139,8 +141,8 @@ class Vec2:
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)
elif isinstance(other, SupportsFloat):
return Vec2(self.x + float(other), self.y + float(other))
raise ValueError(
f"Unsupported operand type(s) for +: 'Vec2' and '{type(other)}'"
)
@ -148,8 +150,8 @@ class Vec2:
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)
elif isinstance(other, SupportsFloat):
return Vec2(self.x - float(other), self.y - float(other))
raise ValueError(
f"Unsupported operand type(s) for -: 'Vec2' and '{type(other)}'"
)
@ -157,8 +159,8 @@ class Vec2:
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)
elif isinstance(other, SupportsFloat):
return Vec2(self.x * float(other), self.y * float(other))
raise ValueError(
f"Unsupported operand type(s) for *: 'Vec2' and '{type(other)}'"
)
@ -166,8 +168,8 @@ class Vec2:
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)
elif isinstance(other, SupportsFloat):
return Vec2(self.x / float(other), self.y / float(other))
raise ValueError(
f"Unsupported operand type(s) for /: 'Vec2' and '{type(other)}'"
)
@ -202,34 +204,9 @@ class Vec2:
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:
"""
La fenêtre d'un jeu.
Une classe utilitaire pour la gestion de la fenêtre du jeu.
"""
WIDTH = 1440.0
@ -254,6 +231,120 @@ class Display:
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:
"""
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):
"""
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):
"""
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:
"""
Une scène dans le jeu.
@ -433,22 +589,8 @@ def start_game(
keyboard = Keyboard()
mouse = Mouse()
# 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))
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] = {}
# Chargements des assets
assets = Assets(surface)
# On récupère la première scène
scene = scenes.get(start_scene)
@ -458,8 +600,10 @@ def start_game(
# Initialisation du monde
world = World()
world[Game] = Game()
world[Assets] = assets
world[Keyboard] = keyboard
world[Mouse] = mouse
world[Time] = Time(time())
# Initialisation de la scène
for system in scene.init_systems:
@ -500,35 +644,111 @@ def start_game(
((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
for system in scene.update_systems:
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
entities = sorted(world.query(Order, Position), key=lambda e: e[Order])
for entity in entities:
for entity in sorted(world.query(Order, Position), key=lambda e: e[Order]):
# Récupération de la position de l'entité
position: Vec2 = entity[Position]
if Offset in entity:
position = position + entity[Offset]
centered = Centered in entity
# Affichage de la texture
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(
textures.get(entity[Texture], error_texture),
(position.x, position.y),
texture,
(
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
if Text in entity and TextSize in entity:
if Text in entity:
color = entity[Color] if Color in entity else Color(255, 255, 255)
size = entity[TextSize]
font = fonts.get(size)
if font is None:
font = pygame.font.Font("font.ttf", size)
fonts[size] = font
size = entity[TextSize] if TextSize in entity else 50
font = assets.get_font(size)
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
rect = Display._calculate_surface_rect()

View file

@ -2,11 +2,39 @@
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(
{"menu": Scene([], [], [])},
"menu",
{"example": Scene([initialize_world], [], []), "test": Scene([], [], [])},
"example",
title="Guess The Number",
)