虚拟现实简介:创建一款面向 Oculus Rift* 的第一人称游戏

查看 PDF [12,544KB]

简介

本文介绍了虚拟现实 (VR) 概念,讨论了如何将 Unity* 应用集成至 Oculus Rift*,添加 Oculus 第一人称玩家角色至游戏以及将玩家远距传动至场景。本文的目标受众是想要将 Oculus Rift 集成至 Unity 场景的 Unity 开发人员。假定前提是读者的装备能够创建面向 Oculus 的虚拟现实游戏,拥有一台面向 Oculus 的 PCOculus Rift 以及触摸控制器

开发工具

  • Unity 5.5 或更高版本
  • Oculus Rift 和触摸控制器

在 Unity 中创建地形

可获取多个在线资源,了解如何在 Unity 中创建一个基本地形。我参阅的是《Unity 手册》。在场景中添加大量树木和草地将影响性能,导致每秒帧数 (FPS) 大幅下降。确保您的树木数量达到最佳值,如需要,最大限度降低草地的最小高度/最大高度以及最小宽度/最大宽度,以减轻对 FPS 的影响。为了改进游戏的虚拟现实体验,建议 FPS 最低为 90。

设置 Oculus Rift

本章节介绍了如何设置 Oculus Rift,将 Oculus 第一人称角色置于场景,以及将玩家从一个场景远距传动至另一个场景。

从 Oculus 网站下载说明,并按照说明进行操作。

完成设置后,确保 Oculus 已集成至设备,然后执行以下操作:

  1. 下载面向 Unity 5 的 Oculus 实用程序
  2. 将 Unity 程序包导入您的 Unity 项目。
  3. 从您的场景中移除主摄像头对象。该摄像头没有必要保留,因为 Oculus OVRPlayerController 预制件自带了定制虚拟现实摄像头。
  4. 导航至 Assets/OVR/Prefabs 文件夹。
  5. 将 OVRPlayerController 预制件拖放到您的场景。您可以使用 OVRCameraRig 预制件。如欲了解这些预制件和它们之间的差别,请访问本链接。使用 OVRPlayerController 实施以下示例。

将头戴式设备调整为最佳状态,您便可以清晰看到整个场景。设置 Oculus Rift 时,根据需要和提供的说明调整设置。单击 Stats 按钮观察场景的 FPS。如果 FPS 低于建议的 90 FPS,减少 Unity 场景中的细节,或排除故障,以了解场景中哪些部分消耗了过多 CPU/GPU,以及为什么会影响 FPS。

现在我们看一下如何利用 Oculus 触摸控制器与场景中的对象交互。我们在场景中添加一个猎枪模型,玩家便可以攻击敌人。您可以自行创建模型,也可以从 Unity Asset Store 中下载。我从 Unity store 中下载了一个模型。

  1. 如下所示,将该模型置于 OVRPlayerController 的 RightHandAnchor 下。
  2. 调整模型的尺寸和方向,使其适合场景且符合您的要求。

移动右触摸控制器后,将直接控制场景中的猎枪。

添加代码,以操控 Oculus 触摸控制器

在如下的代码片段中,我们检查了 OVRInput,根据在 Oculus 触摸控制器中按下的按钮,执行以下三项之一:

  • 如果 Primary Index Trigger 按钮(即右控制器的触发按钮)被按下,将调用 RayCastShoot 函数,远距传动选项被设置为 false。在这种情况下,玩家对象将向敌人以及场景中搭建的任何目标开火。我们还确保在特定时间内仅开火一次,通过检查 Time.Time > nextfire 条件,可以更改间隔的时间。
  • 如果控制器中的 A 按钮被按下,将调用 RayCastShoot 函数,将远距传动选项设置为 true。该选项将玩家远距传动至地形中不同的点。既可以远距传动场景中设置的预定义点,也可以直接将玩家远距传动至命中点。开发人员基于游戏的要求,决定将玩家远距传动至场景的哪个位置。
  • 在游戏中,无论何时按下控制器的 B 按钮,玩家将重置于初始位置。
void Update () {

        if (OVRInput.Get(OVRInput.Button.PrimaryIndexTrigger) && (Time.Time > nextfire))

        {
            //If the Primary Index trigger is pressed on the touch controller we fire at the targets
            nextfire = Time.Time + fireRate;
            audioSource.Play();

            // Teleporting is set to false here
            RayCastShoot(false);        }
        else if (OVRInput.Get(OVRInput.RawButton.A) && (Time.time > nextfire))
        {

            // Teleporting is set to true when Button A is pressed on the controller
            nextfire = Time.time + fireRate;

            RayCastShoot(true);

        }

        else if (OVRInput.Get(OVRInput.RawButton.B) && (Time.time > nextfire))
        {
            // If Button B is pressed on the controller player is reset to his original position

            nextfire = Time.time + fireRate;
            player.transform.position = resetPosition;

        }
    }

