Comment concevoir des menus contextuels basés sur quel que soit l'objet?

21

Je recherche une solution pour un comportement "Options de clic droit".

Fondamentalement, chaque élément d'un jeu, lorsqu'il est cliqué avec le bouton droit, peut afficher un ensemble d'options en fonction de l'objet.

Exemples de clic droit pour différents scénarios :

Inventaire: le casque affiche les options (équiper, utiliser, déposer, description)

Banque: le casque affiche les options (Take 1, Take X, Take All, Description)

Étage: le casque affiche les options (prendre, marcher ici, description)

De toute évidence, chaque option pointe en quelque sorte vers une certaine méthode qui fait ce qui est dit. Cela fait partie du problème que j'essaie de comprendre. Avec autant d'options de potention pour un seul article, comment pourrais-je avoir mes classes conçues de manière à ne pas être extrêmement désordonnées?

  • J'ai pensé à l'héritage mais cela pourrait être très long et la chaîne pourrait être énorme.
  • J'ai pensé à utiliser des interfaces, mais cela me limiterait probablement un peu car je ne serais pas en mesure de charger les données d'élément à partir d'un fichier Xml et de les placer dans une classe générique "Élément".

Je fonde mon résultat final souhaité sur un jeu appelé Runescape. Chaque objet peut être cliqué avec le bouton droit dans le jeu et selon ce qu'il est et où il se trouve (inventaire, sol, banque, etc.) affiche un ensemble différent d'options disponibles pour le joueur avec lequel interagir.

Comment pourrais-je y parvenir? Quelle approche dois-je adopter pour tout d'abord, décider quelles options DEVRAIENT être affichées et une fois cliqué, comment appeler la méthode correspondante.

J'utilise C # et Unity3D, mais aucun des exemples fournis ne doit être lié à l'un ou l'autre car je suis après un modèle par opposition au code réel.

Toute aide est très appréciée et si je n'ai pas été clair dans ma question ou les résultats souhaités, veuillez poster un commentaire et je m'y attacherai dès que possible.

Voici ce que j'ai essayé jusqu'à présent:

  • J'ai en fait réussi à implémenter une classe "Item" générique qui contient toutes les valeurs pour différents types d'objets (attaque supplémentaire, défense supplémentaire, coût etc ...). Ces variables sont remplies par les données d'un fichier Xml.
  • J'ai pensé à placer toutes les méthodes d'interaction possibles à l'intérieur de la classe Item mais je pense que c'est une forme incroyablement désordonnée et pauvre. J'ai probablement adopté la mauvaise approche pour implémenter ce type de système en utilisant uniquement une seule classe et non une sous-classification dans différents éléments, mais c'est la seule façon de charger les données à partir d'un fichier XML et de les stocker dans la classe.
  • La raison pour laquelle j'ai choisi de charger tous mes articles à partir d'un fichier Xml est due au fait que ce jeu a la possibilité de plus de 40000 articles. Si mes calculs sont corrects, une classe pour chaque élément est un grand nombre de classes.
Mike Hunt
la source
En regardant votre liste de commandes, à l'exception de "Equip", il semble que toutes soient génériques et s'appliquent quel que soit l'élément - prendre, déposer, décrire, déplacer ici, etc.
ashes999
Si un objet n'était pas échangeable, au lieu de "Drop", il pourrait avoir "Destroy"
Mike Hunt
Pour être parfaitement franc, de nombreux jeux résolvent ce problème en utilisant une DSL - un langage de script personnalisé spécifique au jeu.
corsiKa
1
+1 pour modéliser votre jeu après RuneScape. J'adore ce jeu.
Zenadix

Réponses:

23

Comme pour tout dans le développement de logiciels, il n'y a pas de solution idéale. Seule la solution idéale pour vous et votre projet. Voici quelques exemples que vous pourriez utiliser.

Option 1: le modèle procédural

L' ancienne méthode obsolète de la vieille école.

Tous les éléments sont des types de données simples et anciens sans aucune méthode, mais de nombreux attributs publics qui représentent toutes les propriétés qu'un élément pourrait avoir, y compris certains indicateurs booléens comme isEdible, isEquipableetc., qui déterminent les entrées de menu contextuel disponibles pour lui (peut-être pourriez-vous également se passer de ces drapeaux lorsque vous pouvez le dériver des valeurs d'autres attributs). Ayez quelques méthodes comme Eat, Equipetc. dans votre classe de joueur qui prend un élément et qui a toute la logique pour le traiter selon les valeurs d'attribut.

Option 2: le modèle orienté objet

Il s'agit plus d'une solution OOP-by-the-book qui est basée sur l'héritage et le polymorphisme.

