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.
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.
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 :
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 :
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 :
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 :
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.