SteamVR 拋物線移動
其實實現拋物線很簡單,生成一組拋物點,然后將點渲染成線就好。渲染成線有很多方式,你可以用模型,也可以用GL的繪制線段,也可以用LineRender。重點是優化點的生成計算。
樓主上班比較忙,很少寫demo。我做項目一般有嚴密的框架,但是為了更加簡明的為大家展示功能的實現剔除了很多代碼,並且寫了不符合我風格的代碼,就是為了讓大家能看清楚功能的實現。
我不是個人開發者,家中無測試環境,如果有問題可以在下面回復。本教程中的代碼已經用在實際的案例上,因為工作保密我不能截圖或者共享項目源碼,只是把開發過程的部分代碼功能以個人的名義分享,歡迎交流。
如何生成拋物點,一個很簡單的公式:
nextPos = lastPos + 水平位移 + 垂直位移。
稍后的代碼有詳盡的注釋我就不多贅述了。
重點是對點集的優化,優化拋物點分兩個部分,一是如何計算碰撞點,二是內存開銷的優化。考慮到一般的VR項目只在水平面上進行移動,所以通過判斷拋物點的y軸來判定碰撞。關於內存開銷,我們可以限定生成的拋物點個數,同時優化計算。我們使用一個List來保存點集,動態的根據點的情況來生成點,或者改變點的位置。例如當手柄角度不變時,只需要將List集合中的點改變Y軸就行了。
現在思路已經明了了。以下是偽代碼:
int i = 0;
while( nextPos.y>0 && maxPoint>0 ){
if(list.count<=i){
list.add(nextPos);
}
else{
list[i] = nextPos;
}
///生成,優化,計算下一個拋物點
i++;
}
list.remove(i,list.count-i);//移除上一個點集的多余數據
生成拋物線點集后,接下來就是繪制曲線,LineRender,GL都可以很方便的繪制。但是就性能來說,GL更加快一點。建議大家用GL。當然大家也可以使用自己的模型繪制,原理就是,在對應的點生成你的模型,然后計算對應的角度即可。這幾種實現方案我都會寫到代碼中,效果大家自己調用查看。
最后就是生成拋物線的終點,因為我是按照y軸來判斷拋物線的落點,但是落點可能有障礙物等等,或者落點在牆角,這些位置顯然是不能跳躍的,所以我們需要對落點進行判斷。在Physics中有一個方法可以檢測一個球形范圍是否會碰撞,我們可以給地面添加一個層,以便忽略地面的檢測,這樣就可以安全的着陸了。
好了,也許你沒聽懂,沒關系,接下來就是代碼,代碼中也有詳盡的注釋。
using UnityEngine;
using System.Collections.Generic;
using System;
/// <summary>
/// 拋物線腳本
/// </summary>
public class HandRay : MonoBehaviour
{
private Transform CurrentHand; //當前觸發的手
private List<Vector3> pointList; //曲線點集合
private Quaternion lastRotation; //上一個移動的角度
private Vector3 lastPostion; //上一個位置
private Vector3 lastPos; //上一個點,為了優化將一個臨時變量做成全局的,節省內存開銷
private Vector3 nextPos;//下一個點,理由同上
private event Action OnChangeTransform;//一個事件,用來檢測手柄位置和角度變化的
private Material material;//渲染射線的材質球
private Vector3 HitPoint;//拋物線的碰撞點
private Ray ray;
private bool canJump = false;//
public GameObject PointEffect;//一個特效,就是在射線的終點放置一個光柱什么的,大家可以自己做這個特效
public int MaxPoint; //生成曲線的點最大數量
public float Distence;//水平位移
public float Grity;//垂直位移
public float CheckRange;//檢測位置是否存在障礙物
public void Awake()
{
SetData();
}
public void Start()
{
pointList = new List<Vector3>();
OnChangeTransform += OnChangeTransformCallBack;
HitPoint = -Vector3.one;
ray = new Ray();
}
public void Update()
{
//當手柄按下觸摸鍵同時角度合適時觸發事件開始計算點
if (CurrentHand != null && ((CurrentHand.eulerAngles.x > 275 && CurrentHand.eulerAngles.x <= 360) || (CurrentHand.eulerAngles.x >= -0.01f && CurrentHand.eulerAngles.x < 85)))
{
if (OnChangeTransform != null) OnChangeTransform();
}
else
{
pointList.Clear();
PointEffect.SetActive(false);
}
}
/// <summary>
/// 計算拋物線的點
/// 此方法已經優化過性能
///
/// </summary>
private void OnChangeTransformCallBack()
{
if (lastRotation != CurrentHand.rotation || lastPostion != CurrentHand.position)
{
lastPos = nextPos = CurrentHand.position;
int i = 0;
while (nextPos.y > 0 && (i < MaxPoint))
{
if (pointList.Count <= i)
{
pointList.Add(nextPos);
}
else
{
pointList[i] = nextPos;
}
if (lastRotation == CurrentHand.rotation && lastPostion != CurrentHand.position && i < pointList.Count - 1)
{
nextPos = pointList[i + 1] + CurrentHand.position - lastPostion;
}
else
{
nextPos = lastPos + CurrentHand.rotation * Vector3.forward * Distence + Vector3.up * Grity * 0.1f * i * Time.fixedDeltaTime;
}
lastPos = nextPos;
i++;
}
if (pointList.Count > i)
{
pointList.RemoveRange(i, pointList.Count - i);
}
lastRotation = CurrentHand.rotation;
lastPostion = CurrentHand.position;
if (pointList.Count > 1)
{
HitPoint = pointList[pointList.Count - 1];
PointEffect.SetActive(true);
PointEffect.transform.position = HitPoint;
}
else
{
HitPoint = -Vector3.one;
PointEffect.SetActive(false);
}
}
}
public void Enable()
{
SteamVR_InitManager.Instance.OnLeftDeviceActive += OnHandActive;
SteamVR_InitManager.Instance.OnRightDeviceActive += OnHandActive;
OnChangeTransform += OnChangeTransformCallBack;
}
public void OnHandActive(SteamVR_TrackedObject obj)
{
DeviceInput device = obj.GetComponent<DeviceInput>();
device.OnPressDownPadV3 += OnPressDownPad;
device.OnPressUpPad += OnPressUpPadAction;
}
public void OnHandDis(SteamVR_TrackedObject obj)
{
if (obj && obj.gameObject.activeSelf)
{
DeviceInput device = obj.GetComponent<DeviceInput>();
device.OnPressDownPadV3 -= OnPressDownPad;
device.OnPressUpPad -= OnPressUpPadAction;
}
}
public void Disable()
{
SteamVR_InitManager.Instance.OnLeftDeviceActive -= OnHandActive;
SteamVR_InitManager.Instance.OnRightDeviceActive -= OnHandActive;
OnHandDis(SteamVR_InitManager.Instance.LeftObject);
OnHandDis(SteamVR_InitManager.Instance.LeftObject);
OnChangeTransform -= OnChangeTransformCallBack;
}
public void SetData()
{
if (PointEffect)
PointEffect.SetActive(false);
}
/// <summary>
/// 抬起觸摸板時,計算落腳點
/// </summary>
private void OnPressUpPadAction()
{
if (CurrentHand == null) return;
canJump = true;
ray.origin = CurrentHand.position;
Vector3 dir = HitPoint - CurrentHand.position;
ray.direction = dir;
if (Physics.CheckSphere(HitPoint, CheckRange, ~(1 << 8)))
{
canJump = false;
}
if (canJump)
{
JumpPoint(HitPoint);
}
CurrentHand = null;
}
/// <summary>
/// 跳到指定的點
/// </summary>
/// <param name="point"></param>
public void JumpPoint(Vector3 point)
{
point.y = transform.position.y;
transform.position = point;
}
private void OnPressDownPad(Transform parent)
{
CurrentHand = parent;
}
/// <summary>
/// 使用GL來繪制曲線
/// 將點繪制出來
/// </summary>
void OnRenderObject()
{
material.SetPass(0);
if (pointList == null) return;
GL.Begin(GL.LINES);
for (int i = 0; i < pointList.Count; i++)
{
GL.Vertex(pointList[i]);
}
GL.End();
}
/// <summary>
/// 一個額外的附加方法,即用一個曲線來繪制拋物線,性能較低,因為點數比較多
/// 感興趣的可以把此方法添加到Update中更新
/// </summary>
public void ShowLineByRender()
{
LineRenderer line = GetComponent<LineRenderer>();
if (line)
{
line.SetVertexCount(pointList.Count);
for (int i = 0; i < pointList.Count; i++)
{
line.SetPosition(i, pointList[i]);
}
}
}
}
