Jak zaprojektować menu kontekstowe na podstawie dowolnego obiektu?

21

Szukam rozwiązania problemu „Opcje kliknięcia prawym przyciskiem”.

Zasadniczo każdy element w grze, po kliknięciu prawym przyciskiem myszy, może wyświetlić zestaw opcji w zależności od tego, czym jest obiekt.

Przykłady kliknięcia prawym przyciskiem myszy dla różnych scenariuszy :

Inventory: Helmet pokazuje opcje (Equip, Use, Drop, Description)

Bank: Hełm pokazuje opcje (Weź 1, Weź X, Weź wszystko, Opis)

Piętro: kask pokazuje opcje (Take, Walk Here, Description)

Oczywiście każda opcja w jakiś sposób wskazuje na pewną metodę, która robi to, co mówi. Jest to część problemu, który próbuję rozwiązać. Przy tak wielu opcjach poprawiania pojedynczego przedmiotu, jak miałbym tak zaprojektować moje zajęcia, aby nie były wyjątkowo niechlujne?

  • Myślałem o dziedziczeniu, ale mogło to być naprawdę długie i łańcuch może być ogromny.
  • Myślałem o użyciu interfejsów, ale prawdopodobnie ograniczyłoby to mnie trochę, ponieważ nie byłbym w stanie załadować danych elementu z pliku Xml i umieścić go w ogólnej klasie „Element”.

Opieram pożądany wynik końcowy na grze o nazwie Runescape. Każdy obiekt można kliknąć prawym przyciskiem myszy w grze i w zależności od tego, co to jest i gdzie to jest (ekwipunek, piętro, bank itp.) Wyświetla inny zestaw opcji dostępnych dla gracza do interakcji.

Jak miałbym to osiągnąć? Jakie podejście powinienem zastosować przede wszystkim, zdecydować, które POWINNY zostać wyświetlone, a po kliknięciu, jak wywołać odpowiednią metodę.

Używam C # i Unity3D, ale wszelkie podane przykłady nie muszą być powiązane z żadnym z nich, ponieważ szukam wzorca w przeciwieństwie do rzeczywistego kodu.

Jakakolwiek pomoc jest mile widziana, a jeśli nie wyraziłem się jasno w swoim pytaniu lub oczekiwanych wynikach, proszę o komentarz, a ja postaram się jak najszybciej.

Oto, co dotychczas próbowałem:

  • W rzeczywistości udało mi się zaimplementować ogólną klasę „Przedmiot”, która przechowuje wszystkie wartości dla różnych rodzajów przedmiotów (dodatkowy atak, dodatkowa obrona, koszt itp.). Te zmienne są zapełniane danymi z pliku Xml.
  • Myślałem o umieszczeniu każdej możliwej metody interakcji w klasie przedmiotów, ale myślę, że jest to niewiarygodnie niechlujna i zła forma. Prawdopodobnie podjąłem niewłaściwe podejście do wdrażania tego rodzaju systemu, używając tylko jednej klasy, a nie podklasowania do różnych elementów, ale jest to jedyny sposób, w jaki mogę załadować dane z Xml i zapisać je w klasie.
  • Powodem, dla którego zdecydowałem się załadować wszystkie moje elementy z pliku Xml, jest to, że ta gra ma możliwość ponad 40 000 przedmiotów. Jeśli moja matematyka jest poprawna, klasa dla każdego przedmiotu to wiele klas.
Mike Hunt
źródło
Patrząc na listę poleceń, z wyjątkiem „Wyposażenia”, wydaje się, że wszystkie są ogólne i obowiązują niezależnie od tego, czym jest przedmiot - weź, upuść, opis, przenieś się tutaj itp.
ashes999
Jeśli przedmiotu nie można wymienić, zamiast „Upuść” może on mieć „Zniszczenie”
Mike Hunt
Mówiąc szczerze, wiele gier rozwiązuje ten problem za pomocą DSL - niestandardowego języka skryptowego specyficznego dla gry.
corsiKa
1
+1 za modelowanie gry według RuneScape. Kocham tę grę.
Zenadix

Odpowiedzi:

23

