美文网首页
VR 射击馆(一)箭的轨迹、传送

VR 射击馆(一)箭的轨迹、传送

作者: 烂醉花间dlitf | 来源:发表于2020-11-06 18:30 被阅读0次

射击相关

抛物线

箭的轨迹是一个抛物线。我们如果都基于现实来计算的话,那么暴露出来的配置只有:

  • 物体的质量
  • 重力加速度,地球上一般是 9.8 啦
  • 发射的力度
  • 发射的方向,这里默认是向着物体的前方,毕竟是射箭游戏嘛。
  • 空气阻力的系数,因为空气阻力 = 0.5空气阻力系数空气密度迎风面积速度的平方,前几个量有些是实验结果,我们就把除了速度的因素都合为一个系数。这个系数其实算下来很小很小,箭的话基本就算 0.0005 吧,迎风面积大的物体可以稍微改大一下。
    先把这些设置为 public ,然后按照真实的物理公式来计算就好了。

代码(第一版)

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ParabolaTest1 : MonoBehaviour
{
    // 能够计算位移的全部条件
    private struct ParabalaInfo
    {
        public Vector3 velocity;
        public Vector3 acceleration;
        public Vector3 position;
        public float time;

        public Vector3 GetNewPostion()
        {
            return position + velocity * time + 0.5f * acceleration * Mathf.Pow(time, 2);
        }

        public Vector3 GetNewVelocity()
        {
            return velocity + acceleration * time;
        }
    };

    public float force; // 开始施加的力
    public float mass; // 重量,不是重力,如果想要让物体下落的更快应该是改变重力加速度,物体下落速度与重量无关
    public float gravityUnit = 9.8f; // 重力加速度,地球就算是 9.8 吧
    public float airK = 1;

    private float airResistance; // 空气阻力 = 0.5*空气阻力系数*空气密度*迎风面积*速度的平方,这里将前面几个量合并为一个系数 k
    private ParabalaInfo LastInfo;
    private bool isMoveing = false;
    private Transform head;
    private Vector3 previousHead;

    private void Start()
    {
        head = transform.Find("Head");
    }

    private void FixedUpdate ()
    {
        if (Input.GetKeyDown(KeyCode.Space) && !isMoveing)
        {
            isMoveing = true;
            LastInfo.velocity = transform.forward * force;
            LastInfo.acceleration = new Vector3(0, -gravityUnit, 0);
            LastInfo.position = transform.position;
            previousHead = head.position;
        }

        if (isMoveing)
        {
            LastInfo.time = Time.fixedDeltaTime;
            transform.position = LastInfo.GetNewPostion();
            Quaternion rota = Quaternion.FromToRotation(transform.forward, transform.position - LastInfo.position);
            transform.rotation = rota * transform.rotation;

            // 检测是否射到靶子
            Ray ray = new Ray(previousHead, head.position - previousHead);
            RaycastHit[] hits = Physics.RaycastAll(ray, Vector3.Distance(previousHead, head.position));
            if (hits != null)
            {
                foreach (RaycastHit hit in hits)
                {
                    if (hit.transform != transform)
                    {
                        isMoveing = false;
                        break;
                    }
                }
            }
            Debug.DrawLine(ray.origin, ray.origin + ray.direction * Vector3.Distance(previousHead, head.position), Color.red);
            LastInfo.position = transform.position;   
            LastInfo.velocity = LastInfo.GetNewVelocity(); // 这里要小心调用顺序,更新速度要在更新加速度之前,因为计算中要用到
            airResistance = airK * Mathf.Pow(LastInfo.velocity.magnitude,2);  // 空气阻力等于系数乘速度的平方
            LastInfo.acceleration = new Vector3(0, -gravityUnit, 0) - transform.forward * airResistance;
            previousHead = head.position;
        }
    }
}

