nsi-rpg/src/engine/renderer.py

230 lines
12 KiB
Python

import math
from pygame import display, image, surface, transform, draw
from pygame.locals import RESIZABLE, SRCALPHA, FULLSCREEN
import src.engine.engine as engine
from src.engine.animation import Anim
from src.engine.enums import GameState
class Renderer:
"""Classe contenant le moteur de rendu. On utilise, pour cela la bibliothèque Pygame."""
def __init__(self, core: 'engine.Engine'):
self.engine = core
self.window_type = FULLSCREEN
self.window_size = (display.Info().current_w, display.Info().current_h) if self.window_type == FULLSCREEN else (600, 600)
self.window = display.set_mode(self.window_size, self.window_type)
self.tiles = []
self.tile_size = 0
self.animations: dict[str: Anim] = {}
# Variables utilisées pour les combats de boss
self.boss_fight_boss_animations: dict[str: Anim] = {}
self.boss_fight_player_animations: dict[str: Anim] = {}
self.boss_fight_GUI_container = None
# Variables utilisées par le menu principal
self.main_menu_assets: dict[str: Anim] = {}
def load_main_menu_assets(self, path: str):
"""Charge les assets du menu principal depuis le dossier donné."""
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."""
tile_set = image.load(file_path).convert_alpha()
self.tile_size = tile_size
# Scan tout le tile set et le découpe pour créer des tiles de {tile_size} px de hauteur et de largeur
for y in range(tile_set.get_height() // tile_size):
for x in range(tile_set.get_width() // tile_size):
tile = tile_set.subsurface((x * tile_size, y * tile_size, tile_size, tile_size))
self.tiles.append(tile)
def update(self, delta: float):
"""Fait le rendu du jeu."""
self.window.fill((255, 255, 255))
if self.engine.game_state == GameState.NORMAL:
# 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)
# On crée une surface qui sera ajoutée à la fenêtre apres rendered_surface pour pouvoir mettre des GUI
gui_surface = surface.Surface(display.get_window_size(), SRCALPHA)
gui_surface.fill((0, 0, 0, 0))
self.render_layer(0, rendered_surface)
self.render_layer(1, rendered_surface)
self.render_entities(rendered_surface, gui_surface, delta)
self.render_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))
self.window.blit(gui_surface, (0, 0))
elif self.engine.game_state == GameState.BOSS_FIGHT:
self.window.fill((255, 230, 230))
self.render_boss_fight_scene(delta)
self.render_boss_fight_gui()
# Apres avoir tout rendu, on met à jour l'écran
display.update()
def register_animation(self, animation: Anim, name: str):
"""Enregistre une animation."""
self.animations[name] = animation
def register_boss_fight_boss_animation(self, animation: Anim, name: str):
"""Ajoute une animation pour le boss lors d'un combat de boss."""
self.boss_fight_boss_animations[name] = animation
def register_boss_fight_player_animation(self, animation: Anim, name: str):
"""Ajoute une animation pour le joueur lors d'un combat de boss."""
self.boss_fight_player_animations[name] = animation
def render_boss_fight_scene(self, delta: float):
"""Rend les sprites du joueur et du boss lors d'un combat de boss."""
# On récupère l'image de l'animation du boss
boss_animation: Anim = self.boss_fight_boss_animations[self.engine.boss_fight_manager.current_boss_animation]
frame = boss_animation.get_frame(delta)
# On redimensionne l'image
frame = transform.scale(frame, (display.get_window_size()[0] / 5, display.get_window_size()[0] / 5))
# On colle le boss à droite de la fenêtre
self.window.blit(frame, (display.get_window_size()[0]-frame.get_width()-display.get_window_size()[0]/20,
display.get_window_size()[1]/4-frame.get_height()/2))
# On récupère l'image de l'animation du joueur
player_animation = self.boss_fight_player_animations[self.engine.boss_fight_manager.current_player_animation]
frame = player_animation.get_frame(delta)
# On redimensionne l'image
frame = transform.scale(frame, (display.get_window_size()[0] / 5, display.get_window_size()[0] / 5))
# On colle le joueur à gauche de la fenêtre
self.window.blit(frame, (display.get_window_size()[0]/20, display.get_window_size()[1]/4-frame.get_height()/2))
def render_boss_fight_gui(self):
"""Rend la barre d'action en bas de l'écran pendant le combat de boss."""
resized_container = transform.scale(self.boss_fight_GUI_container, (display.get_window_size()[0], self.boss_fight_GUI_container.get_height()/self.boss_fight_GUI_container.get_width()*display.get_window_size()[0]))
self.window.blit(resized_container, (0, display.get_window_size()[1]-resized_container.get_height()))
def render_entities(self, rendered_surface: surface.Surface, gui_surface: surface.Surface, delta: float):
"""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():
# On récupère la frame courante de l'animation
anim: Anim = self.animations[entity.animation_name]
frame = anim.get_frame(delta)
# On flip l'image horizontalement si l'entité est retournée
if entity.direction == 1:
frame = transform.flip(frame, True, False)
# Si l'entité n'apparait pas à l'écran, on passe son rendu
if (entity.x - self.engine.camera.x + x_middle_offset + frame.get_width() < 0 or
entity.x - self.engine.camera.x - x_middle_offset - frame.get_width() > 0 or
entity.y - self.engine.camera.y + y_middle_offset + frame.get_height() < 0 or
entity.y - self.engine.camera.y - y_middle_offset - frame.get_height() > 0):
continue
# On calcule les coordonnées de rendu de l'entité
entity_dest = (math.floor(entity.x - self.engine.camera.x + x_middle_offset - frame.get_width() / 2),
math.floor(entity.y - self.engine.camera.y + y_middle_offset - frame.get_height() / 2))
# On affiche l'image
rendered_surface.blit(frame, entity_dest)
if entity.max_life_points != -1:
# Rendu de la barre de vie des entités
life_bar_width = 50
life_bar_height = 8
life_bar_y_offset = 5
life_bar_border = 2
life_bar_value = entity.life_points / entity.max_life_points
cooldown_value = entity.damage_cooldown / entity.default_damage_cooldown
# On calcule où placer la barre de vei sur la surface des GUI
life_bar_dest = (math.floor((entity.x - self.engine.camera.x + x_middle_offset) * self.engine.camera.zoom -
life_bar_width / 2),
math.floor((entity.y - self.engine.camera.y + y_middle_offset - frame.get_height() / 2) *
self.engine.camera.zoom - life_bar_height - life_bar_y_offset))
# Contour de la barre de vie
draw.rect(gui_surface, (20, 0, 0), (life_bar_dest[0] - life_bar_border,
life_bar_dest[1] - life_bar_border,
life_bar_width + life_bar_border * 2,
life_bar_height + life_bar_border * 2))
# Barre de vie
draw.rect(gui_surface, (255 - 255 * life_bar_value, 255 * life_bar_value, 0),
life_bar_dest + (life_bar_width * life_bar_value, life_bar_height))
draw.rect(gui_surface, (200, 200, 200),
life_bar_dest + (life_bar_width * life_bar_value * cooldown_value, life_bar_height))
if self.engine.DEBUG_MODE:
top_let_corner_x = entity.x - self.engine.camera.x + x_middle_offset
top_let_corner_y = entity.y - self.engine.camera.y + y_middle_offset
draw.rect(rendered_surface, (255, 0, 0),
(top_let_corner_x + entity.collision_rect[0],
top_let_corner_y + entity.collision_rect[1],
entity.collision_rect[2] - entity.collision_rect[0],
entity.collision_rect[3] - entity.collision_rect[1]),
width=1)
def render_main_menu(self):
"""Rend le menu principal du jeu."""
def render_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] / self.tile_size / self.engine.camera.zoom) + 2
y_map_range = int(display.get_window_size()[1] / self.tile_size / self.engine.camera.zoom) + 2
# 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
# On calcule le décalage du début de rendu des tiles
x_map_offset = math.floor((self.engine.camera.x - x_middle_offset) / self.tile_size)
y_map_offset = math.floor((self.engine.camera.y - y_middle_offset) / self.tile_size)
# On itère pour chaque couche, toutes les tiles visibles par la caméra
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, layer_id)
# 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],
(math.floor(x * self.tile_size - self.engine.camera.x + x_middle_offset),
math.floor(y * self.tile_size - self.engine.camera.y + y_middle_offset)))
if self.engine.DEBUG_MODE and layer_id == 1:
draw.rect(rendered_surface, (100, 100, 255),
(math.floor(x * self.tile_size - self.engine.camera.x + x_middle_offset),
math.floor(y * self.tile_size - self.engine.camera.y + y_middle_offset),
self.tile_size, self.tile_size), width=1)