This post is also available in: English (Anglais)

Unity – Créer un éditeur de map maison (partie 1)

Introduction

Bonjour à tous !

Je prends un peu de temps pour partager mes compétences avec vous et vous montrer comment faire un editeur de carte sous Unity.

L’idée n’est pas nécessairement de vous donner un outil fonctionnel gratuitement, mais plutôt de vous enseigner les notions nécessaires à l’élaboration d’un outil qui vous corresponde.
La solution exposée n’est sûrement pas la seule existante, mais c’est celle que nous utilisons pour créer Radiant Blade.

Aperçu de l'éditeur de carte qui va être créé pendant ce tutoriel

Le tutoriel va être divisé en plusieurs parties du fait qu’il y ait pas mal de sujets à traiter, mais je ne sais pas encore combien ^_^

A propos du code

Les méthodes et classes qui sont documentés dans Unity ne seront pas obligatoirement traités ici, étant donné que  les infos sont déjà disponibles avec une petite recherche google 😊

Le code montré est complètement libre de droits. C’est un code que j’ai fait spécialement pour cet article.

Il n’y a rien que vous ne devriez pas être capable de faire, novice ou professionnel, mais ça devrait vous donner des bases solides pour commencer.

Sommaire de la partie 1

Nous allons traiter les points suivants :

  • Les “Editor Windows” ;
  • Afficher un indicateur dans la Scene View ;
  • Créer des prefabs pour remplir la palette de l’éditeur ;
  • Instancier les prefabs en cliquant dans la Scene View.

Quand vous aurez fini, vous constaterez que l’outil commence à ressembler à quelque chose, mais que l’on est loin d’un outil surpuissant. Enfin bon… Chaque chose en son temps !

Les Editor Windows (fenêtre d’éditeur)

https://docs.unity3d.com/ScriptReference/EditorWindow.html

Les fenêtres d’éditeur proposées par Unity nous permettent la création d’outils directement dans l’éditeur. Quand vos projets commencent à être professionnels, ce genre de petit outil fleurit un peu partout afin d’améliorer la productivité.

Si c’est la première fois que vous créez une « Editor Window », n’ayez crainte, Unity permet de le faire très simplement.

using UnityEngine;
using UnityEditor;

public class MyMapEditor : EditorWindow
{
    // The window is selected if it already exists, else it's created.
    [MenuItem("Window/My Map Editor")]
    private static void ShowWindow()
    {
        EditorWindow.GetWindow(typeof(MyMapEditor));
    }

    // Called to draw the MapEditor windows.
    private void OnGUI() { }

    // Does the rendering of the map editor in the scene view.
    private void OnSceneGUI(SceneView sceneView) { }

    void OnFocus()
    {
        SceneView.onSceneGUIDelegate -= this.OnSceneGUI; // Just in case
        SceneView.onSceneGUIDelegate += this.OnSceneGUI;
    }

    void OnDestroy()
    {
        SceneView.onSceneGUIDelegate -= this.OnSceneGUI;
    }
}

Ce squelette contient tout ce qui est nécessaire pour créer une fenêtre d’éditeur qui a pour but de manipuler la Scene View. Elle ne fait encore rien mais ça va arriver 😉

Ce qu’on vient d’écrire s’appelle du « code éditeur ». Il ne sera pas livré avec le jeu mais uniquement exécuté dans l’éditeur Unity.

Pour qu’il fonctionne, vous devez impérativement le mettre dans un dossier appelé « Editor », lui même quelque part dans le dossier “Assets”, sinon Unity ne le prendra pas en compte (et le livrera au passage).

Le menu "My Map Editor" est maintenant disponible dans l'onglet Window

Comme vous pouvez vous en douter, nous allons utiliser la méthode « OnGUI » pour ajouter l’UI générale de notre éditeur et OnSceneGUI pour ajouter des informations sur la « Scene View »
Notez que OnSceneGUI doit être appelé par un « delegate ». C’est une curiosité de Unity et on va devoir faire avec.

Voilà toute la doc dont vous avez besoin pour les « Editor Window GUI » :

https://docs.unity3d.com/ScriptReference/EditorGUI.html