Podobnie jak w przypadku wszystkich elementów związanych z tworzeniem oprogramowania, nie ma idealnego rozwiązania. Tylko rozwiązanie idealne dla Ciebie i Twojego projektu. Oto niektóre, których możesz użyć.

Opcja 1: model proceduralny

Starożytny przestarzała metoda starej szkoły.

Wszystkie elementy są głupimi, zwykłymi, starymi typami danych bez żadnych metod, ale wiele publicznych atrybutów, które reprezentują wszystkie właściwości, które może mieć element, w tym niektóre flagi boolowskie isEdible, isEquipableitp., Które określają, jakie pozycje menu kontekstowego są dla niego dostępne (być może możesz również obejść się bez tych flag, jeśli można je wywnioskować z wartości innych atrybutów). Mają pewne metody, takie jak Eat, Equipitd. W klasie gracza, który zajmuje pozycję i który posiada wszystkie logiki przetwarzać zgodnie z wartościami atrybutów.

Opcja 2: model obiektowy

Jest to bardziej rozwiązanie OOP-by-the-book oparte na dziedziczeniu i polimorfizmie.

Mają bazową klasy Item, z której inne przedmioty, takie jak EdibleItem, EquipableItemitd dziedziczenia. Klasa bazowa powinna mieć metodę publiczną GetContextMenuEntriesForBank, GetContextMenuEntriesForFlooritp, które zwróci listę ContextMenuEntry. Każda dziedzicząca klasa zastąpiłaby te metody, aby zwrócić pozycje menu kontekstowego odpowiednie dla tego typu elementu. Może również wywołać tę samą metodę klasy podstawowej, aby uzyskać domyślne wpisy, które można zastosować do dowolnego typu elementu. ContextMenuEntryByłaby klasa z metodą Perform, która następnie wywołuje odpowiednią metodę z elementu, który ją stworzył (można użyć delegata do tego).

Jeśli chodzi o problemy z implementacją tego wzorca podczas odczytywania danych z pliku XML: Najpierw sprawdź węzeł XML dla każdego elementu, aby określić typ elementu, a następnie użyj specjalnego kodu dla każdego typu, aby utworzyć instancję odpowiedniej podklasy.

Opcja 3: model oparty na komponentach

Ten wzór wykorzystuje kompozycję zamiast dziedziczenia i jest bliższy temu, jak działa reszta Jedności. W zależności od tego, jak ustrukturyzujesz swoją grę, może być możliwe / korzystne użycie systemu komponentów Unity do tego ... lub nie, twój przebieg może się różnić.

Każdy obiekt klasy Itemmusiałaby listę składników jak Equipable, Edible, Sellable, Drinkable, itd. Element może mieć jeden lub żaden z każdego komponentu (na przykład kask wykonany z czekolady byłoby zarówno Equipablea Edible, a gdy nie jest to działka o znaczeniu krytycznym przedmiot zadania równieżSellable ). Logika programowania specyficzna dla komponentu jest zaimplementowana w tym komponencie. Gdy użytkownik kliknie element prawym przyciskiem myszy, elementy elementu są iterowane i dla każdego istniejącego elementu dodawane są pozycje menu kontekstowego. Gdy użytkownik wybierze jeden z tych wpisów, składnik, który dodał ten wpis, przetwarza opcję.

Możesz to przedstawić w pliku XML, mając podwęzeł dla każdego komponentu. Przykład:

   <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
źródło
Dziękuję za cenne wyjaśnienia i czas poświęcony na odpowiedź na moje pytanie. Chociaż nie zdecydowałem jeszcze, którą metodą pójdę, doceniam alternatywne metody implementacji, które podałeś. Usiądę i zastanowię się, która metoda będzie dla mnie lepsza, i zacznę odtąd. Dzięki :)
Mike Hunt
@MikeHunt Model listy komponentów jest zdecydowanie czymś, co powinieneś zbadać, ponieważ działa ładnie z ładowaniem definicji elementów z pliku.
user253751
@immibis właśnie tego spróbuję najpierw, ponieważ moja pierwsza próba była podobna do tej. Dzięki :)
Mike Hunt
Stara odpowiedź, ale czy istnieje dokumentacja dotycząca sposobu wdrażania modelu „listy składników”?
Jeff
@Jeff Jeśli chcesz wdrożyć ten wzór w swojej grze i masz jakieś pytania, jak to zrobić, napisz nowe pytanie.
Philipp
9

