Unity 游戏开发者必备,如何实现适用于Windows与移动平台双端的轮盘交互式提示菜单?轮盘菜单技术揭秘

Unity 3D 从原理到实践:打造独特的游戏提示菜单系统

效果:

请添加图片描述

介绍

在当今游戏开发领域,用户体验已经成为吸引玩家并保持他们的关键因素之一。交互式提示菜单作为游戏界面设计的一部分,正日益受到开发者们的重视。本文将探索交互式提示菜单在游戏界面设计中的创新应用和重要性,以及如何通过它们提升游戏的用户体验。我们将深入了解交互式提示菜单的设计原理、技术实现和最佳实践,并探讨它们对游戏体验的影响。无论您是游戏开发者、界面设计师还是对游戏用户体验感兴趣的读者,本文都将为您揭示交互式提示菜单的奥秘,帮助您打造出色的游戏界面设计。

源码

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public enum WheelStateType
{
    Expand,
    Hide,
    Single,
    Close
}

public class HintWheel : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IDragHandler
{
    [Header("外圈放大系数")]
    public float enlargementFactor = 2;
    [Header("内圈取消距离")]
    public float minWheelDistance = 50;
    [Header("选中节点放大系数")]
    public float selectWheelSize = 1.2f;
    [Header("节点信息距离中心偏移量")]
    [Range(1f, 3)]
    public float iconOffset = 2.5f;
    private float lerpValue = 0;

    private WheelStateType wheelStateType;

    private Vector3 wheelChangeSizeValue;
    private Vector3 wheelOriginSize;
    private Vector3 wheelMaxSize;
    private Vector3 iconOriginSize;
    private Vector2 inputDirection = Vector2.zero;

    private bool _isLerp = false;
    private bool _isChangeSize = false;
    private bool _isChangeSelectWheelSize = false;

    private List<WheelNode> _selectInfos = new List<WheelNode>();
    private List<WheelNode> _currentAllActivateNodes = new List<WheelNode>();

    private GraphicRaycaster _ray;
    private StandaloneInputModule _input;
    private CanvasGroup _wheelAlpha;
    private Transform _defaultHintInfo;
    private WheelNode _currentSelectNode;
    private Image _wheel, _bgWheel, _cursorIcon;

    private bool IsLerp
    {
        get => _isLerp;
        set
        {
            lerpValue = 0;
            _isLerp = value;
        }
    }

    private WheelNode CurrentSelectNode
    {
        get => _currentSelectNode;
        set
        {
            if (_currentSelectNode != value)
            {
                IsChangeSelectWheelSize = true;
                _currentSelectNode = value;
            }
        }
    }

    private bool IsChangeSize
    {
        get => _isChangeSize;
        set
        {
            _isChangeSize = value;
            IsLerp = value;
        }
    }

    private bool IsChangeSelectWheelSize
    {
        get => _isChangeSelectWheelSize;
        set
        {
            _isChangeSelectWheelSize = value;
            IsLerp = value;
        }
    }

    public WheelStateType WheelStateType
    {
        get => wheelStateType;
        set
        {
            wheelStateType = value;
            switch (wheelStateType)
            {
                case WheelStateType.Expand:
                    _defaultHintInfo.gameObject.SetActive(false);
                    CurrentSelectNode = null;
                    _cursorIcon.GetComponent<RectTransform>().anchoredPosition = Vector3.zero;
                    _cursorIcon.gameObject.SetActive(true);
                    break;
                case WheelStateType.Hide:
                    _defaultHintInfo.gameObject.SetActive(true);
                    CurrentSelectNode = null;
                    _cursorIcon.GetComponent<RectTransform>().anchoredPosition = Vector3.zero;
                    _cursorIcon.gameObject.SetActive(false);
                    break;
                case WheelStateType.Single:
                    _defaultHintInfo.gameObject.SetActive(false);
                    wheelChangeSizeValue = wheelOriginSize;
                    IsChangeSize = true;
                    CurrentSelectNode = _currentAllActivateNodes[0];
                    _currentAllActivateNodes[0].wheel.fillAmount = 0;
                    _currentAllActivateNodes[0].iconPoint.localPosition = Vector3.zero;
                    _currentAllActivateNodes[0].iconPoint.localScale = iconOriginSize;
                    _cursorIcon.GetComponent<RectTransform>().anchoredPosition = Vector3.zero;
                    _cursorIcon.gameObject.SetActive(false);
                    break;
                case WheelStateType.Close:
                    wheelChangeSizeValue = Vector3.zero;
                    IsChangeSize = true;
                    CurrentSelectNode = null;
                    _cursorIcon.GetComponent<RectTransform>().anchoredPosition = Vector3.zero;
                    _cursorIcon.gameObject.SetActive(false);
                    break;
                default:
                    break;
            }
        }
    }

