1)已安装好Steam和SteamVR。
2)运行Unity,创建一个Project,在Asset Store中搜索Steam VR,安装好插件。
3)删除掉默认的Camera,在Project中选出Stream VR的CameraRig,拖入到场景中。
(拖入相关prefab 先删除所有默认GameObject ,然后将SteamVR/Prefabs中的所有prefab拖入Hierarchy窗口。)
[SteamVR]附加了一个Steam VR_Render组件,用于处理所有VR摄像机的渲染。
[CameraRig]控制着Vive的headset和控制器。 选择[CameraRig]并在Inspector中将其Position设置为(X:0,Y:0,Z:-1.1),以便在桌子后面滑动整个装备。
4)随便添加一些环境元素,然后运行。戴上你的Vive VR,就能看到刚刚设计的内容了。
大家可以参考SteamVR Plugin自带的示例场景,分别是:
SteamVR/Scenes/example
SteamVR/Extras/SteamVR_TestIK
SteamVR/Extras/SteamVR_TestThrow
Unity开发小技巧:
请把 显示分辨率对话框属性设置为HiddenByDefault
在Unity 中Player Settings的选项里,https://docs.unity3d.com/Manual/class - PlayerSettingsStandalone.html
当用户带上HMD的时候我们没办法消除掉这个对话框,当启动游戏的时候,他们不得不摘下HMD手动消除这个对话框。但是这个对话框有时还是有用的,这就是为什么我推荐HiddenByDefault而不是Disable. Unity提供的方法是用命令行选项来控制当设置为HiddenByDefault(或者Ctrl),但是当设置为Enable的时候没有用。
https://www.bilibili.com/video/av59711404
本章节主要讲解使用SteamVR Plugins插件为基础,来介绍Vive开发的入门,重点讲解设备的激活和控制接口。SteamVR Plugins在unity官方资源商店可以免费下载,这里我就不给出链接了。
导入SteamVR Plugins后,不要在他的示例上去做开发,那样你会走弯路。因为这个插件的设备初始化代码很怪异,甚至可以说无法使用。因此我们需要自己写设备的初始化代码以及设备的操作接口。
我们对原插件进行封装后就可以愉快的使用了,之后怎么开发就和这个设备无关了,我们开发这类的应用无非是获取相关的接口操作。
新建场景,然后在Camera上添加组件SteamVR_Camera,然后点击 Expand 按钮,之后我们就得到了一个基本的VR相机。
然后我们在Camera的下面新建两个GameObject,并命名为LeftHand和RightHand。同时添加组件SteamVR_TrackedObject。然后在各自的下面添加一个对象名为Mode,并绑定组件SteamVR_RenderModel。
接下来就是开始写代码了,新建脚本SteamVR_InitManager.cs,来获取手柄的激活和相关的初始化代码
using UnityEngine;
using System.Collections;
using Valve.VR;
using System;
public class SteamVR_InitManager : MonoBehaviour
{
public event Action<SteamVR_TrackedObject> OnLeftDeviceActive;//左手柄激活事件
public event Action<SteamVR_TrackedObject> OnRightDeviceActive;//右手柄激活事件
public SteamVR_TrackedObject LeftObject;
public SteamVR_TrackedObject RightObject;
private bool[] isAllConnect;//0代表右手状态,1代表左手状态
private uint leftIndex = 100;//左手柄对应的设备ID
private uint rightIndex = 100;//右手柄对应的设备ID
private static SteamVR_InitManager instance;
public DeviceInput LeftHandInput;
public DeviceInput RightHandInput;
public static SteamVR_InitManager Instance {
get {
if (instance == null) {
instance = GameObject.FindObjectOfType<SteamVR_InitManager>();
}
return instance;
}
}
// Use this for initialization
void Awake()
{
LeftObject = transform.FindChild("LeftHand").GetComponent<SteamVR_TrackedObject>();
RightObject = transform.FindChild("RightHand").GetComponent<SteamVR_TrackedObject>();
LeftObject.gameObject.SetActive(false);
RightObject.gameObject.SetActive(false);
}
void Start()
{
StartCoroutine(CheckDeviceActive());
}
void OnEnable()
{
OnLeftDeviceActive = LeftDeviceActive;
OnRightDeviceActive = RightDeviceActive;
}
void OnDisable()
{
OnLeftDeviceActive -= LeftDeviceActive;
OnRightDeviceActive -= RightDeviceActive;
}
/// <summary>
/// 检测手柄设备是否激活
/// </summary>
/// <returns></returns>
IEnumerator CheckDeviceActive()
{
yield return new WaitForSeconds(1);
while (!isAllConnect[0] || !isAllConnect[1])
{
for (uint i = 1; i < OpenVR.k_unMaxTrackedDeviceCount; i )
{
if (i == leftIndex || i == rightIndex) continue;//已经初始化的不再进入判断
if (OpenVR.System != null && OpenVR.System.IsTrackedDeviceConnected(i))
{
OnDeviceConnected(new object[] { i, true });
}
}
yield return new WaitForFixedUpdate();
}
yield return 0;
}
/// <summary>
/// 检测激活的设备是否是手柄
/// </summary>
/// <param name="args"></param>
private void OnDeviceConnected(object[] args)
{
if (args != null && args.Length > 1)
{
uint index = (uint)args[0];
bool isConnect = (bool)args[1];
var system = OpenVR.System;
if (isConnect && system != null && system.GetTrackedDeviceClass(index) == ETrackedDeviceClass.Controller)
{
uint tmpleftIndex = (uint)system.GetTrackedDeviceIndexForControllerRole(ETrackedControllerRole.LeftHand);
uint tmprightIndex = (uint)system.GetTrackedDeviceIndexForControllerRole(ETrackedControllerRole.RightHand);
if (index == tmprightIndex)
{
isAllConnect[0] = true;
rightIndex = index;
OnRightDeviceActive(RightObject);
}
else if (index == tmpleftIndex)
{
isAllConnect[1] = true;
leftIndex = index;
OnLeftDeviceActive(LeftObject);
}
}
}
}
private void RightDeviceActive(SteamVR_TrackedObject obj)
{
DeviceActive(obj, rightIndex);
RightHandInput = obj.GetComponent<DeviceInput>();
}
private void LeftDeviceActive(SteamVR_TrackedObject obj)
{
DeviceActive(obj, leftIndex);
LeftHandInput = obj.GetComponent<DeviceInput>();
}
/// <summary>
/// 匹配对应的设备号,完成手柄模型的设置
/// </summary>
/// <param name="device"></param>
/// <param name="index"></param>
void DeviceActive(SteamVR_TrackedObject device, uint index)
{
SteamVR_TrackedObject.EIndex eIndex = (SteamVR_TrackedObject.EIndex)Enum.Parse(typeof(SteamVR_TrackedObject.EIndex), "Device" index);
device.index = eIndex;
device.GetComponentInChildren<SteamVR_RenderModel>().index = eIndex;
device.gameObject.SetActive(true);
}
}
然后就是手柄控制接口代码DeviceInput.cs,基本上把所有的手柄控制事件都写完了.这个脚本添加到左右Hand对象上就行了。
using System;
using UnityEngine;
using Valve.VR;
/// <summary>
///
/// </summary>
public class DeviceInput : MonoBehaviour
{
//Swipe directions
public enum SwipeDirection
{
NONE,
UP,
DOWN,
LEFT,
RIGHT
};
public event Action OnPressTrigger; //按住扳机键
public event Action OnPressDownTrigger; // 按下扳机键
public event Action OnPressUpTrigger; //抬起扳机键
public event Action OnClickTrigger; //单击扳机
public event Action OnDoubleClickTrigger; //双击扳机
public event Action OnTouchPad; //按住触摸板
public event Action OnPressDownGripButton; //按下侧键
public event Action OnPressDownMenuButton; //按下菜单键
public event Action<Vector2> OnBeginTouch; //触摸触摸板的位置
public event Action<Vector2> OnEndTouch;//抬起触摸板的位置
public event Action OnPressDownPad; // 按下扳机键
public event Action OnPressUpPad; //抬起扳机键
public event Action OnPressPad; //抬起扳机键
public event Action<Transform> OnPressDownPadV3; // 按下扳机键
public event Action<Transform> OnPressUpPadV3; //抬起扳机键
public event Action<Transform> OnPressPadV3; //抬起扳机键
public event Action OnTouchPadDown; //按下触摸板
public event Action OnTouchPadUp;//抬起触摸板
public event Action<SwipeDirection> OnPadSwipe;
[SerializeField]
private float m_SwipeWidth = 0.6f; //The width of a swipe
private Vector2 m_PadDownPosition; // The screen position of the mouse when Fire1 is pressed.
private float m_LastHorizontalValue; // The previous value of the horizontal axis used to detect keyboard swipes.
private float m_LastVerticalValue; // The previous value of the vertical axis used to detect keyboard swipes.
private SteamVR_TrackedObject Hand;
private SteamVR_Controller.Device device;
private Vector2 m_PadPosition;
[SerializeField]
private float m_DoubleClickTime = 0.3f; //The max time allowed between double clicks
private float m_LastMouseUpTime = 0; // The time when Fire1 was last released.
public void Enable()
{
Hand = GetComponent<SteamVR_TrackedObject>();
}
public void Update()
{
CheckInput();
}
private void CheckInput()
{
// Set the default swipe to be none.
SwipeDirection swipe = SwipeDirection.NONE;
if (device == null)
{
device = SteamVR_Controller.Input((int)Hand.index);
print("NUll Device");
return;
}
if (device.GetPress(EVRButtonId.k_EButton_SteamVR_Trigger))
{
if (OnPressTrigger != null) OnPressTrigger();
}
if (device.GetPressDown(EVRButtonId.k_EButton_SteamVR_Trigger))
{
if (OnPressDownTrigger != null) OnPressDownTrigger();
}
if (device.GetPressUp(EVRButtonId.k_EButton_SteamVR_Trigger))
{
if (OnPressUpTrigger != null) OnPressUpTrigger();
if (Time.time - m_LastMouseUpTime < m_DoubleClickTime)
{
if (OnDoubleClickTrigger != null)
OnDoubleClickTrigger();
}
else
{
if (OnClickTrigger != null)
OnClickTrigger();
}
m_LastMouseUpTime = Time.time;
}
if (device.GetPressDown(EVRButtonId.k_EButton_SteamVR_Touchpad))
{
if (OnPressDownPad != null) OnPressDownPad();
if (OnPressDownPadV3 != null) OnPressDownPadV3(transform);
}
if (device.GetPressUp(EVRButtonId.k_EButton_SteamVR_Touchpad))
{
if (OnPressUpPad != null) OnPressUpPad();
if (OnPressUpPadV3 != null) OnPressUpPadV3(transform);
}
if (device.GetPress(EVRButtonId.k_EButton_SteamVR_Touchpad))
{
if (OnPressPad != null) OnPressPad();
if (OnPressPadV3 != null) OnPressPadV3(transform);
}
if (device.GetTouch(EVRButtonId.k_EButton_SteamVR_Touchpad))
{
if (OnTouchPad != null) OnTouchPad();
m_PadPosition = device.GetAxis();
}
if (device.GetTouchUp(EVRButtonId.k_EButton_SteamVR_Touchpad))
{
if (OnTouchPadUp != null) OnTouchPadUp();
if (OnEndTouch != null) OnEndTouch(m_PadPosition);
swipe = DetectSwipe();
m_PadDownPosition = Vector2.zero;
}
if (device.GetTouchDown(EVRButtonId.k_EButton_SteamVR_Touchpad))
{
if (OnTouchPadDown != null) OnTouchPadDown();
if (OnBeginTouch != null) OnBeginTouch(m_PadDownPosition = device.GetAxis());
}
if (device.GetPressDown(EVRButtonId.k_EButton_Grip))
{
if (OnPressDownGripButton != null) { OnPressDownGripButton(); }
}
if (device.GetPressDown(EVRButtonId.k_EButton_ApplicationMenu))
{
if (OnPressDownMenuButton != null) OnPressDownMenuButton();
}
if (swipe != SwipeDirection.NONE)
{
if (OnPadSwipe != null) OnPadSwipe(swipe);
if (device.GetTouch(EVRButtonId.k_EButton_SteamVR_Touchpad))
{
m_PadPosition = device.GetAxis();
}
else {
m_PadDownPosition = Vector2.zero;
}
}
}
private SwipeDirection DetectSwipe()
{
// Get the direction from the mouse position when Fire1 is pressed to when it is released.
Vector2 swipeData = (m_PadPosition - m_PadDownPosition).normalized;
//print(m_PadPosition " " m_PadDownPosition " " swipeData);
// If the direction of the swipe has a small width it is vertical.
bool swipeIsVertical = Mathf.Abs(swipeData.y) > m_SwipeWidth;
// If the direction of the swipe has a small height it is horizontal.
bool swipeIsHorizontal = Mathf.Abs(swipeData.x) > m_SwipeWidth;
// If the swipe has a positive y component and is vertical the swipe is up.
if (swipeData.y > 0f && swipeIsVertical)
return SwipeDirection.UP;
// If the swipe has a negative y component and is vertical the swipe is down.
if (swipeData.y < 0f && swipeIsVertical)
return SwipeDirection.DOWN;
// If the swipe has a positive x component and is horizontal the swipe is right.
if (swipeData.x > 0f && swipeIsHorizontal)
return SwipeDirection.RIGHT;
// If the swipe has a negative x component and is vertical the swipe is left.
if (swipeData.x < 0f && swipeIsHorizontal)
return SwipeDirection.LEFT;
// If the swipe meets none of these requirements there is no swipe.
return SwipeDirection.NONE;
}
public void Disable()
{
OnPressTrigger = null; //按住扳机键
OnPressDownTrigger = null; // 按下扳机键
OnPressUpTrigger = null; //抬起扳机键
OnTouchPad = null; //按住触摸板
OnPressDownGripButton = null; //按下侧键
OnPressDownMenuButton = null; //按下菜单键
OnBeginTouch = null; //触摸触摸板的位置
OnEndTouch = null;//抬起触摸板的位置
OnTouchPadDown = null; //按下触摸板
OnTouchPadUp = null;//抬起触摸板
OnPadSwipe = null;
}
}
接下来写一个简单的测试类来测试相关的代码:TestEvent.cs
using UnityEngine;
using System.Collections;
using System;
/// <summary>
/// 这是一个测试类,简单的测试了手柄的激活以及部分手柄的操作事件
/// 两个手柄分开注册,这样扩展性非常好,相同的按键可以做不同的处理
/// 大家可以补充测试
/// </summary>
public class TestEvent : MonoBehaviour {
void OnEnable() {
SteamVR_InitManager.Instance.OnLeftDeviceActive = OnLeftDeviceActive;//左手柄激活
SteamVR_InitManager.Instance.OnRightDeviceActive = OnRightDeviceActive;//右手柄激活
}
private void OnRightDeviceActive(SteamVR_TrackedObject obj)
{
print("OnRightDeviceActive" obj);
SteamVR_InitManager.Instance.RightHandInput.OnPressDownTrigger = OnPressDownTrigger;
SteamVR_InitManager.Instance.RightHandInput.OnTouchPadDown = OnTouchPadDown;
}
private void OnPressDownTrigger()
{
print("OnPressDownTrigger");
}
private void OnTouchPadDown()
{
print("OnTouchPadDown");
}
private void OnLeftDeviceActive(SteamVR_TrackedObject obj)
{
print("OnLeftDeviceActive" obj);
SteamVR_InitManager.Instance.LeftHandInput.OnPressDownTrigger = OnPressDownTrigger;
SteamVR_InitManager.Instance.LeftHandInput.OnTouchPadDown = OnTouchPadDown;
}
void OnDisable() {
SteamVR_InitManager.Instance.OnLeftDeviceActive -= OnLeftDeviceActive;//左手柄激活
SteamVR_InitManager.Instance.OnRightDeviceActive -= OnRightDeviceActive;//右手柄激活
}
}
其实实现抛物线很简单,生成一组抛物点,然后将点渲染成线就好。渲染成线有很多方式,你可以用模型,也可以用GL的绘制线段,也可以用LineRender。重点是优化点的生成计算。
如何生成抛物点,一个很简单的公式:
nextPos = lastPos 水平位移 垂直位移。
稍后的代码有详尽的注释我就不多赘述了。
重点是对点集的优化,优化抛物点分两个部分,一是如何计算碰撞点,二是内存开销的优化。考虑到一般的VR项目只在水平面上进行移动,所以通过判断抛物点的y轴来判定碰撞。关于内存开销,我们可以限定生成的抛物点个数,同时优化计算。我们使用一个List来保存点集,动态的根据点的情况来生成点,或者改变点的位置。例如当手柄角度不变时,只需要将List集合中的点改变Y轴就行了。
现在思路已经明了了。以下是伪代码:
int i = 0;
while( nextPos.y>0 && maxPoint>0 ){
if(list.count<=i){
list.add(nextPos);
}
else{
list = nextPos;
}
///生成,优化,计算下一个抛物点
i ;
}
list.remove(i,list.count-i);//移除上一个点集的多余数据
生成抛物线点集后,接下来就是绘制曲线,LineRender,GL都可以很方便的绘制。但是就性能来说,GL更加快一点。建议大家用GL。当然大家也可以使用自己的模型绘制,原理就是,在对应的点生成你的模型,然后计算对应的角度即可。
最后就是生成抛物线的终点,是按照y轴来判断抛物线的落点,但是落点可能有障碍物等等,或者落点在墙角,这些位置显然是不能跳跃的,所以需要对落点进行判断。在Physics中有一个方法可以检测一个球形范围是否会碰撞,可以给地面添加一个层,以便忽略地面的检测,这样就可以安全的着陆了。
using UnityEngine;
using System.Collections.Generic;
using System;
/// <summary>
/// 抛物线脚本
/// </summary>
public class HandRay : MonoBehaviour
{
private Transform CurrentHand; //当前触发的手
private List<Vector3> pointList; //曲线点集合
private Quaternion lastRotation; //上一个移动的角度
private Vector3 lastPostion; //上一个位置
private Vector3 lastPos; //上一个点,为了优化将一个临时变量做成全局的,节省内存开销
private Vector3 nextPos;//下一个点,理由同上
private event Action OnChangeTransform;//一个事件,用来检测手柄位置和角度变化的
private Material material;//渲染射线的材质球
private Vector3 HitPoint;//抛物线的碰撞点
private Ray ray;
private bool canJump = false;//
public GameObject PointEffect;//一个特效,就是在射线的终点放置一个光柱什么的,大家可以自己做这个特效
public int MaxPoint; //生成曲线的点最大数量
public float Distence;//水平位移
public float Grity;//垂直位移
public float CheckRange;//检测位置是否存在障碍物
public void Awake()
{
SetData();
}
public void Start()
{
pointList = new List<Vector3>();
OnChangeTransform = OnChangeTransformCallBack;
HitPoint = -Vector3.one;
ray = new Ray();
}
public void Update()
{
//当手柄按下触摸键同时角度合适时触发事件开始计算点
if (CurrentHand != null && ((CurrentHand.eulerAngles.x > 275 && CurrentHand.eulerAngles.x <= 360) || (CurrentHand.eulerAngles.x >= -0.01f && CurrentHand.eulerAngles.x < 85)))
{
if (OnChangeTransform != null) OnChangeTransform();
}
else
{
pointList.Clear();
PointEffect.SetActive(false);
}
}
/// <summary>
/// 计算抛物线的点
/// 此方法已经优化过性能
///
/// </summary>
private void OnChangeTransformCallBack()
{
if (lastRotation != CurrentHand.rotation || lastPostion != CurrentHand.position)
{
lastPos = nextPos = CurrentHand.position;
int i = 0;
while (nextPos.y > 0 && (i < MaxPoint))
{
if (pointList.Count <= i)
{
pointList.Add(nextPos);
}
else
{
pointList[i] = nextPos;
}
if (lastRotation == CurrentHand.rotation && lastPostion != CurrentHand.position && i < pointList.Count - 1)
{
nextPos = pointList[i 1] CurrentHand.position - lastPostion;
}
else
{
nextPos = lastPos CurrentHand.rotation * Vector3.forward * Distence Vector3.up * Grity * 0.1f * i * Time.fixedDeltaTime;
}
lastPos = nextPos;
i ;
}
if (pointList.Count > i)
{
pointList.RemoveRange(i, pointList.Count - i);
}
lastRotation = CurrentHand.rotation;
lastPostion = CurrentHand.position;
if (pointList.Count > 1)
{
HitPoint = pointList[pointList.Count - 1];
PointEffect.SetActive(true);
PointEffect.transform.position = HitPoint;
}
else
{
HitPoint = -Vector3.one;
PointEffect.SetActive(false);
}
}
}
public void Enable()
{
SteamVR_InitManager.Instance.OnLeftDeviceActive = OnHandActive;
SteamVR_InitManager.Instance.OnRightDeviceActive = OnHandActive;
OnChangeTransform = OnChangeTransformCallBack;
}
public void OnHandActive(SteamVR_TrackedObject obj)
{
DeviceInput device = obj.GetComponent<DeviceInput>();
device.OnPressDownPadV3 = OnPressDownPad;
device.OnPressUpPad = OnPressUpPadAction;
}
public void OnHandDis(SteamVR_TrackedObject obj)
{
if (obj && obj.gameObject.activeSelf)
{
DeviceInput device = obj.GetComponent<DeviceInput>();
device.OnPressDownPadV3 -= OnPressDownPad;
device.OnPressUpPad -= OnPressUpPadAction;
}
}
public void Disable()
{
SteamVR_InitManager.Instance.OnLeftDeviceActive -= OnHandActive;
SteamVR_InitManager.Instance.OnRightDeviceActive -= OnHandActive;
OnHandDis(SteamVR_InitManager.Instance.LeftObject);
OnHandDis(SteamVR_InitManager.Instance.LeftObject);
OnChangeTransform -= OnChangeTransformCallBack;
}
public void SetData()
{
if (PointEffect)
PointEffect.SetActive(false);
}
/// <summary>
/// 抬起触摸板时,计算落脚点
/// </summary>
private void OnPressUpPadAction()
{
if (CurrentHand == null) return;
canJump = true;
ray.origin = CurrentHand.position;
Vector3 dir = HitPoint - CurrentHand.position;
ray.direction = dir;
if (Physics.CheckSphere(HitPoint, CheckRange, ~(1 << 8)))
{
canJump = false;
}
if (canJump)
{
JumpPoint(HitPoint);
}
CurrentHand = null;
}
/// <summary>
/// 跳到指定的点
/// </summary>
/// <param name="point"></param>
public void JumpPoint(Vector3 point)
{
point.y = transform.position.y;
transform.position = point;
}
private void OnPressDownPad(Transform parent)
{
CurrentHand = parent;
}
/// <summary>
/// 使用GL来绘制曲线
/// 将点绘制出来
/// </summary>
void OnRenderObject()
{
material.SetPass(0);
if (pointList == null) return;
GL.Begin(GL.LINES);
for (int i = 0; i < pointList.Count; i )
{
GL.Vertex(pointList[i]);
}
GL.End();
}
/// <summary>
/// 一个额外的附加方法,即用一个曲线来绘制抛物线,性能较低,因为点数比较多
/// 感兴趣的可以把此方法添加到Update中更新
/// </summary>
public void ShowLineByRender()
{
LineRenderer line = GetComponent<LineRenderer>();
if (line)
{
line.SetVertexCount(pointList.Count);
for (int i = 0; i < pointList.Count; i )
{
line.SetPosition(i, pointList[i]);
}
}
}
}