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 :
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 :
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 :
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 :
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 :
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 :
void
OnValuesUpdated
(
) {
if
(!
Application.
isPlaying) {
DrawMapInEditor (
);
}
}
Ainsi connectée :
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 :
public
struct
MapData {
public
readonly
float
[,]
heightMap;
public
MapData (
float
[,]
heightMap)
{
this
.
heightMap =
heightMap;
}
}
Par conséquent, la fonction GenerateMapData est tronquée :
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 :
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 :
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 :
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() :
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éé :
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 :
void
OnTextureValuesUpdated
(
) {
textureData.
ApplyToMaterial (
terrainMaterial);
}
Cette fonction est connectée à l'événement, dans la fonction OnValidate() :
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.