https://docs.unity3d.com/ScriptReference/EditorGUILayout.html

https://docs.unity3d.com/ScriptReference/GUILayout.html

Mode « peinture » et repère visuel

Dans le cas d’un éditeur de carte, un truc plutôt sympa à faire est de pouvoir s’en servir tout en faisant autre chose à côté, sans être obligé de le fermer chaque fois que l’on souhaite travailler sur autre chose..
Nous allons ajouter un repère visuel pour faciliter le placement d’un objet au sein de la scène. Le problème, c’est que si cet indicateur est visible en permanence, l’existence de l’éditeur de carte au sein du projet va rapidement devenir casse-pied.. Du coup, une solution simple : un mode activé/désactivé.

    private bool paintMode = false;

    // Called to draw the MapEditor windows.
    private void OnGUI()
    {
        paintMode = GUILayout.Toggle(paintMode, "Start painting", "Button", GUILayout.Height(60f));
    }

    // Does the rendering of the map editor in the scene view.
    private void OnSceneGUI(SceneView sceneView)
    {
        if (paintMode)
        {
            DisplayVisualHelp();
        }
    }

Maintenant que nous avons un mode activé/désactivé, on peut passer au côté un peu plus vicieux : afficher le repère visuel dans la Scene View/
Dans cet exemple, nous allons afficher un carré vert fixé sur une grille (la position sera arrondie), afin de pouvoir positionner nos objets comme si nous faisions un jeu tuile par tuile. Vous êtes peut-être en train de faire un platformer ou un jeu 3D, ou autre, mais n’oubliez pas que peu importe ce que vous faites, les techniques sont les mêmes, seul le code changera légèrement.

Pour afficher l’indicateur visuel, nous allons utiliser les API suivantes :
https://docs.unity3d.com/ScriptReference/Handles.html

https://docs.unity3d.com/ScriptReference/HandleUtility.html

Personnellement, je trouve que ces classes sont vraiment mal nommées. La chose la plus dur que j’ai faite quand j’ai créé cet outil a été de trouver ces classes. Sans rire.
Bref, voici la partie manquante du code de la méthode DisplayVisualHelp :

private Vector2 cellSize = new Vector2(2f, 2f);

    private void DisplayVisualHelp()
    {
        // Get the mouse position in world space such as z = 0
        Ray guiRay = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition);
        Vector3 mousePosition = guiRay.origin - guiRay.direction * (guiRay.origin.z / guiRay.direction.z);

        // Get the corresponding cell on our virtual grid
        Vector2Int cell = new Vector2Int(Mathf.RoundToInt(mousePosition.x / cellSize.x), Mathf.RoundToInt(mousePosition.y / cellSize.y));
        Vector2 cellCenter = cell * cellSize;

        // Vertices of our square
        Vector3 topLeft = cellCenter + Vector2.left * cellSize * 0.5f + Vector2.up * cellSize * 0.5f;
        Vector3 topRight = cellCenter - Vector2.left * cellSize * 0.5f + Vector2.up * cellSize * 0.5f;
        Vector3 bottomLeft = cellCenter + Vector2.left * cellSize * 0.5f  - Vector2.up * cellSize * 0.5f;
        Vector3 bottomRight = cellCenter - Vector2.left * cellSize * 0.5f - Vector2.up * cellSize * 0.5f;

        // Rendering
        Handles.color = Color.green;
        Vector3[] lines = { topLeft, topRight, topRight, bottomRight, bottomRight, bottomLeft, bottomLeft, topLeft };
        Handles.DrawLines(lines);
    }

Ce code vous donne les fondamentaux :

  • Récupérer la position de la souris;
  • Afficher des formes dans la Scene View.

Bouton "Start Painting"

Le résultat correspond à ce qu’on attendait. On a désormais un outil surpuissant qui affiche un carré vert dans la Scene View… Remarquable ! 😉

Palette de prefab, collecte

Un éditeur de carte permet principalement d’ajouter ou de retirer des objets. Pour le moment on va se concentrer sur la partie ajout.

