Як розробити контекстні меню на основі будь-якого об'єкта?


21

Я шукаю рішення для поведінки "Параметри правої кнопки".

В основному будь-який предмет у грі, при натисканні правою кнопкою миші, може відображати набір опцій, залежно від того, що є об’єктом.

Приклади правою кнопкою миші для різних сценаріїв :

Інвентар: Шолом показує варіанти (Оснащення, використання, скидання, опис)

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

Поверх: Шлем показує варіанти (Take, Walk Here, Опис)

Очевидно, кожен варіант якимось чином вказує на певний метод, який робить те, що говориться. Це частина проблеми, яку я намагаюся розібратися. З такою кількістю варіантів потенціалу для одного предмета, як би я створив свої заняття таким чином, щоб не бути надзвичайно безладним?

  • Я думав про спадщину, але це могло б бути насправді довго накрученим і ланцюжок може бути величезною.
  • Я думав про використання інтерфейсів, але це, ймовірно, трохи обмежить мене, оскільки я не зможу завантажити дані елемента з Xml-файлу та розмістити їх у загальному класі "Item".

Я базую свій бажаний кінцевий результат на грі під назвою Runescape. Кожен об’єкт може клацнути правою кнопкою миші в грі, і залежно від того, що він є, і де він знаходиться (інвентар, підлога, банк тощо), відображається різний набір опцій, доступних гравцю для взаємодії.

Як би я міг досягти цього? Який підхід слід застосувати, перш за все, вирішити, які параметри ОБОВ'ЯЗКОВО відобразити та натиснути один раз, як викликати відповідний метод.

Я використовую C # і Unity3D, але будь-які надані приклади не повинні бути пов’язані з жодним із них, оскільки я за шаблоном на відміну від фактичного коду.

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

Ось що я спробував поки що:

  • Мені вдалося реалізувати загальний клас "Item", який містить усі значення для різних типів предметів (додаткова атака, додаткова захист, вартість тощо ...). Ці змінні заповнюються даними з файлу Xml.
  • Я думав над тим, щоб розмістити кожен можливий метод взаємодії всередині класу Item, але думаю, що це неймовірно безладно і погано. Я, мабуть, прийняв неправильний підхід до впровадження такої системи, використовуючи лише один клас і не підкласифікуючись на різні елементи, але єдиний спосіб я можу завантажувати дані з Xml та зберігати їх у класі.
  • Причина, по якій я вирішив завантажити всі мої предмети з файлу Xml, пов’язана з цією грою, яка має можливість для 40 000+ предметів. Якщо моя математика правильна, клас для кожного предмету - це багато класів.

Дивлячись на ваш список команд, за винятком "Оснащення", здається, що всі вони є загальними і застосовуються незалежно від того, що це за пункт - взяти, відпустити, описати, перемістити сюди тощо
ashes999,

Якщо предмет не можна торгувати, замість "Відкинути" він міг би "Знищити"
Майк Хант,

Щоб бути абсолютно відвертим, багато ігор вирішують це за допомогою DSL - спеціальної мови сценаріїв, характерної для гри.
corsiKa

1
+1 для моделювання вашої гри після RuneScape. Я люблю цю гру.
Зенадікс

Відповіді:


23

Як і у всьому, що стосується розробки програмного забезпечення, ідеального рішення немає. Тільки рішення, яке ідеально підходить для вас та вашого проекту. Ось декілька, які ви могли б використати.

Варіант 1: Процедурна модель

Древній застарілі метод старої школи.

Усі елементи - це тупі звичайні старі дані без будь-яких методів, але безліч загальнодоступних атрибутів, які представляють усі властивості, які може мати елемент, у тому числі деякі булеві прапори isEdible, isEquipableтощо. обійтися без цих прапорів, коли ви зможете отримати це зі значень інших атрибутів). У вашому класі гравців є такі методи, як Eatі Equipт. Д., Який приймає предмет і який має всю логіку для його обробки відповідно до значень атрибутів.

Варіант 2: Об'єктно-орієнтована модель

Це більше рішення OOP за книгою, яке базується на спадкуванні та поліморфізмі.

У базового класу , Itemвід якого інші елементи , такі як EdibleItem, і EquipableItemт.д. Успадковувати. Базовий клас повинен мати відкритий метод GetContextMenuEntriesForBank, і GetContextMenuEntriesForFloorт.д. , який повертає список ContextMenuEntry. Кожен клас успадкування замінить ці методи, щоб повернути записи контекстного меню, які відповідають цьому типу елементів. Він також може викликати той самий метод базового класу, щоб отримати деякі записи за замовчуванням, які застосовні для будь-якого типу елемента. Це ContextMenuEntryбув би клас із методом, Performякий потім викликає відповідний метод із пункту, який його створив (ви можете використовувати для цього делегата ).

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

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

Ця закономірність використовує композицію замість успадкування і ближче до того, як працює решта Єдності. Залежно від того, як ви структуруєте свою гру, можливо використовувати / вигідно використовувати компонентну систему 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>

Дякую за ваші цінні пояснення та час, який ви витратили на відповідь на моє запитання. Хоча я ще не вирішив, до якого методу я буду користуватися, я вдячний за запропоновані вами альтернативні методи реалізації. Я сяду і подумаю про те, який метод буде краще для мене, і піти звідти. Дякую :)
Майк Хант

@MikeHunt Модель списку компонентів, безумовно, слід вивчити, оскільки вона добре працює з завантаженням визначень елемента з файлу.
користувач253751

@immibis, це те, що я спробую спершу, так як моя перша спроба була схожа на цю. Дякую :)
Майк Хант

Стара відповідь, але чи є документація про те, як реалізувати модель "список компонентів"?
Джефф

@Jeff Якщо ви хочете реалізувати цю схему у своїй грі і у вас є питання щодо того, будь ласка, опублікуйте нове запитання.
Філіп

9

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

(Зверніть увагу, що це НЕ дуже хороший код, тому я прийму будь-які зміни)

Створення панелі вмісту

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

  • Створити новий UI Panel
  • Встановити anchorвнизу зліва
  • Встановіть width300 (за бажанням)
  • Додайте до панелі новий компонент Vertical Layout Groupі встановіть Child Alignmentверхній центр, Child Force Expandна ширину (не висоту)
  • Додайте до панелі новий компонент Content Size Fitterі встановіть Vertical Fitзначення Min Size
  • Збережіть це як збірне

(У цей момент наша панель зменшиться до рядка. Це нормально. Ця панель прийме кнопки як діти, вирівняє їх по вертикалі та розтягнеться до підсумкової висоти вмісту)

Створення кнопки зразка

(Ця кнопка буде створена миттєво та налаштована для відображення пунктів контекстного меню)

  • Створіть нову кнопку інтерфейсу користувача
  • Встановити anchorугорі зліва
  • Додайте до кнопки новий компонент Layout Element, встановлений Min Heightна 30, 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


Дуже дякую тобі за те, що взяв час у твоєму дні для здійснення цього. Я перевіряю вашу реалізацію, як тільки повернусь до свого комп’ютера. Я думаю, що для пояснення я прийму відповідь Філіпа на основі різноманітних пояснень методів, які можна використовувати. Я залишу вашу відповідь тут, тому що я вважаю це надзвичайно цінним, і люди, які переглядають це питання в майбутньому, матимуть реальну реалізацію, а також деякі методи впровадження подібних речей в гру. Дуже дякую та молодець. Я також проголосував за це :)
Майк Хант,

1
Ласкаво просимо. Було б чудово, якщо ця відповідь комусь допоможе.
Екзеріон
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.