1. Introduction

Le célèbre jeu, vendu à plus de 144 millions de copies, Minecraft fête le 17 mai ses 10 ans. Oui le même jour où ce projet est dû. Pour cette occasion, je propose de recrée une version Python du jeu! Pour rappelle Minecraft est un jeu dont le terrain se génère procéduralement et à l'infini. Ce même terrain peut être altéré par le joueur. La version recréer dans ce projet sera cependant en deux dimensions pour simplifier le travail. En plus de cela, chaque élément(blocs, interface graphique, ...) qui composera le jeu devra être stockés sous forme d’un fichier JSON. Cela permet de facilement modifier le jeu et de ne pas avoir à modifier le code pour ajouter certains éléments.

1.1 Définition

Biome:
Un biome défini l'apparence d'un bout de terrain. (ex: désert, plaine, ...)

Bloc:
Les blocs sont ce qui composent le terrain. Les blocs occupent une position bien définie et ne peuvent pas se chevaucher. Les blocs ne peuvent pas se déplacer. La plupart des blocs peuvent être cassés par le joueur.

Entité:
Les entités regroupent tous les éléments pouvant se déplacer. Les animaux et le joueur sont des entités.

Les trançons:
Un trançon (ou chunk en anglais) est un morceau de terrain ayant une taille de 16 par 256 blocs. (16x256x16 dans le jeu original.)

2. Matériel et méthodes

2.1 Matériel

Un ordinateur avec : -python 3 et la librairie pygame d'installé. -Un éditeur d'image

2.2 Méthode

Pour réaliser ce projet, nous allons avoir besoin d'utiliser des classes. Les classes nous permettent de découper notre programme en plusieurs petits parties. Ce qui simplifie largement la programmation.

2.2.1 Les blocs

Pour rappelle, les fichiers JSON nous servent à stocké des objets du jeu comme les biomes, les blocs et les entités. Pour les blocs, les fichiers JSON ressemble à ceci:

{
	"id" : "pycraft:stone",
	"sprites" : ["textures/block/stone.png"],
	"animation_speed" : "0",
	"destroy_speed" : "5.0",
	"destroy_tool" : "pickaxe",
	"creative_tab" : "0",
	"tnt_resistance" : "0",
	"has_gravity" : "0",
	"light_opacity" : "16",
	"luminosity" : "0",
	"sounds" : [],
	"drop_pool" : "",
	"block_type" : "solid"
}

Ici, la valeur "id" correspond à un identifiant unique et quant à la liste "sprites" et la valeur "animation_speed", ils sont utilisés pour l'affichage des blocs. Toutes les autres valeurs ne sont présentes que pour d'éventuelles ajouts futures.

Il nous faut maintenant pour ce fichier et comme pour tout les autres objets du jeu une classe pour les décrire. De cette manière, il sera facile d'accéder à certaines valeurs ou de pouvoir manipuler certaines objets. Voici la classe qui définis les blocs:

import os
import json
import game_class.texture as tex

BLOCK_SEIZE = 48

class block:
    def __init__(self):
        pass

    @staticmethod
    def find_with_id(block_id, block_list):
        for blo in block_list:
            if block_id == blo.item_id:
                return blo
            
    @staticmethod
    def LoadBlocks(path, TEXTURES):
        block_list = []
        for direc in os.listdir(path):
            block_data = json.load(open("./"+path+"/"+direc))

            tmp = block()
            
            tmp.item_id = block_data["id"]
            tmp.sprites =[]
        
            for sprite in block_data["sprites"]:
                tmp.sprites.append(tex.texture.find_with_id(sprite,TEXTURES))

            tmp.animation_speed = int(block_data["animation_speed"])
            tmp.dest_speed = float(block_data["destroy_speed"])
            tmp.dest_tool = block_data["destroy_tool"]
            tmp.creative_tab_index = int(block_data["creative_tab"])
            tmp.tnt_resi = float(block_data["tnt_resistance"])
            tmp.has_gravity = bool(block_data["has_gravity"])
            tmp.light_opacity = int(block_data["light_opacity"])
            tmp.luminosity = int(block_data["luminosity"])
            tmp.sounds = block_data["sounds"]
            tmp.drop = block_data["drop_pool"]
            tmp.block_type = block_data["block_type"]

            block_list.append(tmp)

        return block_list

