1/3
2/3
3/3
三维斜抛运动定点打击解算和unity实现
xxxNeedFor虚空藏xxx
编辑于 2023年05月19日 04:43

思路概述

    假设起点为source,目标点为target,source以theta角度朝上抛出,求初速度M使得能够正好落在target。

    可以将问题分为2个部分

    第1部分,求得“仰角”为theta的抛出方向ThrowDir。

    第2部分,解算求得初速度M。

    可以将运动划分为2个部分。

    第1部分,从出发点开始到最高点结束。

    第2部分,从最高点开始到target结束。

抛射方向

我用chatGPT问的,然后改了一下bug。dir为target-source方向,angle为“仰角”

代码块
C#
自动换行
复制代码
public static Vector3 EnsureUpAngle(Vector3 dir, float angle)
        {
            dir = dir.normalized;
            if(dir.magnitude == 0)
            {
                Debug.LogError("zero dir");
                return Vector3.forward;
            }
            // 将 dir 向量投影到水平平面上(即 y 轴为零)
            Vector3 dirFlat = Vector3.ProjectOnPlane(dir, Vector3.up).normalized;
            // 计算从 dirFlat 指向 dir 的旋转值
            Quaternion rotation = Quaternion.FromToRotation(Vector3.forward, dirFlat);
            // 将旋转值绕着世界空间的 x 轴旋转目标角度
            rotation = rotation * Quaternion.AngleAxis(-angle, Vector3.right) ;
            // 应用旋转值到 dir 向量上
            dir = rotation * Vector3.forward;
            return dir.normalized;
        }
复制成功

列关系式

设throwDir的xz平面分量大小为k1,垂直大小为k2。

设source和target的水平距离d1,垂直距离d2。

设h为第一阶段垂直距离。

设第1阶段用时t1,第2阶段用时t2。

    关系1:t1*g=M*k2。因为只受重力,垂直速度均匀降为0

    关系2:h=M*k2*t-1/2*g*t^2。初中“公式”,其实是速度Mk2-gt积分来的。

    关系3:d2+h=1/2*g*t^2。同理初中“公式”,因为第二阶段是从最高点0速度开始的。

    关系4:M*k1*(t1+t2) = d1。 水平方向是匀速运动。

公式见下图,

由于貌似不能化简出简单方程,所以用二分法(我图省事)解算方程。

以上图波浪线的关系,也就是水平位移关系式做解方程f(x)=0。

即为f(M) = M*k1*(t1+t2) - d1 = 0,求零点。

f比较复杂:

代码块
C#
自动换行
复制代码
Func<float, float> fx = x => k1*(x*k2/gravity+sqrt((2*d2+x*x*k2*k2/gravity)/gravity))*x-d1;
复制成功

整个变量初始化和解算:

代码块
C#
自动换行
复制代码
float CalculateThrowSpeedMagnitude(Vector3 throwDir)
    {
        if(throwDir.magnitude==0)
        {
            Debug.LogError("throwDir is 0");
            return 0.0f;
        }
        throwDir = throwDir.normalized;
        Vector3 dis = endPos - startPos;
        float d1 = new Vector2(dis.x, dis.z).magnitude;
        float d2 = abs(dis.y);
        float k1 = new Vector2(throwDir.x, throwDir.z).magnitude;
        float k2 = abs(throwDir.y);

        Func<float, float> fx = x => k1*(x*k2/gravity+sqrt((2*d2+x*x*k2*k2/gravity)/gravity))*x-d1;
        return SolveFunc_Range2(fx, 0.0f, 100.0f, 0.0001f);
    }
复制成功

二分法解方程代码(设定了最多100000步,其实10来步error就<0.0001了):

代码块
C#
自动换行
复制代码
float SolveFunc_Range2(Func<float,float> f,float x1,float x2, float accept)
    {
        float y1 = f(x1);
        float y2 = f(x2);
        float newx = 0.5f * (x1 + x2);
        float error = abs(y1);
        int count = 0;
        if(y1*y2>0)
        {
            Debug.LogError("invalid range to solve");
            return 0;
        }
        while(error > accept)
        {
            count++;
            if(count>=100000)
            {
                Debug.LogError(count+" cannot solve!!!");
                return 0;
            }         
            float newy = f(newx);
            if(newy*y1>0)
            {
                x1 = newx;
            }
            else
            {
                x2 = newx;
            }
            newx = (x1 + x2) * 0.5f;
            error = abs(newy);
            //Debug.Log(error);
        }
        Debug.Log("solved use rounds:" + count);
        return newx;
    }
复制成功

迭代和终点问题

1.必须使用FixedUpdate

2.必须先speed+=a*dt,其次pos+=speed*dt

3.由于到终点速度过快,无法完全对准:

由于我的需求是完全对准,所以就硬set过去了,这点误差对我的项目来说可以接受:

代码块
C#
自动换行
复制代码
if (source.transform.position.y <= endY)
            {
                if((source.transform.position-endPos).magnitude>0.5f)
                {
                    Debug.Log("Warning: XCThrow error too large!!! >0.5f");
                }
                source.transform.position = endPos;
                doing = false;
                done = true;
            }
复制成功

结束。