在以下示例中,我将僵尸添加为敌人(僵尸可以从 Unity Asset Store 中下载),还添加了一些目标,如岩石和手榴弹,以增加场景的粒子效果,如爆炸、岩石撞击等。我还根据该教程创建了一个简单的僵尸动画。

现在,我们来看一下 RayCastshoot 函数。Physics.Raycast 从 gunTransform 位置正向发射一条光线,指向场景中的碰撞器。在 weaponRange 变量中规定了范围。如果光线击中某物,将被存储在命中变量。

RaycastHit hit;

        if (Physics.Raycast(gunTransform.position, gunTransform.forward, out hit, weaponRange))

RayCastshoot 函数接收一个布尔值。如果该值为 true,函数远距传动玩家;如果该值为 false,则检查场景中的所有对象,如僵尸、岩石、手榴弹等,撞击并摧毁敌人和目标。

首先处理僵尸对象。添加 Physics rigid body 组件,并将 kinematic 值设置为 true。我们还添加了一个名为 Enemy.cs 的小脚本,并将其连接至敌人对象。以下脚本接收一个函数,并检查敌人的生命值。每调用 1 次 enemyhit 函数(每当向敌人开火使)将减少敌人的 1 个生命值。敌人被击中 5 次后,将被摧毁。

在 RayCastshoot 函数中调用该函数,以处理僵尸对象,并确定是否瞄准了僵尸。

Enemy enemy = hit.collider.GetComponentInParent<Enemy>();

如果敌人对象不是 null,将调用 enemyhit 函数,敌人的生命值将减少 1。如下所示,每次击中僵尸时,我们都会实例化血液效果预制件。我们检查敌人的完整生命值,如果低于零,将毁灭僵尸对象。

//Enemy.cs
public class Enemy :MonoBehaviour {

    //public GameObject explosionPrefab;
    public int fullLife = 5;


    public void enemyhit(int life)
    {
        //subtract life  when Damage function is called
        fullLife -= life;

        //Check if full life has fallen below zero
        if (fullLife <= 0)
        {
            //Destroy the enemy if the full life is less than or equal to zero
            Destroy(gameObject);

        }
    }

}

// if the hit object is the enemy
//Raycastexample.cs from where we are calling the enemyhit function
if (enemy != null)

            {
                enemy.enemyhit(1);

                //Checks the health of the enemy and resets to  max again
                //Instantiates the blood effect prefab for each hit

                var bloodEffect = Instantiate(bloodPrefab);
                bloodEffect.transform.position = hit.point;

                if (enemy.fullLife <= 0)
                {

                    enemy.fullLife = 5;
                }
            }

如果被击中的对象不是僵尸,通过为场景中不同对象添加标记来访问对象。例如,我们为地面添加了名为“Mud”的标记,为岩石添加了“Rock”标记等。如以下代码示例所示,我们可以通过对比标记与被击中的对象,针对这些对象实例化各自的预制件效果。

//If the hit targets are the targets other than the enemy like the mud, Rocks , Grenades on the terrain
else
            {
                var impactEffect = Instantiate(impactEffectPrefab);
                impactEffect.transform.position = hit.point;
                Destroy(impactEffect, 4);

                // If the Target is the ground
                if ((hit.collider.gameObject.CompareTag("Mud")))
                {

                    var mudeffect = Instantiate(mudPrefab);
                    mudeffect.transform.position = hit.point;


                }

                // If the Target is  Rocks
                else if ((hit.collider.gameObject.CompareTag("Rock")))
                {

                    var rockeffect = Instantiate(rockPrefab);
                    rockeffect.transform.position = hit.point;
                }

                // If the Target is the Grenades

                else if ((hit.collider.gameObject.CompareTag("Grenade")))
                {

                    var grenadeEffect = Instantiate(explosionPrefab);
                    grenadeEffect.transform.position = hit.point;
                    Destroy(grenadeEffect, 4);

                }
            }

        }

远距传动

