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

Stockage des paramètres de génération

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 - Stockage des paramètres de génération


III. Résumé

Ce chapitre se concentre sur des améliorations du code afin de pouvoir ajouter de nouvelles fonctionnalités facilement. Aussi, dans cette vidéo, vous verrez comment sauvegarder les valeurs de votre générateur de terrain.

III-A. Implémentation

Dans un dossier « Data » (sous dossier du dossier « Script »), créez un nouveau script « NoiseData.cs » qui hérite de ScriptableObject. Un ScriptableObject permet de définir de nouveaux types de ressource que vous pouvez créer facilement par la suite et qui contiendront les données que vous avez définies au travers du script.
Dans notre cas, nous allons définir toutes les variables nécessaires pour la génération du bruit :

NoiseData.cs
Sélectionnez
using UnityEngine;
using System.Collections;

[CreateAssetMenu()]
public class NoiseData : UpdatableData {

    public Noise.NormalizeMode normalizeMode;

    public float noiseScale;

    public int octaves;
    [Range(0,1)]
    public float persistance;
    public float lacunarity;

    public int seed;
    public Vector2 offset;


    protected override void OnValidate() {
        if (lacunarity < 1) {
            lacunarity = 1;
        }
        if (octaves < 0) {
            octaves = 0;
        }

        base.OnValidate ();
    }

}

La fonction OnValidate() est aussi déplacée, car elle permet la cohérence des données choisies par l'utilisateur.

De même, un deuxième script, TerrainData.cs, est créé pour conserver les données de la génération du terrain :

TerrainData.cs
Sélectionnez
using UnityEngine;
using System.Collections;

[CreateAssetMenu()]
public class TerrainData : UpdatableData {
    
    public float uniformScale = 2.5f;

    public bool useFlatShading;
    public bool useFalloff;

    public float meshHeightMultiplier;
    public AnimationCurve meshHeightCurve;
}

Pour utiliser ces nouvelles classes, vous devez ajouter des références à celles-ci dans MapGenerator.cs :

MapGenerator.cs
Sélectionnez
public TerrainData terrainData;
public NoiseData noiseData;

Évidemment, toutes les références aux variables qui ont été déplacées doivent être mises à jour afin d'utiliser ces structures. Il est nécessaire de faire de même pour le fichier EndlessTerrain.cs.

Ensuite, il est nécessaire de créer les données NoiseData et TerrainData à assigner à l'objet de la scène. Ces ressources sont placées dans un nouveau dossier « Terrain Assets ».

III-B. Génération à la volée

Suite à cette modification des scripts, le terrain n'est plus mis à jour lorsque les données changent. Pour corriger cela, un troisième script est créé : UpdatableData.cs. Voici son contenu :

UpdatableData.cs
Sélectionnez
using UnityEngine;
using System.Collections;

public class UpdatableData : ScriptableObject {

    public event System.Action OnValuesUpdated;
    public bool autoUpdate;

    protected virtual void OnValidate() {
        if (autoUpdate) {
            NotifyOfUpdatedValues ();
        }
    }

    public void NotifyOfUpdatedValues() {
        if (OnValuesUpdated != null) {
            OnValuesUpdated ();
        }
    }

}

Cette classe est héritée par NoiseData et TerrainData. Elle définit un événement (« System.Action ») qui est appelé s'il est défini. La fonction OnValidate() est appelée, car une nouvelle valeur est disponible et donc, qu'il faut notifier de ce changement.

Aussi, nous ajoutons un nouveau bouton dans l'éditeur, utile si la mise à jour automatique est désactivée. Cela se fait à travers la création d'un nouveau script, UpdatableDataEditor.cs, héritant de la classe Editor :

UpdatableDataEditor.cs
Sélectionnez
using UnityEngine;
using System.Collections;
using UnityEditor;

[CustomEditor (typeof(UpdatableData), true)]
public class UpdatableDataEditor : Editor {

    public override void OnInspectorGUI ()
    {
        base.OnInspectorGUI ();

        UpdatableData data = (UpdatableData)target;

        if (GUILayout.Button ("Update")) {
            data.NotifyOfUpdatedValues ();
        }
    }
    
}

L'option true de l'attribut CustomEditor permet d'indiquer que ce code doit être appliqué aussi aux classes héritant de UpdatableData. Sans cette option, NoiseData et TerrainData ne pourraient avoir le bouton nouvellement défini.

Finalement, dans MapGenerator.cs, il est nécessaire de connecter l'événement indiquant le changement des valeurs à une fonction :

MapGenerator.cs
Sélectionnez
void OnValuesUpdated() {
    if (!Application.isPlaying) {
        DrawMapInEditor ();
    }
}

Ainsi connectée :

 
Sélectionnez
void OnValidate() {

    if (terrainData != null) {
        terrainData.OnValuesUpdated -= OnValuesUpdated;
        terrainData.OnValuesUpdated += OnValuesUpdated;
    }
    if (noiseData != null) {
        noiseData.OnValuesUpdated -= OnValuesUpdated;
        noiseData.OnValuesUpdated += OnValuesUpdated;
    }

    falloffMap = FalloffGenerator.GenerateFalloffMap (mapChunkSize);
}

Afin de ne pas connecter plusieurs fois la fonction à l'événement, on déconnecte l'ancienne connexion avant de faire la nouvelle connexion.

III-C. Amélioration du code

Pour la suite de la série, nous supprimons les données des couleurs et donc, les types de terrains. La structure MapData devient donc :

