Como projetar menus de contexto com base no que o objeto é?

21

Estou procurando uma solução para o comportamento "Opções do botão direito".

Basicamente, todo e qualquer item de um jogo, quando clicado com o botão direito, pode exibir um conjunto de opções com base em qualquer objeto que seja.

Clique com o botão direito do mouse em exemplos para diferentes cenários :

Inventário: o capacete mostra as opções (Equipar, Usar, Soltar, Descrição)

Banco: capacete mostra opções (Take 1, Take X, Take All, Description)

Andar: capacete mostra opções (pegar, andar aqui, descrição)

Obviamente, cada opção de alguma forma aponta para um determinado método que faz o que diz. Isso faz parte do problema que estou tentando descobrir. Com tantas opções de potência para um único item, como eu teria minhas aulas projetadas de forma a não serem extremamente confusas?

  • Pensei em herança, mas isso poderia ser muito longo e a cadeia poderia ser enorme.
  • Pensei em usar interfaces, mas isso provavelmente me restringiria um pouco, já que eu não seria capaz de carregar dados de itens de um arquivo Xml e colocá-los em uma classe "Item" genérica.

Estou baseando meu resultado final desejado em um jogo chamado Runescape. Cada objeto pode ser clicado com o botão direito do jogo e, dependendo do que é e de onde está (inventário, piso, banco etc.) exibe um conjunto diferente de opções disponíveis para o jogador interagir.

Como eu iria conseguir isso? Que abordagem devo levar para antes de tudo, decidir quais opções DEVEM ser exibidas e, uma vez clicadas, como chamar o método correspondente.

Estou usando C # e Unity3D, mas os exemplos fornecidos não precisam estar relacionados a nenhum deles, pois estou atrás de um padrão em oposição ao código real.

Qualquer ajuda é muito apreciada e, se eu não tiver sido claro na minha pergunta ou nos resultados desejados, por favor, poste um comentário e eu o tratarei o mais rápido possível.

Aqui está o que eu tentei até agora:

  • Na verdade, eu consegui implementar uma classe "Item" genérica que contém todos os valores para diferentes tipos de itens (ataque extra, defesa extra, custo etc ...). Essas variáveis ​​são preenchidas por dados de um arquivo Xml.
  • Pensei em colocar todos os métodos de interação possíveis dentro da classe Item, mas acho que essa é uma forma inacreditavelmente bagunçada e ruim. Eu provavelmente adotei a abordagem errada para implementar esse tipo de sistema usando apenas a classe e não subclassificando itens diferentes, mas é a única maneira de carregar os dados de um Xml e armazená-los na classe.
  • A razão pela qual escolhi carregar todos os meus itens de um arquivo Xml é porque este jogo tem a possibilidade de mais de 40.000 itens. Se minha matemática estiver correta, uma classe para cada item é muitas.
Mike Hunt
fonte
Olhando para a sua lista de comandos, com exceção de "Equipar", parece que todos eles são genéricos e se aplicam independentemente do item - pegar, soltar, descrição, mover aqui, etc.
ashes999
Se um item foi un transaccionáveis, em vez de "Drop" poderia ter "Destruir"
Mike caça
Para ser franco, muitos jogos resolvem isso usando uma DSL - uma linguagem de script personalizada específica para o jogo.
CorsiKa
11
+1 para modelar seu jogo após o RuneScape. Eu amo esse jogo.
Zenadix 17/09/2015

Respostas:

23

Como em tudo no desenvolvimento de software, não há solução ideal. Somente a solução ideal para você e seu projeto. Aqui estão alguns que você poderia usar.

Opção 1: O modelo processual

O antigo método obsoleto da velha escola.

Todos os itens são tipos de dados antigos simples, sem métodos, mas muitos atributos públicos que representam todas as propriedades que um item poderia ter, incluindo alguns sinalizadores booleanos isEdible, isEquipableetc. , que determinam quais entradas do menu de contexto estão disponíveis para ele (talvez você também possa fique sem esses sinalizadores quando puder derivá-lo dos valores de outros atributos). Tenha em sua classe de jogador alguns métodos Eat, Equipetc. , que pegam um item e que têm toda a lógica para processá-lo de acordo com os valores dos atributos.

Opção 2: O modelo orientado a objetos

Esta é mais uma solução OOP-by-the-book que é baseada em herança e polimorfismo.

Tenha uma classe base Itemda qual outros itens EdibleItem, como EquipableItemetc. , sejam herdados. A classe base deve ter um método público GetContextMenuEntriesForBank, GetContextMenuEntriesForFlooretc. , que retorne uma lista de ContextMenuEntry. Cada classe herdada substituiria esses métodos para retornar as entradas do menu de contexto apropriadas para esse tipo de item. Também poderia chamar o mesmo método da classe base para obter algumas entradas padrão aplicáveis ​​a qualquer tipo de item. A ContextMenuEntryseria uma classe com um métodoPerform que chama o método relevante do item que o criou (você poderia usar um delegado para isso).

