Skip to content

Latest commit

 

History

History
585 lines (470 loc) · 21.5 KB

File metadata and controls

585 lines (470 loc) · 21.5 KB
title date draft
Partie 2 - Une entité générique, les fonctions de rendu et la carte]
2019-03-30 08:39:20 -0700
false

Maintenant que l'on peut déplacer notre petit symbole "@", on doit lui donner un cadre. Mais avant ça pensons un instant à l'objet joueur lui même.

Pour l'instant, nous représentons simplement le joueur avec un '@' et ses coordonnées x et y. On devrait regrouper ces éléments dans un objet avec d'autres données et des fonctions qui lui correspondent.

Créons une classe générique qui représente non seulement le joueur mais aussi tous les éléments de notre monde de jeu. Ennemis, items et tout autre entité à laquelle on pourrait penser feront parties de cette classe qui s'appellera Entity.

Créez un nouveau fichier et nommez le entity.py. Dans ce fichier, ajouter la classe suivante :

{{< highlight py3 >}} class Entity: """ A generic object to represent players, enemies, items, etc. """ def init(self, x, y, char, color): self.x = x self.y = y self.char = char self.color = color

def move(self, dx, dy):
    # Move the entity by a given amount
    self.x += dx
    self.y += dy

{{</ highlight >}}

C'est plutôt explicite. La classe Entity qui contient les coordonnées x et y ainsi que le caractère (le symbole @ en ce qui concerne le joueur) et la couleur (blanche par défaut pour le joueur). Nous avons aussi une méthode appelée move qui permettra à l'entité d'être déplacée selon des coordonnées x et y données.

Mettons cette nouvelle classe en action ! Modifiez la première partie de engine.py pour qu'elle ressemble à ceci :

{{< codetab >}} {{< diff-tab >}} {{< highlight diff >}} import tcod as libtcod

+from entity import Entity from input_handlers import handle_keys

def main(): screen_width = 80 screen_height = 50

  • player_x = int(screen_width / 2)
  • player_y = int(screen_height / 2)
  • player = Entity(int(screen_width / 2), int(screen_height / 2), '@', libtcod.white)
  • npc = Entity(int(screen_width / 2 - 5), int(screen_height / 2), '@', libtcod.yellow)
  • entities = [npc, player] ... {{</ highlight >}} {{</ diff-tab >}} {{< original-tab >}}
import tcod as libtcod

from entity import Entity
from input_handlers import handle_keys


def main():
    screen_width = 80
    screen_height = 50

    player_x = int(screen_width / 2)
    player_y = int(screen_height / 2)

    player = Entity(int(screen_width / 2), int(screen_height / 2), '@', libtcod.white)
    npc = Entity(int(screen_width / 2 - 5), int(screen_height / 2), '@', libtcod.yellow)
    entities = [npc, player]
    ...

{{</ original-tab >}} {{</ codetab >}}

On importe la classe Entity dans engine.py et on l'emploie pour initialiser le joueur et un nouveau NPC. On range ces éléments dans une liste qui contiendra toutes les entités sur la carte.

Modifiez aussi la partie qui gère le mouvement de façon à ce que ce soit la classe Entity qui s'en occupe.

{{< codetab >}} {{< diff-tab >}} {{< highlight diff >}} if move: dx, dy = move

  •       player_x += dx
    
  •       player_x += dy
    
  •       player.move(dx, dy)
    

{{</ highlight >}} {{</ diff-tab >}} {{< original-tab >}}

        if move:
            dx, dy = move
            player_x += dx
            player_x += dy
            player.move(dx, dy)

{{</ original-tab >}} {{</ codetab >}}

Enfin, mettez à jour les fonctions de dessin pour utiliser le nouvel objet joueur.

