原文:Advanced VR Mechanics With Unity and the HTC Vive Part 1

作者:Eric Van de Kerckhove

译者:kmyhy

VR 从来没有这样时髦过,但是游戏不是那么好做的。为了提供真实的沉浸式体验,游戏内部机制和物理必须让人觉得非常、非常的真实,尤其当你在和游戏中的对象进行交互的时候。

在本教程的第一部分,你会学习如何创建一个可扩展的交互系统,并在系统中实现多种抓取虚拟物品的方式,并飞快地将它们扔出去。

学完本教程后,你可以拥有几个灵活的交互系统并可以用在你自己的 VR 项目中。

注意:本教程适合于高级读者,不会涉及如何添加组件、创建新的游戏对象脚本或者 C# 语法这样的东西。如果你需要提升自己的 Unity 技能,请先阅读我们的getting started with Unityintroduction to Unity Scriptin,然后在阅读本文。

开始

在本教程中,你将必须具备下列条件:

  • 安装好 Unity 5.6.0f3(或以上)
  • 一套带手柄的、安装好、电源开启,准备就绪的 HTV View

如果你之前没有用过 HTC Vive,你可以去看我们之前的HTC Vive tutorial,以了解如何在 Unity 中使用 HTC Vive。HTC Vive 是目前最好的头戴式显示器之一,它所支持的 room-scale 功能提供了精彩的沉浸式体验。

下载开始项目,解压缩,用 Unity 打开项目文件夹。

在项目窗口中看一下目录结构:

分别介绍如下:

  • Materials: 场景中用到的材质。
  • Models: 本文用到的所有模型。
  • Prefabs: 目前只有一个预制件,用于关卡中随处可见的柱子。
  • Scenes:游戏画面和灯光数据。
  • Scripts: 有几个现成的脚本;你自己的脚本也会放到这里。
  • Sounds: 弓箭射出的声音。
  • SteamVR: 放置 SteamVR 插件及其相关脚本、预制件和示例。
  • Textures: 包含了几乎所有模型都共用的纹理(为了效率),以及 book 对象的纹理。

打开 Scenes 文件夹下的 Game 场景。

看一下 Game 视图,你会发现场景中缺少了相机:

在下一节,我们来解决这个问题,添加必要的东西,让 HTC Vive 能够工作。

场景设置

将 SteamVR\Prefabs 目录中将 [CameraRig] 和 [SteamVR] 预制件拖进结构视图。

摄像机现在应该是在地上,但要将它放在木塔上。将 [CameraRig] 的 position 修改为 (X:0, Y:3.35, Z:0) 。现在 Game 视图应该是这个样子:

保存场景,按 Play 按钮试一下是否顺利。四处逛逛,起码用一支手柄试试看能够看到游戏中的控制器。

如果手柄不工作,别担心!在写到此处的时候,最新版的 SteamVR 插件(版本 1.2.1)在 Unity 5.6 中有一个 bug,导致手柄的动作没有被注册。

要解决这个问题,选择 [CameraRig]/Camera (head) 下选择的 Camera (eye),然后为它添加一个 SteamVR_Update_Poses 组件:

这个脚本手动修改手柄的位置和角度。再次运行这个场景,问题解决了。

在编写任何脚本之前,看一下项目中的这几个 tag:

这几个 tag 允许我们更加容易判断哪种种对象发生碰撞或者触象。

交互系统:InteractionObject

交互系统允许场景中的玩家和物理用一种灵活的、模块化的方式进行交互。替代为每个对象和控制器编写重复的代码,你将编写几个类给其它脚本进行继承。

第一个脚本是 RWVR_InteractionObject 类;所有能够被交互的对象都应该从此类继承。这个基类中包含了几个基本的变量和方法。

注意:为了避免和 SteamVR 创建冲突或者便于搜索,本文中所有 VR 脚本都使用 RWVR 前缀。

新建文件夹 Scripts/RWVR。新建类 RWVR_InteractionObject。

