最近需要在SteamVR上做类似VRTK的射线和UI交互的功能。我自己开发的时候的思路是在右手手柄上加一个LineRender组件,然后从手柄的位置为起点,手柄正方向transform.forward*10000作为终点画一根线。然后给Canvas上的UI组件添加碰撞体。在Update中利用碰撞检测去调用UI的事件。我的这种方法对于按钮或者Toggle这种hi直接触发的实现方案比较简单,但是如果是操作滑动条啊之类的包含位置操作的,就会处理的逻辑相对复杂些。后来我的同事给出了更好的解决方法:重写Unity 的 BaseInputModel,让射线跟UI交互就跟我们的鼠标跟UI交互一样。话不多说,先上代码:
/****************************************************
文件:VRInputModel.cs
作者:Paul 邮箱: 794451358@qq.com
日期:2020/12/18 11:22:22
功能:为VR UI重写BaseInputModel
*****************************************************/
using UnityEngine;
using System.Collections.Generic;
using System.Collections;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace PFarmeWork
{
public class VRInputModel : BaseInputModule
{
/// <summary>
/// 事件摄像机
/// </summary>
public Camera eventCamera;
/// <summary>
/// 是否执行UI操作标志位
/// </summary>
public bool isExecute = false;
/// <summary>
/// 指针事件数据
/// </summary>
public PointerEventData Data { get; private set; } = null;
protected override void Start()
{
Data = new PointerEventData(eventSystem);
//设定射线的起始点为事件相机的视窗中心
Data.position = new Vector2(eventCamera.pixelWidth / 2, eventCamera.pixelHeight / 2);
}
public override void Process()
{
//发射射线检测UI
eventSystem.RaycastAll(Data, m_RaycastResultCache);
//从由近到远的射线碰撞结果m_RaycastResultCache中获取第一个(最近)的碰撞结果对应的射线结果
Data.pointerCurrentRaycast = FindFirstRaycast(m_RaycastResultCache);
//先处理射线点进入或移出UI游戏物体(这个事件让继承IPointerEnterHandler和IPointerExitHandler中的事件触发)
HandlePointerExitAndEnter(Data, Data.pointerCurrentRaycast.gameObject);
//按下点击按钮的标志位
if (isExecute)
{
ProcessPress();
}
else
{
ProcessRelease();
}
ExecuteEvents.Execute(Data.pointerDrag, Data, ExecuteEvents.dragHandler);
}
private void ProcessPress()
{
print("process");
//把当前的射线信息赋值给光标按下射线
Data.pointerPressRaycast = Data.pointerCurrentRaycast;
//把光标按下射线对应的游戏物体赋值给指针数据中的pointPress
Data.pointerPress = ExecuteEvents.GetEventHandler<IPointerClickHandler>(Data.pointerPressRaycast.gameObject);
//执行光标按下事件,该事件会让继承了IPointerClickHandler的派生类中的事件触发
ExecuteEvents.Execute(Data.pointerPress, Data, ExecuteEvents.pointerDownHandler);
//把光标按下射线对应的游戏物体赋值给指针数据中的pointDrag
Data.pointerDrag = ExecuteEvents.GetEventHandler<IDragHandler>(Data.pointerPressRaycast.gameObject);
//执行光标开始拖动事件,该事件会让继承了IIDragHandler的派生类中的事件触发
ExecuteEvents.Execute(Data.pointerDrag, Data, ExecuteEvents.beginDragHandler);
}
private void ProcessRelease()
{
GameObject pointRelease = ExecuteEvents.GetEventHandler<IPointerClickHandler>(Data.pointerCurrentRaycast.gameObject);
if (Data.pointerPress == pointRelease)
ExecuteEvents.Execute(Data.pointerPress, Data, ExecuteEvents.pointerClickHandler);
ExecuteEvents.Execute(Data.pointerPress, Data, ExecuteEvents.pointerUpHandler);
ExecuteEvents.Execute(Data.pointerDrag, Data, ExecuteEvents.endDragHandler);
Data.pointerPress = null;
Data.pointerDrag = null;
Data.pointerCurrentRaycast.Clear();
}
}
}
在我们的手柄对应的输入检测函数中,将IsExecute设置为true。
/****************************************************
文件:InteractionSys.cs
作者:Paul 邮箱: 794451358@qq.com
日期:2020/11/6 11:48:55
功能:交互系统主要的交互功能函数
*****************************************************/
using UnityEngine;
using System.Collections.Generic;
using System.Collections;
using PFarmeWork;
using Valve.VR.InteractionSystem;
using UnityEngine.Events;
using DG.Tweening;
using Valve.VR;
using UnityEditor;
using UniRx;
using System;
namespace DaschowStreet
{
public class InteractionSys : MonoSingleton<InteractionSys>
{
public SteamVR_Action_Boolean uiClick;
public SteamVR_Action_Boolean m_UIPoint;
public SteamVR_Action_Boolean m_UI;
private GameObject m_Player;
public Transform rightHand;
private VRInputModel m_InputModel;
private LineRenderer m_Line;
private void Awake()
{
if (GetComponent<CustomInput>())
{
m_UIPoint = GetComponent<CustomInput>().UIPoint;
m_Player = GameObject.FindGameObjectWithTag("Player");
m_UI = GetComponent<CustomInput>().UI;
m_UIPoint.onState += M_UIPoint_onState;
m_UIPoint.onStateUp += M_UIPoint_onStateUp;
m_Line = GetComponent<LineRenderer>();
m_InputModel = rightHand.GetComponent<VRInputModel>();
}
else
{
Destroy(gameObject);
}
}
private void M_UIPoint_onStateUp(SteamVR_Action_Boolean fromAction, SteamVR_Input_Sources fromSource)
{
m_InputModel.isExecute = false;
var line = GetComponent<LineRenderer>();
line.SetPosition(0, Vector3.zero);
line.SetPosition(1, Vector3.zero);
}
private void M_UIPoint_onState(SteamVR_Action_Boolean fromAction, SteamVR_Input_Sources fromSource)
{
if (uiClick.stateDown)
{
m_InputModel.isExecute = true;
}
else
{
m_InputModel.isExecute = false;
}
var line = GetComponent<LineRenderer>();
line.SetPosition(0, rightHand.position);
line.SetPosition(1, rightHand.forward*10000);
}
}
}
然后,最重要的一步:在手柄上添加一个Camera作为事件相机,所有的UI事件都是基于这个相机触发的(但是这个相机的enable可以失能)。把这个事件相机拖到第一个脚本的事件相机eventCamera 和Canvas中的EventCamera中
关于第一个脚本,如果理解不了的,可以先看这个大佬的博客:https://blog.csdn.net/ecidevilin/article/details/52503595。理解了BaseInputModel和EventSystem后再去看代码会容易理解些。