PARTIE IV: Croissance

Au cours des précédentes parties, nous avons pu voir qu’il était possible, a partir de quelques règles géométriques simples, d’obtenir des structures assez complexes. Nous avons pu mettre en oeuvre un modèle de plante avec le moteur Unity.

Le problème auquel nous allons nous intéresser maintenant, c’est celui de simuler une croissance progressive de ces structures: en effet, jusqu’ici, nos plantes “poussent” de façon quasi instantanée. Nous allons remédier à cela, en enrichissant notre modèle, par l’introduction de la notion de temps: les plantes et les organes vont avoir leur propre horloge interne. Cette horloge va être utilisée pour donner un rythme à la croissance.

Croissance progressive

Pour intégrer la notion de temps dans le modèle de l’organe, nos allons ajouter un compteur qui augmente de façon continue avec le temps (simulé ou réel), son age en quelque sorte.

La taille de chaque organe est initialement fixée à 0,  et va augmenter progressivement, avec l’age de l’organe. La fonction pour calculer la taille va dépendre des paramètres suivants:

  • une vitesse d’élongation: c’est le coefficient appliqué à l’age de l’organe pour calculer sa taille
  • Une taille maximale, comprise entre 0 et 1. Ce paramètre permet essentiellement de diminuer la taille des organes le long des tiges.
  • le temps avant la création d’un nouvel élément: Un organe ne peut pas produire d’élément avant un certain temps. Permet de temporiser la production de nouvel organe.
  • le temps avant de démarrer la croissance: permet de légèrement désynchroniser les différents organes, pour qu’ils n’apparaissent pas tous en même temps.

Dans la classe, nous ajoutons donc les membres suivants:

public class MorphoScript : MorphoModule
{
    public float lifeCounter = 0.0f;
    public float maxInitialDelay = 0.0f;
	public float delayBeforeChild = 1.0f;
	public float growthSpeed = 2.0f;
    public float maxsize = 1.0f;
    // ...
}

Dans la méthode Start(), la variable lifeCounter est initialisée de la sorte:

void Start ()
{
    lifeCounter = Random.Range (-maxInitialDelay, 0.0f); 
     //...
}

Il faudra aussi modifier la méthode addOrgan pour que l’organe ait une taille initiale nulle, et copier ces nouveaux paramètres (hormis le lifeCounter) dans chaque nouvel organe créé.

GameObject addOrgan (string orgpref, Vector3 rot)
{
	GameObject g = AddChild (orgpref, "MorphoScript");
	if (g) {
			g.transform.localScale = Vector3.zero;
			g.transform.localEulerAngles = rot;
					
			if (level > 0)			
				g.transform.localPosition = (1.9f) * Vector3.up;
				MorphoScript cp = g.GetComponent ("MorphoScript") as MorphoScript;
			if (cp) {
				cp.level = level + 1;
				cp.maxLevel = maxLevel;
				cp.phyllo = phyllo;
				cp.inclin = inclin;
				cp.childPrefabName = childPrefabName;
				cp.fruitPrefabName = fruitPrefabName;
							
				cp.maxsize = maxsize * 0.98f;
				cp.growthSpeed = growthSpeed;
				cp.delayBeforeChild = delayBeforeChild;
				cp.maxInitialDelay = maxInitialDelay;
			}
	}
	return g;
}

La fonction Update, enfin, contient le code qui permet de fixer la taille de l’organe en fonction du temps:

void Update ()
{
    lifeCounter += Time.deltaTime;
    float scoef = lifeCounter * growthSpeed;
    if (scoef > 1.0f) 
        scoef = 1.0f;
    if (scoef < 0.1f) 
        scoef = 0.1f;
    gameObject.transform.localScale = Vector3.one * scoef * maxsize;
        
    if ((lifeCounter > delayBeforeChild) && (level < maxLevel) && (gameObject.transform.childCount == 0)) {
        if (level < 2) {
            addOrgan (childPrefabName, Vector3.zero);
        } else { //..
}
        
    

A la ligne 11, le test original est modifié pour tenir compte du délai avant création de nouveaux organes.  La suite du code reste inchangée.

En l’état, le code fonctionne, mais pour explorer différentes configurations, pourquoi ne pas ajouter ces quelques lignes dans la fonction de mutation:

public void mutation ()
	{
		delayBeforeChild = Random.Range (0.1f, 1f);
		growthSpeed = Random.Range (0.1f, 3f);
		maxInitialDelay = Random.Range (0.0f, 1f);
        //...
	}

Voila! normalement, les plantes peuvent désormais de façon progressive. Selon les paramètres, la croissance pourra être plus ou moins rapide, plus ou moins synchrone, etc. A vous d’ajouter d’autres lois pour changer la taille des organes (par exemple, quelque chose de plus cartoonesque, avec un effet de rebond, etc.).

Le code a ce stade est disponible ici.  Cette partie est en effet décomposée en 3 étapes principales, il reste donc encore deux autres archives.

Explosion du nombre d’éléments

Si vous avez procédé à des modifications dans les règles de croissance, vous êtes peut être arrivés a faire ralentir votre machine. C’est probablement dû a un trop grand nombre d’organes, même si il n’y en a pas forcément beaucoup de visibles à l’écran. Pour remédier a cela, nous allons fixer un nombre maximum d’organe par plante.

Tout se fait au moment d’ajouter un organe dans la plante. C’est a  ce moment que nous allons:

  • compter le nombre d’organes
  • vérifier que le nombre max n’est pas atteint. Si il est atteint, aucun organe n’est créé.

Mais cela implique que chaque organe ait accès au même compteur, global à la plante. Ajoutons un membre ‘root’ a notre script. Ce dernier pointera vers le script du premier GameObject dans la hiérarchie. Avec l’ajout des 2 autres membres, numOrgans et maxNumOrgans, respectivement pour stocker le nombre actuel d’organes dans la plante, et le nombre maximum d’organes dans la plante, la définition de la classe commence ainsi:

public class MorphoScript : MorphoModule
{
    // Reference vers le module 'racine'
    MorphoScript root;
    // Compteur global
	public int numOrgans = 0;
	// Desactivé si -1, sinon nombre max de blocks
    public int numOrgans = 200; 
    // ...
    
    MorphoScript ()
	{				
		root = this;
        //...
	}
}

Au début de la méthode addOrgan(), il faut ajouter un test pour vérifier que le nombre maximum d’organe n’est pas atteint. Le cas échéant, la méthode renverra un pointeur nul.

GameObject addOrgan (string orgprefab, Vector3 rot)
{
    if ((root.maxNumOrgans > 0) && (root.numOrgans >= root.maxNumOrgans))
        return null;
        
    GameObject g = AddChild (orgpref, "MorphoScript");
    	if (g) {
            root.numOrgans++;
			g.transform.localScale = Vector3.zero;
			g.transform.localEulerAngles = rot;			
            // ..

A la destruction, d’un organe, il faut décrémenter le compteur, du nombre d’éléments.  Pour compter correctement le nombre de sous organes.  Ajoutons une méthode detachR(), qui va récursivement détruire tous les éléments d’un organe, en prenant soin de décrémenter le compteur global. Cette fonction sera enrichie par la suite.

public void detachR ()
{
	root.numOrgans--;
    GameObject.Destroy (gameObject);
    
	if (gameObject.transform.parent != null) 
    {
		gameObject.transform.parent = null;
    }

    while (gameObject.transform.childCount>0) 
    {
			GameObject g = gameObject.transform.GetChild (0).gameObject;
			MorphoScript cp = g.GetComponent ("MorphoScript") as MorphoScript;			
			cp.detachR ();	
	}    
}

Actuellement, le seul endroit ou l’on retire un organe, c’est lorsque l’on clique dessus. La méthode OnMouseOver() est changée de la sorte:

void OnMouseOver ()
{    		
    if (Input.GetMouseButton (0) || Input.GetMouseButton (1)) {	//if (gameObject.transform.childCount == 0)
        if (gameObject.transform.parent != null) {
            GameObject g = gameObject.transform.parent.gameObject;
            MorphoScript cp = g.GetComponent ("MorphoScript") as MorphoScript;
            cp.mutation ();        
            detachR ();
        }
    }
}

Re-testez en augmentant la profondeur autorisée à 10, voire 20, vous verrez que la plante ne se développe pas complètement. Mais si vous élaguez la plante d’un coté, elle pourra reprendre sa croissance. Le code complet après l’ajout de cette limitation peut être trouvé dans cette archive.

Une dernière remarque: notre structure de donnée n’est pas optimale, dans le sens ou certains paramètres globaux à la plante n’ont pas besoin d’être déclarés dans chaque organe. Pour les besoins du tutoriel, nous laisserons le code en l’état.

Durée de vie des organes

Pour avoir la joie d’observer les nombreuses variations que l’on peut obtenir, sans avoir à cliquer sur la plante, donnons enfin une durée de vie aux organes, ce qui permettra de les faire disparaître automatiquement après un certain temps, pour les remplacer par d’autres.

Commençons par ajouter quelques variables pour fixer une durée de vie.

public class MorphoScript : MorphoModule
{
	public float maxlife = -1f;
	public float absmaxlife = 16f; 
	public bool growthEnabled = true;
    // ...
}

Le détachement ne va plus détruire les éléments, mais arrêter la croissance en plaçant le flag enableGrowth à false, et en fixant la fin de vie 400 ms après, le temps de faire une rapide animation pour faire disparaître les organes.

public void detachR ()
{
	root.numOrgans--;
	growthEnabled = false;
	if ((maxlife < 0) || (maxlife > lifeCounter))
		maxlife = lifeCounter + 0.4f;
    // ...
}

Enfin, -morceau de choix- la méthode update, ici reproduite en intégralité.

void Update ()
{
	lifeCounter += Time.deltaTime;
	// Destruction automatique des organes
	if (lifeCounter > absmaxlife) {
    	if (gameObject.transform.parent != null) {
			growthEnabled = false;
			if (gameObject.transform.childCount == 0) {
				detachR ();
			}
		}
	}
			
	if ((maxlife > 0) && (lifeCounter >= maxlife))
	    GameObject.Destroy (gameObject);
	else {
		float size; 
		if ((maxlife > 0) && (lifeCounter > maxlife - 0.4)) {
		    size = maxsize * (maxlife - lifeCounter) / 0.4f;
			if (gameObject.transform.localScale.x > size)			
				gameObject.transform.localScale = Vector3.one * size;						
			} else {
				if (growthEnabled) {				
					float scoef = lifeCounter * growthSpeed;
					if (scoef > 1.0f) 
						scoef = 1.0f;
					if (scoef < 0.1f) 
		    			scoef = 0.1f;					
					gameObject.transform.localScale = Vector3.one * scoef * maxsize;	
				}
			}
		}
	
	if (growthEnabled) {					
		if ((maxlife < 0) && (lifeCounter > delayBeforeChild) && (level < maxLevel) && (gameObject.transform.childCount == 0)) {
			if (level < 2) {
				addOrgan (childPrefabName, Vector3.zero);
				} else {			
				if (level == maxLevel - 1)
    				addOrgan (fruitPrefabName, new Vector3 (0.0f, phyllo, 0.0f));
				else {
					if ((inclin > 30) && (level < 3))
						addOrgan (childPrefabName, new Vector3 (0.0f, phyllo, 0.0f));
					if (Random.Range (0, 100) < 99)
	    				addOrgan (childPrefabName, new Vector3 (inclin, phyllo, 0.0f));
					if (Random.Range (0, 100) < 99)
						addOrgan (childPrefabName, new Vector3 (inclin, phyllo + 180f, 0.0f));
				}
		    }
	    }		
    }
}

Le code final pour cette partie peut être téléchargé ici.

Placement de la caméra

Dernier point abordé dans cette partie, le placement et le contrôle de caméra. Il suffit de créer le script suivant et l’assigner a la caméra de la scène (mais tout autre objet peut convenir). Il place la caméra automatiquement, et les flèches du clavier permettent de tourner autour de la plante.

public class kbinterface : MonoBehaviour
{
	float lookheight = 9.0f;
	void Start ()
	{
		transform.LookAt (Vector3.up * lookheight);
    }

void FixedUpdate ()
	{
		// Déplacement de la caméra
		float xAxisValue = Input.GetAxis ("Horizontal");
		float zAxisValue = Input.GetAxis ("Vertical");				
		if (Camera.current != null) {
			if ((Mathf.Abs (xAxisValue) > 0.0f) || (Mathf.Abs (zAxisValue) > 0.0f)) {
				if (Input.GetKey (KeyCode.RightShift)) {
					lookheight += zAxisValue * 0.1f;				
			    } else {
				    if (Input.GetKey (KeyCode.RightControl)) {
						Camera.current.transform.Translate (new Vector3 (0.0f, 0.0f, zAxisValue));
					} else {
						Camera.current.transform.Translate (new Vector3 (0.0f, zAxisValue, 0.0f));		
					}
				}
				Camera.current.transform.Translate (new Vector3 (xAxisValue * 2.0f, 0.0f, 0.0f));
				Camera.current.transform.LookAt (Vector3.up * lookheight);
			}
		}
	} 
} 

Pour la dernière partie, nous allons améliorer la façon dont les organes sont éliminés de la scène. Avant de les faire disparaître, nous allons utiliser le moteur physique pour les faire chuter, ou les faire partir en éclats… Nous en profiterons aussi pour animer les plantes avec un peu de mouvement, pour simuler du vent.

software design & creative process