UGUI 分屏显示BUG解决办法

UGUI提供了分屏显示功能,可以通过 Canvas 的 TargetDisplay 属性指定在哪个显示器进行显示,但是经过实践发现,该功能存在一些Bug,主要是两个分屏的UGUI会相互干扰,(点击屏幕A内的UI,其他界面相应位置也会被点击),根据官方的说法,由于当前版本(2017.4.2f2)多屏显示存在问题,所以UGUI的多屏显示无法进行修复。(吐槽下Unity对PC端的支持和更新真是差)

为了解决该问题,需要研究一下UGUI的源码,找到问题的原因和解决办法,这里使用的是 ILSpy 对Dll进行反编译,也可以直接去 BitBucket 下载对应版本的 UGUI源码 进行查看,造成两个屏幕操作相互干扰主要原因是由射线检测引起,所以这里需要对GraphicRaycaster进行分析和修改,所幸,Raycast检测接口是 virtual 可重写的,只需要继承 GraphicRaycaster 对 Raycast接口进行重写即可,直接上代码。

using System.Collections.Generic;
using System.Runtime.InteropServices;
using System;
using UnityEngine;

#if UNITY_EDITOR
using System.Reflection;
using UnityEditor;
#endif

public class MultiDisplayUtil
{
    delegate bool MonitorEnumDelegate(IntPtr hMonitor, IntPtr hdcMonitor, ref Rect lprcMonitor, IntPtr dwData);

