如何根据对象是什么设计上下文菜单?


21

我正在寻找“右键单击选项”行为的解决方案。

基本上,当右键单击游戏时,游戏中的所有项目都可以根据对象的不同显示一组选项。

右键单击不同情况的示例

库存:头盔显示选项(装备,用途,掉落,描述)

银行:头盔显示选项(Take 1,Take X,Take All,Description)

楼层:头盔显示选项(“乘车”,“在这里行走”,“描述”)

显然,每个选项都以某种方式指向执行所说内容的特定方法。这是我要解决的问题的一部分。对于单个项目有这么多的潜在选项,我如何以不至于太混乱的方式设计班级?

  • 我曾考虑过继承,但是这可能会很漫长,而且链条可能很大。
  • 我已经考虑过使用接口,但这可能会限制我一点,因为我无法从Xml文件加载项目数据并将其放入通用的“ Item”类中。

我将期望的最终结果基于一个名为Runescape的游戏。可以在游戏中右键单击每个对象,并根据其位置以及位置(存货,楼层,银行等)显示不同的选项集,供玩家进行交互。

我将如何实现这一目标?首先,我应该采用哪种方法,决定应该显示哪些选项,单击后如何调用相应的方法。

我正在使用C#和Unity3D,但是提供的任何示例都不必与它们中的任何一个相关,因为我追求的是与实际代码相反的模式。

非常感谢您的帮助,如果我在问题或期望的结果中不清楚,请发表评论,我会尽快解决。

到目前为止,这是我尝试过的:

  • 实际上,我已经设法实现了一个通用的“ Item”类,其中包含了不同类型项目的所有值(额外攻击,额外防御,成本等)。这些变量由Xml文件中的数据填充。
  • 我曾考虑过将每个可能的交互方法都放置在Item类中,但是我认为这是令人难以置信的混乱且糟糕的形式。我可能仅通过使用一个类而不将其子类化为不同的项目而采用了错误的方法来实现这种系统,但这是我可以从Xml加载数据并将其存储在类中的唯一方法。
  • 我之所以选择从Xml文件中加载我的所有物品,是因为该游戏可以容纳40,000多个物品。如果我的数学正确,则每个项目的班级很多。

看你的命令的列表,除外“装备”,好像个个都是通用的,无论该项目是什么样的应用-冒,滴,描述说明,移动这里,等
ashes999

如果某项商品不可交易,则它可能不是“ Drop”而是“销毁”
Mike Hunt 2015年

坦率地说,许多游戏都使用DSL(一种特定于游戏的自定义脚本语言)解决了这一问题。
corsiKa 2015年

1
+1用于在RuneScape之后为游戏建模。我喜欢那个游戏。
Zenadix

Answers:


23

与软件开发中的所有内容一样,没有理想的解决方案。只有最适合您和您的项目的解决方案。这是您可以使用的一些。

选项1:程序模型

古老的 陈旧老派的方法。

所有的项目都是没有任何方法哑普通老式的数据类型,但很多公共属性,它代表所有属性的项目可以有,包括一些布尔标志一样的isEdibleisEquipable等它确定哪些上下文菜单条目可用于它(也许你也可以当您可以从其他属性的值派生它时,不要使用这些标志。在播放器类中有一些方法,例如EatEquip等等,该方法接受一个项目,并且具有根据属性值处理该项目的所有逻辑。

选项2:面向对象的模型

这更多是基于继承和多态性的按需OOP解决方案。

有一个基类Item,其他项目(例如EdibleItemEquipableItem等等)从基类继承。基类应该有一个公共的方法GetContextMenuEntriesForBankGetContextMenuEntriesForFloor等它返回一个列表ContextMenuEntry。每个继承类都将重写这些方法以返回适合于此项目类型的上下文菜单项。它还可以调用基类的相同方法来获取一些适用于任何项目类型的默认条目。该ContextMenuEntry会是一个方法的类Perform,然后调用从创建它(你可以使用该项目的相关方法代表了这一点)。

关于从XML文件读取数据时实现此模式的问题:首先检查每个项目的XML节点以确定项目的类型,然后对每种类型使用专门的代码来创建适当子类的实例。

选项3:基于组件的模型

这种模式使用合成而不是继承,并且更接近Unity其余部分的工作方式。取决于您游戏的结构方式,是否可以为此目的使用Unity组件系统……是否可行,您的里程可能会有所不同。

类的每个对象Item将具有组件的列表等EquipableEdibleSellableDrinkable,等。一个项目可以有一个或没有各成分的(例如,由巧克力头盔将是既EquipableEdible,而当它不是一个情节关键任务项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>

感谢您的宝贵解释以及您回答我的问题所花费的时间。尽管我尚未决定将使用哪种方法,但我确实感谢您提供的替代实现方法。我将坐下来,考虑哪种方法对我更好,然后从那里开始。谢谢:)
Mike Hunt 2015年

@MikeHunt绝对应该对组件列表模型进行研究,因为它可以很好地与从文件中加载项目定义一起使用。
user253751

@immibis这就是我将首先尝试的内容,因为我的最初尝试与此类似。谢谢:)
Mike Hunt

旧的答案,但是有没有关于如何实现“组件列表”模型的文档?
Jeff

@Jeff如果您想在游戏中实现此模式,并且对操作方法有任何疑问,请发布一个新问题。
菲利普

9

因此,迈克·亨特(Mike Hunt),您的问题让我很感兴趣,因此我决定实施一个完整的解决方案。经过三个小时的尝试,我最终得到了以下逐步解决方案:

(请注意,这不是很好的代码,因此我将接受任何修改)

创建内容面板

(此面板将成为上下文菜单按钮的容器)

  • 创建新的 UI Panel
  • 设置anchor为左下
  • 设置width为300(根据需要)
  • 将一个新组件添加到面板中,Vertical Layout Group并设置Child Alignment为上居中,Child Force Expand宽度(而非高度)
  • 向面板添加新组件Content Size Fitter并设置Vertical Fit为“最小大小”
  • 将其另存为预制件

(这时我们的面板会缩小到一行。这是正常的。该面板会将按钮作为子代接受,将它们垂直对齐并拉伸到摘要内容的高度)

创建样本按钮

(此按钮将被实例化和自定义以显示上下文菜单项)

  • 创建新的UI按钮
  • 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自身拖动到slot 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


哇,非常感谢您抽出宝贵的时间来执行此操作。我将在回到计算机上后立即测试您的实现。我认为出于解释的目的,我将基于对可用方法的各种解释接受Philipp的回答。我将在这里留下您的答案,因为我认为它非常有价值,并且将来查看此问题的人们将有一个实际的实现以及一些在游戏中实现此类操作的方法。非常感谢您,做得很好。我也对此投了赞成票:)
Mike Hunt

1
别客气。如果这个答案可以帮助某人,那就太好了。
Exerion
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.