Wie gestalte ich Kontextmenüs basierend auf dem Objekt?

21

Ich suche nach einer Lösung für das Verhalten "Rechtsklickoptionen".

Grundsätzlich kann jeder Gegenstand in einem Spiel, wenn mit der rechten Maustaste darauf geklickt wird, eine Reihe von Optionen basierend auf dem Objekt anzeigen.

Rechtsklick-Beispiele für verschiedene Szenarien :

Inventar: Helm zeigt Optionen (Ausrüsten, Benutzen, Ablegen, Beschreibung)

Bank: Helm zeigt Optionen (Take 1, Take X, Take All, Beschreibung)

Etage: Helm zeigt Optionen (Take, Walk Here, Description)

Offensichtlich verweist jede Option irgendwie auf eine bestimmte Methode, die das tut, was gesagt wird. Dies ist ein Teil des Problems, das ich herauszufinden versuche. Wie würde ich bei so vielen Potenzierungsoptionen für einen einzelnen Gegenstand meine Klassen so gestalten, dass sie nicht extrem chaotisch sind?

  • Ich habe über Vererbung nachgedacht, aber das könnte sehr langwierig sein und die Kette könnte riesig sein.
  • Ich habe über die Verwendung von Schnittstellen nachgedacht, aber dies würde mich wahrscheinlich ein wenig einschränken, da ich keine Artikeldaten aus einer XML-Datei laden und in eine generische "Item" -Klasse einfügen könnte.

Ich stütze mein gewünschtes Endergebnis auf ein Spiel namens Runescape. Jedes Objekt kann im Spiel mit der rechten Maustaste angeklickt werden. Abhängig davon, was es ist und wo es sich befindet (Inventar, Etage, Bank usw.), werden dem Spieler verschiedene Optionen zur Interaktion angeboten.

Wie würde ich das erreichen? Wie gehe ich vor? Entscheide, welche Optionen angezeigt werden SOLLTEN und wie die entsprechende Methode aufgerufen werden soll.

Ich verwende C # und Unity3D, aber die bereitgestellten Beispiele müssen sich nicht auf eines von beiden beziehen, da ich nach einem Muster im Gegensatz zum tatsächlichen Code arbeite.

Jede Hilfe wird sehr geschätzt und wenn ich in meiner Frage oder den gewünschten Ergebnissen nicht klar war, schreibe bitte einen Kommentar und ich werde mich so schnell wie möglich darum kümmern.

Folgendes habe ich bisher versucht:

  • Ich habe es tatsächlich geschafft, eine generische "Item" -Klasse zu implementieren, die alle Werte für verschiedene Arten von Gegenständen enthält (zusätzlicher Angriff, zusätzliche Verteidigung, Kosten usw.). Diese Variablen werden mit Daten aus einer XML-Datei gefüllt.
  • Ich habe darüber nachgedacht, jede einzelne mögliche Interaktionsmethode in die Item-Klasse aufzunehmen, aber ich denke, das ist eine unglaublich chaotische und schlechte Form. Ich habe wahrscheinlich den falschen Ansatz gewählt, um diese Art von System zu implementieren, indem ich nur eine Klasse verwendet habe und keine Unterklassen für verschiedene Elemente erstellt habe. Es ist jedoch die einzige Möglichkeit, die Daten aus einer XML-Datei zu laden und in der Klasse zu speichern.
  • Der Grund, warum ich mich dazu entschlossen habe, alle meine Artikel aus einer XML-Datei zu laden, ist, dass dieses Spiel die Möglichkeit hat, mehr als 40.000 Artikel zu speichern. Wenn meine Mathematik stimmt, besteht eine Klasse für jeden Gegenstand aus vielen Klassen.
Mike Hunt
quelle
Wenn Sie sich Ihre
Befehlsliste ansehen
Wenn ein Gegenstand nicht handelbar war, könnte er anstelle von "Drop" "Destroy" haben
Mike Hunt
Um ganz ehrlich zu sein, lösen viele Spiele dieses Problem mithilfe von DSL - einer für das Spiel spezifischen Skriptsprache.
corsiKa
1
+1 für das Modellieren Ihres Spiels nach RuneScape. Ich liebe dieses Spiel.
Zenadix,

Antworten:

23

Wie bei allem in der Softwareentwicklung gibt es keine ideale Lösung. Nur die Lösung, die für Sie und Ihr Projekt optimal ist. Hier sind einige, die Sie verwenden könnten.