打开这个脚本,删除 Start() 和 Update() 方法。

添加下列变量,就在类声明的下方:

protected Transform cachedTransform; // 1
[HideInInspector] // 2
public  RWVR_InteractionController currentController; // 3

你可能会看到报错 “RWVR_InteractionController couldn’t be found”。目前请忽略它,后面我们会创建这个类。

上面代码分别解释如下:

  1. 为了改善性能,将 tranform 值缓存。
  2. 这个属性使下面的变量在检视器窗口中不可见,哪怕它是public 的。
  3. 当前对象正在交互的手柄。后面我们会用到这个手柄。

保存脚本,回到编辑器。

在 RWVR 下面新建一个 C# 文件 RWVR_InteractionController。打开它,删除 Start() 和 Update() 方法,保存。

打开 RWVR_InteractionObject ,之前的错误消失。

注意:如果错误仍然存在,关闭代码编辑器,点一下 Unity,然后再次打开脚本。

在刚刚添加的变量后面新增 3 个方法:

public virtual void OnTriggerWasPressed(RWVR_InteractionController controller)
{
    currentController = controller;
}

public virtual void OnTriggerIsBeingPressed(RWVR_InteractionController controller)
{
}

public virtual void OnTriggerWasReleased(RWVR_InteractionController controller)
{
    currentController = null;
}

这 3 个方法会在手柄的扳机按下、按住和放开时调用。当手柄被按下时,controller 被赋值,当它释放时,controller 被移除。

所有方法都是虚方法,它们将在更复杂的脚本中覆盖,以便它们能使用这些控制器回调方法。

在 OnTriggerWasReleased 方法后新增方法:

public virtual void Awake()
{
    cachedTransform = transform; // 1
    if (!gameObject.CompareTag("InteractionObject")) // 2
    {
        Debug.LogWarning("This InteractionObject does not have the correct tag, setting it now.", gameObject); // 3
        gameObject.tag = "InteractionObject"; // 4
    }
}

分别解释如下:

  1. 缓存 transform 以改善性能。
  2. 检查 InteractionObjet 是否有指定的 tag 值。如果没有,执行 if 后面的代码。
  3. 在检视器中输出一个警告,告诉开发者忘记设置 tag。
  4. 及时设置 tag,以便对象能够像我们期望的工作。

这个交互系统严重依赖于 InteractionObject 和控制器的 tag 来区分特殊对象和其它对象。忘记设置 tag 是很可能的,所以我们专门为这个编写了脚本。这是一种“失效保险”的设计。小心使得万年船。

最后,在 Awake() 方法后添加方法:

public bool IsFree() // 1
{
    return currentController == null;
}

public virtual void OnDestroy() // 2
{
    if (currentController)
    {
        OnTriggerWasReleased(currentController);
    }
}

这些方法分别负责:

  1. 一个公有的 Boolean 方法,表示当前对象是否正在被控制器所用。
  2. 当对象被销毁,将它从当前控制器(如果有的话)中释放。这有助于解决一些莫名其妙的问题。

爆粗脚本,打开 RWVR_InteractionController。

现在它还是空的。我们马上会充实它!

交互系统: Controller

控制器脚本是最重要的部分,因为它是玩家和游戏之间的直接联系。尽可能地接受输入并返回用户正确的反馈很重要。

首先,在类声明下面添加变量:

public Transform snapColliderOrigin; // 1
public GameObject ControllerModel; // 2

[HideInInspector]
public Vector3 velocity; // 3
[HideInInspector]
public Vector3 angularVelocity; // 4

private RWVR_InteractionObject objectBeingInteractedWith; // 5

private SteamVR_TrackedObject trackedObj; // 6

