HTC vive设备结合unity开发手柄转动阀门功能
现在需求是:使用手柄握住一个阀门,进行旋转。
如下图:
所有的交互都是要在两个互动的物体之间做文章,VIVE里也是一样,所有要在手柄和阀门两个方面进行“加工”。
先看手柄需要做哪些“加工”
程序现在都在走“短小快”的路线。所以插件VRTK肯定是很好的选择。
在手柄上加上VRTK里的交互必要的脚本,这些脚本插件里都有,如下图(蓝色箭头标记为必须加的脚本)。
在本案例中我使用的是Grab的方式进行转动阀的,所以添加的是VRTK_Interact Grab的脚本。也可以根据需求自己修改。修改方法为在Events脚本里有各种触发方式的进行对应按键的选择。如下图:
有了这些脚本手柄的交互功能就已经具备了。只剩下被触碰的物体了。
接受触碰的物体需要进行的准备:
因为需要交互所以collider是必不可少的,还有rigidbody,记住不要勾选重力选项。因为这个要配合下面的VRTK_Knob脚本使用。Device_Value是我自己写的传值脚本,此处只讲转动方法不需要添加该脚本。如下图:
上图中的Clickpress脚本继承了VRTK_InteractableObject脚本,这个脚本也是VRTK插件里的。如果只是单纯实现本案例的转动功能完全可以使用VRTK_InteractableObject脚本。此处要注意转动的原理是采用unity里的铰链的方法,所以在该脚本里有一次选择抓取机制方法的地方要选择Spring_Joint的方法。同样既然是要抓取那肯定要勾选抓取的选项 ,如下图:
如果要添加其他功能,需要继承该脚本重写某些方法。下面的代码是最常用 的几个方法也是我的脚本Clickpress里用的方法:
VRTK_Knob脚本是一个用来转动跟随的脚本。
既然转动那可得要选择转动的物体和轴向,如图:
DIrection就是要转动的轴向,下面的两个参数是转动最大小的限度,step size是转动数值的精确度。
根据需求本案例选择Y轴,如图:
GO物体就是要被旋转的物体,使用时直接拖动过来就可以。这个GO物体原本脚本是没有的,我把原本的脚本稍稍做了加工。
代码如下:
namespace VRTK
{
using UnityEngine;
public class VRTK_Knob : VRTK_Control
{
public GameObject go;
public enum KnobDirection
{
x, y, z // TODO: autodetect not yet done, it's a bit more difficult to get it right
}
public KnobDirection direction = KnobDirection.x;
public float min = 0f;
public float max = 100f;
public float stepSize = 1f;
private static float MAX_AUTODETECT_KNOB_WIDTH = 3; // multiple of the knob width
private KnobDirection finalDirection;
private Quaternion initialRotation;
private Vector3 initialLocalRotation;
private Rigidbody rb;
private VRTK_InteractableObject io;
protected override void InitRequiredComponents()
{
initialRotation = transform.rotation;
initialLocalRotation = transform.localRotation.eulerAngles;
InitRigidBody();
InitInteractable();
SetContent(go,false);//cdl
}
protected override bool DetectSetup()
{
finalDirection = direction;
SetConstraints(finalDirection);
return true;
}
protected override ControlValueRange RegisterValueRange()
{
return new ControlValueRange() { controlMin = min, controlMax = max };
}
protected override void HandleUpdate()
{
value = CalculateValue();
}
private void InitRigidBody()
{
rb = GetComponent();
if (rb == null)
{
rb = gameObject.AddComponent();
}
rb.isKinematic = false;
rb.useGravity = false;
rb.angularDrag = 10; // otherwise knob will continue to move too far on its own
}
private void SetConstraints(KnobDirection direction)
{
if (!rb) return;
rb.constraints = RigidbodyConstraints.FreezeAll;
switch (direction)
{
case KnobDirection.x:
rb.constraints -= RigidbodyConstraints.FreezeRotationX;
break;
case KnobDirection.y:
rb.constraints -= RigidbodyConstraints.FreezeRotationY;
break;
case KnobDirection.z:
rb.constraints -= RigidbodyConstraints.FreezeRotationZ;
break;
}
}
private void InitInteractable()
{
io = GetComponent();
if (io == null)
{
io = gameObject.AddComponent();
}
io.isGrabbable = true;
io.precisionSnap = true;
io.grabAttachMechanic = VRTK_InteractableObject.GrabAttachType.Spring_Joint;
}
private KnobDirection DetectDirection()
{
KnobDirection direction = KnobDirection.x;
Bounds bounds = Utilities.GetBounds(transform);
// shoot rays in all directions to learn about surroundings
RaycastHit hitForward;
RaycastHit hitBack;
RaycastHit hitLeft;
RaycastHit hitRight;
RaycastHit hitUp;
RaycastHit hitDown;
Physics.Raycast(bounds.center, Vector3.forward, out hitForward, bounds.extents.z* MAX_AUTODETECT_KNOB_WIDTH, Physics.DefaultRaycastLayers, QueryTriggerInteraction.UseGlobal);
Physics.Raycast(bounds.center, Vector3.back, out hitBack, bounds.extents.z* MAX_AUTODETECT_KNOB_WIDTH, Physics.DefaultRaycastLayers, QueryTriggerInteraction.UseGlobal);
Physics.Raycast(bounds.center, Vector3.left, out hitLeft, bounds.extents.x* MAX_AUTODETECT_KNOB_WIDTH, Physics.DefaultRaycastLayers, QueryTriggerInteraction.UseGlobal);
Physics.Raycast(bounds.center, Vector3.right, out hitRight, bounds.extents.x* MAX_AUTODETECT_KNOB_WIDTH, Physics.DefaultRaycastLayers, QueryTriggerInteraction.UseGlobal);
Physics.Raycast(bounds.center, Vector3.up, out hitUp, bounds.extents.y* MAX_AUTODETECT_KNOB_WIDTH, Physics.DefaultRaycastLayers, QueryTriggerInteraction.UseGlobal);
Physics.Raycast(bounds.center, Vector3.down, out hitDown, bounds.extents.y* MAX_AUTODETECT_KNOB_WIDTH, Physics.DefaultRaycastLayers, QueryTriggerInteraction.UseGlobal);
// shortest valid ray wins
float lengthX = (hitRight.collider != null) ? hitRight.distance : float.MaxValue;
float lengthY = (hitDown.collider != null) ? hitDown.distance : float.MaxValue;
float lengthZ = (hitBack.collider != null) ? hitBack.distance : float.MaxValue;
float lengthNegX = (hitLeft.collider != null) ? hitLeft.distance : float.MaxValue;
float lengthNegY = (hitUp.collider != null) ? hitUp.distance : float.MaxValue;
float lengthNegZ = (hitForward.collider != null) ? hitForward.distance : float.MaxValue;
// TODO: not yet the right decision strategy, works only partially
if (Utilities.IsLowest(lengthX, new float[] { lengthY, lengthZ, lengthNegX, lengthNegY, lengthNegZ }))
{
direction = KnobDirection.z;
}
else if (Utilities.IsLowest(lengthY, new float[] { lengthX, lengthZ, lengthNegX, lengthNegY, lengthNegZ }))
{
direction = KnobDirection.y;
}
else if (Utilities.IsLowest(lengthZ, new float[] { lengthX, lengthY, lengthNegX, lengthNegY, lengthNegZ }))
{
direction = KnobDirection.x;
}
else if (Utilities.IsLowest(lengthNegX, new float[] { lengthX, lengthY, lengthZ, lengthNegY, lengthNegZ }))
{
direction = KnobDirection.z;
}
else if (Utilities.IsLowest(lengthNegY, new float[] { lengthX, lengthY, lengthZ, lengthNegX, lengthNegZ }))
{
direction = KnobDirection.y;
}
else if (Utilities.IsLowest(lengthNegZ, new float[] { lengthX, lengthY, lengthZ, lengthNegX, lengthNegY }))
{
direction = KnobDirection.x;
}
return direction;
}
private float CalculateValue()
{
float angle = 0;
switch (finalDirection)
{
case KnobDirection.x:
angle = transform.localRotation.eulerAngles.x - initialLocalRotation.x;
break;
case KnobDirection.y:
angle = transform.localRotation.eulerAngles.y - initialLocalRotation.y;
break;
case KnobDirection.z:
angle = transform.localRotation.eulerAngles.z - initialLocalRotation.z;
break;
}
angle = Mathf.Round(angle* 1000f) / 1000f; // not rounding will produce slight offsets in 4th digit that mess up initial value
// Quaternion.angle will calculate shortest route and only go to 180
float value = 0;
if (angle > 0 && angle <= 180)
{
value = 360 - Quaternion.Angle(initialRotation, transform.rotation);
}
else
{
value = Quaternion.Angle(initialRotation, transform.rotation);
}
// adjust to value scale
value = Mathf.Round((min + Mathf.Clamp01(value / 360f) * (max - min)) / stepSize) * stepSize;
if (min > max && angle != 0)
{
value = (max + min) - value;
}
return value;
}
}
}
这样手柄和被接触物体需要的东西都满足了就实现了该功能。