19
\$\begingroup\$

I'm looking for a solution for a "Right Click Options" behaviour.

Basically any and every item in a game, when right clicked, can display a set of options based on whatever the object is.

Right click examples for different scenarios:

Inventory: Helmet shows options (Equip, Use, Drop, Description)

Bank: Helmet shows options (Take 1, Take X, Take All, Description)

Floor: Helmet shows options (Take, Walk Here, Description)

Obviously each option somehow points to a certain method that does what is says. This is part of the issue I'm trying to figure out. With so many potention options for a single item, how would I have my classes designed in such a way as to not be extremely messy?

  • I've thought about inheritance but that could be really long winded and the chain could be huge.
  • I've thought about using interfaces, but this would probably restrict me a little as I wouldn't be able to load item data from an Xml file and place it into a generic "Item" class.

I'm basing my desired end result on a game called Runescape. Every object can be right clicked in the game and depending on what it is, and where it is (inventory, floor, bank etc.) displays a different set of options available to the player to interact with.

How would I go about achieving this? What approach should I take to first of all, decide which options SHOULD be displayed and once clicked, how to call the corresponding method.

I am using C# and Unity3D, but any examples provided do not have to be related to either of them as I'm after a pattern as opposed to actual code.

Any help is much appreciated and if I have not been clear in my question or desired results, please post a comment and I'll tend to it ASAP.

Here is what I have tried so far:

  • I've actually managed to implement a generic "Item" class that holds all of the values for different types of items (extra attack, extra defence, cost etc...). These variables get populated by data from an Xml file.
  • I have thought about placing every single possible interaction method inside of the Item class but I think this is unbelievably messy and poor form. I've probably taken the wrong approach for implementing this kind of system by only using the one class and not sub-classing to different items, but its the only way I can load the data from an Xml and store it in the class.
  • The reason I've chose to load all my items from an Xml file is due to this game having the possibility for 40,000+ items. If my math is correct, a class for each item is a lot of classes.
\$\endgroup\$
  • \$\begingroup\$ Looking at your list of commands, with the exception of "Equip," it seems like all of them are generic and apply regardless of what the item is -- take, drop, description, move here, etc. \$\endgroup\$ – ashes999 Sep 17 '15 at 13:27
  • \$\begingroup\$ If an item was un-tradable, instead of "Drop" it could have "Destroy" \$\endgroup\$ – Mike Hunt Sep 17 '15 at 13:29
  • \$\begingroup\$ To be perfectly frank, many games solve this using a DSL - a custom scripting language specific to the game. \$\endgroup\$ – corsiKa Sep 17 '15 at 16:10
  • 1
    \$\begingroup\$ +1 for modelling your game after RuneScape. I love that game. \$\endgroup\$ – Zenadix Sep 17 '15 at 19:28
21
\$\begingroup\$

As with everything in software development, there is no ideal solution. Only the solution which is ideal for you and your project. Here are some you could use.

Option 1: The procedural model

The ancient obsolete old-school method.

All items are dumb plain-old-data types without any methods but lots of public attributes which represent all properties an item could have, including some boolean flags like isEdible, isEquipable etc. which determine what context menu entries are available for it (maybe you could also do without these flags when you can derive it from the values of other attributes). Have some methods like Eat, Equip etc. in your player class which takes an item and which has all the logic to process it according to the attribute values.

Option 2: The object-oriented model

This is more of a OOP-by-the-book solution which is based on inheritance and polymorphism.

Have a base-class Item from which other items like EdibleItem, EquipableItem etc. inherit. The base class should have a public method GetContextMenuEntriesForBank, GetContextMenuEntriesForFloor etc. which return a list of ContextMenuEntry. Each inheriting class would override these methods to return the context menu entries which are appropriate for this item type. It could also call the same method of the base class to get some default entries which are applicable for any item type. The ContextMenuEntry would be a class with a method Perform which then calls the relevant method from the Item which created it (you could use a delegate for this).

Regarding your problems with implementing this pattern when reading data from the XML file: First examine the XML node for each item to determine the type of item, then use specialized code for each type to create an instance of the appropriate sub-class.

Option 3: The component-based model