分段解释如下:

  1. 保存对手柄尖端的引用。后面我们会添加一个透明的球,表示你能够到触摸的位置以及距离你可以够到的地方有多远:

  2. 手柄的可见对象。上图中白色的部分。

  3. 手柄的速度和方向。可以用于计算当你做抛掷时物体如何飞出。
  4. 手柄的角度,在抛掷时计算物体的移动也会用到它。
  5. 手柄当前正在交互的 InteractionObjecdt 对象。用它来向当前对象发送事件。
  6. 用于获得真实手柄的引用。

继续在下面添加:

private SteamVR_Controller.Device Controller // 1
{
    get { return SteamVR_Controller.Input((int)trackedObj.index); }
}

public RWVR_InteractionObject InteractionObject // 2
{
    get { return objectBeingInteractedWith; }
}

void Awake() // 3
{
    trackedObj = GetComponent<SteamVR_TrackedObject>();
}

代码解释如下:

  1. 这个变量通过 trackedObj 获得了一个对真实 SteamVR 手柄的引用。
  2. 返回和手柄进行交互的 InteractionObjecdt。对这个对象进行再次封装,是为了对其他类保持只读。
  3. 最后,保持一个和当前控制器相绑定的 TrackedObject 组件的引用,以便后面用到。

然后是这个方法:

private void CheckForInteractionObject()
{
    Collider[] overlappedColliders = Physics.OverlapSphere(snapColliderOrigin.position, snapColliderOrigin.lossyScale.x / 2f); // 1

    foreach (Collider overlappedCollider in overlappedColliders) // 2
    {
        if (overlappedCollider.CompareTag("InteractionObject") && overlappedCollider.GetComponent<RWVR_InteractionObject>().IsFree()) // 3
        {
            objectBeingInteractedWith = overlappedCollider.GetComponent<RWVR_InteractionObject>(); // 4
            objectBeingInteractedWith.OnTriggerWasPressed(this); // 5
            return; // 6
        }
    }
}

这个方法从控制器的碰撞体的某个范围内查找 InteractionObject。一旦找到一个,就将赋给 objectBeingInteractedWith。

代码解释如下:

  1. 创建一个碰撞体的数组,保存 OverlapSpherer() 方法找到的所有碰撞体,查找的位置和 scale 是 snapColliderOrigin,这是一个透明球体,如上图所示,我们后面会添加它。
  2. 遍历整个数组。
  3. 如果找到的碰撞体 tag 值等于 InteractionObject,同时它又是自由的,继续。
  4. 保存碰撞体的 RWVR_InteractionObject 在 objectBeingInteractedWidth。
  5. 调用 objectedBeingInteractedWith 的 OnTriggerWasPressed 方法,将当前控制器传递给它。
  6. 退出循环,完成查找。

新增方法,调用刚刚的这个方法:

void Update()
{
    if (Controller.GetHairTriggerDown()) // 1
    {
        CheckForInteractionObject();
    }

    if (Controller.GetHairTrigger()) // 2
    {
        if (objectBeingInteractedWith)
        {
            objectBeingInteractedWith.OnTriggerIsBeingPressed(this);
        }
    }

    if (Controller.GetHairTriggerUp()) // 3
    {
        if (objectBeingInteractedWith)
        {
            objectBeingInteractedWith.OnTriggerWasReleased(this);
            objectBeingInteractedWith = null;
        }
    }
}

代码非常简单:

  1. 当扳机被按下时,调用 CheckForInteractionObject() 方法,说明有可能发生了一次交互。
  2. 当扳机被按住时,同时有一个对象被抓住时,调用这个对象的 OnTriggerIsBeingPressed()。
  3. 当扳机被松开,同时有一个对象被抓住时,调用这个对象的 OnTriggerWasReleased() 方法,并停止交互。

这些检查确保玩家的所有输入都能被传递到正在和他们交互的 InteractionObject 对象。

添加两个方法,记录控制器的速度和角速度:

private void UpdateVelocity()
{
    velocity = Controller.velocity;
    angularVelocity = Controller.angularVelocity;
}

void FixedUpdate()
{
    UpdateVelocity();
}

