Come progettare menu contestuali in base all'oggetto?


21

Sto cercando una soluzione per un comportamento "Opzioni clic destro".

Fondamentalmente qualsiasi oggetto in un gioco, se cliccato con il tasto destro, può visualizzare una serie di opzioni basate su qualunque sia l'oggetto.

Esempi di clic con il tasto destro per diversi scenari :

Inventario: il casco mostra le opzioni (Equipaggia, Usa, Rilascia, Descrizione)

Banca: il casco mostra le opzioni (Prendi 1, Prendi X, Prendi tutto, Descrizione)

Piano: il casco mostra le opzioni (Take, Walk Here, Description)

Ovviamente ogni opzione indica in qualche modo un certo metodo che fa ciò che dice. Questo è parte del problema che sto cercando di capire. Con così tante opzioni di potenziamento per un singolo oggetto, come avrei progettato le mie lezioni in modo tale da non essere estremamente disordinate?

  • Ho pensato all'eredità, ma potrebbe essere molto lungo e la catena potrebbe essere enorme.
  • Ho pensato di utilizzare le interfacce, ma questo probabilmente mi limiterebbe un po 'poiché non sarei in grado di caricare i dati degli articoli da un file Xml e inserirli in una classe "Item" generica.

Sto basando il mio risultato finale desiderato su un gioco chiamato Runescape. Ogni oggetto può essere cliccato con il tasto destro del mouse nel gioco e in base a ciò che è e dove si trova (inventario, piano, banca, ecc.) Visualizza un diverso set di opzioni disponibili per il giocatore con cui interagire.

Come farei per raggiungere questo obiettivo? Quale approccio devo adottare prima di tutto, decidere quali opzioni DOVREBBERO essere visualizzate e una volta cliccate, come chiamare il metodo corrispondente.

Sto usando C # e Unity3D, ma tutti gli esempi forniti non devono essere correlati a nessuno dei due in quanto sto seguendo uno schema al contrario del codice reale.

Qualsiasi aiuto è molto apprezzato e se non sono stato chiaro nella mia domanda o nei risultati desiderati, si prega di inviare un commento e lo farò al più presto.

Ecco cosa ho provato finora:

  • In realtà sono riuscito a implementare una classe "Item" generica che contiene tutti i valori per diversi tipi di oggetti (attacco extra, difesa extra, costo ecc ...). Queste variabili vengono popolate da dati provenienti da un file Xml.
  • Ho pensato di collocare ogni singolo metodo di interazione possibile all'interno della classe Item, ma penso che questa sia incredibilmente disordinata e povera. Probabilmente ho adottato un approccio sbagliato per l'implementazione di questo tipo di sistema usando solo una classe e non sottoclassificando su elementi diversi, ma è l'unico modo in cui posso caricare i dati da un Xml e archiviarli nella classe.
  • Il motivo per cui ho scelto di caricare tutti i miei oggetti da un file Xml è dovuto al fatto che questo gioco ha la possibilità di oltre 40.000 articoli. Se la mia matematica è corretta, una classe per ogni articolo è molte classi.

Guardando la tua lista di comandi, ad eccezione di "Equipaggia", sembra che tutti siano generici e si applichino indipendentemente da quale sia l'oggetto - prendi, rilascia, descrizione, sposta qui, ecc.
ashes999

Se un oggetto non fosse commerciabile, invece di "Rilasciare" potrebbe avere "Distruggi"
Mike Hunt,

Per essere sinceri, molti giochi risolvono questo problema usando un DSL, un linguaggio di scripting personalizzato specifico per il gioco.
corsiKa

1
+1 per modellare il tuo gioco dopo RuneScape. Adoro quel gioco.
Zenadix,

Risposte:


23

Come in tutto lo sviluppo del software, non esiste una soluzione ideale. Solo la soluzione ideale per te e il tuo progetto. Eccone alcuni che potresti usare.

Opzione 1: il modello procedurale

L' antico metodo obsoleto della vecchia scuola.

Tutti gli elementi sono stupidi tipi di dati semplici senza alcun metodo ma molti attributi pubblici che rappresentano tutte le proprietà che un elemento potrebbe avere, inclusi alcuni flag booleani come isEdible, isEquipableecc. Che determinano quali voci del menu contestuale sono disponibili per esso (forse potresti anche fare a meno di questi flag quando è possibile derivarlo dai valori di altri attributi). Avere alcuni metodi come Eat, Equipecc. Nella tua classe di giocatori che prende un oggetto e che ha tutta la logica per elaborarlo in base ai valori degli attributi.

Opzione 2: il modello orientato agli oggetti

Questa è più una soluzione OOP-by-the-book che si basa sull'ereditarietà e sul polimorfismo.

Avere una classe base Itemda cui ereditano altri oggetti come EdibleItem, EquipableItemecc. La classe base dovrebbe avere un metodo pubblico GetContextMenuEntriesForBank, GetContextMenuEntriesForFloorecc. Che restituisce un elenco di ContextMenuEntry. Ogni classe ereditaria sovrascriverà questi metodi per restituire le voci del menu di scelta rapida appropriate per questo tipo di elemento. Potrebbe anche chiamare lo stesso metodo della classe base per ottenere alcune voci predefinite applicabili a qualsiasi tipo di elemento. La ContextMenuEntrysarebbe una classe con un metodo Performche poi chiama il metodo in questione dalla voce che lo ha generato (si può utilizzare un delegato per questo).