    private void Start()
    {
        _ray = GetComponentInParent<GraphicRaycaster>();
        _input = FindObjectOfType<StandaloneInputModule>();
        _wheel = transform.Find("Wheel/WheelImage").GetComponent<Image>();
        _wheelAlpha = transform.Find("Wheel").GetComponent<CanvasGroup>();
        _bgWheel = transform.Find("BGImage").GetComponent<Image>();
        _cursorIcon = transform.Find("Cursor").GetComponent<Image>();
        _defaultHintInfo = transform.Find("SelectPointer/IconInfo");
        Initialize();
    }

    void Initialize()
    {
        wheelOriginSize = transform.localScale;
        wheelMaxSize = transform.localScale * enlargementFactor;
        iconOriginSize = _defaultHintInfo.localScale;
        _wheel.gameObject.SetActive(false);
        _cursorIcon.gameObject.SetActive(false);
        _defaultHintInfo.gameObject.SetActive(false);
        transform.localScale = Vector3.zero;
        WheelStateType = WheelStateType.Close;
#if UNITY_ANDROID
        _defaultHintInfo.GetComponentInChildren<Text>().text = "按住展开";
#elif UNITY_STANDALONE || UNITY_EDITOR
        defaultHintInfo.GetComponentInChildren<Text>().text = "按E展开";
#endif
    }

    private void LateUpdate()
    {
        if (IsLerp)
        {
            lerpValue += Time.deltaTime;
            if (lerpValue > 1) lerpValue = 1;

            if (IsChangeSize)
            {
                if (lerpValue == 1) IsChangeSize = false;
                transform.localScale = Vector3.Lerp(transform.localScale, wheelChangeSizeValue, lerpValue);

                Vector3 iconChangeSizeValue = Vector3.zero;
                switch (WheelStateType)
                {
                    case WheelStateType.Single:
                        iconChangeSizeValue = iconOriginSize;
                        break;
                    case WheelStateType.Expand:
                        iconChangeSizeValue = wheelChangeSizeValue * (1 / enlargementFactor) / enlargementFactor;
                        break;
                    case WheelStateType.Hide:
                        iconChangeSizeValue = Vector3.zero;
                        break;
                }

                foreach (var item in _currentAllActivateNodes)
                {
                    item.iconPoint.localScale = Vector3.Lerp(item.iconPoint.localScale, iconChangeSizeValue, lerpValue);
                }

                _bgWheel.transform.localScale = Vector3.Lerp(_bgWheel.transform.localScale,
                    WheelStateType == WheelStateType.Expand ?
                    wheelChangeSizeValue / 2 * (1 / enlargementFactor) / enlargementFactor : wheelChangeSizeValue, lerpValue);
                _wheelAlpha.alpha = Mathf.Lerp(_wheelAlpha.alpha, WheelStateType == WheelStateType.Expand ? 1 : 0, lerpValue);
            }

            if (IsChangeSelectWheelSize)
            {
                if (lerpValue == 1) IsChangeSelectWheelSize = false;
                if (WheelStateType == WheelStateType.Expand)
                {
                    foreach (var item in _currentAllActivateNodes)
                    {
                        if (item == CurrentSelectNode)
                        {
                            item.wheel.transform.localScale = Vector3.Lerp(item.wheel.transform.localScale,
                                wheelOriginSize * selectWheelSize, lerpValue);
                            item.iconPoint.localScale = Vector3.Lerp(item.iconPoint.localScale,
                                wheelChangeSizeValue * (1 / enlargementFactor) / enlargementFactor * selectWheelSize, lerpValue);
                        }
                        else
                        {
                            item.wheel.transform.localScale = Vector3.Lerp(item.wheel.transform.localScale, wheelOriginSize, lerpValue);
                            if (wheelChangeSizeValue == wheelMaxSize)
                                item.iconPoint.localScale = Vector3.Lerp(item.iconPoint.localScale,
                                    wheelChangeSizeValue * (1 / enlargementFactor) / enlargementFactor, lerpValue);
                        }
                    }
                }
            }
        }

        if (UnityEngine.Input.GetKeyDown(KeyCode.E))
        {
            switch (WheelStateType)
            {
                case WheelStateType.Hide:
                    ChangeWheelState(true);
                    break;
                case WheelStateType.Single:
                    CurrentSelectNode?.selectInfo.selectEvent?.Invoke();
                    break;
                default:
                    break;
            }
        }

        if (UnityEngine.Input.GetKeyUp(KeyCode.E))
        {
            if (WheelStateType == WheelStateType.Expand)
            {
                CurrentSelectNode?.selectInfo.selectEvent?.Invoke();
                ChangeWheelState(false);
            }
        }

        if (UnityEngine.Input.GetKey(KeyCode.E))
        {
            CheckSelectNode(UnityEngine.Input.mousePosition);
            AnalogCursorPoint(new Vector2(UnityEngine.Input.GetAxis("Mouse X"), UnityEngine.Input.GetAxis("Mouse Y")));
        }
    }