FixedUpdate() 以固定帧率调用 UpdateVelocity() ,后者更新 velocity 和 angularVelocity 变量。然后,你会将这两个值传递给一个刚体,以确保扔出去的东西能够更真实的移动。

有时候需要隐藏手柄,以确保体验更加浸入式,避免遮住视线。再添加两个方法:

public void HideControllerModel()
{
    ControllerModel.SetActive(false);
}

public void ShowControllerModel()
{
    ControllerModel.SetActive(true);
}

这些方法简单地启用或禁用代表了控制器的 GameObject。

最后加入这两个方法:

public void Vibrate(ushort strength) // 1
{
    Controller.TriggerHapticPulse(strength);
}

public void SwitchInteractionObjectTo(RWVR_InteractionObject interactionObject) // 2
{
    objectBeingInteractedWith = interactionObject; // 3
    objectBeingInteractedWith.OnTriggerWasPressed(this); // 4
}

代码解释如下:

  1. 这个方法造成了控制器中的压电线型驱动器(这个词不是我编造的)振动多次。它振动的时间越长,震动感就越强烈。它的强度是 1-3999。
  2. 这个方法将激活的 InteractionObject 换成参数指定的对象。
  3. 将指定的 InterationObject 变成激活状态。
  4. 在新的 InteractionObject 对象上调用 OnTriggerWasPressed() 方法,并传入当前控制器。

保存脚本,回到编辑器。为了让控制器按照我们的想法工作,还需要做一些调整。

在结构视图中选中两个控制器。它们都是[ CameraRig ]的子对象。

给它们各添加一个刚体。这允许它们使用固定连接,并和其它物体进行交互。

反选 Use Gravity,勾选 Is Kinematic。控制器不需要受物理的影响,因为在真实世界中,它们被你抓在手上。

将 RWVR_Interaction 控制器组件提交给两个手柄。我们待会要配置它。

展开 Controller(left),右键点击它,选择 3D Object > Sphere,为它添加一个球体。

选中球体,命名为 SnapOrigin,按 F 键让它在场景视图中居中。你会在地板中央看到一个巨大的白色半球体。

设置它的 Position 为 (X:0, Y:-0.045, Z:0.001) ,Scale 设为 (X:0.1, Y:0.1, Z:0.1)。这会将球放到控制器的前端。

删除 Sphere Collider 组件,因为物理检查通过代码进行。

最后,将它的 Mesh Renderer 修改为 Transparent 材质,让球体透明。

复制 SnapOrigin,将 SnapOrigin(1) 拖到 Controller(right)上,变成右手柄的子对象。命名为 SnapOrigin。

最后一步是创建控制器,使用它们的模型和 SnapOrigin。

选择并展开 Controller(left),将它的 SnapOrigin 子对象拖到 Snap Collider Origin 一栏中,将 Model 拖到 Controller Model 一栏。

在 Controller(right) 上重复同样的动作。

现在来放松一下!打开手柄电源,运行这个场景。

将手柄举到头盔前面,看看球体是否能够看见并和控制器粘在一起。

测试完后,保存场景,准备进入交互系统的使用!

用交互系统抓取物体

你可能看到附近有这些东西:

你只能看着它们,但无法把它们拿起来。你最好尽快解决这个问题,否则你怎么去读我们那本精彩的 Unity 教程呢?:]

为了和这些刚体进行交互,你需要创建一个新的 RWVR_InteractionObject 子类,用它来实现抓和扔的功能。

在 Scripts/RWVR 目录下创建新的 c# 脚本,名为 RWVR_SimpleGrab。

用代码编辑器打开它,删除里面的 Start() 和 Update() 方法。

将这一句:

public class RWVR_SimpleGrab : MonoBehaviour

修改为:

public class RWVR_SimpleGrab : RWVR_InteractionObject

这样这个类就继承了 RWVR_InteractionObject,后者提供了获得控制器输入的钩子,这样它就能对输入进行适当的处理。

