[CF-AL] Placer des préfabs aléatoirement sur un terrain.

Pour les scripts écrits en Javascript (UnityScript). :!: Obsolète :!:
Règles du forum
Merci de respecter la NOMENCLATURE suivante pour vos TITRES de messages :

Commencez par le niveau de vos scripts
DB = Débutant
MY = Moyen
CF = Confirmé

Puis le domaine d'application
-RS = Réseau
-AL = Algorithmie

Exemple :

[DB-RS] Mouvement perso multijoueur
Avatar de l’utilisateur
Alesk
Messages : 2303
Inscription : 13 Mars 2012 09:09
Localisation : Bordeaux - France
Contact :

Re: [CF-AL] Placer des préfabs aléatoirement sur un terrain.

Message par Alesk » 03 Sep 2016 11:07

Bon, c'est pas bien de trop te mâcher le boulot, mais ça m'a motivé ce truc ... donc j'ai pondu une classe qui fait le job, avec la possibilité d'avoir des cubes de tailles différentes, de définir une marge par rapport au bordures et même de régler le % de couverture total.

Tu assignes ce script à un terrain, tu mets le cube par défaut d'unity dans la variable Mesh (ça ne créé pas des gameobject, mais affiche les cubes en wireframe avec des gizmos), et tu n'auras plus qu'à jouer avec les réglages.

Pour relancer le calcul après un changement de réglages, il faut désactiver puis réactiver le script dans l'inspecteur.

C'est pas ultra optimal au niveau des performances (même si j'ai bidouillé un poil pour que ça reste raisonnable), une autre approche éventuellement plus rapide serait de lancer aléatoirement des rayons sur toute la surface du terrain, et dès qu'un spot correct est détecté, y placer un cube, puis à partir de cette position calculer la position des cubes voisins, de proche en proche tant que le sol est plat.

J'ai commenté l'essentiel ... mais si tu as des questions, j'y répondrai avec plaisir ;)

Code : Tout sélectionner

using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;

[ExecuteInEditMode]
public class RandomCubeSpawner : MonoBehaviour {

	struct CubeData{
		public Vector3 position;
		public float size;
		public float angle;
		public float diagonale;

		public CubeData(Vector3 position, float size, float angle, float diagonale){
			this.position = position;
			this.size = size;
			this.angle = angle;
			this.diagonale = diagonale;
		}
	}

	private List<CubeData> cubesData = new List<CubeData>();

	[Range(0f,1f)]
	public float coverage = 1f;

	public Mesh cubeMesh;

	public bool multiPass = false;

	[Range(1f,20f)]
	public float margin = 1f;

	[Range(1f,200f)]
	public float minHeight = 1f;

	[Range(1f,20f)]
	public float minSize = 1f;

	[Range(1f,20f)]
	public float maxSize = 5f;


	void OnEnable(){
		
		PlaceCubes ();

	}