    public void OnPointerDown(PointerEventData eventData)
    {
        if (_ray == null || _input == null)
            return;
        switch (WheelStateType)
        {
            case WheelStateType.Hide:
                ChangeWheelState(true);
                break;
            case WheelStateType.Single:
                CurrentSelectNode?.selectInfo.selectEvent?.Invoke();
                break;
            default:
                break;
        }
    }

    public void OnPointerUp(PointerEventData eventData)
    {
        if (WheelStateType == WheelStateType.Expand)
        {
            CurrentSelectNode?.selectInfo.selectEvent?.Invoke();
            ChangeWheelState(false);
        }
    }

    public void OnDrag(PointerEventData eventData)
    {
        CheckSelectNode(eventData.position);
        AnalogCursorPoint(eventData);
    }

    private void ChangeWheelState(bool state)
    {
        if (_currentAllActivateNodes.Count > 1)
        {
            WheelStateType = state ? WheelStateType.Expand : WheelStateType.Hide;
            IsChangeSelectWheelSize = true;
            inputDirection = Vector2.zero;
            wheelChangeSizeValue = state ? wheelMaxSize : wheelOriginSize;
            _defaultHintInfo.gameObject.SetActive(!state);
            lerpValue = 0;
            IsChangeSize = true;
        }
    }

    private WheelNode CheckSelectNode(Vector2 checkPoint)
    {
        checkPoint = _cursorIcon.rectTransform.position;
        if (WheelStateType == WheelStateType.Expand)
        {
            float distance = Vector2.Distance(checkPoint, _bgWheel.rectTransform.position);
            if (distance < minWheelDistance)
            {
                CurrentSelectNode = null;
                return null;
            }
            else
            {
                WheelNode wheelNode = null;
                Vector2 localPos;
                RectTransformUtility.ScreenPointToLocalPointInRectangle(transform as RectTransform, checkPoint, _ray.eventCamera, out localPos);
                float angle = Mathf.Atan2(localPos.x, -localPos.y) * Mathf.Rad2Deg;
                angle = (angle < 0) ? angle + 360 : angle;
                float angleAreaValue = 360 / _currentAllActivateNodes.Count;
                float angleRange = 0;
                for (int i = 1; i <= _currentAllActivateNodes.Count; i++)
                {
                    if (angle > angleRange && angle < angleRange + angleAreaValue)
                    {
                        wheelNode = i == _currentAllActivateNodes.Count ? _currentAllActivateNodes[0] : _currentAllActivateNodes[i];
                        if (i == _currentAllActivateNodes.Count) break;
                    }
                    angleRange += angleAreaValue;
                }
                CurrentSelectNode = wheelNode;
                return wheelNode;
            }
        }
        else
        {
            return null;
        }
    }

    private void AnalogCursorPoint(PointerEventData ped)
    {
        if (WheelStateType == WheelStateType.Close) return;
        Vector2 position;
        RectTransformUtility.ScreenPointToLocalPointInRectangle(
            _wheel.rectTransform,
            ped.position,
            ped.pressEventCamera,
            out position);
        position = position + _wheel.rectTransform.sizeDelta / 2;
        position.x = (position.x / _wheel.rectTransform.sizeDelta.x);
        position.y = (position.y / _wheel.rectTransform.sizeDelta.y);

        float x = (_wheel.rectTransform.pivot.x == 1f) ? position.x * 2 + 1 : position.x * 2 - 1;
        float y = (_wheel.rectTransform.pivot.y == 1f) ? position.y * 2 + 1 : position.y * 2 - 1;

        inputDirection = new Vector2(x, y);
        inputDirection = (inputDirection.magnitude > 1) ? inputDirection.normalized : inputDirection;

        float newX = inputDirection.x * (_wheel.rectTransform.sizeDelta.x / 2);
        float newY = inputDirection.y * (_wheel.rectTransform.sizeDelta.y / 2);
        _cursorIcon.rectTransform.anchoredPosition = new Vector2(newX, newY);
    }