Per quanto riguarda i problemi con l'implementazione di questo modello durante la lettura dei dati dal file XML: esaminare innanzitutto il nodo XML per ciascun elemento per determinare il tipo di elemento, quindi utilizzare il codice specializzato per ciascun tipo per creare un'istanza della sottoclasse appropriata.

Opzione 3: il modello basato su componenti

Questo modello utilizza la composizione anziché l'ereditarietà ed è più vicino a come funziona il resto di Unity. A seconda di come strutturi il tuo gioco, potrebbe essere possibile / utile utilizzare il sistema Unity per questo ... oppure no, il tuo chilometraggio può variare.

Ogni oggetto di classe Item avrebbe una lista di componenti come Equipable, Edible, Sellable, Drinkable, ecc Un elemento può avere uno o nessuno di ciascun componente (per esempio, un casco di cioccolato sarebbe sia Equipablee Edible, e quando non è una trama critico oggetto di ricerca anche Sellable). La logica di programmazione specifica per il componente è implementata in quel componente. Quando l'utente fa clic con il pulsante destro del mouse su un elemento, i componenti dell'elemento vengono ripetuti e le voci del menu di scelta rapida vengono aggiunte per ciascun componente esistente. Quando l'utente seleziona una di queste voci, il componente che ha aggiunto quella voce elabora l'opzione.

Puoi rappresentarlo nel tuo file XML disponendo di un sotto-nodo per ciascun componente. Esempio:

   <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>

Grazie per le tue preziose spiegazioni e il tempo che hai impiegato per rispondere alla mia domanda. Anche se non ho ancora deciso quale metodo seguirò, apprezzo i metodi alternativi di implementazione che hai fornito. Mi siederò e penserò a quale metodo funzionerà meglio per me e andrò da lì. Grazie :)
Mike Hunt,

@MikeHunt Il modello dell'elenco dei componenti è sicuramente qualcosa da esaminare, poiché funziona bene con il caricamento delle definizioni degli elementi da un file.
user253751

@immibis è quello che proverò prima perché il mio tentativo iniziale era simile a quello. Grazie :)
Mike Hunt,

Vecchia risposta, ma esiste una documentazione su come implementare un modello di "elenco di componenti"?
Jeff,

@Jeff Se desideri implementare questo schema nel tuo gioco e hai domande su come, per favore pubblica una nuova domanda.
Philipp

9

Quindi, Mike Hunt, la tua domanda mi ha interessato così tanto, ho deciso di implementare una soluzione completa. Dopo tre ore di tentativi diversi, ho finito con questa soluzione dettagliata:

(Nota che questo NON è un ottimo codice, quindi accetterò qualsiasi modifica)

Creazione del pannello dei contenuti

(Questo pannello sarà un contenitore per i nostri pulsanti del menu contestuale)

  • Creare nuovo UI Panel
  • Imposta anchorin basso a sinistra
  • Imposta widthsu 300 (come desideri)
  • Aggiungi a un pannello un nuovo componente Vertical Layout Groupe imposta Child Alignmentin alto al centro, Child Force Expandin larghezza (non in altezza)
  • Aggiungi a un pannello un nuovo componente Content Size Fittere imposta Vertical Fitsu Dimensione minima
  • Salvalo come prefabbricato

(A questo punto il nostro pannello si ridurrà a una linea. È normale. Questo pannello accetterà i pulsanti come bambini, li allineerà verticalmente e si estenderà all'altezza del contenuto di riepilogo)

Creazione del pulsante campione

(Questo pulsante verrà istanziato e personalizzato per mostrare le voci del menu contestuale)

  • Crea nuovo pulsante UI
  • Imposta anchorin alto a sinistra
  • Aggiungi a un pulsante un nuovo componente Layout Element, impostato Min Heightsu 30, Preferred Heightsu 30
  • Salvalo come prefabbricato

Creazione dello script ContextMenu.cs

(Questo script ha un metodo che crea e mostra il menu contestuale)

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);
        }
    }
}
  • Allega questo script a una tela e popola i campi. Trascina ContentPanelprefabbricato nello slot corrispondente e trascina Canvas stesso nello slot Canvas.

Creazione dello 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);
    }
}
  • Crea un oggetto campione nella scena (es. Cube), Posizionalo in modo che sia visibile alla telecamera e allega questo script. Trascina e rilascia sampleButtonprefabbricati nello slot corrispondente.

Ora prova a eseguirlo. Quando fai clic con il pulsante destro del mouse sull'oggetto, dovrebbe apparire il menu di scelta rapida, popolato con l'elenco che abbiamo creato. Premendo i pulsanti, nella console verrà stampato del testo e il menu di scelta rapida verrà distrutto.

Possibili miglioramenti:

  • ancora più generico!
  • migliore gestione della memoria (collegamenti sporchi, pannello non distruttivo, disabilitazione)
  • alcune cose fantasiose

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


Wow, grazie mille per aver dedicato del tempo per implementarlo. Testerò la tua implementazione non appena torno sul mio computer. Penso ai fini della spiegazione, accetterò la risposta di Philipp in base alla sua varietà di spiegazioni per i metodi che possono essere utilizzati. Lascerò la tua risposta qui perché ritengo che sia estremamente prezioso e le persone che vedranno questa domanda in futuro avranno un'implementazione effettiva e alcuni metodi per implementare questo tipo di cose in un gioco. Grazie mille e ben fatto. Ho anche votato questo :)
Mike Hunt,

1
Prego. Sarebbe bello se questa risposta potesse aiutare qualcuno.
Esercizio
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.