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 平台
最后附上示例工程:下载地址
如果大家有其他解决办法,欢迎留言分享^_^