	void PlaceCubes(){
		
		double now = Time.realtimeSinceStartup;

		Terrain terrain = this.GetComponent<Terrain> ();
		Vector3 mapSize = terrain.terrainData.size;

		// purge
		cubesData.Clear ();

		// initialisation de la reandom seed pour toujours obtenir le même résultat
		Random.seed = 123;

		// nombre de cubes créés
		int total = 0;

		int processed = 0;

		RaycastHit hit,hitCorner;

		if (minSize > maxSize) {
			float m = maxSize;
			maxSize = minSize;
			minSize = m;
		}

		// le placement des cubes est fait en plusieurs passes pour permettre de placer d'abord les gros, puis les petits dans les espaces restants
		int stepCount = Mathf.CeilToInt((maxSize - minSize) / 5);

		// dans le cas d'une création en plusieurs passes, on commence par créer les plus gros cubes
		// puis on réduit la valeur max à chaque passe consécutive
		float endSize = maxSize;
		float sizeStep = (maxSize - minSize) / (stepCount + 1);

		if (sizeStep < minSize || !multiPass) {
			stepCount = 1;
		}


		for (int s = 0; s < stepCount; s++) {
			
			Vector3 randomPos = Vector3.zero;
			Quaternion quat = Quaternion.identity;
			Vector3 pos = Vector3.zero;

			float size = 1f;
			float diagonale = 1f;

			float moveStep = minSize * 0.5f;

			Ray r = new Ray (randomPos, Vector3.down);
			Ray test = new Ray (randomPos, Vector3.down);
			bool createCube = true;
			float randomAngle;

			while (pos.x < mapSize.x) {

				pos.z = 0f;

				while (pos.z < mapSize.z) {
					processed++;

					size = Random.Range (minSize, endSize);
					diagonale = Mathf.Sqrt (size * size * 2);

					randomPos = pos + Random.insideUnitSphere * size;
					randomPos.y = 500;

					r.origin = randomPos;

					createCube = Physics.Raycast (r, out hit);
					if (createCube) {
						// si le terrain est plat à cet endroit
						if (hit.normal == Vector3.up && hit.point.y >= minHeight) {

							// recherche d'un cube proche dans les positions précédentes
							// on commence par la fin de la liste car il y a plus de chances de trouver un cube proche dans les derniers ajoutés
							for (int j = cubesData.Count - 1; j >= 0; j--) {
								// le test est fait sur la distance entre les deux centres des cubes, et selon leurs diagonales (ils sont donc considérés comme des sphères)
								if (Vector3.Distance (cubesData [j].position, hit.point) <= (diagonale + cubesData[j].diagonale) * 0.5f) {
									createCube = false;
									break;
								}
							}

							// si aucun autre cube ne gène
							if (createCube) {

								// angle de rotation aléatoire du cube
								randomAngle = Random.Range (0f, 360);

								float offset = (size + margin * 2) * 0.5f;

								// ray cast sur les 4 coins et bords pour détecter s'ils sont dans le vide
								for (var k = 0; k < 360; k += 90) {
									quat.eulerAngles = new Vector3 (0, randomAngle + k, 0);

									// positionnement du rayon sur un coin du cube
									r.origin = randomPos + quat * ((Vector3.forward + Vector3.right) * offset );

									// test de l'impact sur le sol
									createCube = Physics.Raycast (r, out hitCorner);
									if (createCube) {
										// vérification que le sol sous ce point est au même niveau que le sol sous le centre du cube
										createCube = (hitCorner.point.y == hit.point.y && hitCorner.normal == Vector3.up);
									}

									// même test sous le centre du bord du cube
									if (createCube) {
										r.origin = randomPos + quat * (Vector3.forward * offset);
										createCube = Physics.Raycast (r, out hitCorner);

										if (createCube) {
											// vérification que le sol sous le centre de ce bord est au même niveau que le sol sous le centre du cube
											createCube = (hitCorner.point.y == hit.point.y && hitCorner.normal == Vector3.up);
										}
									}

									// un coin ou un bord n'est pas valide, on sort de la boucle
									if (!createCube)
										break;
								}


								if (createCube) {
									// tout est ok, on peut ajouter un cube à cette position
									cubesData.Add (new CubeData (hit.point,size,randomAngle,diagonale));
									total++;
								}

							}
						}
					}


					if (createCube) {
						// on avance de la moitié de la taille du dernier cube créé
						pos.z += size * 0.5f;
					} else {
						// on avance de la moitié de la taille maximale possible
						pos.z += endSize * 0.5f;
					}

				}

				pos.x += moveStep;

			}

			// réduction de la taille max de création de cube pour la passe suivante
			endSize -= sizeStep;

		}


		// mélange aléatoire de la liste
		int lastIndex = cubesData.Count - 1;
		for(int i=0;i<=lastIndex;i++){
			CubeData temp = cubesData [i];
			int index = Random.Range (0, lastIndex);
			cubesData [i] = cubesData [index];
			cubesData [index] = temp;
		}

		now = Time.realtimeSinceStartup - now;
		Debug.Log (total+" cubes / "+(now).ToString("f2")+" s / "+processed+" operations");

	}

	void OnDrawGizmos(){
		if (!cubeMesh)
			return;

		Quaternion quat = Quaternion.identity;

		for (int i = 0, l = Mathf.RoundToInt(cubesData.Count*coverage); i < l; i++) {
			Gizmos.color = Color.red;
			quat.eulerAngles = new Vector3 (0, cubesData[i].angle, 0);
			Gizmos.DrawWireMesh (cubeMesh, cubesData [i].position + Vector3.up*cubesData [i].size*0.5f, quat,Vector3.one*cubesData [i].size);
		}

	}

}


Manga
Messages : 6
Inscription : 01 Sep 2016 15:11

Re: [CF-AL] Placer des préfabs aléatoirement sur un terrain.

Message par Manga » 05 Sep 2016 13:11

Désolé, j'étais déconnecté. Je viens de lire ton message.
Waouuuu ! Merci, oui cela fonctionne très bien comme ça. Cela a du te prendre un peu de temps. C'est vrai que, si la question est simple, la réponse n'est pas forcément triviale...

Ta solution fonctionne très bien. Entretemps, j'avais élaboré ma propre solution en jouant sur le BoxCollider : je teste les collisions entre cubes + je récupère les quatre points inférieurs du BoxCollider pour vérifier que, pour chacun, la hauteur du terrain correspondante est bien de 10m.