在类声明下面声明几个变量:

public bool hideControllerModelOnGrab; // 1
private Rigidbody rb; // 2

很简单:

  1. 一个标志,用于表示控制器模型是否应该在该物体被拿起时隐藏。
  2. 为了性能和简单起见,缓存了刚体组件。

在变量声明之后添加方法:

public override void Awake()
{
    base.Awake(); // 1
    rb = GetComponent<Rigidbody>(); // 2
}
  1. 调用基类的 Awake() 方法。这会缓存对象的 Transform 组件并检查 InteractionObject 的 tag 是否赋值。
  2. 保存刚体组件,以便后面使用。

然后是一些助手方法,用于将对象用 FixedJoint 附着在手柄上,或者从手柄上放开。

在 Awake() 方法后面添加:

private void AddFixedJointToController(RWVR_InteractionController controller) // 1
{
    FixedJoint fx = controller.gameObject.AddComponent<FixedJoint>();
    fx.breakForce = 20000;
    fx.breakTorque = 20000;
    fx.connectedBody = rb;
}
private void RemoveFixedJointFromController(RWVR_InteractionController controller) // 2
{
    if (controller.gameObject.GetComponent<FixedJoint>())
    {
        FixedJoint fx = controller.gameObject.GetComponent<FixedJoint>();
        fx.connectedBody = null;
        Destroy(fx);
    }
}

这两个方法分别用于:

  1. 这个方法接收一个控制器作为参数,然后创建一个 FixedJoint 组件添加到手柄上,配置这个连接,使它不是那么容易掉,最后连接上当前的 InteractionObjecdt。在连接上添加一个力是为了防止用户将对象移过其他坚固的物体上,否则可能导致一些奇怪的物理问题。
  2. 将参数指定的控制器的 FixedJoint 组件(如果有的话)断开。所连接的对象将被删除,然后销毁 FixedJoint。

写完这些方法,我们可以实现来自于基类的几个 OnTrigger 方法,以处理用户输入。首先添加 OnTriggerWasPressed() 方法:

public override void OnTriggerWasPressed(RWVR_InteractionController controller) // 1
{
    base.OnTriggerWasPressed(controller); // 2

    if (hideControllerModelOnGrab) // 3
    {
        controller.HideControllerModel();
    }

    AddFixedJointToController(controller); // 4
}

这个方法在玩家按下扳机抓住一个对象时添加 FixedJoint 连接。代码分为几个阶段:

  1. 覆盖基类的 OnTriggerWasPressed() 方法。
  2. 如果 hideControllerModelOnGrab 标志为 true,隐藏控制器模型。
  3. 添加一个 FixedJoint 到控制器。

最后一步是添加 OnTriggerWasReleased() 方法:

public override void OnTriggerWasReleased(RWVR_InteractionController controller) // 1
{
    base.OnTriggerWasReleased(controller); //2

    if (hideControllerModelOnGrab) // 3
    {
        controller.ShowControllerModel();
    }

    rb.velocity = controller.velocity; // 4
    rb.angularVelocity = controller.angularVelocity;

    RemoveFixedJointFromController(controller); // 5
}

这个方法移除参数指定的控制器的 FixedJoint,将控制器的速度传递给刚体,以实现真实的抛掷效果。代码解释如下:

  1. 覆盖基类的 OnTriggerWasReleased() 方法。
  2. 调用基类方法解绑控制器。
  3. 如果 hideControllerModelOnGrab 标志为 true,再次显示控制器模型。
  4. 将控制器的速度和角速度传递给对象的刚体。这样当你放开对象时,对象会表现出真实的行为。例如,如果你扔出一个球,你会将手柄从后向前做一个抛物线动作。球应当获得旋转和向前的力,就像是在真实世界中你将动能传递给它一样。
  5. 删除 FixedJoint。

保存脚本,返回编辑器。

骰子和书在 Prefabs 文件夹中都有相应的预制件。在项目视图中打开这个文件夹:

选择 Book 和 Die 预制件,将 RWVR_Simple Grab 组件添加到二者。同时开启 Hide Controller Model。

保存场景运行游戏。尝试拿起几本书或骰子,扔到一边。

在下一节,我将介绍另一种抓取对象的方法:吸附。

拿起对象和吸附对象

在手柄所在的位置和角度拿起东西是可以的,但有时候将手柄吸附到物体的某个位置可能更有用。例如,如果用户看到一只枪,当他们拿起枪时会希望枪被指向右边。这就是 snapping (吸附)的意思。

为了吸附对象,你需要创建另外一个脚本。在 Scripts/RWVR 目录创建新的 C# 脚本,命名为 RWVR_SnapToController。用代码编辑器打开它,删除 Start() 和 Update() 方法。

将这句:

public class RWVR_SnapToController : MonoBehaviour

改成:

public class RWVR_SnapToController : RWVR_InteractionObject

这允许脚本具备所有 InteractionObject 的功能。

添加变量声明:

public bool hideControllerModel; // 1
public Vector3 snapPositionOffset; // 2
public Vector3 snapRotationOffset; // 3

private Rigidbody rb; // 4
  1. 一个标志,表示手柄模型是否要在玩家抓住对象时隐藏。
  2. 当抓住对象时添加的位置。该对象默认会用这个位置吸附到手柄上。
  3. 同上,只是这个变量用于表示角度。
  4. 引用了对象的刚体组件。

然后增加方法:

public override void Awake()
{
    base.Awake();
    rb = GetComponent<Rigidbody>();
}

和 SimpleGrab 脚本一样,覆盖了基类的 Awake() 方法,然后保存刚体组件。

接下来是几个助手方法,这才算是这个脚本的肉戏。

添加如下方法:

private void ConnectToController(RWVR_InteractionController controller) // 1
{
    cachedTransform.SetParent(controller.transform); // 2

    cachedTransform.rotation = controller.transform.rotation; // 3
    cachedTransform.Rotate(snapRotationOffset);
    cachedTransform.position = controller.snapColliderOrigin.position; // 4
    cachedTransform.Translate(snapPositionOffset, Space.Self);

    rb.useGravity = false; // 5
    rb.isKinematic = true; // 6
}

这个方法和 SimpleGrab 脚本中的方法不同,它不使用 FixedJoint 连接,而是将它自己作为控制器的子对象。也就是说控制器和所吸附的对象是无法被外力所打断的。在这个教程中,这种方式会很稳定,但在你自己的项目中你更应该采取 FixedJoint 连接。

代码解释如下:

  1. 接收一个控制器参数,用于连接它。
  2. 将对象的 parent 设置为该控制器。
  3. 让对象的方向和控制器保持一定的偏移。
  4. 让对象的位置和控制器保持一定的偏移。
  5. 关闭重力,否则它会从你的手上掉落。
  6. 开启运动学特征。当附着到手柄上后,这个对象不会受福利引擎的影响。

现在来添加放开对象的方法:

private void ReleaseFromController(RWVR_InteractionController controller) // 1
{
    cachedTransform.SetParent(null); // 2

    rb.useGravity = true; // 3
    rb.isKinematic = false;

    rb.velocity = controller.velocity; // 4
    rb.angularVelocity = controller.angularVelocity;
}

这个方法简单地将对象从父对象中解除,重置刚体并应用控制器的速度。详细解释一下:

  1. 方法参数指定要松开对象的控制器。
  2. 将对象的父对象解开。
  3. 重新打开重力,并再次使对象再次变成非运动学的。
  4. 应用控制器的速度给对象。

覆盖如下方法以实现 snapping 操作:

public override void OnTriggerWasPressed(RWVR_InteractionController controller) // 1
{
    base.OnTriggerWasPressed(controller); // 2

    if (hideControllerModel) // 3
    {
        controller.HideControllerModel();
    }

    ConnectToController(controller); // 4
}

