Que recherchez-vous ?
Engine & platform

Aller en profondeur avec IMGUI et la personnalisation de l'éditeur

RICHARD FINE / UNITY TECHNOLOGIESContributor
Dec 22, 2015|32 Min
Aller en profondeur avec IMGUI et la personnalisation de l'éditeur
Cette page a été traduite automatiquement pour faciliter votre expérience. Nous ne pouvons pas garantir l'exactitude ou la fiabilité du contenu traduit. Si vous avez des doutes quant à la qualité de cette traduction, reportez-vous à la version anglaise de la page web.
Le nouveau système Unity UI est sorti il y a maintenant plus d'un an. J'ai donc pensé faire un billet sur l'ancien système d'interface utilisateur, IMGUI.

Un drôle de timing, me direz-vous. Pourquoi s'intéresser à l'ancien système d'interface utilisateur maintenant que le nouveau est disponible ? Bien que le nouveau système d'interface utilisateur soit destiné à couvrir toutes les situations d'interface utilisateur dans le jeu, IMGUI est toujours utilisé, en particulier dans une situation très importante : l'éditeur Unity lui-même. Si vous souhaitez étendre l'éditeur Unity avec des outils et des fonctionnalités personnalisés, il est très probable que l'une des choses que vous devrez faire sera de vous mesurer à IMGUI.

Procéder immédiatement

Première question donc : Pourquoi s'appelle-t-il "IMGUI" ? IMGUI est l'abréviation de Immediate Mode GUI (interface graphique en mode immédiat). OK, alors, qu'est-ce que c'est ? Il existe deux approches principales des systèmes GUI : l'approche "immédiate" et l'approche "retenue".

Une GUI en mode retenu est une GUI dans laquelle le système GUI "retient" des informations sur votre GUI : vous configurez vos différents widgets GUI - étiquettes, boutons, curseurs, champs de texte, etc. - et ces informations sont conservées et utilisées par le système pour rendre l'écran, répondre aux événements, etc. Lorsque vous souhaitez modifier le texte d'une étiquette ou déplacer un bouton, vous manipulez des informations qui sont stockées quelque part, et lorsque vous avez effectué votre modification, le système continue à fonctionner dans son nouvel état. Lorsque l'utilisateur modifie les valeurs et déplace les curseurs, le système enregistre simplement les changements, et c'est à vous d'interroger les valeurs ou de répondre aux rappels. Le nouveau système Unity UI est un exemple de GUI en mode retenu ; vous créez vos UI.Labels, UI.Buttons et ainsi de suite en tant que composants, vous les mettez en place, puis vous les laissez là, et le nouveau système Unity UI s'occupe du reste.

Par ailleurs, une GUI en mode immédiat est une GUI dans laquelle le système GUI ne conserve généralement pas d'informations sur votre GUI, mais vous demande à plusieurs reprises de spécifier à nouveau quels sont vos contrôles, où ils se trouvent, etc. Comme vous spécifiez chaque partie de l'interface utilisateur sous la forme d'appels de fonction, elle est traitée immédiatement - dessinée, cliquée, etc. - et les conséquences de toute interaction avec l'utilisateur vous sont renvoyées immédiatement, sans que vous ayez à les rechercher. C'est inefficace pour une interface utilisateur de jeu - et peu pratique pour les artistes, car tout devient très dépendant du code - mais cela s'avère très pratique pour les situations non temps réel (comme les panneaux de l'éditeur) qui sont fortement dépendantes du code (comme les panneaux de l'éditeur) et qui veulent changer les contrôles affichés facilement en réponse à l'état actuel (comme les panneaux de l'éditeur !), c'est donc un bon choix pour des choses comme l'équipement de construction lourd. Non, attendez. Je voulais dire que c'est un bon choix pour les panneaux de l'éditeur.

Si vous souhaitez en savoir plus, Casey Muratori a réalisé une excellente vidéo dans laquelle il présente certains des avantages et des principes d'une GUI en mode immédiat. Vous pouvez aussi continuer à lire !

Chaque événement -ualité

Chaque fois que le code du GUI s'exécute, un "événement" est géré. Il peut s'agir de quelque chose comme "l'utilisateur a cliqué sur le bouton de la souris" ou de quelque chose comme "l'interface graphique doit être repeinte". Vous pouvez savoir quel est l'événement en cours en vérifiant Event.current.type.

Imaginez ce à quoi cela pourrait ressembler si vous faisiez un ensemble de boutons dans une fenêtre quelque part et que vous deviez écrire un code séparé pour répondre à "l'utilisateur a cliqué sur le bouton de la souris" et "la GUI a besoin d'être repeinte". Au niveau d'un bloc, cela peut ressembler à ceci :

Diagramme GUI 1

Il est assez fastidieux d'écrire ces fonctions pour chaque événement GUI, mais vous remarquerez qu'il y a une certaine similitude structurelle entre les fonctions. À chaque étape, nous faisons quelque chose en rapport avec la même commande (bouton 1, bouton 2 ou bouton 3). Ce que nous faisons exactement dépend de l'événement, mais la structure est la même. Cela signifie que nous pouvons faire ceci à la place :

Diagramme GUI 2

Nous avons une seule fonction OnGUI qui appelle des fonctions de bibliothèque comme GUI.Button, et ces fonctions de bibliothèque font des choses différentes en fonction de l'événement que nous traitons. C'est simple !

Il existe 5 types d'événements qui sont utilisés la plupart du temps :

