简体中文 | English

    本教程解释了如何让Unity的Canvas响应虚拟Cursor,请在Android手机中测试test.unity场景。本教程的方法也可用于开发GearVR、SteamVR等其他平台的应用。

前言

    与传统手游不同,VR游戏中常常使用位于屏幕中央的准心来完成与GUI的交互。Unity的Canvas是目前使用最广泛的GUI系统,但由于该系统是为传统手游设计的,所以许多开发者或多或少地遇到了交互的难题。

    在VR游戏中,我们推荐使用WorldSpace的Canvas,将整个GUI融入到游戏中。而ScreenSpace的GUI则会受双目视觉(瞳距)的影响,GUI会有叠影。在此我们仅讨论WorldSpace的Canvas。

教程

    下载,解压,并打开Unity(Unity5.3.4)项目TestDpnGraphicRaycaster2.zip,里面有一个叫做“test”的场景,打开这个场景。这个场景中有一个WorldSpace的Canvas,它是固定于场景中的,不会随着摄像机的移动而动。另外还有一个MainCamera,这个MainCamera会跟着头部而转动。为了演示的方便,我们在MainCamera下面挂了一个Cursor,可以看到准心位置。这时,生成并在Android手机中运行这个程序,那些Button并不会与Cursor交互。

    这个MainCamera下方挂了SensorFusion.cs的脚本,这个脚本让MainCamera跟随头部转动。

    为了与Cursor交互,我们需要做一些修改。我们在Canvas下面挂一个新的Component “DpnGraphicRaycaster”,用于取代Unity的GraphicRaycaster。所以需要禁用Unity的“GraphicRaycaster”,如图去掉钩子或者直接删掉这个Component。然后我们将渲染的MainCamera拖到DpnGraphicRaycaster的Interact Camera。这样所有的交互都是这个Interact Camera和Canvas之间的交互。

    注意,Interact Camera就是跟随头部转动的那个Camera。如果是DpnUnity插件,那么就应该是LeftEyeAnchor或者RightEyeAnchor。

    完成以上步骤之后再生成并运行,就可以看到能正常和这些Button交互了。Button可以接收到OnMouseEnter,OnMouseOver和OnMouseExit事件。

代码解析

    整个代码实际上非常简单,100行左右完成了所有的工作。主要是DpnGraphicRaycaster的Raycast和Update函数。

    我们继承了Unity标准的GraphicRaycaster类,用来完成Canvas的碰撞检测。

public class DpnGraphicRaycaster : GraphicRaycaster

重载Raycast函数

    这个函数原本用于鼠标或者触摸屏与Canvas的碰撞检测。但是在VR应用中,则是使用了Cursor,所以我们需要重载这个函数。此外做曲面Canvas的时候,也需要重载这个函数。

    它先从Canvas获得所有Graphic的引用。

IList<Graphic> list = GraphicRegistry.GetGraphicsForCanvas(_canvas);

    然后是矩形和射线的求交算法,判断射线是否与这个矩形碰撞。

Vector3[] corners = new Vector3[6];

g.rectTransform.GetWorldCorners(corners);

corners[4] = corners[0];

corners[5] = corners[1];


Plane rect_plane = new Plane();

rect_plane.Set3Points(corners[0], corners[1], corners[2]);


float enter;

if (false == rect_plane.Raycast(ray, out enter)) continue;


Vector3 intersection = ray.GetPoint(enter);

bool is_inside = true;

for (int j = 0; j < 4; ++j)

{

Vector3 b = corners[j + 1];

Vector3 ba = corners[j + 0] - b;

Vector3 bc = corners[j + 2] - b;

Vector3 bp = intersection - b;


Vector3 cross_ba_bp = Vector3.Cross(ba, bp);

Vector3 cross_bp_bc = Vector3.Cross(bp, bc);


if (Vector3.Dot(cross_ba_bp, cross_bp_bc) < 0)

{

is_inside = false;

break;

}

}


if (false == is_inside) continue;

    接着将碰撞的Graphic排序,放入返回列表。

sorted_graphic.Sort((g1, g2) => g2.graph.depth.CompareTo(g1.graph.depth));


for (int i = 0; i < sorted_graphic.Count; ++i)

{

var castResult = new RaycastResult

{

gameObject = sorted_graphic[i].graph.gameObject,

module = this,

distance = (sorted_graphic[i].worldPos - ray.origin).magnitude,

index = resultAppendList.Count,

depth = sorted_graphic[i].graph.depth,

worldPosition = sorted_graphic[i].worldPos,

};

resultAppendList.Add(castResult);

}

    至此,已经可以看到Cursor进入、移出、点击这些Graphic时,它会在外观上有反应。这是Unity3D内部完成的工作。

重载Update函数

    前面已经实现了Raycast函数,并且Graphic的外观会随着Cursor而发生变化。但是这些Graphic并不会响应OnMouseEnter、OnMouseOver和OnMouseExit事件。于是我们需要另外重载Update函数。

    这个函数更加简单,首先利用Raycast函数获得碰撞的Graphic List。

// raycast

List<RaycastResult> list = new List<RaycastResult>();

Raycast(null, list);

    然后比较上一帧与这一帧的List,查找哪些Graphic是这一帧新添的,就发送“OnMouseEnter”消息和“OnMouseOver消息”。哪些是最近两帧都有的,那么就只发送“OnMouseOver”消息。

// enter and over

foreach (GameObject co in cur_objects)

{

if (false == _last_objects.Contains(co))

{

co.SendMessage("OnMouseEnter");

}

co.SendMessage("OnMouseOver");

}


    而上一帧有,这一帧没有的Graphic,则发送“OnMouseExit”消息。

// exit

foreach (GameObject lo in _last_objects)

{

if (false == cur_objects.Contains(lo))

{

lo.SendMessage("OnMouseExit");

}

}

    至此,就可以用Cursor模拟出鼠标的消息了。

    我们可以在那些Graphic挂一个脚本来检查是否能收到这些消息。

public class OnMouseEvents : MonoBehaviour

{

void Start()

{

Debug.Log("UGUI: Start " + transform.name);

}

void OnMouseEnter()

{

Debug.Log("UGUI: OnMouseEnter " + transform.name);

}

void OnMouseOver()

{

Debug.Log("UGUI: OnMouseOver " + transform.name);

}

void OnMouseExit()

{

Debug.Log("UGUI: OnMouseExit " + transform.name);

}

}

其他事项

    目前本方法还不能支持曲面的Canvas,需要另外实现Raycast的方法。

    本文中的cursor会由于Timewarp算法而抖动,有两个方法。如果场景简单,则可以设置Application.targetFrameRate为机器屏幕刷新率,同时设置Edit->PlayerSetting->Time的”Fixed Timestep”为 1/屏幕刷新率。但一般来说,应用的场景会比较复杂,那就需要专门一个通道给Cursor渲染,这个通道不进行Timewarp。这需要SDK的支持,大朋将在近期提供这种支持。

    DpnGraphicsRaycaster中还有几个参数”Ignore Reversed Graphic”、”Blocking Object”、”Blocking Mask”,都没有实现,可以参考DpnGraphicRaycaster的代码自己实现一下。

    DpnGraphicRaycaster中还有Screen Position,用于指定交互的Cursor在屏幕的上的位置。位置。一般在正中心(0.5,0.5),左上角为(0,0),右下角为(1,1)。


Copyright © 2015deepoon.com,All Rights Reserved 沪ICP备15019466号-1 上海乐相科技有限公司