代码非常简单:

  1. 覆盖 OnTriggerWasPressed(),以添加吸附逻辑。
  2. 调用机类方法。
  3. 如果 hideControllerModel 标志为 true,隐藏控制器模型。
  4. 将对象连接到控制器。

然后是 release 方法:

public override void OnTriggerWasReleased(RWVR_InteractionController controller) // 1
{
    base.OnTriggerWasReleased(controller); // 2

    if (hideControllerModel) // 3
    {
        controller.ShowControllerModel();
    }

    ReleaseFromController(controller); // 4
}

同样十分简单:

  1. 覆盖 OnTriggerWasReleased() 方法。
  2. 调用基类的方法。
  3. 如果 hideControllerModel 标志为 true,重新显示手柄的模型。
  4. 将对象从控制器上放开。

保存脚本返回编辑器。从 Prefabs 目录中将 RealArrow 预制件拖到结构视图。

选择 arrow,设置它的 position 为 (X:0.5, Y:4.5, Z:-0.8)。它会悬浮在石板上方:

在结构视图中,将 RWVR_Snap To Controller 组件附加到箭支上,这样你就可以和它交互,同时将它的 Hide Controller Model 设为 true。最后点击检视器窗口上方的 Apply 按钮,将修改应用到该预制件。

对于这个对象,不需要修改 offset,默认它的握持部位就可以了。

保存并运行场景。抓住箭支,然后扔出去。唤醒你内心野兽吧!

注意,箭支握在手上的位置总是固定的,不管你如何拿起它。

本教程的内容就到此为止了,试玩一下游戏,感受一下交互中的变化。

结束

此处下载最终项目。

在本教程中,你学习了如何创建可扩展的交互系统,你已经通过这个交互式系统找出了几种抓取物品的方法。

在第二部分的教程中,你将学习如何扩展这个系统,制作一套功能完备的弓和箭,以及一个功能完备的背包。

如果你想学习更多关于用 Unity 编写杀手游戏,请阅读我们的Unity Games By Tutorials

在这本书中,你将创建 4 个完整的游戏:

  • 一个 twin-stick 射击游戏
  • 一个第一人称射击游戏
  • 一个塔防游戏(带 VR 支持!)
  • 一个 2D 平台游戏

    学完这本书后,你将能够编写自己的游戏运行在 Windows、macOS、iOS及更多平台。

本书完全针对 Unity 初学者,将他们的 Unity 技能升级到专家水准。本书假设你有一定的编程经验(任何语言)。

感谢你阅读本教程!如果有任何意见和建议,请留言!