Avoir une classe Itemde base dont héritent d' autres éléments comme EdibleItem, EquipableItemetc. La classe de base doit avoir une méthode publique GetContextMenuEntriesForBank, GetContextMenuEntriesForFlooretc. qui retourne une liste de ContextMenuEntry. Chaque classe héritée remplacerait ces méthodes pour renvoyer les entrées du menu contextuel appropriées pour ce type d'élément. Il peut également appeler la même méthode de la classe de base pour obtenir des entrées par défaut applicables à tout type d'élément. Le ContextMenuEntryserait une classe avec une méthode Performqui appelle ensuite la méthode appropriée à partir de l'élément qui l'a créé (vous pouvez utiliser un délégué pour cela).

Concernant vos problèmes d'implémentation de ce modèle lors de la lecture des données du fichier XML: Examinez d'abord le nœud XML de chaque élément pour déterminer le type d'élément, puis utilisez un code spécialisé pour chaque type pour créer une instance de la sous-classe appropriée.

Option 3: le modèle basé sur les composants

Ce modèle utilise la composition au lieu de l'héritage et est plus proche de la façon dont le reste d'Unity fonctionne. Selon la façon dont vous structurez votre jeu, il peut être possible / avantageux d'utiliser le système de composants Unity pour cela ... ou non, votre kilométrage peut varier.