Em relação aos problemas com a implementação desse padrão ao ler dados do arquivo XML: Primeiro examine o nó XML de cada item para determinar o tipo de item, depois use um código especializado para cada tipo para criar uma instância da subclasse apropriada.

Opção 3: O modelo baseado em componente

Esse padrão usa composição em vez de herança e está mais próximo de como o restante do Unity funciona. Dependendo de como você estrutura seu jogo, pode ser possível / benéfico usar o sistema de componentes do Unity para isso ... ou não, sua milhagem pode variar.

Cada objecto de classe Itemteria uma lista de componentes como Equipable, Edible, Sellable, Drinkable, etc. Um produto pode ter um ou nenhum de cada componente (por exemplo, um capacete de chocolate seria ambos Equipablee Edible, e quando não é uma trama-crítico item de missão também Sellable). A lógica de programação específica do componente é implementada nesse componente. Quando o usuário clica com o botão direito do mouse em um item, os componentes do item são iterados e as entradas do menu de contexto são adicionadas para cada componente existente. Quando o usuário seleciona uma dessas entradas, o componente que adicionou essa entrada processa a opção.

Você pode representar isso no seu arquivo XML tendo um subnó para cada componente. Exemplo:

   <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
fonte
Obrigado por suas valiosas explicações e pelo tempo que levou para responder à minha pergunta. Embora ainda não tenha decidido qual método seguir, aprecio os métodos alternativos de implementação que você forneceu. Vou me sentar e pensar sobre qual método funcionará melhor para mim e partir daí. Obrigado :)
Mike Hunt
@MikeHunt O modelo de lista de componentes é definitivamente algo que você deve investigar, pois funciona muito bem com o carregamento de definições de itens de um arquivo.
User253751
@immibis é o que eu vou experimentar primeiro, pois minha tentativa inicial foi semelhante a essa. Obrigado :)
Mike Hunt
Resposta antiga, mas existe alguma documentação sobre como implementar um modelo de "lista de componentes"?
Jeff
@ Jeff Se você gostaria de implementar esse padrão no seu jogo e tiver alguma dúvida sobre como, por favor, poste uma nova pergunta.
Philipp
9

Então, Mike Hunt, sua pergunta me interessou tanto, que decidi implementar uma solução completa. Depois de três horas tentando coisas diferentes, acabei com esta solução passo a passo:

(Observe que esse código NÃO é muito bom, portanto aceitarei edições)

Criando painel de conteúdo

(Este painel será um contêiner para nossos botões do menu de contexto)

  • Crie um novo UI Panel
  • Definir anchorpara o canto inferior esquerdo
  • Defina widthpara 300 (como desejar)
  • Adicione ao painel um novo componente Vertical Layout Groupe defina-o Child Alignmentpara o centro superior, Child Force Expandpara a largura (não para a altura)
  • Adicione a um painel um novo componente Content Size Fittere defina Vertical Fitcomo Tamanho mínimo
  • Salve-o como pré-fabricado

(Nesse momento, nosso painel diminuirá para uma linha. É normal. Este painel aceitará botões como filhos, alinhará-los verticalmente e se estenderá à altura do conteúdo resumido)

Criando botão de amostra

(Este botão será instanciado e personalizado para mostrar itens do menu de contexto)

  • Criar novo botão de interface do usuário
  • Definir anchorpara o canto superior esquerdo
  • Adicione a um botão um novo componente Layout Element, definido Min Heightcomo 30, Preferred Heightpara 30
  • Salve-o como pré-fabricado

Criando o script ContextMenu.cs

(Este script possui um método que cria e mostra o menu de contexto)

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);
        }
    }
}
  • Anexe esse script a um Canvas e preencha os campos. Arraste e solte a casa ContentPanelpré - fabricada no slot correspondente e arraste o próprio Canvas para o slot Canvas.

Criando o 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);
    }
}
  • Crie um objeto de amostra na cena (ex. Cube), Coloque-o para ficar visível para a câmera e anexe esse script a ele. Arraste e solte a casa sampleButtonpré - fabricada no slot correspondente.

Agora, tente executá-lo. Quando você clica com o botão direito do mouse no objeto, o menu de contexto deve aparecer, preenchido com a lista que criamos. Pressionar os botões imprimirá algum texto no console e o menu de contexto será destruído.

Possíveis melhorias:

  • ainda mais genérico!
  • melhor gerenciamento de memória (links sujos, painel não destruindo, desativação)
  • algumas coisas chiques

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

Exerion
fonte
Uau, muito obrigado por dedicar um tempo do seu dia para implementar isso. Testarei sua implementação assim que voltar ao meu computador. Penso que, para fins de explicação, aceitarei a resposta de Philipp com base em sua variedade de explicações para métodos que podem ser usados. Deixarei sua resposta aqui porque acredito que é extremamente valiosa e as pessoas que visualizarem essa pergunta no futuro terão uma implementação real, bem como alguns métodos para implementar esse tipo de coisa em um jogo. Muito obrigado e muito bem. Eu também votei em cima disso :)
Mike Hunt
11
Seja bem-vindo. Seria ótimo se essa resposta ajudasse alguém.
Exerion