Ma solution est plus courte, mais la tienne est plus complète ! Merci encore.

Code : Tout sélectionner

using UnityEngine;
using System.Collections;

public class Programme : MonoBehaviour {

	private GameObject[] cubes = new GameObject [50];

	void Start () {
		for (int i = 0; i < 50; i++) {
			// ajout du cube
			cubes[i] = GameObject.CreatePrimitive(PrimitiveType.Cube);
			cubes[i].transform.localScale = new Vector3(5F, 5F, 5F); // échelle de 5
			bool flag = true;
			while (flag) {
				// calcul de la position 
				float posX = Random.value * 500f;
				float posZ = Random.value * 500f;
				cubes[i].transform.position = new Vector3(posX, 12.5f, posZ); // 10 + 2.5 : pour placer le cube au dessus du terrain
				// calcul de la position des huit points du BoxCollider
				// (on s'intéresse aux quatre premiers points)
				Vector3[] pointsCollider = GetVertices(cubes[i]); 
				// calcul de la hauteur du terrain pour les quatre points inférieurs du BoxCollider 
				float hauteurTerrain0 = Terrain.activeTerrain.SampleHeight(pointsCollider[0]);
				float hauteurTerrain1 = Terrain.activeTerrain.SampleHeight(pointsCollider[1]);
				float hauteurTerrain2 = Terrain.activeTerrain.SampleHeight(pointsCollider[2]);
				float hauteurTerrain3 = Terrain.activeTerrain.SampleHeight(pointsCollider[3]);
				// test sur la hauteur du terrain 
				if (hauteurTerrain0 >= 9.99 && hauteurTerrain1 >= 9.99 && hauteurTerrain2 >= 9.99 && hauteurTerrain3 >= 9.99) {
					flag = false;
				}
				// test de la collision de deux cubes
				for (int j = 0; j < i; j++) {
					if(cubes[i].GetComponent<BoxCollider> ().bounds.Intersects(cubes[j].GetComponent<BoxCollider> ().bounds)) {
						flag = true;
					}
				}
			}
		}
	}

	// fonction pour obtenir les huit coins d'un box collider
	Vector3[] GetVertices (GameObject obj)  {
		BoxCollider b  = obj.GetComponent<BoxCollider> ();
		Vector3[] vertices  = new Vector3[8];
		vertices[0] = obj.transform.TransformPoint(b.center + new Vector3(-b.size.x, -b.size.y, -b.size.z) * 0.5f);
		vertices[1] = obj.transform.TransformPoint(b.center + new Vector3(b.size.x, -b.size.y, -b.size.z) * 0.5f);
		vertices[2] = obj.transform.TransformPoint(b.center + new Vector3(b.size.x, -b.size.y, b.size.z) * 0.5f);
		vertices[3] = obj.transform.TransformPoint(b.center + new Vector3(-b.size.x, -b.size.y, b.size.z) * 0.5f);
		vertices[4] = obj.transform.TransformPoint(b.center + new Vector3(-b.size.x, b.size.y, -b.size.z) * 0.5f);
		vertices[5] = obj.transform.TransformPoint(b.center + new Vector3(b.size.x, b.size.y, -b.size.z) * 0.5f);
		vertices[6] = obj.transform.TransformPoint(b.center + new Vector3(b.size.x, b.size.y, b.size.z) * 0.5f);
		vertices[7] = obj.transform.TransformPoint(b.center + new Vector3(-b.size.x, b.size.y, b.size.z) * 0.5f);
		return vertices;
	}
}

Avatar de l’utilisateur
Alesk
Messages : 2303
Inscription : 13 Mars 2012 09:09
Localisation : Bordeaux - France
Contact :

Re: [CF-AL] Placer des préfabs aléatoirement sur un terrain.

Message par Alesk » 05 Sep 2016 16:38

Le principal c'est que ça fonctionne ;)

Avatar de l’utilisateur
Iwa
Messages : 1131
Inscription : 25 Avr 2012 16:20
Contact :

Re: [CF-AL] Placer des préfabs aléatoirement sur un terrain.

Message par Iwa » 27 Sep 2016 10:54

Hello,

Merci de penser à mettre le sujet en [RESOLU] si tout est bon. Pour cela il te suffit d'éditer ton premier post.
"N'est stupide que la stupidité Monsieur..." - Forest Gump
... sauf si tu lis pas ça :)

Si tu as tout ce qu'il te faut, merci de penser à basculer ton sujet en [RESOLU] en éditant ton tout premier post ;)

Verrouillé

Revenir vers « (Js) Javascript »