Как спроектировать контекстные меню, основанные на том, чем является объект?

21

Я ищу решение для поведения "Параметры правой кнопки мыши".

По сути, любой элемент в игре, если щелкнуть правой кнопкой мыши, может отображать набор параметров в зависимости от того, что это за объект.

Щелкните правой кнопкой мыши примеры для различных сценариев :

Инвентарь: Шлем показывает опции (Экипировка, Использование, Сброс, Описание)

Банк: Шлем показывает варианты (Take 1, Take X, Take All, Описание)

Этаж: шлем показывает варианты (возьми, иди сюда, описание)

Очевидно, что каждая опция как-то указывает на определенный метод, который делает то, что говорит. Это часть проблемы, которую я пытаюсь выяснить. С таким большим количеством возможностей для одного предмета, как бы мои классы были спроектированы таким образом, чтобы не быть слишком грязными?

  • Я думал о наследовании, но это может быть очень долго, и цепь может быть огромной.
  • Я думал об использовании интерфейсов, но это, вероятно, немного ограничило бы меня, так как я не смог бы загрузить данные элементов из файла XML и поместить их в общий класс «Item».

Я основываю свой желаемый конечный результат на игре под названием Runescape. Каждый объект можно щелкнуть правой кнопкой мыши в игре и в зависимости от того, что это такое и где он находится (инвентарь, пол, банк и т. Д.), Отображает различный набор параметров, доступных игроку для взаимодействия.

Как бы я пошел для достижения этого? Какой подход я должен использовать в первую очередь, чтобы решить, какие параметры ДОЛЖНЫ отображаться, и после щелчка выбрать способ вызова соответствующего метода.

Я использую C # и Unity3D, но любые приведенные примеры не обязательно должны быть связаны ни с одним из них, так как я использую шаблон, а не реальный код.

Любая помощь очень ценится, и если я не получил четкого ответа на свой вопрос или желаемых результатов, пожалуйста, оставьте комментарий, и я буду стремиться к нему как можно скорее.

Вот что я пробовал до сих пор:

  • Мне действительно удалось реализовать универсальный класс «Предмет», который содержит все значения для различных типов предметов (дополнительная атака, дополнительная защита, стоимость и т. Д.). Эти переменные заполняются данными из файла XML.
  • Я думал о размещении всех возможных методов взаимодействия внутри класса Item, но я думаю, что это невероятно грязная и плохая форма. Я, вероятно, выбрал неправильный подход для реализации системы такого типа, используя только один класс, а не подклассифицируя различные элементы, но это единственный способ, которым я могу загрузить данные из Xml и сохранить их в классе.
  • Причина, по которой я решил загрузить все свои предметы из XML-файла, заключается в том, что в этой игре есть возможность более 40 000 предметов. Если моя математика верна, класс для каждого предмета - это много классов.
Майк Хант
источник
Глядя на ваш список команд, за исключением «Одета», кажется, что все они являются общими и применимы независимо от того, что это за элемент - взять, отбросить, описать, переместить сюда и т. Д.
ashes999
Если бы предмет был неторгуемым, вместо «Дропа» он мог бы иметь «Уничтожить»
Майк Хант
Чтобы быть совершенно откровенным, многие игры решают эту проблему с помощью DSL - специального языка сценариев, специфичного для игры.
CorsiKa
1
+1 за моделирование вашей игры после RuneScape. Я люблю эту игру.
Zenadix

Ответы:

23

Как и все в разработке программного обеспечения, идеального решения не существует. Только решение, которое идеально подходит для вас и вашего проекта. Вот некоторые из них, которые вы могли бы использовать.

Вариант 1: процедурная модель

Древний устаревшие метод старой школы.

Все элементы являются глупыми обычными типами старых данных без каких-либо методов, но имеют множество открытых атрибутов, которые представляют все свойства, которые может иметь элемент, включая некоторые логические флаги, например isEdible, isEquipableи т. Д., Которые определяют, какие записи контекстного меню доступны для него (возможно, вы могли бы также обойтись без этих флагов, когда вы можете получить его из значений других атрибутов). Есть несколько методов, как Eat,Equip вашем классе игрока т. Д., Которые берут предмет и имеют всю логику для его обработки в соответствии со значениями атрибута.

Вариант 2: объектно-ориентированная модель

Это скорее решение ООП, основанное на наследовании и полиморфизме.

Иметь базовый класс, Itemот которого наследуются другие элементы, такие как EdibleItemи EquipableItemт. Д. Базовый класс должен иметь открытый метод GetContextMenuEntriesForBankи GetContextMenuEntriesForFloorт. Д., Которые возвращают список ContextMenuEntry. Каждый наследующий класс переопределяет эти методы, чтобы возвращать записи контекстного меню, которые подходят для этого типа элемента. Он также может вызвать тот же метод базового класса, чтобы получить некоторые записи по умолчанию, которые применимы для любого типа элемента. Это ContextMenuEntryбудет класс с методом, Performкоторый затем вызывает соответствующий метод из элемента, который его создал (для этого можно использовать делегат ).

Что касается ваших проблем с реализацией этого шаблона при чтении данных из файла XML: сначала проверьте узел XML для каждого элемента, чтобы определить тип элемента, а затем используйте специализированный код для каждого типа, чтобы создать экземпляр соответствующего подкласса.

