Unity - Génération procédurale de terrain

Threads

L'auteur

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Cette série explique comment générer de manière procédurale des continents dans Unity.
Vous pouvez retrouver les autres épisodes de cette série dans le sommaire dédié.

II. Vidéo


Unity - Génération procédurale de terrain - Threads


III. Résumé

Dans cet épisode, nous finissons le travail commencé lors de la précédente session en connectant le générateur de cartes à notre mécanisme de carte infinie. De plus, pour que le jeu ne se bloque pas lors du chargement des morceaux de la carte, nous utilisons les threads.

III-A. Travail préliminaire

Avant tout, le code de génération d'affichage des cartes (bruit et couleurs) ainsi que du modèle est déplacé dans une nouvelle méthode qui sera spécifique à l'utilisation dans l'éditeur.

 
Sélectionnez
public void DrawMapInEditor() {
    MapData mapData = GenerateMapData ();

    MapDisplay display = FindObjectOfType<MapDisplay> ();
    if (drawMode == DrawMode.NoiseMap) {
        display.DrawTexture (TextureGenerator.TextureFromHeightMap (mapData.heightMap));
    } else if (drawMode == DrawMode.ColourMap) {
        display.DrawTexture (TextureGenerator.TextureFromColourMap (mapData.colourMap, mapChunkSize, mapChunkSize));
    } else if (drawMode == DrawMode.Mesh) {
        display.DrawMesh (MeshGenerator.GenerateTerrainMesh (mapData.heightMap, meshHeightMultiplier, meshHeightCurve, levelOfDetail), TextureGenerator.TextureFromColourMap (mapData.colourMap, mapChunkSize, mapChunkSize));
    }
}

Ensuite, nous créons une nouvelle structure MapData pour contenir les informations de bruit et de couleurs. Cette structure est utilisée comme retour pour la fonction GenerateMap(). Ainsi, il est possible d'utiliser GenerateMap() à de multiples endroits dans le code.

 
Sélectionnez
public struct MapData {
    public readonly float[,] heightMap;
    public readonly Color[] colourMap;

    public MapData (float[,] heightMap, Color[] colourMap)
    {
        this.heightMap = heightMap;
        this.colourMap = colourMap;
    }
}

On remarque que Sebastian Lague est très précautionneux et rigoureux sur le nommage de ses variables et de ses méthodes. Cela est une bonne pratique, afin de garder un code clair et facilement compréhensible. En effet, le nom d'une variable ou d'une méthode doit indiquer son but.

III-B. Threads

Les threads permettent de calculer les données des cartes et du modèle en parallèle (notamment sur un autre cœur du CPU). Ainsi, le joueur ne ressent pas de latence lorsqu'il se déplace sur la carte et cela, même si le jeu doit charger/générer des données.

Même si les données ne peuvent pas être calculées directement, le principal est de conserver un taux de rafraîchissement stable et haut.

Lors de la mise en place des threads, il est toujours nécessaire d'être précautionneux. Ici, le script EndlessTerrain.cs va lancer les demandes de données des cartes au script MapGenerator.cs. Celui-ci va générer les données, puis, à travers la fonction MapDataThread(), les placer dans une queue. En effet, les manipulations graphiques (par exemple, l'affichage d'un modèle) ne peuvent être effectuées que dans le thread principal. Si vous appelez une fonction dans un thread, alors l'action est effectuée dans ce thread. La queue permet de stocker les informations jusqu'à leur utilisation par la méthode Update() du script MapGenerator.cs. En effet, cette dernière est exécutée par le thread principal. Ainsi, pour autant de fois que de données prêtes à être traitées, la fonction OnMapDataReceived() du script EndlessTerrain.cs sera appelée.

Voici l'implémentation des fonctions du script MapGenerator.cs :

 
Sélectionnez
public void RequestMapData(Action<MapData> callback) {
    ThreadStart threadStart = delegate {
        MapDataThread (callback);
    };

    new Thread (threadStart).Start ();
}