Comme toutes les classes, de ce projet, associé à un fichier JSON, elle contient une fonction de listage qui va aller chercher dans les dossiers du jeu les fichiers JSON correspondant à un bloc pour en créer une instance. Ces dernières seront stockées dans une liste. La classe bloc contient aussi un fonction find_with_id qui permet de retrouver dans la liste des blocs un bloc en particulier.

2.2.2 Génération du terrain

Pour générer le terrain, j'ai décidé de m'inspirer du système de génération de Minecraft. Dans ce dernier, le terrain se divise en "chunk" ou tronçon. Ces morceaux de terrain permettent à Minecraft de ne pas avoir à générer l'entièreté du terrain (qui mesure 6'000'000x6'000'000 de blocs) et de pouvoir facilement déchargé la mémoire du jeu en "déchargeant" les parties trop élongées du joueur. Dans le jeu original, ces tronçons mesurent 16x256x16 blocs.
Nous avons donc une classe "chunk" qui contient un tableau de variable qui contient lui-même 16 autre tableau de 256 éléments. Ces tableaux sont remplis par des instances de la classe suivant:

class block_slot:
    def __init__(self, block_id, light_level):
        self.block_id = block_id
        self.light_level = light_level

Cette classe permet de stocker dans un seul tableau de variable bidimensionnelle plusieurs informations: quel bloc occupe cet emplacement et quel est la quantité de lumière présente à cet endroit.
Au fur et à mesure que le joueur se déplace, le jeu va créer de nouvelle instance de la classe "chunk". Ceci permet d'avoir un terrain qui est en théorie infini. Pour remplir ces tronçons, j'ai choisi de diviser le problème en deux: la gauche et la droite. Pour chacun des côtés, le principe est le même: Une variable dans un array désigne la distance en blocs générée depuis le centre du terrain, tandis qu'une autre désigne la hauteur jusqu'à là quelle va se générer la prochaine rangée de bloc. Cette variable peut change aléatoirement après chaque nouvelle rangée de bloc.

A cela vient s'ajouter une autre variable permettant d’empêcher les changements de hauteur trop fréquents. Ceci permet d'avoir de temps en temps des petites variations de hauteur comme sur l'image ci-dessous:

PyCraft_002.PNG
Figure N°1: Génération du terrain
Pour donner un peu plus de variété au jeu, les biomes vont venir changer la façon dont le terrain se remplit (le désert va être recouvert de sable tandis que la plaine sera recouverte de terre), la façon dont la hauteur change (une plaines sera plus plate qu'un désert) et ce qu'on y trouve. (dans les plaines, on trouve occasionnellement des arbres tandis que l'on trouvera des cactus dans le désert) Voici ce que cela donne en pratique:
Pycraft 13.05.2019 20_22_27.png
Figure N°2: Différence entre le désert et la plaine

Pour pouvoir rajouter facilement des biomes, j'ai décidé de les stocké dans des fichiers JSON comme celui ci:

{
	"id" : "pycraft:plain",
	"generation" : {
		"min_height" : "64",
		"max_height" : "70",
		"min_flat_area" : "5",
		"max_flat_area" : "7",
		"slope" : "1"
	},
	"tiles" : ["pycraft:grass_block", 1, "pycraft:dirt", 2],
	"temperature" : "0",
	"plants" : ["pycraft:grass","pycraft:azure_bluet","pycraft:oxeye_daisy","pycraft:cornflower"],
	"plants_rarety" : "1",
	"structures" : ["oak_tree"],
	"structures_rarety" : [10],
	"seize_min" : 30,
	"seize_max" : 50
}

Ici la valeur "id" est un identifiant unique pour chaque biome. Les valeurs "min_height" et "max_height" de "generation" correspondent au minimum et au maximum que la valeur de hauteur peut atteindre. Changer ces valeurs permet d'avoir des biomes qui sont plus haut ou plus bas que certains autres. "min_flat_area" et "max_flat_area" permet de gérée la platitude du terrain. Plus ces valeurs sont grandes, plus le terrain sera plat. "slope" définis de combien la valeur de hauteur peut être modifié.
"structures" et "structures_rarety" gère l'apparition de structure. (voir section 2.2.5) La liste "tiles" défini les blocs qui vont recouvrir le terrain. La liste "plants" contient les blocs qui vont venir s'ajouter au-dessus du terrain afin de décorer ce dernier. "plants_rarety" gère à quelle fréquence ces blocs vont apparaître.
La valeur "temperature" serre à ne pas avoir des biomes "non-compatible" l'un à côté de l'autre. Ceci empêche par exemple d'avoir une plaine enneigé à côté d'un désert. Finalement, les valeurs "seize_min" et "seize_max" définissent la taille maximal d'un biome.

2.2.3 Affichage du terrain

Nous disposons maintenant de tronçons qui contient divers blocs. Or, le problème actuel est que ces derniers ne sont pas visibles par le joueur. Pour les afficher à l'écran, j'ai décidé d'utiliser les sprites de pygame. J'ai donc crée une sous-classe de pygame.sprite.Sprite que voici:

import pygame

class sprite(pygame.sprite.Sprite):
    def __init__(self, texture, color, x, y, width, height):
       super().__init__()

       self.image = pygame.transform.scale(texture.texture, (width, height))
       self.rect = self.image.get_rect()
       self.rect.x = x
       self.rect.y = y

Ici la classe prend 6 paramètre lors de son instanciation: la position en x et y, la taille horizontal et vertical, la couleur (Note : la couleur n'était encore utilisée lors que ce paragraphe à été écrit.) et une texture. Cette texture est donnée par cette autre classe:

import pygame
import os

BLOCK_SEIZE_DRAW = 48

class texture:
    texture_list = []
    def __init__(self, texture):
        self.texture_id = texture
        self.texture = pygame.image.load(texture).convert_alpha()
        self.texture = pygame.transform.scale(self.texture,(BLOCK_SEIZE_DRAW,BLOCK_SEIZE_DRAW))

    @staticmethod
    def find_with_id(texture_id, texture_list):
        for tex in texture_list:
            if texture_id == tex.texture_id:
                return tex

    @staticmethod
    def LoadTextures(path):
        texture_list = []
        for file in os.listdir(path):
            if file[-3:]=="png":
                tmp = texture(path+"/"+file)
                texture_list.append(tmp)

        return texture_list

Cette classe va lister toutes les images dans un dossier spécifié et les charger de manière à ce que pygame comprenne que ce sont des images.
% Pour ensuite demander à pygame d'afficher les blocs, il nous suffit de créer un groupe de sprites de cette manière et d'y ajouter une instance de tile pour chaque bloc à afficher. Voici un exemple pour afficher un bloc de pierre dans le coin supérieur gauche de l'écran:

import game_class.texture as tex
sprite_list.add(tex.tile(tex.texture.find_with_id("textures/block/stone.png", TEXTURES),
                        (255,255,255), 0, 0, 48, 48))

Ceci ne peut pas être appliqué à tous les blocs chargé en mémoire. Car comme le montre le schéma suivant, une grande partie des blocs se situe hors de l’écran et demander à pygame d'afficher ses blocs (même s'ils ne sont pas visible à l'écran) est coûteux en performance.

Render.png
Figure N°3: Ce qui est chargé en mémoire et ce qui doit être affiché
Il nous faut donc demander à pygame de n'afficher que les blocs présents dans la zone de l'écran. Cela donne le code suivant:

def draw(self, BLOCKS, TEXTURES, screen_width, screen_height):
        sprite_list = pygame.sprite.Group()

        for single_chunk in self.loaded_chunk:
            for x in range(chunk.CHUNK_SEIZE):
                if x*blo.BLOCK_SEIZE+single_chunk.x*blo.BLOCK_SEIZE*chunk.CHUNK_SEIZE+
                    self.camera_x*blo.BLOCK_SEIZE+blo.BLOCK_SEIZE*3 < -screen_width or 
                   x*blo.BLOCK_SEIZE+single_chunk.x*blo.BLOCK_SEIZE*chunk.CHUNK_SEIZE+
                   self.camera_x*blo.BLOCK_SEIZE-blo.BLOCK_SEIZE > screen_width :
                    break

                for y in range(int((self.camera_y*blo.BLOCK_SEIZE-screen_height/2)//blo.BLOCK_SEIZE), 
                                       int((self.camera_y*blo.BLOCK_SEIZE+screen_height/2)//blo.BLOCK_SEIZE)+2):
                    if single_chunk.getblock(x,y)!= "pycraft:air" and 
                        (self.camera_y*blo.BLOCK_SEIZE-y*blo.BLOCK_SEIZE+screen_height/2) <= screen_height and 
                        (self.camera_y*blo.BLOCK_SEIZE y*blo.BLOCK_SEIZE+screen_height/2) >= -blo.BLOCK_SEIZE:
                        sprite_list.add(tex.tile(blo.block.find_with_id(single_chunk.getblock(x,y),BLOCKS).m_sprites[0],(255,255,255),
                                        x*blo.BLOCK_SEIZE+single_chunk.x*blo.BLOCK_SEIZE*chunk.CHUNK_SEIZE+self.camera_x*blo.BLOCK_SEIZE,
                                        self.camera_y*blo.BLOCK_SEIZE-y*blo.BLOCK_SEIZE+screen_height/2,
                                        blo.BLOCK_SEIZE,blo.BLOCK_SEIZE))

Ici en plus de demander d'afficher les blocs. Nous devons appliquer plusieurs opérations sur les coordonnées des blocs pour pouvoir prendre en compte la position de la camera et l'espace qu'occupent les blocs.

2.2.4 Sauvegarde du terrain

Nous avons maintenant un terrain qui se génère aléatoirement et à l'infini. Le problème est maintenant que plus le joueur va se déplacer plus, il y aura de terrain chargé en mémoire. Il nous faut donc décharger la mémoire. Pour ce faire, nous ne pouvons pas simplement supprimer des parties de terrain, il faut aussi que nous les sauvegardions. Pour ce faire, nous allons utiliser les tronçons. Comme chaque tronçon possédé une position, il nous suffit de vérifier s'ils sont plus loin qu'une certaine distance du joueur pour ensuite les sauvegarder et les supprimer de la mémoire. Pour sauvegarder les tronçons, il nous faut sauvegarder les blocs qu'il contient. Or comme les tronçons ont une dimension finie, nous n'avons pas besoin de sauvegarder les coordonnées des blocs. Il nous suffit de prendre une liste contenant tout les blocs. Une de mes premières idées aurait été de sauvegarder une simple liste d'id. On se retrouverait alors avec un fichier de ce type:

pycraft:air;pycraft:air;pycraft:air;pycraft:air;pycraft:stone;pycraft:stone;...

Le problème avec cette méthode est que cela est très volumineux. Pour rendre cela plus compacte, nous pouvons ajouter une ligne contenant une fois chaque IDs des blocs présents dans le tronçon. De cette manière, la deuxième ligne ne serait composée que d'index faisant référence à un ID de la ligne supérieur. Ceci s’apparente beaucoup à faire une jointure entre deux tables d'une base de donnée.

pycraft:stone;pycraft:sandstone;pycraft:sand;pycraft:dirt;pycraft:grass_block;
pycraft:air;pycraft:azure_bluet;pycraft:grass;pycraft:oxeye_daisy;
0;1;1;1;1;1;1;1;1;0;0;0;0;0;0;0;0;1;1;1;1;1;1;1;1;0;0;0;0;0;1;1;1;2;2;2;2;2;2;2;2;3;3;3;3;3;
1;1;1;2;2;2;2;2;2;2;2;3;3;3;3;3;2;2;2;2;2;2;2;2;2;2;2;4;4;4;4;4;2;2;2;5;5;5;5;5;5;5;5;
6;7;8;5;6;2;2;2;5;5;5;5;5;5;5;5;5;5;5;5;5;5;5;5;5;5;5;5;5;5

2.2.5 Les structures

Maintenant que nous avons une méthode pour sauvegarder et de charger des parties de terrains, nous pouvons ajouter des structures! Les structures dans Minecraft sont des ensembles de blocs formants généralement un bâtiment. Celles-ci peuvent être trouvées aléatoirement dans le monde. Ici, nous allons utiliser le même principe pour créer les arbres. Pour ce faire, nous avons besoin d'une classe similaire à celle utiliser pour les blocs et les biomes. Cependant, elle a une particularité: elle n'utilise pas un fichier JSON, mais plutôt un fichier similaire à ceux utiliser pour sauvegarder les tronçons. Comme les structures n'ont pas une taille de 16x256 et que nous voulons qu'elle ait un id, il nous faut rajouter une ligne supplémentaire avec ces informations. Ceci nous donne un fichier de ce type:

oak_tree;5;6;
pycraft:void;pycraft:oak_log;pycraft:oak_leaves;pycraft:oak_log_leaves;
0;0;1;0;0;0;0;1;0;0;2;2;3;2;2;2;2;3;2;2;0;2;3;2;0;0;0;2;0;0;

Nous avons donc ensuite besoin d'une classe contenant l'id de la structure et un tableau de variable contenant les blocs composants la structure. Comme cette classe est très similaire à celle des blocs et des biomes, je ne vais pas vous la montrer ici. Ces structures pourront ensuite être placées aléatoirement lors de la génération du terrain grâce aux valeurs stockées dans le fichier JSON des biomes. Voici ce que cela donne:

Pycraft 13.05.2019 21_36_28.png
Figure N°4: Exemple de structure

2.2.6 Ciel

Dans cette section, je vais parler de l'affiche du ciel qui est constitué de nuages, d'un soleil et d'un fond. Le soleil a une particularité: il ne se déplace pas de manière linéaire. Au début et à la fin de la journée, il sera situé vers le bas de l'écran tandis qu'à midi, il sera en haut de l'écran. Comme nous avons trois points par les quelles, nous voulons que le soleil passe, il est possible de trouver l'équation d'une courbe passant par ces points.

Sun.png
Figure N°5: Trajectoire du soleil

Pour trouver cette équation, nous n'avons pas besoin d'utiliser des mathématiques très avancées; ce simple système d'équations peut nous suffire:

sun_equation.png
Figure N°6: Equation pour la trajectoire du soleil

En résolvant ce système d'équations en a, b et c, nous pouvons trouver une équation du deuxième degré passant par les points que nous voulons. Il nous suffis alors de remplace h1, h2, h3 par la hauteur à la quelle nous voulons que le soleil soit et t1, t2, t3 par le temps au quelle nous voulons que le soleil atteigne la hauteur. Pour simplifier le problème, il nous mettre sur une échelle allant de 0 à 1. On a donc: h1 = 0 (corresponds au bas de l'écran), h2 = 1(corresponds au haut de l'écran), h3 = 0, t1 = 0 (corresponds au matin), t2 = 0.5 (corresponds à midi), t3 = 1 (corresponds au soir) De cette manière, si nous changeons la taille de l'écran ou la durée d'une journée, nous n'aurons pas besoin de recalculer l'équation. Il nous suffira de multiplier la position par la taille de l'écran. L'équation final pour la trajectoire du soleil est : -4x^2+4x


Le fond est lui ne se déplace pas, mais change de couleur en fonction de l'heure: bleu le jour, noir la nuit et orange lors du coucher et lever du soleil. Pour ce faire, j'ai décidé d'utiliser une liste contenant les couleurs que le ciel doit avoir en fonction de l'heure dans le jeu. Cependant, si l'on récupère simplement la couleur dans le tableau, nous allons avoir un ciel qui change de couleur sans transition (ce qui n'est pas très beau).
Pour corriger cela, j'ai décidé de faire des moyennes pondérée des couleurs du ciel en fonction du temps. (La pondération dépends de combien, le temps actuel est proche de l'heure suivant.) Pour ce faire, j'ai créé une nouvelle classe couleur que voici:

class color:
    def __init__(self, r, g, b):
        self.r = float(r)
        self.g = float(g)
        self.b = float(b)

    def __add__(self, other):
        return color(self.r+other.r, self.g+other.g, self.b+other.b)

    def __mul__(self, other):
        return color(self.r*other, self.g*other, self.b*other)

    def tuple(self):
        return (min(max(0,int(self.r)),255), min(max(0,int(self.g)),255), min(max(0,int(self.b)),255))

    @staticmethod
    def mix(color1, value1, color2, value2):
        return (color1*value1 + color2*value2)*(1/(value1+value2))

Cette classe possède deux fonctions particulières: add et mul. Ces dernières permettent de pouvoir définir de règle d'addition et de multiplication pour la classe. Ici, elle nous permettent de pouvoir additonner deux couleurs et de les multiplier un scalaire. Nous avons aussi une fonction pour convertir la couleur en tuple. Ce tuple pourra ensuite être donné à Pygame pour colorier le ciel.
Il nous suffit ensuite de faire une moyenne pondérée entre deux couleurs du tableau de variable.

SKYCOLOR = [color(180,220,255),color(180,220,255),color(180,220,255),color(180,220,255),
            color(180,220,255),color(180,220,255),color(180,220,255),color(180,220,255),
            color(180,220,255),color(180,220,255),color(250,145,47),color(0,0,40),color(0,0,40),
            color(0,0,40),color(0,0,40),color(0,0,40),color(0,0,40),color(0,0,40),
            color(0,0,40),color(0,0,40),color(0,0,40),color(0,0,40),color(0,0,40),color(250,145,47)]
value = time/HOURTIME%len(SKYCOLOR)%1
        
skycolor = color.mix(SKYCOLOR[int((time/HOURTIME) % (len(SKYCOLOR)))], 1-value,
                            SKYCOLOR[int(((time/HOURTIME)+1) % (len(SKYCOLOR)))], value)

Voici le déroulement d'une journée dans le jeu.

2.2.7 Le joueur

Pour le joueur, j'ai décidé de crée une classe "entité" de là quelle serait dérivé une classe "joueur". De cette manière, si dans le futur nous rajoutons des animaux, ils pourront eux aussi être une sous-classe de "entité". La classe entité est constitué de trois fonctions: une pour l'instanciation, une pour afficher l'entité à l'écran et une pour que l'entité interagisse avec le monde. C'est dans cette dernière que sont gérées la gravité ainsi que les colision avec le terrain.

def step(self, deltaTime, WORLD, BLOCK, ENTITY):
        gravity = True
        for i in range(0, math.ceil(self.hitbox[0])):
            if blo.block.find_with_id(WORLD.getblock(self.x+i+0.5,self.y - self.hitbox[1]/2), BLOCK).block_type == 
                     "solid" and self.motion[1] <= 0:
                if self.motion[1]<FALL_DAMAGE_SPEED:
                    self.hp += (self.motion[1]-FALL_DAMAGE_SPEED)*FALL_DAMAGE_SCALE
               
                self.motion[1] = 0
                gravity=False

        if gravity:
            self.motion[1] -= GRAVITY
        
        for i in range(0, math.ceil(self.hitbox[1])):
            if blo.block.find_with_id(WORLD.getblock(self.x + self.hitbox[0],self.y+i), BLOCK).block_type == 
                 "solid" and self.motion[0] > 0:
                self.motion[0] = 0

        for i in range(0, math.ceil(self.hitbox[1])):
            if blo.block.find_with_id(WORLD.getblock(self.x - self.hitbox[0]/2,self.y+i), BLOCK).block_type == 
                 "solid" and self.motion[0] < 0:
                self.motion[0] = 0

        if blo.block.find_with_id(WORLD.getblock(self.x + self.hitbox[0]/2,self.y), BLOCK).block_type == 
              "solid" and self.motion[1] > 0:
            self.motion[1] = 0

        while blo.block.find_with_id(WORLD.getblock(self.x+0.5,self.y - self.hitbox[1]/2 +0.1), BLOCK).block_type == "solid":
            self.y += 0.1

        if self.motion[0]!=0:
            self.walking_time += deltaTime
            self.direction = self.motion[0] // abs(self.motion[0])
        else:
            self.walking_time = 0

        
        self.x += self.motion[0] * deltaTime
        self.y += self.motion[1] * deltaTime

Ici, pour calculer les collisions, le jeu va regarder si dans la direction ou se déplace l'entité, il y a un bloc de type "solid" (ce type est défini par les fichiers JSON associé au bloc). Si le bloc en face de l'entité est de type solide alors la vitesse de l'entité est mise à zéro. De cette manière, elle ne pourra pas traverser le terrain.
Pour appliquer la gravité à l'entité, le jeu va regarder s'il n'y a pas un bloc de type solide sous l'entité. Ceci permet comme le montre cette image de ne pas passer à travers le sol et de pouvoir être devant un arbre.

Pycraft 16.05.2019 14_56_53.png
Figure N°7: Exemple de bloc traversable et non traversable

La classe joueur contient les mêmes fonctions que la classe entité. La seule différence est que la classe joueur va venir rajouter des nouvelles instructions dans chacune de ses fonctions. De cette manière, le joueur peut avoir plus de fonctionnalité.

def step(self, deltaTime, WORLD, BLOCK, ENTITY):
        super().step(deltaTime, WORLD, BLOCK, ENTITY)
        
        keys = pygame.key.get_pressed()
        if (keys[pygame.K_LEFT] or keys[pygame.K_a]):
            self.motion[0] = self.speed
            
        if (keys[pygame.K_RIGHT] or keys[pygame.K_d]):
            self.motion[0] = -self.speed

        if (not keys[pygame.K_LEFT] and not keys[pygame.K_a] and not keys[pygame.K_RIGHT] and not keys[pygame.K_d]):
            self.motion[0] = 0
            
        if blo.block.find_with_id(WORLD.getblock(self.x,self.y - self.hitbox[1]/2), BLOCK).block_type == 
                      "solid" and (keys[pygame.K_UP] or keys[pygame.K_w]):
            self.motion[1] = self.jump_power

Ici, la deuxième ligne permet d'exécuter les instructions de la fonction step de la classe parent à la classe joueur. Autrement dit elle exécute le morceau de code montré un peu plus haut. En plus de cela, nous venons ajouter le code pour les déplacements du joueur. Ceci permet d'avoir un joueur qui peut se déplacer, qui ne passe pas à travers le terrain et qui est soumis à la gravité.

2.2.8 Les menus et interfaces graphiques

Le joueur peut maintenant explorer un monde et le sauvegarder. Cependant, il ne peut pas créer de nouvelle parti. Pour ajouter cela, nous allons devoir ajouter un menu principal. Ici, j'ai encore une fois choisi de passer par les fichiers JSON.

{
	"id" : "title",
	"elements" : [
		{
			"type" : "image",
			"sprites" : ["textures/ui/title_background.png"],
			"x" : -640,
			"y" : 0,
			"w" : 640,
			"h" : 480
		},
		{
			"type" : "button",
			"id" : "title_play",
			"function":"title_play",
			"text" : "Play",
			"text_x" : 300,
			"text_y" : 247,
			"text_seize" : 15,
			"text_color" : [255, 255, 255],
			"sprites":["textures/ui/button_0.png", "textures/ui/button_1.png", "textures/ui/button_2.png", 
                                        "textures/ui/button_3.png", "textures/ui/button_4.png", "textures/ui/button_5.png", 
                                        "textures/ui/button_6.png", "textures/ui/button_7.png", "textures/ui/button_8.png"],
			"n" : 5,
			"x" : 240,
			"y" : 240,
			"w" : 32,
			"h" : 32
		},
		{
			"type" : "text",
			"text" : "version 1.0.0",
			"text_x" : 0,
			"text_y" : 460,
			"text_seize" : 15,
			"text_color" : [255, 255, 255]
		}
	]
}

La particularité des fichiers JSON pour les interfaces graphiques est qu'ils contient une liste d'objets qui sont tous différents les uns des autres. Pour savoir de quel type sont ces objets, la valeur "type" est là.
Encore une fois, ces fichiers JSON sont associés à une classe très similaire à celle des blocs; je ne vais donc pas trop en parler.

Pour pouvoir faire en sorte que quand le joueur clique sur un bouton une action s'effectue, j'ai utilisé un dictionnaire. Dans ce dernier, la clé correspond à la valeur "function" du fichier JSON et la valeur associé à cette clé correspond au nom d'une fonction:

def function_pause_game():
    global is_Game_Paused, pause_menu
    is_Game_Paused = not is_Game_Paused
    pause_menu = "pause"

hud_item = {"pause_continue":function_pause_game}

Avec cela, il est possible d'appeler la fonction grâce au dictionnaire de cette manière:

hud_item["pause_continue"]()

Voici un exemple d'interface réalisée avec ce système:

Pycraft 15.05.2019 13_19_05.png
Figure N°8: Menu principal

3. Résultats

Comme résultat, nous avons que les principes de base fonctionne: Nous avons donc un jeu dont le terrain se génère aléatoirement et peut être sauvegardé puis rechargé. Le joueur peut placer et casser des blocs. Et la plupart des éléments comme les blocs, les biomes et les interfaces graphiques sont stockés dans des fichiers JSON. Le jeu tourne avec un nombre image par seconde satisfaisant.

Cependant, le jeu est loin d'être fini. Il y a plusieurs fonctionnalités que je n'ai pas pu implémenter par manque de temps. On peut par exemple voir dans les fichiers JSON de blocs plusieurs valeurs qui ne sont pas utilisées. En plus de cela, les tronçons contiennent des valeurs pour l'éclairage des blocs à dire que le système de lumière n'a pas été implémenté.

4. Discussion

Le jeu est aussi loin d'être parfait. J'ai noté plusieurs points qui pourraient être amélioré.

4.1 Les fichiers JSON

Le premier défaut avec ma façon de charger les fichiers JSON est qu'elle n'est pas "futur prof". C'est-à-dire que si je décide un jour d'ajouter une clé pour certains fichiers JSON, je vais devoir modifier chaqu'un des fichiers. L'idéale ici aurait été de vérifé si la clé est présente dans le fichier JSON et dans le cas contraire définir une valeur par défaut.

En plus de cela, je ne suis pas très content avec ma façon de faire interragir les différentes classes ensemble. Dans le code actuel pour transférer des données d'une classe à une autre, j'utilise une liste retournée par une première classe et je la donne comme argument à une fonction de la deuxième classe. Cela pourrait être supprimé si toutes les classes étaient dans le même fichier ou si une des classes rechargeaient de son côté tout les données (blocs, biomes, textures, ...).

4.2 Génération du terrain

Ici, le seul point que je pourrais améliorer serait de permettre au jeu de créer les tronçons qui auraient été supprimés, car dans le code actuel, les tronçons ne peuvent être généré qu'une seule fois. Ceci permettrait de continuer une partie même si celle-ci a été partiellement corrompue.
Du côté des structures, le problème principal est qu'elles ne peuvent pas se générer entre deux tronçons. Le problème ici vient du fait qu'il faut que le tronçon existe pour que la structure puisse placer chaqu'un de ses blocs. Or, dans le bord d'un tronçon, la structure est obligé de déborder sur le tronçon d'à côté (celui-ci n'existant pas encore). Une correction à cela serait de forcer la génération du tronçon suivant pour que la structure puisse être générée.

4.3 Affichage du terrain

Bien que mon système pour afficher le terrain fonctionne correctement, il est loin d'être optimisé. Cela est peut-être dû au fait que j'utiliser des sprites pour l'affichage.
Dans le cas où le système de rendu du terrain serait plus performant, il serait possible d'afficher beaucoup plus de blocs à l'écran que maintenant. Il existe d'autre jeu comme Starbound qui suivent le même principe que celui-ci et qui arrive à afficher beaucoup plus de blocs:

Starbound.png
Figure N°9: Affichage du terrain dans Starbound (source: https://playstarbound.com/
)

4.4 Sauvegarde du terrain

La sauvegarder le terrain pourrait être modifié de beaucoup de manières. La première serait de sauvegarder plusieurs tronçons dans un seul fichier. Ceci permettrait d'utiliser moins d'espace de stockage. J'avais aussi pensé à utiliser un indexage unique pour tout le monde. De cette façon, les fichiers n'auraient plus besoin de la première ligne.

4.4 Entité et joueur

Le système de colisions est le défaut principal du joueur, car le joueur peut tomber avant d'avoir atteint la bordure d'un bloc. Ce point mériterai que l'on se penche plus dessus afin de perfectionner le système de colision.
Le système mise en place pour casser les blocs a lui aussi quelles que défauts. Le plus important est que le sélecteur est parfois un peu décaler de l'endroit où il devrait être ce qui cause des problèmes lorsque l'on veut casser.

5. Conclusion

En conclusion, j'aurai réussi à recréer une version en deux dimensions de Minecraft en python. Durant ce projet, j'aurai appris à optimiser un système d'affichage, à décrire des instances de classe grâce à des fichiers JSON et à utiliser une bonne partie des fonctionnalités de Pygame. Cependant, le jeu peut encore être amélioré et beaucoup de fonctionnalités pourraient être ajoutés.
J'ai aussi essayé d'appliquer une certaine philosophie tout au long de ce projet, qui s'appelle le soft code. Il s'agit d'une façon de programmer qui consiste à faire en sorte que le software soit modifiable grâce à des paramètres d'entrée. Celle-ci permet maintenant à quelqun d'exterieur de venir et de par exemple modifier un bloc juste en modifiant un fichier JSON. Les utilisateurs pourraient ainsi modifier le jeu comme il le souhaite et cela sans toucher au code source. Je pense que cette manière de programmer devrait être appliquées à beaucoup plus de programme. Cela permettrait par exemple d'avoir des interfaces qui conviendrait beaucoup plus à l'utilisateur puisqu'il pourrait les modifier à sa guise.

Références

https://www.pygame.org/news https://minecraft-fr.gamepedia.com/ https://www.minecraft.net/fr-fr/ Le site utilisé pour crée le logo: https://textcraft.net/

!Notes

Tous les sprites ont été réalisé par moi à l'exeption de ceux utilisés pour les fleurs. Ceux-ci provienne du jeu d'origne.