Chaque objet de classe Itemaurait une liste des composants comme Equipable, Edible, Sellable, Drinkable, etc. Un élément peut avoir une ou aucune de chaque composant (par exemple, un casque en chocolat serait à la fois Equipableet Edible, quand il n'est pas un complot critique objet de quête également Sellable). La logique de programmation propre au composant est implémentée dans ce composant. Lorsque l'utilisateur clique avec le bouton droit sur un élément, les composants de l'élément sont itérés et des entrées de menu contextuel sont ajoutées pour chaque composant existant. Lorsque l'utilisateur sélectionne l'une de ces entrées, le composant qui a ajouté cette entrée traite l'option.

Vous pouvez représenter cela dans votre fichier XML en ayant un sous-nœud pour chaque composant. Exemple:

   <item>
      <name>Chocolate Helmet</name>
      <sprite>helmet-chocolate.png</sprite>
      <description>Protects you from enemies and from starving</description>
      <edible>
          <taste>sweet</taste>
          <calories>2560</calories>
      </edible>
      <equipable>
          <slot>head</slot>
          <def>20</def>
      </equipable>
      <sellable>
          <value>120</value>
      </sellable>
   </item>
Philipp
la source
Merci pour vos précieuses explications et le temps que vous avez pris pour répondre à ma question. Bien que je n'aie pas encore décidé de la méthode à utiliser, j'apprécie les autres méthodes de mise en œuvre que vous avez fournies. Je vais m'asseoir et réfléchir à quelle méthode fonctionnera mieux pour moi et partir de là. Merci :)
Mike Hunt
@MikeHunt Le modèle de liste de composants est certainement quelque chose que vous devriez étudier, car il fonctionne bien avec le chargement des définitions d'élément à partir d'un fichier.
user253751
@immibis, c'est ce que je vais essayer en premier car ma première tentative était similaire à cela. Merci :)
Mike Hunt
Ancienne réponse, mais existe-t-il une documentation sur la façon d'implémenter un modèle de "liste de composants"?
Jeff
@Jeff Si vous souhaitez implémenter ce modèle dans votre jeu et avez des questions sur la façon de le faire, veuillez poster une nouvelle question.
Philipp
9

Donc, Mike Hunt, votre question m'intéressait tellement, j'ai décidé de mettre en œuvre une solution complète. Après trois heures à essayer différentes choses, je me suis retrouvé avec cette solution étape par étape:

(Veuillez noter que ce n'est PAS un très bon code, donc j'accepterai toutes les modifications)

Création du panneau de contenu

(Ce panneau sera un conteneur pour nos boutons de menu contextuel)

  • Créer un nouveau UI Panel
  • Définir anchoren bas à gauche
  • Réglez widthà 300 (comme vous le souhaitez)
  • Ajouter à un panneau un nouveau composant Vertical Layout Groupet définir Child Alignmenten haut au centre, Child Force Expandsur largeur (pas sur hauteur)
  • Ajouter à un panneau un nouveau composant Content Size Fitteret définir la Vertical Fittaille minimale
  • Enregistrez-le comme préfabriqué

(À ce stade, notre panneau se réduira à une ligne. C'est normal. Ce panneau acceptera les boutons comme des enfants, les alignera verticalement et s'étirera à la hauteur du contenu résumé)

Création d'un exemple de bouton

(Ce bouton sera instancié et personnalisé pour afficher les éléments du menu contextuel)

  • Créer un nouveau bouton d'interface utilisateur
  • Définir anchoren haut à gauche
  • Ajouter à un bouton un nouveau composant Layout Element, défini Min Heightsur 30, Preferred Heightsur 30
  • Enregistrez-le comme préfabriqué

Création d'un script ContextMenu.cs

(Ce script a une méthode qui crée et affiche le menu contextuel)

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

[System.Serializable]
public class ContextMenuItem
{
    // this class - just a box to some data

    public string text;             // text to display on button
    public Button button;           // sample button prefab
    public Action<Image> action;    // delegate to method that needs to be executed when button is clicked

    public ContextMenuItem(string text, Button button, Action<Image> action)
    {
        this.text = text;
        this.button = button;
        this.action = action;
    }
}

public class ContextMenu : MonoBehaviour
{
    public Image contentPanel;              // content panel prefab
    public Canvas canvas;                   // link to main canvas, where will be Context Menu

    private static ContextMenu instance;    // some kind of singleton here

    public static ContextMenu Instance
    {
        get
        {
            if(instance == null)
            {
                instance = FindObjectOfType(typeof(ContextMenu)) as ContextMenu;
                if(instance == null)
                {
                    instance = new ContextMenu();
                }
            }
            return instance;
        }
    }

    public void CreateContextMenu(List<ContextMenuItem> items, Vector2 position)
    {
        // here we are creating and displaying Context Menu

        Image panel = Instantiate(contentPanel, new Vector3(position.x, position.y, 0), Quaternion.identity) as Image;
        panel.transform.SetParent(canvas.transform);
        panel.transform.SetAsLastSibling();
        panel.rectTransform.anchoredPosition = position;

        foreach(var item in items)
        {
            ContextMenuItem tempReference = item;
            Button button = Instantiate(item.button) as Button;
            Text buttonText = button.GetComponentInChildren(typeof(Text)) as Text;
            buttonText.text = item.text;
            button.onClick.AddListener(delegate { tempReference.action(panel); });
            button.transform.SetParent(panel.transform);
        }
    }
}
  • Attachez ce script à un canevas et remplissez les champs. Glissez-déposez le ContentPanelpréfabriqué dans l'emplacement correspondant et faites glisser Canvas lui-même vers l'emplacement Canvas.

Création d'un script ItemController.cs

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class ItemController : MonoBehaviour
{
    public Button sampleButton;                         // sample button prefab
    private List<ContextMenuItem> contextMenuItems;     // list of items in menu

    void Awake()
    {
        // Here we are creating and populating our future Context Menu.
        // I do it in Awake once, but as you can see, 
        // it can be edited at runtime anywhere and anytime.

        contextMenuItems = new List<ContextMenuItem>();
        Action<Image> equip = new Action<Image>(EquipAction);
        Action<Image> use = new Action<Image>(UseAction);
        Action<Image> drop = new Action<Image>(DropAction);

        contextMenuItems.Add(new ContextMenuItem("Equip", sampleButton, equip));
        contextMenuItems.Add(new ContextMenuItem("Use", sampleButton, use));
        contextMenuItems.Add(new ContextMenuItem("Drop", sampleButton, drop));
    }

    void OnMouseOver()
    {
        if(Input.GetMouseButtonDown(1))
        {
            Vector3 pos = Camera.main.WorldToScreenPoint(transform.position);
            ContextMenu.Instance.CreateContextMenu(contextMenuItems, new Vector2(pos.x, pos.y));
        }

    }

    void EquipAction(Image contextPanel)
    {
        Debug.Log("Equipped");
        Destroy(contextPanel.gameObject);
    }

    void UseAction(Image contextPanel)
    {
        Debug.Log("Used");
        Destroy(contextPanel.gameObject);
    }

    void DropAction(Image contextPanel)
    {
        Debug.Log("Dropped");
        Destroy(contextPanel.gameObject);
    }
}
  • Créez un exemple d'objet dans la scène (c.-à-d. Cube), Placez-le pour qu'il soit visible par la caméra et attachez-y ce script. Glissez-déposez le sampleButtonpréfabriqué dans l'emplacement correspondant.

Maintenant, essayez de l'exécuter. Lorsque vous cliquez avec le bouton droit sur l'objet, le menu contextuel doit apparaître, rempli avec la liste que nous avons créée. Appuyez sur les boutons pour imprimer dans la console du texte et le menu contextuel sera détruit.

Améliorations possibles:

  • encore plus générique!
  • meilleure gestion de la mémoire (liens sales, ne pas détruire le panneau, désactivation)
  • quelques trucs de fantaisie

Exemple de projet (Unity Personal 5.2.0, plug-in VisualStudio): https://drive.google.com/file/d/0B7iGjyVbWvFwUnRQRVVaOGdDc2M/view?usp=sharing

Exerion
la source
Wow merci beaucoup d'avoir pris le temps de votre journée pour mettre en œuvre cela. Je testerai votre implémentation dès que je serai de retour sur mon ordinateur. Je pense qu'à des fins d'explication, j'accepterai la réponse de Philipp sur la base de sa variété d'explications sur les méthodes qui peuvent être utilisées. Je vais laisser votre réponse ici parce que je pense qu'elle est extrêmement précieuse et que les personnes qui verront cette question à l'avenir auront une implémentation réelle ainsi que des méthodes pour implémenter ce genre de chose dans un jeu. Merci beaucoup et bravo. J'ai également voté cela :)
Mike Hunt
1
Je vous en prie. Ce serait formidable si cette réponse aiderait quelqu'un.
Exerion