EventType.MouseDownSet lorsque l'utilisateur vient d'appuyer sur un bouton de la souris.EventType.MouseUpSet lorsque l'utilisateur vient de relâcher un bouton de la souris.EventType.KeyDownSet lorsque l'utilisateur vient d'appuyer sur une touche.EventType.KeyUpSet lorsque l'utilisateur vient de relâcher une touche.EventType.RepaintSet lorsque l'IMGUI doit redessiner l'écran.

Cette liste n'est pas exhaustive. Consultez la documentation EventType pour en savoir plus.

Comment un contrôle standard, tel que GUI.Button, pourrait-il répondre à certains de ces événements ?

EventType.RepaintDessine le bouton dans le rectangle fourni.EventType.MouseDownVérifie si la souris se trouve dans le rectangle du bouton. Si c'est le cas, marquez le bouton comme étant enfoncé et déclenchez un repeint pour qu'il soit redessiné comme s'il était enfoncé.EventType.MouseUpUnflag le bouton comme étant enfoncé et déclenchez un repeint, puis vérifiez si la souris se trouve toujours dans le rectangle du bouton : si c'est le cas, renvoyez true, pour que l'appelant puisse répondre au clic sur le bouton.

La réalité est plus compliquée que cela - un bouton répond également aux événements du clavier, et il y a du code pour s'assurer que seul le bouton sur lequel vous avez initialement cliqué peut répondre à MouseUp - mais cela vous donne une idée générale. Tant que vous appelez GUI.Button au même endroit dans votre code pour chacun de ces événements, avec la même position et le même contenu, les différents comportements fonctionneront ensemble pour fournir toutes les fonctionnalités d'un bouton.

Pour vous aider à relier ces différents comportements dans le cadre de différents événements, IMGUI dispose d'un concept d'"ID de contrôle". L'idée d'un identifiant de contrôle est de donner un moyen cohérent de se référer à un contrôle donné dans tous les types d'événements. Chaque partie distincte de l'interface utilisateur ayant un comportement interactif non trivial demandera un identifiant de contrôle ; il est utilisé pour garder une trace de choses telles que le contrôle qui a actuellement le focus clavier, ou pour stocker une petite quantité d'informations associées à un contrôle. Les ID de contrôle sont simplement attribués aux contrôles dans l'ordre dans lequel ils les demandent, donc, encore une fois, tant que vous appelez les mêmes fonctions GUI dans le même ordre sous différents événements, ils finiront par se voir attribuer les mêmes ID de contrôle et les différents événements se synchroniseront.

L'énigme du contrôle personnalisé

Si vous souhaitez créer vos propres classes d'éditeur personnalisées, vos propres classes EditorWindow ou vos propres classes PropertyDrawer, la classe GUI - ainsi que la classe EditorGUI - fournit une bibliothèque de contrôles standard utiles que vous verrez utilisés dans tout Unity.