这里是第一版,也就是轨迹看起来是对的,但是还是有很多问题存在。
这里检测是否射中直接从箭头发一个射线,然后判断 hit 的距离其实是不合适的,因为比如判断距离是否小于 0.1f,那么在箭的速度足够大的时候,这一帧的距离还是 0.5f,下一帧箭就过去了,raycast 就返回 false 了。那把 0.1f 改大呢?比如改成 0.5f,但这样又会出现箭头没到靶子就停住的效果。所以想到一种办法。就是记录上一帧箭头的位置,然后发射一个 这一帧箭头的位置到上一帧箭头的位置的射线 ,判断是否有碰撞,这样的话,整个轨迹都是连续的,所以一定不会出现箭头穿过一个碰撞体,但是两帧都没有检测出现导致箭直接穿过碰撞的情况出现。
但是!问题出现了!因为 raycast 的射线是有方向的,并且是不能检测出从一个物体内部到外部的碰撞的,只能检测出从一个物体外部到内部的碰撞,刚刚我说的是从 这一帧箭头的位置到上一帧箭头的位置的射线(图A),但其实应该是 上一帧箭头的位置到这一帧箭头的位置的射线(图B),如下图所示

A
B

还要考虑速度过大的时候,射线会穿过自己的情况,所以需要用 RayCastAll 来排除掉自己。

射中效果

A参数 A击中效果 B参数 B击中效果

重力加速度测试

预想:g 越大,下落的越快


A重力加速度 A重力加速度 B重力加速度 B重力加速度

空气阻力系数测试

预想:系数越大,横向速度减的越快

A空气阻力系数
A空气阻力系数 B空气阻力系数
B空气阻力系数

射击目标检测

抛物线的样子基本完成之后,就要真正的让用到项目中了,但是过程中发现了一些问题

  • 会出现穿过物体之后悬在空中的情况,因为速度过大,所以最后一个射线检测到确实穿过了物体,但是箭的长度远小于最后一个射线的长度,所以就会悬在半空中,解决方法是判断物体是否穿过了一个物体,如果穿过了第一个物体,那么就强行把它拉回没穿之前的状态,也就是露出箭羽,然后面朝 hit 的发现方向。
  • 这样的话,需要用到碰撞盒来检测箭是否和另一个物体正在发生碰撞,但碰撞的几个函数都是在 fixedUpdate 之后执行,所以需要将关于碰撞的逻辑判断放在 laterUpdate 中。
  • 现在箭已经一定不会穿过所有的物体了,但是很尴尬的是,这样会在射出去的一刹那有概率碰到手的模型,而手需要碰撞体来检测是否需要抓住东西,就导致必须要加碰撞,那我们又需要将不希望被箭射到的物体进行标示,能想到想法就是放在不同的 layer 或者 不同的 Tag,但这样都会占据物体本身的 layertag,所以暂且使用的方法是加一个空脚本,如果有这个空脚本就说明可以被穿过,如果没有就说明不可以。

代码(第二版)

其实写的有点乱,我是希望把所有 关于抛物线 的内容都写在父类的,但最后把靶子的检测也都混在一起了...

using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.Design;
using System;
using UnityEngine;
using UnityEngine.Events;

public class ScoreEvent : UnityEvent<int,GameObject> { }

public class ParabolaObjectBase : MonoBehaviour
{
    // 能够计算位移的全部条件
    private struct ParabalaInfo
    {
        public Vector3 velocity;
        public Vector3 acceleration;
        public Vector3 position;
        public float time;

        public Vector3 GetNewPostion()
        {
            return position + velocity * time + 0.5f * acceleration * Mathf.Pow(time, 2);
        }

        public Vector3 GetNewVelocity()
        {
            return velocity + acceleration * time;
        }
    };

    public float force = 300; // 开始施加的力
    public float mass = 1; // 重量,不是重力,如果想要让物体下落的更快应该是改变重力加速度,物体下落速度与重量无关
    public float gravityUnit = 9.8f; // 重力加速度,地球就算是 9.8 吧
    public float airK = 0.001f;
    protected Vector3 forwardDir;
    public ScoreEvent scoreEvent = new ScoreEvent();
    public GameObject HitEffectprefab;
    

    private float airResistance; // 空气阻力 = 0.5*空气阻力系数*空气密度*迎风面积*速度的平方,这里将前面几个量合并为一个系数 k
    private ParabalaInfo LastInfo;
    protected bool isMoveing = false;
    private Transform head;
    private Transform submergePosition;
    private Vector3 previousHead;
    private List<Transform> collisionTranforms = new List<Transform>();