    private void AnalogCursorPoint(Vector2 mouseOffset)
    {
        if (WheelStateType == WheelStateType.Close) return;
        Vector2 newPosition = _cursorIcon.rectTransform.anchoredPosition + mouseOffset * 10;
        float distanceToCenter = newPosition.magnitude;
        if (distanceToCenter > _wheel.rectTransform.sizeDelta.x / 2)
        {
            newPosition = newPosition.normalized * _wheel.rectTransform.sizeDelta.x / 2;
        }
        _cursorIcon.rectTransform.anchoredPosition = newPosition;
    }

    public void ShowHintWheel(SelectInfo selectInfo)
    {
        WheelNode wheelNode = GetWheelNode(selectInfo);
        if (wheelNode == null)
        {
            wheelNode = AddWheelNode(selectInfo);
        }
        wheelNode.SetNodeState(true);
        UpdateAllActiveNodes();
        UpdateWheel();
    }

    public void HideHintWheel(SelectInfo selectInfo)
    {
        GetWheelNode(selectInfo).SetNodeState(false);
        UpdateAllActiveNodes();
        UpdateWheel();
    }

    private void UpdateWheel()
    {
        if (_currentAllActivateNodes.Count == 1)
        {
            WheelStateType = WheelStateType.Single;
        }
        else if (_currentAllActivateNodes.Count == 0)
        {
            WheelStateType = WheelStateType.Close;
        }
        else
            WheelStateType = WheelStateType == WheelStateType.Single ? WheelStateType.Hide : WheelStateType;
        float angle = 0;
        float iconDistance = _bgWheel.GetComponent<RectTransform>().sizeDelta.x / 2;
        iconDistance -= iconDistance / iconOffset;
        if (_currentAllActivateNodes.Count != 1)
        {
            foreach (var item in _currentAllActivateNodes)
            {
                item.wheel.transform.rotation = Quaternion.identity;
                item.wheel.transform.Rotate(0, 0, angle);

                Vector3 iconPosition = item.wheel.transform.localPosition +
                                       Quaternion.Euler(0, 0, angle - 180 - (360 / _currentAllActivateNodes.Count) / 2) * Vector3.up * iconDistance;
                item.iconPoint.localPosition = iconPosition;
                item.iconPoint.localScale = Vector3.zero;
                float value = (360f / _currentAllActivateNodes.Count) / 360f;
                item.wheel.fillAmount = value;
                angle += 360 / _currentAllActivateNodes.Count;
            }
        }
    }

    private void UpdateAllActiveNodes()
    {
        _currentAllActivateNodes.Clear();
        foreach (var item in _selectInfos)
        {
            if (item.GetNodeState())
            {
                _currentAllActivateNodes.Add(item);
            }
        }
    }

    private WheelNode GetWheelNode(SelectInfo selectInfo)
    {
        foreach (var item in _selectInfos)
        {
            if (item.selectInfo == selectInfo)
            {
                return item;
            }
        }
        return null;
    }

    private WheelNode AddWheelNode(SelectInfo selectInfo)
    {
        Image wheel = Instantiate(_wheel, _wheel.transform.parent);
        wheel.transform.SetAsFirstSibling();
        Transform iconInfo = Instantiate(_defaultHintInfo, _defaultHintInfo.parent);
        iconInfo.transform.SetAsFirstSibling();
        WheelNode wheelNode = new WheelNode(selectInfo, wheel, iconInfo, false);
        _selectInfos.Add(wheelNode);
        return wheelNode;
    }

    public class WheelNode
    {
        public SelectInfo selectInfo;
        public Transform iconPoint;
        public Image wheel;
        public Image icon;
        public Text info;
        private bool activate = false;

        public WheelNode(SelectInfo selectInfo, Image wheel, Transform iconInfo, bool activate)
        {
            this.selectInfo = selectInfo;
            this.wheel = wheel;
            this.activate = activate;
            iconPoint = iconInfo;
            icon = iconInfo.GetComponentInChildren<Image>(true);
            info = iconInfo.GetComponentInChildren<Text>(true);
            icon.sprite = selectInfo.sprite;
            info.text = selectInfo.content;
        }

