オブジェクトが何であるかに基づいてコンテキストメニューを設計する方法


21

「右クリックオプション」動作のソリューションを探しています。

基本的に、ゲーム内のすべてのアイテムは、右クリックすると、オブジェクトが何であるかに基づいて一連のオプションを表示できます。

さまざまなシナリオの例を右クリックします

インベントリ:ヘルメットはオプション(装備、使用、ドロップ、説明)を表示します

銀行:ヘルメットにオプションが表示されます(テイク1、テイクX、すべてテイク、説明)

フロア:ヘルメットはオプションを表示します(テイク、ここを歩く、説明)

明らかに、各オプションは、言われていることを行う特定の方法を何らかの形で指し示しています。これは、私が理解しようとしている問題の一部です。単一のアイテムに非常に多くのポテンショニングオプションがあるのに、クラスを非常に乱雑にならないように設計するにはどうすればよいですか?

  • 私は継承について考えましたが、それは本当に長くかかり、チェーンが巨大になる可能性があります。
  • インターフェイスを使用することを考えましたが、Xmlファイルからアイテムデータをロードして汎用の「Item」クラスに配置することができないため、これはおそらく少し制限されます。

Runescapeと呼ばれるゲームに基づいて、目的の最終結果を作成しています。ゲーム内ですべてのオブジェクトを右クリックすると、オブジェクトの種類や場所(インベントリ、フロア、銀行など)に応じて、プレーヤーが操作できるさまざまなオプションセットが表示されます。

これをどのように達成しますか?まず、どのアプローチをとるべきか、どのオプションを表示すべきかを決定し、クリックしたら、対応するメソッドを呼び出す方法を決定する必要があります。

私はC#とUnity3Dを使用していますが、実際のコードとは対照的にパターンを追っているので、提供された例はどちらにも関係する必要はありません。

どんな助けでも大歓迎です。質問や希望する結果が明確でない場合は、コメントを投稿してください。できるだけ早く対応します。

ここに私がこれまで試したものがあります:

  • 私は実際に、さまざまな種類のアイテム(追加攻撃、追加防御、コストなど)のすべての値を保持する一般的な「アイテム」クラスを実装することができました。これらの変数には、Xmlファイルのデータが入力されます。
  • 可能な限りすべての対話メソッドをItemクラス内に配置することを考えましたが、これは信じられないほど面倒で貧弱なフォームだと思います。おそらく、1つのクラスのみを使用し、異なるアイテムにサブクラス化することにより、この種のシステムを実装するための間違ったアプローチを取っていますが、Xmlからデータをロードしてクラスに保存できる唯一の方法です。
  • Xmlファイルからすべてのアイテムをロードすることを選択した理由は、このゲームが40,000以上のアイテムを使用できる可能性があるためです。私の数学が正しければ、各アイテムのクラスは多くのクラスです。

テイク、ドロップ、説明、ここでは移動など-それらのすべてが一般的なものとにかかわらず、項目が何であるかの適用のようにそれはそう「装備」を除いて、コマンドのリストを見て
ashes999

アイテムが取引不能の場合、「ドロップ」の代わりに「破壊」される可能性があります
マイクハント

率直に言って、多くのゲームは、DSL(ゲーム固有のカスタムスクリプト言語)を使用してこれを解決します。
corsiKa

1
RuneScapeの後にゲームをモデリングするための+1。私はそのゲームが大好きです。
ゼナディックス

回答:


23

ソフトウェア開発のすべてと同様に、理想的なソリューションはありません。あなたとあなたのプロジェクトに理想的なソリューションのみ。使用できるものを次に示します。

オプション1:手続き型モデル

古代の 時代遅れの古い学校の方法。

すべてのアイテムは、メソッドを持たない愚かな平凡なデータ型ですが、使用できるコンテキストメニューエントリを決定するなどのブールフラグなどisEdible、アイテムが持つことのできるすべてのプロパティを表す多くのパブリック属性ですisEquipable(おそらく他の属性の値から派生できる場合は、これらのフラグを使用しないでください)。プレーヤークラスには、アイテムを取得し、属性値に応じてアイテムを処理するためのすべてのロジックを持つEatEquipなどのメソッドをいくつか用意します。

オプション2:オブジェクト指向モデル