    protected virtual void Start()
    {
        head = transform.Find("Head");
        submergePosition = transform.Find("FeatherHead");
        if (forwardDir == null)
        {
            forwardDir = transform.forward;
        }
    }

    
    protected void StartParabolaMove(float force)
    {

        forwardDir = (head.position - transform.position).normalized;
        this.force = force;
        isMoveing = true;
        collisionTranforms.Clear();
        LastInfo.velocity = forwardDir* force;
        LastInfo.acceleration = new Vector3(0, -gravityUnit, 0);
        LastInfo.position = transform.position;
        previousHead = head.position;
    }

    protected virtual void FixedUpdate()
    {
        forwardDir = (head.position - transform.position).normalized;
        
        if (isMoveing)
        {
            LastInfo.time = Time.fixedDeltaTime;
            transform.position = LastInfo.GetNewPostion();

            Quaternion rota = Quaternion.FromToRotation(forwardDir, transform.position - LastInfo.position);
            transform.rotation = rota * transform.rotation;

            LastInfo.position = transform.position;
            LastInfo.velocity = LastInfo.GetNewVelocity(); // 这里要小心调用顺序,更新速度要在更新加速度之前,因为计算中要用到
            airResistance = airK * Mathf.Pow(LastInfo.velocity.magnitude, 2);  // 空气阻力等于系数乘速度的平方
            LastInfo.acceleration = new Vector3(0, -gravityUnit, 0) - forwardDir * airResistance;
        }


    }

    protected virtual void LateUpdate()
    {
        if (isMoveing)
        {
            // 检测是否射到靶子
            Ray ray = new Ray(previousHead, head.position - previousHead);
            RaycastHit[] hits = Physics.RaycastAll(ray, Vector3.Distance(previousHead, head.position)); // 返回值是无须的,需要要自己排序
            Array.Sort<RaycastHit>(hits, HitComparison); // 将结果按照远近排序
            if (hits != null && (hits.Length > 1 || (hits.Length==1 && hits[0].transform!=transform)))
            {
                bool is_shoot_target = false;
                bool is_pass_throught = true;
                bool is_detected_first_collision = false;
                bool is_all_cant_hit = true;
                foreach (RaycastHit hit in hits)
                {
                    if (hit.transform.GetComponent<CantHit>() != null) // TODO:如何不使用 layer,Tag,在类中引用,名字检索就能判断这不是可以穿透的物体
                    {
                        continue;
                    }

                    is_all_cant_hit = false;
                    if (hit.transform != transform)
                    {
                        if (hit.transform.CompareTag("Target") && hit.transform.GetComponent<ShootingTarget>()!=null)
                        {
                            int score = hit.transform.GetComponent<ShootingTarget>().GetScore(hit.point);
                            InstantiateHitPoint(in hit);
                            Debug.Log("分数:" + score, hit.transform);
                            scoreEvent?.Invoke(score,hit.transform.gameObject); // 播报声音等行为
                            is_shoot_target = true;
                        }


                        // 只需要检测第一个,也就是最近的那一个
                        if (!is_detected_first_collision) // 没有穿过去
                        {
                            is_detected_first_collision = true;

                            if (collisionTranforms.Contains(hit.transform))
                            {
                                is_pass_throught = false;
                                DebugText.Instance().SetInfo("Not Through Object", hit.transform.name);
                            }
                        }
                        isMoveing = false;
                        
                        // break;
                    }
                }
                if (!is_shoot_target && !is_all_cant_hit)
                {
                    Debug.Log("分数1:" + 0);
                    scoreEvent?.Invoke(0, null);
                }


                // 如果穿过去了,就返回到露出羽毛的位置
                if (is_pass_throught)
                {
                    // Debug
                    Debug.Log("穿过去了");
                    RaycastHit h = hits[0].transform == transform ? hits[1] : hits[0]; // 因为前面的判断很充分,这里不用担心越界或者空
                    DebugText.Instance().SetInfo("Through Object", h.transform.name);
                    
                    Vector3 hit_point = hits[0].transform == transform ? hits[1].point : hits[0].point;
                    Vector3 offset = submergePosition.position - transform.position;
                    transform.position = hit_point - offset;
                }
            }
            previousHead = head.position;
        }
    }