MapGenerator.cs
Sélectionnez
public struct MapData {
    public readonly float[,] heightMap;

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

Par conséquent, la fonction GenerateMapData est tronquée :

MapGenerator.cs
Sélectionnez
MapData GenerateMapData(Vector2 centre) {
    float[,] noiseMap = Noise.GenerateNoiseMap (mapChunkSize + 2, mapChunkSize + 2, noiseData.seed, noiseData.noiseScale, noiseData.octaves, noiseData.persistance, noiseData.lacunarity, centre + noiseData.offset, noiseData.normalizeMode);

        for (int y = 0; y < mapChunkSize+2; y++) {
            for (int x = 0; x < mapChunkSize+2; x++) {
                if (terrainData.useFalloff) {
                    noiseMap [x, y] = Mathf.Clamp01 (noiseMap [x, y] - falloffMap [x, y]);
                }
            
            }
        }

    }


    return new MapData (noiseMap);
}

Ainsi que DrawMapInEditor :

MapGenerator.cs
Sélectionnez
public void DrawMapInEditor() {
    MapData mapData = GenerateMapData (Vector2.zero);

    MapDisplay display = FindObjectOfType<MapDisplay> ();
    if (drawMode == DrawMode.NoiseMap) {
        display.DrawTexture (TextureGenerator.TextureFromHeightMap (mapData.heightMap));
    } else if (drawMode == DrawMode.Mesh) {
        display.DrawMesh (MeshGenerator.GenerateTerrainMesh (mapData.heightMap, terrainData.meshHeightMultiplier, terrainData.meshHeightCurve, editorPreviewLOD,terrainData.useFlatShading));
    } else if (drawMode == DrawMode.FalloffMap) {
        display.DrawTexture(TextureGenerator.TextureFromHeightMap(FalloffGenerator.GenerateFalloffMap(mapChunkSize)));
    }
}

La classe MapDisplay est aussi impactée :

MapDisplay.cs
Sélectionnez
using UnityEngine;
using System.Collections;

public class MapDisplay : MonoBehaviour {

    public Renderer textureRender;
    public MeshFilter meshFilter;
    public MeshRenderer meshRenderer;

    public void DrawTexture(Texture2D texture) {
        textureRender.sharedMaterial.mainTexture = texture;
        textureRender.transform.localScale = new Vector3 (texture.width, 1, texture.height);
    }

    public void DrawMesh(MeshData meshData) {
        meshFilter.sharedMesh = meshData.CreateMesh ();

        meshFilter.transform.localScale = Vector3.one * FindObjectOfType<MapGenerator> ().terrainData.uniformScale;
    }

}

L'auteur en profite pour dimensionner le modèle à la taille indiquée dans TerrainData.

Finalement, la classe EndlessTerrain.cs doit aussi être modifiée :

EndlessTerrain.cs
Sélectionnez
void OnMapDataReceived(MapData mapData) {
    this.mapData = mapData;
    mapDataReceived = true;

    UpdateTerrainChunk ();
}

III-D. Bogue de bordure

Lors de l'utilisation de la carte de diminution, un bogue apparaît en bordure de terrain. Le problème est provoqué par la différence de taille entre la carte de diminution et la carte des bruits.
La méthode MapGenerator::Awake() est supprimée. De même, la génération de la carte de diminution dans la méthode OnValidate() est supprimée. Cela est remplacé en générant la carte dans la fonction GenerateMapData() :

MapGenerator.cs
Sélectionnez
MapData GenerateMapData(Vector2 centre) {
    float[,] noiseMap = Noise.GenerateNoiseMap (mapChunkSize + 2, mapChunkSize + 2, noiseData.seed, noiseData.noiseScale, noiseData.octaves, noiseData.persistance, noiseData.lacunarity, centre + noiseData.offset, noiseData.normalizeMode);

    if (terrainData.useFalloff) {

        if (falloffMap == null) {
            falloffMap = FalloffGenerator.GenerateFalloffMap (mapChunkSize + 2);
        }

        for (int y = 0; y < mapChunkSize+2; y++) {
            for (int x = 0; x < mapChunkSize+2; x++) {
                if (terrainData.useFalloff) {
                    noiseMap [x, y] = Mathf.Clamp01 (noiseMap [x, y] - falloffMap [x, y]);
                }
            
            }
        }

    }


    return new MapData (noiseMap);
}

III-E. Données des textures

La modification effectuée précédemment sur les données de bruit et de terrain est aussi appliquée sur les données des textures. Ainsi, un nouveau script TerrainData.cs est créé :

TerrainData.cs
Sélectionnez
using UnityEngine;
using System.Collections;

public class TextureData : UpdatableData {

    public void ApplyToMaterial(Material material) {
        //
    }
}

Évidemment, cette nouvelle classe doit être instanciée et utilisée dans la classe MapGenerator.cs comme cela a été fait pour NoiseData et TerrainData. Aussi, une nouvelle fonction est ajoutée pour la mise à jour des données :

MapGenerator.cs
Sélectionnez
void OnTextureValuesUpdated() {
    textureData.ApplyToMaterial (terrainMaterial);
}

Cette fonction est connectée à l'événement, dans la fonction OnValidate() :

MapGenerator.cs
Sélectionnez
if (textureData != null) {
    textureData.OnValuesUpdated -= OnTextureValuesUpdated;
    textureData.OnValuesUpdated += OnTextureValuesUpdated;
}

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 © 2018 Unity Technologies. Aucune reproduction, même partielle, ne peut être faite de ce site ni 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.