Pour cela, on peut soit faire une interface pour ajouter des assets via un Drag&Drop, soit avoir une interface avec un bouton « ajouter ». J’étais partie sur cette idée à la base, mais j’ai trouvé que le workflow était beaucoup trop lourd. Au final, j’ai opté pour une solution ou on va chercher les assets directement depuis un dossier. C’est beaucoup plus simple et direct.

Donc, voilà le plan d’attaque:

  • Ouvrir un dossier spécifique aux prefabs;
  • Récupérer tous les prefabs sous forme d’une liste;
  • Les afficher et permettre à l’utilisateur d’en choisir un.

Petit détail : le code éditeur n’a pas le droit d’accéder au dossier « Ressources » pour charger les prefabs, ce dossier est réservé aux ressources pour le « runtime ». Heureusement Unity nous permet d’utiliser le dossier « Editor Default Resources », c’est donc ce dernier qu’on va utiliser pour notre outil.

https://docs.unity3d.com/Manual/SpecialFolders.html

Et voici le code qui permet de récupérer les prefabs, simple et concis :

    void OnFocus()
    {
        SceneView.onSceneGUIDelegate -= this.OnSceneGUI; // Don't add twice
        SceneView.onSceneGUIDelegate += this.OnSceneGUI;

        RefreshPalette(); // Refresh the palette (can be called uselessly, but there is no overhead.)
    }

    // A list containing the available prefabs.
    [SerializeField]
    private List<GameObject> palette = new List<GameObject>();

    private string path = "Assets/Editor Default Resources/Palette";

    private void RefreshPalette()
    {
        palette.Clear();

        string[] prefabFiles = System.IO.Directory.GetFiles(path , "*.prefab");
        foreach (string prefabFile in prefabFiles)
            palette.Add(AssetDatabase.LoadAssetAtPath(prefabFile, typeof(GameObject)) as GameObject);
    }

J’ai modifié la méthode “OnFocus” pour ajouter la mise à jour de la palette. Cela va provoquer des rafraichissement inutiles de la palette, mais je suis pour la transparence niveau utilisateur, de manière à ce qu’il n’ai pas besoin de s’en soucier quand il ajoute ses prefabs au dossier.
A l’inverse, si cette routine était impactante niveau performance, il serait plus intéressant de ne l’appeler que lorsqu’on en a réellement besoin.

La liste est taguée “Serializable”. Cela nous permet de garder les informations en entrant en Playmode (sinon elles seraient perdues). Comme on rafraichit la palette sur l’événement “OnFocus” ce n’est pas vraiment nécessaire de la sérialiser, mais c’est une bonne habitude à prendre.

Bon, on passe à l’affichage ?

Palette de prefabs, affichage

Au niveau de l’affichage, on veut un truc un minimum intuitif.

On pourrait simplement avoir une liste de noms, ou une liste d’activables. Sauf que l’un comme l’autre ne sont pas très user-friendly.
Après avoir passé un peu de temps à chercher, j’ai trouvé ça :

https://docs.unity3d.com/ScriptReference/GUILayout.SelectionGrid.html

Cet outil nous permet d’afficher une grille d’images sélectionnables, ce qui est tout à fait ce dont on a besoin.
En ce qui concerne la prévisualisation, qui consiste à récupérer des images pour nourrir notre SelectionGrid, Unity nous propose un outil plutôt sympa : AssetPreview. Grosso modo, vous renseignez votre objet à Unity et il se débrouille.

https://docs.unity3d.com/ScriptReference/AssetPreview.html

A part savoir comment alimenter SelectionGrid, il n’y a pas de gros challenge ici :

    [SerializeField]
    private int paletteIndex;

    // Called to draw the MapEditor windows.
    private void OnGUI()
    {
        paintMode = GUILayout.Toggle(paintMode, "Start painting", "Button", GUILayout.Height(60f));

        // Get a list of previews, one for each of our prefabs
        List<GUIContent> paletteIcons = new List<GUIContent>();
        foreach (GameObject prefab in palette)
        {
            // Get a preview for the prefab
            Texture2D texture =AssetPreview.GetAssetPreview(prefab);
            paletteIcons.Add(new GUIContent(texture));
        }

        // Display the grid
        paletteIndex = GUILayout.SelectionGrid(paletteIndex, paletteIcons.ToArray(), 6);
    }