    //因为 OnCollisionXXX 都调用在 update 之后,所以
    private void OnTriggerEnter(Collider other)
    {
        collisionTranforms.Add(other.transform);
    }

    //private void OnTriggerStay(Collider other)
    //{
    //    collisionTranforms.Add(other.transform);
    //}

    private void OnTriggerExit(Collider other)
    {
        collisionTranforms.Remove(other.transform);
    }


    // 调用的是子物体内的函数体,但这样写感觉好丑陋,应该写成抽象函数的
    protected virtual void InstantiateHitPoint(in RaycastHit hit)
    {
        //Gizmos.DrawWireSphere(hit.point,0.1f);
        //GameObject obb = GameObject.CreatePrimitive(PrimitiveType.Sphere);
        //obb.transform.position = hit.point;
        //obb.transform.localScale = Vector3.one * 0.5f;

    }

    // 测试
    [ContextMenu("Load Hiteffect")]
    protected virtual void InstantiateHitPoint()
    {
        GameObject ob = Resources.Load<GameObject>("Prefabs/HitEffect") as GameObject;
        ob.transform.position = Vector3.zero;
        //ob.transform.rotation = Quaternion.FromToRotation(-ob.transform.forward, hit.normal);
    }

    private int HitComparison(RaycastHit a,RaycastHit b)
    {
        if (a.distance <= b.distance)
        {
            return -1;
        }
        return 1;
    }
}

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UIElements;

public class Arrow : ParabolaObjectBase
{
    public UnityEvent FiredEvent;
    private Vector3 originPosition;
    private Quaternion originRotation;
    private Rigidbody rigi;
    private Camera cam;
    private bool camView = false;
    private TrailRenderer tr;

    public HitPoint hitPoint;
    protected override void Start()
    {
        base.Start();
        rigi = GetComponent<Rigidbody>();
        rigi.isKinematic = true;
        rigi.detectCollisions = true;
        originPosition = transform.position;
        originRotation = transform.rotation;
        tr = GetComponent<TrailRenderer>();
        tr.enabled = false;
    }

    public void ResetTransform()
    {
        gameObject.SetActive(true);
        transform.position = originPosition;
        transform.rotation = originRotation;
    }

    public void Fire(float force)
    {
        transform.parent = null;
        base.StartParabolaMove(force);
        tr.enabled = true;
        FiredEvent?.Invoke();
    }

    private void OnReplacedController(int controller,GameObject ob)
    {
        //cam.enabled = true;
    }

    private void OnReplaceControllerEnd(int controller,GameObject o)
    {
        if (controller == 1)
        {
            //rigi.isKinematic = false;
            //rigi.useGravity = true;
            //rigi.detectCollisions = true;
        }
    }

    protected override void LateUpdate()
    {
        base.LateUpdate();
        //cam.enabled = isMoveing;
    }

    private void Update()
    {
        if(Mathf.Abs(transform.position.y)> 1000)
        {
            Destroy(gameObject);
        }

        if (Input.GetKey(KeyCode.H))
        {
            Debug.DrawLine(transform.position, transform.position + forwardDir * 100, Color.black, 1000);
        }
    }

    public Vector3 GetForwardDirection()
    {
        return transform.up.normalized;
    }

    protected override void InstantiateHitPoint(in RaycastHit hit)
    {
        base.InstantiateHitPoint(hit);
        GameObject ob = Instantiate(HitEffectprefab,GameManager.Instance().hitPoints.transform) as GameObject;
        hitPoint = ob.GetComponent<HitPoint>();
        hitPoint.SetHighlight();
        ob.transform.position = hit.point + hit.normal * 0.03f;
        ob.transform.rotation = Quaternion.FromToRotation(-ob.transform.forward, hit.normal);
    }
}

最终的效果就是箭不会穿过任何没有加 CantHit 组件的物体,然后射中靶子的话会报得了几分(根据距离判断的),其他情况都会报脱靶,然后射中靶子会在屏幕中出现一个明显的击中效果,这个效果不会消失,所以是打算做一个菜单可以选择清楚痕迹的,同时把场景内的箭也给清除,但现在这个项目终止了,所以菜单来不及做,就导致每次射出去的箭都不会消失,这样的话其实用对象池做会比较好点。