{{< codetab >}} {{< diff-tab >}} {{< highlight diff >}} while not libtcod.console_is_window_closed(): libtcod.sys_check_for_event(libtcod.EVENT_KEY_PRESS, key, mouse)

    libtcod.console_set_default_foreground(con, libtcod.white)
  •   libtcod.console_put_char(con, player_x, player_y, '@', libtcod.BKGND_NONE)
    
  •   libtcod.console_put_char(con, player.x, player.y, '@', libtcod.BKGND_NONE)
      libtcod.console_blit(con, 0, 0, screen_width, screen_height, 0, 0, 0)
      libtcod.console_set_default_foreground(0, libtcod.white)
    
  •   libtcod.console_put_char(0, player_x, player_y, '@', libtcod.BKGND_NONE)
    
  •   libtcod.console_put_char(0, player.x, player.y, '@', libtcod.BKGND_NONE)
      libtcod.console_flush()
    
  •   libtcod.console_put_char(con, player_x, player_y, ' ', libtcod.BKGND_NONE)
    
  •   libtcod.console_put_char(0, player_x, player_y, ' ', libtcod.BKGND_NONE)
    
  •   libtcod.console_put_char(con, player.x, player.y, ' ', libtcod.BKGND_NONE)
    
  •   libtcod.console_put_char(0, player.x, player.y, ' ', libtcod.BKGND_NONE)
    
      action = handle_keys(key)
    

{{</ highlight >}} {{</ diff-tab >}} {{< original-tab >}}

    while not libtcod.console_is_window_closed():
        libtcod.sys_check_for_event(libtcod.EVENT_KEY_PRESS, key, mouse)

        libtcod.console_set_default_foreground(con, libtcod.white)
        libtcod.console_put_char(con, player_x, player_y, '@', libtcod.BKGND_NONE)
        libtcod.console_put_char(con, player.x, player.y, '@', libtcod.BKGND_NONE)
        libtcod.console_blit(con, 0, 0, screen_width, screen_height, 0, 0, 0)
        libtcod.console_set_default_foreground(0, libtcod.white)
        libtcod.console_put_char(0, player_x, player_y, '@', libtcod.BKGND_NONE)
        libtcod.console_put_char(0, player.x, player.y, '@', libtcod.BKGND_NONE)
        libtcod.console_flush()

        libtcod.console_put_char(con, player_x, player_y, ' ', libtcod.BKGND_NONE)
        libtcod.console_put_char(0, player_x, player_y, ' ', libtcod.BKGND_NONE)
        libtcod.console_put_char(con, player.x, player.y, ' ', libtcod.BKGND_NONE)
        libtcod.console_put_char(0, player.x, player.y, ' ', libtcod.BKGND_NONE)

        action = handle_keys(key)
    

{{</ original-tab >}} {{</ codetab >}}

On doit modifier la manière dont l'entité est dessinée à l'écran. Si vous exécutez le code maintenant, seul le joueur est dessiné. Ecrivons quelques fonctions qui dessinent à la fois le joueur mais aussi toute entité de la liste des entités.

Créez un nouveau fichier appelé render_functions.py. Il contiendra nos fonctions de dessin et une fonction nettoyant l'écran. Ajoutez le code suivant dans ce fichier.

{{< highlight py3 >}} import tcod as libtcod

def render_all(con, entities, screen_width, screen_height): # Draw all entities in the list for entity in entities: draw_entity(con, entity)

libtcod.console_blit(con, 0, 0, screen_width, screen_height, 0, 0, 0)

def clear_all(con, entities): for entity in entities: clear_entity(con, entity)

def draw_entity(con, entity): libtcod.console_set_default_foreground(con, entity.color) libtcod.console_put_char(con, entity.x, entity.y, entity.char, libtcod.BKGND_NONE)

def clear_entity(con, entity): # erase the character that represents this object libtcod.console_put_char(con, entity.x, entity.y, ' ', libtcod.BKGND_NONE) {{</ highlight >}}

Voici un découpage rapide de ce que font ces fonctions :

  • La fonction render_all est celle qui sera appelée de notre boucle de jeu pour dessiner les entités et, dans un instant, la carte. Pour l'instant elle prend la console (con), une liste d'entité et les dimensions (hauteur/largeur) de l'écran en paramètres et elle appelle la fonction draw_entity sur chaque élément. Ensuite elle colle (blit) les changements à l'écran.
  • draw_entity est ce qui réalise vraiment le dessin. Le code devrait être très proche de ce qui est dans notre boucle de jeu actuellement à ceci près qu'elle utilise les variables de l'entité (x, y, char et color) pour faire le dessin. Cela est assez flexible en théorie pour dessiner n'importe quelle entité qu'on lui donne.
  • clear_all et ce qu'on utilisera pour nettoyer les entités après les avoir dessinées à l'écran. C'est simplement une boucle qui appelle une autre fonction.
  • clear_entity est justement cette fonction. Elle nettoie les entités de l'écran (de façon à ce qu'elle bouge sans laisser une trace derrière elle).

