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 (
);
}
}
#endif
Toutefois, 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 (
);
}
#endif
III-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électionnezpublic
void
UpdateCollisionMesh
(
){
if
(!
hasSetCollider){
float
sqrDstFromViewerToEdge=
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.