Option 1: Das Vorgehensmodell

Die alte veraltete Old-School-Methode.

Alle Einzelteile sind stumm plain-old-Datentypen ohne Methoden , aber viele öffentliche Attribute , die alle Eigenschaften ein Element darstellen könnte, darunter auch einige boolean Flags wie haben isEdible, isEquipableusw. , die bestimmen , welche Kontextmenüeinträge für sie verfügbar sind (vielleicht könnten Sie auch Verzichten Sie auf diese Flags, wenn Sie sie aus den Werten anderer Attribute ableiten können. Habe einige Methoden wie Eat, Equipusw. in Ihrer Player - Klasse , die ein Element nimmt und die alle die Logik es zu verarbeiten hat die Attributwerte nach.

Option 2: Das objektorientierte Modell

Dies ist eher eine OOP-by-the-Book-Lösung, die auf Vererbung und Polymorphismus basiert.

Haben Sie eine Basisklasse , Itemvon denen andere Einzelteile mögen EdibleItem, EquipableItemusw. erben. Die Basisklasse sollte über eine öffentliche Methode usw. verfügen GetContextMenuEntriesForBank, GetContextMenuEntriesForFloordie eine Liste von zurückgibt ContextMenuEntry. Jede übernehmende Klasse würde diese Methoden überschreiben, um die Kontextmenüeinträge zurückzugeben, die für diesen Elementtyp geeignet sind. Es könnte auch dieselbe Methode der Basisklasse aufrufen, um einige Standardeinträge abzurufen, die für jeden Elementtyp gelten. Das ContextMenuEntrywäre eine Klasse mit einer Methode, Performdie dann die entsprechende Methode aus dem Item aufruft, das sie erstellt hat (Sie könnten dafür einen Delegaten verwenden ).

Bezüglich Ihrer Probleme bei der Implementierung dieses Musters beim Lesen von Daten aus der XML-Datei: Untersuchen Sie zunächst den XML-Knoten für jedes Element, um den Elementtyp zu bestimmen, und verwenden Sie dann für jeden Typ speziellen Code, um eine Instanz der entsprechenden Unterklasse zu erstellen.

Option 3: Das komponentenbasierte Modell

Dieses Muster verwendet Komposition anstelle von Vererbung und entspricht eher der Funktionsweise von Unity. Abhängig davon, wie Sie Ihr Spiel strukturieren, kann es möglich / vorteilhaft sein, das Unity-Komponentensystem für diese Aufgabe zu verwenden.

Jedes Objekt der Klasse Itemeine Liste der Komponenten haben würde wie Equipable, Edible, Sellable, Drinkableusw. Ein Element eine oder keines der einzelnen Komponenten (zum Beispiel haben kann, ein Helm aus Schokolade wäre sowohl Equipableund Edible, und , wenn es nicht ein grafische Darstellung kritische Questgegenstand auch Sellable). Die für die Komponente spezifische Programmierlogik ist in dieser Komponente implementiert. Wenn der Benutzer mit der rechten Maustaste auf ein Element klickt, werden die Komponenten des Elements iteriert und Kontextmenüeinträge für jede vorhandene Komponente hinzugefügt. Wenn der Benutzer einen dieser Einträge auswählt, verarbeitet die Komponente, die diesen Eintrag hinzugefügt hat, die Option.

Sie könnten dies in Ihrer XML-Datei darstellen, indem Sie für jede Komponente einen Unterknoten haben. Beispiel:

   <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
quelle
Vielen Dank für Ihre wertvollen Erklärungen und die Zeit, die Sie gebraucht haben, um meine Frage zu beantworten. Obwohl ich mich noch nicht für eine Methode entschieden habe, schätze ich die alternativen Implementierungsmethoden, die Sie zur Verfügung gestellt haben. Ich werde mich hinsetzen und überlegen, welche Methode für mich besser funktionieren wird und von dort aus gehen. Vielen Dank :)
Mike Hunt
@MikeHunt Das Modell mit der Liste der Komponenten sollten Sie unbedingt untersuchen, da es sich gut zum Laden von Elementdefinitionen aus einer Datei eignet.
user253751
@immibis das ist es, was ich zuerst ausprobieren werde, da mein anfänglicher Versuch ähnlich war. Danke :)
Mike Hunt
Alte Antwort, aber gibt es eine Dokumentation zur Implementierung eines Modells mit einer Liste von Komponenten?
Jeff,
@ Jeff Wenn Sie dieses Muster in Ihr Spiel implementieren möchten und Fragen dazu haben, posten Sie bitte eine neue Frage.
Philipp
9