Maitenant qu'on a quelques fonctions pour nous aider à dessiner les entités mettons les en action. Réalisez les modifications dans la partie où le joueur est dessiné dans engine.py

{{< codetab >}} {{< diff-tab >}} {{< highlight diff >}} ... libtcod.sys_check_for_event(libtcod.EVENT_KEY_PRESS, key, mouse)

  • libtcod.console_set_default_foreground(con, libtcod.white)
  • libtcod.console_put_char(con, player.x, player.y, '@', libtcod.BKGND_NONE)
  • libtcod.console_blit(con, 0, 0, screen_width, screen_height, 0, 0, 0)
  • render_all(con, entities, screen_width, screen_height)

    libtcod.console_flush()

  • libtcod.console_put_char(con, player.x, player.y, ' ', libtcod.BKGND_NONE)
  • clear_all(con, entities)

    action = handle_keys(key) ... {{</ highlight >}} {{</ diff-tab >}} {{< original-tab >}}

        ...
    libtcod.sys_check_for_event(libtcod.EVENT_KEY_PRESS, key, mouse)

    libtcod.console_set_default_foreground(con, libtcod.white)
    libtcod.console_put_char(con, player.x, player.y, '@', libtcod.BKGND_NONE)
    libtcod.console_blit(con, 0, 0, screen_width, screen_height, 0, 0, 0)
    render_all(con, entities, screen_width, screen_height)

    libtcod.console_flush()

    libtcod.console_put_char(con, player.x, player.y, ' ', libtcod.BKGND_NONE)
    clear_all(con, entities)

    action = handle_keys(key)
    ...

{{</ original-tab >}} {{</ codetab >}}

N'oubliez pas d'importez render_all et clear_all en haut de votre fichier. Votre partie d'imports devrait ressemble à quelque chose comme cela :

{{< codetab >}} {{< diff-tab >}} {{< highlight diff >}} import tcod as libtcod

from entity import Entity from input_handlers import handle_keys +from render_functions import clear_all, render_all {{</ highlight >}} {{</ diff-tab >}} {{< original-tab >}}

import tcod as libtcod

from entity import Entity
from input_handlers import handle_keys
from render_functions import clear_all, render_all

{{</ original-tab >}} {{</ codetab >}}

Si vous exécutez le projet maintenant, vous devriez voir votre symbole '@' accompagné d'un autre symbole jaune représentant notre NPC. Il ne fait rien pour l'instant mais nous avons une méthode permettant dessiner plus d'un personnage à l'écran.

Il est temps de changer de vitesse et de mettre la carte en place. La carte est un tableau 2d d'objets Tile (tuile). Les tuiles aurons quelques propriétés qui définissent si on peut voir à travers ou voir à travers.

On devrait commencer en définissant la taille de notre carte. Ajoutez ces variables juste après avoir défini la hauteur et la largeur de l'écran.

{{< codetab >}} {{< diff-tab >}} {{< highlight diff >}} ... screen_height = 50

  • map_width = 80
  • map_height = 45 {{</ highlight >}} {{</ diff-tab >}} {{< original-tab >}}
    ...
    screen_height = 50
    map_width = 80
    map_height = 45

{{</ original-tab >}} {{</ codetab >}}

Assez simple. On doit maintenant trouver une place pour notre classe Tile et d'autres classes. Je préfère ranger les classes similaires dans le même dossier donc créez un nouveau package Python (c'est un dossier avec un fichier appelé __init__.py, ce fichier étant vide dans notre cas) appelé map_objects. Dans ce dossier, crées un fichier tile.py et ajoutez-y le code suivant.

{{< highlight py3 >}} class Tile: """ A tile on a map. It may or may not be blocked, and may or may not block sight. """ def init(self, blocked, block_sight=None): self.blocked = blocked

    # By default, if a tile is blocked, it also blocks sight
    if block_sight is None:
        block_sight = blocked

    self.block_sight = block_sight

{{</ highlight >}}

Rien de très compliqué ici. La classe Tile contient l'information selon laquelle elle est bloquante (blocked, si elle l'est, vous ne pouvez vous déplacer dedans et l'information nous permettant de savoir si on peut voir à travers (block_sight pour notre algorithme de champ de vision FOV). Remarquez qu'il n'est pas nécessaire de passer block_sight à chaque fois ; ce paramètre supposé être le même que blocked. En séparant les deux, une tuile peut être transparente sans pouvoir être traversée (un puis de lave, peut-être ?) ou inversement (une pièce sombre, par exemple).