This pattern uses composition instead of inheritance and is closer to how the rest of Unity works. Depending on how you structure your game it might be possible/beneficial to use the Unity component system for this... or not, your mileage may vary.

Each object of class Item would have a list of components like Equipable, Edible, Sellable, Drinkable, etc. An item can have one or none of each component (for example, a helmet made of chocolate would be both Equipable and Edible, and when it is not a plot-critical quest item also Sellable). The programming logic which is specific to the component is implemented in that component. When the user right-clicks on an item, the components of the item are iterated and context-menu entries are added for each component which exists. When the user selects one of these entries, the component which added that entry processes the option.

You could represent this in your XML-file by having a sub-node for each component. Example:

   <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>
\$\endgroup\$
  • \$\begingroup\$ Thank you for your valuable explanations and the time you took to answer my question. While I haven't yet decided which method I will go with, I do appreciate the alternate methods of implementation you have provided. I will sit down and think about what method will work better for me and go from there. Thanks :) \$\endgroup\$ – Mike Hunt Sep 18 '15 at 4:04
  • \$\begingroup\$ @MikeHunt The list-of-components model is definitely something you should investigate, since it works nicely with loading item definitions from a file. \$\endgroup\$ – user253751 Sep 18 '15 at 5:12
  • \$\begingroup\$ @immibis that is what i'll be trying out first as my initial attempt was similar to that. Thanks :) \$\endgroup\$ – Mike Hunt Sep 18 '15 at 5:58
  • \$\begingroup\$ Old answer, but is there any documentation on how to implement a "list-of-components" model? \$\endgroup\$ – Jeff Jun 13 at 23:57
  • \$\begingroup\$ @Jeff If you would like to implement this pattern in your game and have any questions about how, please post a new question. \$\endgroup\$ – Philipp Jun 14 at 8:02
8
\$\begingroup\$

So, Mike Hunt, your question so interested me, I decided to implement a full solution. After three hours of trying different things, I ended up with this step-by-step solution:

(Please note, that this IS NOT very good code, so I will accept any edits)

Creating Content Panel

(This panel will be a container for our context-menu buttons)

  • Create new UI Panel
  • Set anchor to bottom-left
  • Set width to 300 (as you wish)
  • Add to a panel a new component Vertical Layout Group and set Child Alignment to upper-center, Child Force Expand to width (not height)
  • Add to a panel a new component Content Size Fitter and set Vertical Fit to Min Size
  • Save it as prefab

(At this point our Panel will shrink down to a line. It's normal. This panel will accept buttons as children, align them vertically and stretch to summary content height)

Creating Sample Button

(This button will be instantiated and customized to show context-menu items)

  • Create new UI Button
  • Set anchor to top-left
  • Add to a button a new component Layout Element, set Min Height to 30, Preferred Height to 30
  • Save it as prefab

Creating ContextMenu.cs script

(This script has a method that creates and shows Context Menu)

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);
        }
    }
}
  • Attach this script to a Canvas and populate fields. Drag-n-drop ContentPanel prefab to corresponding slot, and drag Canvas itself to slot Canvas.

Creating ItemController.cs script

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);
    }
}
  • Create sample object in the scene (i.e Cube), place it to be visible to camera and attach this script to it. Drag-n-drop sampleButton prefab to corresponding slot.

Now, try to run it. When you're right-clicking the object, the context menu should appear, populated with list that we made. Pressing buttons will print into the console some text, and the context menu will be destroyed.

Possible improvements:

  • even more generic!
  • better memory management (dirty links, not destroying panel, disabling)
  • some fancy stuff

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

\$\endgroup\$
  • \$\begingroup\$ Wow thank you so much for taking the time out of your day to implement this. I will be testing out your implementation as soon as I'm back on my computer. I think for the purposes of explanation, I will be accepting Philipp's answer based on its variety of explanations for methods that can be used. I will leave your answer here because I believe it is extremely valuable and people viewing this question in the future will have an actual implementation as well as some methods for implementing this kind of thing in a game. Thank you very much and well done. I have also up voted this :) \$\endgroup\$ – Mike Hunt Sep 18 '15 at 4:01
  • 1
    \$\begingroup\$ You're welcome. It would be great if this answer will help someone. \$\endgroup\$ – Exerion Sep 18 '15 at 5:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.