Extension de l'éditeur Unity avec la fenêtre de l'éditeur, l'objet scriptable et l'éditeur personnalisé

Bonjour à tous! Je m'appelle Grisha et je suis le fondateur de CGDevs. Aujourd'hui, je veux parler des extensions de l'éditeur et parler de l'un de mes projets, que j'ai décidé de publier dans OpenSource.

L'unité est un excellent outil, mais elle a un petit problème. Pour un débutant, afin de créer une pièce simple (une boîte avec des fenêtres), il faut soit maîtriser la modélisation 3D, soit essayer d'assembler quelque chose à partir de quads. Récemment, il est devenu ProBuilder entièrement gratuit, mais c'est également un package de modélisation 3D simplifié. Je voulais un outil simple qui nous permettrait de créer rapidement des environnements comme des pièces avec des fenêtres et de corriger les UV. Il y a longtemps, j'ai développé un plug-in pour Unity, qui vous permet de prototyper rapidement des environnements comme des appartements et des pièces à l'aide d'un dessin 2D, et maintenant j'ai décidé de le mettre dans OpenSource. À l'aide de son exemple, nous analyserons comment vous pouvez développer l'éditeur et quels outils existent pour cela. Si vous êtes intéressé, bienvenue au chat. Un lien vers le projet à la fin, comme toujours, est joint.



Unity3d dispose d'une boîte à outils suffisamment large pour étendre les capacités de l'éditeur. Grâce à des classes telles que EditorWindow , ainsi que les fonctionnalités de l' inspecteur personnalisé , du tiroir de propriétés et de TreeView (+ UIElements devraient apparaître bientôt), il est facile de créer vos cadres de différents degrés de complexité au-dessus de l'unité.

Aujourd'hui, nous allons parler de l'une des approches que j'ai utilisées pour développer ma solution et de quelques problèmes intéressants auxquels j'ai dû faire face.



La solution est basée sur l'utilisation de trois classes, telles que EditorWindow (toutes les fenêtres supplémentaires), ScriptableObject (stockage de données) et CustomEditor (fonctionnalité d'inspecteur supplémentaire pour Scriptable Object).

Lors du développement d'extensions d'éditeur, il est important d'essayer de respecter le principe selon lequel les développeurs Unity utiliseront l'extension, de sorte que les interfaces doivent être claires, natives et intégrées dans le flux de travail Unity.

Parlons de tâches intéressantes.

Pour créer un prototype, nous devons d'abord apprendre à dessiner des dessins à partir desquels nous générerons notre environnement. Pour ce faire, nous avons besoin d'une fenêtre EditorWindow spéciale, dans laquelle nous afficherons tous les dessins. En principe, il serait possible de dessiner dans SceneView, mais l'idée initiale était que lors de la finalisation de la solution, vous souhaitiez peut-être ouvrir plusieurs dessins en même temps. En général, la création d'une fenêtre séparée dans une unité est une tâche assez simple. Cela peut être trouvé dans les manuels Unity. Mais la grille de dessin est une tâche plus intéressante. Il y a plusieurs problèmes à ce sujet.

Unity a plusieurs styles qui affectent les couleurs des fenêtres.

Le fait est que la plupart des personnes utilisant la version Pro d'Unity utilisent un thème sombre, et seule la version claire est disponible dans la version gratuite. Cependant, les couleurs utilisées dans l'éditeur de dessin ne doivent pas fusionner avec l'arrière-plan. Ici, vous pouvez trouver deux solutions. La difficulté est de créer votre propre version des styles, de la vérifier et de changer la palette de la version de l'unité. Et la chose simple est de remplir l'arrière-plan de la fenêtre avec une certaine couleur. Dans le développement, il a été décidé d'utiliser une méthode simple. Un exemple de la façon dont cela peut être fait est d'appeler un tel code dans la méthode OnGUI.

Une certaine couleur
GUI.color = BgColor; GUI.DrawTexture(new Rect(Vector2.zero, maxSize), EditorGUIUtility.whiteTexture); GUI.color = Color.white; 