Maintenant qu'on a une classe tuile, il nous faut un conteneur pour garder nos tuiles. Créeons une classe GameMap qui contiendra notre tableau 2d de tuiles ainsi que quelques méthodes pour régler et intéragir avec elles. Créez un fichier dans le dossier map_objects et appelez le game_map.py. Ajoutez-y le code suivant :

{{< highlight py3 >}} from map_objects.tile import Tile

class GameMap: def init(self, width, height): self.width = width self.height = height self.tiles = self.initialize_tiles()

def initialize_tiles(self):
    tiles = [[Tile(False) for y in range(self.height)] for x in range(self.width)]

    tiles[30][22].blocked = True
    tiles[30][22].block_sight = True
    tiles[31][22].blocked = True
    tiles[31][22].block_sight = True
    tiles[32][22].blocked = True
    tiles[32][22].block_sight = True

    return tiles

{{</ highlight >}}

On lui passe la largeur et la hauteur de la carte (définies dans notre moteur) et on initialise un tableau 2d de tuiles, réglées sur non bloquantes par défaut. On règle quelques tuiles comme étant bloquantes, pour démontrer le principe. J'ai conservé les réglages des tuiles hors de la fonction __init__ pour deux raisons. D'une part parce qu'on peut l'appeler en dehors de l'initialisation, d'autre part parce que je prefère avoir des fonctions __init__ aussi simples que possible.

Revenez sur engine.py où nous allons faire quelques changement pour initialiser la carte et l'afficher à l'écran.

D'abord, nous définissons quelle couleur employer pour les tuiles bloquantes et non bloquantes. Définissons un dictionnaire qui contient les couleurs qu'on utilisera pour l'instant (il s'agrandira avec l'avancée du tutoriel).

{{< codetab >}} {{< diff-tab >}} {{< highlight diff >}} ... map_height = 45

  • colors = {

  •   'dark_wall': libtcod.Color(0, 0, 100),
    
  •   'dark_ground': libtcod.Color(50, 50, 150)
    
  • }

    player = Entity(int(screen_width / 2), int(screen_height / 2), '@', libtcod.white) {{</ highlight >}} {{</ diff-tab >}} {{< original-tab >}}

    ...
    map_height = 45

    colors = {
        'dark_wall': libtcod.Color(0, 0, 100),
        'dark_ground': libtcod.Color(50, 50, 150)
    }

    player = Entity(int(screen_width / 2), int(screen_height / 2), '@', libtcod.white)

{{</ original-tab >}} {{</ codetab >}}