Bon c’est bien beau, mais pour que ça marche, il nous faut des prefabs ! Du coup j’ai ajouté 2 prefabs avec des sprites de Radiant Blade, histoire que vous puissiez vous rendre compte de ce que ça donne.

L'editeur de carte avec ses prefabs

Bon ! On est plus qu’à un clic d’avoir une version fonctionnelle de notre éditeur de map. Et ce clic est important : c’est celui qui permet de placer les objets.

Placer les prefabs

Le clic dans la Scene View c’est (pardonnez-moi l’expression) un peu la merde. Au début tu te dis que c’est simple…. Et en fait non, parce que ca dépend du type de curseur, de si tu cliques (ou non) sur un objet, et enfin de si tu es intéressé par le clic en lui même ou si le bouton est “pressé” (pressed down).

L’API fournie par Unity est bien plus complexe que “Input” et certains des éléments clés sont manquants. Pour cette fois, on ne va heureusement pas avoir trop à creuser, on veut juste un clic pour placer des éléments, c’est tout.

Commençons. Du fait qu’on ajoute des éléments dans la scène, nous ne sommes pas intéressés par les objets déjà présents, on veut donc éviter de les sélectionner. Pour ça, il y a plusieurs manières de faire, et celle que je trouve la plus efficace, c’est celle là :

    // Does the rendering of the map editor in the scene view.
    private void OnSceneGUI(SceneView sceneView)
    {
        if (paintMode)
        {
           Vector2 cellCenter = GetSelectedCell(); // Refactoring, I moved some code in this function


            DisplayVisualHelp(cellCenter);
            HandleSceneViewInputs(cellCenter);

            // Refresh the view
            sceneView.Repaint();
        }
    }

    private void HandleSceneViewInputs(Vector2 cellCenter)
    {
        // Filter the left click so that we can't select objects in the scene
        if (Event.current.type == EventType.Layout)
        {
            HandleUtility.AddDefaultControl(0); // Consume the event
        }
    }

On ajoute une action vide pour “consommer” l’évènement. Le code qui suit dans notre éditeur de carte, lui, va s’occuper de l’événement réel. L’important, c’est que Unity ne nous embêtera pas après.

Maintenant, on veut savoir si on fait un clic gauche, et, le cas échéant, ajouter un prefab au niveau de la souris. Prêt ?

        // We have a prefab selected and we are clicking in the scene view with the left button
        if (paletteIndex < palette.Count && Event.current.type == EventType.MouseDown && Event.current.button == 0)
        {
            // Create the prefab instance while keeping the prefab link
            GameObject prefab = palette[paletteIndex];
            GameObject gameObject = PrefabUtility.InstantiatePrefab(prefab) as GameObject;
            gameObject.transform.position = cellCenter;

            // Allow the use of Undo (Ctrl+Z, Ctrl+Y).
            Undo.RegisterCreatedObjectUndo(gameObject, "");
        }

En bonus, j’utilise le système d’annulation de Unity et je conserve le lien du prefab. Vous pouvez annuler une action en pressant Ctrl+Z et la map sera mise à jour quand vous mettrez à jour vos prefabs.

Voilà le résultat :

Editeur de carte fonctionnel en vidéo

Et maintenant ?

Cet article vous a donné les fondamentaux pour avoir un éditeur de carte minimaliste, mais on est encore loin d’avoir un outil de production fiable.

Petite liste non exhaustive des choses à rajouter:

  • Des brosses pour peindre des zones ;
  • Une sélection aléatoire parmi des variations disponibles ;
  • Une suppression des objets existants grâce un clic ;
  • Du remplacement automatique lorsqu’on met un objet au dessus d’un autre ;
  • Avoir la possibilité de peindre en continu sans avoir à spammer son clic gauche (R.I.P petite souris);
  • Un système de couche (pour placer des arbres au dessus d’une zone d’herbe par exemple).

Que dire de plus ? N’hésitez pas à nous donner votre avis en commentant, et à la prochaine, on se retrouve pour la partie 2 !

 

Un commentaire

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée Champs requis marqués avec *

Publier des commentaires