传送相关

效果

teleport

介绍

基本思路就是用一个 LineRenderer 固定画多少个点,然后从箭的前方为第一个点,箭羽到箭头的方向上的延长线上找第二个点,目标传送点为第三个点,三个点画一条贝塞尔曲线,然后打开目标传送点的粒子传送圈圈。
这里目标传送点的选择是根据箭头到目标传送点的向量和箭头前方的向量的夹角来确定的,用向量点乘来计算。
还需要区分传送的逻辑和射箭的逻辑,不然会在传送的时候听到一声 “脱靶”,还有一只箭跟着你飞出去,力度够大的话说不定会戳到屁股(我瞎说的)

代码

using Pvr_UnitySDKAPI;
using System.Collections;
using UnityEngine;
using System.Collections.Generic;
using DG.Tweening;

public class Teleport : MonoBehaviour
{
    public bool IsScreenFade = false;
    public Material LineMat;
    public float teleportDistance = 100;
    public TeleportTarget standingTarget;
    private GameObject cube;
    private Material fademat;

    private LineRenderer line;
    private List<TeleportTarget> targets = new List<TeleportTarget>();
    private bool selecting = false;
    private TeleportTarget selecttedTarget;
    private TeleportTarget lastSelectTarget;

    public static Vector3[] GetBeizerPathPointList(Vector3 startPoint, Vector3 controlPoint, Vector3 endPoint, int pointNum)
    {
        Vector3[] BeizerPathPointList = new Vector3[pointNum];
        for (int i = 1; i <= pointNum; i++)
        {
            float t = i / (float)pointNum;
            Vector3 point = GetBeizerPathPoint(t, startPoint,
                controlPoint, endPoint);
            BeizerPathPointList[i - 1] = point;
        }
        return BeizerPathPointList;
    }

    private static Vector3 GetBeizerPathPoint(float t, Vector3 p0, Vector3 p1, Vector3 p2)
    {
        return (1 - t) * (1 - t) * p0 + 2 * t * (1 - t) * p1 + t * t * p2;
    }

    private TeleportTarget SelectTarget()
    {
        float max_dot = -1;
        TeleportTarget ret = null;
        foreach(TeleportTarget t in targets)
        {
            float dot = Vector3.Dot((t.GetLineEndPosition() - GameManager.Instance().archery.arrow.transform.Find("Head").position).normalized, GameManager.Instance().archery.arrow.GetForwardDirection());
            if (dot > max_dot)
            {
                max_dot = dot;
                ret = t;
            }
        }
        selecttedTarget = ret;
        return ret;
    }

    private void DrawLine()
    {
        TeleportTarget tt = SelectTarget();
        Vector3 startPoint = GameManager.Instance().archery.arrow.transform.Find("Head").position;
        Vector3 endPoint = tt.GetLineEndPosition();
        Vector3 controlPoint = startPoint + GameManager.Instance().archery.arrow.GetForwardDirection()*(endPoint - startPoint).magnitude / 2;

        Vector3[] bcList = GetBeizerPathPointList(startPoint, controlPoint, endPoint, 50);
        line.positionCount = bcList.Length + 1;
        line.SetPosition(0, startPoint);
        for (int i = 0; i < bcList.Length; i++)
        {
            Vector3 v = bcList[i];
            line.SetPosition(i + 1, v);
        }

        tt.ShowEffect();
        if (lastSelectTarget != null && lastSelectTarget != tt)
        {
            lastSelectTarget.HideEffect();
        }
        lastSelectTarget = tt;
    }


    private void LineInit()
    {
        if (GetComponent<LineRenderer>())
            line = GetComponent<LineRenderer>();
        else
            line = gameObject.AddComponent<LineRenderer>();

        line.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;


        line.material = LineMat;
        line.startWidth = 0.04f;
        line.numCapVertices = 5;
    }

    private void MoveCameraPrefab(TeleportTarget t)
    {
        if (line.enabled)
        {
            line.enabled = false;
        }
        //TODO
        //DOTweenPath tweenpath = new DOTweenPath();
        if (t == null)
        {
            Debug.LogError("传送目的地为空");
            return;
        }
        GameManager.Instance().TeleportComplete(in t);
        Pvr_UnitySDKManager.SDK.transform.position = t.GetSDKPostion();
        Pvr_UnitySDKManager.SDK.transform.rotation = Quaternion.Euler(t.GetFaceTo());
    }

