¿Cómo diseñar menús contextuales basados ​​en el objeto que sea?

21

Estoy buscando una solución para el comportamiento de "Opciones de clic derecho".

Básicamente, cualquier elemento de un juego, cuando se hace clic con el botón derecho, puede mostrar un conjunto de opciones en función del objeto que sea.

Ejemplos de clic derecho para diferentes escenarios :

Inventario: el casco muestra opciones (equipar, usar, soltar, descripción)

Banco: el casco muestra opciones (Take 1, Take X, Take All, Descripción)

Piso: el casco muestra opciones (tomar, caminar aquí, descripción)

Obviamente, cada opción apunta de alguna manera a un cierto método que hace lo que dice. Esto es parte del problema que estoy tratando de resolver. Con tantas opciones de potencia para un solo elemento, ¿cómo diseñaría mis clases de tal manera que no sean extremadamente desordenadas?

  • He pensado en la herencia, pero eso podría ser muy largo y la cadena podría ser enorme.
  • He pensado en usar interfaces, pero esto probablemente me restringiría un poco, ya que no podría cargar datos de elementos de un archivo Xml y colocarlos en una clase genérica de "Elementos".

Estoy basando mi resultado final deseado en un juego llamado Runescape. Se puede hacer clic derecho en cada objeto en el juego y, según lo que sea y dónde esté (inventario, piso, banco, etc.) muestra un conjunto diferente de opciones disponibles para que el jugador interactúe.

¿Cómo haría para lograr esto? En primer lugar, qué enfoque debo tomar, decidir qué opciones DEBERÍAN mostrarse y, una vez que se haga clic, cómo llamar al método correspondiente.

Estoy usando C # y Unity3D, pero cualquier ejemplo proporcionado no tiene que estar relacionado con ninguno de ellos, ya que busco un patrón en lugar del código real.

Cualquier ayuda es muy apreciada y si no he sido claro en mi pregunta o en los resultados deseados, publique un comentario y lo atenderé lo antes posible.

Esto es lo que he intentado hasta ahora:

  • De hecho, he logrado implementar una clase genérica de "Objeto" que contiene todos los valores para diferentes tipos de artículos (ataque adicional, defensa adicional, costo, etc.). Estas variables se rellenan con datos de un archivo Xml.
  • He pensado en colocar todos los métodos de interacción posibles dentro de la clase Item, pero creo que esta es una forma increíblemente desordenada y pobre. Probablemente he tomado el enfoque equivocado para implementar este tipo de sistema al usar solo una clase y no subclasificar a diferentes elementos, pero es la única forma en que puedo cargar los datos de un Xml y almacenarlos en la clase.
  • La razón por la que elegí cargar todos mis artículos desde un archivo Xml se debe a que este juego tiene la posibilidad de más de 40,000 artículos. Si mis cálculos son correctos, una clase para cada artículo es muchas clases.
Mike Hunt
fuente
Mirando su lista de comandos, con la excepción de "Equipar", parece que todos ellos son genéricos y se aplican independientemente de cuál sea el elemento: tomar, soltar, descripción, mover aquí, etc.
cenizas999
Si un artículo no se puede intercambiar, en lugar de "Soltar" podría tener "Destruir"
Mike Hunt
Para ser franco, muchos juegos resuelven esto usando un DSL, un lenguaje de scripting personalizado específico para el juego.
corsiKa
1
+1 para modelar tu juego después de RuneScape. Amo ese juego
Zenadix

Respuestas:

23

Como con todo en el desarrollo de software, no existe una solución ideal. Solo la solución ideal para usted y su proyecto. Aquí hay algunos que podrías usar.

Opción 1: el modelo de procedimiento

El antiguo método obsoleto de la vieja escuela.

Todos los elementos son tipos simples de datos antiguos sin ningún método, pero con muchos atributos públicos que representan todas las propiedades que un elemento podría tener, incluidas algunas banderas booleanas como isEdible, isEquipableetc. , que determinan qué entradas del menú contextual están disponibles para él (tal vez también podría prescindir de estos indicadores cuando puede derivarlo de los valores de otros atributos). Tenga algunos métodos como Eat, Equipetc. en su clase de jugador que toma un elemento y que tiene toda la lógica para procesarlo de acuerdo con los valores de los atributos.

Opción 2: el modelo orientado a objetos

Esta es más una solución OOP por libro que se basa en la herencia y el polimorfismo.

Tener una clase base Itemde la que hereden otros elementos como EdibleItem, EquipableItemetc. La clase base debe tener un método público GetContextMenuEntriesForBank, GetContextMenuEntriesForFlooretc. que devuelva una lista de ContextMenuEntry. Cada clase heredado anularía estos métodos para devolver las entradas del menú contextual que sean apropiadas para este tipo de elemento. También podría llamar al mismo método de la clase base para obtener algunas entradas predeterminadas que son aplicables para cualquier tipo de elemento. La ContextMenuEntryhabría una clase con un método Performque a su vez llama al método pertinente del artículo que lo creó (se puede utilizar un delegado para esto).

