So, Mike Hunt, your question is 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 IS NOT very good code, so I will accept any editions)
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 setChild Alignment
to upper-center,Child Force Expand
to width (not height) - Add to a panel a new component
Content Size Fitter
and setVertical Fit
to Min Size - Save it as prefab
(at this point our Panel will shrink down to line. It's normal. This panel will accept buttons as childs, 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
, setMin 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 need 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 singletone 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 slotCanvas
.
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-dropsampleButton
prefab to corresponding slot.
Now, try to run it. When you right-clicking object, context menu should appear, populated with list that we made. Pressing buttons will print into console some text, and context menu will be destroyed.
P.S. sorry for my English.
Possible improvements:
- even more generic!
- better memory management (dirty links, not destroying panel, disabling)
- some fancy stuff