(Les codeurs débutants de l'éditeur commettent souvent l'erreur de négliger la classe GUI - mais les contrôles de cette classe peuvent être utilisés lors de l'extension de l'éditeur tout aussi librement que les contrôles de EditorGUI. Il n'y a rien de particulier entre GUI et EditorGUI - il s'agit simplement de deux bibliothèques de contrôles que vous pouvez utiliser - mais la différence est que les contrôles de EditorGUI ne peuvent pas être utilisés dans les jeux, car leur code fait partie de l'éditeur, alors que GUI fait partie du moteur lui-même).

Mais que se passe-t-il si vous voulez faire quelque chose qui va au-delà de ce qui est disponible dans la bibliothèque standard ?

Voyons comment nous pouvons créer un contrôle d'interface utilisateur personnalisé. Essayez de cliquer et de faire glisser les cases colorées dans cette petite démo :

(NOTE : L'application WebGL originale intégrée ici ne fonctionne plus dans les navigateurs)

(Vous aurez besoin d'un navigateur supportant le WebGL pour voir la démo, comme les versions actuelles de Firefox).

Ces curseurs personnalisés pilotent chacun une valeur "flottante" distincte comprise entre 0 et 1. Vous pourriez vouloir utiliser un tel élément dans l'inspecteur comme autre moyen d'afficher, par exemple, l'intégrité de la coque pour différentes parties d'un objet spatial, où 1 représente "aucun dommage" et 0 représente "totalement détruit" - le fait que les barres représentent les valeurs sous forme de couleurs peut faciliter l'identification, d'un seul coup d'œil, de l'état du vaisseau. Le code permettant de créer un contrôle IMGUI personnalisé que vous pouvez utiliser comme n'importe quel autre contrôle est assez simple, nous allons donc le parcourir.

La première étape consiste à choisir la signature de notre fonction. Afin de couvrir tous les types d'événements, notre contrôle aura besoin de trois éléments :

- un Rect qui définit l'endroit où il doit se dessiner et l'endroit où il doit répondre aux clics de la souris.

- la valeur flottante actuelle que la barre représente.

- un GUIStyle, qui contient toutes les informations nécessaires sur l'espacement, les polices, les textures, etc. dont le contrôle aura besoin. Dans notre cas, il s'agit de la texture que nous utiliserons pour dessiner la barre. Nous reviendrons plus tard sur ce paramètre.

Il devra également renvoyer la valeur que l'utilisateur a définie en faisant glisser la barre. Cela n'a de sens que pour certains événements comme les événements de souris, et non pour des événements comme les événements de repeinture ; par défaut, nous renverrons donc la valeur que le code appelant a transmise. L'idée est que le code appelant peut simplement faire "value = MyCustomSlider(... value ...)" sans se soucier de l'événement qui se produit, donc si nous ne renvoyons pas une nouvelle valeur définie par l'utilisateur, nous devons préserver la valeur actuelle.

La signature obtenue ressemble donc à ceci :

public static float MyCustomSlider(Rect controlRect, float value, GUIStyle style)

Nous allons maintenant commencer à mettre en œuvre la fonction. La première étape consiste à récupérer un identifiant de contrôle. Nous l'utiliserons pour certaines choses lorsque nous répondrons aux événements de la souris. Cependant, même si l'événement traité n'est pas celui qui nous intéresse, nous devons tout de même demander un identifiant, afin de nous assurer qu'il n'est pas attribué à un autre contrôle pour cet événement particulier. Rappelez-vous que l'IMGUI distribue les identifiants dans l'ordre où ils sont demandés, donc si vous ne demandez pas d'identifiant, il sera donné au contrôle suivant, ce qui fera que ce contrôle aura des identifiants différents pour des événements différents, ce qui risque de le casser. Ainsi, lorsque vous demandez des identifiants, c'est tout ou rien - soit vous demandez un identifiant pour chaque type d'événement, soit vous ne le demandez jamais pour aucun d'entre eux (ce qui peut être acceptable si vous créez un contrôle extrêmement simple ou non interactif).

{
	int controlID = GUIUtility.GetControlID (FocusType.Passive);

Le paramètre FocusType.Passive indique à l'IMGUI le rôle de ce contrôle dans la navigation au clavier, c'est-à-dire s'il est possible que le contrôle soit celui qui réagit à la pression des touches. Mon curseur personnalisé ne réagit pas du tout à la pression des touches, il spécifie donc Passif, mais les contrôles qui réagissent à la pression des touches peuvent spécifier Natif ou Clavier. Consultez la documentation FocusType pour plus d'informations à ce sujet.

Ensuite, nous faisons ce que la majorité des contrôles personnalisés feront à un moment ou à un autre de leur mise en œuvre : nous nous branchons en fonction du type d'événement, à l'aide d'une instruction de commutation. Au lieu d'utiliser directement Event .current.type, nous utiliserons Event.current.GetTypeForControl(), en lui transmettant l'ID de notre contrôle ; cela permet de filtrer le type d'événement, afin de s'assurer que, par exemple, les événements clavier ne sont pas envoyés au mauvais contrôle dans certaines situations. Il ne filtre pas tout, cependant, et nous devrons donc effectuer quelques vérifications de notre côté.

switch (Event.current.GetTypeForControl(controlID))
	{

Nous pouvons maintenant commencer à mettre en œuvre les comportements spécifiques aux différents types d'événements. Commençons par dessiner le contrôle :

case EventType.Repaint:
		{
			// Work out the width of the bar in pixels by lerping
			int pixelWidth = (int)Mathf.Lerp (1f, controlRect.width, value);

			// Build up the rectangle that the bar will cover
			// by copying the whole control rect, and then setting the width
			Rect targetRect = new Rect (controlRect){ width = pixelWidth };

			// Tint whatever we draw to be red/green depending on value
			GUI.color = Color.Lerp (Color.red, Color.green, value);

			// Draw the texture from the GUIStyle, applying the tint
			GUI.DrawTexture (targetRect, style.normal.background);

			// Reset the tint back to white, i.e. untinted
			GUI.color = Color.white;

			break;
		}

À ce stade, vous pouvez terminer la fonction et vous disposez d'un contrôle en lecture seule qui permet de visualiser des valeurs flottantes comprises entre 0 et 1. Mais continuons et rendons le contrôle interactif.

Pour mettre en œuvre un comportement agréable de la souris pour le contrôle, nous avons une exigence : une fois que vous avez cliqué sur le contrôle et commencé à le faire glisser, vous ne devriez pas avoir besoin de garder la souris au-dessus du contrôle. Il est beaucoup plus agréable pour l'utilisateur de pouvoir se concentrer sur l'emplacement horizontal de son curseur, sans se préoccuper du mouvement vertical. Cela signifie qu'ils peuvent déplacer la souris sur d'autres contrôles pendant qu'ils la font glisser, et nous avons besoin que ces contrôles ignorent la souris jusqu'à ce que l'utilisateur relâche à nouveau le bouton.

La solution consiste à utiliser GUIUtility.hotControl. Il s'agit d'une simple variable destinée à contenir l'ID du contrôle qui a capturé la souris. IMGUI utilise cette valeur dans GetTypeForControl() ; si elle n'est pas égale à 0, les événements souris sont filtrés à moins que l'ID du contrôle transmis ne soit le hotControl.

Le réglage et la réinitialisation de hotControl sont donc assez simples :

case EventType.MouseDown:
		{
			// If the click is actually on us...
			if (controlRect.Contains (Event.current.mousePosition)
			// ...and the click is with the left mouse button (button 0)...
			 && Event.current.button == 0)
				// ...then capture the mouse by setting the hotControl.
				GUIUtility.hotControl = controlID;

			break;
		}

		case EventType.MouseUp:
		{
			// If we were the hotControl, we aren't any more.
			if (GUIUtility.hotControl == controlID)
				GUIUtility.hotControl = 0;

			break;
		}

Notez que si un autre contrôle est le contrôle actif - c'est-à-dire si GUIUtility.hotControl est différent de 0 et de l'ID de notre propre contrôle - ces cas ne seront tout simplement pas exécutés, car GetTypeForControl() renverra "ignore" au lieu des événements mouseUp/mouseDown.

Définir le hotControl est une bonne chose, mais nous n'avons toujours rien fait pour modifier la valeur lorsque la souris est abaissée. La façon la plus simple de procéder est de fermer le commutateur et de dire que tout événement de souris (clic, glissement ou relâchement) qui se produit alors que nous sommes le hotControl (et que nous sommes donc en train de cliquer et de glisser - mais pas de relâcher, parce que nous avons mis à zéro le hotControl dans ce cas ci-dessus) doit entraîner un changement de valeur :

if (Event.current.isMouse && GUIUtility.hotControl == controlID) {

		// Get mouse X position relative to left edge of the control
		float relativeX = Event.current.mousePosition.x - controlRect.x;

		// Divide by control width to get a value between 0 and 1
		value = Mathf.Clamp01 (relativeX / controlRect.width);

		// Report that the data in the GUI has changed
		GUI.changed = true;

		// Mark event as 'used' so other controls don't respond to it, and to
		// trigger an automatic repaint.
		Event.current.Use ();
	}

Ces deux dernières étapes - définir GUI.changed et appeler Event.current.Use() - sont particulièrement importantes, non seulement pour que ce contrôle se comporte correctement, mais aussi pour qu'il soit compatible avec d'autres contrôles et fonctionnalités IMGUI. En particulier, la définition de GUI.changed à true permettra au code appelant d'utiliser les fonctions EditorGUI.BeginChangeCheck() et EditorGUI.EndChangeCheck() pour détecter si l'utilisateur a effectivement modifié la valeur de votre contrôle ou non ; mais vous devriez également éviter de définir GUI.changed à false, car cela pourrait finir par cacher le fait qu'un contrôle précédent a vu sa valeur modifiée.

Enfin, nous devons renvoyer une valeur à partir de la fonction. Vous vous souviendrez que nous avons dit que nous retournerions la valeur modifiée du flotteur - ou la valeur originale, si rien n'a changé, ce qui sera le cas la plupart du temps :

return value;
}

Et nous avons terminé. MyCustomSlider est désormais un contrôle IMGUI simple et fonctionnel, prêt à être utilisé dans des éditeurs personnalisés, des PropertyDrawers, des fenêtres d'éditeur, etc. Nous pouvons encore l'améliorer, par exemple en prenant en charge l'édition multiple, mais nous y reviendrons plus loin.

Plus que vous ne pouvez gérer

Il y a une autre chose particulièrement importante et non évidente à propos de l'IMGUI, c'est sa relation avec la vue de la scène. Vous connaissez tous les éléments d'aide de l'interface utilisateur qui sont dessinés dans la vue de la scène lorsque vous voulez translater, faire pivoter et mettre à l'échelle des objets - les flèches orthogonales, les anneaux et les lignes en forme de boîte que vous pouvez cliquer et faire glisser pour manipuler des objets. Ces éléments de l'interface utilisateur sont appelés "Handles".

Ce qui n'est pas évident, c'est que les Handles sont également alimentés par IMGUI !

Après tout, il n'y a rien d'inhérent à ce que nous avons dit sur IMGUI jusqu'à présent qui soit spécifique à la 2D ou aux éditeurs/fenêtres d'édition. Les contrôles standard que vous trouvez dans les classes GUI et EditorGUI sont tous en 2D, certes, mais les concepts de base comme EventType et les ID de contrôle ne dépendent pas du tout de la 2D. Ainsi, alors que les classes GUI et EditorGUI fournissent des contrôles 2D destinés aux fenêtres d'édition et aux éditeurs pour les composants de l'inspecteur, la classe Handles fournit des contrôles 3D destinés à être utilisés dans la vue de la scène. Tout comme EditorGUI.IntField dessine un contrôle qui permet à l'utilisateur de modifier un seul nombre entier, nous disposons de fonctions telles que :

Vector3 PositionHandle(Vector3 position, Quaternion rotation) ;

qui permettra à l'utilisateur de modifier visuellement une valeur Vector3, en fournissant un ensemble de flèches interactives dans la vue de la scène. Comme auparavant, vous pouvez définir vos propres fonctions Handle pour dessiner des éléments d'interface utilisateur personnalisés ; la gestion de l'interaction avec la souris est un peu plus complexe, car il ne suffit plus de vérifier si la souris se trouve à l'intérieur d'un rectangle ou non - la classe HandleUtility peut vous être utile à cet égard - mais la structure et les concepts de base sont les mêmes.

Si vous fournissez une fonction OnSceneGUI dans votre classe d'éditeur personnalisé, vous pouvez utiliser les fonctions Handle pour dessiner dans la vue de la scène, et elles seront positionnées correctement dans l'espace mondial, comme vous vous y attendez. Gardez cependant à l'esprit qu'il est possible d'utiliser les Handles dans des contextes 2D tels que les éditeurs personnalisés, ou d'utiliser des fonctions GUI dans la vue de la scène - vous devrez peut-être simplement faire des choses comme configurer les matrices GL ou appeler Handles.BeginGUI() et Handles.EndGUI() pour configurer le contexte avant de les utiliser.

État de l'Union graphique

Dans le cas de MyCustomSlider, il n'y avait que deux informations dont nous devions tenir compte : la valeur actuelle du curseur (qui était transmise par l'utilisateur et lui était renvoyée) et le fait que l'utilisateur était en train de modifier la valeur (ce que nous avons fait en utilisant hotControl pour en tenir compte). Mais qu'en est-il si un contrôle doit conserver plus d'informations que cela ?

L'IMGUI fournit un système de stockage simple pour les "objets d'état" associés à un contrôle. Vous définissez votre propre classe pour le stockage des valeurs, puis vous demandez à IMGUI d'en gérer une instance, associée à l'ID de votre contrôle. Vous n'avez droit qu'à un seul objet d'état par ID de contrôle, et vous ne devez pas l'instancier vous-même - IMGUI le fait pour vous, en utilisant le constructeur par défaut de l'objet d'état. Les objets d'état ne sont pas non plus sérialisés lors du rechargement du code de l'éditeur - ce qui se produit chaque fois que votre code est recompilé - et vous ne devriez donc les utiliser que pour des choses de courte durée. (Notez que cela est vrai même si vous marquez vos objets d'état comme [Serializable] - le sérialiseur ne visite tout simplement pas ce coin particulier du tas).

En voici un exemple. Supposons que nous voulions un bouton qui renvoie un message vrai chaque fois qu'il est enfoncé, mais qui clignote également en rouge si vous le maintenez enfoncé pendant plus de deux secondes. Nous devrons garder une trace de l'heure à laquelle le bouton a été pressé à l'origine ; nous le ferons en le stockant dans un objet state. Voici donc notre classe d'objets d'état :

public class FlashingButtonInfo
{
      private double mouseDownAt;

      public void MouseDownNow()
      {
      		mouseDownAt = EditorApplication.timeSinceStartup;
      }

      public bool IsFlashing(int controlID)
      {
            if (GUIUtility.hotControl != controlID)
                  return false;

            double elapsedTime = EditorApplication.timeSinceStartup - mouseDownAt;
            if (elapsedTime < 2f)
                  return false;

            return (int)((elapsedTime - 2f) / 0.1f) % 2 == 0;
      }
}

Nous stockons l'heure à laquelle la souris a été pressée dans "mouseDownAt" lorsque MouseDownNow() est appelée, puis nous utilisons la fonction IsFlashing pour nous dire "si le bouton doit être coloré en rouge maintenant" - comme vous pouvez le voir, il ne sera certainement pas rouge s'il ne s'agit pas du hotControl ou si moins de 2 secondes se sont écoulées depuis qu'il a été cliqué, mais après cela, nous le faisons changer de couleur toutes les 0,1 secondes.

Voici le code de la commande de bouton proprement dite :

public static bool FlashingButton(Rect rc, GUIContent content, GUIStyle style)
{
        int controlID = GUIUtility.GetControlID (FocusType.Native);

        // Get (or create) the state object
        var state = (FlashingButtonInfo)GUIUtility.GetStateObject(
                                             typeof(FlashingButtonInfo),
                                             controlID);

        switch (Event.current.GetTypeForControl(controlID)) {
                case EventType.Repaint:
                {
                        GUI.color = state.IsFlashing (controlID)
                            ? Color.red
                            : Color.white;
                        style.Draw (rc, content, controlID);
                        break;
                }
                case EventType.MouseDown:
                {
                        if (rc.Contains (Event.current.mousePosition)
                         && Event.current.button == 0
                         && GUIUtility.hotControl == 0)
                        {
                                GUIUtility.hotControl = controlID;
                                state.MouseDownNow();
                        }
                        break;
                }
                case EventType.MouseUp:
                {
                        if (GUIUtility.hotControl == controlID)
                                GUIUtility.hotControl = 0;
                        break;
                }
        }

        return GUIUtility.hotControl == controlID;
}

Vous devriez reconnaître le code dans les cas mouseDown/mouseUp comme étant très similaire à ce que nous avons fait pour capturer la souris dans le curseur personnalisé, ci-dessus. Les seules différences sont l'appel à state.MouseDownNow() lorsque vous appuyez sur la souris, et la modification de GUI.color dans l'événement repaint.

Les plus perspicaces d'entre vous auront peut-être remarqué qu'il existe une autre différence essentielle dans l'événement repaint - l'appel à style.Draw(). Qu'est-ce qui se passe ?

Faire de la GUI avec du style

Lorsque nous avons créé le contrôle de curseur personnalisé, nous avons utilisé GUI.DrawTexture pour dessiner la barre elle-même . Cela a bien fonctionné, mais notre FlashingButton doit avoir une légende, en plus de l'image du "rectangle arrondi" qui est le bouton lui-même. Nous pourrions essayer d'arranger quelque chose avec GUI.DrawTexture pour dessiner l'image du bouton et ensuite GUI.Label par-dessus pour dessiner la légende... mais nous pouvons faire mieux. Nous pouvons utiliser la même technique que GUI.Label pour se dessiner, et supprimer l'intermédiaire.

Un GUIStyle contient des informations sur les propriétés visuelles d'un élément GUI - à la fois des éléments de base comme la police ou la couleur du texte à utiliser, et des propriétés de mise en page plus subtiles comme l'espacement à lui donner. Toutes ces informations sont stockées dans un GUIStyle, avec des fonctions permettant de calculer la largeur et la hauteur d'un contenu utilisant le style, et des fonctions permettant de dessiner le contenu à l'écran.

En fait, GUIStyle ne s'occupe pas seulement d'un style pour un contrôle : il peut s'occuper du rendu dans un tas de situations dans lesquelles un élément GUI peut se trouver - en le dessinant différemment lorsqu'il est survolé, lorsqu'il a le focus clavier, lorsqu'il est désactivé, et lorsqu'il est "actif" (par exemple, lorsqu'un bouton est en train d'être pressé). Vous pouvez fournir les informations relatives à la couleur et à l'image d'arrière-plan pour toutes ces situations, et le GUIStyle choisira la couleur et l'image d'arrière-plan appropriées au moment du dessin, en fonction de l'ID du contrôle.

Il existe quatre façons principales d'obtenir des GUIStyles que vous pouvez utiliser pour dessiner vos contrôles :

- Construisez-en un dans le code (new GUIStyle()) et définissez ses valeurs.

- Utilisez l'un des styles intégrés de la classe EditorStyles. Si vous souhaitez que vos contrôles personnalisés ressemblent aux contrôles intégrés - dessin de vos propres barres d'outils, contrôles de type Inspecteur, etc.

- Si vous souhaitez simplement créer une petite variation d'un style existant - par exemple, un bouton normal mais avec un texte aligné à droite - vous pouvez cloner les styles dans la classe EditorStyles (new GUIStyle(existingStyle)) et ne modifier que les propriétés que vous souhaitez changer.

- Récupérez-les à partir d'un GUISkin.

Un GUISkin est essentiellement un gros paquet d'objets GUIStyle ; il peut être créé en tant qu'actif dans votre projet et édité librement dans l'inspecteur. Si vous en créez un et que vous y jetez un coup d'œil, vous verrez des emplacements pour tous les types de contrôle standard - boîtes, boutons, étiquettes, bascules, etc. - mais en tant qu'auteur d'un contrôle personnalisé, portez votre attention sur la section "styles personnalisés" située vers le bas. Ici, vous pouvez définir un nombre quelconque d'entrées GUIStyle personnalisées, en donnant à chacune un nom unique. Vous pourrez ensuite les récupérer en utilisant GUISkin.GetStyle("nameOfCustomStyle"). Si vous conservez votre skin dans le dossier "Editor Default Resources", vous pouvez utiliser EditorGUIUtility.LoadRequired(); sinon, vous pouvez utiliser une méthode comme AssetDatabase.LoadAssetAtPath() pour le charger à partir d'un autre endroit du projet. (Ne placez pas vos ressources réservées à l'éditeur dans un endroit où elles seront intégrées par erreur dans les groupes de ressources ou dans le dossier Resources).

Armé d'un GUIStyle, vous pouvez alors dessiner un GUIContent - un mélange de texte, d'icône et d'infobulle - en utilisant GUIStyle.Draw(), en lui passant le rectangle dans lequel vous dessinez, le GUIContent que vous voulez dessiner et l'ID du contrôle qui doit être utilisé pour déterminer si le contenu a des choses comme le focus clavier.

La mise en place des positions

Vous aurez remarqué que tous les contrôles GUI que nous avons étudiés et écrits jusqu'à présent comprennent un paramètre Rect qui détermine la position du contrôle à l'écran. Maintenant que nous avons parlé du GUIStyle, vous vous êtes peut-être arrêté lorsque j'ai dit qu'un GUIStyle comprend des "propriétés de mise en page comme l'espacement dont il a besoin". Vous êtes peut-être en train de penser : "euh oh. Cela signifie-t-il que nous devons calculer nos valeurs Rect de manière à ce que les valeurs d'espacement soient respectées ?"

C'est certainement une approche qui s'offre à nous, mais il y a un moyen plus simple. IMGUI comprend un mécanisme de "mise en page" qui peut calculer automatiquement les valeurs de Rect appropriées pour nos contrôles, en tenant compte d'éléments tels que l'espacement. Comment cela fonctionne-t-il ?

L'astuce consiste à ajouter une valeur EventType aux contrôles pour qu'ils puissent y répondre : EventType.Layout. IMGUI envoie l'événement à votre code GUI, et les contrôles que vous invoquez répondent en appelant les fonctions de mise en page IMGUI - GUILayoutUtility.GetRect(), GUILayout.BeginHorizonal / Vertical, et GUILayout.EndHorizontal / Vertical, entre autres - que IMGUI enregistre, construisant effectivement une arborescence des contrôles dans votre mise en page et l'espace qu'ils requièrent. Une fois qu'il a terminé et que l'arbre est entièrement construit, IMGUI effectue un passage récursif sur l'arbre, en calculant les largeurs et hauteurs réelles des éléments et leur position les uns par rapport aux autres, en positionnant les contrôles successifs les uns à côté des autres et ainsi de suite.

Ensuite, lorsqu'il s'agit d'un événement de type EventType.Repaint - ou même de tout autre type d'événement - les contrôles font appel aux mêmes fonctions de mise en page IMGUI. Cette fois-ci, au lieu d'enregistrer les appels, l'IMGUI "rejoue" les appels qu'il a précédemment enregistrés lors de l'événement Layout, en renvoyant les rectangles qu'il a calculés ; après avoir appelé GUILayoutUtility.GetRect() lors de l'événement layout pour enregistrer le fait que vous avez besoin d'un rectangle, vous l'appelez à nouveau lors de l'événement repaint et il renvoie effectivement le rectangle que vous devez utiliser.

Comme pour les ID de contrôle, cela signifie que vous devez être cohérent dans les appels de mise en page que vous faites entre les événements Layout et d'autres événements - sinon vous finirez par récupérer les rectangles calculés pour les mauvais contrôles. Cela signifie également que les valeurs renvoyées par GUILayoutUtility.GetRect() lors d'un événement Layout sont inutiles, car IMGUI ne connaîtra pas le rectangle qu'il est censé vous donner tant que l'événement n'est pas terminé et que l'arbre de disposition n'a pas été traité.

À quoi cela ressemble-t-il pour notre commande de curseur personnalisée ? Nous pouvons en fait écrire une version de notre contrôle compatible avec la mise en page très facilement, car une fois que nous avons récupéré un rectangle de IMGUI, nous pouvons simplement appeler le code que nous avons déjà écrit :

public static float MyCustomSlider(float value, GUIStyle style)
{
	Rect position = GUILayoutUtility.GetRect(GUIContent.none, style);
	return MyCustomSlider(position, value, style);
}

L'appel à GUILayoutUtility.GetRect aura deux fonctions : lors d'un événement Layout, il enregistrera que nous voulons utiliser le style donné pour dessiner un contenu vide - vide parce qu'il n'y a pas de texte ou d'image spécifique pour lequel nous devons faire de la place - et lors d'autres événements, il récupérera un rectangle réel pour que nous puissions l'utiliser. Cela signifie qu'au cours d'un événement de mise en page, nous appelons MyCustomSlider avec un faux rectangle, mais cela n'a pas d'importance - nous devons quand même le faire, afin de nous assurer que les appels habituels sont faits à GetControlID(), et que le rectangle n'est pas réellement utilisé pour quoi que ce soit au cours d'un événement de mise en page.

Vous vous demandez peut-être comment l'IMGUI peut déterminer la taille du curseur à partir d'un contenu "vide" et d'un simple style. Il ne s'agit pas de beaucoup d'informations - nous comptons sur le fait que le style contienne toutes les informations nécessaires, que IMGUI peut utiliser pour déterminer le rectangle à assigner. Mais qu'en est-il si nous voulons laisser l'utilisateur contrôler cela - ou, disons, utiliser une hauteur fixe à partir du style mais laisser l'utilisateur contrôler la largeur. Comment faire ?

La réponse se trouve dans la classe GUILayoutOption. Les instances de cette classe représentent des directives données au système de mise en page pour qu'un rectangle particulier soit calculé d'une certaine manière ; par exemple, "doit avoir une hauteur de 30" ou "doit s'étendre horizontalement pour remplir l'espace" ou "doit avoir une largeur d'au moins 20 pixels". Nous les créons en appelant des fonctions d'usine dans la classe GUILayout - GUILayout.ExpandWidth(), GUILayout.MinHeight(), etc. - et nous les transmettons à GUILayoutUtility.GetRect() sous la forme d'un tableau. Ils sont stockés dans l'arbre de présentation et pris en compte lorsque l'arbre est traité à la fin de l'événement de présentation.

Pour permettre à l'utilisateur de fournir aussi peu ou autant d'instances de GUILayoutOption qu'il le souhaite sans avoir à créer et à gérer ses propres tableaux, nous tirons parti du mot-clé C# "params", qui vous permet d'appeler une méthode en lui passant un nombre quelconque de paramètres, et de faire en sorte que ces paramètres arrivent dans la méthode automatiquement emballés dans un tableau. Voici maintenant notre curseur modifié :

public static float MyCustomSlider(float value, GUIStyle style, params GUILayoutOption[] opts)
{
	Rect position = GUILayoutUtility.GetRect(GUIContent.none, style, opts);
	return MyCustomSlider(position, value, style);
}

Comme vous pouvez le voir, nous prenons simplement ce que l'utilisateur nous a donné et le transmettons à GetRect.

L'approche que nous avons utilisée ici, qui consiste à envelopper une fonction de contrôle IMGUI positionnée manuellement dans une version de routage automatique, fonctionne pour pratiquement tous les contrôles IMGUI, y compris ceux qui sont intégrés dans la classe GUI. En fait, la classe GUILayout utilise exactement cette approche pour fournir des versions à affichage automatique des contrôles de la classe GUI (et nous proposons une classe EditorGUILayout correspondante pour envelopper les contrôles de la classe EditorGUI). Vous voudrez peut-être suivre cette convention de classes jumelles lorsque vous créerez vos propres contrôles IMGUI.

Il est également tout à fait possible de combiner les contrôles automatiques et les contrôles manuels. Vous pouvez appeler GetRect pour réserver un morceau d'espace, puis effectuer vos propres calculs pour diviser ce rectangle en sous-rectangles que vous utiliserez ensuite pour dessiner plusieurs contrôles ; le système de mise en page n'utilise en aucune façon les ID de contrôle, de sorte qu'il n'y a aucun problème à avoir plusieurs contrôles par rectangle de mise en page (ou même plusieurs rectangles de mise en page par contrôle). Cela peut parfois être beaucoup plus rapide que l'utilisation complète du système de mise en page.

Notez également que si vous écrivez des PropertyDrawers, vous ne devez pas utiliser le système de mise en page ; au lieu de cela, vous devez simplement utiliser le rectangle transmis à votre surcharge PropertyDrawer.OnGUI(). La raison en est que, sous le capot, la classe Editor elle-même n'utilise pas le système de mise en page, pour des raisons de performance ; elle calcule elle-même un simple rectangle, qu'elle déplace vers le bas pour chaque propriété successive. Ainsi, si vous utilisez le système de disposition dans votre PropertyDrawer, il n'aura aucune connaissance des propriétés qui ont été dessinées avant la vôtre et finira par vous positionner au-dessus d'elles. Ce qui n'est pas ce que vous souhaitez !

Leeloo Dallas Multi-Property

Jusqu'à présent, tout ce dont nous avons parlé vous permet de créer votre propre contrôle IMGUI qui fonctionnera sans problème. Il y a juste quelques points supplémentaires à discuter lorsque vous voulez vraiment peaufiner ce que vous avez construit au même niveau que les contrôles intégrés d'Unity.

La première est l'utilisation de SerializedProperty. Je ne veux pas entrer dans les détails du système SerializedProperty dans ce billet - nous laisserons cela pour une autre fois - mais juste pour résumer rapidement : Une SerializedProperty "enveloppe" une variable unique gérée par le système de sérialisation (chargement et sauvegarde) d'Unity. Chaque variable de chaque script que vous écrivez et qui apparaît dans l'inspecteur - ainsi que chaque variable de chaque objet de moteur que vous voyez dans l'inspecteur - est accessible via l'API SerializedProperty, du moins dans l'éditeur.

SerializedProperty est utile parce qu'il ne vous donne pas seulement accès à la valeur de la variable, mais aussi à des informations telles que la différence entre la valeur de la variable et la valeur d'un préfabriqué dont elle provient, ou si une variable avec des champs enfants (par exemple une structure) est développée ou réduite dans l'inspecteur. Il intègre également toutes les modifications que vous apportez à la valeur dans les systèmes d'annulation et de nettoyage de scène. Il vous permet également de le faire sans jamais créer la version gérée de votre objet, ce qui peut grandement améliorer les performances. Ainsi, si nous voulons que nos contrôles IMGUI soient compatibles avec une multitude de fonctionnalités de l'éditeur - annuler, salir la scène, remplacer les préfabriqués, etc - nous devons nous assurer que nous prenons en charge SerializedProperty.

Si vous examinez les méthodes de l'EditorGUI qui prennent une SerializedProperty comme argument, vous verrez que la signature est légèrement différente. Au lieu de l'approche "prendre un flotteur, retourner un flotteur" de notre précédent curseur personnalisé, les contrôles IMGUI activés par SerializedProperty prennent simplement une instance de SerializedProperty comme argument, et ne retournent rien. En effet, les modifications qu'ils doivent apporter à la valeur sont appliquées directement à la SerializedProperty elle-même. Ainsi, notre curseur personnalisé de tout à l'heure peut maintenant ressembler à ceci :

public static void MyCustomSlider(Rect controlRect, SerializedProperty prop, GUIStyle style)

Le paramètre "value" que nous avions auparavant a disparu, ainsi que la valeur de retour, et à la place, le paramètre "prop" est là pour transmettre la SerializedProperty. Pour récupérer la valeur actuelle de la propriété afin de dessiner la barre de défilement, nous accédons simplement à prop.floatValue, et lorsque l'utilisateur modifie la position du défilement, nous attribuons simplement prop.floatValue.

Le fait que la propriété sérialisée soit présente dans le code du contrôle IMGUI présente d'autres avantages. Prenons l'exemple de l'affichage en gras des propriétés modifiées dans les instances de préfabrication. Vérifiez simplement la propriété prefabOverride sur la SerializedProperty, et si elle est vraie, faites ce qu'il faut pour afficher le contrôle différemment. Heureusement, si vous souhaitez mettre du texte en gras, IMGUI s'en chargera automatiquement tant que vous ne spécifiez pas de police dans votre GUIStyle lorsque vous dessinez. (Si vous spécifiez une police dans votre GUIStyle, vous devrez vous en occuper vous-même - en ayant des versions normale et grasse de votre police et en choisissant l'une ou l'autre en fonction de prefabOverride lorsque vous voudrez dessiner).

L'autre fonctionnalité majeure dont vous avez besoin est la prise en charge de l'édition multi-objets, c'est-à-dire la gestion gracieuse de la situation lorsque votre contrôle doit afficher plusieurs valeurs simultanément. Testez cela en vérifiant la valeur de EditorGUI.showMixedValue; si elle est vraie, votre contrôle est utilisé pour représenter plusieurs valeurs différentes simultanément, alors faites ce qu'il faut pour l'indiquer.

Les mécanismes bold-on-prefabOverride et showMixedValue nécessitent que le contexte de la propriété ait été défini à l'aide des fonctions EditorGUI.BeginProperty() et EditorGUI.EndProperty(). Le modèle recommandé est de dire que si votre méthode de contrôle prend une SerializedProperty comme argument, elle fera elle-même les appels à BeginProperty et EndProperty, tandis que si elle traite des valeurs "brutes" - comme, par exemple, EditorGUI.IntField, qui prend et renvoie directement des ints et ne travaille pas avec des propriétés - le code appelant est responsable de l'appel à BeginProperty et EndProperty. (C'est logique, car si votre contrôle traite des valeurs "brutes", il n'a pas de valeur SerializedProperty qu'il peut transmettre à BeginProperty de toute façon).

public class MySliderDrawer : PropertyDrawer
{
    public override float GetPropertyHeight (SerializedProperty property, GUIContent label)
    {
        return EditorGUIUtility.singleLineHeight;
    }

    private GUISkin _sliderSkin;

    public override void OnGUI (Rect position, SerializedProperty property, GUIContent label)
    {
        if (_sliderSkin == null)
            _sliderSkin = (GUISkin)EditorGUIUtility.LoadRequired ("MyCustomSlider Skin");

        MyCustomSlider (position, property, _sliderSkin.GetStyle ("MyCustomSlider"), label);

    }
}

// Then, the updated definition of MyCustomSlider:
public static void MyCustomSlider(Rect controlRect, SerializedProperty prop, GUIStyle style, GUIContent label)
{
    label = EditorGUI.BeginProperty (controlRect, label, prop);
    controlRect = EditorGUI.PrefixLabel (controlRect, label);

    // Use our previous definition of MyCustomSlider, which we’ve updated to do something
    // sensible if EditorGUI.showMixedValue is true
    EditorGUI.BeginChangeCheck();
    float newValue = MyCustomSlider(controlRect, prop.floatValue, style);
    if(EditorGUI.EndChangeCheck())
        prop.floatValue = newValue;

    EditorGUI.EndProperty ();
}
C'est tout pour l'instant

J'espère que cet article vous a éclairé sur certaines parties essentielles de IMGUI que vous devrez comprendre si vous voulez vraiment porter la personnalisation de votre éditeur à un niveau supérieur. Il y a plus à couvrir avant de devenir un gourou de l'édition - le système SerializedObject / SerializedProperty, l'utilisation de CustomEditor versus EditorWindow versus PropertyDrawer, la gestion de Undo, etc - mais IMGUI joue un rôle important dans le déblocage de l'immense potentiel d'Unity pour la création d'outils personnalisés - à la fois en vue de la vente sur l'Asset Store, et en vue de l'autonomisation des développeurs de vos propres équipes.

Posez vos questions et donnez votre avis dans les commentaires !