Ces couleurs nous serviront pour les murs et le sol en dehors du champ de vision quand on y sera (d'où le 'dark' de leurs noms).

Maintenant nous allons initialiser la carte de jeu elle même. Cela peut-être placé n'importe où avant la boucle principale. J'ajoute la mienne juste en dessous de l'initialisation de la console.

{{< codetab >}} {{< diff-tab >}} {{< highlight diff >}} con = libtcod.console_new(screen_width, screen_height)

  • game_map = GameMap(map_width, map_height)

    key = libtcod.Key() {{</ highlight >}} {{</ diff-tab >}} {{< original-tab >}}

    con = libtcod.console_new(screen_width, screen_height)

    game_map = GameMap(map_width, map_height)

    key = libtcod.Key()

{{</ original-tab >}} {{</ codetab >}}

N'oublions pas d'importer l'objet GameMap de façon à l'utiliser dans le moteur.

{{< codetab >}} {{< diff-tab >}} {{< highlight diff >}} from entity import Entity from input_handlers import handle_keys +from map_objects.game_map import GameMap from render_functions import clear_all, render_all {{</ highlight >}} {{</ diff-tab >}} {{< original-tab >}}

from entity import Entity
from input_handlers import handle_keys
from map_objects.game_map import GameMap
from render_functions import clear_all, render_all

{{</ original-tab >}} {{</ codetab >}}

Maintenant que notre objet carte est prêt passons le à render_all de façon à le dessiner. Nous passerons aussi le dictionnaire colors parce que render_all aura besoin de connaître les couleurs des éléments de la carte. Remarquez que l'ordre dans lequel vous passez ces arguments n'a pas d'importance cela doit simplement être cohérent avec la définition de la fonction quand vous l'appelez.

{{< codetab >}} {{< diff-tab >}} {{< highlight diff >}}

  •   render_all(con, entities, screen_width, screen_height)
    
  •   render_all(con, entities, game_map, screen_width, screen_height, colors)
    

{{</ highlight >}} {{</ diff-tab >}} {{< original-tab >}}

        render_all(con, entities, screen_width, screen_height)
        render_all(con, entities, game_map, screen_width, screen_height, colors)

{{</ original-tab >}} {{</ codetab >}}

Ouvrez render_functions.py et modifiez render_all ainsi :

{{< codetab >}} {{< diff-tab >}} {{< highlight diff >}} -def render_all(con, entities, screen_width, screen_height): +def render_all(con, entities, game_map, screen_width, screen_height, colors):

  • Draw all the tiles in the game map

  • for y in range(game_map.height):

  •   for x in range(game_map.width):
    
  •       wall = game_map.tiles[x][y].block_sight
    
  •       if wall:
    
  •           libtcod.console_set_char_background(con, x, y, colors.get('dark_wall'), libtcod.BKGND_SET)
    
  •       else:
    
  •           libtcod.console_set_char_background(con, x, y, colors.get('dark_ground'), libtcod.BKGND_SET)
    
  • Draw all entities in the list

    for entity in entities: draw_entity(con, entity)

    libtcod.console_blit(con, 0, 0, screen_width, screen_height, 0, 0, 0) {{</ highlight >}} {{</ diff-tab >}} {{< original-tab >}}

def render_all(con, entities, screen_width, screen_height):
def render_all(con, entities, game_map, screen_width, screen_height, colors):
    # Draw all the tiles in the game map
    for y in range(game_map.height):
        for x in range(game_map.width):
            wall = game_map.tiles[x][y].block_sight

            if wall:
                libtcod.console_set_char_background(con, x, y, colors.get('dark_wall'), libtcod.BKGND_SET)
            else:
                libtcod.console_set_char_background(con, x, y, colors.get('dark_ground'), libtcod.BKGND_SET)
    
    # Draw all entities in the list
    for entity in entities:
        draw_entity(con, entity)

    libtcod.console_blit(con, 0, 0, screen_width, screen_height, 0, 0, 0)

{{</ original-tab >}} {{</ codetab >}}

render_all boucle maintenant sur les tuiles de la carte de jeu et vérifie si elles bloquent la vue ou non. Si c'est le cas, il dessine la tuile comme un mur et sinon comme le sol.

Lancez le projet maintenant et vous devriez voir la carte dessinée avec des couleurs. Vous verrez nos trois blocs de mur mais il y a un problème : vous pouvez bouger à travers le mur !

Nous devons ajouter deux choses avant de conclure cette étape. Modifiez la partie où la fonction de déplacement du joueur est appelée pour qu'elle ressemble à ceci :

{{< codetab >}} {{< diff-tab >}} {{< highlight diff >}} if not game_map.is_blocked(player.x + dx, player.y + dy): player.move(dx, dy)

  •       player.move(dx, dy)
    

{{</ highlight >}} {{</ diff-tab >}} {{< original-tab >}}

            if not game_map.is_blocked(player.x + dx, player.y + dy):
                player.move(dx, dy)
            player.move(dx, dy)

{{</ original-tab >}} {{</ codetab >}}

* Remarquez le changement d'indentation pour plaer.move(dx, dy). En Python l'indentation est importante !

Maintenant on doit simplement crée la méthode is_blocked dans la carte du jeu. Ouvrez le fichier game_map.py et ajouter cette méthode à la classe :

{{< highlight py3 >}} def is_blocked(self, x, y): if self.tiles[x][y].blocked: return True

    return False

{{</ highlight >}}

* Remarque : vous pouvez raccourcir la fonction is_blocked en écrivant simplement return self.tiles[x][y].blocked mais nous changerons cette fonction pour qu'elle vérifie plus de choses plus tard aussi nous prenons un chemin plus explicite.

Lancez le projet à nouveau et vous serez bloqués par les murs.

Cela fera l'affaire pour ce tutoriel. Il n'y parait peut-être pas mais nous avons fait ce qu'il faut pour créer un donjon réalise dans la prochaine partie.

Si vous voulez voir le code actuel entièrement, cliquez ici.

Cliquez ici pour vous rendre à la partie suivante de ce tutoriel.

<script src="/js/codetabs.js"></script>