Unity | VRTK游戏的基础设计 === ###### tags: `C#` `Unity` [TOC] # HTCVIVE与设备连接 1. 汇入SteamVR和VRTK • 注意点选I made a back up/ Go ahead更新 • 对于汇入后报错需要跳转到 line 146: new Texture改为new Texture2D(0,0) 2. 新建场景Pro1使用SteamVR/Prefabs/[CameraRig],同时删除主摄影机 3. 给[CameraRig]/Controller(Left)/Model和[CameraRig]/Controller(Right)/Model分别添加BoxCollider(isTrigger=True,Size=(0,0,0)))和Rigibody(useGravity=false) 4. 場景中加入Cube,添加BoxCollider(isTrigger=T,rigibody(useGravity=F) ,设置Cube.Tag=(new)Object 5. [CameraRig]/Controller(Left)/Model和[CameraRig]/Controller(Right)/Model添加SteamVRController.cs,将Controller(Left)和Controller(Right)拖入Trackobj槽中 ```csharp= using System.Collections; using System.Collections.Generic; using UnityEngine; public class SteamVRController : MonoBehaviour { [Header("VIVE手把控制器")] public SteamVR_TrackedObject trackobj; //当HTC VIVE 手把碰到物件 void OnTriggerStay(Collider hit) { var steam_state=SteamVR_Controller.Input((int)trackobj.index) ; /* 更改ButtonMask.Grip/Trigger/System/Touchpad * *steam_state.GetPress(SteamVR_Controller.ButtonMask.Grip); * steam_state.GetPress(SteamVR_Controller.ButtonMask.Trigger); *steam_state.GetPress(SteamVR_Controller.ButtonMask.System); *steam_state.GetPress(SteamVR_Controller.ButtonMask.Touchpad); *GetPressDown; *GetPressUp; *if(steam_state.GetPress(SteamXR_Controller.ButtonMask.Grip)) * */ //持续按下HTC VIVE 手把上的Grip键 if(steam_state.GetPress(SteamVR_Controller.ButtonMask.Grip)) { //如果手把碰到的物件标签为Object(如Cube物件) if(hit.GetComponent<Collider>().tag=="Object") { //碰撞物的坐标位置/角度=手把位置/角度 hit.transform.position=transform.position; hit.transform.rotation=transform.rotation; //手把先开机的为左手把 //如果按下左手把Grip按钮 if(trackobj.name=="Controller (left)") { print("Controller (left) Grip"); var deviceIndex=SteamVR_Controller.GetDeviceIndex(SteamVR_Controller.DeviceRelation.Leftmost); //手把震动幅度0-3999,如果值小于100,基本上没啥感觉 SteamVR_Controller.Input(deviceIndex).TriggerHapticPulse(3000); } else { print("Controller (right) Grip"); var deviceIndex=SteamVR_Controller.GetDeviceIndex(SteamVR_Controller.DeviceRelation.Rightmost); //手把震动幅度0-3999,如果值小于100,基本上没啥感觉 SteamVR_Controller.Input(deviceIndex).TriggerHapticPulse(3000); } } } } } ``` # 实现手柄控制发射子弹功能 1. 新建场景Pro2,使用SteamVR/Prefabs/[CameraRig],同时删除主摄影机 2. 在[CameraRig]/Controller(Left)/Model或[CameraRig]/Controller(Right)/Model下建立Sphere. 設置Rigibody.UseGravity=F;SphereCollider.IsTrigger=F 3. 给[CameraRig]/Controller(Left)/Model或[CameraRig]/Controller(Right)/Model添加SteamVRFire.cs。将Sphere拖入Bullet槽,Controller(Left)或Controller(Right)拖入Trackobj槽。 ```csharp= using System.Collections; using System.Collections.Generic; using UnityEngine; public class SteamVRFire : MonoBehaviour { [Header("子弹物件")] public GameObject Bullet; //子弹移速 private float Speed=1000f; //设定子弹销毁时间 private float BulletLife=5f; [Header("VIVE手把控制器")] public SteamVR_TrackedObject trackobj; void Start(){} // Update is called once per frame void Update() { var steam_state=SteamVR_Controller.Input((int)trackobj.index); //如果按下滑鼠左键或HTC VIVE 手把Trigger按键 //if(Input.GetMouseButton(0)||steam_state.GetPress(SteamVR_Controller.ButtonMask.Trigger))// if(Input.GetMouseButton(0)||steam_state.GetPressDown(SteamVR_Controller.ButtonMask.Trigger)) { //动态生成一颗子弹(强制转换) GameObject BulletPrefab=Instantiate(Bullet,Bullet.transform.position,Bullet.transform.rotation) as GameObject; //开启动态生成的物件(子弹) BulletPrefab.SetActive(true); //抓取子弹的RigiBody属性 Rigidbody rb=BulletPrefab.GetComponent<Rigidbody>(); //透过AddForce给子弹某一个方向力,让物体继续往前 rb.AddForce(BulletPrefab.transform.forward*Speed); //子弹没有达到任何东西的情况下就等待5秒删除子弹 Destroy(BulletPrefab,BulletLife); } } } ``` # VRTK插件与UGUI的交互:Menu Scene制作 必备Game Object如下。Directional Light,Floor分别控制打光和场景的“地面”,这里不再作具体说明。 ![f1](https://i.imgur.com/GoauE0Q.png) ## [VRTK_SDKManager] 為VR SDK提供支持。核心是SDKSetups,SDKSetupSwitcher將SDKSetups轉換為便於操作的GUI界面。 ## [VRTK_Scripts] 设定手柄等控制器与UGUI的交互。 这里只赋予右手把(RightController)交互功能,需要添加的Component如下。 ![f2](https://i.imgur.com/6k8KLym.png) ### VRTK_Controller Events ![f3](https://i.imgur.com/bhiuPlB.png) [*details*](https://vrtoolkit.readme.io/docs/vrtk_controllerevents-1) ### VRTK_Pointer Provides a basis of being able to emit a pointer from a specified GameObject. ![f4](https://i.imgur.com/57miUrZ.png) [*details*](https://vrtoolkit.readme.io/docs/vrtk_pointer) ### VRTK_Straight Pointer Renderer A visual pointer representation of a straight beam with an optional cursor at the end. ![f5](https://i.imgur.com/piycVQW.png) [*details*](https://vrtoolkit.readme.io/docs/vrtk_straightpointerrenderer) 記得更改設置 **General Appearance Settings.Tracer Visibility = Always On; General Appearance Settings.Cursor Visibility = Always On;** ### VRTK_UI Pointer Provides the ability to interact with UICanvas elements and the contained Unity UI elements within. ![f6](https://i.imgur.com/54S5Yb3.png) [*details*](https://vrtoolkit.readme.io/docs/vrtk_uipointer) 記得更改設置 **Activation.Activation Mode = Always On;** ## [Canvas] ![f7](https://i.imgur.com/uMd8RqO.png) * Canvas基礎設置 **Canvas.RenderMode = WorldSpace;** * 添加VRTK_UI Canvas * 綁定Menu.cs控制程式 ```csharp using System.Collections; using System.Collections.Generic; using UnityEngine; public class Menu : MonoBehaviour { public void ToProj1() { Application.LoadLevel("Pro1"); } public void ToProj2() { Application.LoadLevel("Pro2"); } public void ToProj4() { Application.LoadLevel("Pro4"); } } ``` * Button添加對應的OnClick()事件 ![](https://i.imgur.com/0stbZYS.png) ## [EventSystem] ## Menu Scene制作步骤概括 1. 新建保存一个Scene。 2. 匯入[VRTK_SDKManager]預知 3. 創建Empty Game Object作為Right Controller和Left Controller。 4. 添加Canvas及其下的Button。 5. 确保有EventSystem生成(若没有从**Scene/[SDKSetupManager]/SDKSetupSwitcher/EventSystem** 中复制粘贴一个EventSystem到**Scene/EventSystem**并勾选Inspector的小方框使之有效)。 6. 設置Canvas模式為**World Space**,并添加**VRTK_UIPointer元件**。 7. 编写menu.cs程式,并将之绑定在Canvas上,然后为Button增添Click事件。 8. 給**Scene/LeftController**添加**VRTK_Controller Events**。 9. 給**Scene/RighttController**添加 * VRTK_Controller Events * VRTK_UI Pointer: .ActivationMode = Always On; * VRTK_Controller UI Pointer Events_Listener Example * VRTK_Pointer * VRTK_Straight Pointer Renderer: .TracerVisibility = Always On; .CursorVisibility = Always On; 10. 在build setting中增添设计的场景并编译 11. 运行(调整button位置)。 # 加入VRTK的遊戲製作:模擬水果忍者 在Menu场景的基础上修改: ![f1](https://i.imgur.com/GoauE0Q.png) [VRTK_Scripts]不变,需要修改[VRTK_SDKManager],把游戏结构加入到其中,即可: ![](https://i.imgur.com/8WOgAOr.png) ## 修改Canvas 1. 删除Button元件 2. 去掉VRTK_UIPointer元件和menu.cs(用不上了) 3. 设置Canvas模式為Screen Space (Camera) 5. 在Canvas下添加三个Text,分别管理Score,Time和GAME OVER文字显示 6. 将Canvas拖至[VRTK_SDKManager]/SDKSetups/SteamVR/[CameraRig]/Camera(head)下 ![](https://i.imgur.com/R5u7lbG.png) ## 设置Controller 1. 制作左右手的武器(从asset找或用cube替代皆可) 2. 分别添加带Trigger的Box Collider和不带Gravity的Rigibody 3. 将武器分别拖至[VRTK_SDKManager]/SDKSetups/SteamVR/[CameraRig]/Controller(Left)和[VRTK_SDKManager]/SDKSetups/SteamVR/[CameraRig]/Controller(Right)下 ![](https://i.imgur.com/e2SZN7P.png) ## 制作游戏 1. 创建Empty Object命名为GM,并赋其GM.cs ```csharp= using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class GM : MonoBehaviour { #region 倒计时 [Header("倒数时间的文字")] public Text CountdownTimeText; [Header("属性面板中可调整整体游戏的时间")] public int Timer; //程式中倒数计时 int ScriptTimer; #endregion #region 计分 [Header("计分文字")] public Text ScoreText; //计算总分数 int TotalScore; #endregion #region 产出物件 [Header("所有要产出的物件")] public GameObject[] CreateObject; [Header("物件产出的位置")] public GameObject[] CreatePos; #endregion [Header("游戏结束文字")] public GameObject GameOverObj; //判断是否游戏结束 bool isGameOver; // Start is called before the first frame update void Start() { //程式中的倒计时=属性面板中调整的时间 ScriptTimer=Timer; InvokeRepeating("CountdownTime",1f,1f); InvokeRepeating("CreateObj",Random.Range(0.5f,1.5f),Random.Range(0.5f,1.5f)); } // Update is called once per frame void Update() { //判断是否游戏结束 if(isGameOver) { GameOverObj.SetActive(true); } else { GameOverObj.SetActive(false); } } void CountdownTime() { ScriptTimer--; ScriptTimer=Mathf.Clamp(ScriptTimer,0,Timer); CountdownTimeText.text="Time: "+ScriptTimer+"s"; if(ScriptTimer<=0) { isGameOver=true; } } public void totalScore(int AddScore) { TotalScore+=AddScore; ScoreText.text="Score: "+TotalScore; } void CreateObj() { if(!isGameOver) { Instantiate(CreateObject[Random.Range(0,CreateObject.Length)],CreatePos[Random.Range(0,CreateObject.Length)].transform.position,transform.rotation); } } } ``` 2. 将Canvas中的Text拖至GM设定中的对应槽中,随便设置一个游戏时间(30s为例) ![](https://i.imgur.com/4sUReRp.png) 3. 制作产出位置:新建Empty Object命名为Game拖至Position,在该空物件下再新建空物件若干,分别调节各个空物件位置(空物件所处位置即为产出位置)。 ![](https://i.imgur.com/tV810Bo.png) > Note:可用Select Icon与其它游戏物件作区分 4. 将GamePosition拖至[VRTK_SDKManager]/SDKSetups/SteamVR下 5. 指定GM中的产出位置数目,将GamePosition中的空物件拖至生成的对应槽中。 ![](https://i.imgur.com/q8YUCYx.png) 6. 制作产出物件之水果:从3D农产品中拖出几种水果,设置Tag=Food;添加带Trigger的Sphere Collider和带Gravity的Rigibody;同时赋其Object.cs;给水果的子物件分别添加不带Trigger的Sphere Collider;完成后将物件制成Prefab保存后从场景中删除。 > Note: 水果的各个Collider尤其要注意范围设置适当。 7. 制作产出物件之炸弹:从3D Weapon中拖出几种炸弹,设置Tag=NotFood;添加带Trigger的Sphere Collider和带Gravity的Rigibody;同时赋其Object.cs;完成后将物件制成Prefab保存后从场景中删除。 ```csharp= using System.Collections; using System.Collections.Generic; using UnityEngine; public class Objects : MonoBehaviour { //往某个方向飞的力量值 float StartForce; Rigidbody rigidbodyObject;// * 动态生成物件最好声明为私有 // Start is called before the first frame update void Start() { //如果物件没有被削到,过5s消失 Destroy(gameObject,5f); //随机生成一定范围内的力量值 StartForce=Random.Range(7.5f,8.5f); //抓取水果/炸弹物件身上的rigibody rigidbodyObject = GetComponent<Rigidbody>(); //给带有Rigibody的物件一个方向力 //Vector3.up;//世界坐标y轴 //transform.up;//物件坐标y轴 rigidbodyObject.AddForce(transform.up * StartForce,ForceMode.Impulse); } } ``` 8. 指定GM中的产出物件数目,将制作的产出物件预制物拖至生成的对应槽中。 ![](https://i.imgur.com/MGM6vl3.png) 9. 完善特效预制:从Prefab中拖出效果预知,分别赋予DestroyObject.cs并指定效果持续时间(假设为2s)后保存修改。 ```csharp= using System.Collections; using System.Collections.Generic; using UnityEngine; public class DestroyObject : MonoBehaviour { public float DeletTime; // Start is called before the first frame update void Start() { Destroy(gameObject,DeletTime); } } ``` 11. 添加武器劈砍产出物件的效果:给之前制作的左右手武器赋予HitObject.cs,指定效果槽数为2,Element0放置刀影效果,Element1放置爆炸效果。 ```csharp= using System.Collections; using System.Collections.Generic; using UnityEngine; public class HitObject : MonoBehaviour { //透过脚本抓取食物子物件 Transform[] FoodChildren; //水果与爆炸物的特效 0:水果特效;1:爆炸特效 public GameObject[] EffectPrefab; // Start is called before the first frame update //void Start(){} private void OnTriggerEnter(Collider hit) { //如果刀子碰到食物 if(hit.GetComponent<Collider>().tag=="Food") { //当刀子砍到水果,防止水果物件重新被触发,将父物件Collider关闭 hit.GetComponent<Collider>().enabled=false; //将父物件的Rigidbody属性删除 Destroy(hit.transform.GetComponent<Rigidbody>()); //找寻下一阶层的子物件 FoodChildren=hit.GetComponentsInChildren<Transform>(); //给子阶层物件Rigidbody FoodChildren[2].gameObject.AddComponent<Rigidbody>(); FoodChildren[1].gameObject.AddComponent<Rigidbody>(); //开启子阶层物件Collider FoodChildren[2].gameObject.GetComponent<Collider>().enabled=true; FoodChildren[1].gameObject.GetComponent<Collider>().enabled=true; //传递分数资讯给GM并显示加250分 GameObject.Find("GM").GetComponent<GM>().totalScore(250); //动态产生水果特效 Instantiate(EffectPrefab[0],hit.transform.position,hit.transform.rotation); } //如果刀子碰到爆炸物 if(hit.GetComponent<Collider>().tag=="NotFood") { //传递分数资讯给GM并显示扣150分 GameObject.Find("GM").GetComponent<GM>().totalScore(-150); //动态产生爆炸物特效 Instantiate(EffectPrefab[1],hit.transform.position,hit.transform.rotation); //删除爆炸物件 Destroy(hit.gameObject); } } } ``` ## 测试 1. 将任一武器拖至Scene下后运行游戏(Simulator模式下)通过拖动武器检验游戏进行中的水果炸弹生成上抛,武器劈砍,水果切开,炸弹爆炸等效果。 2. 暂停游戏后开启SteamVR关闭Simulator后继续进行游戏,观察游戏界面分数与时间的显示,以及游戏结束后“Game Over”字体的显示。 ## 参考设置图片 ![](https://i.imgur.com/oiIzAsR.png) ![](https://i.imgur.com/wpUvov1.png) ![](https://i.imgur.com/OU6IzpL.png) ![handle](https://i.imgur.com/U691syB.png) ![](https://i.imgur.com/R9qHCAH.png) ![](https://i.imgur.com/uytl1RS.png) ![](https://i.imgur.com/zvgtqiF.png) ![](https://i.imgur.com/PZRokx0.png) ![](https://i.imgur.com/q3KXImH.png) ![sword](https://i.imgur.com/26m8Pib.png) ![fruit](https://i.imgur.com/qADuEVE.png) ![fruit_child](https://i.imgur.com/JunbrGl.png) ![boom](https://i.imgur.com/0xxjk3x.png) ![](https://i.imgur.com/i4U8U2n.png) ![](https://i.imgur.com/HYxGEN8.png)