En substance, nous venons de dessiner la texture de couleur BgColor sur toute la fenêtre.



Dessinez et déplacez la grille

Ici, plusieurs problèmes ont été révélés à la fois. Tout d'abord, vous deviez saisir votre système de coordonnées. Le fait est que pour un travail correct et pratique, nous devons recalculer les coordonnées GUI de la fenêtre dans les coordonnées de la grille. Pour cela, deux méthodes de conversion ont été implémentées (essentiellement, ce sont deux matrices TRS peintes)

Conversion des coordonnées de fenêtre en coordonnées d'écran
 public Vector2 GUIToGrid(Vector3 vec) { Vector2 newVec = ( new Vector2(vec.x, -vec.y) - new Vector2(_ParentWindow.position.width / 2, -_ParentWindow.position.height / 2)) * _Zoom + new Vector2(_Offset.x, -_Offset.y); return newVec.RoundCoordsToInt(); } public Vector2 GridToGUI(Vector3 vec) { return (new Vector2(vec.x - _Offset.x, -vec.y - _Offset.y) ) / _Zoom + new Vector2(_ParentWindow.position.width / 2, _ParentWindow.position.height / 2); } 



_ParentWindow est la fenêtre dans laquelle nous allons dessiner la grille, _Offset est la position actuelle de la grille et _Zoom est le degré d'approximation.

Deuxièmement, pour tracer les lignes, nous avons besoin de la méthode Handles.DrawLine . La classe Handles possède de nombreuses méthodes utiles pour le rendu de graphiques simples dans les fenêtres de l'éditeur, l'inspecteur ou SceneView. Au moment du développement du plugin (Unity 5.5), Handles.DrawLine - allouait de la mémoire et fonctionnait généralement assez lentement. Pour cette raison, le nombre de lignes possibles pour le rendu a été limité par la constante CELLS_IN_LINE_COUNT , et le "niveau LOD" a également été créé au zoom pour obtenir des images par seconde acceptables dans l'éditeur.

Dessin de la grille
 void DrawLODLines(int level) { var gridColor = SkinManager.Instance.CurrentSkin.GridColor; var step0 = (int) Mathf.Pow(10, level); int halfCount = step0 * CELLS_IN_LINE_COUNT / 2 * 10; var length = halfCount * DEFAULT_CELL_SIZE; int offsetX = ((int) (_Offset.x / DEFAULT_CELL_SIZE)) / (step0 * step0) * step0; int offsetY = ((int) (_Offset.y / DEFAULT_CELL_SIZE)) / (step0 * step0) * step0; for (int i = -halfCount; i <= halfCount; i += step0) { Handles.color = new Color(gridColor.r, gridColor.g, gridColor.b, 0.3f); Handles.DrawLine( GridToGUI(new Vector2(-length + offsetX * DEFAULT_CELL_SIZE, (i + offsetY) * DEFAULT_CELL_SIZE)), GridToGUI(new Vector2(length + offsetX * DEFAULT_CELL_SIZE, (i + offsetY) * DEFAULT_CELL_SIZE)) ); Handles.DrawLine( GridToGUI(new Vector2((i + offsetX) * DEFAULT_CELL_SIZE, -length + offsetY * DEFAULT_CELL_SIZE)), GridToGUI(new Vector2((i + offsetX) * DEFAULT_CELL_SIZE, length + offsetY * DEFAULT_CELL_SIZE)) ); } offsetX = (offsetX / (10 * step0)) * 10 * step0; offsetY = (offsetY / (10 * step0)) * 10 * step0; ; for (int i = -halfCount; i <= halfCount; i += step0 * 10) { Handles.color = new Color(gridColor.r, gridColor.g, gridColor.b, 1); Handles.DrawLine( GridToGUI(new Vector2(-length + offsetX * DEFAULT_CELL_SIZE, (i + offsetY) * DEFAULT_CELL_SIZE)), GridToGUI(new Vector2(length + offsetX * DEFAULT_CELL_SIZE, (i + offsetY) * DEFAULT_CELL_SIZE)) ); Handles.DrawLine( GridToGUI(new Vector2((i + offsetX) * DEFAULT_CELL_SIZE, -length + offsetY * DEFAULT_CELL_SIZE)), GridToGUI(new Vector2((i + offsetX) * DEFAULT_CELL_SIZE, length + offsetY * DEFAULT_CELL_SIZE)) ); } } 