これは、継承とポリモーフィズムに基づいたOOP-by-the-bookソリューションです。

基底クラスの持っているItem他の項目が好む、そこからEdibleItemEquipableItemなどの継承を。基底クラスは、パブリックメソッドが必要GetContextMenuEntriesForBankGetContextMenuEntriesForFloorのリストを返すなどContextMenuEntry。各継承クラスはこれらのメソッドをオーバーライドして、このアイテムタイプに適したコンテキストメニューエントリを返します。また、基本クラスの同じメソッドを呼び出して、任意のアイテムタイプに適用可能なデフォルトエントリを取得することもできます。ContextMenuEntryメソッドを持つクラスになりPerform、それを作成した項目(あなたが使用することができますから、関連するメソッドを呼び出すデリゲートをこのため)。

XMLファイルからデータを読み取るときにこのパターンを実装する際の問題について:最初に各アイテムのXMLノードを調べてアイテムのタイプを判断し、次に各タイプに特化したコードを使用して適切なサブクラスのインスタンスを作成します。

オプション3:コンポーネントベースのモデル

このパターンは、継承の代わりに構成を使用し、Unityの残りの動作に近いものです。ゲームの構造に応じて、Unityコンポーネントシステムを使用することが可能/有益な場合がありますが、そうでない場合もありますが、走行距離は異なる場合があります。

クラスの各オブジェクトは、Item同様の構成要素のリストを有することになるEquipableEdibleSellableDrinkableアイテムは、1つまたは各成分のいずれを有していてもよい、など(例えば、チョコレート製のヘルメットの両方であろう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>

貴重な説明と、私の質問に答えるためにかかった時間をありがとう。どちらの方法を採用するかはまだ決定していませんが、あなたが提供した代替の実装方法に感謝しています。私は座って、どの方法が私にとってよりうまくいくかを考え、そこから行きます。ありがとう:)
マイクハント

@MikeHuntコンポーネントのリストモデルは、ファイルからアイテム定義を読み込む際にうまく機能するため、間違いなく調査する必要のあるものです。
user253751

@immibisは、最初の試みがそれに似ていたので、最初に試すものです。ありがとう:)
マイクハント

古い答えですが、「コンポーネントのリスト」モデルを実装する方法に関するドキュメントはありますか?
ジェフ

@Jeffゲームにこのパターンを実装し、その方法について質問がある場合は、新しい質問を投稿してください。
フィリップ

9

それで、マイク・ハント、あなたの質問は私にとても興味があったので、私は完全な解決策を実装することにしました。3時間さまざまなことを試した後、私はこの段階的な解決策になりました。

(これは非常に良いコードではないので、編集を受け入れることに注意してください)

コンテンツパネルの作成

(このパネルは、コンテキストメニューボタンのコンテナになります)

  • 新しく作る UI Panel
  • anchor左下に設定
  • width300に設定(必要に応じて)
  • パネルに新しいコンポーネントVertical Layout Groupを追加し、幅(高さではなく)のChild Alignment中央上部に設定しますChild Force Expand
  • パネルに新しいコンポーネントContent Size Fitterを追加し、Vertical Fit最小サイズに設定します
  • プレハブとして保存する

(この時点で、Panelは行に縮小されます。これは正常です。このパネルは、子としてボタンを受け入れ、ボタンを垂直に整列し、サマリーコンテンツの高さに合わせて伸縮します)

サンプルボタンの作成

(このボタンは、コンテキストメニュー項目を表示するためにインスタンス化およびカスタマイズされます)

  • 新しいUIボタンを作成
  • anchor左上に設定
  • ボタンに新しいコンポーネントを追加Layout Element設定し、Min Height30にをPreferred Height30に
  • プレハブとして保存する

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);
        }
    }
}
  • このスクリプトをCanvasに添付して、フィールドに入力します。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の答え​​を受け入れると思います。私はそれが非常に価値があり、将来この質問を見る人々が実際の実装とゲームでこの種のことを実装するためのいくつかの方法を持っていると思うので、私はあなたの答えをここに残します。どうもありがとうございました。私もこれに賛成票を投じました:)
マイクハント

1
どういたしまして。この答えが誰かを助けるならば、それは素晴らしいことです。
Exerion
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.