VR游戏穿墙处理方案,目前我用过的几种,推荐第一种和第三种。
这里都是代码片段和一些思路,仅供参考。
首先我们给场景拉入XR Origin,挂一个胶囊和刚体(或者用CharacterController),我自己喜欢自己写移动控制。
我们用方向控制来让XR Origin的胶囊移动,可以避免穿墙,但是现实世界中的移动就是Camera的移动,所以就可以穿墙。
首先我们看第一种穿墙方案,类似于半条命-阿莱克斯里的遮挡相机方式。当头部碰到物体眼前显示遮挡,防止眼睛看到。
第一种遮挡视线
首先在相机上挂一个节点,这个节点前面放入一个面片,能够完全遮挡住相机,放入一个脚本。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.XR;
using UnityEngine.XR;
using UnityEngine.XR.Management;
public class HeadCheckWall : MonoBehaviour
{
[SerializeField] float fadeSpeed = 5f;
[SerializeField] float sphereCheckSize = .15f;
[SerializeField] Transform vrOrgin;
[SerializeField] Renderer render;
Material fadeMat;
bool isCameraFadeOut;
int layer;
void Start()
{
fadeMat = render.material;
render.enabled = false;
layer = 1 << 0 | 1 << 1;
}
private void Update()
{
if (transform.position.y - vrOrgin.position.y < 0.1f)
return;
bool throwwall = false;
if (HeroMe.inst != null)
{
if (HeroMe.inst.firstChar != null)
{
throwwall = HeroMe.inst.firstChar.throwwall;
}
}
bool check = Physics.CheckSphere(transform.position, sphereCheckSize, layer, QueryTriggerInteraction.Ignore) || throwwall;
if (check)
{
CameraFade(1f);
isCameraFadeOut = true;
}
else
{
if (!isCameraFadeOut)
return;
CameraFade(0f);
}
}
void CameraFade(float a)
{
float fadevalue = Mathf.MoveTowards(fadeMat.GetFloat("_AlphaColor"),a,Time.deltaTime * fadeSpeed);
fadeMat.SetFloat("_AlphaColor", fadevalue);
if (fadevalue < 0.01f)
{
isCameraFadeOut = false;
render.enabled = false;
}
else
{
if (!render.enabled)
render.enabled = true;
}
}
private void OnDrawGizmos()
{
Gizmos.color = new Color(0f, 1f, 0f, 0.75f);
Gizmos.DrawSphere(transform.position, sphereCheckSize);
}
}
这里的Shader你可以自己搞一个纯色的带Alpha渐变的。这里用来检测如果头部撞墙了,就会显示出这个遮挡物面片。
这里会遗留一个问题:墙体是有厚度的,如果穿过了,那么也就失去效果了。弥补的办法可以从XROrgin的胶囊碰撞体发射射线到相机,如果碰到墙体,说明有墙体阻挡。
//穿墙检测
Vector3 capsultAt = xrOriginTrans.TransformPoint(capsule.center);
//Debug.DrawLine(capsultAt, capsultAt+Vector3.up*3f, Color.blue, 0.1f);
float dis = Vector3.Distance(vrCamera.transform.position, capsultAt);
rayThrowWall.origin = capsultAt;// vrCamera.transform.position;
rayThrowWall.direction = (vrCamera.transform.position - capsultAt).normalized;
//Debug.DrawLine(vrCamera.transform.position, capsultAt, Color.red, 0.1f);
throwwall = Physics.Raycast(rayThrowWall, out throwHit, dis, wallLayer,QueryTriggerInteraction.Ignore);
if (throwwall)
{
Debug.Log("穿墙了" + Time.time);
}
这个方法稍微有点缺陷,可能出现在人物在墙壁转角处,进行现实世界的转弯移动,可能会误报。
我们在玩家移动的时候进行一定的优化,让玩家通过手柄控制的时候瞬间让胶囊切换到相机所在位置,来进行一些误差修复。下面的刚开始移动代码就是修复。
void InputCameraMoveUpdate()
{
if (HeroMe.inst == null || HeroMe.inst.player == null)
return;
bool needrot = false;
stand = inputMove.magnitude < 0.01f ;
staticBody = rig.velocity.magnitude < 0.01f;
//穿墙检测
Vector3 capsultAt = xrOriginTrans.TransformPoint(capsule.center);
//Debug.DrawLine(capsultAt, capsultAt+Vector3.up*3f, Color.blue, 0.1f);
float dis = Vector3.Distance(vrCamera.transform.position, capsultAt);
rayThrowWall.origin = capsultAt;// vrCamera.transform.position;
rayThrowWall.direction = (vrCamera.transform.position - capsultAt).normalized;
//Debug.DrawLine(vrCamera.transform.position, capsultAt, Color.red, 0.1f);
throwwall = Physics.Raycast(rayThrowWall, out throwHit, dis, wallLayer,QueryTriggerInteraction.Ignore);
//if (throwwall)
//{
// Debug.Log("穿墙了" + Time.time);
//}
capsule.height = vrCamera.transform.position.y - xrOriginTrans.position.y;
if (capsule.height <= 0.01f)
capsule.height = 0.2f;
if (!throwwall)
{
if (!stand && laststand != stand)
{
//刚开始移动
originalCenter = xrOriginTrans.InverseTransformPoint(ToPos);// - xrOrigin.transform.position;
}
if (Mathf.Abs(inputRot.x) > 0f)
{
needrot = true;
originalCenter = xrOriginTrans.InverseTransformPoint(vrCamera.transform.position);// - xrOrigin.transform.position;
}
}
originalCenter.y = capsule.height * 0.5f;
capsule.center = originalCenter;
laststand = stand;
smoothInput = Vector3.SmoothDamp(smoothInput, inputMove, ref smoothInputV, 0.1f);
// Rotate input to avatar space
Vector3 forward = vrCamera.transform.forward;// ctrlMe.transform.forward;// transform.forward;
forward.y = 0f;
forward = forward.normalized;
Quaternion avatarSpace = Quaternion.LookRotation(forward);
if (!stand)
{
transform.rotation = Quaternion.Slerp(transform.rotation, avatarSpace, Time.deltaTime * 5f);
}
//Debug.Log(avatarSpace * smoothInput);
nowVelocity = avatarSpace * smoothInput * Time.deltaTime * vSpeed;
if (onGround)
{
rig.velocity = nowVelocity;
}
else
{
nowVelocity.y += rig.velocity.y;
rig.velocity = nowVelocity;
}
if (needrot)
{
xrOrigin.RotateAroundCameraUsingOriginUp(inputRot.x * 60f);
inputRot.x = 0f;
}
//刚体摩擦力是0的时候,刚体速度就是1秒移动距离
ToPos = vrCamera.transform.position - vrCamera.transform.forward * thirdOffset;
ToPos.y = xrOriginTrans.position.y;
ToRot = xrOriginTrans.localEulerAngles;// forward.normalized;
ThirdMove();
}
float thirdOffset = 0.1f;
public float movespd = 5f;
float moveOver = 0.2f; //超过这个距离才开始动
public bool standmoving;
Vector3 zeroV3;
void ThirdMove()
{
if (staticBody)
{
//float nowtime = Time.time;
float dis = Vector3.Distance(ToPos, transform.position);
if (dis > moveOver)
{
standmoving = true;
}
else
{
if (dis < 0.05f)
standmoving = false;
}
standToMove = (ToPos - transform.position).normalized;
if (standmoving)
{
transform.position = Vector3.Lerp(transform.position, ToPos, Time.deltaTime * movespd);
}
}
else
{
transform.position = ToPos;
}
}
视线遮挡大致就是这么多了。
第二种物理挤压方式
下面是第二种方案让相机无法穿过墙壁
这种方式比较简单,我们可以在FixeUpdate中控制胶囊或者CharacterController强制移动,这样就激活了系统的物理计算,如果碰到了东西会被挤出去。
//胶囊随时跟随相机
originalCenter = xrOriginTrans.InverseTransformPoint(vrCamera.transform.position);
originalCenter.y = capsule.height * 0.5f;
capsule.center = originalCenter;
//重点在这里
rig.MovePosition(rig.position);
//或者CharacterController调用一次移动,触发胶囊挤压出来
//CharacterController.Move(Vector3.Zero);
//不行可以试试下面的
//CharacterController.Move(new Vector3(0.001f, -0.001f, 0.001f))
//CharacterController.Move(new Vector3(-0.001f, 0.001f, -0.001f))
这个方式有个缺点,在玩家弯腰拾取桌子上的东西的时候,因为胶囊跟着头,所以会人会被往后推,导致不好拾取物品。
下面我们来说另外一种,类似恐鬼症的方式。
第三种头部物理挤压方式
他是让头部不能穿过,胶囊可以,有个弊端就是可以穿过桌子行走,因为头没碰到桌子,就会看到这个人穿过了桌子。但是通过方向遥感是无法穿过的。
具体实现方式:
首先在相机上增加刚体和胶囊
vr的相机上是有这个TrackedPoseDriver这个脚本的,我们新建一个脚本来替换他,改写他一些函数。
using System.Collections;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using UnityEngine;
using UnityEngine.InputSystem.XR;
public class HeadRigMove : TrackedPoseDriver
{
Vector3 cameraLocalPos;
Vector3 cameraWorldPos;
Rigidbody rig;
float colliderRadius = 1f;
Vector3 dir;
const float wallThickness = 0.01f; //设置让刚体强制移动这么多。可能顶墙,这个距离不要超过相机碰撞的半径
int wallLayer;
float lastUpdateTime;
protected override void Awake()
{
base.Awake();
rig = transform.gameObject.GetComponent<Rigidbody>();
SphereCollider collider = transform.gameObject.GetComponent<SphereCollider>();
colliderRadius = collider.radius;
wallLayer = 1 << 0 | 1 << 1 | 1 << LayerMask.NameToLayer("item");
}
protected override void SetLocalTransform(Vector3 newPosition, Quaternion newRotation)
{
if (trackingType == TrackingType.RotationAndPosition ||
trackingType == TrackingType.RotationOnly)
{
transform.localRotation = newRotation;
}
if (trackingType == TrackingType.RotationAndPosition ||
trackingType == TrackingType.PositionOnly)
{
cameraLocalPos = newPosition;
}
}
Ray rayThrowWall;
RaycastHit throwHit;
private void FixedUpdate()
{
/*
cameraWorldPos = transform.parent.TransformPoint(cameraLocalPos);
bool throwwall = false;
if (cameraLocalPos.y > 0.01f) //如果vr设备没连接,这里都是0f
{
float dis = Vector3.Distance(cameraWorldPos, rig.position) + colliderRadius;
//检测头部到目标位置是否有墙体,如果有只能移动一部分
rayThrowWall.origin = rig.position;// vrCamera.transform.position;
rayThrowWall.direction = (cameraWorldPos - rig.position).normalized;
//Debug.DrawLine(vrCamera.transform.position, capsultAt, Color.red, 0.1f);
throwwall = Physics.Raycast(rayThrowWall, out throwHit, dis, wallLayer, QueryTriggerInteraction.Ignore);
}
if (!throwwall)
{
rig.MovePosition(cameraWorldPos);
}
else
{
rig.MovePosition(rig.position + rayThrowWall.direction * wallThickness);
}
*/
//上面的屏蔽掉了,之前写MovePosition可能有点问题
if (cameraLocalPos.y < 0.2f)
cameraLocalPos.y = 0.2f;
cameraWorldPos = transform.parent.TransformPoint(cameraLocalPos);
bool throwwall = false;
float dis = Vector3.Distance(cameraWorldPos, rig.position) + colliderRadius;
if (dis > 0.001f)
{
//检测头部到目标位置是否有墙体,如果有只能移动一部分
rayThrowWall.origin = rig.position;// vrCamera.transform.position;
rayThrowWall.direction = (cameraWorldPos - rig.position).normalized;
throwwall = Physics.Raycast(rayThrowWall, out throwHit, dis, wallLayer, QueryTriggerInteraction.Ignore);
}
if (!throwwall)
{
transform.position = cameraWorldPos;
rig.position = cameraWorldPos;//.MovePosition(cameraWorldPos);
if (!rig.isKinematic)
{
//如果没碰东西,速度要渐渐变为0
var deltaTime = (Time.realtimeSinceStartup - lastUpdateTime);
rig.velocity = Vector3.MoveTowards(rig.velocity, Vector3.zero, rig.velocity.magnitude * deltaTime);
rig.angularVelocity = Vector3.zero;
}
}
else
{
//rig.position = cameraWorldPos;
if (!rig.isKinematic && !nowSetPoint)
{
//如果碰了东西就计算一个速度
var vel = (cameraWorldPos - rig.position).normalized * 50f * dis;
rig.velocity = vel;
}
else
{
nowSetPoint = false;
cameraWorldPos = rig.position + rayThrowWall.direction * wallThickness;
rig.position = cameraWorldPos;
}
}
lastUpdateTime = Time.realtimeSinceStartup;
}
}
}
}
这样在头显获取到坐标数据后计算实际的坐标,让刚体移动过去,增加一个墙体厚度,不允许超过。这样就实现了。
上面代码仅供参考,可能并不适合,只是一个思路,特别是整套的VR互动,可能有各种问题需要调整。
整套好的解决方案可以看下AutoHand。个人觉得是最完善的,比较好理解的。
AutoHand3.2.1下载
用于学习,商业用途请支持