Mike Hunt, twoje pytanie mnie tak zainteresowało, postanowiłem wdrożyć pełne rozwiązanie. Po trzech godzinach próbowania różnych rzeczy, skończyłem z tym krok po kroku:

(Należy pamiętać, że NIE JEST to bardzo dobry kod, więc zaakceptuję wszelkie zmiany)

Tworzenie panelu zawartości

(Ten panel będzie pojemnikiem na nasze przyciski menu kontekstowego)

  • Tworzyć nowe UI Panel
  • Ustaw anchorw lewym dolnym rogu
  • Ustaw widthna 300 (jak chcesz)
  • Dodaj do panelu nowy komponent Vertical Layout Groupi ustaw Child Alignmentna górną środkową, Child Force Expandna szerokość (nie wysokość)
  • Dodaj do panelu nowy komponent Content Size Fitteri ustaw Vertical Fitna Min. Rozmiar
  • Zapisz jako prefabrykat

(W tym momencie nasz Panel zmniejszy się do linii. To normalne. Ten panel akceptuje przyciski jako dzieci, wyrównuje je w pionie i rozciąga do wysokości zawartości podsumowania)

Tworzenie przycisku próbki

(Ten przycisk zostanie utworzony i dostosowany, aby wyświetlać elementy menu kontekstowego)

  • Utwórz nowy przycisk interfejsu użytkownika
  • Ustaw anchorw lewym górnym rogu
  • Dodaj do przycisku nowy komponent Layout Element, ustawiony Min Heightna 30, Preferred Heightna 30
  • Zapisz jako prefabrykat

Tworzenie skryptu ContextMenu.cs

(Ten skrypt ma metodę, która tworzy i pokazuje menu kontekstowe)

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);
        }
    }
}
  • Dołącz ten skrypt do obszaru roboczego i wypełnij pola. Przeciągnij i upuść ContentPanelprefabrykat do odpowiedniego gniazda i przeciągnij płótno do gniazda Canvas.

Tworzenie skryptu 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);
    }
}
  • Utwórz przykładowy obiekt w scenie (tj. Cube), Umieść go tak, aby był widoczny dla kamery i dołącz do niego ten skrypt. Przeciągnij i upuść sampleButtonprefabrykat do odpowiedniego gniazda.

Teraz spróbuj go uruchomić. Po kliknięciu obiektu prawym przyciskiem myszy powinno pojawić się menu kontekstowe z zapełnioną przez nas listą. Naciśnięcie przycisków spowoduje wydrukowanie tekstu na konsoli, a menu kontekstowe zostanie zniszczone.

Możliwe ulepszenia:

  • jeszcze bardziej ogólny!
  • lepsze zarządzanie pamięcią (brudne linki, nie niszcząc panelu, wyłączając)
  • jakieś wymyślne rzeczy

Przykładowy projekt (Unity Personal 5.2.0, wtyczka VisualStudio): https://drive.google.com/file/d/0B7iGjyVbWvFwUnRQRVVaOGdDc2M/view?usp=sharing

Ćwiczenia
źródło
Wow, dziękuję bardzo za poświęcenie czasu na wdrożenie tego. Będę testować twoje wdrożenie, jak tylko wrócę na mój komputer. Myślę, że w celu wyjaśnienia przyjmuję odpowiedź Filipa w oparciu o różnorodność wyjaśnień dotyczących metod, które można zastosować. Zostawię tutaj twoją odpowiedź, ponieważ uważam, że jest ona niezwykle cenna, a osoby przeglądające to pytanie w przyszłości będą miały rzeczywistą implementację, a także pewne metody implementacji tego rodzaju rzeczy w grze. Dziękuję bardzo i dobrze zrobione. Głosowałem również za tym :)
Mike Hunt
1
Nie ma za co. Byłoby wspaniale, gdyby ta odpowiedź komuś pomogła.
Ćwiczenie