Presque tout est prêt pour la grille. Son mouvement est décrit très simplement. _Le décalage est essentiellement la position actuelle de la «caméra».

Mouvement de grille
  public void Move(Vector3 dv) { var x = _Offset.x + dv.x * _Zoom; var y = _Offset.y + dv.y * _Zoom; _Offset.x = x; _Offset.y = y; } 



Dans le projet lui-même, vous pouvez vous familiariser avec le code de la fenêtre en général et voir comment les boutons peuvent être ajoutés à la fenêtre.

Nous allons plus loin. En plus d'une fenêtre séparée pour dessiner des dessins, nous devons en quelque sorte stocker les dessins eux-mêmes. Le moteur de sérialisation Unity interne, l'objet scriptable, est idéal pour cela. En fait, il vous permet de stocker les classes décrites comme actifs dans le projet, ce qui est très pratique et natif pour de nombreux développeurs d'unités. Par exemple, la partie de la classe Appartement qui est chargée de stocker les informations de disposition en général

Partie de la classe Appartement
  public class Apartment : ScriptableObject { #region fields public float Height; public bool IsGenerateOutside; public Material OutsideMaterial; public Texture PlanImage; [SerializeField] private List<Room> _Rooms; [SerializeField] private Rect _Dimensions; private Vector2[] _DimensionsPoints = new Vector2[4]; #endregion 



Dans l'éditeur, cela ressemble à ceci dans la version actuelle:



Ici, bien sûr, CustomEditor a déjà été appliqué, mais néanmoins, vous pouvez remarquer que des paramètres tels que _Dimensions, Height, IsGenerateOutside, OutsideMaterial et PlanImage sont affichés dans l'éditeur.

Tous les champs publics et les champs marqués avec [SerializeField] sont sérialisés (c'est-à-dire enregistrés dans un fichier dans ce cas). Cela aide beaucoup si vous devez enregistrer des dessins, mais lorsque vous travaillez avec ScriptableObject et toutes les ressources de l'éditeur, vous devez vous rappeler qu'il est préférable d'appeler la méthode AssetDatabase.SaveAssets () pour enregistrer l'état des fichiers. Sinon, les modifications ne seront pas enregistrées. Si vous ne sauvegardez pas le projet avec vos mains.

Nous allons maintenant analyser partiellement la classe ApartmentCustomInspector et son fonctionnement.

Classe ApartmentCustomInspector
  [CustomEditor(typeof(Apartment))] public class ApartmentCustomInspector : Editor { private Apartment _ThisApartment; private Rect _Dimensions; private void OnEnable() { _ThisApartment = (Apartment) target; _Dimensions = _ThisApartment.Dimensions; } public override void OnInspectorGUI() { TopButtons(); _ThisApartment.Height = EditorGUILayout.FloatField("Height (cm)", _ThisApartment.Height); var dimensions = EditorGUILayout.Vector2Field("Dimensions (cm)", _Dimensions.size).RoundCoordsToInt(); _ThisApartment.PlanImage = (Texture) EditorGUILayout.ObjectField(_ThisApartment.PlanImage, typeof(Texture), false); _ThisApartment.IsGenerateOutside = EditorGUILayout.Toggle("Generate outside (Directional Light)", _ThisApartment.IsGenerateOutside); if (_ThisApartment.IsGenerateOutside) _ThisApartment.OutsideMaterial = (Material) EditorGUILayout.ObjectField( "Outside Material", _ThisApartment.OutsideMaterial, typeof(Material), false); GenerateButton(); var dimensionsRect = new Rect(-dimensions.x / 2, -dimensions.y / 2, dimensions.x, dimensions.y); _Dimensions = dimensionsRect; _ThisApartment.Dimensions = _Dimensions; } private void TopButtons() { GUILayout.BeginHorizontal(); CreateNewBlueprint(); OpenBlueprint(); GUILayout.EndHorizontal(); } private void CreateNewBlueprint() { if (GUILayout.Button( "Create new" )) { var manager = ApartmentsManager.Instance; manager.SelectApartment(manager.CreateOrGetApartment("New Apartment" + GUID.Generate())); } } private void OpenBlueprint() { if (GUILayout.Button( "Open in Builder" )) { ApartmentsManager.Instance.SelectApartment(_ThisApartment); ApartmentBuilderWindow.Create(); } } private void GenerateButton() { if (GUILayout.Button( "Generate Mesh" )) { MeshBuilder.GenerateApartmentMesh(_ThisApartment); } } } 


CustomEditor est un outil très puissant qui vous permet de résoudre avec élégance de nombreuses tâches typiques pour l'extension de l'éditeur. Associé à ScriptableObject, il vous permet de créer des extensions d'éditeur simples, pratiques et intuitives. Cette classe est un peu plus compliquée que l'ajout de boutons, car vous pouvez voir dans la classe d'origine que le champ List_Rooms privé [SerializeField] est en cours de sérialisation. L'afficher dans l'inspecteur, d'une part, à rien, et d'autre part - cela peut entraîner des bogues imprévus et des états de dessin. La méthode OnInspectorGUI est responsable du rendu de l'inspecteur, et si vous avez juste besoin d'ajouter des boutons, vous pouvez y appeler la méthode DrawDefaultInspector () et tous les champs seront dessinés.

Les champs et boutons nécessaires sont ensuite dessinés manuellement. La classe EditorGUILayout elle-même possède de nombreuses implémentations pour une grande variété de types de champs pris en charge par l'unité. Mais le rendu des boutons dans Unity est implémenté dans la classe GUILayout. Comment fonctionne le traitement par pression sur un bouton dans ce cas. OnInspectorGUI - exécute la saisie de la souris pour chaque événement (mouvement de la souris, clics de souris à l'intérieur de la fenêtre de l'éditeur, etc.). Si l'utilisateur clique sur le bouton dans la zone, la méthode renvoie true et traite les méthodes qui se trouvent à l'intérieur du if 'décrit par vous. a. Pour un exemple:

Bouton de génération de maillage
 private void GenerateButton() { if (GUILayout.Button( "Generate Mesh" )) { MeshBuilder.GenerateApartmentMesh(_ThisApartment); } } 


Lorsque vous cliquez sur le bouton Générer un maillage, la méthode statique est appelée, qui est responsable de la génération d'un maillage d'une disposition spécifique.

En plus de ces mécanismes de base utilisés pour développer l'éditeur Unity, je voudrais mentionner séparément un outil très simple et très pratique, dont, pour une raison quelconque, beaucoup oublient - Sélection. La sélection est une classe statique qui vous permet de sélectionner les objets nécessaires dans l'inspecteur et ProjectView.

Pour sélectionner un objet, il vous suffit d'écrire Selection.activeObject = MyAwesomeUnityObject. Et la meilleure partie est qu'il fonctionne avec ScriptableObject. Dans ce projet, il est chargé de sélectionner un dessin et des pièces dans une fenêtre avec des dessins.

Merci de votre attention! J'espère que l'article et le projet vous seront utiles et que vous apprendrez quelque chose de nouveau par vous-même dans l'une des approches pour développer l'éditeur Unity. Et comme toujours - un lien vers le projet GitHub , où vous pouvez voir l'ensemble du projet. Il est encore un peu humide, mais néanmoins il permet déjà de faire des plans en 2D simplement et rapidement.

Source: https://habr.com/ru/post/fr431856/


All Articles