picoXR为Unity提供的Unity XR SDK是基于Unity XR实现的各个功能,针对手柄和头盔的各个按键摇杆事件的获取,均是采用的Unity XR提供的方法。目前UnityXR只提供了if判断的方式每帧监听的方式,还未提供事件接口等形式。
为方便读者理解后续API,先粗略介绍一下Unity XR监听按键摇杆的步骤。首先是获取需要监听的设备,然后判断此设备的某种行为是否发生,UnityXR是无差别对待各种品牌的设备的,每种品牌的设备的按键等也都不尽相同。自然设备的行为也是多种多样的,UnityXR提供了一组普遍的行为特征(CommonsUsages),但要具体使用需要根据使用的设备查阅设备具体按键和UnityXR提供的普遍特征的对应关系。本文主要介绍Pico产品,这是pico官网提供的映射关系文档链接https://sdk.picovr.com/docs/XRPlatformSDK/Unity/cn/chapter_five.html#id5
关于设备的获取和事件的监听UnityXR的官方文档有很好的解释和示例,更推荐大家去官网学习https://docs.unity3d.com/Manual/xr_input.html
首先需要引入UnityEngine.XR的命名空间,设备的获取主要用到的API为InputDevice和InputDevices
接下来介绍两种设备获取的方式
获取全部的设备
using UnityEngine.XR;
public List<InputDevice> GetAll()
{
List<InputDevice> deviceList = new List<InputDevice>();
//实际为传入list引用获取的InputDevice全部放在list中
InputDevices.GetDevices(deviceList);
return deviceList;
}
根据XRNode获取设备
//XRNode为枚举变量
//常用的有 Head LeftHand RightHand
//根据这些枚举可以轻松获得指定的头盔,左手柄,右手柄
InputDevice headController = InputDevices.GetDeviceAtXRNode(XRNode.Head);
InputDevice leftHandController = InputDevices.GetDeviceAtXRNode(XRNode.LeftHand);
InputDevice rightHandController = InputDevices.GetDeviceAtXRNode(XRNode.RightHand);
前面提到过,光获取到设备还需要知道设备上各个按键或摇杆的行为特征,注意行为特征一定要去官网查看其和设备各个按键的映射关系才能正确的开发。
这里拿Pico neo3的手柄映射关系举例
主要用到的API是InputDevice.TryGetFeatureValue(InputFeatureUsage usage,T value);
返回值为bool — 是否获取到行为特征值
参数1 — 行为特征(根据不同行为有不同的参数,例如按键型就是bool value代表是否按下,摇杆型就是Vector2,value代表偏移量axis)
判断是否按下了Trigger键
InputDevice device;
//省略device的获取 使用前要先获取
public void Test()
{
bool isDown; //记录是否按下
if(device.TryGetFeatureValue(CommonUsages.triggerButton,out isDown) && isDown)
{
//xxxxx 处理逻辑
}
}
判断是否移动了摇杆
InputDevice device;
public void Test()
{
Vector2 axis;
if (device.TryGetFeatureValue(usage, out axis) && !axis.Equals(Vector2.zero))
{
//xxxxx相关逻辑
}
}
对比Unity的其它组件提供的事件响应机制,大多靠event,委托和接口实现,而UnityXR提供的这个输入事件使用起来一般都是放在Update里每帧轮询检测,不足是显而易见的。
所以需要自行实现一组公开event事件,实现相应事件源,供其他观察者进行注册,即可解决上面提到的一些不足。
笔者自行实现了一个简易的InputEvent单例类,提供了按键类的Enter,Down,Up事件和摇杆类的Move事件。
InputEvent.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR;
using Common;
///
/// 提供各种输入事件
///
public class InputEvent:MonoSingleton<InputEvent>
{
//*************输入设别**************************
InputDevice leftHandController;
InputDevice rightHandController;
InputDevice headController;
//**************对外提供公开事件******************
#region public event
public Action onLeftTriggerEnter;
public Action onLeftTriggerDown;
public Action onLeftTriggerUp;
public Action onRightTriggerEnter;
public Action onRightTriggerDown;
public Action onRightTriggerUp;
public Action onLeftGripEnter;
public Action onLeftGripDown;
public Action onLeftGripUp;
public Action onRightGripEnter;
public Action onRightGripDown;
public Action onRightGripUp;
public Action onLeftAppButtonEnter;
public Action onLeftAppButtonDown;
public Action onLeftAppButtonUp;
public Action onRightAppButtonEnter;
public Action onRightAppButtonDown;
public Action onRightAppButtonUp;
public Action onLeftJoyStickEnter;
public Action onLeftJoyStickDown;
public Action onLeftJoyStickUp;
public Action onRightJoyStickEnter;
public Action onRightJoyStickDown;
public Action onRightJoyStickUp;
public Action<Vector2> onLeftJoyStickMove;
public Action<Vector2> onRightJoyStickMove;
public Action onLeftAXButtonEnter;
public Action onLeftAXButtonDown;
public Action onLeftAXButtonUp;
public Action onLeftBYButtonEnter;
public Action onLeftBYButtonDown;
public Action onLeftBYButonUp;
public Action onRightAXButtonEnter;
public Action onRightAXButtonDown;
public Action onRightAXButtonUp;
public Action onRightBYButtonEnter;
public Action onRightBYButtonDown;
public Action onRightBYButtonUp;
#endregion
//提供状态字典独立记录各个feature的状态
Dictionary<string, bool> stateDic;
//单例模式提供的初始化函数
protected override void Init()
{
base.Init();
leftHandController = InputDevices.GetDeviceAtXRNode(XRNode.LeftHand);
rightHandController = InputDevices.GetDeviceAtXRNode(XRNode.RightHand);
headController = InputDevices.GetDeviceAtXRNode(XRNode.Head);
stateDic = new Dictionary<string, bool>();
}
//*******************事件源的触发**************************
///
/// 按钮事件源触发模板
///
/// 设备
/// 功能特征
/// 开始按下按钮事件
/// 按下按钮事件
/// 抬起按钮事件
private void ButtonDispatchModel(InputDevice device,InputFeatureUsage<bool> usage,Action btnEnter,Action btnDown,Action btnUp)
{
Debug.Log("usage:" + usage.name);
//为首次执行的feature添加bool状态 -- 用以判断Enter和Up状态
string featureKey = device.name + usage.name;
if(!stateDic.ContainsKey(featureKey))
{
stateDic.Add(featureKey, false);
}
bool isDown;
if(device.TryGetFeatureValue(usage,out isDown) && isDown)
{
if(!stateDic[featureKey])
{
stateDic[featureKey] = true;
if(btnEnter != null)
btnEnter();
}
if(btnDown!=null)
btnDown();
}
else
{
if(stateDic[featureKey])
{
if(btnUp!=null)
btnUp();
stateDic[featureKey] = false;
}
}
}
///
/// 摇杆事件源触发模板
///
/// 设备
/// 功能特征
/// 移动摇杆事件
private void JoyStickDispatchModel(InputDevice device,InputFeatureUsage<Vector2> usage,Action<Vector2> joyStickMove)
{
Vector2 axis;
if (device.TryGetFeatureValue(usage, out axis) && !axis.Equals(Vector2.zero))
{
if(joyStickMove!=null)
joyStickMove(axis);
}
}
//******************每帧轮询监听事件***********************
private void Update()
{
ButtonDispatchModel(leftHandController, CommonUsages.triggerButton, onLeftTriggerEnter, onLeftTriggerDown, onLeftTriggerUp);
ButtonDispatchModel(rightHandController, CommonUsages.triggerButton, onRightTriggerEnter, onRightTriggerDown, onRightTriggerUp);
ButtonDispatchModel(leftHandController, CommonUsages.gripButton, onLeftGripEnter, onLeftGripDown, onLeftGripUp);
ButtonDispatchModel(rightHandController, CommonUsages.gripButton, onRightGripEnter, onRightGripDown, onRightGripUp);
ButtonDispatchModel(leftHandController, CommonUsages.primaryButton, onLeftAXButtonEnter, onLeftAXButtonDown, onLeftAXButtonUp);
ButtonDispatchModel(rightHandController, CommonUsages.primaryButton, onRightAXButtonEnter, onRightAXButtonDown, onRightAXButtonUp);
ButtonDispatchModel(leftHandController, CommonUsages.secondaryButton, onLeftBYButtonEnter, onLeftBYButtonDown, onLeftBYButonUp);
ButtonDispatchModel(rightHandController, CommonUsages.secondaryButton, onRightBYButtonEnter, onRightBYButtonDown, onRightBYButtonUp);
ButtonDispatchModel(leftHandController, CommonUsages.primary2DAxisClick, onLeftJoyStickEnter, onLeftJoyStickDown, onLeftJoyStickUp);
ButtonDispatchModel(rightHandController, CommonUsages.primary2DAxisClick, onRightJoyStickEnter, onRightJoyStickDown, onRightJoyStickUp);
ButtonDispatchModel(leftHandController, CommonUsages.menuButton, onLeftAppButtonEnter, onLeftAppButtonDown, onLeftAppButtonUp);
ButtonDispatchModel(rightHandController, CommonUsages.menuButton, onRightAppButtonEnter, onRightAppButtonDown,onRightAppButtonUp);
JoyStickDispatchModel(leftHandController, CommonUsages.primary2DAxis, onLeftJoyStickMove);
JoyStickDispatchModel(rightHandController, CommonUsages.primary2DAxis, onRightJoyStickMove);
}
}
单例模式采用的是脚本单例,更多具体的单例模式有兴趣的读者可以自行搜索一下。
MonoSingleton.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Common
{
///
///脚本单例类,负责为唯一脚本创建实例
///
public class MonoSingleton<T> : MonoBehaviour where T : MonoSingleton<T> //注意此约束为T必须为其本身或子类
{
/*
相较于直接在需要唯一创建的脚本中创建实例,Awake初始化的过程需要解决的问题
1.代码重复
2.在Awake里面初始化,其它脚本在Awake中调用其可能会为Null的异常情况
*/
//解决1:使用泛型创建实例 解决2:使用按需加载(即有其它脚本调用时在get中加载)
private static T instance; //创建私有对象记录取值,可只赋值一次避免多次赋值
public static T Instance
{
//实现按需加载
get
{
//当已经赋值,则直接返回即可
if (instance != null) return instance;
instance = FindObjectOfType<T>();
//为了防止脚本还未挂到物体上,找不到的异常情况,可以自行创建空物体挂上去
if (instance == null)
{
//如果创建对象,则会在创建时调用其身上脚本的Awake即调用T的Awake(T的Awake实际上是继承的父类的)
//所以此时无需为instance赋值,其会在Awake中赋值,自然也会初始化所以无需init()
/*instance = */
new GameObject("Singleton of " + typeof(T)).AddComponent<T>();
}
else instance.Init(); //保证Init只执行一次
return instance;
}
}
private void Awake()
{
//若无其它脚本在Awake中调用此实例,则可在Awake中自行初始化instance
instance = this as T;
//初始化
Init();
}
//子类对成员进行初始化如果放在Awake里仍会出现Null问题所以自行制作一个init函数解决(可用可不用)
protected virtual void Init()
{
}
}
}