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分别控制打光和场景的“地面”,这里不再作具体说明。

## [VRTK_SDKManager]
為VR SDK提供支持。核心是SDKSetups,SDKSetupSwitcher將SDKSetups轉換為便於操作的GUI界面。
## [VRTK_Scripts]
设定手柄等控制器与UGUI的交互。
这里只赋予右手把(RightController)交互功能,需要添加的Component如下。

### VRTK_Controller Events

[*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.

[*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.

[*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.

[*details*](https://vrtoolkit.readme.io/docs/vrtk_uipointer)
記得更改設置
**Activation.Activation Mode = Always On;**
## [Canvas]

* 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()事件

## [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场景的基础上修改:

[VRTK_Scripts]不变,需要修改[VRTK_SDKManager],把游戏结构加入到其中,即可:

## 修改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)下

## 设置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)下

## 制作游戏
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为例)

3. 制作产出位置:新建Empty Object命名为Game拖至Position,在该空物件下再新建空物件若干,分别调节各个空物件位置(空物件所处位置即为产出位置)。

> Note:可用Select Icon与其它游戏物件作区分
4. 将GamePosition拖至[VRTK_SDKManager]/SDKSetups/SteamVR下
5. 指定GM中的产出位置数目,将GamePosition中的空物件拖至生成的对应槽中。

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中的产出物件数目,将制作的产出物件预制物拖至生成的对应槽中。

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”字体的显示。
## 参考设置图片