远距传动是虚拟现实游戏的一个重要方面,建议使用远距传动,避免用户在场景中移动时产生眩晕感。以下示例在 Unity 中实施了一个简单的远距传动机制。借助代码,我们可以将玩家远距传动至“命中”点,也可以在地形中创建多个点,将玩家远距传动至这些点。

  1. 创建一个空的游戏对象,将它命名为“Teleport”。

  2. 创建一个名为“Teleport”的标记。

  3. 如下所示,将 Teleport 标记分配给 teleport 对象。

  4. 按下 CTRL+D 并复制点,在场景中创建更多的远距传动点。调整点的位置,使其跨越整个地形。我将 y 位置设置为 OVR 玩家预制件的位置,那么,这些点的 y 值和我的摄像头位置相同。

根据以下代码,如果远距传动被设置为 true,在 teleportPoints 变量中得到所有点的阵列,随机选取一个点用于玩家进行远距传动。

var newPosition = teleportPoints[Random.Range(0, teleportPoints.Length)];

最后,将玩家的变换位置设置为新位置。

player.transform.position = newPosition.transform.position;
if (teleport)
            {
                //If the player needs to be teleported to the hit point
                // Vector3 newposition = hit.point;
                //player.transform.position = new Vector3(newposition.x, player.transform.position.y, newposition.z);


                //If the player needs to be teleported to the teleport points that are created in the Unity scene.Below code teleports the player
                // to one of the points randomly

                var teleportPoints = GameObject.FindGameObjectsWithTag("Teleport");
                var newPosition = teleportPoints[Random.Range(0, teleportPoints.Length)];


                player.transform.position = newPosition.transform.position;

                return;
            }

创建设置和部署虚拟现实应用

完成游戏后,部署面向 PC 的虚拟现实应用。

  1. 转至 File > Build Settings,然后转至 Target Platform,选择 Windows。

  2. 转至 Edit > Project Settings > Player,然后单击 Inspector 选项卡。
  3. 单击 Other Settings,然后选择 Virtual Reality Supported 复选框。

  4. 编译并创建最终的虚拟现实应用。

结论

创建一款虚拟现实游戏非常有趣,但是需要极高的准确性。如果您是一位 Unity 开发人员,您的游戏不是专为虚拟现实设计,也可以与 Oculus Rift 集成,作为虚拟现实游戏导出。本文结尾处列出了许多关于虚拟现实最佳实践的参考文献。

