VR穿墙处理的三种方式(视线遮挡和胶囊物理挤压和头部物理挤压)

3 篇文章 0 订阅

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下载
用于学习,商业用途请支持

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值