Con respecto a sus problemas con la implementación de este patrón al leer datos del archivo XML: Primero examine el nodo XML para cada elemento para determinar el tipo de elemento, luego use un código especializado para cada tipo para crear una instancia de la subclase apropiada.

Opción 3: el modelo basado en componentes

Este patrón usa composición en lugar de herencia y está más cerca de cómo funciona el resto de Unity. Dependiendo de cómo estructura su juego, podría ser posible / beneficioso utilizar el sistema de componentes Unity para esto ... o no, su millaje puede variar.

Cada objeto de la clase Itemtendría una lista de componentes como Equipable, Edible, Sellable, Drinkable, etc. Un elemento puede tener uno o ninguno de cada componente (por ejemplo, un casco de chocolate sería a la vez Equipabley Edible, y cuando no es una trama crítica artículo de búsqueda también Sellable). La lógica de programación que es específica del componente se implementa en ese componente. Cuando el usuario hace clic derecho en un elemento, los componentes del elemento se repiten y se agregan entradas de menú contextual para cada componente que existe. Cuando el usuario selecciona una de estas entradas, el componente que agregó esa entrada procesa la opción.

Puede representar esto en su archivo XML al tener un subnodo para cada componente. Ejemplo:

   <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
fuente
Gracias por sus valiosas explicaciones y el tiempo que tomó para responder mi pregunta. Si bien aún no he decidido con qué método seguiré, aprecio los métodos alternativos de implementación que me han proporcionado. Me sentaré y pensaré qué método funcionará mejor para mí e iré desde allí. Gracias :)
Mike Hunt
@MikeHunt El modelo de lista de componentes es definitivamente algo que debe investigar, ya que funciona muy bien al cargar definiciones de elementos desde un archivo.
user253751
@immibis, eso es lo que probaré primero, ya que mi intento inicial fue similar. Gracias :)
Mike Hunt
Respuesta anterior, pero ¿hay alguna documentación sobre cómo implementar un modelo de "lista de componentes"?
Jeff
@Jeff Si desea implementar este patrón en su juego y tiene alguna pregunta sobre cómo hacerlo, publique una nueva pregunta.
Philipp
9

Entonces, Mike Hunt, tu pregunta me interesó tanto que decidí implementar una solución completa. Después de tres horas de probar cosas diferentes, terminé con esta solución paso a paso:

(Tenga en cuenta que este NO ES un código muy bueno, por lo que aceptaré cualquier edición)

Crear panel de contenido

(Este panel será un contenedor para nuestros botones de menú contextual)

  • Crear nuevo UI Panel
  • Establecer anchoren la parte inferior izquierda
  • Establecer widthen 300 (como desee)
  • Agregue a un panel un nuevo componente Vertical Layout Groupy configúrelo Child Alignmenten el centro superior, Child Force Expanden ancho (no en altura)
  • Agregue a un panel un nuevo componente Content Size Fittery Vertical Fitconfigúrelo en Tamaño mínimo
  • Guárdelo como prefabricado

(En este punto, nuestro Panel se reducirá a una línea. Es normal. Este panel aceptará botones como elementos secundarios, los alineará verticalmente y se estirará hasta la altura del contenido de resumen)

Crear botón de muestra

(Este botón se instanciará y personalizará para mostrar elementos del menú contextual)

  • Crear nuevo botón de IU
  • Establecer anchoren la parte superior izquierda
  • Agregue a un botón un nuevo componente Layout Element, establecido Min Heighten 30, Preferred Heighten 30
  • Guárdelo como prefabricado

Crear script ContextMenu.cs

(Este script tiene un método que crea y muestra el menú contextual)

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);
        }
    }
}
  • Adjunte este script a un Canvas y complete los campos. Arrastre y suelte el ContentPanelprefab a la ranura correspondiente, y arrastre Canvas mismo a la ranura Canvas.

Crear script de 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);
    }
}
  • Cree un objeto de muestra en la escena (es decir Cube), colóquelo para que sea visible para la cámara y adjunte este script a él. Arrastrar y soltar sampleButtonprefabricado a la ranura correspondiente.

Ahora, intenta ejecutarlo. Cuando hace clic con el botón derecho en el objeto, debe aparecer el menú contextual, completado con la lista que hicimos. Al presionar los botones se imprimirá en la consola algo de texto, y el menú contextual se destruirá.

Posibles mejoras:

  • ¡aún más genérico!
  • Mejor administración de memoria (enlaces sucios, no destruir el panel, deshabilitar)
  • algunas cosas elegantes

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

Ejercicio
fuente
Wow, muchas gracias por tomarse el tiempo de su día para implementar esto. Probaré su implementación tan pronto como regrese a mi computadora. Creo que, a los fines de la explicación, aceptaré la respuesta de Philipp en función de su variedad de explicaciones de los métodos que se pueden utilizar. Dejaré su respuesta aquí porque creo que es extremadamente valioso y las personas que vean esta pregunta en el futuro tendrán una implementación real, así como algunos métodos para implementar este tipo de cosas en un juego. Muchas gracias y bien hecho. También he votado esto :)
Mike Hunt
1
De nada. Sería genial si esta respuesta ayudara a alguien.
Ejercicio