以下是本文所讨论的示例场景的完整脚本。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Raycastexample :MonoBehaviour {


    //Audio clip to play
    public AudioClip clip;
    public AudioSource audioSource;

    //rate of firing at the targets
    public float fireRate = .25f;
    // Range to which Raycast will detect the collision
    public float weaponRange = 300f;

    //Prefab for Impacts at the target
    public GameObject impactEffectPrefab;
    //Prefab for Impacts for grenade explosions
    public GameObject explosionPrefab;

    //Prefab at gun transform position
    public GameObject GunfirePrefab;

    //Prefab if the target is the terrain
    public GameObject mudPrefab;
    // Prefab when hits the Zombie
    public GameObject bloodPrefab;

    // prefabs when hits the rocks
    public GameObject rockPrefab;

    // Player transform that is used in teleporting
    public Transform player;
    private float nextfire;

    //transform at the Gun end to show some muzzle effects when firing
    public Transform gunTransform;
    // Position to reset the player to its original position when "B" is pressed on the touch controller
    private Vector3 resetPosition;

    // Use this for initialization
    void Start () {

        // Play the Audio clip while firing
        audioSource = GetComponent();
        audioSource.clip = clip;
        // Reset position after teleporting to set the position to his original position
        resetPosition = transform.position;

    }

	// Update is called once per frame
	void Update () {
        //If the Primary Index trigger is pressed on the touch controller we fire at the targets
        if (OVRInput.Get(OVRInput.Button.PrimaryIndexTrigger) && (Time.time > nextfire))
        {
            nextfire = Time.time + fireRate;
            audioSource.Play();
            // Teleporting is set to false here
            RayCastShoot(false);
        }
        else if (OVRInput.Get(OVRInput.RawButton.A) && (Time.time > nextfire))
        {

            // Teleporting is set to true when Button A is pressed on the controller
            nextfire = Time.time + fireRate;

            RayCastShoot(true);

        }

        else if (OVRInput.Get(OVRInput.RawButton.B) && (Time.time > nextfire))
        {
            // If Button B is pressed on the controller player is reset to his original position

            nextfire = Time.time + fireRate;
            player.transform.position = resetPosition;

        }

    }

    private void RayCastShoot(bool teleport)
    {
        RaycastHit hit;
        //Casts a ray against the targets in the scene and returns the "hit" object.
        if (Physics.Raycast(gunTransform.position, gunTransform.forward, out hit, weaponRange))
        {

            if (teleport)
            {
                //If the player needs to be teleported to the hit point
                // Vector3 newposition = hit.point;
                //player.transform.position = new Vector3(newposition.x, player.transform.position.y, newposition.z);


                //If the player needs to be teleported to the teleport points that are created in the Unity scene.Below code teleports the player
                // to one of the points randomly

                var teleportPoints = GameObject.FindGameObjectsWithTag("Teleport");
                var newPosition = teleportPoints[Random.Range(0, teleportPoints.Length)];


                player.transform.position = newPosition.transform.position;



                return;
            }

            //Attach the Enemy script as component to the enemy

            Enemy enemy = hit.collider.GetComponentInParent();

            // Muzzle effects of the Gun and its tranfrom poisiton is the Gun

            var GunEffect = Instantiate(GunfirePrefab);
            GunfirePrefab.transform.position = gunTransform.position;



            // if the hit object is the enemy

            if (enemy != null)

            {
                enemy.enemyhit(1);

                //Checks the health of the enemy and resets to  max again
                //Instantiates the blood effect prefab for each hit

                var bloodEffect = Instantiate(bloodPrefab);
                bloodEffect.transform.position = hit.point;

                if (enemy.fullLife <= 0)
                {

                    enemy.fullLife = 5;
                }
            }

            //If the hit targets are the targets other than the enemy like the mud, Rocks , Grenades on the terrain

            else
            {
                var impactEffect = Instantiate(impactEffectPrefab);
                impactEffect.transform.position = hit.point;
                Destroy(impactEffect, 4);

                // If the Target is the groud
                if ((hit.collider.gameObject.CompareTag("Mud")))
                {
                    Debug.Log(hit.collider.name + ", " + hit.collider.tag);
                    var mudeffect = Instantiate(mudPrefab);
                    mudeffect.transform.position = hit.point;


                }

                // If the Target is  Rocks
                else if ((hit.collider.gameObject.CompareTag("Rock")))
                {

                    var rockeffect = Instantiate(rockPrefab);
                    rockeffect.transform.position = hit.point;
                }

                // If the Target is the Grenades

                else if ((hit.collider.gameObject.CompareTag("Grenade")))
                {

                    var grenadeEffect = Instantiate(explosionPrefab);
                    grenadeEffect.transform.position = hit.point;
                    Destroy(grenadeEffect, 4);

                }
            }

        }
    }
}
//Enemy.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Enemy :MonoBehaviour {

    //public GameObject explosionPrefab;
    public int fullLife = 5;

    // Use this for initialization
    void Start () {

	}


    public void enemyhit(int life)
    {
        //subtract life  when Damage function is called
        fullLife -= life;

        //Check if full life has fallen below zero
        if (fullLife <= 0)
        {
            //Destroy the enemy if the full life is less than or equal to zero
            Destroy(gameObject);

        }
    }


}

英特尔® 开发人员专区上关于虚拟现实的参考文献

VRMonkey 提供的虚拟现实用户体验技巧:https://software.intel.com/zh-cn/articles/virtual-reality-user-experience-tips-from-vrmonkey

《亚利桑那阳光》的临场感、逼真性和震撼性:https://software.intel.com/zh-cn/blogs/2016/12/01/presence-reality-and-the-art-of-astonishment-in-arizona-sunshine

利用用户体验设计对抗虚拟现实眩晕:https://software.intel.com/zh-cn/articles/combating-vr-sickness-with-user-experience-design

对 Well Told Entertainment 及其虚拟现实密室逃脱游戏的采访:https://software.intel.com/zh-cn/blogs/2016/11/30/interview-with-well-told-entertainment-about-their-virtual-reality-escape-room-game

虚拟现实体验的下一个飞跃是什么?:https://software.intel.com/zh-cn/videos/what-is-the-next-leap-in-vr-experiences

来自 Underminer 工作室的虚拟现实优化建议:https://software.intel.com/zh-cn/articles/vr-optimization-tips-from-underminer-studios

利用英特尔® 图形性能分析器优化虚拟现实性能:https://software.intel.com/zh-cn/videos/vr-optimizations-with-intel-graphics-performance-analyzers

利用最新款 CPU 创建沉浸式虚拟世界:https://software.intel.com/zh-cn/articles/creating-immersive-virtual-worlds-within-reach-of-current-generation-cpus

关于作者

Praveen Kundurthy 任职于英特尔® 软件和服务事业部。他主要专注于移动技术、Microsoft Windows*、虚拟现实和游戏开发领域。

有关编译器优化的更完整信息,请参阅优化通知