用 Unity 和 HTC Vive 实现高级 VR 机制(1)的更多相关文章

  1. 用Unity开发HTC VIVE——手柄控制篇

    写这篇文章的原因主要是因为现在虚拟现实非常的火爆但目前主流的虚拟现实设备(HTC VIVE)的教程却少的可怜,这个我深有体会.所以,我想将我平时开发中遇到的问题以及解决方法记录下来,分享给大家,若其中 ...

  2. Unity的HTC VIVE SDK研究(手柄按键功能的研究,比较详细)

    http://blog.csdn.net/ystistheking/article/details/51553237 想交流的朋友我们可以微博互粉,我的微博黑石铸造厂厂长 ,缺粉丝啊 .....求粉求 ...

  3. unity htc vive, ugui for vr

    http://wacki.me/blog/2016/06/vr-gui-input-module-for-unity-htc-vive/

  4. HTC VIVE 虚拟现实眼镜VR游戏体验

    HTC的VIVE入手一段时间了,体验了几个免费的VR游戏,效果还不错,分享一下. 1. VIVE主要部件 VIVE的主要部件有3个,分别是头盔,两个无线控制手柄和两个定位器. 1.1 头盔 头盔整体照 ...

  5. 用Unity开发HTC VIVE——移动漫游篇

    这篇文章主要写的是通过手柄控制移动在场景中漫游.在通过手柄控制移动时,我主要写了两个脚本一个ChildTransform.cs.Move.cs;1. ChildTransform这个脚本主要是为了获取 ...

  6. HTC vive VR设备软硬件安装+运行unity开发的VR程序

    总结在HTC vive VR开发过程中的HTC vive的安装调试 1.首先确保电脑的配置满足要求: 进入官网,测试电脑是否满足要求 链接:https://www.vive.com/us/produc ...

  7. Unity 5.4大赞:HTC Vive经典The lab渲染器开源

    HTC Vive提供了一个不错的免费VR demo,最近1周仔细体验了一番. 仔细看了其安装文件,竟然是Unity 5.4beta版本(通过查log,知道Valve公司用的是最新的5.4.0b11版本 ...

  8. Unity正式发布首个“实验性”VR编辑器,支持HTC Vive和Oculus Rift

    Unity今天正式推出"实验性"VR编辑器.据悉,EditorVR是Unity游戏引擎中的一个组件,可让开发者在虚拟现实环境中开发游戏.为何要称之为"实验性"? ...

  9. unity htc vive使用

    本文介绍如何在Unity中使用HTC vive设备,当前VR作为市场比较火热的热点,HTC VIVE设备作为三大供应商之一,许多人购买了该设备,却不知道如何使用,本文通过图文并茂的形式,进行手把手的讲 ...

随机推荐

  1. jdk eclipse SDK下载安装及配置教程

    原文地址https://blog.csdn.net/dr_neo/article/details/49870587 最新鲜最详细的Android SDK下载安装及配置教程 最近Neo突发神经,想要将学 ...

  2. Hadoop mapreduce自定义排序WritableComparable

    本文发表于本人博客. 今天继续写练习题,上次对分区稍微理解了一下,那根据那个步骤分区.排序.分组.规约来的话,今天应该是要写个排序有关的例子了,那好现在就开始! 说到排序我们可以查看下hadoop源码 ...

  3. 微信小程序中公用内容

    微信小程序中各个页面调用公用的js 在util.js文件中 // 跳转哪里 function go(where) { wx.reLaunch({ url: where, }) } // 将方法暴露出去 ...

  4. c++第二十天

    p111~p114: 与旧代码的接口 1.C++程序中的C风格字符串:任何出现字符串字面值的地方都可以以空字符结束的字符数组来替代. const char str[] = {"Hello, ...

  5. pyDay14

    内容来自廖雪峰的官方网站. 1.map的优点:省代码 + 提高可读性. 2.map 运用示例: >>> def f(x): ... return x * x ... >> ...

  6. 20145325张梓靖 实验二"Java面向对象程序设计"

    20145325张梓靖 实验二"Java面向对象程序设计" 程序设计过程 实验内容 使用TDD的方式设计实现复数类 Complex 编写代码 设计实现复数类 Complex,复数类 ...

  7. 20172305 2018-2019-1 《Java软件结构与数据结构》第五周学习总结

    20172305 2018-2019-1 <Java软件结构与数据结构>第五周学习总结 教材学习内容总结 本周内容主要为书第九章内容: 查找是在某个项目组中寻找到某一指定目标元素,或者确定 ...

  8. Two Sum(II和IV)

    本文包含leetcode上的Two Sum(Python实现).Two Sum II - Input array is sorted(Python实现).Two Sum IV - Input is a ...

  9. [QA翻译]如何在Storm里拆分stream流?

    原文:http://stackoverflow.com/questions/19807395/how-would-i-split-a-stream-in-apache-storm 问题:我现在不清楚如 ...

  10. Nature 为引,一文看懂个体化肿瘤疫苗前世今生

    进入2017年,当红辣子鸡PD-1疗法,一路横扫多个适应症.而CAR-T治疗的“小车”在获得FDA专委会推荐后也已经走上高速路,成为免疫治疗又一里程碑事件.PD-1.CAR-T之后,下一个免疫治疗产品 ...