    [DllImport("user32.dll")]
    static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, MonitorEnumDelegate lpfnEnum, IntPtr dwData);

    [DllImport("user32.dll")]
    static extern bool GetMonitorInfo(IntPtr hMonitor, ref MonitorInfo lpmi);

    [DllImport("user32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool GetCursorPos(out POINT lpPoint);

    [StructLayout(LayoutKind.Sequential)]
    public struct POINT
    {
        public int x;
        public int y;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct Rect
    {
        public int left;
        public int top;
        public int right;
        public int bottom;

        public bool Contains(int x, int y)
        {
            return x >= this.left && x < this.right && y >= this.top && y < this.bottom;
        }
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
    internal struct MonitorInfo
    {
        /// <summary>
        /// The size, in bytes, of the structure. Set this member to sizeof(MONITORINFOEX) (72) before calling the GetMonitorInfo function. 
        /// Doing so lets the function determine the type of structure you are passing to it.
        /// </summary>
        public int size;

        /// <summary>
        /// A RECT structure that specifies the display monitor rectangle, expressed in virtual-screen coordinates. 
        /// Note that if the monitor is not the primary display monitor, some of the rectangle's coordinates may be negative values.
        /// </summary>
        public Rect monitor;

        /// <summary>
        /// A RECT structure that specifies the work area rectangle of the display monitor that can be used by applications, 
        /// expressed in virtual-screen coordinates. Windows uses this rectangle to maximize an application on the monitor. 
        /// The rest of the area in rcMonitor contains system windows such as the task bar and side bars. 
        /// Note that if the monitor is not the primary display monitor, some of the rectangle's coordinates may be negative values.
        /// </summary>
        public Rect workArea;

        /// <summary>
        /// The attributes of the display monitor.
        /// 
        /// This member can be the following value:
        ///   1 : MONITORINFOF_PRIMARY
        /// </summary>
        public uint flags;
    }

    /// <summary>
    /// The struct that contains the display information
    /// </summary>
    public class DisplayInfo
    {
        public string Availability { get; set; }
        public string ScreenHeight { get; set; }
        public string ScreenWidth { get; set; }
        public Rect MonitorArea { get; set; }
        public Rect WorkArea { get; set; }
    }

    /// <summary>
    /// Collection of display information
    /// </summary>
    public class DisplayInfoCollection : List<DisplayInfo>
    {
    }

    /// <summary>
    /// Returns the number of Displays using the Win32 functions
    /// </summary>
    /// <returns>collection of Display Info</returns>
    public static DisplayInfoCollection GetDisplays()
    {
        DisplayInfoCollection col = new DisplayInfoCollection();

        EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero,
            delegate (IntPtr hMonitor, IntPtr hdcMonitor, ref Rect lprcMonitor, IntPtr dwData)
            {
                MonitorInfo mi = new MonitorInfo();
                mi.size = Marshal.SizeOf(mi);
                bool success = GetMonitorInfo(hMonitor, ref mi);
                if (success)
                {
                    DisplayInfo di = new DisplayInfo();
                    di.ScreenWidth = (mi.monitor.right - mi.monitor.left).ToString();
                    di.ScreenHeight = (mi.monitor.bottom - mi.monitor.top).ToString();
                    di.MonitorArea = mi.monitor;
                    di.WorkArea = mi.workArea;
                    di.Availability = mi.flags.ToString();
                    col.Add(di);
                }
                return true;
            }, IntPtr.Zero);
        return col;
    }


    public static int GetCurrentDisplay()
    {
#if UNITY_EDITOR
        if (!IsGameWindow(EditorWindow.focusedWindow))
            return -1;

        return GetGameViewDisplay(EditorWindow.focusedWindow);
#else
        var displays = GetDisplays();
        POINT cursorPos;
        GetCursorPos(out cursorPos);
        for (int i = 0; i < displays.Count; i++)
        {
            var v = displays[i];
            if (v.WorkArea.Contains(cursorPos.x, cursorPos.y))
                return i;
        }

        return -1;
#endif
    }

#if UNITY_EDITOR

    public static bool IsGameWindow(EditorWindow window)
    {
        if (window == null)
            return false;

        if (window.GetType() != GetGameWindowType())
            return false;

        return true;
    }

    public static UnityEngine.Rect GetGameViewPosition(EditorWindow window)
    {
        return window.position;
    }

    public static int GetGameViewDisplay(EditorWindow window)
    {
        Type viewType = GetGameWindowType();
        FieldInfo fieldInf = viewType.GetField("m_TargetDisplay", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
        return (int)fieldInf.GetValue(window);
    }

    private static Type GetGameWindowType()
    {
        Type gameViewType = Type.GetType("UnityEditor.GameView,UnityEditor");
        return gameViewType;
    }

#endif

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

public enum DisplayIndex : int
{
    Display1 = 0,
    Display2,
    Display3,
    Display4,
    Display5,
    Display6,
    Display7,
    Display8,
}

public class MultiDisplayGraphicRaycaster : GraphicRaycaster
{
    public DisplayIndex DisplayIndex
    {
        get
        {
            return displayIndex;
        }
        set
        {
            displayIndex = value;
        }
    }

    //这里自己重新定义了目标显示器字段,其实完全没有必要
    //完全可以使用Canvas的targetDisplay或者eventCamera的targetDisplay属性
    [SerializeField]
    private DisplayIndex displayIndex;

    public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
    {
        if (MultiDisplayUtil.GetCurrentDisplay() != (int)displayIndex)
            return;

        base.Raycast(eventData, resultAppendList);
    }
}

使用时只需要将原来的 GraphicRaycaster 替换成 MultiDisplayGraphicRaycaster 即可。

至此基本解决了多屏之间的相互干扰问题,但是还存在一个小问题,就是所有屏幕只能有一个物体被选中,如果是一个用户去操作多个屏幕,这个问题就不是问题,但是如果不同用户对应不同的屏幕,就会产生奇怪的情况,以 Dropdown 为例,当 <用户A> 打开 Dropdown 选框还未进行选择,此时恰好 <用户B> 进行了操作,那么A的Dropdown就会关闭,对<用户A>来说效果很诡异。
问题的解决办法也很简单,当前选择对象是存储在 EventSystem 当中,默认所有的Canvas 公用一个 EventSystem ,所以这里只需要为每个 屏幕分配一个单独的 EventSystem 即可,同样需要对 EventSystem进行一些扩展,代码如下:

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

public class MultiDisplayEventSystem : EventSystem
{
    [SerializeField]
    DisplayIndex displayIndex;

    protected override void Update()
    {
        if (MultiDisplayUtil.GetCurrentDisplay() == (int)displayIndex)
        {
            if (current != this) current = this;
        }
        else
            return;

        base.Update();
    }
}

替换了EventSystem 之后,发现还是Dropdown 组件还是会被其他屏幕操作影响,这是由于 Dropdown 组件在操作过程中,会生成一个Blocker(用来检测点击背景),Blocker上会添加GraphicRaycaster 组件,其实不只是Dropdown,只要使用GraphicRaycaster的组件,都会存在这种问题。所以如果需要各屏幕之间Dropdown 操作不相互干扰,需要做一些修改采用使用,代码如下:

using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;

public class MultiDisplayDropdown : Dropdown
{
    protected GameObject m_DropdownComp
    {
        get
        {
            if (baseDropdownhandler != null)
            {
                return baseDropdownhandler.GetValue(this) as GameObject;
            }
            return null;
        }
    }

    private FieldInfo _baseDropdownHandler = null;
    FieldInfo baseDropdownhandler
    {
        get
        {
            if (_baseDropdownHandler == null)
            {
                _baseDropdownHandler = typeof(Dropdown).GetField("m_Dropdown", BindingFlags.NonPublic | BindingFlags.Instance);
            }
            return _baseDropdownHandler;
        }
    }

    protected override GameObject CreateBlocker(Canvas rootCanvas)
    {
        GameObject blockerObj = new GameObject("Blocker");
        RectTransform rectTransform = blockerObj.AddComponent<RectTransform>();
        rectTransform.SetParent(rootCanvas.transform, false);
        rectTransform.anchorMin = Vector3.zero;
        rectTransform.anchorMax = Vector3.one;
        rectTransform.sizeDelta = Vector2.zero;
        Canvas canvas = blockerObj.AddComponent<Canvas>();
        canvas.overrideSorting = true;
        Canvas component = this.m_DropdownComp.GetComponent<Canvas>();
        canvas.sortingLayerID = component.sortingLayerID;
        canvas.sortingOrder = component.sortingOrder - 1;
        var ray = blockerObj.AddComponent<MultiDisplayGraphicRaycaster>();
        InheritGraphicRaycaster(ray);
        Image image = blockerObj.AddComponent<Image>();
        image.color = Color.clear;
        Button button = blockerObj.AddComponent<Button>();
        button.onClick.AddListener(new UnityAction(this.Hide));
        return blockerObj;
    }

    protected override GameObject CreateDropdownList(GameObject template)
    {
        GameObject obj = Instantiate(template);
        var ray = obj.GetComponent<MultiDisplayGraphicRaycaster>();
        InheritGraphicRaycaster(ray);
        return obj;
    }

    private void InheritGraphicRaycaster(MultiDisplayGraphicRaycaster caster)
    {
        var parentRaycaster = gameObject.GetComponentInParent<MultiDisplayGraphicRaycaster>();
        if (parentRaycaster != null)
        {
            caster.DisplayIndex = parentRaycaster.DisplayIndex;
        }
    }
}

使用时将 Dropdown 组件替换成MultiDisplayDropdwon ,同时在 Dropdown 物体下面找到 Templete 物体,添加 MultiDisplayGraphicRaycaster 组件。

这种方法只适用于 Windows 平台

最后附上示例工程:下载地址

如果大家有其他解决办法,欢迎留言分享^_^

猜你喜欢

转载自blog.csdn.net/salvare/article/details/81280839
今日推荐