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 - Débogage et optimisation
III. Résumé▲
Cet épisode couvre la correction de quelques problèmes et la mise en place de petites optimisations.
III-A. Corrections de bogues▲
III-A-1. Erreur PropertyToID▲
Si vous lancez le projet (mode « Play »), vous obtenez une erreur stipulant que la PropertyToID ne peut être appelée que dans le thread principal.
En regardant les détails de l'erreur, vous pourrez mieux comprendre son origine (et où, dans votre code, l'erreur se produit).
Pour résoudre l'erreur, il faut déplacer l'appel à la fonction UpdateMeshHeights() afin qu'elle soit exécutée dans le thread principal. Pour cela, placez l'appel dans la fonction DrawMapInEditor() et ajoutez un appel dans une nouvelle méthode Awake() dans la classe MapGenerator.cs :
void Awake() {
textureData.UpdateMeshHeights (terrainMaterial, terrainData.minHeight, terrainData.maxHeight);
}III-A-2. Correction de la construction de l'exécutable final▲
Maintenant, si vous essayez de construire le projet (afin de le distribuer) vous obtiendrez une nouvelle erreur stipulant que UnityEditor n'existe pas. En effet, le fichier UpdatableData.cs fait référence à la classe UnityEditor, qui elle, n'est pas présente dans l'exécutable final. Il est possible de conditionner le code, afin qu'il ne soit compilé que si l'on est dans l'éditeur :
#if UNITY_EDITOR
protected virtual void OnValidate() {
if (autoUpdate) {
UnityEditor.EditorApplication.update += NotifyOfUpdatedValues;
}
}
public void NotifyOfUpdatedValues() {
UnityEditor.EditorApplication.update -= NotifyOfUpdatedValues;
if (OnValuesUpdated != null) {
OnValuesUpdated ();
}
}
#endifToutefois, une telle modification fait que la méthode NoiseData::OnValidate() n'est plus une surcharge, car la méthode UpdatableData::OnValidate() n'existe plus. Du coup, une protection est aussi ajoutée dans NoiseData.cs :
#if UNITY_EDITOR
protected override void OnValidate() {
if (lacunarity < 1) {
lacunarity = 1;
}
if (octaves < 0) {
octaves = 0;
}
base.OnValidate ();
}
#endifIII-A-3. Terrain noir▲
Même si maintenant vous pouvez construire le projet, vous obtiendrez un terrain noir. Il faut modifier la méthode MapGenerator::Awake() pour mettre à jour, au démarrage de l'application, le matériau à utiliser pour le terrain :
void Awake() {
textureData.ApplyToMaterial (terrainMaterial);
textureData.UpdateMeshHeights (terrainMaterial, terrainData.minHeight, terrainData.maxHeight);
}III-B. Optimisations▲
Grâce au profileur, on peut remarquer que la génération des données de collision impacte énormément les performances. Pour améliorer cette situation, les données de collision ne seront plus générées que pour le morceau de terrain sur lequel le joueur se trouve.
Par ailleurs, on peut remarquer qu'il est possible d'activer la génération des données de collisions pour plusieurs niveaux de détails à la fois. Évidemment, cela ne devrait pas être le cas. Pour corriger cela, vous pouvez faire en sorte que l'utilisateur entre un nombre déterminant le niveau de détails à utiliser.
III-B-1. Implémentation▲
Le code du fichier EndlessTerrain.cs est fortement impacté par ces améliorations :
- ajoutez un int colliderLODIndex ;
- ajoutez un float constant colliderGenerationDistanceThreshold dont la valeur est 5 ;
- passez la variable colliderLODIndex au constructeur de TerrainChunk ;
- modifiez le constructeur pour prendre en compte cette nouvelle variable : elle est copiée dans une variable colliderLODIndex membre de la classe TerrainChunk. La variable collisionLODMesh n'est plus utile ;
- ajoutez un booléen, membre de la classe TerrainChunk, appelé hasSetCollider ;
- ajoutez une variable sqrVisibleDstThreshold avec un getter permettant de récupérer le carré de la variable visibleDstThreshold ;
-
ajoutez une nouvelle méthode, permettant de définir les données de collision suivant la distance du joueur, ainsi que de générer les données si nécessaire :
EndlessTerrain.csSélectionnezpublicvoidUpdateCollisionMesh(){if(!hasSetCollider){floatsqrDstFromViewerToEdge=bounds.SqrDistance(viewerPosition);if(sqrDstFromViewerToEdge<detailLevels[colliderLODIndex].sqrVisibleDstThreshold){if(!lodMeshes[colliderLODIndex].hasRequestedMesh){lodMeshes[colliderLODIndex].RequestMesh(mapData);}}if(sqrDstFromViewerToEdge<colliderGenerationDistanceThreshold*colliderGenerationDistanceThreshold){if(lodMeshes[colliderLODIndex].hasMesh){meshCollider.sharedMesh=lodMeshes[colliderLODIndex].mesh;hasSetCollider=true;}}}} - La fonction est appelée à chaque mise à jour du jeu, si le joueur a bougé, dans la méthode EndlessTerrain::Udpate() :
if (viewerPosition != viewerPositionOld) {
foreach (TerrainChunk chunk in visibleTerrainChunks) {
chunk.UpdateCollisionMesh ();
}
}Sachant que seules les positions X et Z sont utilisées pour savoir si le joueur bouge, au lancement du jeu, les données de collision ne sont pas générées : le joueur n'a as bougé (mis à part sur l'axe des Y).. Pour corriger ce cas, l'action du LODMesh est transformée en event pour permettre à deux fonctions d'être appelées au déclenchement de l'événement. L'ancien code lié à l'action est retiré. L'événement est configuré dans le constructeur de TerrainChunk lors de la création des LODMesh :
lodMeshes = new LODMesh[detailLevels.Length];
for (int i = 0; i < detailLevels.Length; i++) {
lodMeshes[i] = new LODMesh(detailLevels[i].lod);
lodMeshes[i].updateCallback += UpdateTerrainChunk;
if (i == colliderLODIndex) {
lodMeshes[i].updateCallback += UpdateCollisionMesh;
}
}III-B-2. Diminution de la taille des morceaux de terrain▲
Une seconde possibilité pour réduire le temps de génération est de réduire le nombre des données à générer en réduisant la taille des morceaux de terrain.
Toutefois, les valeurs utilisées jusqu'à présent sont soumises à deux contraintes :
- ne pas être trop grandes, car le nombre de sommets d'un seul modèle est limité par Unity ;
- être compatible avec l'implémentation du niveau de détail. Sachant que nous supportons jusqu'à sept niveaux de détail, les valeurs possibles pour la taille des morceaux sont très limitées. Si nous n'en supportions plus que cinq, un choix plus grand de valeurs serait possible.
Ajoutez un entier constant, membre de la classe MeshGenerator, déterminant le nombre de niveaux supportés :
public const int numSupportedLODs = 5;Grâce à cette nouvelle variable, restreignez les valeurs possibles pour la variable LODInfo::lod :
[Range(0,MeshGenerator.numSupportedLODs-1)]
public int lod;Il est nécessaire de faire de même pour la variable MapGenerator::editorPreviewLOD :
[Range(0,MeshGenerator.numSupportedLODs-1)]
public int editorPreviewLOD;Maintenant que nous avons plus de choix pour les tailles des morceaux, nous pouvons mettre en place un mécanisme permettant à l'utilisateur de choisir parmi les différentes valeurs valides :
public const int numSupportedChunkSizes = 9;
public const int numSupportedFlatshadedChunkSizes = 3;
public static readonly int[] supportedChunkSizes = {48,72,96,120,144,168,192,216,240};
public static readonly int[] supportedFlatshadedChunkSizes = {48,72,96};Les tableaux ne peuvent pas être définis constants. Par contre, on peut obtenir un comportement assez proche en combinant les qualificateurs static et readonly.
Ainsi, dans le fichier MapGenerator.cs, nous pouvons afficher le nouveau paramètre à l'utilisateur :
[Range(0,MeshGenerator.numSupportedChunkSizes-1)]
public int chunkSizeIndex; [Range(0,MeshGenerator.numSupportedFlatshadedChunkSizes-1)]
public int flatshadedChunkSizeIndex;Et utiliser les variables dans le getter de mapChunkSize :
public int mapChunkSize {
get {
if (terrainData.useFlatShading) {
return MeshGenerator.supportedFlatshadedChunkSizes [flatshadedChunkSizeIndex] -1;
} else {
return MeshGenerator.supportedChunkSizes [chunkSizeIndex] -1;
}
}
}III-B-3. Amélioration de la gestion de la visibilité des morceaux▲
La gestion de la visibilité des morceaux de terrains peut être améliorée. Pour cela :
- renommez la variable terrainChunksVisibleLastUpdate en visibleTerrainChunks ;
-
modifiez la méthode UpdateTerrainChunk() pour qu'elle gère d'elle-même la visibilité des morceaux :
EndlessTerrain.csSélectionnezif(wasVisible!=visible){if(visible){visibleTerrainChunks.Add(this);}else{visibleTerrainChunks.Remove(this);}SetVisible(visible);} - modifiez la méthode UpdateVisibleChunks() pour prendre en compte ces changements :
void UpdateVisibleChunks() {
HashSet<Vector2> alreadyUpdatedChunkCoords = new HashSet<Vector2> ();
for (int i = visibleTerrainChunks.Count-1; i >= 0; i--) {
alreadyUpdatedChunkCoords.Add (visibleTerrainChunks [i].coord);
visibleTerrainChunks [i].UpdateTerrainChunk ();
}
int currentChunkCoordX = Mathf.RoundToInt (viewerPosition.x / chunkSize);
int currentChunkCoordY = Mathf.RoundToInt (viewerPosition.y / chunkSize);
for (int yOffset = -chunksVisibleInViewDst; yOffset <= chunksVisibleInViewDst; yOffset++) {
for (int xOffset = -chunksVisibleInViewDst; xOffset <= chunksVisibleInViewDst; xOffset++) {
Vector2 viewedChunkCoord = new Vector2 (currentChunkCoordX + xOffset, currentChunkCoordY + yOffset);
if (!alreadyUpdatedChunkCoords.Contains (viewedChunkCoord)) {
if (terrainChunkDictionary.ContainsKey (viewedChunkCoord)) {
terrainChunkDictionary [viewedChunkCoord].UpdateTerrainChunk ();
} else {
terrainChunkDictionary.Add (viewedChunkCoord, new TerrainChunk (viewedChunkCoord, chunkSize, detailLevels, colliderLODIndex, transform, mapMaterial));
}
}
}
}
}Une trace des morceaux mis à jour est conservée, afin de ne pas mettre deux fois à jour les mêmes morceaux. La position des morceaux est utilisée pour identifier les morceaux mis à jour.
La boucle démarre de la fin de la liste, car la méthode appelée peut supprimer des éléments de cette liste. Sans ce parcours inversé, l'index utilisé serait hors des limites du tableau une fois ces éléments retirés.
IV. Commenter▲
Vous pouvez commenter et donner vos avis dans la discussion associée sur le forum.