        public void SetNodeState(bool state)
        {
            if (activate != state)
            {
                activate = state;
                wheel.gameObject.SetActive(state);
                iconPoint.gameObject.SetActive(state);
            }
        }

        public bool GetNodeState()
        {
            return activate;
        }
    }
}

public class SelectInfo
{
    public Sprite sprite;
    public string content;
    public string details;
    public Action selectEvent;
}

这段代码是一个用于实现提示菜单功能的脚本,主要用于在游戏中展示一个可拖拽的环形菜单,用于提供不同选项或功能的快捷访问。现在让我们逐步解释这个代码的各个部分以及它们的作用:

对外界开放的方法:

ShowHintWheel 和 HideHintWheel 方法
如需要在特定的时候展示一个按钮就调用ShowHintWheel传入参数SelectInfo类,同样关闭也是如此
SelectInfo 类:用于存储选项的相关信息,包括图标、内容、详情和自定义选择后执行的事件。

我们写一个例子来实现这个效果:

将此脚本挂载到场景中的一个物体上

using UnityEngine;
using UnityEngine.UI;

public class testSelect : MonoBehaviour
{
    public HintWheel hintWheel;
    public Sprite sprite;
    public string content = "";
    public Text text;
    public SelectInfo selectInfo = new SelectInfo();

    // Start is called before the first frame update
    void Start()
    {
        selectInfo.sprite = sprite;
        selectInfo.content = content;
        selectInfo.selectEvent = SelectFunc;
    }
    public void ChangeToggleState(bool state)
    {
        if (state)
        {
            hintWheel.ShowHintWheel(selectInfo);
        }
        else
        {
            hintWheel.HideHintWheel(selectInfo);
        }
    }
    public void SelectFunc()
    {
        if (text)
            text.text = content;
    }
}

我们使用Toggle来测试,当点击Toggle的时候便会出现按钮,按钮上显示的信息就是我们定义好的这两个字段

public Sprite sprite;
public string content = "";

在这里插入图片描述

当然,我们可以继续添加各种其他的功能,只要扩展SelectInfo类就可以了

接着我们解释一下HintWheel 的其他功能

HintWheel 类:实现了 IPointerDownHandler、IPointerUpHandler 和 IDragHandler 接口,用于处理指针事件(鼠标或触摸)。
字段和属性:
  • 包括外圈放大系数、内圈取消距离、选中节点放大系数等参数,用于控制菜单的大小和行为。
  • 一些私有字段用于跟踪菜单的状态、选中的节点等信息。

在Start方法中初始化一些变量和对象,包括获取场景中的 UI 元素,设置初始状态等。

LateUpdate 方法:在每帧更新时执行的逻辑,主要包括:
  • 处理菜单的展开、关闭、选中等逻辑。
  • 更新菜单的大小和状态。
  • 根据用户的输入更新菜单的位置和状态。
  • 检查选中的节点。

ChangeWheelState 方法:根据传入的状态展开或隐藏提示菜单,并更新菜单的状态和大小。

CheckSelectNode

此方法适应不同屏幕尺寸和分辨率: 使用了 RectTransformUtility.ScreenPointToLocalPointInRectangle 方法,可以将屏幕上的点转换为相对于 RectTransform 的本地坐标系中的点。这样可以确保在不同分辨率和屏幕尺寸下,提示菜单的交互功能仍然有效。使用了迭代方式处理多个节点的选择,使得可以轻松地添加、删除或修改提示菜单中的节点,从而实现不同需求下的菜单设计。代码使用了向量运算和角度计算等数学计算,以及简洁的迭代方式,保证了代码的执行效率和性能表现,能够在游戏中保持流畅的交互体验。

AnalogCursorPoint 方法

模拟光标在菜单上的移动,使用方法的重载处理Windows与移动平台双端的计算

UpdateWheel 和 UpdateAllActiveNodes 方法:更新菜单的显示状态和选项信息。

GetWheelNode 和 AddWheelNode 方法:根据选项信息获取或添加菜单节点。

WheelNode 类:表示提示菜单中的一个节点,包含选项信息、图标、文字等。

我们来看看UI是怎么做到像扇形一样只显示一部分的

首先我们创建一个Image
在这里插入图片描述
先在Image上添加图片,这是重点,不添加图片就不会有特定的属性
之后再调整Image的属性
在这里插入图片描述
那么调整FillAmount的值就可以了
请添加图片描述

Demo案例下载

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

唐沢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值