Вариант 3: модель на основе компонентов

Этот шаблон использует композицию вместо наследования и ближе к тому, как работает остальная часть Unity. В зависимости от того, как вы структурируете свою игру, может быть возможно / полезно использовать систему компонентов Unity для этого ... или нет, ваш пробег может отличаться.

Каждый объект класса Itemбудет иметь список компонентов , как Equipable, Edible, Sellable, Drinkableи т.д. Элемент может иметь один или ни один из каждого компонента (например, шлет из шоколада было бы как Equipableи Edible, а когда это не сюжет критичного квестовый предмет тоже Sellable). В этом компоненте реализована логика программирования, характерная для данного компонента. Когда пользователь щелкает правой кнопкой мыши по элементу, компоненты элемента повторяются, и для каждого существующего компонента добавляются записи контекстного меню. Когда пользователь выбирает одну из этих записей, компонент, добавивший эту запись, обрабатывает эту опцию.

Вы можете представить это в своем XML-файле, имея подузел для каждого компонента. Пример:

   <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
источник
Спасибо за ваши ценные объяснения и время, которое вы потратили, чтобы ответить на мой вопрос. Хотя я еще не решил, какой метод я выберу, я ценю альтернативные методы реализации, которые вы предоставили. Я сижу и думаю о том, какой метод будет работать лучше для меня, и пойду оттуда. Спасибо :)
Майк Хант
@MikeHunt Модель списка компонентов - это определенно то, что вы должны исследовать, поскольку она прекрасно работает с загрузкой определений элементов из файла.
user253751
@immibis, это то, что я сначала попробую, так как моя первоначальная попытка была похожа на это. Спасибо :)
Майк Хант
Старый ответ, но есть ли документация о том, как реализовать модель «список компонентов»?
Джефф
@Jeff Если вы хотите внедрить этот шаблон в вашу игру и у вас есть вопросы о том, как, пожалуйста, опубликуйте новый вопрос.
Филипп
9

Итак, Майк Хант, твой вопрос меня так заинтересовал, что я решил реализовать полное решение. После трех часов пробных попыток я получил пошаговое решение:

(Обратите внимание, что это НЕ очень хороший код, поэтому я буду принимать любые изменения)

Создание панели содержимого

(Эта панель будет контейнером для кнопок нашего контекстного меню)

  • Создайте новый UI Panel
  • Установить anchorвнизу слева
  • Установите widthна 300 (как вы хотите)
  • Добавьте на панель новый компонент Vertical Layout Groupи установите Child Alignmentв верхний центр, Child Force Expandв ширину (не высоту)
  • Добавьте на панель новый компонент Content Size Fitterи установите Vertical Fitминимальный размер
  • Сохранить как сборный

(На этом этапе наша панель будет уменьшена до линии. Это нормально. Эта панель будет принимать кнопки как дочерние элементы, выравнивать их по вертикали и растягивать до итоговой высоты содержимого)

Создание образца кнопки

(Эта кнопка будет создана и настроена для отображения элементов контекстного меню)

  • Создать новую кнопку интерфейса
  • Установить anchorв верхнем левом углу
  • Добавить к кнопке новый компонент Layout Element, установить Min Height30, Preferred Heightдо 30
  • Сохранить как сборный

Создание сценария ContextMenu.cs

(Этот скрипт имеет метод, который создает и показывает контекстное меню)

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);
        }
    }
}
  • Прикрепите этот скрипт к холсту и заполните поля. Перетащите ContentPanelпрефаб в соответствующий слот и перетащите сам Canvas в слот Canvas.

Создание скрипта 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);
    }
}
  • Создайте образец объекта в сцене (то есть Cube), поместите его, чтобы он был виден камере, и прикрепите к нему этот скрипт. Перетащите sampleButtonпрефаб в соответствующий слот.

Теперь попробуйте запустить его. Когда вы щелкаете правой кнопкой мыши по объекту, должно появиться контекстное меню, заполненное списком, который мы создали. Нажатие кнопок выведет в консоль некоторый текст, и контекстное меню будет уничтожено.

Возможные улучшения:

  • еще более общий!
  • улучшенное управление памятью (грязные ссылки, не разрушение панели, отключение)
  • некоторые модные вещи

Пример проекта (Unity Personal 5.2.0, плагин VisualStudio): https://drive.google.com/file/d/0B7iGjyVbWvFwUnRQRVVaOGdDc2M/view?usp=sharing

Exerion
источник
Вау, большое спасибо за то, что вы нашли время, чтобы реализовать это. Я буду тестировать вашу реализацию, как только вернусь на свой компьютер. Я думаю, что в целях объяснения я приму ответ Филиппа, основанный на его разнообразных объяснениях методов, которые можно использовать. Я оставлю здесь ваш ответ, потому что я считаю, что это чрезвычайно ценно, и у людей, рассматривающих этот вопрос в будущем, будет реальная реализация, а также некоторые методы для реализации такого рода вещей в игре. Большое спасибо и хорошо сделано. Я также проголосовал за это :)
Майк Хант
1
Пожалуйста. Было бы здорово, если этот ответ кому-нибудь поможет.
Exerion