    private void Start()
    {
        LineInit();

        MoveCameraPrefab(standingTarget);

        // cube 没用了,没做屏幕渐变
        fademat = new Material(Shader.Find("Sprites/Default"));
        cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
        cube.GetComponent<MeshRenderer>().material = fademat;
        cube.transform.position = Pvr_UnitySDKManager.SDK.transform.position;
        cube.transform.parent = Pvr_UnitySDKManager.SDK.transform;
        cube.SetActive(false);
        GameManager.Instance().archery.TeleportFireEvent.AddListener(TeleportFireAction);
    }


    private void Update()
    {
        if (GameManager.Instance().archery.isAttach && (Controller.UPvr_GetKey(1, Pvr_KeyCode.Right) || Input.GetMouseButton(0)))
        {
            targets.Clear();
            foreach (TeleportTarget t in FindObjectsOfType<TeleportTarget>())
            {
                if ((t.transform.position - MainControlHandMode.Instance().head.position).magnitude <= teleportDistance) // 在一定范围内的传送点可以被候选
                {
                    targets.Add(t);
                }
            }

            line.enabled = true;
            DrawLine();
            selecting = true;
            GameManager.Instance().archery.fireType = ArcheryControl.FireType.Teleport;
        }

        if(selecting &&(Controller.UPvr_GetKeyUp(1, Pvr_KeyCode.Right) || Input.GetMouseButtonUp(0)))
        {
            lastSelectTarget = null;
            line.enabled = false;
            selecting = false;
            selecttedTarget.HideEffect();
            GameManager.Instance().archery.fireType = ArcheryControl.FireType.Normal;
        }
    }

    private void TeleportFireAction()
    {
        GameManager.Instance().soundMgr.PlayTeleport();
        lastSelectTarget = null;
        MoveCameraPrefab(selecttedTarget);
        selecttedTarget.HideEffect();
    }

    private void OnDrawGizmos()
    {
        Gizmos.DrawWireSphere(MainControlHandMode.Instance().head.position, teleportDistance);
    }

    [ContextMenu("move1")]
    public void Move1()
    {
        targets.Clear();
        foreach (TeleportTarget t in FindObjectsOfType<TeleportTarget>())
        {
            if ((t.transform.position - MainControlHandMode.Instance().head.position).magnitude <= teleportDistance) // 在一定范围内的传送点可以被候选
            {
                targets.Add(t);
            }
        }
        MoveCameraPrefab(targets[0]);
    }

    [ContextMenu("move2")]
    public void Move2()
    {
        targets.Clear();
        foreach (TeleportTarget t in FindObjectsOfType<TeleportTarget>())
        {
            if ((t.transform.position - MainControlHandMode.Instance().head.position).magnitude <= teleportDistance) // 在一定范围内的传送点可以被候选
            {
                targets.Add(t);
            }
        }
        MoveCameraPrefab(targets[1]);
    }

    [ContextMenu("move3")]
    public void Move3()
    {
        targets.Clear();
        foreach (TeleportTarget t in FindObjectsOfType<TeleportTarget>())
        {
            if ((t.transform.position - MainControlHandMode.Instance().head.position).magnitude <= teleportDistance) // 在一定范围内的传送点可以被候选
            {
                targets.Add(t);
            }
        }
        MoveCameraPrefab(targets[2]);
    }

    [ContextMenu("move4")]
    public void Move4()
    {
        targets.Clear();
        foreach (TeleportTarget t in FindObjectsOfType<TeleportTarget>())
        {
            if ((t.transform.position - MainControlHandMode.Instance().head.position).magnitude <= teleportDistance) // 在一定范围内的传送点可以被候选
            {
                targets.Add(t);
            }
        }
        MoveCameraPrefab(targets[3]);
    }
}  

相关文章

网友评论

      本文标题:VR 射击馆(一)箭的轨迹、传送

      本文链接:https://www.haomeiwen.com/subject/aratbktx.html