diff --git a/assets/entities/test/none/0.png b/assets/entities/test/none/0.png new file mode 100644 index 0000000..b7720ab Binary files /dev/null and b/assets/entities/test/none/0.png differ diff --git a/assets/entities/test/none/1.png b/assets/entities/test/none/1.png new file mode 100644 index 0000000..38bf925 Binary files /dev/null and b/assets/entities/test/none/1.png differ diff --git a/assets/entities/test/none/2.png b/assets/entities/test/none/2.png new file mode 100644 index 0000000..0979bff Binary files /dev/null and b/assets/entities/test/none/2.png differ diff --git a/assets/tiles.png b/assets/tiles.png index c148732..e1092fb 100644 Binary files a/assets/tiles.png and b/assets/tiles.png differ diff --git a/src/animation.py b/src/animation.py new file mode 100644 index 0000000..cad5c4b --- /dev/null +++ b/src/animation.py @@ -0,0 +1,66 @@ +# Classe animation adaptée depuis ESG Engine : +# https://github.com/yannis300307/ESG_Engine/blob/main/ESG_Engine/client/animation.py +import os + +import pygame + + +class Anim: + """Une animation contenant les images pygame et le temps entre chaque frames.""" + def __init__(self, change_frame_time: float): + self.frames = [] + self.change_frame_time = change_frame_time # Delay entre chaque frames + self.current_frame = 0 + self.time = 0 + + def add_frame(self, frame: pygame.Surface): + """Enregistre une nouvelle image dans la liste des frames.""" + self.frames.append(frame.convert_alpha()) + + def get_frame_nbr(self): + """Renvoie le nombre d'images enregistrées.""" + return len(self.frames) + + def get_frame(self, delta: float): + """Donne l'image courante de l'animation.""" + # Avant de retourner l'image, on met à jour le delay + self.update_current_frame(delta) + + # Si le delay entre deux images est écoulé, on incrémente le numéro de l'image et on remet le temps à 0 + if self.time >= self.change_frame_time: + self.current_frame += 1 + self.time = 0 + + # Si on sort de la liste d'images, on revient au début + if self.current_frame >= len(self.frames): + self.current_frame = 0 + + return self.frames[self.current_frame] + + def update_current_frame(self, delta: float): + """Met à jour le delay de l'image courante avec le delta time.""" + self.time += delta + + def get_specific_frame(self, base: int): + """Donne la {base} ème image apres l'image courante.""" + # Si le delay entre deux images est écoulé, on incrémente le numéro de l'image et on remet le temps à 0 + if self.time >= self.change_frame_time: + self.current_frame += 1 + self.time = 0 + + # Si on sort de la liste d'images, on revient au début + if self.current_frame >= len(self.frames): + self.current_frame = 0 + + return self.frames[(self.current_frame + base) % len(self.frames)] + + def load_animation_from_directory(self, path: str): + """Récupère toutes les images au format png dans un dossier et les enregistre comme frames de l'animation.""" + + # On récupère tous les fichiers dans le dossier donné + files = os.listdir(path) + + # Si ils sont bien en PNG, on les enregistre + for file in files: + if file.endswith(".png"): + self.add_frame(pygame.image.load(path + os.sep + file)) diff --git a/src/camera.py b/src/camera.py index c711aed..26f2c5c 100644 --- a/src/camera.py +++ b/src/camera.py @@ -1,3 +1,6 @@ +from src.entity import Entity + + class Camera: def __init__(self): self.x = 0 @@ -11,8 +14,19 @@ class Camera: self.smoothness = 20. + self.followed_entity: Entity | None = None + def update(self): """Met à jour la caméra. Permet, par exemple, de faire le scrolling.""" + + # Si on suit une entité, on met à jour les coordonnées de suivi + if self.followed_entity is not None: + self.target_x = self.followed_entity.x + self.target_y = self.followed_entity.y + self.x += (self.target_x - self.x) / self.smoothness self.y += (self.target_y - self.y) / self.smoothness self.zoom += (self.target_zoom - self.zoom) / self.smoothness + + def follow_entity(self, entity: Entity): + self.followed_entity = entity diff --git a/src/engine.py b/src/engine.py index 5e6008c..dadd01b 100644 --- a/src/engine.py +++ b/src/engine.py @@ -1,4 +1,6 @@ +from src.animation import Anim from src.camera import Camera +from src.entity_manager import EntityManager from src.event_handler import EventHandler from src.map_manager import MapManager from src.renderer import Renderer @@ -18,11 +20,19 @@ class Engine: self.event_handler = EventHandler(self) self.map_manager = MapManager() self.camera = Camera() + self.entity_manager = EntityManager() - self.map_manager.load_new("maps/map1.tmj") + self.map_manager.load_new("maps/map2.tmj") self.renderer.load_tile_set("assets/tiles.png", 16) + anim = Anim(0.5) # TODO : REMOVE (ONLY USED FOR TESTING) + anim.load_animation_from_directory("assets/entities/test/none") + self.renderer.register_animation(anim, "test_none") + + test_entity = self.entity_manager.register_entity("test") + test_entity.link_animation("test_none") + def loop(self): """Fonction à lancer au début du programme et qui va lancer les updates dans une boucle. Attend jusqu'à la fin du jeu.""" @@ -34,9 +44,10 @@ class Engine: def update(self): """Fonction qui regroupe toutes les updates des composants. Elle permet de mettre à jour le jeu quand on l'appelle.""" + self.camera.update() + self.entity_manager.update(0.016666666) self.renderer.update() self.event_handler.update() - self.camera.update() def stop(self): """Arrête le programme.""" diff --git a/src/entity.py b/src/entity.py index b359e2c..b52a3b3 100644 --- a/src/entity.py +++ b/src/entity.py @@ -1,4 +1,21 @@ class Entity: - def __init__(self): + """Classe permettant de gérer les entités. Créée automatiquement par `EntityManager.register_entity()`""" + def __init__(self, name: str): self.x = 2 self.y = 2 + + # Time utilisé pour les IA + self.time = 0 + + self.name = name + + self.animation_name = None + + def update(self, delta: float): + """Met à jour l'entité.""" + self.x += 1 + + self.time += delta + + def link_animation(self, name: str): + self.animation_name = name diff --git a/src/entity_manager.py b/src/entity_manager.py new file mode 100644 index 0000000..0bdb1b5 --- /dev/null +++ b/src/entity_manager.py @@ -0,0 +1,22 @@ +from src.entity import Entity + + +class EntityManager: + """Classe chargée de gérer les entités.""" + def __init__(self): + self.entities: dict[str:Entity] = {} + + def register_entity(self, name: str): + """Crée une entité et l'enregistre dans un dictionnaire.""" + entity = Entity(name) + self.entities[name] = entity + return entity + + def update(self, delta: float): + """Met à jour toutes les entités enregistrées.""" + for entity in self.entities.values(): + entity.update(delta) + + def get_all_entities(self): + """Donne la liste de toutes les entités enregistrées.""" + return list(self.entities.values()) diff --git a/src/renderer.py b/src/renderer.py index f8c42b0..e9c98aa 100644 --- a/src/renderer.py +++ b/src/renderer.py @@ -1,7 +1,10 @@ +import math + from pygame import display, image, surface, transform from pygame.locals import RESIZABLE import src.engine as engine +from src.animation import Anim class Renderer: @@ -11,6 +14,7 @@ class Renderer: self.window = display.set_mode((600, 600), RESIZABLE) self.tiles = [] self.tile_size = 0 + self.animations: dict[str: Anim] = {} def load_tile_set(self, file_path: str, tile_size: int): """Charge le jeu de tuiles en utilisant le fichier donné et la taille donnée.""" @@ -28,12 +32,40 @@ class Renderer: """Fait le rendu du jeu.""" self.window.fill((255, 255, 255)) - self.render_map() + # On crée une surface temporaire qui nous permettra de faire le rendu à l'échelle 1:1 + rendered_surface_size = (display.get_window_size()[0] / self.engine.camera.zoom, display.get_window_size()[1] / self.engine.camera.zoom) + rendered_surface = surface.Surface(rendered_surface_size) + + self.renderer_layer(0, rendered_surface) + self.render_entities(rendered_surface) + self.renderer_layer(1, rendered_surface) + self.renderer_layer(2, rendered_surface) + + # Enfin, on redimensionne notre surface et on la colle sur la fenêtre principale + self.window.blit(transform.scale(rendered_surface, (math.ceil(rendered_surface_size[0] * self.engine.camera.zoom), + math.ceil(rendered_surface_size[1] * self.engine.camera.zoom))), + (0, 0)) # Apres avoir tout rendu, on met à jour l'écran display.update() - def render_map(self): + def register_animation(self, animation: Anim, name: str): + """Enregistre une animation.""" + self.animations[name] = animation + + def render_entities(self, rendered_surface: surface.Surface): + """Rend toutes les entités.""" + # On calcule le décalage pour centrer la caméra + x_middle_offset = display.get_window_size()[0] / 2 / self.engine.camera.zoom + y_middle_offset = display.get_window_size()[1] / 2 / self.engine.camera.zoom + + for entity in self.engine.entity_manager.get_all_entities(): + anim: Anim = self.animations[entity.animation_name] + frame = anim.get_frame(0.01666667) + rendered_surface.blit(frame, (entity.x-self.engine.camera.x+x_middle_offset, entity.y-self.engine.camera.y+y_middle_offset)) + + def renderer_layer(self, layer_id: int, rendered_surface: surface.Surface): + """Rend la map.""" # On calcule le nombre de tiles à mettre sur notre écran en prenant en compte le zoom x_map_range = int(display.get_window_size()[0] / 16 / self.engine.camera.zoom) + 2 y_map_range = int(display.get_window_size()[1] / 16 / self.engine.camera.zoom) + 2 @@ -46,27 +78,19 @@ class Renderer: x_map_offset = int((self.engine.camera.x - x_middle_offset) / self.tile_size) y_map_offset = int((self.engine.camera.y - y_middle_offset) / self.tile_size) - # On crée une surface temporaire qui nous permettra de la redimensionner - rendered_surface_size = (x_map_range * self.tile_size, y_map_range * self.tile_size) - rendered_surface = surface.Surface(rendered_surface_size) - # On itère pour chaque couche, toutes les tiles visibles par la caméra - for i in range(len(self.engine.map_manager.map_layers)): - for x in range(x_map_offset, x_map_offset + x_map_range): - for y in range(y_map_offset, y_map_offset + y_map_range): + for x in range(x_map_offset, x_map_offset + x_map_range): + for y in range(y_map_offset, y_map_offset + y_map_range): - # On récupère l'id de la tile à la position donnée - tile_id = self.engine.map_manager.get_tile_at(x, y, i) + # On récupère l'id de la tile à la position donnée + tile_id = self.engine.map_manager.get_tile_at(x, y, layer_id) - # Si l'id est 0, il s'agit de vide donc on saute le rendu - if tile_id == 0: - continue + # Si l'id est 0, il s'agit de vide donc on saute le rendu + if tile_id == 0: + continue - # Puis, on cherche à quelle image elle correspond et on la colle sur notre surface - rendered_surface.blit(self.tiles[tile_id-1], - ((x*self.tile_size-self.engine.camera.x+x_middle_offset), - (y*self.tile_size-self.engine.camera.y+y_middle_offset))) + # Puis, on cherche à quelle image elle correspond et on la colle sur notre surface + rendered_surface.blit(self.tiles[tile_id-1], + ((x*self.tile_size-self.engine.camera.x+x_middle_offset), + (y*self.tile_size-self.engine.camera.y+y_middle_offset))) - # Enfin, on redimensionne notre surface et on la colle sur la fenêtre principale - self.window.blit(transform.scale(rendered_surface, (rendered_surface_size[0]*self.engine.camera.zoom, - rendered_surface_size[1]*self.engine.camera.zoom)), (0, 0))