Also, Mike Hunt, Ihre Frage hat mich so interessiert, dass ich beschlossen habe, eine vollständige Lösung zu implementieren. Nachdem ich drei Stunden lang verschiedene Dinge ausprobiert hatte, endete ich mit dieser schrittweisen Lösung:

(Bitte beachten Sie, dass dies KEIN sehr guter Code ist, so dass ich alle Änderungen akzeptieren werde.)

Inhaltsbereich erstellen

(Dieser Bereich ist ein Container für unsere Kontextmenü-Schaltflächen.)

  • Erstelle neu UI Panel
  • Set anchornach unten-links
  • Stellen Sie widthauf 300 ein (wie Sie wünschen)
  • Hinzufügen einer neuen Komponente zu einem Bedienfeld Vertical Layout Groupund Festlegen der Breite (nicht der Höhe) Child Alignmentin der oberen MitteChild Force Expand
  • Fügen Sie einer Kontrollleiste eine neue Komponente hinzu Content Size Fitterund setzen Sie sie Vertical Fitauf Min. Größe
  • Speichern Sie es als Fertighaus

(An diesem Punkt wird unser Bedienfeld auf eine Linie verkleinert. Es ist normal. In diesem Bedienfeld werden Schaltflächen als untergeordnete Elemente akzeptiert, vertikal ausgerichtet und auf die Höhe des Zusammenfassungsinhalts gedehnt.)

Beispielschaltfläche erstellen

(Diese Schaltfläche wird instanziiert und angepasst, um Kontextmenüelemente anzuzeigen.)

  • Neue UI-Schaltfläche erstellen
  • Set anchornach oben links
  • Fügen Sie einer Schaltfläche eine neue Komponente hinzu Layout Element, die Min Heightauf 30 und Preferred Height30 gesetzt ist
  • Speichern Sie es als Fertighaus

Erstellen eines ContextMenu.cs-Skripts

(Dieses Skript verfügt über eine Methode zum Erstellen und Anzeigen des Kontextmenüs.)

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);
        }
    }
}
  • Hängen Sie dieses Skript an eine Zeichenfläche an und füllen Sie die Felder aus. Ziehen Sie das ContentPanelPrefab per Drag & Drop auf den entsprechenden Steckplatz und ziehen Sie den Canvas-Bereich auf den Steckplatz Canvas.

Script ItemController.cs erstellen

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);
    }
}
  • Erstellen Sie ein Beispielobjekt in der Szene (dh Cube), platzieren Sie es so, dass es für die Kamera sichtbar ist, und hängen Sie dieses Skript daran an. sampleButtonFertighaus per Drag & Drop in den entsprechenden Steckplatz ziehen.

Versuchen Sie nun, es auszuführen. Wenn Sie mit der rechten Maustaste auf das Objekt klicken, sollte das Kontextmenü mit der von uns erstellten Liste angezeigt werden. Durch Drücken der Tasten wird Text in die Konsole gedruckt und das Kontextmenü wird zerstört.

Mögliche Verbesserungen:

  • noch generischer!
  • Bessere Speicherverwaltung (Dirty Links, Panel nicht zerstören, deaktivieren)
  • einige ausgefallene Sachen

Beispielprojekt (Unity Personal 5.2.0, VisualStudio Plugin): https://drive.google.com/file/d/0B7iGjyVbWvFwUnRQRVVaOGdDc2M/view?usp=sharing

Übung
quelle
Wow, vielen Dank, dass Sie sich die Zeit genommen haben, dies umzusetzen. Ich werde Ihre Implementierung testen, sobald ich wieder auf meinem Computer bin. Ich denke, zum Zwecke der Erklärung akzeptiere ich Philipps Antwort auf der Grundlage seiner Vielzahl von Erklärungen für Methoden, die verwendet werden können. Ich werde Ihre Antwort hier lassen, weil ich glaube, dass es äußerst wertvoll ist und die Leute, die diese Frage in Zukunft sehen, eine tatsächliche Implementierung sowie einige Methoden zur Implementierung solcher Dinge in einem Spiel haben werden. Vielen Dank und gut gemacht. Ich habe auch darüber abgestimmt :)
Mike Hunt
1
Bitte. Es wäre toll, wenn diese Antwort jemandem helfen würde.
Exerion