VR中的交互
概述
在VR中我们经常需要激活用户正在看的对象,比如VRSamples中,我们内建了一个简单的可扩展的,轻量级用户交互系统框架.它包括了三个主要脚本::VREyeRaycaster, VRInput, 和 VRInteractiveItem,下面对它们进行简要的说明.
VREyeRaycaster
这个脚本需要放置在Main Camera.在每个Update()中脚本会用 Physics.Raycast去射线给任何看到的碰撞器.当然也可以通过层(docs.unity3d.com/Manual/Layers.html)的设置做排除.也可以将所有需要互动的对象放到一个单独的层中来优化性能.
如果射线投射到了一个碰撞器,这个脚本将去找这个游戏对象上的VRInteractiveItem组件.
VRInteractiveItem interactible = hit.collider.GetComponent<VRInteractiveItem>(); //试图获得被射对象上的VRInteractiveItem组件
因此我们可以判断用户是否在看一个物体或者不再看这个物体,通过这个状态的获取再去做相应的动作.
VRInput
VRInput 是一个简单的判断在GearVR设备中发生的单击,双击,滑动的类或DK2在PC做同样的输入.我们可以直接从VRInput中获取输入事件:
public event Action<SwipeDirection> OnSwipe; //在每一帧中滑动或没有滑动事件时调用 public event Action OnClick; // 当Fire1键被触发且不是双击时调用. public event Action OnDown; // 在Fire1被按下时调用 public event Action OnUp; // 在Fire1被松开时调用. public event Action OnDoubleClick; // 当检测到双击事件时被调用. public event Action OnCancel; // 当Cancel被触发时调用.
更多输入事件的信息请查阅(unity3d.com/ru/learn/tutorials/modules/intermediate/scripting/events).
VRInteractiveItem
这个组件可以添加到VR中任何希望用户交互的游戏物体上.它需要物体上附加一个碰撞器组件.
在物体上我们可以获得六种事件:
public event Action OnOver; //当目光移到这个物体上时触发. public event Action OnOut; //当目光已到这个物体上时触发. public event Action OnClick; //当这个物体被凝视并且有点击事件时触发. public event Action OnDoubleClick; //当这个物体被凝视并且有双击事件时被触发. public event Action OnUp; //当这个物体被凝视并且松开Fire1键时被触发. public event Action OnDown; // 当这个物体被凝视并且按下Fire1键时被触发.
以及一个布尔值来获得当前物体是否被凝视:
public bool IsOver { get{ return m_IsOver;} //当前这个物体被凝视吗? }
我们可以创建自己的脚本来对这些事件做出相应的反应.下面这个简单的例子演示这些事件的使用:
using UnityEngine; using VRStandardAssets.Utils; namespace VRStandardAssets.Examples { // 这是一个与物体交互的简单例子 // 被用来处理物体响应输入事件 public class ExampleInteractiveItem : MonoBehaviour { [SerializeField] private Material m_NormalMaterial; [SerializeField] private Material m_OverMaterial; [SerializeField] private Material m_ClickedMaterial; [SerializeField] private Material m_DoubleClickedMaterial; [SerializeField] private VRInteractiveItem m_InteractiveItem; [SerializeField] private Renderer m_Renderer; private void Awake () { m_Renderer.material = m_NormalMaterial; } private void OnEnable() { m_InteractiveItem.OnOver += HandleOver; m_InteractiveItem.OnOut += HandleOut; m_InteractiveItem.OnClick += HandleClick; m_InteractiveItem.OnDoubleClick += HandleDoubleClick; } private void OnDisable() { m_InteractiveItem.OnOver -= HandleOver; m_InteractiveItem.OnOut -= HandleOut; m_InteractiveItem.OnClick -= HandleClick; m_InteractiveItem.OnDoubleClick -= HandleDoubleClick; } //处理当前物体 private void HandleOver() { Debug.Log("Show over state"); m_Renderer.material = m_OverMaterial; } //处理非当前物体 private void HandleOut() { Debug.Log("Show out state"); m_Renderer.material = m_NormalMaterial; } //处理单击事件 private void HandleClick() { Debug.Log("Show click state"); m_Renderer.material = m_ClickedMaterial; } //处理双击事件 private void HandleDoubleClick() { Debug.Log("Show double click"); m_Renderer.material = m_DoubleClickedMaterial; } } }
关于这段脚本的完整示例请查看VR示例的VRSampleScenes/Scenes/Examples/InteractiveItem场景
环状选择框(SelectionRadial) 和 滑块选择条(SelectionSlider)
我们做个环状选择框及滑块选择条,让用户凝视物体并按住Fire1键实现”确定”命令的交互:
随着输入的持续,选择栏进行填充,并产生OnSelectionComplete 或 被填满时OnBarFilled 事件,相关代码可以在 electionRadial.cs 和SelectionSlider.cs中找到,有很详尽的注释.从用户体验上来讲让用户知道他们在做什么,能准确的确定或取消与虚拟世界的交互.
VR示例中的交互
现在让我们来看VR示例中包含的交互功能,了解它们是如何实现的.
Menu场景的交互
每项菜单都有好几个组件,这里我们重点看 MenuButton, VRInteractiveItem, 和 Mesh Collider.
MenuButton组件用来获得在VRInteractiveItem 组件上的OnOver 和 OnOut事件.所以当视野在菜单项时,选择条将填充出现,目光不在菜单项时,选择条减小填充.当选择条完全填满并按住Fire1时将出现环状选择框开始填充:
同样的在环状选择条填满时也有OnSelectionComplete 事件,然后HandleSelectionComplete被调用,接着相机镜头淡出后载入选择的关卡.
private void OnEnable () { m_InteractiveItem.OnOver += HandleOver; m_InteractiveItem.OnOut += HandleOut; m_SelectionRadial.OnSelectionComplete += HandleSelectionComplete; } private void OnDisable () { m_InteractiveItem.OnOver -= HandleOver; m_InteractiveItem.OnOut -= HandleOut; m_SelectionRadial.OnSelectionComplete -= HandleSelectionComplete; } private void HandleOver() { // 当用户正在凝视场景菜单项时,显示环状选择条. m_SelectionRadial.Show(); m_GazeOver = true; } private void HandleOut() { // 当用户不在凝视场景菜单项时,隐藏环状选择条. m_SelectionRadial.Hide(); m_GazeOver = false; } private void HandleSelectionComplete() { // 当用户凝视凝视菜单项并且环状选择条填充满时激活按钮. if(m_GazeOver) StartCoroutine (ActivateButton()); } private IEnumerator ActivateButton() { // 如果相机已经淡出则忽略 if (m_CameraFade.IsFading) yield break; // 在有任何按钮选择事件发生时调去它 if (OnButtonSelected != null) OnButtonSelected(this); // 等待相机的淡出 yield return StartCoroutine(m_CameraFade.BeginFadeOut(true)); // 载入关卡 SceneManager.LoadScene(m_SceneToLoad, LoadSceneMode.Single); }
让我们看看环状选择框的示例,注意视野截图中心的粉红圆点.
当用户凝视菜单项时可以看到还未开始填充的环状选择框.
当用户继续凝视这个菜单项并且按下Fire1键,会看到环状选择框开始填充
我们建议让环状选择框和滑块选择条的时间一致,提升用户体验,以帮助用户更快的使用VR这种新媒介.
Maze场景中的交互
Maze游戏提供了一个桌游形式互动的例子,通过引导角色到出口再通过开关阻止炮塔的攻击(剧透了!!).
当选择一个角色,这个角色会出现一条即将行走的路线,我们可以通过滑动触控板,按下光标键或用游戏手柄的左摇杆来旋转视图.
MazeFloor 物体用一个MeshCollider 和VRInteractiveItem 组件来实现交互:
MazeCourse 游戏物体作为父物体,包含了 MazeFloor 和 MazeWalls游戏对象,用于构成迷宫的布局.
有一个MazeTargetSetting 脚本在MazeCourse上并引用了MazeFloor上的VRInteractiveItem 组件.
MazeTargetSetting 获取到VRInteractiveItem上的OnDoubleClick事件, 然后将调用OnTargetSet 事件,通过转换作为Transform的参数:
public event Action<Transform> OnTargetSet; // 当目的地设定后触发 private void OnEnable() { m_InteractiveItem.OnDoubleClick += HandleDoubleClick; } private void OnDisable() { m_InteractiveItem.OnDoubleClick -= HandleDoubleClick; } private void HandleDoubleClick() { // 一旦目标设定启用及获取了OnTargetSet事件就执行这个方法. if (m_Active && OnTargetSet != null) OnTargetSet (m_Reticle.ReticleTransform); }
无论MazeCharacter游戏对象上的Player组件还是在MazeDestinationMarkerGUI游戏对象上的DestinationMarker组件获取到这个事件,都会做出相应的反应.
角色用导航网个系统去在迷宫中寻路.Player组件中HandleSetTarget函数被设置成 Nav Mesh Agent(docs.unity3d.com/Manual/nav-CreateNavMeshAgent.html)去响应变换位置,并更新绘制Agent的路线,让角色路径可视化:
private void HandleSetTarget(Transform target) { // 如果游戏没有结束则设定角色的AI和路径线 if (m_IsGameOver) return; m_AiCharacter.SetTarget(target.position); m_AgentTrail.SetDestination(); }
DestinationMarker 移动标识物到准星的Transform位置.
private void HandleTargetSet(Transform target) { // 当目标被设定就显示标识物 Show(); // 把准星目标位置赋予标识物的位置 transform.position = target.position; // 播放音效. m_MarkerMoveAudio.Play(); // 播放标识物的动画 m_Animator.Play(m_HashMazeNavMarkerAnimState, -1, 0.0f); }
可以看到被标识物目的地的准星及所选角色的行动轨迹.
迷宫中的触发开关也是VR中常用的交互例子.本文使用碰撞器以及VRInteractiveItem和SelectionSlider类来实现.
如上图所示的要接受交互对象的脚本,SelectionSlider 脚本监听来自VRInteractiveItem 和 VRInput的事件:
private void OnEnable () { m_VRInput.OnDown += HandleDown; m_VRInput.OnUp += HandleUp; m_InteractiveItem.OnOver += HandleOver; m_InteractiveItem.OnOut += HandleOut; }
Flyer场景中的交互
Flyer场景是一个有时间限制游戏,玩家引导太空中的飞船躲避小行星,用Fire1键射击小行星,穿越积分圈获得积分等等.这游戏有点像飞行俱乐部和星际火狐.
在互动方面,Flyer是非常简单的;使用FlyerLaserController获取VRInput中的OnDown事件发射激光射击:
private void OnEnable() { m_VRInput.OnDown += HandleDown; } private void HandleDown() { //如果游戏没有运行终止执行 if (!m_GameController.IsGameRunning) return; // 从任意位置发射激光束 SpawnLaser(m_LaserSpawnPosLeft); SpawnLaser(m_LaserSpawnPosRight); }
Shooter180 和 Shooter360场景中的交互(180度第一人称射击和360度守堡式射击)
VR示例中有两个目标射击游戏,有点近似X战警的场景画面,一个是180度的沿着隧道行走的第一视角射击游戏,另一个是原地360度守堡式的射击游戏.
射击游戏中每个生成的靶子都有个碰撞器,VRInteractiveItem 和 ShootingTarget.
ShootingTarget自己获取VRInteractiveItem上的OnDown事件来判断目标是否被击中.这种方法适合用与点射,如果是连发射击,我们需要用其它方案.
我们对游戏里的基本交互做了概述以及它们在VR示例中如何用的.现在我们来讨论如何用凝视作为准星.
凝视
在VR中监测用户视线是非常重要的,无论是允许用户与对象进行触发动画的交互还是向目标射击.请注意”凝视(GAZE)”这个术语,后面我们会经常提到.
大多数的头戴显示设备还不支持眼球跟踪,所以我们只能用头部的朝向来估计用户的目光朝向.如前面概述中说的,我们只需在相机中心发射条射线用射线检测目光凝视什么对象就可以了.当然,这意味着任何被检测的物体都必须有个碰撞器组件.
准星
一个准星用来显示用户视野中心,准星的样式可以是一个简单的点或者一个十字线,这取决于你项目的风格.
传统游戏中往往会使用十字线作为准星,在VR中做准星定位是有点复杂的:用户会将眼睛的焦点放在接近于相机的物体上,那么用户会看到两个准星,我们可以把手指放在眼前模拟聚焦效果,手指离眼睛越近,背景越模糊,反之亦然,这就是所谓的自愿复视(voluntary Diplopia(https://en.wikipedia.org/wiki/Diplopia#Voluntary)).
为了避免玩家看场景时由于焦点不同出现两个准星,我们需要将准星放在玩家看的对象的表面上.
如果单单去移动准星的位置,当距离远时,准星会变得很小,为了保证准星大小不变,我们需要根据它距离相机的距离去做缩放.
为了说明这个缩放,可以查看Examples/Reticle场景,它包含了不同距离和准星缩放的示例.
准星位于相机所观察到较近的一个物体上:
准星在一个较远的物体上
准星位于更远距离的位置:
由于定位和缩放准星,所以用户会觉得准星无论距离多远都是相同的大小,当准星没有处于场景的某个对象上时,我们只需将准星放在预订的一个距离,这个距离取决于场景大小,比如一个室外环境,我们可以把准星放置在相机远一些,而在室内场景,可能让准星距离相机更近一些.
在游戏对象上绘制准星
如果在物体相同位置绘制准星,那么准星可能会与游戏对象发生穿插问题:
要解决这个问题,我们要确保准星渲染于场景中所有物体之上,这里我们用到基于Unity内置着色器”UI/Unlit/Text”改写名为UIOverlay.shader着色器.为材质选择着色器时会在”UI/Overlay”下看到它.
本场景中有两个UI控件和文本,将把它画在场景中其它物体之上:
对齐准星到场景中的物体
最后,我们想实现准星的旋转匹配到被附加的对象法线,我们可以用 RaycastHit.normal(docs.unity3d.com/ScriptReference/RaycastHit-normal.html),看下面的代码:
public void SetPosition (RaycastHit hit) { m_ReticleTransform.position = hit.point; m_ReticleTransform.localScale = m_OriginalScale * hit.distance; // 如果物体表面的法线可以被准星使用 if (m_UseNormal) // ... 设定它的正向向量旋转,使其沿着法线方向 m_ReticleTransform.rotation = Quaternion.FromToRotation (Vector3.forward, hit.normal); else // 然后如果没有正确的法向方向可用的话就让它保持本来的朝向. m_ReticleTransform.localRotation = m_OriginalRotation; }
我们可以在迷宫场景中看到准星叠加到了墙体表面
这里能看到准星叠加到了地板上:
我们同样提供了完整的示例脚本来说明通过VREyeRaycaster定位准星到凝视到的物体表面.
以上所有内容均可在VRSampleScenes/Scenes/Examples/找到
VR中头部的旋转和位置
通过旋转头部,头戴式显示设备可以看周围的环境,有时候我们需要访问这个视角值.我们就需要用到 VR.InputTracking(docs.unity3d.com/ScriptReference/VR.InputTracking.html)类,并指定需要得到哪些VRNode(docs.unity3d.com/ScriptReference/VR.VRNode.html)数据.比如我们要获得头部的旋转,就要用VRNode.Head,而不是取眼睛的节点.
在示例项目中可以找到一个基于头部旋转作为菜单选择的例子,可以看VRSampleScenes/Examples/Rotation场景及ExampleRotation脚本:
// 存储游戏对象的旋转欧拉角 var eulerRotation = transform.rotation.eulerAngles; //根据用户头部的y轴值设定对象的旋转 eulerRotation.x = 0; eulerRotation.z = 0; eulerRotation.y = InputTracking.GetLocalRotation(VRNode.Head).eulerAngles.y;
运行后我们就可以看到游戏对象是如何旋转的:
在Flyer游戏,我们用头部动作来控制飞船的位,线看FlyerMovementController这个脚本:
Quaternion headRotation = InputTracking.GetLocalRotation (VRNode.Head); m_TargetMarker.position = m_Camera.position + (headRotation * Vector3.forward) * m_DistanceFromCamera;
在VR游戏中用触控板和键盘交互
Gear VR的设备侧面有一块触控板,在Unity中相当于鼠标,我们可以用如下的命令:
Input.mousePosition(docs.unity3d.com/ScriptReference/Input-mousePosition.html)
Input.GetMouseButtonDown(docs.unity3d.com/ScriptReference/Input.GetMouseButtonDown.html)
Input.GetMouseButtonUp(docs.unity3d.com/ScriptReference/Input.GetMouseButtonUp.html)
在GearVR我们也可以获得滑动输入.在示例脚本VRInput中写好了关于滑动,单机,双击的输入获取.也同样支持按键盘上的左Ctrl键(Unity默认输入术语叫Fire1)或鼠标左键的鼠标滑动或鼠标单击.
而在Unity编辑器中,我们如果用DK2设备还没有很好的办法来测试Unity开发到GearVR的项目.不过我们可以用鼠标模拟触控板,配合键盘来实现测试.
左Ctrel(Fire1)作为单击,滑动鼠标模拟触控板的滑动旋转物体.
可以查看VRSampleScenes/Scenes/Examples/Touchpad场景了解上面说到的输入操作.
下面是ExampleTouchpad脚本,它依照滑动的方向给物体上的Rigidbody
以AddTorque实现旋转.
using UnityEngine; using VRStandardAssets.Utils; namespace VRStandardAssets.Examples { // 处理滑动控制的简单脚本例子 public class ExampleTouchpad : MonoBehaviour { [SerializeField] private float m_Torque = 10f; [SerializeField] private VRInput m_VRInput; [SerializeField] private Rigidbody m_Rigidbody; private void OnEnable() { m_VRInput.OnSwipe += HandleSwipe; } private void OnDisable() { m_VRInput.OnSwipe -= HandleSwipe; } //给Ridigbody施加AddTorque来处理滑动事件 private void HandleSwipe(VRInput.SwipeDirection swipeDirection) { switch (swipeDirection) { case VRInput.SwipeDirection.NONE: break; case VRInput.SwipeDirection.UP: m_Rigidbody.AddTorque(Vector3.right * m_Torque); break; case VRInput.SwipeDirection.DOWN: m_Rigidbody.AddTorque(-Vector3.right * m_Torque); break; case VRInput.SwipeDirection.LEFT: m_Rigidbody.AddTorque(Vector3.up * m_Torque); break; case VRInput.SwipeDirection.RIGHT: m_Rigidbody.AddTorque(-Vector3.up * m_Torque); break; } } } }
VR示例中的VRInput示例
如上所述,我们在示例游戏中使用VRInput处理触摸板和键盘的输入.
在迷宫游戏中,CameraOrbit监听滑动事件来响应视图的旋转.
private void OnEnable () { m_VrInput.OnSwipe += HandleSwipe; } private void HandleSwipe(VRInput.SwipeDirection swipeDirection) { // 如果游戏没有开始或者正在播放相机淡出效果,不处理滑动输入. if (!m_MazeGameController.Playing) return; if (m_CameraFade.IsFading) return; //否则根据滑动的进行相应的正转或负转. switch (swipeDirection) { case VRInput.SwipeDirection.LEFT: StartCoroutine(RotateCamera(m_RotationIncrement)); break; case VRInput.SwipeDirection.RIGHT: StartCoroutine(RotateCamera(-m_RotationIncrement)); break; } }
原文:http://unity3d.com/learn/tutorials/topics/virtual-reality
由 四角钱 (XK) 翻译,转载请注明来自 http://blog.1vr.cn
刚刚看完。不错 。谢谢分享。
不错,国内的Unity VR文章还很少。