该脚本需要和Main Camera关联。在每次调用Update()时,该脚本都会使用Physics.Raycast来投射一条射线,以确认该射线是否命中任何collider(碰撞体)。使用该脚本还可以排除特定的Unity - Manual: Layers-在某些场景中,我们可能为了性能考虑,把所有的可交互对象移到一个单独的层。
如果某个碰撞体被射线命中,那么该脚本将尝试在GameObject上找到一个VRInteractiveItem组件。
C#脚本:
- VRInteractiveItem interactible = hit.collider.GetComponent<VRInteractiveItem>();
- //attempt to get the VRInteractiveItem on the hit object
VRInput是个简单的类,可以判断用户在GearVR上(或是使用DK2时在PC上)所进行的一些简单操作,比如滑动、触碰、或双触。
我们可以直接在VRInput上订阅事件:
C#脚本:
- public event Action<SwipeDirection> OnSwipe;
- // Called every frame passing in the swipe, including if there is no swipe.
- public event Action OnClick;
- // Called when Fire1 is released and its not a double click.
- public event Action OnDown;
- // Called when Fire1 is pressed.
- public event Action OnUp;
- // Called when Fire1 is released.
- public event Action OnDoubleClick;
- // Called when a double click is detected.
- public event Action OnCancel;
- // Called when Cancel is pressed.
- public event Action OnCancel;
- // Called when a double click is detected.
- public event Action OnDoubleClick;
- // Called when Fire1 is released.
- public event Action OnUp;
- // Called when Fire1 is pressed.
- public event Action OnDown;
- // Called when Fire1 is released and its not a double click.
- public event Action OnClick;
- // Called every frame passing in the swipe, including if there is no swipe.
当按住输入键时,选择条会进行填充,并在填充完整后分发OnSelectionComplete或OnBarFilled事件。关于此部分的代码,可以在SelectionRadial.cs和SelectionSlider.cs中找到,并进行了详细的注释。
在VR的世界里,从用户交互的角度看,用户需要时刻知道自己在做什么,而且可以掌控一切。通过这种“held input”的确认输入方式,可以确保用户不会出现误操作。
VR Sample项目中的交互示例
现在让我们来一起看看VR Sample项目中的部分交互示例。我们将提到每个场景中所使用的交互方式,以及具体实现的方式。
Menu 场景中的交互
每个menu场景都包含了几个组件,其中我们需要重点关注的是MenuButton,VRInteractiveItem和Mesh Collider。
MenuButton组件订阅了VRInteractiveItem组件上的OnOver和OnOut事件,这样当十字准星移到menu上时,selection radial会出现。当用户的实现离开菜单选项时,selection radial会消失。而当selection radial可见,且用户按住Fire1键时,则radial会自动填充:
该类还订阅了OnSelectionRadial的OnSelectionComplete事件,这样当radial被填充满的时候,会调用HandleSelectionComplete。该方法的作用是让摄像机淡出,并调用所选的关卡。
C#脚本:
- 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()
- {
- // When the user looks at the rendering of the scene, show the radial.
- m_SelectionRadial.Show();
- m_GazeOver = true;
- }
- private void HandleOut()
- {
- // When the user looks away from the rendering of the scene, hide the radial.
- m_SelectionRadial.Hide();
- m_GazeOver = false;
- }
- private void HandleSelectionComplete()
- {
- // If the user is looking at the rendering of the scene when the radials selection finishes, activate the button.
- if(m_GazeOver)
- StartCoroutine (ActivateButton());
- }
- private IEnumerator ActivateButton()
- {
- // If the camera is already fading, ignore.
- if (m_CameraFade.IsFading)
- yield break;
- // If anything is subscribed to the OnButtonSelected event, call it.
- if (OnButtonSelected != null)
- OnButtonSelected(this);
- // Wait for the camera to fade out.
- yield return StartCoroutine(m_CameraFade.BeginFadeOut(true));
- // Load the level.
- SceneManager.LoadScene(m_SceneToLoad, LoadSceneMode.Single);
- }
- }
- SceneManager.LoadScene(m_SceneToLoad, LoadSceneMode.Single);
- // Load the level.
- yield return StartCoroutine(m_CameraFade.BeginFadeOut(true));
- // Wait for the camera to fade out.
- OnButtonSelected(this);
- if (OnButtonSelected != null)
- // If anything is subscribed to the OnButtonSelected event, call it.
- yield break;
- if (m_CameraFade.IsFading)
- // If the camera is already fading, ignore.
- {
- private IEnumerator ActivateButton()
- }
- StartCoroutine (ActivateButton());
- if(m_GazeOver)
- // If the user is looking at the rendering of the scene when the radials selection finishes, activate the button.
- {
- private void HandleSelectionComplete()
- }
- m_GazeOver = false;
- m_SelectionRadial.Hide();
- // When the user looks away from the rendering of the scene, hide the radial.
- {
- private void HandleOut()
- }
- m_GazeOver = true;
- m_SelectionRadial.Show();
- // When the user looks at the rendering of the scene, show the radial.
- {
- private void HandleOver()
- }
- m_SelectionRadial.OnSelectionComplete -= HandleSelectionComplete;
- m_InteractiveItem.OnOut -= HandleOut;
- m_InteractiveItem.OnOver -= HandleOver;
- {
- private void OnDisable ()
- }
- m_SelectionRadial.OnSelectionComplete += HandleSelectionComplete;
- m_InteractiveItem.OnOut += HandleOut;
- m_InteractiveItem.OnOver += HandleOver;
- {
当用户“凝视”菜单选项时,空白的Selection Radial可见。
Selection Radial 填充(当用户“凝视”菜单选项,且按下fire1输入键)
在整个示例项目中,我们尝试用同样的风格,也就是使用bar和radial以固定的速度进行填充。在此建议大家在开发自己的VR项目时注意到这一点,因为交互设计中的连贯性对用户很重要,特别是对于VR这种新媒介。
Maze场景中的交互
Maze(迷宫)游戏中提供了一个桌面式的交互示例,其中我们可以指引游戏角色到出口,并避免触发炮塔。
在选择角色的目的地时,会出现一个目的地标记,同时还会显示一个角色的路径。玩家可以通过在触摸板上使用swipe,按下方向键,或是使用游戏操纵杆上的左键来旋转视图。
在MazeFloor游戏对象上关联了MeshCollider和VRInteractiveItem,从而允许在VR场景中进行交互:
MazeCourse 游戏对象是一个parent对象,其中包含了MazeFloor和MazeWalls GameObjects,这两个对象依次包含了迷宫布局中的几何信息。
MazeCourse关联了一个MazeTargetSetting脚本,其中包含了对MazeFloor对象上VRInteractiveItem组件的引用。
MazeTargetSetting订阅了VRInteractiveItem上的OnDoubleClick事件,随后会分发OnTargetSet事件。该事件将把十字准星的Transform作为参数:
C#脚本:
- public event Action<Transform> OnTargetSet;
- // This is triggered when a destination is set.
- private void OnEnable()
- {
- m_InteractiveItem.OnDoubleClick += HandleDoubleClick;
- }
- private void OnDisable()
- {
- m_InteractiveItem.OnDoubleClick -= HandleDoubleClick;
- }
- private void HandleDoubleClick()
- {
- // If target setting is active and there are subscribers to OnTargetSet, call it.
- if (m_Active OnTargetSet != null)
- OnTargetSet (m_Reticle.ReticleTransform);
- }
- OnTargetSet (m_Reticle.ReticleTransform);
- if (m_Active OnTargetSet != null)
- // If target setting is active and there are subscribers to OnTargetSet, call it.
- {
- private void HandleDoubleClick()
- }
- m_InteractiveItem.OnDoubleClick -= HandleDoubleClick;
- {
- private void OnDisable()
- }
- m_InteractiveItem.OnDoubleClick += HandleDoubleClick;
- {
- private void OnEnable()
- // This is triggered when a destination is set.
迷宫中的切换开关也是在VR中和物体进行交互的示例,其中用到了Collider,以及VRInteractiveItem,和SelectionSlider三个类。
正如上图中显示的,和交互对象一起,SelectionSlider脚本会由VRInteractiveItem和VRInput所分发的事件。
C#脚本:
- private void OnEnable ()
- {
- m_VRInput.OnDown += HandleDown;
- m_VRInput.OnUp += HandleUp;
- m_InteractiveItem.OnOver += HandleOver;
- m_InteractiveItem.OnOut += HandleOut;
- }
- m_InteractiveItem.OnOut += HandleOut;
- m_InteractiveItem.OnOver += HandleOver;
- m_VRInput.OnUp += HandleUp;
- m_VRInput.OnDown += HandleDown;
- {
ShootingTarget组件订阅了VRInteractiveItem的OnDown事件,以判断目标是否被击中。该方法适用于瞬间命中(比如激光枪这种)的设定,如果要展示时间,我们就需要考虑解决方案了。
现在我们应该对基本的VR交互组件有了大概的印象,包括任何在VR Samples项目中具体使用这些组件。现在让我们来看看VR Samples项目中如何使用gaze(凝视)和reticles(十字星)。
GAZE(凝视)
在VR应用中判断用户正在看什么很重要,可能是用于判断用户和游戏对象的交互,或是触发一个动画,也可能是向目标发射。我们将VR中“看”这个动作定义为gaze(凝视),而在后续的教程中我们将频繁使用这个词。
考虑到目前大多数HMD头戴设备还不支持眼部追踪,因此我们只能估计用户的gaze(凝视)。透镜的扭曲意味着用户正看着正前方,有一个简单的解决方案。正如在概览中提到的,我们只需要从摄像机的中心发射一条射线,然后找到这条射线所碰撞的物体即可。当然,这就意味着所有要被碰撞(或是需要通过“凝视”进行交互)的对象都必须关联一个Collider组件。
Reticle(十字准星)
十字准星用于辅助标记用户视野的中心。十字准星的样式可能是简单的点,也可能是一个十字准线,具体形式取决于项目需求。
在传统的3D游戏中,十字准星被设置为空间中的固定点,比如通常是屏幕的中央。但是在VR中使用十字准星变得非常复杂:当用户在VR环境中四处观望时,双眼将汇集在靠近摄像机的物体上。如果十字准星处在一个固定的位置,那么用户会看到两个准星:我们在现实世界里面可以轻易模仿这种效果。把某个手指放在眼睛前面,然后聚焦到近处和远处的物体上。当我们聚焦在这个手指上时,就会看到两个背景,反之亦然。这就是传说中的Diplopia 现象。
为了避免用户在查看周围环境和注视不同距离的物体时看到两个准星,我们需要将准星放到3D空间的同一个点,也就是用户所关注对象的表面。
将准星放在空间的这个点意味着从远处看准星将非常小,当靠近时会变大。为了让准星的大小不随距离发生变化,我们需要根据它到摄像机的距离对其进行缩放。
为了说明这一点,我们从Examples/Reticle场景中找了一些例子,展示了处于不同距离和比例的准星。
准星放置在靠近摄像机的物体上:
准星放置在稍远的物体上:
准星放置在远处:
根据所处的位置和自身比例,用户在任何距离上看到的准星大小都是相同的。
如果没有击中任何对象,那么我们只需把准星放到一个预设的距离上。在室外环境中,可能会放在摄像机的Far clip plane前面,在室内场景中可能会近得多。
将十字准星渲染到游戏对象的表面
如果十字准星恰好和某个对象的位置相同,那么准星可能会嵌入到临近的对象中。
为了解决这个问题,我们需要确保将准星渲染到场景中所有对象的前面。在VR Samples中,我们提供了一个shader,基于Unity现有的名为UIOverlay.shader的”UI/Unlit/Text” shader。在选择某个材质的shader时,可以在”UI/Overlay”中找到。
这个shader对UI 元素和文本都适用,会在场景中物体的前面绘制。
将准星和场景中的游戏对象对齐
我们希望准星的旋转方向和它所命中的对象的法线相匹配。通过RaycastHit.normal就可以实现这一点,以下是具体的实现代码:
C#脚本:
- public void SetPosition (RaycastHit hit)
- {
- m_ReticleTransform.position = hit.point;
- m_ReticleTransform.localScale = m_OriginalScale * hit.distance;
- // If the reticle should use the normal of what has been hit...
- if (m_UseNormal)
- // ... set its rotation based on its forward vector facing along the normal.
- m_ReticleTransform.rotation = Quaternion.FromToRotation (Vector3.forward, hit.normal);
- else
- // However if it isnt using the normal then its local rotation should be as it was originally.
- m_ReticleTransform.localRotation = m_OriginalRotation;
- }
- m_ReticleTransform.localRotation = m_OriginalRotation;
- // However if it isnt using the normal then its local rotation should be as it was originally.
- else
- m_ReticleTransform.rotation = Quaternion.FromToRotation (Vector3.forward, hit.normal);
- // ... set its rotation based on its forward vector facing along the normal.
- if (m_UseNormal)
- // If the reticle should use the normal of what has been hit...
- m_ReticleTransform.localScale = m_OriginalScale * hit.distance;
- m_ReticleTransform.position = hit.point;
- {
下图展示了准星如何匹配地板的法线:
我们还提供了一个示例的Reticle脚本。该脚本可以跟VREyeRaycaster一起适用,从而将准星放置到场景的正确位置,并且可以选择跟所命中的对象法线帖齐。
以上内容都可以在VRSampleScens/Scens/Examples/中看到。
在VR项目中头部的旋转和位置
在头戴设备中跟踪头部的旋转和位置可以用沉浸式的体验来感受周围环境,但同时也可以让对象根据这些数值所相应的相应。
为了获取这些数值我们需要用到VR.InputTracking类,并指定我们要访问的VRNode。为了获取头部的旋转,我们会希望用到VRNode.Head,而不是两只眼睛。想了解更多的信息,可以参考Getting Started with VR Development一文中的Camera Nodes。
使用头部旋转作为输入方式的可能应用是精细旋转菜单或是对象。在VRSampleScenes/Examples/Rotation场景中可以看到这一点的示例。
下面是ExampleRotation的脚本:
C#脚本:
- // Store the Euler rotation of the gameobject.
- var eulerRotation = transform.rotation.eulerAngles;
- // Set the rotation to be the same as the users in the y axis.
- eulerRotation.x = 0;
- eulerRotation.z = 0;
- eulerRotation.y = InputTracking.GetLocalRotation(VRNode.Head).eulerAngles.y;
- eulerRotation.z = 0;
- eulerRotation.x = 0;
- // Set the rotation to be the same as the users in the y axis.
- var eulerRotation = transform.rotation.eulerAngles;
在Flyer游戏场景中,我们将看到太空飞船基于头部的旋转来调整自身位置,具体参考FlyerMovementController:
C#脚本:
- Quaternion headRotation = InputTracking.GetLocalRotation (VRNode.Head);
- m_TargetMarker.position = m_Camera.position + (headRotation * Vector3.forward) * m_DistanceFromCamera;
在VR游戏中使用触摸板和键盘进行交互
Gear VR在头戴设备的侧边配备了一个触摸板。Unity把这个触摸板当做鼠标来使用,所以我们可以使用以下方法:
Unity - Scripting API: Input.mousePosition
Unity - Scripting API: Input.GetMouseButtonDown
Unity - Scripting API: Input.GetMouseButtonUp
在使用Gear VR时,开发者可能会希望从触摸板中获取swipe数据。我们提供了一个名为VRInput的示例脚本,可以处理swipe,触碰和双触。此外它还支持方向键和键盘上的左Ctrl键(在Unity中的默认输入术语是Fire1),或者是鼠标上的左键,以此来处罚swipe和触摸。
在Unity Editor中,我们可能会希望使用DK2来测试Gear VR的内容。因为目前暂时无法直接从Unity直接关联到Gear VR进行测试。考虑到Gear VR的触摸板作用跟鼠标类似,我们可以考虑使用鼠标来模拟输入。当用户佩戴HMD设备时操控键盘会更容易,因此VRInput同时也会讲方向键操作处理成swipe,将Left-Ctrl(Fire1)处理成触碰。
在使用游戏手柄时,左侧的stick可以用作swipe,其中的某个按键可以用作触碰。
关于如何处理swipe,可以参考VRSampleScenes/Scenes/Examples/Touchpad
以下是ExampleTouchpad脚本,其中根据swipe的方向将AddTorque方法作用于一个刚体,从而让对象旋转。
C#脚本:
- using UnityEngine;
- using VRStandardAssets.Utils;
- namespace VRStandardAssets.Examples
- {
- // This script shows a simple example of how
- // swipe controls can be handled.
- 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;
- }
-
- //Handle the swipe events by applying AddTorque to the Ridigbody
- 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;
- }
- }
- }
- }
- }
- }
- }
- }
- break;
- m_Rigidbody.AddTorque(-Vector3.up * m_Torque);
- case VRInput.SwipeDirection.RIGHT:
- break;
- m_Rigidbody.AddTorque(Vector3.up * m_Torque);
- case VRInput.SwipeDirection.LEFT:
- break;
- m_Rigidbody.AddTorque(-Vector3.right * m_Torque);
- case VRInput.SwipeDirection.DOWN:
- break;
- m_Rigidbody.AddTorque(Vector3.right * m_Torque);
- case VRInput.SwipeDirection.UP:
- break;
- case VRInput.SwipeDirection.NONE:
- {
- switch (swipeDirection)
- {
- private void HandleSwipe(VRInput.SwipeDirection swipeDirection)
-
- }
- m_VRInput.OnSwipe -= HandleSwipe;
- {
- private void OnDisable()
- }
- m_VRInput.OnSwipe += HandleSwipe;
- {
- private void OnEnable()
- [SerializeField] private Rigidbody m_Rigidbody;
- [SerializeField] private VRInput m_VRInput;
- [SerializeField] private float m_Torque = 10f;
- {
- public class ExampleTouchpad : MonoBehaviour
- // swipe controls can be handled.
- // This script shows a simple example of how
- {
- namespace VRStandardAssets.Examples
- using VRStandardAssets.Utils;
VR Samples项目中的VRInput示例
正如上面所提到的,我们所有的示例游戏都使用VRInput来处理触摸屏和键盘的输入。Maze游戏中的摄像机也会对swipe作出响应:
Maze
在这个场景中,CameraOrbit对swipe进行,从而允许对视点进行调整:
C#脚本:
- private void OnEnable ()
- {
- m_VrInput.OnSwipe += HandleSwipe;
- }
- private void HandleSwipe(VRInput.SwipeDirection swipeDirection)
- {
- // If the game isnt playing or the camera is fading, return and dont handle the swipe.
- if (!m_MazeGameController.Playing)
- return;
- if (m_CameraFade.IsFading)
- return;
- // Otherwise start rotating the camera with either a positive or negative increment.
- switch (swipeDirection)
- {
- case VRInput.SwipeDirection.LEFT:
- StartCoroutine(RotateCamera(m_RotationIncrement));
- break;
- case VRInput.SwipeDirection.RIGHT:
- StartCoroutine(RotateCamera(-m_RotationIncrement));
- break;
- }
- }
- }
- }
- break;
- StartCoroutine(RotateCamera(-m_RotationIncrement));
- case VRInput.SwipeDirection.RIGHT:
- break;
- StartCoroutine(RotateCamera(m_RotationIncrement));
- case VRInput.SwipeDirection.LEFT:
- {
- switch (swipeDirection)
- // Otherwise start rotating the camera with either a positive or negative increment.
- return;
- if (m_CameraFade.IsFading)
- return;
- if (!m_MazeGameController.Playing)
- // If the game isnt playing or the camera is fading, return and dont handle the swipe.
- {
- private void HandleSwipe(VRInput.SwipeDirection swipeDirection)
- }
- m_VrInput.OnSwipe += HandleSwipe;
- {
相关阅读:VR游戏开发干货教程:如何创建一个VR项目
锐亚教育,游戏开发论坛|游戏制作人|游戏策划|游戏开发|独立游戏|游戏产业|游戏研发|游戏运营| unity|unity3d|unity3d官网|unity3d 教程|金融帝国3|8k8k8k|mcafee8.5i|游戏蛮牛|蛮牛 unity|蛮牛
- 还没有人评论,欢迎说说您的想法!