void MapDataThread(Action<MapData> callback) {
    MapData mapData = GenerateMapData ();
    lock (mapDataThreadInfoQueue) {
        mapDataThreadInfoQueue.Enqueue (new MapThreadInfo<MapData> (callback, mapData));
    }
}

public void RequestMeshData(MapData mapData, Action<MeshData> callback) {
    ThreadStart threadStart = delegate {
        MeshDataThread (mapData, callback);
    };

    new Thread (threadStart).Start ();
}

void MeshDataThread(MapData mapData, Action<MeshData> callback) {
    MeshData meshData = MeshGenerator.GenerateTerrainMesh (mapData.heightMap, meshHeightMultiplier, meshHeightCurve, levelOfDetail);
    lock (meshDataThreadInfoQueue) {
        meshDataThreadInfoQueue.Enqueue (new MapThreadInfo<MeshData> (callback, meshData));
    }
}

Un verrou (lock) est ajouté, car les queues peuvent être utilisées par plusieurs threads en même temps.

Pour conserver les données dans la queue, une nouvelle structure, générique afin d'être utilisée aussi bien pour la génération des cartes que du modèle, est mise en place :

 
Sélectionnez
struct MapThreadInfo<T> {
    public readonly Action<T> callback;
    public readonly T parameter;

    public MapThreadInfo (Action<T> callback, T parameter)
    {
        this.callback = callback;
        this.parameter = parameter;
    }
    
}

Finalement, une nouvelle méthode Update() est intégrée afin de faire en sorte que le thread principal traite les nouvelles données générées :

 
Sélectionnez
void Update() {
    if (mapDataThreadInfoQueue.Count > 0) {
        for (int i = 0; i < mapDataThreadInfoQueue.Count; i++) {
            MapThreadInfo<MapData> threadInfo = mapDataThreadInfoQueue.Dequeue ();
            threadInfo.callback (threadInfo.parameter);
        }
    }

    if (meshDataThreadInfoQueue.Count > 0) {
        for (int i = 0; i < meshDataThreadInfoQueue.Count; i++) {
            MapThreadInfo<MeshData> threadInfo = meshDataThreadInfoQueue.Dequeue ();
            threadInfo.callback (threadInfo.parameter);
        }
    }
}

Du côté du script EndlessTerrain.cs, nous ajoutons une référence pour pointer et avoir accès aux fonctions de MapGenerator.cs. Ensuite, nous implémentons les fonctions OnMapDataReceived() et OnMeshDataReceived() afin de traiter les données nouvellement générées :

 
Sélectionnez
void OnMapDataReceived(MapData mapData) {
    mapGenerator.RequestMeshData (mapData, OnMeshDataReceived);
}

void OnMeshDataReceived(MeshData meshData) {
    meshFilter.mesh = meshData.CreateMesh ();
}

Aussi, le constructeur de TerrainChunk est modifié pour envoyer les requêtes de données du terrain. Évidemment, le plan utilisé lors du précédent épisode pour tester le script n'est plus utile. De plus, maintenant nous voulons afficher un modèle, donc il est utile d'ajouter un MeshRenderer et un MeshFilter à l'objet généré.

III-C. Problème de la parallélisation

Maintenant que les modèles sont générés à la volée et possiblement par plusieurs threads à la fois, un bogue apparaît, lié à la courbe d'animation (AnimationCurve) utilisée dans la fonction GenerateTerrainMesh(). En effet, celle-ci retourne des valeurs incorrectes lorsque plusieurs threads exécutent la fonction Evaluate().
Il est possible de protéger les accès avec un verrou (comme pour les queues du script MapGenerator.cs). Toutefois, ce verrou va ralentir les autres threads et donc la génération des modèles. Une meilleure solution est de créer une nouvelle instance de la courbe au début de la fonction. La nouvelle instance sera donc une copie, mais locale au thread.

IV. Commenter

Vous pouvez commenter et donner vos avis dans la discussion associée sur le forum.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2017 Unity Technologies. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.